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