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(val: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 = winsize {
41 ws_row: 0,
42 ws_col: 0,
43 ws_xpixel: 0,
44 ws_ypixel: 0,
45 };
46
47 let file: Result = File::open("/dev/tty").map(|file: File| (FileDesc::new(file.into_raw_fd(), close_on_drop:true)));
48 let fd: i32 = if let Ok(file: &FileDesc) = &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: WindowSize) = 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: MutexGuard<'_, RawMutex, …> = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
73
74 if original_mode.is_some() {
75 return Ok(());
76 }
77
78 let tty: FileDesc = tty_fd()?;
79 let fd: i32 = tty.raw_fd();
80 let mut ios: termios = get_terminal_attr(fd)?;
81 let original_mode_ios: termios = 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: MutexGuard<'_, RawMutex, …> = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
99
100 if let Some(original_mode_ios: &termios) = original_mode.as_ref() {
101 let tty: FileDesc = tty_fd()?;
102 set_terminal_attr(tty.raw_fd(), termios: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: Result = 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: Output = process::Command::new(program:"tput").arg(arg).output().ok()?;
193 let value: u16 = output
194 .stdout
195 .into_iter()
196 .filter_map(|b| char::from(b).to_digit(10))
197 .fold(init:0, |v: u16, n: u32| 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(arg:"cols"), tput_value(arg:"lines")) {
212 (Some(w: u16), Some(h: u16)) => 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: 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}
241