//! Terminal output and prompts. //! //! Two rules shape this module: //! //! 1. **A run's output is a support artifact.** `docs/installer/INSTALL.md` shows operators what a //! run looks like, and the first thing anyone asks for in a bug report is a pasted log — so the //! marks and the column layout here match the guide rather than being decided per call site. //! 2. **Nothing here is a secret.** The auth token is printed by the handoff (Phase 2) straight to //! the operator's terminal and never routed through a log file (PLAN.md §6). use std::io::{self, IsTerminal, Write}; /// Enables UTF-8 on the Windows console so the status marks below are not mojibake. /// /// The guide's illustrated output uses `✓ ⚠ ✗`, and PowerShell 5.1 on a machine whose console code /// page is still 437/1252 renders those as garbage. `SetConsoleOutputCP` is the one-call fix; it is /// declared inline rather than pulling in a Windows binding crate for a single symbol, and a /// failure is ignored because a wrongly-encoded tick is a cosmetic problem, not a reason to refuse /// to install. #[cfg(windows)] pub fn init_console() { extern "system" { fn SetConsoleOutputCP(code_page: u32) -> i32; } const CP_UTF8: u32 = 65001; unsafe { SetConsoleOutputCP(CP_UTF8); } } #[cfg(not(windows))] pub fn init_console() {} pub fn ok(msg: &str) { println!("✓ {msg}"); } pub fn warn(msg: &str) { println!("⚠ {msg}"); } pub fn heading(msg: &str) { println!("\n{msg}"); } /// A two-column row: ` label value`. pub fn row(label: &str, value: &str) { println!(" {label:<16} {value}"); } /// Asks a yes/no question. /// /// `assume_yes` (`--yes`) takes the default without asking, which is what makes an unattended run /// expressible. A non-interactive run *without* `--yes` is an error rather than a silent default: /// the questions this asks decide whether stock ServUO files get edited, and a pipe with no /// terminal on the other end cannot consent to that. pub fn confirm(question: &str, default: bool, assume_yes: bool) -> io::Result { if assume_yes { println!( "{question} [{}] (--yes)", if default { "Y/n" } else { "y/N" } ); return Ok(default); } if !io::stdin().is_terminal() { return Err(io::Error::other(format!( "cannot ask \"{question}\" — stdin is not a terminal. \ Pass --yes to take the default, or the matching flag to answer it explicitly." ))); } loop { print!("{question} [{}] ", if default { "Y/n" } else { "y/N" }); io::stdout().flush()?; let mut line = String::new(); // EOF (0 bytes) is not "yes". It means the operator is gone; take the default and move on. if io::stdin().read_line(&mut line)? == 0 { println!(); return Ok(default); } match line.trim().to_ascii_lowercase().as_str() { "" => return Ok(default), "y" | "yes" => return Ok(true), "n" | "no" => return Ok(false), _ => println!(" please answer y or n"), } } } /// Asks for a line of text. An empty answer keeps `default` when one is offered. pub fn prompt(question: &str, default: Option<&str>) -> io::Result { if !io::stdin().is_terminal() { return Err(io::Error::other(format!( "cannot ask \"{question}\" — stdin is not a terminal. Pass the matching flag." ))); } loop { match default { Some(d) => print!("{question} [{d}]: "), None => print!("{question}: "), } io::stdout().flush()?; let mut line = String::new(); if io::stdin().read_line(&mut line)? == 0 { println!(); return match default { Some(d) => Ok(d.to_string()), None => Err(io::Error::new( io::ErrorKind::UnexpectedEof, format!("no answer for \"{question}\""), )), }; } let answer = line.trim(); if !answer.is_empty() { return Ok(answer.to_string()); } if let Some(d) = default { return Ok(d.to_string()); } println!(" an answer is required"); } }