| 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 | |
| 5 | use std::io::{BufRead, IsTerminal, Write}; |
| 6 | |
| 7 | use crate::error::{Error, Result}; |
| 8 | |
| 9 | pub 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. |
| 16 | pub fn needed(what: &str, flag: &str) -> Error { |
| 17 | Error::usage(format!("{what} is needed: pass {flag}.")) |
| 18 | } |
| 19 | |
| 20 | fn 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 | |
| 29 | pub 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 | |
| 45 | pub 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. |
| 51 | pub 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. |
| 68 | pub 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. |
| 106 | pub 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. |
| 129 | pub 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 | } |