use std::cmp::{max, min}; use std::env; use std::io::{self, Write}; #[cfg(target_os = "linux")] use std::path::PathBuf; use std::process::Command; use std::sync::Mutex; use std::time::Duration; use chrono::{Local, Timelike}; use console::{style, Key, Term}; use lazy_static::lazy_static; use log::{debug, error}; #[cfg(target_os = "macos")] use notify_rust::{Notification, Timeout}; #[cfg(windows)] use which_crate::which; use crate::report::StepResult; #[cfg(target_os = "linux")] use crate::utils::which; lazy_static! { static ref TERMINAL: Mutex = Mutex::new(Terminal::new()); } #[cfg(unix)] pub fn shell() -> String { env::var("SHELL").unwrap_or_else(|_| "sh".to_string()) } #[cfg(windows)] pub fn shell() -> &'static str { which("pwsh").map(|_| "pwsh").unwrap_or("powershell") } pub fn run_shell() { Command::new(shell()) .env("IN_TOPGRADE", "1") .spawn() .unwrap() .wait() .unwrap(); } struct Terminal { width: Option, prefix: String, term: Term, set_title: bool, display_time: bool, desktop_notification: bool, #[cfg(target_os = "linux")] notify_send: Option, } impl Terminal { fn new() -> Self { let term = Term::stdout(); Self { width: term.size_checked().map(|(_, w)| w), term, prefix: env::var("TOPGRADE_PREFIX") .map(|prefix| format!("({}) ", prefix)) .unwrap_or_else(|_| String::new()), set_title: true, display_time: true, desktop_notification: false, #[cfg(target_os = "linux")] notify_send: which("notify-send"), } } fn set_desktop_notifications(&mut self, desktop_notifications: bool) { self.desktop_notification = desktop_notifications } fn set_title(&mut self, set_title: bool) { self.set_title = set_title } fn display_time(&mut self, display_time: bool) { self.display_time = display_time } #[allow(unused_variables)] fn notify_desktop>(&self, message: P, timeout: Option) { debug!("Desktop notification: {}", message.as_ref()); cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { let mut notification = Notification::new(); notification.summary("Topgrade") .body(message.as_ref()) .appname("topgrade"); if let Some(timeout) = timeout { notification.timeout(Timeout::Milliseconds(timeout.as_millis() as u32)); } notification.show().ok(); } else if #[cfg(target_os = "linux")] { if let Some(ns) = self.notify_send.as_ref() { let mut command = Command::new(ns); if let Some(timeout) = timeout { command.arg("-t"); command.arg(format!("{}", timeout.as_millis())); } command.args(["-a", "Topgrade", "Topgrade"]); command.arg(message.as_ref()); command.output().ok(); } } } } fn print_separator>(&mut self, message: P) { if self.set_title { self.term .set_title(format!("{}Topgrade - {}", self.prefix, message.as_ref())); } if self.desktop_notification { self.notify_desktop(message.as_ref(), Some(Duration::from_secs(5))); } let now = Local::now(); let message = if self.display_time { format!( "{}{:02}:{:02}:{:02} - {}", self.prefix, now.hour(), now.minute(), now.second(), message.as_ref() ) } else { String::from(message.as_ref()) }; match self.width { Some(width) => { self.term .write_fmt(format_args!( "{}\n", style(format_args!( "\n―― {} {:―^border$}", message, "", border = max( 2, min(80, width as usize) .checked_sub(4) .and_then(|e| e.checked_sub(message.len())) .unwrap_or(0) ) )) .bold() )) .ok(); } None => { self.term.write_fmt(format_args!("―― {} ――\n", message)).ok(); } } } #[allow(dead_code)] fn print_warning>(&mut self, message: P) { let message = message.as_ref(); self.term .write_fmt(format_args!("{}\n", style(message).yellow().bold())) .ok(); } #[allow(dead_code)] fn print_info>(&mut self, message: P) { let message = message.as_ref(); self.term .write_fmt(format_args!("{}\n", style(message).blue().bold())) .ok(); } fn print_result>(&mut self, key: P, result: &StepResult) { let key = key.as_ref(); self.term .write_fmt(format_args!( "{}: {}\n", key, match result { StepResult::Success => format!("{}", style("OK").bold().green()), StepResult::Failure => format!("{}", style("FAILED").bold().red()), StepResult::Ignored => format!("{}", style("IGNORED").bold().yellow()), StepResult::Skipped(reason) => format!("{}: {}", style("SKIPPED").bold().blue(), reason), } )) .ok(); } #[allow(dead_code)] fn prompt_yesno(&mut self, question: &str) -> Result { self.term .write_fmt(format_args!( "{}", style(format!("{} (y)es/(N)o", question,)).yellow().bold() )) .ok(); loop { match self.term.read_char()? { 'y' | 'Y' => break Ok(true), 'n' | 'N' | '\r' | '\n' => break Ok(false), _ => (), } } } #[allow(unused_variables)] fn should_retry(&mut self, interrupted: bool, step_name: &str) -> Result { if self.width.is_none() { return Ok(false); } if self.set_title { self.term.set_title("Topgrade - Awaiting user"); } self.notify_desktop(format!("{} failed", step_name), None); self.term .write_fmt(format_args!( "\n{}", style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix)) .yellow() .bold() )) .ok(); let answer = loop { match self.term.read_key() { Ok(Key::Char('y')) | Ok(Key::Char('Y')) => break Ok(true), Ok(Key::Char('s')) | Ok(Key::Char('S')) => { println!("\n\nDropping you to shell. Fix what you need and then exit the shell.\n"); run_shell(); break Ok(true); } Ok(Key::Char('n')) | Ok(Key::Char('N')) | Ok(Key::Enter) => break Ok(false), Err(e) => { error!("Error reading from terminal: {}", e); break Ok(false); } Ok(Key::Char('q')) | Ok(Key::Char('Q')) => return Err(io::Error::from(io::ErrorKind::Interrupted)), _ => (), } }; self.term.write_str("\n").ok(); answer } fn get_char(&self) -> Result { self.term.read_key() } } impl Default for Terminal { fn default() -> Self { Self::new() } } pub fn should_retry(interrupted: bool, step_name: &str) -> Result { TERMINAL.lock().unwrap().should_retry(interrupted, step_name) } pub fn print_separator>(message: P) { TERMINAL.lock().unwrap().print_separator(message) } #[allow(dead_code)] pub fn print_warning>(message: P) { TERMINAL.lock().unwrap().print_warning(message) } #[allow(dead_code)] pub fn print_info>(message: P) { TERMINAL.lock().unwrap().print_info(message) } pub fn print_result>(key: P, result: &StepResult) { TERMINAL.lock().unwrap().print_result(key, result) } /// Tells whether the terminal is dumb. pub fn is_dumb() -> bool { TERMINAL.lock().unwrap().width.is_none() } pub fn get_key() -> Result { TERMINAL.lock().unwrap().get_char() } pub fn set_title(set_title: bool) { TERMINAL.lock().unwrap().set_title(set_title); } pub fn set_desktop_notifications(desktop_notifications: bool) { TERMINAL .lock() .unwrap() .set_desktop_notifications(desktop_notifications); } #[allow(dead_code)] pub fn prompt_yesno(question: &str) -> Result { TERMINAL.lock().unwrap().prompt_yesno(question) } pub fn notify_desktop>(message: P, timeout: Option) { TERMINAL.lock().unwrap().notify_desktop(message, timeout) } pub fn display_time(display_time: bool) { TERMINAL.lock().unwrap().display_time(display_time); }