1#[cfg(any(unix, windows))]
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::{io, time::Duration};
11
12#[cfg(any(unix, windows))]
14static OWNED: AtomicBool = AtomicBool::new(false);
15
16#[cfg(any(unix, windows))]
18#[derive(Debug)]
19struct Ownership;
20
21#[cfg(any(unix, windows))]
22impl Ownership {
23 fn acquire() -> io::Result<Self> {
24 OWNED
25 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
26 .map(|_| Self)
27 .map_err(|_| {
28 io::Error::new(io::ErrorKind::AlreadyExists, "the process tty is already in use")
29 })
30 }
31}
32
33#[cfg(any(unix, windows))]
34impl Drop for Ownership {
35 fn drop(&mut self) {
36 OWNED.store(false, Ordering::Release);
37 }
38}
39
40pub trait Terminal {
42 fn write(&mut self, bytes: &[u8]) -> io::Result<()>;
47
48 fn read(&mut self, buf: &mut [u8], timeout: Option<Duration>) -> io::Result<Option<usize>>;
55
56 fn restore(&mut self) -> io::Result<()> {
62 Ok(())
63 }
64
65 fn modes(&mut self, _paste: bool, _kitty: bool) {}
68}
69
70#[cfg(any(unix, windows))]
72pub fn is_tty() -> bool {
73 sys::is_tty()
74}
75
76#[cfg(any(unix, windows))]
78pub fn stdout_is_tty() -> bool {
79 sys::stdout_is_tty()
80}
81
82#[cfg(any(unix, windows))]
84pub fn columns() -> Option<u16> {
85 sys::columns()
86}
87
88#[cfg(any(unix, windows))]
91#[derive(Debug)]
92pub struct Tty {
93 inner: sys::Raw,
95 restored: bool,
97 ownership: Option<Ownership>,
99}
100
101#[cfg(any(unix, windows))]
102impl Tty {
103 pub fn open() -> io::Result<Self> {
109 let ownership = Ownership::acquire()?;
110 Ok(Self { inner: sys::Raw::enter()?, restored: false, ownership: Some(ownership) })
111 }
112}
113
114#[cfg(any(unix, windows))]
115impl Terminal for Tty {
116 fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
117 use io::Write as _;
118 let mut out = io::stdout().lock();
119 out.write_all(bytes)?;
120 out.flush()
121 }
122
123 fn read(&mut self, buf: &mut [u8], timeout: Option<Duration>) -> io::Result<Option<usize>> {
124 self.inner.read(buf, timeout)
125 }
126
127 fn restore(&mut self) -> io::Result<()> {
128 if self.restored {
129 return Ok(());
130 }
131 self.restored = true;
132 let result = self.inner.leave();
133 self.ownership.take();
134 result
135 }
136
137 fn modes(&mut self, paste: bool, kitty: bool) {
138 sys::set_modes(paste, kitty);
139 }
140}
141
142#[cfg(any(unix, windows))]
143impl Drop for Tty {
144 fn drop(&mut self) {
145 if let Err(err) = self.restore() {
146 tracing::warn!(%err, "could not restore the terminal");
147 }
148 }
149}
150
151#[cfg(all(test, any(unix, windows)))]
152mod tests {
153 use super::Ownership;
154
155 #[test]
156 fn tty_ownership_is_exclusive_and_released_by_drop() {
157 let first = Ownership::acquire().expect("first owner");
158 assert!(Ownership::acquire().is_err());
159 drop(first);
160 assert!(Ownership::acquire().is_ok());
161 }
162}
163
164pub(crate) const fn mode_reset(paste: bool, kitty: bool) -> &'static [u8] {
166 match (paste, kitty) {
167 (true, true) => b"\x1b[<u\x1b[?2004l",
168 (false, true) => b"\x1b[<u",
169 (true, false) => b"\x1b[?2004l",
170 (false, false) => b"",
171 }
172}
173
174#[cfg(unix)]
175mod sys {
176 use std::{
177 cell::UnsafeCell,
178 io,
179 mem::MaybeUninit,
180 sync::atomic::{AtomicBool, AtomicU8, Ordering},
181 time::Duration,
182 };
183
184 struct Saved(UnsafeCell<MaybeUninit<libc::termios>>);
186
187 unsafe impl Sync for Saved {}
190
191 static SAVED: Saved = Saved(UnsafeCell::new(MaybeUninit::uninit()));
193 static ARMED: AtomicBool = AtomicBool::new(false);
195 static MODES: AtomicU8 = AtomicU8::new(0);
197
198 const SIGNALS: [libc::c_int; 4] = [libc::SIGTERM, libc::SIGHUP, libc::SIGINT, libc::SIGQUIT];
200
201 #[derive(Debug)]
203 pub(super) struct Raw {
204 saved: libc::termios,
206 previous: Vec<(libc::c_int, libc::sigaction)>,
208 }
209
210 pub(super) fn is_tty() -> bool {
211 unsafe { libc::isatty(0) == 1 && libc::isatty(1) == 1 }
213 }
214
215 pub(super) fn stdout_is_tty() -> bool {
216 unsafe { libc::isatty(1) == 1 }
218 }
219
220 pub(super) fn columns() -> Option<u16> {
221 let mut size = MaybeUninit::<libc::winsize>::zeroed();
222 let ok = unsafe { libc::ioctl(1, libc::TIOCGWINSZ, size.as_mut_ptr()) } == 0;
224 let size = unsafe { size.assume_init() };
226 (ok && size.ws_col > 0).then_some(size.ws_col)
227 }
228
229 pub(super) fn set_modes(paste: bool, kitty: bool) {
230 MODES.store(u8::from(paste) | (u8::from(kitty) << 1), Ordering::Release);
231 }
232
233 extern "C" fn on_signal(sig: libc::c_int) {
235 if ARMED.load(Ordering::Acquire) {
236 let modes = MODES.load(Ordering::Acquire);
237 let reset = super::mode_reset(modes & 1 != 0, modes & 2 != 0);
238 unsafe {
241 libc::write(1, reset.as_ptr().cast(), reset.len());
242 libc::tcsetattr(0, libc::TCSANOW, (*SAVED.0.get()).as_ptr());
243 }
244 }
245 unsafe {
247 libc::signal(sig, libc::SIG_DFL);
248 libc::raise(sig);
249 }
250 }
251
252 impl Raw {
253 pub(super) fn enter() -> io::Result<Self> {
254 let mut termios = MaybeUninit::<libc::termios>::uninit();
255 if unsafe { libc::tcgetattr(0, termios.as_mut_ptr()) } != 0 {
257 return Err(io::Error::last_os_error());
258 }
259 let saved = unsafe { termios.assume_init() };
261 let mut raw = saved;
262 raw.c_iflag &= !(libc::IGNBRK
263 | libc::BRKINT
264 | libc::PARMRK
265 | libc::ISTRIP
266 | libc::INLCR
267 | libc::IGNCR
268 | libc::ICRNL
269 | libc::IXON);
270 raw.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
271 raw.c_cflag &= !(libc::CSIZE | libc::PARENB);
272 raw.c_cflag |= libc::CS8;
273 raw.c_cc[libc::VMIN] = 1;
274 raw.c_cc[libc::VTIME] = 0;
275 unsafe { (*SAVED.0.get()).write(saved) };
277 ARMED.store(true, Ordering::Release);
278 let mut previous = Vec::with_capacity(SIGNALS.len());
279 for sig in SIGNALS {
280 unsafe {
282 let mut action: libc::sigaction = std::mem::zeroed();
283 action.sa_sigaction = on_signal as extern "C" fn(libc::c_int) as usize;
284 libc::sigemptyset(&mut action.sa_mask);
285 let mut old: libc::sigaction = std::mem::zeroed();
286 if libc::sigaction(sig, &action, &mut old) != 0 {
287 let err = io::Error::last_os_error();
288 let mut state = Self { saved, previous };
289 let _ = state.leave();
290 return Err(err);
291 }
292 previous.push((sig, old));
293 }
294 }
295 let mut state = Self { saved, previous };
296 if unsafe { libc::tcsetattr(0, libc::TCSANOW, &raw) } != 0 {
298 let err = io::Error::last_os_error();
299 let _ = state.leave();
300 return Err(err);
301 }
302 Ok(state)
303 }
304
305 #[allow(clippy::unused_self, reason = "the Windows version reads through its handle")]
306 pub(super) fn read(
307 &self,
308 buf: &mut [u8],
309 timeout: Option<Duration>,
310 ) -> io::Result<Option<usize>> {
311 let ms = timeout
312 .map_or(-1, |t| libc::c_int::try_from(t.as_millis()).unwrap_or(libc::c_int::MAX));
313 let mut fd = libc::pollfd { fd: 0, events: libc::POLLIN, revents: 0 };
314 let n = unsafe { libc::poll(&mut fd, 1, ms) };
316 if n < 0 {
317 let err = io::Error::last_os_error();
318 return if err.kind() == io::ErrorKind::Interrupted {
319 Ok(None)
320 } else {
321 Err(err)
322 };
323 }
324 if n == 0 {
325 return Ok(None);
326 }
327 let got = unsafe { libc::read(0, buf.as_mut_ptr().cast(), buf.len()) };
329 if got < 0 {
330 let err = io::Error::last_os_error();
331 return match err.kind() {
332 io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock => Ok(None),
333 _ => Err(err),
334 };
335 }
336 Ok(Some(got as usize))
337 }
338
339 pub(super) fn leave(&mut self) -> io::Result<()> {
340 let result = if unsafe { libc::tcsetattr(0, libc::TCSANOW, &self.saved) } == 0 {
342 Ok(())
343 } else {
344 Err(io::Error::last_os_error())
345 };
346 ARMED.store(false, Ordering::Release);
347 MODES.store(0, Ordering::Release);
348 for (sig, old) in self.previous.drain(..) {
349 unsafe { libc::sigaction(sig, &old, std::ptr::null_mut()) };
351 }
352 result
353 }
354 }
355}
356
357#[cfg(windows)]
358mod sys {
359 use std::{io, time::Duration};
360
361 use windows_sys::Win32::{
362 Foundation::{HANDLE, WAIT_OBJECT_0},
363 Storage::FileSystem::ReadFile,
364 System::{
365 Console::{
366 CONSOLE_SCREEN_BUFFER_INFO, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT,
367 ENABLE_PROCESSED_INPUT, ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_INPUT,
368 ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode, GetConsoleScreenBufferInfo,
369 GetStdHandle, INPUT_RECORD, KEY_EVENT, PeekConsoleInputW, ReadConsoleInputW,
370 STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, SetConsoleMode,
371 },
372 Threading::{INFINITE, WaitForSingleObject},
373 },
374 };
375
376 #[derive(Debug)]
378 pub(super) struct Raw {
379 input: HANDLE,
381 output: HANDLE,
383 in_mode: u32,
385 out_mode: u32,
387 }
388
389 fn mode(handle: HANDLE) -> Option<u32> {
391 let mut mode = 0;
392 (unsafe { GetConsoleMode(handle, &mut mode) } != 0).then_some(mode)
394 }
395
396 pub(super) fn is_tty() -> bool {
397 let (input, output) =
399 unsafe { (GetStdHandle(STD_INPUT_HANDLE), GetStdHandle(STD_OUTPUT_HANDLE)) };
400 mode(input).is_some() && mode(output).is_some()
401 }
402
403 pub(super) fn stdout_is_tty() -> bool {
404 mode(unsafe { GetStdHandle(STD_OUTPUT_HANDLE) }).is_some()
406 }
407
408 pub(super) fn columns() -> Option<u16> {
409 unsafe {
411 let mut info: CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed();
412 if GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &mut info) == 0 {
413 return None;
414 }
415 u16::try_from(info.srWindow.Right - info.srWindow.Left + 1).ok()
416 }
417 }
418
419 pub(super) const fn set_modes(_paste: bool, _kitty: bool) {}
420
421 impl Raw {
422 pub(super) fn enter() -> io::Result<Self> {
423 let (input, output) =
425 unsafe { (GetStdHandle(STD_INPUT_HANDLE), GetStdHandle(STD_OUTPUT_HANDLE)) };
426 let in_mode = mode(input).ok_or_else(io::Error::last_os_error)?;
427 let out_mode = mode(output).ok_or_else(io::Error::last_os_error)?;
428 let raw_in = (in_mode & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT))
429 | ENABLE_VIRTUAL_TERMINAL_INPUT;
430 let raw_out = out_mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
431 unsafe {
433 if SetConsoleMode(input, raw_in) == 0 {
434 return Err(io::Error::last_os_error());
435 }
436 if SetConsoleMode(output, raw_out) == 0 {
437 let err = io::Error::last_os_error();
438 SetConsoleMode(input, in_mode);
439 return Err(err);
440 }
441 }
442 Ok(Self { input, output, in_mode, out_mode })
443 }
444
445 fn key_ready(&self) -> io::Result<bool> {
448 loop {
449 let mut record = INPUT_RECORD::default();
450 let mut n = 0;
451 if unsafe { PeekConsoleInputW(self.input, &mut record, 1, &mut n) } == 0 {
453 return Err(io::Error::last_os_error());
454 }
455 if n == 0 {
456 return Ok(false);
457 }
458 let key_down = u32::from(record.EventType) == KEY_EVENT
460 && unsafe {
461 let key = record.Event.KeyEvent;
462 key.bKeyDown != 0
463 && (key.uChar.UnicodeChar != 0
464 || matches!(key.wVirtualKeyCode,
465 0x08 | 0x09 | 0x0d | 0x1b | 0x20..=0x28 | 0x2d | 0x2e | 0x70..=0x87))
466 };
467 if key_down {
468 return Ok(true);
469 }
470 if unsafe { ReadConsoleInputW(self.input, &mut record, 1, &mut n) } == 0 {
472 return Err(io::Error::last_os_error());
473 }
474 }
475 }
476
477 pub(super) fn read(
478 &self,
479 buf: &mut [u8],
480 timeout: Option<Duration>,
481 ) -> io::Result<Option<usize>> {
482 let ms =
483 timeout.map_or(INFINITE, |t| u32::try_from(t.as_millis()).unwrap_or(INFINITE - 1));
484 if unsafe { WaitForSingleObject(self.input, ms) } != WAIT_OBJECT_0 {
486 return Ok(None);
487 }
488 if !self.key_ready()? {
489 return Ok(None);
490 }
491 let mut got = 0;
492 let len = u32::try_from(buf.len()).unwrap_or(u32::MAX);
493 if unsafe { ReadFile(self.input, buf.as_mut_ptr(), len, &mut got, std::ptr::null_mut()) }
495 == 0
496 {
497 return Err(io::Error::last_os_error());
498 }
499 Ok(Some(got as usize))
500 }
501
502 pub(super) fn leave(&mut self) -> io::Result<()> {
503 unsafe {
505 let input = SetConsoleMode(self.input, self.in_mode);
506 let input_error = (input == 0).then(io::Error::last_os_error);
507 let output = SetConsoleMode(self.output, self.out_mode);
508 if let Some(err) = input_error {
509 return Err(err);
510 }
511 if output == 0 {
512 return Err(io::Error::last_os_error());
513 }
514 }
515 Ok(())
516 }
517 }
518}