use super::state::{Shell, ShellFlags};
const HEADER_PREFIX: &str = "\
# Generated by linthis — DO NOT EDIT MANUALLY.\n\
# Source of truth: ~/.linthis/shell-state.toml\n\
# Toggle with: linthis shell add|remove ac|alias\n\n";
const HEADER_PREFIX_PS: &str = "\
# Generated by linthis - DO NOT EDIT MANUALLY.\n\
# Source of truth: ~/.linthis/shell-state.toml\n\
# Toggle with: linthis shell add|remove ac|alias\n\n";
pub fn render(shell: Shell, flags: ShellFlags) -> Option<String> {
if flags.is_empty() {
return None;
}
Some(match shell {
Shell::Bash | Shell::Zsh => render_posix(shell, flags),
Shell::Fish => render_fish(flags),
Shell::PowerShell => render_powershell(flags),
})
}
fn render_posix(shell: Shell, flags: ShellFlags) -> String {
let mut out = String::from(HEADER_PREFIX);
if flags.ac {
out.push_str("# --- ac ---\n");
out.push_str(&format!(
"eval \"$(linthis shell completion {} 2>/dev/null)\"\n\n",
shell.key()
));
}
if flags.alias {
out.push_str("# --- alias ---\n");
out.push_str("alias lt='linthis'\n");
out.push_str("alias lts='linthis -s'\n");
out.push_str("alias ltm='linthis -m'\n");
out.push_str("ltr() { linthis report show \"$@\"; }\n");
}
out
}
fn render_fish(flags: ShellFlags) -> String {
let mut out = String::from(HEADER_PREFIX);
if flags.ac {
out.push_str("# --- ac ---\n");
out.push_str("linthis shell completion fish 2>/dev/null | source\n\n");
}
if flags.alias {
out.push_str("# --- alias ---\n");
out.push_str("alias lt 'linthis'\n");
out.push_str("alias lts 'linthis -s'\n");
out.push_str("alias ltm 'linthis -m'\n");
out.push_str("function ltr; linthis report show $argv; end\n");
}
out
}
fn render_powershell(flags: ShellFlags) -> String {
let mut out = String::from(HEADER_PREFIX_PS);
if flags.ac {
out.push_str("# --- ac ---\n");
out.push_str(
"(& linthis shell completion powershell 2>$null) | Out-String | Invoke-Expression\n\n",
);
}
if flags.alias {
out.push_str("# --- alias ---\n");
out.push_str("Set-Alias -Name lt -Value linthis\n");
out.push_str("function lts { linthis -s @args }\n");
out.push_str("function ltm { linthis -m @args }\n");
out.push_str("function ltr { linthis report show @args }\n");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn flags(ac: bool, alias: bool) -> ShellFlags {
ShellFlags { ac, alias }
}
#[test]
fn empty_flags_returns_none_so_caller_deletes_file() {
assert!(render(Shell::Bash, flags(false, false)).is_none());
assert!(render(Shell::Fish, flags(false, false)).is_none());
}
#[test]
fn bash_ac_only_emits_completion_no_alias() {
let s = render(Shell::Bash, flags(true, false)).unwrap();
assert!(s.contains("eval \"$(linthis shell completion bash 2>/dev/null)\""));
assert!(!s.contains("alias lt"));
assert!(!s.contains("ltr()"));
}
#[test]
fn bash_alias_only_emits_aliases_no_completion() {
let s = render(Shell::Bash, flags(false, true)).unwrap();
assert!(!s.contains("eval \"$(linthis shell completion"));
assert!(s.contains("alias lt='linthis'"));
assert!(s.contains("alias lts='linthis -s'"));
assert!(s.contains("alias ltm='linthis -m'"));
}
#[test]
fn ltr_is_function_not_alias_for_argv_passthrough() {
let bash = render(Shell::Bash, flags(false, true)).unwrap();
assert!(bash.contains("ltr() { linthis report show \"$@\"; }"));
assert!(!bash.contains("alias ltr="));
let fish = render(Shell::Fish, flags(false, true)).unwrap();
assert!(fish.contains("function ltr; linthis report show $argv; end"));
assert!(!fish.contains("alias ltr"));
let ps = render(Shell::PowerShell, flags(false, true)).unwrap();
assert!(ps.contains("function ltr { linthis report show @args }"));
}
#[test]
fn zsh_completion_uses_zsh_keyword() {
let s = render(Shell::Zsh, flags(true, false)).unwrap();
assert!(s.contains("linthis shell completion zsh"));
assert!(!s.contains("completion bash"));
}
#[test]
fn fish_uses_pipe_to_source_not_eval() {
let s = render(Shell::Fish, flags(true, false)).unwrap();
assert!(s.contains("linthis shell completion fish 2>/dev/null | source"));
}
#[test]
fn powershell_uses_invoke_expression() {
let s = render(Shell::PowerShell, flags(true, false)).unwrap();
assert!(s.contains("Invoke-Expression"));
}
#[test]
fn header_warns_against_manual_edit() {
let s = render(Shell::Bash, flags(true, true)).unwrap();
assert!(s.starts_with("# Generated by linthis — DO NOT EDIT MANUALLY."));
assert!(s.contains("Toggle with: linthis shell add|remove ac|alias"));
}
#[test]
fn header_has_no_leading_whitespace_on_continuation_lines() {
let s = render(Shell::Bash, flags(true, true)).unwrap();
for line in s.lines() {
if line.starts_with(char::is_whitespace) {
assert!(
!line.trim_start().starts_with('#'),
"comment line has leading whitespace: {line:?}"
);
}
}
}
#[test]
fn powershell_header_uses_ascii_dash_not_em_dash() {
let ps = render(Shell::PowerShell, flags(true, false)).unwrap();
assert!(ps.starts_with("# Generated by linthis - DO NOT EDIT MANUALLY."));
assert!(!ps.contains("# Generated by linthis — DO NOT EDIT MANUALLY."));
}
}