//! The 'terminal' module provides a Terminal type that acts as a guard around changes to terminal //! settings, resetting them to their original state when the Terminal is dropped. use super::get_winsize; use libc::{STDIN_FILENO, STDOUT_FILENO}; use log::{debug, warn}; use model::exec::TtyInit; use nix::{ sys::termios::{cfmakeraw, tcgetattr, tcsetattr, SetArg, Termios}, unistd::isatty, }; use snafu::ResultExt; /// The Terminal type acts as a guard around changes to terminal settings, resetting them to their /// original state when the Terminal is dropped. #[derive(Debug)] pub(crate) struct Terminal { /// If the user requested a TTY or we detected one, this will contain a TtyInit that represents /// the desired initial state of the TTY. tty: Option<TtyInit>, /// Represents the original terminal settings we found when we were created, so they can be /// restored when we're dropped. orig_termios: Option<Termios>, } impl Terminal { /// Parameters: /// * tty: Represents the user's desire for a TTY, where `Some(true)` means to use a TTY, /// `Some(false)` means not to use a TTY, and `None` means to detect whether we think we should /// use a TTY. /// /// For the purposes of terminal settings, "use a TTY" means to set the terminal to /// raw mode so that input is read directly, not interpreted; for example, things like ctrl-c /// will no longer generate a signal, so they can be passed on exactly as received. /// /// We detect a TTY by checking whether stdin *and* stdout are linked to a terminal device, /// which seems to surprise the fewest number of users. pub(crate) fn new(tty: Option<bool>) -> Result<Self> { let is_tty = match tty { Some(true) => true, Some(false) => false, None => { let stdin_tty = isatty(STDIN_FILENO) == Ok(true); let stdout_tty = isatty(STDOUT_FILENO) == Ok(true); let is_tty = stdin_tty && stdout_tty; debug!("Detected tty: {}", is_tty); is_tty } }; let mut tty = None; let mut orig_termios = None; if is_tty { // We want any new TTY to match the size of our current terminal. tty = Some(TtyInit { size: get_winsize(STDOUT_FILENO), }); // Get the current settings of the user's terminal so we can restore them later. let current_termios = tcgetattr(STDOUT_FILENO).context(error::TermAttrSnafu { op: "get" })?; debug!("Setting terminal to raw mode, sorry about the carriage returns"); let mut new_termios = current_termios.clone(); // Set to raw mode, sushi-grade. ctrl-c, ctrl-z, etc. will no longer generate local // signals so they can be passed on unchanged and you can interact with remote programs // as expected. cfmakeraw(&mut new_termios); // We make the change 'NOW' because we don't expect any input/output yet, and so should // have nothing to FLUSH. tcsetattr(STDOUT_FILENO, SetArg::TCSANOW, &new_termios) .context(error::TermAttrSnafu { op: "set" })?; orig_termios = Some(current_termios); } Ok(Self { tty, orig_termios }) } pub(crate) fn tty(&self) -> &Option<TtyInit> { &self.tty } } impl Drop for Terminal { /// Restore the user's original terminal settings on drop. fn drop(&mut self) { if let Some(orig_termios) = &self.orig_termios { // We shouldn't fail to reset unless stdout was closed somehow, and there's not much we // can do about cleaning it up then. if tcsetattr(STDOUT_FILENO, SetArg::TCSANOW, orig_termios).is_err() { warn!("Failed to clean up terminal :("); } } } } pub(crate) mod error { use snafu::Snafu; #[derive(Debug, Snafu)] #[snafu(visibility(pub(super)))] pub enum Error { #[snafu(display("Have TTY, but failed to {} terminal attributes: {}", op, source))] TermAttr { op: String, source: nix::Error }, } } pub(crate) use error::Error; type Result<T> = std::result::Result<T, error::Error>;