Skip to main content

git_stk/
setup.rs

1use std::env;
2use std::fs;
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use clap::CommandFactory;
7
8use crate::cli::Cli;
9use crate::prompt::confirm;
10
11/// Marker comment written above the completion line so re-runs can detect it.
12const COMPLETION_MARKER: &str = "# added by git-stk setup";
13
14pub fn setup(yes: bool, refresh: bool) -> Result<()> {
15    if refresh {
16        // Re-render assets that can go stale across versions. Non-interactive;
17        // run by `upgrade` via the newly installed binary. Completion wiring is
18        // left alone because the rc line re-sources from the binary on every
19        // shell start.
20        return install_man_page();
21    }
22
23    install_man_page()?;
24    wire_completions(yes)?;
25    Ok(())
26}
27
28/// Render the man page into the XDG data directory, which is on the default
29/// manpath. This makes `git stk --help` work: git resolves it as `man git-stk`.
30fn install_man_page() -> Result<()> {
31    if cfg!(windows) {
32        return Ok(());
33    }
34
35    let dir = man_dir()?;
36    fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
37
38    let mut buffer = Vec::new();
39    clap_mangen::Man::new(Cli::command())
40        .render(&mut buffer)
41        .context("failed to render man page")?;
42
43    let path = dir.join("git-stk.1");
44    fs::write(&path, buffer).with_context(|| format!("failed to write {}", path.display()))?;
45    println!("installed man page to {}", path.display());
46    Ok(())
47}
48
49fn man_dir() -> Result<PathBuf> {
50    let data_home = env::var_os("XDG_DATA_HOME")
51        .map(PathBuf::from)
52        .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/share")))
53        .context("cannot locate a data directory; set HOME or XDG_DATA_HOME")?;
54    Ok(data_home.join("man/man1"))
55}
56
57/// Append a completion-sourcing line to the detected shell's rc file, once.
58fn wire_completions(yes: bool) -> Result<()> {
59    let Some((shell, rc_path, line)) = completion_target()? else {
60        println!("could not detect a supported shell from $SHELL");
61        println!("see the README for manual completion setup");
62        return Ok(());
63    };
64
65    let existing = match fs::read_to_string(&rc_path) {
66        Ok(contents) => contents,
67        Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
68        Err(error) => {
69            return Err(error).with_context(|| format!("failed to read {}", rc_path.display()));
70        }
71    };
72
73    if existing.contains(COMPLETION_MARKER) || existing.contains("git stk completions") {
74        println!(
75            "{shell} completions already configured in {}",
76            rc_path.display()
77        );
78        return Ok(());
79    }
80
81    if !yes
82        && !confirm(&format!(
83            "append completion setup to {}? [y/N] ",
84            rc_path.display()
85        ))?
86    {
87        println!("skipped completion setup");
88        println!("to configure manually, add this to {}:", rc_path.display());
89        println!("  {line}");
90        return Ok(());
91    }
92
93    let mut updated = existing;
94    if !updated.is_empty() && !updated.ends_with('\n') {
95        updated.push('\n');
96    }
97    updated.push_str(&format!("\n{COMPLETION_MARKER}\n{line}\n"));
98    fs::write(&rc_path, updated)
99        .with_context(|| format!("failed to write {}", rc_path.display()))?;
100    println!("added {shell} completion setup to {}", rc_path.display());
101    Ok(())
102}
103
104/// Resolve (shell name, rc file, completion line) from $SHELL.
105fn completion_target() -> Result<Option<(&'static str, PathBuf, &'static str)>> {
106    let shell = env::var("SHELL").unwrap_or_default();
107    let shell = shell.rsplit('/').next().unwrap_or_default();
108
109    let home = env::var_os("HOME")
110        .map(PathBuf::from)
111        .context("cannot locate home directory; set HOME")?;
112
113    let target = match shell {
114        "bash" => Some((
115            "bash",
116            home.join(".bashrc"),
117            "source <(git stk completions bash)",
118        )),
119        "zsh" => Some((
120            "zsh",
121            home.join(".zshrc"),
122            "source <(git stk completions zsh)",
123        )),
124        "fish" => Some((
125            "fish",
126            home.join(".config/fish/config.fish"),
127            "git stk completions fish | source",
128        )),
129        _ => None,
130    };
131    Ok(target)
132}