Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

153 linesCodeBlameRaw
1//! Questions, asked only of a person at a terminal. Piped, or with
2//! `G1T_PROMPT_DISABLED` set, nothing is asked: a command that needs an
3//! answer fails with the flag that gives it.
4
5use std::io::{BufRead, IsTerminal, Write};
6
7use crate::error::{Error, Result};
8
9pub fn interactive() -> bool {
10 std::io::stdin().is_terminal()
11 && std::io::stderr().is_terminal()
12 && std::env::var_os("G1T_PROMPT_DISABLED").is_none_or(|v| v.is_empty())
13}
14
15/// Fails the way a command does when it would have asked: naming the flag.
16pub fn needed(what: &str, flag: &str) -> Error {
17 Error::usage(format!("{what} is needed: pass {flag}."))
18}
19
20fn read_line() -> Result<String> {
21 let mut line = String::new();
22 let read = std::io::stdin().lock().read_line(&mut line)?;
23 if read == 0 {
24 return Err(Error::exit(crate::error::Kind::Failed));
25 }
26 Ok(line.trim_end_matches(['\n', '\r']).to_owned())
27}
28
29pub fn input(label: &str, default: Option<&str>) -> Result<String> {
30 let mut err = std::io::stderr().lock();
31 match default {
32 Some(d) if !d.is_empty() => write!(err, "? {label} ({d}) ")?,
33 _ => write!(err, "? {label} ")?,
34 }
35 err.flush()?;
36 drop(err);
37 let line = read_line()?;
38 Ok(if line.trim().is_empty() {
39 default.unwrap_or("").to_owned()
40 } else {
41 line.trim().to_owned()
42 })
43}
44
45pub fn secret(label: &str) -> Result<String> {
46 rpassword::prompt_password(format!("? {label} "))
47 .map_err(|e| Error::failed(format!("Could not read it: {e}")))
48}
49
50/// Picks one of `options`, by number.
51pub fn select(label: &str, options: &[String]) -> Result<usize> {
52 let mut err = std::io::stderr().lock();
53 writeln!(err, "? {label}")?;
54 for (i, option) in options.iter().enumerate() {
55 writeln!(err, " {}. {option}", i + 1)?;
56 }
57 drop(err);
58 loop {
59 let answer = input("Number:", Some("1"))?;
60 match answer.parse::<usize>() {
61 Ok(n) if (1..=options.len()).contains(&n) => return Ok(n - 1),
62 _ => eprintln!(" Pick 1 to {}.", options.len()),
63 }
64 }
65}
66
67/// Opens the editor on `initial` and returns what was saved.
68pub fn editor(initial: &str) -> Result<String> {
69 let editor = ["G1T_EDITOR", "VISUAL", "EDITOR"]
70 .iter()
71 .find_map(|k| std::env::var(k).ok().filter(|v| !v.trim().is_empty()))
72 .or_else(|| {
73 crate::git::run(&["config", "core.editor"])
74 .ok()
75 .filter(|v| !v.is_empty())
76 })
77 .unwrap_or_else(|| {
78 if cfg!(windows) {
79 "notepad".to_owned()
80 } else {
81 "vi".to_owned()
82 }
83 });
84 let path = std::env::temp_dir().join(format!("g1t-{}.md", std::process::id()));
85 std::fs::write(&path, initial)?;
86 let shell_line = format!("{editor} \"{}\"", path.display());
87 let status = if cfg!(windows) {
88 std::process::Command::new("cmd")
89 .args(["/C", &shell_line])
90 .status()
91 } else {
92 std::process::Command::new("sh")
93 .args(["-c", &shell_line])
94 .status()
95 }
96 .map_err(|e| Error::failed(format!("Could not open {editor}: {e}")))?;
97 let text = std::fs::read_to_string(&path).unwrap_or_default();
98 let _ = std::fs::remove_file(&path);
99 if !status.success() {
100 return Err(Error::failed(format!("{editor} did not finish.")));
101 }
102 Ok(text.trim().to_owned())
103}
104
105/// A body from `--body`, `--body-file` (`-` for stdin), or the editor.
106pub fn body(body: Option<String>, file: Option<&str>, ask: bool) -> Result<Option<String>> {
107 if let Some(b) = body {
108 return Ok(Some(b));
109 }
110 if let Some(path) = file {
111 let text = if path == "-" {
112 let mut s = String::new();
113 std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?;
114 s
115 } else {
116 std::fs::read_to_string(path)
117 .map_err(|e| Error::failed(format!("Could not read {path}: {e}")))?
118 };
119 return Ok(Some(text));
120 }
121 if ask && interactive() {
122 let text = editor("")?;
123 return Ok((!text.is_empty()).then_some(text));
124 }
125 Ok(None)
126}
127
128/// Opens `url` in a browser, or prints it when there is none to open.
129pub fn open_browser(url: &str) -> bool {
130 if let Some(browser) = std::env::var("G1T_BROWSER")
131 .ok()
132 .or_else(|| std::env::var("BROWSER").ok())
133 .filter(|b| !b.is_empty())
134 {
135 return std::process::Command::new(browser)
136 .arg(url)
137 .status()
138 .is_ok_and(|s| s.success());
139 }
140 let result = if cfg!(windows) {
141 std::process::Command::new("rundll32")
142 .args(["url.dll,FileProtocolHandler", url])
143 .status()
144 } else if cfg!(target_os = "macos") {
145 std::process::Command::new("open").arg(url).status()
146 } else {
147 std::process::Command::new("xdg-open")
148 .arg(url)
149 .stderr(std::process::Stdio::null())
150 .status()
151 };
152 result.is_ok_and(|s| s.success())
153}