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(val: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: Result, Error> = File::open("/dev/tty").map(|file: File| (FileDesc::Owned(file.into())));
88 let fd: BorrowedFd<'static> = if let Ok(file: &FileDesc<'_>) = &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: Winsize = 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: WindowSize) = 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: MutexGuard<'_, RawMutex, …> = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
128 if original_mode.is_some() {
129 return Ok(());
130 }
131
132 let tty: FileDesc<'static> = tty_fd()?;
133 let mut ios: Termios = get_terminal_attr(&tty)?;
134 let original_mode_ios: Termios = 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: MutexGuard<'_, RawMutex, …> = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
162 if let Some(original_mode_ios: &Termios) = original_mode.as_ref() {
163 let tty: FileDesc<'static> = tty_fd()?;
164 set_terminal_attr(&tty, termios: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: Termios = 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: Option| 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: Result, …> = 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: Output = process::Command::new(program:"tput").arg(arg).output().ok()?;
275 let value: u16 = output
276 .stdout
277 .into_iter()
278 .filter_map(|b| char::from(b).to_digit(10))
279 .fold(init:0, |v: u16, n: u32| 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(arg:"cols"), tput_value(arg:"lines")) {
294 (Some(w: u16), Some(h: u16)) => 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}
327