use crate::cli::{InitShellArgs, SupportedShell};
use crate::commands::cd::CD_OUT_ENV;
pub async fn run(args: InitShellArgs) -> anyhow::Result<()> {
let name = &args.name;
let script = match args.shell {
SupportedShell::Powershell => powershell_wrapper(name),
SupportedShell::Bash | SupportedShell::Zsh => posix_wrapper(name),
SupportedShell::Fish => fish_wrapper(name),
};
print!("{script}");
Ok(())
}
fn posix_wrapper(name: &str) -> String {
format!(
r#"{name}() {{
if [ $# -eq 0 ]; then
set -- tui
fi
case "$1" in
cd|tui)
local tmp dest status
tmp=$(mktemp) || return 1
{env}="$tmp" command shoka "$@"
status=$?
if [ "$status" -eq 0 ]; then
dest=$(cat "$tmp")
fi
rm -f "$tmp"
[ "$status" -eq 0 ] && [ -n "$dest" ] && cd -- "$dest"
return $status
;;
*)
command shoka "$@"
;;
esac
}}
"#,
env = CD_OUT_ENV,
)
}
fn fish_wrapper(name: &str) -> String {
format!(
r#"function {name}
if test (count $argv) -eq 0
set argv tui
end
switch "$argv[1]"
case cd tui
set -l tmp (mktemp); or return 1
set -lx {env} $tmp
command shoka $argv
set -l rc $status
set -l dest ""
if test $rc -eq 0
set dest (cat $tmp)
end
rm -f $tmp
if test $rc -eq 0; and test -n "$dest"
cd -- $dest
end
return $rc
case '*'
command shoka $argv
end
end
"#,
env = CD_OUT_ENV,
)
}
fn powershell_wrapper(name: &str) -> String {
format!(
r#"function {name} {{
if (-not $script:ShokaExe) {{
$script:ShokaExe = (Get-Command -Name shoka -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1).Source
}}
if (-not $script:ShokaExe) {{
Write-Error 'shoka binary not found on PATH'
return
}}
$first = if ($args.Count -gt 0) {{ $args[0] }} else {{ 'tui' }}
if ($first -eq 'cd' -or $first -eq 'tui') {{
$tmp = New-TemporaryFile
try {{
$env:{env} = $tmp.FullName
if ($args.Count -eq 0) {{
& $script:ShokaExe tui
}} else {{
& $script:ShokaExe @args
}}
$code = $LASTEXITCODE
if ($code -eq 0) {{
$dest = Get-Content -LiteralPath $tmp.FullName -Raw
if ($dest) {{ Set-Location -LiteralPath $dest.TrimEnd() }}
}}
$global:LASTEXITCODE = $code
}} finally {{
Remove-Item -LiteralPath $tmp.FullName -Force -ErrorAction SilentlyContinue
Remove-Item Env:{env} -ErrorAction SilentlyContinue
}}
}} else {{
& $script:ShokaExe @args
}}
}}
"#,
env = CD_OUT_ENV,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::SupportedShell;
fn args(shell: SupportedShell, name: &str) -> InitShellArgs {
InitShellArgs {
shell,
name: name.into(),
}
}
fn rendered(shell: SupportedShell, name: &str) -> String {
match shell {
SupportedShell::Powershell => powershell_wrapper(name),
SupportedShell::Bash | SupportedShell::Zsh => posix_wrapper(name),
SupportedShell::Fish => fish_wrapper(name),
}
}
#[test]
fn posix_wrapper_dispatches_cd_and_tui_through_sidechannel() {
let body = rendered(SupportedShell::Bash, "shoka");
assert!(
body.contains("shoka()"),
"function name should be `shoka`: {body}"
);
assert!(
body.contains("SHOKA_CD_OUT"),
"wrapper must set the sidechannel env var: {body}"
);
assert!(
body.contains("cd|tui)"),
"wrapper must intercept both `cd` and `tui`: {body}"
);
assert!(
body.contains("command shoka"),
"wrapper must use `command shoka` to bypass the function: {body}"
);
assert!(
!body.contains("$(command shoka cd"),
"wrapper must not capture stdout via command substitution: {body}"
);
}
#[test]
fn posix_wrapper_passes_other_subcommands_through_to_binary() {
let body = rendered(SupportedShell::Bash, "shoka");
let wildcard_pos = body
.find("*)")
.expect("wildcard pass-through branch must exist");
let after = &body[wildcard_pos..];
assert!(
after.contains(r#"command shoka "$@""#),
"pass-through arm must invoke the binary without sidechannel setup: {body}"
);
}
#[test]
fn fish_wrapper_dispatches_cd_and_tui_through_sidechannel() {
let body = rendered(SupportedShell::Fish, "shoka");
assert!(body.contains("function shoka"));
assert!(body.contains("SHOKA_CD_OUT"));
assert!(
body.contains("case cd tui"),
"fish wrapper must intercept both `cd` and `tui`: {body}"
);
assert!(
body.contains("command shoka"),
"fish wrapper must invoke the binary via `command`: {body}"
);
assert!(
!body.contains("(command shoka cd"),
"fish wrapper must not capture stdout via subshell: {body}"
);
}
#[test]
fn powershell_wrapper_dispatches_cd_and_tui_through_sidechannel() {
let body = rendered(SupportedShell::Powershell, "shoka");
assert!(body.contains("function shoka"));
assert!(body.contains("SHOKA_CD_OUT"));
assert!(
body.contains("Get-Command -Name shoka -CommandType Application"),
"PowerShell wrapper must resolve the binary explicitly to avoid recursion: {body}"
);
assert!(
body.contains("$script:ShokaExe"),
"PowerShell wrapper must cache the resolved binary in $script:ShokaExe: {body}"
);
assert!(
body.contains("$first -eq 'cd' -or $first -eq 'tui'"),
"PowerShell wrapper must dispatch on the first arg: {body}"
);
assert!(body.contains("finally"), "wrapper missing cleanup: {body}");
assert!(
body.contains("& $script:ShokaExe tui"),
"PowerShell wrapper must invoke tui literally to avoid the array-unwrap trap: {body}"
);
}
#[test]
fn posix_wrapper_defaults_to_tui_when_called_without_args() {
let body = rendered(SupportedShell::Bash, "shoka");
assert!(
body.contains("if [ $# -eq 0 ]; then\n set -- tui\n fi"),
"bash wrapper must default to `tui` when called without args: {body}"
);
}
#[test]
fn fish_wrapper_defaults_to_tui_when_called_without_args() {
let body = rendered(SupportedShell::Fish, "shoka");
assert!(
body.contains("if test (count $argv) -eq 0\n set argv tui\n end"),
"fish wrapper must default to `tui` when called without args: {body}"
);
}
#[test]
fn powershell_wrapper_defaults_to_tui_when_called_without_args() {
let body = rendered(SupportedShell::Powershell, "shoka");
assert!(
body.contains("if ($args.Count -gt 0) { $args[0] } else { 'tui' }"),
"PowerShell wrapper must compute first-arg default as `tui`: {body}"
);
assert!(
body.contains("if ($args.Count -eq 0) {\n & $script:ShokaExe tui"),
"PowerShell wrapper must invoke shoka.exe tui when called without args: {body}"
);
}
#[test]
fn custom_name_substitutes_throughout() {
let body = rendered(SupportedShell::Bash, "s");
assert!(body.contains("s()"));
assert!(body.contains("cd|tui)"));
assert!(body.contains("command shoka"));
assert!(
!body.contains("shoka()"),
"custom name must not also emit a default `shoka()` definition: {body}"
);
}
#[test]
fn run_emits_wrapper_for_each_supported_shell() {
let rt = tokio::runtime::Runtime::new().unwrap();
for shell in [
SupportedShell::Bash,
SupportedShell::Zsh,
SupportedShell::Fish,
SupportedShell::Powershell,
] {
rt.block_on(run(args(shell, "shoka"))).unwrap();
}
}
}