//! UNIX related logic for terminal manipulation. use crate::terminal::{ sys::file_descriptor::{tty_fd, FileDesc}, WindowSize, }; use libc::{ cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW, TIOCGWINSZ, }; use parking_lot::Mutex; use std::fs::File; use std::os::unix::io::{IntoRawFd, RawFd}; use std::{io, mem, process}; // Some(Termios) -> we're in the raw mode and this is the previous mode // None -> we're not in the raw mode static TERMINAL_MODE_PRIOR_RAW_MODE: Mutex> = parking_lot::const_mutex(None); pub(crate) fn is_raw_mode_enabled() -> bool { TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some() } impl From for WindowSize { fn from(size: winsize) -> WindowSize { WindowSize { columns: size.ws_col, rows: size.ws_row, width: size.ws_xpixel, height: size.ws_ypixel, } } } pub(crate) fn fd_window_size(fd: i32) -> io::Result { let mut size = winsize { ws_row: 0, ws_col: 0, ws_xpixel: 0, ws_ypixel: 0, }; #[allow(clippy::useless_conversion)] if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { return Ok(size.into()); } Err(std::io::Error::last_os_error()) } #[allow(clippy::useless_conversion)] pub(crate) fn window_size() -> io::Result { // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc let mut size = winsize { ws_row: 0, ws_col: 0, ws_xpixel: 0, ws_ypixel: 0, }; let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true))); let fd = if let Ok(file) = &file { file.raw_fd() } else { // Fallback to libc::STDOUT_FILENO if /dev/tty is missing STDOUT_FILENO }; if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { return Ok(size.into()); } Err(std::io::Error::last_os_error().into()) } pub(crate) fn fd_size(fd: i32) -> io::Result<(u16, u16)> { fd_window_size(fd).map(|WindowSize { rows, columns, .. }| (columns, rows)) } #[allow(clippy::useless_conversion)] pub(crate) fn size() -> io::Result<(u16, u16)> { if let Ok(window_size) = window_size() { return Ok((window_size.columns, window_size.rows)); } tput_size().ok_or_else(|| std::io::Error::last_os_error().into()) } pub(crate) fn fd_enable_raw_mode(fd: i32) -> io::Result { let mut ios = get_terminal_attr(fd)?; let original_mode_ios = ios; raw_terminal_attr(&mut ios); set_terminal_attr(fd, &ios)?; Ok(original_mode_ios) } pub(crate) fn enable_raw_mode() -> io::Result<()> { let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); if original_mode.is_some() { return Ok(()); } let tty = tty_fd()?; let fd = tty.raw_fd(); let mut ios = get_terminal_attr(fd)?; let original_mode_ios = ios; raw_terminal_attr(&mut ios); set_terminal_attr(fd, &ios)?; // Keep it last - set the original mode only if we were able to switch to the raw mode *original_mode = Some(original_mode_ios); Ok(()) } pub(crate) fn fd_disable_raw_mode(fd: i32, termios: Termios) -> io::Result<()> { set_terminal_attr(fd, &termios)?; Ok(()) } /// Reset the raw mode. /// /// More precisely, reset the whole termios mode to what it was before the first call /// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's /// effectively disabling the raw mode and doing nothing else. pub(crate) fn disable_raw_mode() -> io::Result<()> { let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); if let Some(original_mode_ios) = original_mode.as_ref() { let tty = tty_fd()?; set_terminal_attr(tty.raw_fd(), original_mode_ios)?; // Keep it last - remove the original mode only if we were able to switch back *original_mode = None; } Ok(()) } /// Queries the terminal's support for progressive keyboard enhancement. /// /// On unix systems, this function will block and possibly time out while /// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called. #[cfg(feature = "events")] pub fn supports_keyboard_enhancement() -> io::Result { if is_raw_mode_enabled() { read_supports_keyboard_enhancement_raw() } else { read_supports_keyboard_enhancement_flags() } } #[cfg(feature = "events")] fn read_supports_keyboard_enhancement_flags() -> io::Result { enable_raw_mode()?; let flags = read_supports_keyboard_enhancement_raw(); disable_raw_mode()?; flags } #[cfg(feature = "events")] fn read_supports_keyboard_enhancement_raw() -> io::Result { use crate::event::{ filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter}, poll_internal, read_internal, InternalEvent, }; use std::io::Write; use std::time::Duration; // This is the recommended method for testing support for the keyboard enhancement protocol. // We send a query for the flags supported by the terminal and then the primary device attributes // query. If we receive the primary device attributes response but not the keyboard enhancement // flags, none of the flags are supported. // // See // ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol). // ESC [ c Query primary device attributes. const QUERY: &[u8] = b"\x1B[?u\x1B[c"; let result = File::open("/dev/tty").and_then(|mut file| { file.write_all(QUERY)?; file.flush() }); if result.is_err() { let mut stdout = io::stdout(); stdout.write_all(QUERY)?; stdout.flush()?; } loop { match poll_internal( Some(Duration::from_millis(2000)), &KeyboardEnhancementFlagsFilter, ) { Ok(true) => { match read_internal(&KeyboardEnhancementFlagsFilter) { Ok(InternalEvent::KeyboardEnhancementFlags(_current_flags)) => { // Flush the PrimaryDeviceAttributes out of the event queue. read_internal(&PrimaryDeviceAttributesFilter).ok(); return Ok(true); } _ => return Ok(false), } } Ok(false) => { return Err(io::Error::new( io::ErrorKind::Other, "The keyboard enhancement status could not be read within a normal duration", )); } Err(_) => {} } } } /// execute tput with the given argument and parse /// the output as a u16. /// /// The arg should be "cols" or "lines" fn tput_value(arg: &str) -> Option { let output = process::Command::new("tput").arg(arg).output().ok()?; let value = output .stdout .into_iter() .filter_map(|b| char::from(b).to_digit(10)) .fold(0, |v, n| v * 10 + n as u16); if value > 0 { Some(value) } else { None } } /// Returns the size of the screen as determined by tput. /// /// This alternate way of computing the size is useful /// when in a subshell. fn tput_size() -> Option<(u16, u16)> { match (tput_value("cols"), tput_value("lines")) { (Some(w), Some(h)) => Some((w, h)), _ => None, } } // Transform the given mode into an raw mode (non-canonical) mode. fn raw_terminal_attr(termios: &mut Termios) { unsafe { cfmakeraw(termios) } } fn get_terminal_attr(fd: RawFd) -> io::Result { unsafe { let mut termios = mem::zeroed(); wrap_with_result(tcgetattr(fd, &mut termios))?; Ok(termios) } } fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> { wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) }) } fn wrap_with_result(result: i32) -> io::Result<()> { if result == -1 { Err(io::Error::last_os_error()) } else { Ok(()) } }