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; missing wiring gets a hint instead of a prompt.
20        install_man_page()?;
21        return print_completion_hint();
22    }
23
24    install_man_page()?;
25    wire_completions(yes)?;
26    Ok(())
27}
28
29/// Render the man page into the XDG data directory, which is on the default
30/// manpath. This makes `git stk --help` work: git resolves it as `man git-stk`.
31fn install_man_page() -> Result<()> {
32    if cfg!(windows) {
33        return Ok(());
34    }
35
36    let dir = man_dir()?;
37    fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
38
39    let mut buffer = Vec::new();
40    clap_mangen::Man::new(Cli::command())
41        .render(&mut buffer)
42        .context("failed to render man page")?;
43
44    let path = dir.join("git-stk.1");
45    fs::write(&path, buffer).with_context(|| format!("failed to write {}", path.display()))?;
46    println!("installed man page to {}", path.display());
47    Ok(())
48}
49
50fn man_dir() -> Result<PathBuf> {
51    let data_home = env::var_os("XDG_DATA_HOME")
52        .map(PathBuf::from)
53        .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/share")))
54        .context("cannot locate a data directory; set HOME or XDG_DATA_HOME")?;
55    Ok(data_home.join("man/man1"))
56}
57
58/// Append a completion-sourcing line to the detected shell's rc file, once.
59fn wire_completions(yes: bool) -> Result<()> {
60    let Some((shell, rc_path, line)) = completion_target()? else {
61        println!("could not detect a supported shell from $SHELL");
62        println!("see the README for manual completion setup");
63        return Ok(());
64    };
65
66    let existing = match fs::read_to_string(&rc_path) {
67        Ok(contents) => contents,
68        Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
69        Err(error) => {
70            return Err(error).with_context(|| format!("failed to read {}", rc_path.display()));
71        }
72    };
73
74    if existing.contains(COMPLETION_MARKER) || existing.contains("git stk completions") {
75        println!(
76            "{shell} completions already configured in {}",
77            rc_path.display()
78        );
79        return Ok(());
80    }
81
82    if !yes
83        && !confirm(&format!(
84            "append completion setup to {}? [y/N] ",
85            rc_path.display()
86        ))?
87    {
88        println!("skipped completion setup");
89        println!("to configure manually, add this to {}:", rc_path.display());
90        println!("  {line}");
91        return Ok(());
92    }
93
94    let mut updated = existing;
95    if !updated.is_empty() && !updated.ends_with('\n') {
96        updated.push('\n');
97    }
98    updated.push_str(&format!("\n{COMPLETION_MARKER}\n{line}\n"));
99    fs::write(&rc_path, updated)
100        .with_context(|| format!("failed to write {}", rc_path.display()))?;
101    println!("added {shell} completion setup to {}", rc_path.display());
102    Ok(())
103}
104
105/// Point at `git stk setup` when the detected shell has no completion
106/// wiring yet. Used after upgrades, where prompting is not an option.
107fn print_completion_hint() -> Result<()> {
108    let Some((shell, rc_path, line)) = completion_target()? else {
109        return Ok(());
110    };
111
112    let configured = fs::read_to_string(&rc_path)
113        .map(|rc| rc.contains(COMPLETION_MARKER) || rc.contains("git stk completions"))
114        .unwrap_or(false);
115    if configured {
116        return Ok(());
117    }
118
119    println!(
120        "{shell} completions are not configured; run `git stk setup`, \
121         or add this to {}:",
122        rc_path.display()
123    );
124    println!("  {line}");
125    Ok(())
126}
127
128/// Resolve (shell name, rc file, completion line) from $SHELL. The lines
129/// guard on the binary existing so a removed git-stk never breaks shell
130/// startup.
131fn completion_target() -> Result<Option<(&'static str, PathBuf, &'static str)>> {
132    let shell = env::var("SHELL").unwrap_or_default();
133    let shell = shell.rsplit('/').next().unwrap_or_default();
134
135    let home = env::var_os("HOME")
136        .map(PathBuf::from)
137        .context("cannot locate home directory; set HOME")?;
138
139    let target = match shell {
140        "bash" => Some((
141            "bash",
142            home.join(".bashrc"),
143            "command -v git-stk >/dev/null && source <(git stk completions bash)",
144        )),
145        "zsh" => Some((
146            "zsh",
147            home.join(".zshrc"),
148            "command -v git-stk >/dev/null && source <(git stk completions zsh)",
149        )),
150        "fish" => Some((
151            "fish",
152            home.join(".config/fish/config.fish"),
153            "command -q git-stk; and git stk completions fish | source",
154        )),
155        _ => None,
156    };
157    Ok(target)
158}