Skip to main content

cc_switch/cli/
completion.rs

1use crate::config::ConfigStorage;
2use anyhow::Result;
3use clap::CommandFactory;
4use std::fs;
5use std::io::Write;
6use std::path::PathBuf;
7
8/// Generate shell aliases for eval
9///
10/// # Arguments
11/// * `shell` - Shell type (fish, zsh, bash)
12///
13/// # Errors
14/// Returns error if shell is not supported
15pub fn generate_aliases(shell: &str) -> Result<()> {
16    match shell {
17        "fish" => {
18            println!("alias cs='cc-switch'");
19            println!("alias ccd='claude --dangerously-skip-permissions'");
20            println!("alias cx='cc-switch codex'");
21        }
22        "zsh" => {
23            println!("alias cs='cc-switch'");
24            println!("alias ccd='claude --dangerously-skip-permissions'");
25            println!("alias cx='cc-switch codex'");
26        }
27        "bash" => {
28            println!("alias cs='cc-switch'");
29            println!("alias ccd='claude --dangerously-skip-permissions'");
30            println!("alias cx='cc-switch codex'");
31        }
32        _ => {
33            anyhow::bail!(
34                "Unsupported shell: {}. Supported shells: fish, zsh, bash",
35                shell
36            );
37        }
38    }
39
40    Ok(())
41}
42
43/// Return the install path for a shell's completion file, if it has a standard location.
44fn completion_install_path(shell: &str) -> Option<PathBuf> {
45    let home = dirs::home_dir()?;
46    match shell {
47        "fish" => Some(home.join(".config/fish/completions/cc-switch.fish")),
48        "zsh" => Some(home.join(".zsh/completions/_cc-switch")),
49        "bash" => Some(home.join(".bash_completion.d/cc-switch")),
50        _ => None,
51    }
52}
53
54/// Generate shell completion script and install it to the standard path.
55///
56/// For fish/zsh/bash the output is written directly to the shell's
57/// completion directory. For other shells the script is printed to stdout.
58///
59/// # Errors
60/// Returns error if shell is not supported or generation fails
61pub fn generate_completion(shell: &str) -> Result<()> {
62    use crate::cli::Cli;
63
64    let mut app = Cli::command();
65    let mut buf: Vec<u8> = Vec::new();
66
67    match shell {
68        "fish" => {
69            generate_fish_completion(&mut app, &mut buf);
70        }
71        "zsh" => {
72            clap_complete::generate(clap_complete::shells::Zsh, &mut app, "cc-switch", &mut buf);
73        }
74        "bash" => {
75            clap_complete::generate(clap_complete::shells::Bash, &mut app, "cc-switch", &mut buf);
76        }
77        "elvish" => {
78            clap_complete::generate(
79                clap_complete::shells::Elvish,
80                &mut app,
81                "cc-switch",
82                &mut std::io::stdout(),
83            );
84            return Ok(());
85        }
86        "powershell" => {
87            clap_complete::generate(
88                clap_complete::shells::PowerShell,
89                &mut app,
90                "cc-switch",
91                &mut std::io::stdout(),
92            );
93            return Ok(());
94        }
95        _ => {
96            anyhow::bail!(
97                "Unsupported shell: {}. Supported shells: fish, zsh, bash, elvish, powershell",
98                shell
99            );
100        }
101    }
102
103    if let Some(path) = completion_install_path(shell) {
104        if let Some(parent) = path.parent() {
105            fs::create_dir_all(parent)?;
106        }
107        fs::write(&path, &buf)?;
108        eprintln!("Installed {shell} completion to {}", path.display());
109    } else {
110        std::io::stdout().write_all(&buf)?;
111    }
112
113    Ok(())
114}
115
116/// List available configuration aliases for shell completion
117///
118/// Outputs all stored configuration aliases, one per line
119/// Also includes 'cc' and 'official' as special aliases for resetting to default Claude
120/// For contexts where user types 'cc-switch use c' or similar, 'current' is prioritized first
121///
122/// # Errors
123/// Returns error if loading configurations fails
124pub fn list_aliases_for_completion() -> Result<()> {
125    let storage = ConfigStorage::load()?;
126
127    // Always include 'cc' and 'official' for reset functionality
128    println!("cc");
129    println!("official");
130
131    // Prioritize 'current' first if it exists - this ensures when user types 'cc-switch use c'
132    // or 'cs use c', the 'current' configuration appears first in completion
133    if storage.configurations.contains_key("current") {
134        println!("current");
135    }
136
137    // Output all other stored aliases in alphabetical order
138    let mut aliases: Vec<String> = storage.configurations.keys().cloned().collect();
139    aliases.sort();
140
141    for alias_name in aliases {
142        if alias_name != "current" {
143            println!("{alias_name}");
144        }
145    }
146
147    Ok(())
148}
149
150/// List available Codex configuration aliases for shell completion
151///
152/// Outputs all stored Codex configuration aliases, one per line
153///
154/// # Errors
155/// Returns error if loading configurations fails
156pub fn list_codex_aliases_for_completion() -> Result<()> {
157    let storage = ConfigStorage::load()?;
158
159    // Output all stored Codex aliases in alphabetical order
160    if let Some(ref configs) = storage.codex_configurations {
161        let mut aliases: Vec<String> = configs.keys().cloned().collect();
162        aliases.sort();
163
164        for alias_name in aliases {
165            println!("{alias_name}");
166        }
167    }
168
169    Ok(())
170}
171
172/// Generate custom fish completion with dynamic alias completion, writing to `out`.
173fn generate_fish_completion(app: &mut clap::Command, out: &mut Vec<u8>) {
174    clap_complete::generate(clap_complete::shells::Fish, app, "cc-switch", out);
175
176    let extra = r#"
177# Custom completion for use subcommand with dynamic aliases
178complete -c cc-switch -n '__fish_cc_switch_using_subcommand use' -f -a '(cc-switch --list-aliases)' -d 'Configuration alias name'
179# Custom completion for switch subcommand (alias for use)
180complete -c cc-switch -n '__fish_cc_switch_using_subcommand switch' -f -a '(cc-switch --list-aliases)' -d 'Configuration alias name'
181# Custom completion for remove subcommand with dynamic aliases
182complete -c cc-switch -n '__fish_cc_switch_using_subcommand remove' -f -a '(cc-switch --list-aliases)' -d 'Configuration alias name'
183
184# Completion for 'completion' subcommand with shell types
185complete -c cc-switch -n '__fish_cc_switch_using_subcommand completion' -f -a 'fish zsh bash elvish powershell' -d 'Shell type'
186complete -c cs -n '__fish_seen_subcommand_from completion' -f -a 'fish zsh bash elvish powershell' -d 'Shell type'
187
188# Custom completion for codex subcommand with dynamic aliases
189complete -c cc-switch -n '__fish_seen_subcommand_from codex' -n '__fish_seen_subcommand_from use' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
190complete -c cc-switch -n '__fish_seen_subcommand_from codex' -n '__fish_seen_subcommand_from remove' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
191
192# Completion for the 'cs' alias
193complete -c cs -w cc-switch
194
195# Completion for 'cs' alias subcommands
196complete -c cs -n '__fish_use_subcommand' -f -a 'add remove list set-default-dir completion alias use switch current codex daemon statusline' -d 'Subcommand'
197
198# Completion for 'daemon' subcommand
199complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and not __fish_seen_subcommand_from start stop status restart' -f -a 'start stop status restart' -d 'Daemon action'
200complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from start' -l foreground -d 'Run in the foreground'
201complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from start' -l log-level -d 'Log level (error/warn/info/debug/trace)' -r -f -a 'error warn info debug trace'
202complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from start' -s v -l verbose -d 'Increase verbosity (-v/-vv/-vvv)'
203complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from restart' -l foreground -d 'Run in the foreground after restart'
204complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from restart' -l log-level -d 'Log level (error/warn/info/debug/trace)' -r -f -a 'error warn info debug trace'
205complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from restart' -s v -l verbose -d 'Increase verbosity (-v/-vv/-vvv)'
206complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from status' -l json -d 'Output as JSON'
207
208# Completion for 'cs list' subcommand
209complete -c cs -n '__fish_seen_subcommand_from list' -l plain -s p -d 'Plain text output'
210complete -c cs -n '__fish_seen_subcommand_from list' -l name -s n -d 'Show only name and URL'
211
212# Completion for 'cs daemon' subcommand
213complete -c cs -n '__fish_seen_subcommand_from daemon; and not __fish_seen_subcommand_from start stop status restart' -f -a 'start stop status restart' -d 'Daemon action'
214complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start' -l foreground -d 'Run in the foreground'
215complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start' -l log-level -d 'Log level' -r -f -a 'error warn info debug trace'
216complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start' -s v -l verbose -d 'Increase verbosity'
217complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from restart' -l foreground -d 'Run in the foreground after restart'
218complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from restart' -l log-level -d 'Log level' -r -f -a 'error warn info debug trace'
219complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from restart' -s v -l verbose -d 'Increase verbosity'
220complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from status' -l json -d 'Output as JSON'
221
222# Completion for 'cs statusline' subcommand
223complete -c cs -n '__fish_seen_subcommand_from statusline' -f -a 'install uninstall' -d 'Statusline action'
224
225# Completion for the 'cx' alias (cc-switch codex)
226complete -c cx -f
227complete -c cx -n '__fish_use_subcommand' -f -a 'add use remove list' -d 'Codex subcommand'
228complete -c cx -n '__fish_seen_subcommand_from use' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
229complete -c cx -n '__fish_seen_subcommand_from remove' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
230complete -c cx -n '__fish_seen_subcommand_from list' -f -l plain -s p -d 'Plain text output'
231complete -c cx -n '__fish_seen_subcommand_from list' -f -l name -s n -d 'Show only name and auth mode'
232complete -c cx -n '__fish_seen_subcommand_from add' -f -l interactive -s i -d 'Interactive mode'
233complete -c cx -n '__fish_seen_subcommand_from add' -f -l from-file -d 'Import from auth.json (defaults to ~/.codex/auth.json if no path)' -r
234"#;
235    out.extend_from_slice(extra.as_bytes());
236
237    generate_cs_completion_file();
238    generate_cx_completion_file();
239}
240
241/// Generate separate completion file for cs fish alias.
242///
243/// Fish only auto-loads completions from files named after the command,
244/// so `cs` needs its own `cs.fish`.
245fn generate_cs_completion_file() {
246    let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("~"));
247    let completions_dir = home.join(".config").join("fish").join("completions");
248
249    if !completions_dir.exists()
250        && let Err(e) = fs::create_dir_all(&completions_dir)
251    {
252        eprintln!("Warning: Could not create completions directory: {e}");
253        return;
254    }
255
256    let cs_content = r#"# Completion for 'cs' alias (cc-switch)
257complete -c cs -w cc-switch
258"#;
259
260    let cs_path = completions_dir.join("cs.fish");
261
262    if let Err(e) = fs::write(&cs_path, cs_content) {
263        eprintln!("Warning: Could not write cs.fish: {e}");
264    }
265
266    eprintln!("Created completion file: {}", cs_path.display());
267}
268
269/// Generate separate completion file for cx fish function
270///
271/// Fish doesn't automatically load completion files for functions, only for commands.
272/// This creates ~/.config/fish/completions/cx.fish
273fn generate_cx_completion_file() {
274    // Fish uses ~/.config/fish/completions on all platforms (including macOS)
275    let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("~"));
276    let completions_dir = home.join(".config").join("fish").join("completions");
277
278    if !completions_dir.exists()
279        && let Err(e) = fs::create_dir_all(&completions_dir)
280    {
281        eprintln!("Warning: Could not create completions directory: {e}");
282        return;
283    }
284
285    let cx_content = r#"# Completion for 'cx' alias (cc-switch codex)
286# cx is a fish function; disable file completion by default
287complete -c cx -f
288complete -c cx -n '__fish_use_subcommand' -f -a 'add use remove list' -d 'Codex subcommand'
289complete -c cx -n '__fish_seen_subcommand_from use' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
290complete -c cx -n '__fish_seen_subcommand_from remove' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
291complete -c cx -n '__fish_seen_subcommand_from list' -f -l plain -s p -d 'Plain text output'
292complete -c cx -n '__fish_seen_subcommand_from list' -f -l name -s n -d 'Show only name and auth mode'
293complete -c cx -n '__fish_seen_subcommand_from add' -f -l interactive -s i -d 'Interactive mode'
294complete -c cx -n '__fish_seen_subcommand_from add' -f -l from-file -d 'Import from auth.json (defaults to ~/.codex/auth.json if no path)' -r
295"#;
296
297    let cx_path = completions_dir.join("cx.fish");
298
299    if let Err(e) = fs::write(&cx_path, cx_content) {
300        eprintln!("Warning: Could not write cx.fish: {e}");
301    }
302
303    eprintln!("\nCreated completion file: {}", cx_path.display());
304}