Skip to content

Commit fb76103

Browse files
committed
feat: auto-install shell completions
Make shell arg optional in `completions` (auto-detects from $SHELL). Add `--install` flag that appends eval line to ~/.zshrc/~/.bashrc or writes fish completions file. Idempotent — skips if already configured. Install script now runs `completions --install` automatically.
1 parent dcff7c8 commit fb76103

6 files changed

Lines changed: 144 additions & 22 deletions

File tree

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ switchboard import ./backup/*.phd --drive another-drive
220220
| `switchboard interactive` | Launch interactive REPL mode |
221221
| `switchboard update` | Self-update to the latest release (shows changelog) |
222222
| `switchboard update --check` | Check for updates without installing |
223-
| `switchboard completions <shell>` | Generate shell completions (bash/zsh/fish) |
223+
| `switchboard completions [shell]` | Generate shell completions (auto-detects shell) |
224+
| `switchboard completions --install` | Auto-install completions into your shell config |
224225
| `switchboard guide <topic>` | Built-in documentation |
225226

226227
## Global Flags
@@ -361,17 +362,18 @@ Features:
361362

362363
## Shell Completions
363364

364-
Generate completions for your shell and add them to your shell config:
365+
The easiest way to set up tab-completion is:
365366

366367
```bash
367-
# Bash
368-
switchboard completions bash >> ~/.bashrc
368+
switchboard completions --install
369+
```
370+
371+
This auto-detects your shell and adds completions to your config file (`~/.zshrc`, `~/.bashrc`, or fish completions dir). The installer also runs this automatically.
369372

370-
# Zsh
371-
switchboard completions zsh >> ~/.zshrc
373+
For manual setup, you can output raw completions:
372374

373-
# Fish
374-
switchboard completions fish >> ~/.config/fish/completions/switchboard.fish
375+
```bash
376+
switchboard completions bash # or zsh, fish
375377
```
376378

377379
## Built-in Documentation

install.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ install() {
131131
;;
132132
esac
133133

134+
# --- setup shell completions -----------------------------------------------
135+
136+
info "Setting up shell completions"
137+
"${INSTALL_DIR}/${BINARY_NAME}" completions --install 2>/dev/null || true
138+
134139
# --- welcome message -------------------------------------------------------
135140

136141
echo ""

src/cli/completions.rs

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,130 @@
1-
use anyhow::Result;
2-
use clap::CommandFactory;
1+
use anyhow::{Context, Result, bail};
2+
use clap::{Args, CommandFactory};
33
use clap_complete::{Shell, generate};
4+
use colored::Colorize;
45
use std::io;
6+
use std::path::PathBuf;
57

68
use crate::cli::Cli;
79

8-
pub fn run(shell: Shell) -> Result<()> {
9-
let mut cmd = Cli::command();
10-
let name = cmd.get_name().to_string();
11-
generate(shell, &mut cmd, name, &mut io::stdout());
10+
#[derive(Args)]
11+
pub struct CompletionsArgs {
12+
/// Shell to generate completions for (auto-detected from $SHELL if omitted)
13+
pub shell: Option<Shell>,
14+
15+
/// Install completions into your shell config file
16+
#[arg(long)]
17+
pub install: bool,
18+
}
19+
20+
// ── Shell detection ────────────────────────────────────────────────────────
21+
22+
fn detect_shell() -> Result<Shell> {
23+
let shell_env = std::env::var("SHELL").unwrap_or_default();
24+
if shell_env.contains("zsh") {
25+
Ok(Shell::Zsh)
26+
} else if shell_env.contains("bash") {
27+
Ok(Shell::Bash)
28+
} else if shell_env.contains("fish") {
29+
Ok(Shell::Fish)
30+
} else {
31+
bail!(
32+
"Could not detect your shell from $SHELL ({shell_env}).\n\
33+
Specify it explicitly: switchboard completions bash|zsh|fish"
34+
)
35+
}
36+
}
37+
38+
fn rc_file(shell: Shell) -> Result<PathBuf> {
39+
let home = dirs::home_dir().context("Could not determine home directory")?;
40+
match shell {
41+
Shell::Bash => Ok(home.join(".bashrc")),
42+
Shell::Zsh => Ok(home.join(".zshrc")),
43+
Shell::Fish => Ok(home
44+
.join(".config/fish/completions")
45+
.join("switchboard.fish")),
46+
_ => bail!("Auto-install is not supported for {shell:?}. Add completions manually."),
47+
}
48+
}
49+
50+
fn eval_line(shell: Shell) -> &'static str {
51+
match shell {
52+
Shell::Bash => r#"eval "$(switchboard completions bash)""#,
53+
Shell::Zsh => r#"eval "$(switchboard completions zsh)""#,
54+
_ => unreachable!(),
55+
}
56+
}
57+
58+
// ── Entry point ────────────────────────────────────────────────────────────
59+
60+
pub fn run(args: CompletionsArgs) -> Result<()> {
61+
let shell = match args.shell {
62+
Some(s) => s,
63+
None => detect_shell()?,
64+
};
65+
66+
if args.install {
67+
install_completions(shell)
68+
} else {
69+
let mut cmd = Cli::command();
70+
let name = cmd.get_name().to_string();
71+
generate(shell, &mut cmd, name, &mut io::stdout());
72+
Ok(())
73+
}
74+
}
75+
76+
// ── Installer ──────────────────────────────────────────────────────────────
77+
78+
fn install_completions(shell: Shell) -> Result<()> {
79+
let target = rc_file(shell)?;
80+
81+
match shell {
82+
Shell::Fish => {
83+
// Fish: write completions directly to the completions directory
84+
if let Some(parent) = target.parent() {
85+
std::fs::create_dir_all(parent)?;
86+
}
87+
let mut buf = Vec::new();
88+
let mut cmd = Cli::command();
89+
let name = cmd.get_name().to_string();
90+
generate(shell, &mut cmd, name, &mut buf);
91+
std::fs::write(&target, &buf)?;
92+
println!("{} Wrote completions to {}", "✓".green(), target.display());
93+
}
94+
Shell::Bash | Shell::Zsh => {
95+
let line = eval_line(shell);
96+
97+
// Check if already installed
98+
if target.exists() {
99+
let contents = std::fs::read_to_string(&target)?;
100+
if contents.contains("switchboard completions") {
101+
println!(
102+
"{} Completions already configured in {}",
103+
"✓".green(),
104+
target.display()
105+
);
106+
return Ok(());
107+
}
108+
}
109+
110+
// Append eval line to rc file
111+
use std::io::Write;
112+
let mut file = std::fs::OpenOptions::new()
113+
.create(true)
114+
.append(true)
115+
.open(&target)?;
116+
writeln!(file)?;
117+
writeln!(file, "# Switchboard CLI completions")?;
118+
writeln!(file, "{line}")?;
119+
120+
println!("{} Added completions to {}", "✓".green(), target.display());
121+
println!(
122+
" Restart your shell or run: {}",
123+
format!("source {}", target.display()).dimmed()
124+
);
125+
}
126+
_ => bail!("Auto-install is not supported for {shell:?}. Add completions manually."),
127+
}
128+
12129
Ok(())
13130
}

src/cli/guide.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,8 @@ TOOLS
659659
interactive (or -i) Launch REPL mode
660660
update [--check] Self-update to latest release
661661
schema Dump GraphQL schema
662-
completions <shell> Generate shell completions
662+
completions [shell] Generate shell completions
663+
completions --install Auto-install completions into shell config
663664
guide <topic> Built-in documentation
664665
665666
GLOBAL FLAGS

src/cli/interactive.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ impl ReplHelper {
9292
"introspect".into(),
9393
"update".into(),
9494
"update --check".into(),
95+
"completions --install".into(),
9596
"guide ".into(),
9697
// REPL-only
9798
"help".into(),

src/cli/mod.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ use colored::Colorize;
2626

2727
use crate::output::OutputFormat;
2828
use clap::{Parser, Subcommand};
29-
use clap_complete::Shell;
3029

3130
#[derive(Parser)]
3231
#[command(name = "switchboard", about = "CLI for Switchboard GraphQL instances")]
@@ -142,11 +141,8 @@ pub enum Commands {
142141
#[command(subcommand)]
143142
Guide(guide::GuideCommand),
144143

145-
/// Generate shell completions
146-
Completions {
147-
/// Shell to generate completions for
148-
shell: Shell,
149-
},
144+
/// Generate shell completions (auto-detects shell, or specify explicitly)
145+
Completions(completions::CompletionsArgs),
150146
}
151147

152148
/// Central dispatcher shared by both the CLI entry point and the interactive REPL.
@@ -181,7 +177,7 @@ pub async fn dispatch(
181177
Commands::Update(args) => update::run(args.check, quiet).await,
182178
Commands::Interactive => anyhow::bail!("Already in interactive mode"),
183179
Commands::Guide(topic) => guide::run(topic),
184-
Commands::Completions { shell } => completions::run(shell),
180+
Commands::Completions(args) => completions::run(args),
185181
}
186182
}
187183

0 commit comments

Comments
 (0)