crossterm/terminal/sys/
unix.rs

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