use crate::{file_resolution_check, scoop_version_check};
pub fn init_script() -> &'static str {
concat!(
r#"# scoop shell integration for zsh
# Disable completion sorting for scoop (preserves command order)
zstyle ':completion:*:scoop:*' sort false
# Wrapper function for scoop
scoop() {
local command="${1:-}"
case "$command" in
use)
command scoop "$@"
local ret=$?
if [[ $ret -eq 0 ]]; then
shift # remove 'use'
local name=""
for arg in "$@"; do
case "$arg" in
-*) ;; # skip options
*) name="$arg"; break ;;
esac
done
if [[ -n "$name" ]]; then
eval "$(command scoop activate "$name")"
fi
fi
return $ret
;;
activate|deactivate|shell)
# Pass through help/version flags without eval
if [[ "$*" == *--help* ]] || [[ "$*" == *-h* ]] || [[ "$*" == *--version* ]] || [[ "$*" == *-V* ]]; then
command scoop "$@"
else
eval "$(command scoop "$@")"
fi
;;
*)
command scoop "$@"
;;
esac
}
# Auto-activate hook
_scoop_hook() {
"#,
scoop_version_check!(zsh),
file_resolution_check!(zsh),
r#"
}
# Set up chpwd hook for auto-activate
if [[ -z "$SCOOP_NO_AUTO" ]]; then
autoload -Uz add-zsh-hook
add-zsh-hook chpwd _scoop_hook
fi
# Run hook on startup
_scoop_hook
# Zsh completion for scoop
_scoop() {
local curcontext="$curcontext" state line
typeset -A opt_args
local cur="${words[$CURRENT]}"
_arguments -C \
'1: :->command' \
'*: :->args'
case $state in
command)
local commands=(
'list:List all virtual environments'
'use:Set local environment for current directory'
'create:Create a new virtual environment'
'remove:Remove a virtual environment'
'info:Show detailed information about a virtual environment'
'install:Install a Python version'
'uninstall:Uninstall a Python version'
'doctor:Diagnose installation issues'
'init:Output shell initialization script'
'completions:Output shell completion script'
'activate:Activate a virtual environment'
'deactivate:Deactivate current virtual environment'
'shell:Set shell-specific environment'
'migrate:Migrate environments from other tools'
'lang:Set or show language preference'
)
_describe -V 'command' commands
;;
args)
case $line[1] in
use)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_unset=false has_global=false has_link=false has_quiet=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
--unset) has_unset=true ;;
--global) has_global=true ;;
--link|--no-link) has_link=true ;;
-q|--quiet) has_quiet=true ;;
--no-color) has_nocolor=true ;;
esac
done
[[ $has_unset == false ]] && opts+=('--unset:Remove version setting')
[[ $has_link == false ]] && opts+=('--link:Create .venv symlink' '--no-link:Do not create .venv symlink')
[[ $has_global == false ]] && opts+=('--global:Set as global default')
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
else
# Check if env name already provided (exclude current word being typed)
local has_env=false
local prev_args=("${words[@]:2:$((CURRENT-3))}")
for w in "${prev_args[@]}"; do
[[ $w != -* && -n $w ]] && has_env=true && break
done
if [[ $has_env == false ]]; then
local envs=(${(f)"$(command scoop list --bare 2>/dev/null)"})
compadd -a envs
fi
fi
;;
remove)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_force=false has_quiet=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
--force) has_force=true ;;
-q|--quiet) has_quiet=true ;;
--no-color) has_nocolor=true ;;
esac
done
[[ $has_force == false ]] && opts+=('--force:Skip confirmation')
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
else
# Check if env name already provided (exclude current word being typed)
local has_env=false
local prev_args=("${words[@]:2:$((CURRENT-3))}")
for w in "${prev_args[@]}"; do
[[ $w != -* && -n $w ]] && has_env=true && break
done
if [[ $has_env == false ]]; then
local envs=(${(f)"$(command scoop list --bare 2>/dev/null)"})
compadd -a envs
fi
fi
;;
info)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_json=false has_allpackages=false has_nosize=false has_quiet=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
--json) has_json=true ;;
--all-packages) has_allpackages=true ;;
--no-size) has_nosize=true ;;
-q|--quiet) has_quiet=true ;;
--no-color) has_nocolor=true ;;
esac
done
[[ $has_json == false ]] && opts+=('--json:Output as JSON')
[[ $has_allpackages == false ]] && opts+=('--all-packages:Show all installed packages')
[[ $has_nosize == false ]] && opts+=('--no-size:Skip directory size calculation')
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
else
# Check if env name already provided (exclude current word being typed)
local has_env=false
local prev_args=("${words[@]:2:$((CURRENT-3))}")
for w in "${prev_args[@]}"; do
[[ $w != -* && -n $w ]] && has_env=true && break
done
if [[ $has_env == false ]]; then
local envs=(${(f)"$(command scoop list --bare 2>/dev/null)"})
compadd -a envs
fi
fi
;;
activate)
# Check if env name already provided (exclude current word being typed)
local has_env=false
local prev_args=("${words[@]:2:$((CURRENT-3))}")
for w in "${prev_args[@]}"; do
[[ $w != -* && -n $w ]] && has_env=true && break
done
if [[ $has_env == false ]]; then
local envs=(${(f)"$(command scoop list --bare 2>/dev/null)"})
compadd -a envs
fi
;;
install)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_version_opt=false has_quiet=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
--latest|--stable) has_version_opt=true ;;
-q|--quiet) has_quiet=true ;;
--no-color) has_nocolor=true ;;
esac
done
[[ $has_version_opt == false ]] && opts+=('--latest:Install latest stable Python' '--stable:Install oldest fully-supported Python')
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
fi
;;
uninstall)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_quiet=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
-q|--quiet) has_quiet=true ;;
--no-color) has_nocolor=true ;;
esac
done
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
else
# Check if version already provided (exclude current word being typed)
local has_ver=false
local prev_args=("${words[@]:2:$((CURRENT-3))}")
for w in "${prev_args[@]}"; do
[[ $w != -* && -n $w ]] && has_ver=true && break
done
if [[ $has_ver == false ]]; then
local pythons=(${(uf)"$(command scoop list --pythons --bare 2>/dev/null)"})
compadd -a pythons
fi
fi
;;
list)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_pythons=false has_json=false has_quiet=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
--pythons) has_pythons=true ;;
--json) has_json=true ;;
-q|--quiet) has_quiet=true ;;
--no-color) has_nocolor=true ;;
esac
done
[[ $has_pythons == false ]] && opts+=('--pythons:Show installed Python versions')
[[ $has_json == false ]] && opts+=('--json:Output as JSON')
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
fi
;;
doctor)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
# Check which options are already used
local has_verbose=false has_quiet=false has_json=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
-v|--verbose) has_verbose=true ;;
-q|--quiet) has_quiet=true ;;
--json) has_json=true ;;
--no-color) has_nocolor=true ;;
esac
done
# Add only unused options
[[ $has_verbose == false ]] && opts+=('-v:Increase verbosity' '--verbose:Increase verbosity')
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_json == false ]] && opts+=('--json:Output as JSON')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
fi
;;
create)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_force=false has_quiet=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
--force) has_force=true ;;
-q|--quiet) has_quiet=true ;;
--no-color) has_nocolor=true ;;
esac
done
[[ $has_force == false ]] && opts+=('--force:Overwrite existing environment')
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
else
# Count positional args before current word
local pos_count=0
local prev_args=("${words[@]:2:$((CURRENT-3))}")
for w in "${prev_args[@]}"; do
[[ $w != -* && -n $w ]] && ((pos_count++))
done
if [[ $pos_count -eq 1 ]]; then
# Second positional arg: python version
local pythons=(${(uf)"$(command scoop list --pythons --bare 2>/dev/null)"})
compadd -a pythons
fi
fi
;;
init|completions)
local shells=('bash:Bash shell' 'zsh:Zsh shell' 'fish:Fish shell' 'powershell:PowerShell')
_describe 'shell' shells
;;
lang)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_list=false has_reset=false has_json=false
for w in "${words[@]}"; do
case "$w" in
--list) has_list=true ;;
--reset) has_reset=true ;;
--json) has_json=true ;;
esac
done
[[ $has_list == false ]] && opts+=('--list:List supported languages')
[[ $has_reset == false ]] && opts+=('--reset:Reset to system default')
[[ $has_json == false ]] && opts+=('--json:Output as JSON')
_describe 'option' opts
else
local langs=('en:English' 'ko:Korean' 'ja:Japanese' 'pt-BR:Portuguese (Brazilian)')
_describe 'language' langs
fi
;;
shell)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
local has_unset=false has_quiet=false has_nocolor=false
for w in "${words[@]}"; do
case "$w" in
--unset) has_unset=true ;;
-q|--quiet) has_quiet=true ;;
--no-color) has_nocolor=true ;;
esac
done
[[ $has_unset == false ]] && opts+=('--unset:Clear shell-specific environment')
[[ $has_quiet == false ]] && opts+=('-q:Suppress all output' '--quiet:Suppress all output')
[[ $has_nocolor == false ]] && opts+=('--no-color:Disable colored output')
_describe 'option' opts
else
# Check if env name already provided (exclude current word being typed)
local has_env=false
local prev_args=("${words[@]:2:$((CURRENT-3))}")
for w in "${prev_args[@]}"; do
[[ $w != -* && -n $w ]] && has_env=true && break
done
if [[ $has_env == false ]]; then
local envs=(${(f)"$(command scoop list --bare 2>/dev/null)"})
compadd -a envs
fi
fi
;;
migrate)
if [[ $cur == -* ]]; then
local opts=('--help:Show help')
_describe 'option' opts
else
local subcmds=('list:List environments available for migration' 'all:Migrate all environments' '@env:Migrate a specific environment')
_describe 'subcommand' subcmds
fi
;;
esac
;;
esac
}
# Register completion only if compdef is available (requires compinit)
(( $+functions[compdef] )) && compdef _scoop scoop
"#
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(unix)]
fn test_init_script_has_valid_zsh_syntax() {
let script = init_script();
let output = std::process::Command::new("zsh")
.arg("-n") .arg("-c")
.arg(script)
.output();
match output {
Ok(result) => {
assert!(
result.status.success(),
"Zsh script has syntax errors:\n{}",
String::from_utf8_lossy(&result.stderr)
);
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("Skipping zsh syntax test: zsh not found");
}
Err(e) => panic!("Failed to run zsh: {}", e),
}
}
#[test]
fn test_init_script_not_empty() {
let script = init_script();
assert!(!script.is_empty(), "Script should not be empty");
}
#[test]
fn test_init_script_defines_required_functions() {
let script = init_script();
let required_functions = ["scoop()", "_scoop_hook()", "_scoop()"];
for func in required_functions {
assert!(
script.contains(func),
"Script missing required function: {}",
func
);
}
}
#[test]
fn test_init_script_registers_chpwd_hook() {
let script = init_script();
assert!(
script.contains("add-zsh-hook chpwd _scoop_hook"),
"Script must register chpwd hook for auto-activation"
);
}
#[test]
fn test_init_script_registers_completion() {
let script = init_script();
assert!(
script.contains("compdef _scoop scoop"),
"Script must register zsh completion"
);
}
#[test]
fn test_init_script_loads_zsh_hooks_module() {
let script = init_script();
assert!(
script.contains("autoload -Uz add-zsh-hook"),
"Script must load zsh hooks module"
);
}
#[test]
#[cfg(unix)]
fn test_init_script_passes_shellcheck() {
let script = init_script();
let temp_file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), script).unwrap();
let output = std::process::Command::new("shellcheck")
.arg("--shell=bash")
.arg("--severity=warning")
.arg("--exclude=SC1087,SC2034,SC2154,SC2168,SC2206,SC2207,SC2296,SC3030,SC3057")
.arg(temp_file.path())
.output();
match output {
Ok(result) => {
assert!(
result.status.success(),
"shellcheck found issues in zsh script:\n{}",
String::from_utf8_lossy(&result.stdout)
);
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!(
"Skipping shellcheck test: shellcheck not found (install: brew install shellcheck)"
);
}
Err(e) => panic!("Failed to run shellcheck: {}", e),
}
}
}