Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/rb-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ path = "src/bin/rb.rs"

[dependencies]
clap = { version = "4.0", features = ["derive", "color", "help", "usage"] }
clap_complete = "4.0"
rb-core = { path = "../rb-core" }
home = "0.5"
colored = "2.0"
Expand All @@ -33,3 +34,4 @@ serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
rb-tests = { path = "../rb-tests" }
tempfile = "3.0"
49 changes: 45 additions & 4 deletions crates/rb-cli/src/bin/rb.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use clap::Parser;
use rb_cli::{
Cli, Commands, environment_command, exec_command, init_command, init_logger,
resolve_search_dir, run_command, runtime_command, sync_command,
resolve_search_dir, run_command, runtime_command, shell_integration_command, sync_command,
};
use rb_core::butler::{ButlerError, ButlerRuntime};

Expand Down Expand Up @@ -53,6 +53,18 @@ fn main() {

let cli = Cli::parse();

// Handle completion generation (hidden flag)
if let Some(complete_args) = &cli.complete {
if complete_args.len() >= 2 {
rb_cli::completion::generate_completions(
&complete_args[0],
&complete_args[1],
cli.config.rubies_dir.clone(),
);
return;
}
}

// Initialize logger early with the effective log level (considering -v/-vv flags)
// This allows us to see config file loading and merging logs
init_logger(cli.effective_log_level());
Expand All @@ -66,8 +78,17 @@ fn main() {
}
};

// Ensure we have a command - if not, show help
let Some(command) = cli.command else {
use clap::CommandFactory;
let mut cmd = Cli::command();
let _ = cmd.print_help();
println!(); // Add newline after help
std::process::exit(0);
};

// Handle init command early - doesn't require Ruby environment
if let Commands::Init = cli.command {
if let Commands::Init = command {
let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
if let Err(e) = init_command(&current_dir) {
eprintln!("{}", e);
Expand All @@ -76,8 +97,24 @@ fn main() {
return;
}

// Handle shell-integration command early - doesn't require Ruby environment
if let Commands::ShellIntegration { shell } = command {
match shell {
Some(s) => {
if let Err(e) = shell_integration_command(s) {
eprintln!("Shell integration error: {}", e);
std::process::exit(1);
}
}
None => {
rb_cli::commands::shell_integration::show_available_integrations();
}
}
return;
}

// Handle sync command differently since it doesn't use ButlerRuntime in the same way
if let Commands::Sync = cli.command {
if let Commands::Sync = command {
if let Err(e) = sync_command(
cli.config.rubies_dir.clone(),
cli.config.ruby_version.clone(),
Expand Down Expand Up @@ -126,7 +163,7 @@ fn main() {
},
};

match cli.command {
match command {
Commands::Runtime => {
runtime_command(&butler_runtime);
}
Expand All @@ -147,5 +184,9 @@ fn main() {
// Already handled above
unreachable!()
}
Commands::ShellIntegration { .. } => {
// Already handled above
unreachable!()
}
}
}
2 changes: 2 additions & 0 deletions crates/rb-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ pub mod exec;
pub mod init;
pub mod run;
pub mod runtime;
pub mod shell_integration;
pub mod sync;

pub use environment::environment_command;
pub use exec::exec_command;
pub use init::init_command;
pub use run::run_command;
pub use runtime::runtime_command;
pub use shell_integration::shell_integration_command;
pub use sync::sync_command;
98 changes: 98 additions & 0 deletions crates/rb-cli/src/commands/shell_integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use crate::Shell;
use colored::Colorize;
use std::io::IsTerminal;

/// Metadata about a shell integration
pub struct ShellIntegration {
pub name: &'static str,
pub shell_name: &'static str,
pub shell: Shell,
pub description: &'static str,
pub install_instruction: &'static str,
}

/// All available shell integrations
pub fn available_integrations() -> Vec<ShellIntegration> {
vec![ShellIntegration {
name: "Bash Completion",
shell_name: "bash",
shell: Shell::Bash,
description: "Dynamic command completion for Bash shell",
install_instruction: "Add to ~/.bashrc: eval \"$(rb shell-integration bash)\"",
}]
}

/// Show all available shell integrations with installation instructions
pub fn show_available_integrations() {
println!("{}\n", "🎩 Available Shell Integrations".bold());
println!("{}", "Shells:".bold());

for integration in available_integrations() {
// Format similar to clap's command list: " command Description text"
println!(
" {:<12} {}",
integration.shell_name.green(),
integration.description
);
}

println!("\n{}", "Installation:".bold());
for integration in available_integrations() {
println!(
" {:<12} {}",
integration.shell_name.green(),
integration.install_instruction
);
}
}

pub fn shell_integration_command(shell: Shell) -> Result<(), Box<dyn std::error::Error>> {
// Generate dynamic completion shim that calls back to rb
match shell {
Shell::Bash => {
generate_bash_shim();
// Only show instructions if output is going to a terminal (not being eval'd)
if std::io::stdout().is_terminal() {
print_bash_instructions();
}
}
}

Ok(())
}

fn generate_bash_shim() {
print!(
r#"# Ruby Butler dynamic completion shim
_rb_completion() {{
local cur prev words cword
_init_completion || return

# Call rb to get context-aware completions
local completions
completions=$(rb --complete "${{COMP_LINE}}" "${{COMP_POINT}}" 2>/dev/null)

if [ -n "$completions" ]; then
COMPREPLY=($(compgen -W "$completions" -- "$cur"))
# Bash will automatically add space for single completion
else
# No rb completions, fall back to default bash completion (files/dirs)
compopt -o default
COMPREPLY=()
fi
}}

complete -F _rb_completion rb
"#
);
}

fn print_bash_instructions() {
eprintln!("\n# 🎩 Ruby Butler Shell Integration");
eprintln!("#");
eprintln!("# To enable completions, add to your ~/.bashrc:");
eprintln!("# eval \"$(rb shell-integration bash)\"");
eprintln!("#");
eprintln!("# This generates completions on-the-fly, ensuring they stay current");
eprintln!("# with your installed version. The generation is instantaneous.");
}
Loading
Loading