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
11const COMPLETION_MARKER: &str = "# added by git-stk setup";
13
14pub fn setup(yes: bool, refresh: bool) -> Result<()> {
15 if refresh {
16 install_man_page()?;
21 return print_completion_hint();
22 }
23
24 install_man_page()?;
25 wire_completions(yes)?;
26 Ok(())
27}
28
29fn 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
58fn 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
105fn 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
128fn 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}