//! completion: generate shell completion scripts (bash, zsh, fish, //! powershell, elvish) via clap_complete. With --installed, a dynamic //! layer is appended: Tab then queries 'am _agents --installed' at runtime, //! so only installed agents, aliases and groups are proposed (issue #41). use crate::cli::Cli; use anyhow::Result; use clap::CommandFactory; use clap_complete::{generate, Shell}; use std::io::Write; pub fn run(shell: Shell, installed: bool) -> Result { let mut cmd = Cli::command(); let mut buf: Vec = Vec::new(); generate(shell, &mut cmd, "am", &mut buf); if installed { let mut text = String::from_utf8_lossy(&buf).to_string(); match shell { Shell::PowerShell => { // Re-bind the base completer to a variable so the wrapper // can delegate to it. let marker = "Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock {"; if let Some(pos) = text.find(marker) { text.replace_range(pos..pos + marker.len(), "$amBase = {"); text.push('\n'); text.push_str(powershell_snippet()); } } _ => { text.push('\n'); text.push_str(snippet(shell)); } } std::io::stdout().write_all(text.as_bytes())?; } else { std::io::stdout().write_all(&buf)?; } Ok(0) } pub fn snippet(shell: Shell) -> &'static str { match shell { Shell::Bash => bash_snippet(), Shell::Zsh => zsh_snippet(), Shell::Fish => fish_snippet(), Shell::Elvish => elvish_snippet(), Shell::PowerShell => powershell_snippet(), _ => "", } } pub fn bash_snippet() -> &'static str { r#"# --- dynamic completion of installed agents, aliases and groups (--installed) --- _am_installed() { local cur cmd cur="${COMP_WORDS[COMP_CWORD]}" cmd="${COMP_WORDS[1]}" case "${cmd}" in start|stop|restart|run|info|logs|open|install|uninstall|update|watch|favorite|unfavorite|note|tag|untag|status|sessions|stats|log) if [[ ${COMP_CWORD} -eq 2 ]]; then COMPREPLY=( $(compgen -W "$(am _agents --installed 2>/dev/null)" -- "${cur}") ) return 0 fi ;; esac _am "$@" } complete -F _am_installed -o nosort -o bashdefault -o default am "# } pub fn zsh_snippet() -> &'static str { r#"# --- dynamic completion of installed agents, aliases and groups (--installed) --- _am_installed() { local -a agents if [[ ${CURRENT} -eq 3 ]] && [[ ${words[2]} =~ ^(start|stop|restart|run|info|logs|open|install|uninstall|update|watch|favorite|unfavorite|note|tag|untag|status|sessions|stats|log)$ ]]; then agents=("${(@f)$(am _agents --installed 2>/dev/null)}") _describe -t agents 'agent' agents return fi _am "$@" } compdef _am_installed am "# } pub fn fish_snippet() -> &'static str { r#"# --- dynamic completion of installed agents, aliases and groups (--installed) --- complete -c am -n "__fish_seen_subcommand_from start stop restart run info logs open install uninstall update watch favorite unfavorite note tag untag status sessions stats log" -f -a "(am _agents --installed 2>/dev/null)" "# } pub fn elvish_snippet() -> &'static str { r#"# --- dynamic completion of installed agents, aliases and groups (--installed) --- var am-base = $edit:completion:arg-completer[am] set edit:completion:arg-completer[am] = {|@words| if (and (>= (count $words) 2) (<= (count $words) 3) (has-value [start stop restart run info logs open install uninstall update watch favorite unfavorite note tag untag status sessions stats log] $words[1])) { put $@(am _agents --installed) } else { $am-base $@words } } "# } pub fn powershell_snippet() -> &'static str { r#" # --- dynamic completion of installed agents, aliases and groups (--installed) --- Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $tokens = $commandAst.CommandElements | ForEach-Object { $_.Extent.Text } $agentCommands = @('start','stop','restart','run','info','logs','open','install','uninstall','update','watch','favorite','unfavorite','note','tag','untag','status','sessions','stats','log') if ($tokens.Count -ge 2 -and $agentCommands -contains $tokens[1]) { (am _agents --installed 2>$null) | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } return } & $amBase $wordToComplete $commandAst $cursorPosition } "# } #[cfg(test)] mod tests { use super::*; #[test] fn dynamic_snippets_are_produced_for_every_shell() { for shell in [ Shell::Bash, Shell::Zsh, Shell::Fish, Shell::Elvish, Shell::PowerShell, ] { let text = snippet(shell); assert!(text.contains("_agents --installed"), "{shell:?}"); assert!(text.contains("favorite"), "{shell:?}"); } } #[test] fn base_script_has_no_dynamic_layer() { let mut cmd = Cli::command(); let mut buf: Vec = Vec::new(); generate(Shell::Bash, &mut cmd, "am", &mut buf); let base = String::from_utf8(buf).unwrap(); assert!(!base.contains("_am_installed")); assert!(base.contains("complete -F _am ")); } }