use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use anyhow::{Context, Result};
use clap::CommandFactory;
use crate::cli::Cli;
use crate::prompt::confirm;
const COMPLETION_MARKER: &str = "# added by git-stk setup";
const POWERSHELL_LINE: &str = "if (Get-Command git-stk -ErrorAction SilentlyContinue) { git stk completions powershell | Out-String | Invoke-Expression }";
pub fn setup(yes: bool, refresh: bool) -> Result<()> {
if refresh {
install_man_page()?;
return print_completion_hint();
}
install_man_page()?;
wire_completions(yes)?;
Ok(())
}
fn install_man_page() -> Result<()> {
if cfg!(windows) {
return Ok(());
}
let dir = man_dir()?;
fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
let mut buffer = Vec::new();
clap_mangen::Man::new(Cli::command())
.render(&mut buffer)
.context("failed to render man page")?;
let path = dir.join("git-stk.1");
fs::write(&path, buffer).with_context(|| format!("failed to write {}", path.display()))?;
println!("installed man page to {}", path.display());
Ok(())
}
fn man_dir() -> Result<PathBuf> {
let data_home = env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/share")))
.context("cannot locate a data directory; set HOME or XDG_DATA_HOME")?;
Ok(data_home.join("man/man1"))
}
fn wire_completions(yes: bool) -> Result<()> {
let Some((shell, rc_path, line)) = completion_target()? else {
println!("could not detect a supported shell");
println!("see the README for manual completion setup");
return Ok(());
};
let existing = match fs::read_to_string(&rc_path) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(error) => {
return Err(error).with_context(|| format!("failed to read {}", rc_path.display()));
}
};
if existing.contains(COMPLETION_MARKER) || existing.contains("git stk completions") {
println!(
"{shell} completions already configured in {}",
rc_path.display()
);
return Ok(());
}
if !yes
&& !confirm(&format!(
"append completion setup to {}? [y/N] ",
rc_path.display()
))?
{
println!("skipped completion setup");
println!("to configure manually, add this to {}:", rc_path.display());
println!(" {line}");
return Ok(());
}
let mut updated = existing;
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
updated.push_str(&format!("\n{COMPLETION_MARKER}\n{line}\n"));
if let Some(parent) = rc_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
fs::write(&rc_path, updated)
.with_context(|| format!("failed to write {}", rc_path.display()))?;
println!("added {shell} completion setup to {}", rc_path.display());
Ok(())
}
fn print_completion_hint() -> Result<()> {
let Some((shell, rc_path, line)) = completion_target()? else {
return Ok(());
};
let configured = fs::read_to_string(&rc_path)
.map(|rc| rc.contains(COMPLETION_MARKER) || rc.contains("git stk completions"))
.unwrap_or(false);
if configured {
return Ok(());
}
println!(
"{shell} completions are not configured; run `git stk setup`, \
or add this to {}:",
rc_path.display()
);
println!(" {line}");
Ok(())
}
fn completion_target() -> Result<Option<(&'static str, PathBuf, &'static str)>> {
if let Some(target) = posix_shell_target() {
return Ok(Some(target));
}
Ok(powershell_target())
}
fn posix_shell_target() -> Option<(&'static str, PathBuf, &'static str)> {
let shell = env::var("SHELL").unwrap_or_default();
let shell = shell.rsplit('/').next().unwrap_or_default();
let home = env::var_os("HOME").map(PathBuf::from)?;
match shell {
"bash" => Some((
"bash",
home.join(".bashrc"),
"command -v git-stk >/dev/null && source <(git stk completions bash)",
)),
"zsh" => Some((
"zsh",
home.join(".zshrc"),
"command -v git-stk >/dev/null && source <(git stk completions zsh)",
)),
"fish" => Some((
"fish",
home.join(".config/fish/config.fish"),
"command -q git-stk; and git stk completions fish | source",
)),
_ => None,
}
}
fn powershell_target() -> Option<(&'static str, PathBuf, &'static str)> {
for exe in ["pwsh", "powershell"] {
let Ok(output) = Command::new(exe)
.args(["-NoProfile", "-Command", "$PROFILE"])
.output()
else {
continue;
};
if !output.status.success() {
continue;
}
let path = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if !path.is_empty() {
return Some(("PowerShell", PathBuf::from(path), POWERSHELL_LINE));
}
}
None
}