1use crate::errors::DisplayErrorChain;
11use crossterm::event::{Event, EventStream, KeyCode};
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)) => {
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 Ok(Self {
347 original: Some(original),
348 })
349 }
350
351 pub(super) fn restore(&mut self) -> io::Result<()> {
352 if let Some(original) = self.original.take() {
353 stdin_tcsetattr(TCSANOW, &original)
354 } else {
355 Ok(())
356 }
357 }
358 }
359
360 fn compute_termios() -> io::Result<TermiosPair> {
361 let mut termios = mem::MaybeUninit::uninit();
362 let res = unsafe { tcgetattr(std::io::stdin().as_raw_fd(), termios.as_mut_ptr()) };
363 if res == -1 {
364 return Err(io::Error::last_os_error());
365 }
366
367 let original = unsafe { termios.assume_init() };
369
370 let mut updated = original;
371
372 updated.c_lflag &= !(ECHO | ICANON);
375 updated.c_cc[VMIN] = 1;
380 updated.c_cc[VTIME] = 0;
381
382 Ok(TermiosPair { original, updated })
383 }
384
385 #[derive(Clone, Debug)]
386 struct TermiosPair {
387 original: libc::termios,
388 updated: libc::termios,
389 }
390
391 fn stdin_tcsetattr(optional_actions: c_int, updated: &libc::termios) -> io::Result<()> {
392 let res = unsafe { tcsetattr(std::io::stdin().as_raw_fd(), optional_actions, updated) };
393 if res == -1 {
394 Err(io::Error::last_os_error())
395 } else {
396 Ok(())
397 }
398 }
399}
400
401#[cfg(windows)]
402mod imp {
403 use std::{
404 io::{self, IsTerminal},
405 os::windows::io::AsRawHandle,
406 };
407 use tracing::debug;
408 use windows_sys::Win32::System::Console::{
409 CONSOLE_MODE, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, GetConsoleMode, SetConsoleMode,
410 };
411
412 pub(super) type Error = io::Error;
413
414 pub(super) fn is_foreground_process() -> bool {
415 if !std::io::stdin().is_terminal() {
422 debug!("stdin is not a terminal => is_foreground_process() is false");
423 return false;
424 }
425
426 debug!("stdin is a terminal => is_foreground_process() is true");
427 true
428 }
429
430 #[derive(Clone, Debug)]
438 pub(super) struct InputGuard {
439 original: Option<CONSOLE_MODE>,
440 }
441
442 impl InputGuard {
443 pub(super) fn new() -> io::Result<Self> {
444 let handle = std::io::stdin().as_raw_handle();
445
446 let mut original: CONSOLE_MODE = 0;
448 let res = unsafe { GetConsoleMode(handle, &mut original) };
449 if res == 0 {
450 return Err(io::Error::last_os_error());
451 }
452
453 let updated = original & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
455
456 let res = unsafe { SetConsoleMode(handle, updated) };
458 if res == 0 {
459 return Err(io::Error::last_os_error());
460 }
461
462 Ok(Self {
463 original: Some(original),
464 })
465 }
466
467 pub(super) fn restore(&mut self) -> io::Result<()> {
468 if let Some(original) = self.original.take() {
469 let handle = std::io::stdin().as_raw_handle();
470 let res = unsafe { SetConsoleMode(handle, original) };
471 if res == 0 {
472 Err(io::Error::last_os_error())
473 } else {
474 Ok(())
475 }
476 } else {
477 Ok(())
478 }
479 }
480 }
481}
482
483#[derive(Copy, Clone, Debug, Eq, PartialEq)]
484pub(crate) enum InputEvent {
485 Info,
486 Enter,
487}