use anyhow::{bail, Context, Result};
use clap_complete::{generate, Shell};
use std::fs;
use std::path::{Path, PathBuf};
pub fn generate_to_stdout(shell: Shell, cmd: &mut clap::Command) {
generate(shell, cmd, "node-app", &mut std::io::stdout());
}
pub fn install(shell: Option<Shell>, cmd: &mut clap::Command) -> Result<()> {
let shell = resolve_shell(shell)?;
let home = home_dir()?;
let path = install_path(shell, &home)?;
let mut buf = Vec::new();
generate(shell, cmd, "node-app", &mut buf);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create {}", parent.display()))?;
}
fs::write(&path, &buf)
.with_context(|| format!("write {}", path.display()))?;
println!("Completions installed → {}", path.display());
patch_rc(shell, &home, &path)?;
Ok(())
}
fn patch_rc(shell: Shell, home: &Path, completion_path: &Path) -> Result<()> {
match shell {
Shell::Bash => patch_file(
&home.join(".bashrc"),
"# node-app completions",
&format!(
"# node-app completions\nsource \"{}\"",
completion_path.display()
),
),
Shell::Zsh => {
let zshrc = home.join(".zshrc");
let fpath_dir = home.join(".zfunc");
let marker = "# node-app: user completions fpath";
let snippet = format!(
"{marker}\nfpath=(\"{fpath}\" $fpath)",
fpath = fpath_dir.display()
);
prepend_if_missing(&zshrc, marker, &snippet)
}
Shell::Fish => {
println!("Fish auto-loads completions; no rc change needed.");
Ok(())
}
Shell::PowerShell => patch_file(
&home.join(".config/powershell/Microsoft.PowerShell_profile.ps1"),
"# node-app completions",
&format!(
"# node-app completions\n. \"{}\"",
completion_path.display()
),
),
Shell::Elvish => patch_file(
&home.join(".config/elvish/rc.elv"),
"# node-app completions",
"# node-app completions\nuse completions/node-app",
),
_ => Ok(()),
}
}
fn patch_file(path: &Path, marker: &str, snippet: &str) -> Result<()> {
let existing = fs::read_to_string(path).unwrap_or_default();
if existing.contains(marker) {
println!("rc already configured ({})", path.display());
return Ok(());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create {}", parent.display()))?;
}
let mut content = existing;
if !content.ends_with('\n') && !content.is_empty() {
content.push('\n');
}
content.push('\n');
content.push_str(snippet);
content.push('\n');
fs::write(path, &content).with_context(|| format!("write {}", path.display()))?;
println!("Updated {}", path.display());
Ok(())
}
fn prepend_if_missing(path: &Path, marker: &str, snippet: &str) -> Result<()> {
let existing = fs::read_to_string(path).unwrap_or_default();
if existing.contains(marker) {
println!("rc already configured ({})", path.display());
return Ok(());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create {}", parent.display()))?;
}
let content = format!("{snippet}\n\n{existing}");
fs::write(path, &content).with_context(|| format!("write {}", path.display()))?;
println!("Updated {} (open a new terminal to activate)", path.display());
Ok(())
}
fn resolve_shell(shell: Option<Shell>) -> Result<Shell> {
if let Some(s) = shell {
return Ok(s);
}
let shell_path = std::env::var("SHELL")
.context("$SHELL is not set; pass --shell bash|zsh|fish|powershell|elvish")?;
let name = Path::new(&shell_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
match name {
"bash" => Ok(Shell::Bash),
"zsh" => Ok(Shell::Zsh),
"fish" => Ok(Shell::Fish),
"pwsh" | "powershell" => Ok(Shell::PowerShell),
"elvish" | "elv" => Ok(Shell::Elvish),
other => bail!(
"unrecognised shell '{other}'; pass --shell bash|zsh|fish|powershell|elvish"
),
}
}
fn home_dir() -> Result<PathBuf> {
std::env::var("HOME")
.map(PathBuf::from)
.context("$HOME is not set")
}
fn xdg_config(home: &Path) -> PathBuf {
std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home.join(".config"))
}
fn xdg_data(home: &Path) -> PathBuf {
std::env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home.join(".local/share"))
}
fn install_path(shell: Shell, home: &Path) -> Result<PathBuf> {
Ok(match shell {
Shell::Bash => xdg_data(home).join("bash-completion/completions/node-app"),
Shell::Zsh => home.join(".zfunc/_node-app"),
Shell::Fish => xdg_config(home).join("fish/completions/node-app.fish"),
Shell::PowerShell => xdg_data(home).join("node-app/completions/node-app.ps1"),
Shell::Elvish => xdg_data(home).join("elvish/lib/completions/node-app.elv"),
_ => bail!(
"unsupported shell for auto-install; use `completions generate` and install manually"
),
})
}