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(
73                clap_complete::shells::Zsh,
74                &mut app,
75                "cc-switch",
76                &mut buf,
77            );
78        }
79        "bash" => {
80            clap_complete::generate(
81                clap_complete::shells::Bash,
82                &mut app,
83                "cc-switch",
84                &mut buf,
85            );
86        }
87        "elvish" => {
88            clap_complete::generate(
89                clap_complete::shells::Elvish,
90                &mut app,
91                "cc-switch",
92                &mut std::io::stdout(),
93            );
94            return Ok(());
95        }
96        "powershell" => {
97            clap_complete::generate(
98                clap_complete::shells::PowerShell,
99                &mut app,
100                "cc-switch",
101                &mut std::io::stdout(),
102            );
103            return Ok(());
104        }
105        _ => {
106            anyhow::bail!(
107                "Unsupported shell: {}. Supported shells: fish, zsh, bash, elvish, powershell",
108                shell
109            );
110        }
111    }
112
113    if let Some(path) = completion_install_path(shell) {
114        if let Some(parent) = path.parent() {
115            fs::create_dir_all(parent)?;
116        }
117        fs::write(&path, &buf)?;
118        eprintln!("Installed {shell} completion to {}", path.display());
119    } else {
120        std::io::stdout().write_all(&buf)?;
121    }
122
123    Ok(())
124}
125
126/// List available configuration aliases for shell completion
127///
128/// Outputs all stored configuration aliases, one per line
129/// Also includes 'cc' and 'official' as special aliases for resetting to default Claude
130/// For contexts where user types 'cc-switch use c' or similar, 'current' is prioritized first
131///
132/// # Errors
133/// Returns error if loading configurations fails
134pub fn list_aliases_for_completion() -> Result<()> {
135    let storage = ConfigStorage::load()?;
136
137    // Always include 'cc' and 'official' for reset functionality
138    println!("cc");
139    println!("official");
140
141    // Prioritize 'current' first if it exists - this ensures when user types 'cc-switch use c'
142    // or 'cs use c', the 'current' configuration appears first in completion
143    if storage.configurations.contains_key("current") {
144        println!("current");
145    }
146
147    // Output all other stored aliases in alphabetical order
148    let mut aliases: Vec<String> = storage.configurations.keys().cloned().collect();
149    aliases.sort();
150
151    for alias_name in aliases {
152        if alias_name != "current" {
153            println!("{alias_name}");
154        }
155    }
156
157    Ok(())
158}
159
160/// List available Codex configuration aliases for shell completion
161///
162/// Outputs all stored Codex configuration aliases, one per line
163///
164/// # Errors
165/// Returns error if loading configurations fails
166pub fn list_codex_aliases_for_completion() -> Result<()> {
167    let storage = ConfigStorage::load()?;
168
169    // Output all stored Codex aliases in alphabetical order
170    if let Some(ref configs) = storage.codex_configurations {
171        let mut aliases: Vec<String> = configs.keys().cloned().collect();
172        aliases.sort();
173
174        for alias_name in aliases {
175            println!("{alias_name}");
176        }
177    }
178
179    Ok(())
180}
181
182/// Generate custom fish completion with dynamic alias completion, writing to `out`.
183fn generate_fish_completion(app: &mut clap::Command, out: &mut Vec<u8>) {
184    clap_complete::generate(clap_complete::shells::Fish, app, "cc-switch", out);
185
186    let extra = r#"
187# Custom completion for use subcommand with dynamic aliases
188complete -c cc-switch -n '__fish_cc_switch_using_subcommand use' -f -a '(cc-switch --list-aliases)' -d 'Configuration alias name'
189# Custom completion for switch subcommand (alias for use)
190complete -c cc-switch -n '__fish_cc_switch_using_subcommand switch' -f -a '(cc-switch --list-aliases)' -d 'Configuration alias name'
191# Custom completion for remove subcommand with dynamic aliases
192complete -c cc-switch -n '__fish_cc_switch_using_subcommand remove' -f -a '(cc-switch --list-aliases)' -d 'Configuration alias name'
193
194# Custom completion for codex subcommand with dynamic aliases
195complete -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'
196complete -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'
197
198# Completion for the 'cs' alias
199complete -c cs -w cc-switch
200
201# Completion for 'cs' alias subcommands
202complete -c cs -n '__fish_use_subcommand' -f -a 'add remove list set-default-dir completion alias use switch current codex daemon statusline' -d 'Subcommand'
203
204# Completion for 'daemon' subcommand
205complete -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'
206complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from start' -l foreground -d 'Run in the foreground'
207complete -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'
208complete -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)'
209complete -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'
210complete -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'
211complete -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)'
212complete -c cc-switch -n '__fish_cc_switch_using_subcommand daemon; and __fish_seen_subcommand_from status' -l json -d 'Output as JSON'
213
214# Completion for 'cs list' subcommand
215complete -c cs -n '__fish_seen_subcommand_from list' -l plain -s p -d 'Plain text output'
216complete -c cs -n '__fish_seen_subcommand_from list' -l name -s n -d 'Show only name and URL'
217
218# Completion for 'cs daemon' subcommand
219complete -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'
220complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start' -l foreground -d 'Run in the foreground'
221complete -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'
222complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from start' -s v -l verbose -d 'Increase verbosity'
223complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from restart' -l foreground -d 'Run in the foreground after restart'
224complete -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'
225complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from restart' -s v -l verbose -d 'Increase verbosity'
226complete -c cs -n '__fish_seen_subcommand_from daemon; and __fish_seen_subcommand_from status' -l json -d 'Output as JSON'
227
228# Completion for 'cs statusline' subcommand
229complete -c cs -n '__fish_seen_subcommand_from statusline' -f -a 'install uninstall' -d 'Statusline action'
230
231# Completion for the 'cx' alias (cc-switch codex)
232complete -c cx -f
233complete -c cx -n '__fish_use_subcommand' -f -a 'add use remove list' -d 'Codex subcommand'
234complete -c cx -n '__fish_seen_subcommand_from use' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
235complete -c cx -n '__fish_seen_subcommand_from remove' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
236complete -c cx -n '__fish_seen_subcommand_from list' -f -l plain -s p -d 'Plain text output'
237complete -c cx -n '__fish_seen_subcommand_from list' -f -l name -s n -d 'Show only name and auth mode'
238complete -c cx -n '__fish_seen_subcommand_from add' -f -l interactive -s i -d 'Interactive mode'
239complete -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
240"#;
241    out.extend_from_slice(extra.as_bytes());
242
243    generate_cx_completion_file();
244}
245
246/// Generate separate completion file for cx fish function
247///
248/// Fish doesn't automatically load completion files for functions, only for commands.
249/// This creates ~/.config/fish/completions/cx.fish
250fn generate_cx_completion_file() {
251    // Fish uses ~/.config/fish/completions on all platforms (including macOS)
252    let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("~"));
253    let completions_dir = home.join(".config").join("fish").join("completions");
254
255    if !completions_dir.exists()
256        && let Err(e) = fs::create_dir_all(&completions_dir)
257    {
258        eprintln!("Warning: Could not create completions directory: {e}");
259        return;
260    }
261
262    let cx_content = r#"# Completion for 'cx' alias (cc-switch codex)
263# cx is a fish function; disable file completion by default
264complete -c cx -f
265complete -c cx -n '__fish_use_subcommand' -f -a 'add use remove list' -d 'Codex subcommand'
266complete -c cx -n '__fish_seen_subcommand_from use' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
267complete -c cx -n '__fish_seen_subcommand_from remove' -f -a '(cc-switch --list-codex-aliases)' -d 'Codex configuration alias name'
268complete -c cx -n '__fish_seen_subcommand_from list' -f -l plain -s p -d 'Plain text output'
269complete -c cx -n '__fish_seen_subcommand_from list' -f -l name -s n -d 'Show only name and auth mode'
270complete -c cx -n '__fish_seen_subcommand_from add' -f -l interactive -s i -d 'Interactive mode'
271complete -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
272"#;
273
274    let cx_path = completions_dir.join("cx.fish");
275
276    if let Err(e) = fs::write(&cx_path, cx_content) {
277        eprintln!("Warning: Could not write cx.fish: {e}");
278    }
279
280    eprintln!("\nCreated completion file: {}", cx_path.display());
281}