1use crate::errors::DisplayErrorChain;
11use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind};
12use futures::StreamExt;
13use std::sync::{Arc, Mutex};
14use thiserror::Error;
15use tracing::{debug, warn};
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum InputHandlerKind {
23 Standard,
25
26 Noop,
28}
29
30impl InputHandlerKind {
31 pub(crate) fn build(self) -> InputHandler {
32 match self {
33 Self::Standard => InputHandler::new(),
34 Self::Noop => InputHandler::noop(),
35 }
36 }
37}
38
39#[derive(Debug)]
41pub(crate) struct InputHandler {
42 imp: Option<(InputHandlerImpl, EventStream)>,
45}
46
47impl InputHandler {
48 const INFO_CHAR: char = 't';
49
50 pub(crate) fn new() -> Self {
52 if imp::is_foreground_process() {
53 match InputHandlerImpl::new() {
55 Ok(handler) => {
56 let stream = EventStream::new();
57 debug!("enabled terminal non-canonical mode, reading input events");
58 Self {
59 imp: Some((handler, stream)),
60 }
61 }
62 Err(error) => {
63 warn!(
64 "failed to enable terminal non-canonical mode, \
65 cannot read input events: {}",
66 error,
67 );
68 Self::noop()
69 }
70 }
71 } else {
72 debug!(
73 "not reading input because nextest is not \
74 a foreground process in a terminal"
75 );
76 Self::noop()
77 }
78 }
79
80 pub(crate) fn noop() -> Self {
82 Self { imp: None }
83 }
84
85 pub(crate) fn status(&self) -> InputHandlerStatus {
86 if self.imp.is_some() {
87 InputHandlerStatus::Enabled {
88 info_char: Self::INFO_CHAR,
89 }
90 } else {
91 InputHandlerStatus::Disabled
92 }
93 }
94
95 pub(crate) async fn recv(&mut self) -> Option<InputEvent> {
100 let (_, stream) = self.imp.as_mut()?;
101 loop {
102 let next = stream.next().await?;
103 match next {
107 Ok(Event::Key(key)) if key.kind == KeyEventKind::Press => {
108 if key.code == KeyCode::Char(Self::INFO_CHAR) && key.modifiers.is_empty() {
109 return Some(InputEvent::Info);
110 }
111 if key.code == KeyCode::Enter {
112 return Some(InputEvent::Enter);
113 }
114 }
115 Ok(event) => {
116 debug!("unhandled event: {:?}", event);
117 }
118 Err(error) => {
119 warn!("failed to read input event: {}", error);
120 }
121 }
122 }
123 }
124
125 #[cfg(unix)]
130 pub(crate) fn suspend(&mut self) {
131 let Some((handler, _)) = self.imp.as_mut() else {
132 return;
133 };
134
135 if let Err(error) = handler.restore() {
136 warn!("failed to suspend terminal non-canonical mode: {}", error);
137 }
139 }
140
141 #[cfg(unix)]
145 pub(crate) fn resume(&mut self) {
146 let Some((handler, _)) = self.imp.as_mut() else {
147 return;
150 };
151
152 if let Err(error) = handler.reinit() {
153 warn!(
154 "failed to resume terminal non-canonical mode, \
155 cannot read input events: {}",
156 error
157 );
158 self.imp = None;
161 }
162 }
163}
164
165pub enum InputHandlerStatus {
168 Enabled {
170 info_char: char,
172 },
173
174 Disabled,
176}
177
178#[derive(Clone, Debug)]
179struct InputHandlerImpl {
180 guard: Arc<Mutex<imp::InputGuard>>,
183}
184
185impl InputHandlerImpl {
186 fn new() -> Result<Self, InputHandlerCreateError> {
187 let guard = imp::InputGuard::new().map_err(InputHandlerCreateError::EnableNonCanonical)?;
188
189 let ret = Self {
192 guard: Arc::new(Mutex::new(guard)),
193 };
194
195 let ret2 = ret.clone();
196 let panic_hook = std::panic::take_hook();
197 std::panic::set_hook(Box::new(move |info| {
198 if let Err(error) = ret2.restore() {
200 eprintln!(
201 "failed to restore terminal state: {}",
202 DisplayErrorChain::new(error)
203 );
204 }
205 panic_hook(info);
206 }));
207
208 Ok(ret)
209 }
210
211 #[cfg(unix)]
212 fn reinit(&self) -> Result<(), InputHandlerCreateError> {
213 let mut locked = self
225 .guard
226 .lock()
227 .map_err(|_| InputHandlerCreateError::Poisoned)?;
228 let guard = imp::InputGuard::new().map_err(InputHandlerCreateError::EnableNonCanonical)?;
229 *locked = guard;
230 Ok(())
231 }
232
233 fn restore(&self) -> Result<(), InputHandlerFinishError> {
234 let mut locked = self
237 .guard
238 .lock()
239 .map_err(|_| InputHandlerFinishError::Poisoned)?;
240 locked.restore().map_err(InputHandlerFinishError::Restore)
241 }
242}
243
244impl Drop for InputHandlerImpl {
247 fn drop(&mut self) {
248 if let Err(error) = self.restore() {
249 eprintln!(
250 "failed to restore terminal state: {}",
251 DisplayErrorChain::new(error)
252 );
253 }
254 }
255}
256
257#[derive(Debug, Error)]
258enum InputHandlerCreateError {
259 #[error("failed to enable terminal non-canonical mode")]
260 EnableNonCanonical(#[source] imp::Error),
261
262 #[cfg(unix)]
263 #[error("mutex was poisoned while reinitializing terminal state")]
264 Poisoned,
265}
266
267#[derive(Debug, Error)]
268enum InputHandlerFinishError {
269 #[error("mutex was poisoned while restoring terminal state")]
270 Poisoned,
271
272 #[error("failed to restore terminal state")]
273 Restore(#[source] imp::Error),
274}
275
276#[cfg(unix)]
277mod imp {
278 use libc::{ECHO, ICANON, TCSAFLUSH, TCSANOW, VMIN, VTIME, tcgetattr, tcsetattr};
279 use std::{
280 ffi::c_int,
281 io::{self, IsTerminal},
282 mem,
283 os::fd::AsRawFd,
284 };
285 use tracing::debug;
286
287 pub(super) type Error = io::Error;
288
289 pub(super) fn is_foreground_process() -> bool {
290 if !std::io::stdin().is_terminal() {
291 debug!("stdin is not a terminal => is_foreground_process() is false");
292 return false;
293 }
294
295 let pgrp = unsafe { libc::getpgrp() };
300 let tc_pgrp = unsafe { libc::tcgetpgrp(std::io::stdin().as_raw_fd()) };
301 if tc_pgrp == -1 {
302 debug!(
303 "stdin is a terminal, and pgrp = {pgrp}, but tcgetpgrp failed with error {} => \
304 is_foreground_process() is false",
305 io::Error::last_os_error()
306 );
307 return false;
308 }
309 if pgrp != tc_pgrp {
310 debug!(
311 "stdin is a terminal, but pgrp {} != tcgetpgrp {} => is_foreground_process() is false",
312 pgrp, tc_pgrp
313 );
314 return false;
315 }
316
317 debug!(
318 "stdin is a terminal, and pgrp {pgrp} == tcgetpgrp {tc_pgrp} => \
319 is_foreground_process() is true"
320 );
321 true
322 }
323
324 #[derive(Clone, Debug)]
331 pub(super) struct InputGuard {
332 original: Option<libc::termios>,
339 }
340
341 impl InputGuard {
342 pub(super) fn new() -> io::Result<Self> {
343 let TermiosPair { original, updated } = compute_termios()?;
344 stdin_tcsetattr(TCSAFLUSH, &updated)?;
345
346 unsafe {
355 libc::signal(libc::SIGTTIN, libc::SIG_IGN);
356 libc::signal(libc::SIGTTOU, libc::SIG_IGN);
357 }
358
359 Ok(Self {
360 original: Some(original),
361 })
362 }
363
364 pub(super) fn restore(&mut self) -> io::Result<()> {
365 if let Some(original) = self.original.take() {
366 stdin_tcsetattr(TCSANOW, &original)
370 } else {
371 Ok(())
372 }
373 }
374 }
375
376 fn compute_termios() -> io::Result<TermiosPair> {
377 let mut termios = mem::MaybeUninit::uninit();
378 let res = unsafe { tcgetattr(std::io::stdin().as_raw_fd(), termios.as_mut_ptr()) };
379 if res == -1 {
380 return Err(io::Error::last_os_error());
381 }
382
383 let original = unsafe { termios.assume_init() };
385
386 let mut updated = original;
387
388 updated.c_lflag &= !(ECHO | ICANON);
391 updated.c_cc[VMIN] = 1;
396 updated.c_cc[VTIME] = 0;
397
398 Ok(TermiosPair { original, updated })
399 }
400
401 #[derive(Clone, Debug)]
402 struct TermiosPair {
403 original: libc::termios,
404 updated: libc::termios,
405 }
406
407 fn stdin_tcsetattr(optional_actions: c_int, updated: &libc::termios) -> io::Result<()> {
408 let res = unsafe { tcsetattr(std::io::stdin().as_raw_fd(), optional_actions, updated) };
409 if res == -1 {
410 Err(io::Error::last_os_error())
411 } else {
412 Ok(())
413 }
414 }
415}
416
417#[cfg(windows)]
418mod imp {
419 use std::{
420 io::{self, IsTerminal},
421 os::windows::io::AsRawHandle,
422 };
423 use tracing::debug;
424 use windows_sys::Win32::System::Console::{
425 CONSOLE_MODE, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, GetConsoleMode, SetConsoleMode,
426 };
427
428 pub(super) type Error = io::Error;
429
430 pub(super) fn is_foreground_process() -> bool {
431 if !std::io::stdin().is_terminal() {
438 debug!("stdin is not a terminal => is_foreground_process() is false");
439 return false;
440 }
441
442 debug!("stdin is a terminal => is_foreground_process() is true");
443 true
444 }
445
446 #[derive(Clone, Debug)]
454 pub(super) struct InputGuard {
455 original: Option<CONSOLE_MODE>,
456 }
457
458 impl InputGuard {
459 pub(super) fn new() -> io::Result<Self> {
460 let handle = std::io::stdin().as_raw_handle();
461
462 let mut original: CONSOLE_MODE = 0;
464 let res = unsafe { GetConsoleMode(handle, &mut original) };
465 if res == 0 {
466 return Err(io::Error::last_os_error());
467 }
468
469 let updated = original & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
471
472 let res = unsafe { SetConsoleMode(handle, updated) };
474 if res == 0 {
475 return Err(io::Error::last_os_error());
476 }
477
478 Ok(Self {
479 original: Some(original),
480 })
481 }
482
483 pub(super) fn restore(&mut self) -> io::Result<()> {
484 if let Some(original) = self.original.take() {
485 let handle = std::io::stdin().as_raw_handle();
486 let res = unsafe { SetConsoleMode(handle, original) };
487 if res == 0 {
488 Err(io::Error::last_os_error())
489 } else {
490 Ok(())
491 }
492 } else {
493 Ok(())
494 }
495 }
496 }
497}
498
499#[derive(Copy, Clone, Debug, Eq, PartialEq)]
500pub(crate) enum InputEvent {
501 Info,
502 Enter,
503}