crossterm/terminal/sys/
unix.rs

1//! UNIX related logic for terminal manipulation.
2
3use crate::terminal::{
4    sys::file_descriptor::{tty_fd, FileDesc},
5    WindowSize,
6};
7use libc::{
8    cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW,
9    TIOCGWINSZ,
10};
11use parking_lot::Mutex;
12use std::fs::File;
13
14use std::os::unix::io::{IntoRawFd, RawFd};
15
16use std::{io, mem, process};
17
18// Some(Termios) -> we're in the raw mode and this is the previous mode
19// None -> we're not in the raw mode
20static TERMINAL_MODE_PRIOR_RAW_MODE: Mutex<Option<Termios>> = parking_lot::const_mutex(None);
21
22pub(crate) fn is_raw_mode_enabled() -> bool {
23    TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some()
24}
25
26impl From<winsize> for WindowSize {
27    fn from(size: winsize) -> WindowSize {
28        WindowSize {
29            columns: size.ws_col,
30            rows: size.ws_row,
31            width: size.ws_xpixel,
32            height: size.ws_ypixel,
33        }
34    }
35}
36
37#[allow(clippy::useless_conversion)]
38pub(crate) fn window_size() -> io::Result<WindowSize> {
39    // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
40    let mut size = winsize {
41        ws_row: 0,
42        ws_col: 0,
43        ws_xpixel: 0,
44        ws_ypixel: 0,
45    };
46
47    let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true)));
48    let fd = if let Ok(file) = &file {
49        file.raw_fd()
50    } else {
51        // Fallback to libc::STDOUT_FILENO if /dev/tty is missing
52        STDOUT_FILENO
53    };
54
55    if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() {
56        return Ok(size.into());
57    }
58
59    Err(std::io::Error::last_os_error().into())
60}
61
62#[allow(clippy::useless_conversion)]
63pub(crate) fn size() -> io::Result<(u16, u16)> {
64    if let Ok(window_size) = window_size() {
65        return Ok((window_size.columns, window_size.rows));
66    }
67
68    tput_size().ok_or_else(|| std::io::Error::last_os_error().into())
69}
70
71pub(crate) fn enable_raw_mode() -> io::Result<()> {
72    let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
73
74    if original_mode.is_some() {
75        return Ok(());
76    }
77
78    let tty = tty_fd()?;
79    let fd = tty.raw_fd();
80    let mut ios = get_terminal_attr(fd)?;
81    let original_mode_ios = ios;
82
83    raw_terminal_attr(&mut ios);
84    set_terminal_attr(fd, &ios)?;
85
86    // Keep it last - set the original mode only if we were able to switch to the raw mode
87    *original_mode = Some(original_mode_ios);
88
89    Ok(())
90}
91
92/// Reset the raw mode.
93///
94/// More precisely, reset the whole termios mode to what it was before the first call
95/// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's
96/// effectively disabling the raw mode and doing nothing else.
97pub(crate) fn disable_raw_mode() -> io::Result<()> {
98    let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
99
100    if let Some(original_mode_ios) = original_mode.as_ref() {
101        let tty = tty_fd()?;
102        set_terminal_attr(tty.raw_fd(), original_mode_ios)?;
103        // Keep it last - remove the original mode only if we were able to switch back
104        *original_mode = None;
105    }
106
107    Ok(())
108}
109
110/// Queries the terminal's support for progressive keyboard enhancement.
111///
112/// On unix systems, this function will block and possibly time out while
113/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
114#[cfg(feature = "events")]
115pub fn supports_keyboard_enhancement() -> io::Result<bool> {
116    if is_raw_mode_enabled() {
117        read_supports_keyboard_enhancement_raw()
118    } else {
119        read_supports_keyboard_enhancement_flags()
120    }
121}
122
123#[cfg(feature = "events")]
124fn read_supports_keyboard_enhancement_flags() -> io::Result<bool> {
125    enable_raw_mode()?;
126    let flags = read_supports_keyboard_enhancement_raw();
127    disable_raw_mode()?;
128    flags
129}
130
131#[cfg(feature = "events")]
132fn read_supports_keyboard_enhancement_raw() -> io::Result<bool> {
133    use crate::event::{
134        filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter},
135        poll_internal, read_internal, InternalEvent,
136    };
137    use std::io::Write;
138    use std::time::Duration;
139
140    // This is the recommended method for testing support for the keyboard enhancement protocol.
141    // We send a query for the flags supported by the terminal and then the primary device attributes
142    // query. If we receive the primary device attributes response but not the keyboard enhancement
143    // flags, none of the flags are supported.
144    //
145    // See <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol>
146
147    // ESC [ ? u        Query progressive keyboard enhancement flags (kitty protocol).
148    // ESC [ c          Query primary device attributes.
149    const QUERY: &[u8] = b"\x1B[?u\x1B[c";
150
151    let result = File::open("/dev/tty").and_then(|mut file| {
152        file.write_all(QUERY)?;
153        file.flush()
154    });
155    if result.is_err() {
156        let mut stdout = io::stdout();
157        stdout.write_all(QUERY)?;
158        stdout.flush()?;
159    }
160
161    loop {
162        match poll_internal(
163            Some(Duration::from_millis(2000)),
164            &KeyboardEnhancementFlagsFilter,
165        ) {
166            Ok(true) => {
167                match read_internal(&KeyboardEnhancementFlagsFilter) {
168                    Ok(InternalEvent::KeyboardEnhancementFlags(_current_flags)) => {
169                        // Flush the PrimaryDeviceAttributes out of the event queue.
170                        read_internal(&PrimaryDeviceAttributesFilter).ok();
171                        return Ok(true);
172                    }
173                    _ => return Ok(false),
174                }
175            }
176            Ok(false) => {
177                return Err(io::Error::new(
178                    io::ErrorKind::Other,
179                    "The keyboard enhancement status could not be read within a normal duration",
180                ));
181            }
182            Err(_) => {}
183        }
184    }
185}
186
187/// execute tput with the given argument and parse
188/// the output as a u16.
189///
190/// The arg should be "cols" or "lines"
191fn tput_value(arg: &str) -> Option<u16> {
192    let output = process::Command::new("tput").arg(arg).output().ok()?;
193    let value = output
194        .stdout
195        .into_iter()
196        .filter_map(|b| char::from(b).to_digit(10))
197        .fold(0, |v, n| v * 10 + n as u16);
198
199    if value > 0 {
200        Some(value)
201    } else {
202        None
203    }
204}
205
206/// Returns the size of the screen as determined by tput.
207///
208/// This alternate way of computing the size is useful
209/// when in a subshell.
210fn tput_size() -> Option<(u16, u16)> {
211    match (tput_value("cols"), tput_value("lines")) {
212        (Some(w), Some(h)) => Some((w, h)),
213        _ => None,
214    }
215}
216
217// Transform the given mode into an raw mode (non-canonical) mode.
218fn raw_terminal_attr(termios: &mut Termios) {
219    unsafe { cfmakeraw(termios) }
220}
221
222fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> {
223    unsafe {
224        let mut termios = mem::zeroed();
225        wrap_with_result(tcgetattr(fd, &mut termios))?;
226        Ok(termios)
227    }
228}
229
230fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> {
231    wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) })
232}
233
234fn wrap_with_result(result: i32) -> io::Result<()> {
235    if result == -1 {
236        Err(io::Error::last_os_error())
237    } else {
238        Ok(())
239    }
240}