Skip to content
Merged
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"
41 changes: 34 additions & 7 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,8 +53,11 @@ fn main() {

let cli = Cli::parse();

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

init_logger(cli.effective_log_level());

// Merge config file defaults with CLI arguments
Expand All @@ -66,8 +69,15 @@ fn main() {
}
};

// Handle init command early - doesn't require Ruby environment
if let Commands::Init = cli.command {
let Some(command) = cli.command else {
use clap::CommandFactory;
let mut cmd = Cli::command();
let _ = cmd.print_help();
println!();
std::process::exit(0);
};

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 +86,23 @@ fn main() {
return;
}

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 +151,7 @@ fn main() {
},
};

match cli.command {
match command {
Commands::Runtime => {
runtime_command(&butler_runtime);
}
Expand All @@ -147,5 +172,7 @@ fn main() {
// Already handled above
unreachable!()
}
Commands::ShellIntegration { .. } => unreachable!(),
Commands::BashComplete { .. } => 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;
95 changes: 95 additions & 0 deletions crates/rb-cli/src/commands/shell_integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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() {
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>> {
match shell {
Shell::Bash => {
generate_bash_shim();
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 __bash_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