crossterm/terminal/sys/
unix.rs1use 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
26static 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 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 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 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 *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 *original_mode = Some(original_mode_ios);
137 Ok(())
138}
139
140#[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 *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 *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#[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 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 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
258fn 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
277fn 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")]
289fn 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}