Skip to content

Commit 5d788e8

Browse files
committed
Add basic bash shell completion.
1 parent d3fa335 commit 5d788e8

14 files changed

Lines changed: 2335 additions & 17 deletions

File tree

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rb-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ path = "src/bin/rb.rs"
2020

2121
[dependencies]
2222
clap = { version = "4.0", features = ["derive", "color", "help", "usage"] }
23+
clap_complete = "4.0"
2324
rb-core = { path = "../rb-core" }
2425
home = "0.5"
2526
colored = "2.0"
@@ -33,3 +34,4 @@ serde = { version = "1.0", features = ["derive"] }
3334

3435
[dev-dependencies]
3536
rb-tests = { path = "../rb-tests" }
37+
tempfile = "3.0"

crates/rb-cli/src/bin/rb.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use clap::Parser;
22
use rb_cli::{
33
Cli, Commands, environment_command, exec_command, init_command, init_logger,
4-
resolve_search_dir, run_command, runtime_command, sync_command,
4+
resolve_search_dir, run_command, runtime_command, shell_integration_command, sync_command,
55
};
66
use rb_core::butler::{ButlerError, ButlerRuntime};
77

@@ -53,8 +53,11 @@ fn main() {
5353

5454
let cli = Cli::parse();
5555

56-
// Initialize logger early with the effective log level (considering -v/-vv flags)
57-
// This allows us to see config file loading and merging logs
56+
if let Some(Commands::BashComplete { line, point }) = &cli.command {
57+
rb_cli::completion::generate_completions(line, point, cli.config.rubies_dir.clone());
58+
return;
59+
}
60+
5861
init_logger(cli.effective_log_level());
5962

6063
// Merge config file defaults with CLI arguments
@@ -66,8 +69,15 @@ fn main() {
6669
}
6770
};
6871

69-
// Handle init command early - doesn't require Ruby environment
70-
if let Commands::Init = cli.command {
72+
let Some(command) = cli.command else {
73+
use clap::CommandFactory;
74+
let mut cmd = Cli::command();
75+
let _ = cmd.print_help();
76+
println!();
77+
std::process::exit(0);
78+
};
79+
80+
if let Commands::Init = command {
7181
let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
7282
if let Err(e) = init_command(&current_dir) {
7383
eprintln!("{}", e);
@@ -76,8 +86,23 @@ fn main() {
7686
return;
7787
}
7888

89+
if let Commands::ShellIntegration { shell } = command {
90+
match shell {
91+
Some(s) => {
92+
if let Err(e) = shell_integration_command(s) {
93+
eprintln!("Shell integration error: {}", e);
94+
std::process::exit(1);
95+
}
96+
}
97+
None => {
98+
rb_cli::commands::shell_integration::show_available_integrations();
99+
}
100+
}
101+
return;
102+
}
103+
79104
// Handle sync command differently since it doesn't use ButlerRuntime in the same way
80-
if let Commands::Sync = cli.command {
105+
if let Commands::Sync = command {
81106
if let Err(e) = sync_command(
82107
cli.config.rubies_dir.clone(),
83108
cli.config.ruby_version.clone(),
@@ -126,7 +151,7 @@ fn main() {
126151
},
127152
};
128153

129-
match cli.command {
154+
match command {
130155
Commands::Runtime => {
131156
runtime_command(&butler_runtime);
132157
}
@@ -147,5 +172,7 @@ fn main() {
147172
// Already handled above
148173
unreachable!()
149174
}
175+
Commands::ShellIntegration { .. } => unreachable!(),
176+
Commands::BashComplete { .. } => unreachable!(),
150177
}
151178
}

crates/rb-cli/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ pub mod exec;
33
pub mod init;
44
pub mod run;
55
pub mod runtime;
6+
pub mod shell_integration;
67
pub mod sync;
78

89
pub use environment::environment_command;
910
pub use exec::exec_command;
1011
pub use init::init_command;
1112
pub use run::run_command;
1213
pub use runtime::runtime_command;
14+
pub use shell_integration::shell_integration_command;
1315
pub use sync::sync_command;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use crate::Shell;
2+
use colored::Colorize;
3+
use std::io::IsTerminal;
4+
5+
/// Metadata about a shell integration
6+
pub struct ShellIntegration {
7+
pub name: &'static str,
8+
pub shell_name: &'static str,
9+
pub shell: Shell,
10+
pub description: &'static str,
11+
pub install_instruction: &'static str,
12+
}
13+
14+
/// All available shell integrations
15+
pub fn available_integrations() -> Vec<ShellIntegration> {
16+
vec![ShellIntegration {
17+
name: "Bash Completion",
18+
shell_name: "bash",
19+
shell: Shell::Bash,
20+
description: "Dynamic command completion for Bash shell",
21+
install_instruction: "Add to ~/.bashrc: eval \"$(rb shell-integration bash)\"",
22+
}]
23+
}
24+
25+
/// Show all available shell integrations with installation instructions
26+
pub fn show_available_integrations() {
27+
println!("{}\n", "🎩 Available Shell Integrations".bold());
28+
println!("{}", "Shells:".bold());
29+
30+
for integration in available_integrations() {
31+
println!(
32+
" {:<12} {}",
33+
integration.shell_name.green(),
34+
integration.description
35+
);
36+
}
37+
38+
println!("\n{}", "Installation:".bold());
39+
for integration in available_integrations() {
40+
println!(
41+
" {:<12} {}",
42+
integration.shell_name.green(),
43+
integration.install_instruction
44+
);
45+
}
46+
}
47+
48+
pub fn shell_integration_command(shell: Shell) -> Result<(), Box<dyn std::error::Error>> {
49+
match shell {
50+
Shell::Bash => {
51+
generate_bash_shim();
52+
if std::io::stdout().is_terminal() {
53+
print_bash_instructions();
54+
}
55+
}
56+
}
57+
58+
Ok(())
59+
}
60+
61+
fn generate_bash_shim() {
62+
print!(
63+
r#"# Ruby Butler dynamic completion shim
64+
_rb_completion() {{
65+
local cur prev words cword
66+
_init_completion || return
67+
68+
# Call rb to get context-aware completions
69+
local completions
70+
completions=$(rb __bash_complete "${{COMP_LINE}}" "${{COMP_POINT}}" 2>/dev/null)
71+
72+
if [ -n "$completions" ]; then
73+
COMPREPLY=($(compgen -W "$completions" -- "$cur"))
74+
# Bash will automatically add space for single completion
75+
else
76+
# No rb completions, fall back to default bash completion (files/dirs)
77+
compopt -o default
78+
COMPREPLY=()
79+
fi
80+
}}
81+
82+
complete -F _rb_completion rb
83+
"#
84+
);
85+
}
86+
87+
fn print_bash_instructions() {
88+
eprintln!("\n# 🎩 Ruby Butler Shell Integration");
89+
eprintln!("#");
90+
eprintln!("# To enable completions, add to your ~/.bashrc:");
91+
eprintln!("# eval \"$(rb shell-integration bash)\"");
92+
eprintln!("#");
93+
eprintln!("# This generates completions on-the-fly, ensuring they stay current");
94+
eprintln!("# with your installed version. The generation is instantaneous.");
95+
}

0 commit comments

Comments
 (0)