Skip to main content

running_process_platform_internal/platform/
terminal_input.rs

1//! Host-neutral terminal-input queue used when native console capture is unavailable.
2
3use std::collections::VecDeque;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::{Arc, Condvar, Mutex};
6use std::thread;
7use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
8
9use thiserror::Error;
10
11pub const NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV: &str =
12    "RUNNING_PROCESS_NATIVE_TERMINAL_INPUT_TRACE_PATH";
13
14#[derive(Debug, Error)]
15pub enum TerminalInputError {
16    #[error("terminal input capture timed out")]
17    Timeout,
18    #[error("terminal input capture is closed")]
19    Closed,
20    #[error("terminal input I/O error: {0}")]
21    Io(#[from] std::io::Error),
22    #[error("terminal input error: {0}")]
23    Other(String),
24}
25
26#[derive(Debug, Clone, PartialEq)]
27pub struct TerminalInputEventRecord {
28    pub data: Vec<u8>,
29    pub submit: bool,
30    pub shift: bool,
31    pub ctrl: bool,
32    pub alt: bool,
33    pub virtual_key_code: u16,
34    pub repeat_count: u16,
35}
36
37pub struct TerminalInputState {
38    pub events: VecDeque<TerminalInputEventRecord>,
39    pub closed: bool,
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub enum TerminalInputWaitOutcome {
44    Event(TerminalInputEventRecord),
45    Timeout,
46    Closed,
47}
48
49pub fn unix_now_seconds() -> f64 {
50    SystemTime::now()
51        .duration_since(UNIX_EPOCH)
52        .unwrap_or_default()
53        .as_secs_f64()
54}
55
56pub fn wait_for_terminal_input_event(
57    state: &Arc<Mutex<TerminalInputState>>,
58    condvar: &Arc<Condvar>,
59    timeout: Option<Duration>,
60) -> TerminalInputWaitOutcome {
61    let deadline = timeout.map(|duration| Instant::now() + duration);
62    let mut guard = state.lock().expect("terminal input mutex poisoned");
63    loop {
64        if let Some(event) = guard.events.pop_front() {
65            return TerminalInputWaitOutcome::Event(event);
66        }
67        if guard.closed {
68            return TerminalInputWaitOutcome::Closed;
69        }
70        match deadline {
71            Some(deadline) => {
72                let now = Instant::now();
73                if now >= deadline {
74                    return TerminalInputWaitOutcome::Timeout;
75                }
76                let (next, result) = condvar
77                    .wait_timeout(guard, deadline.saturating_duration_since(now))
78                    .expect("terminal input mutex poisoned");
79                guard = next;
80                if result.timed_out() && guard.events.is_empty() {
81                    return TerminalInputWaitOutcome::Timeout;
82                }
83            }
84            None => {
85                guard = condvar.wait(guard).expect("terminal input mutex poisoned");
86            }
87        }
88    }
89}
90
91pub struct TerminalInputCore {
92    pub state: Arc<Mutex<TerminalInputState>>,
93    pub condvar: Arc<Condvar>,
94    pub stop: Arc<AtomicBool>,
95    pub capturing: Arc<AtomicBool>,
96    pub worker: Mutex<Option<thread::JoinHandle<()>>>,
97}
98
99impl Default for TerminalInputCore {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl TerminalInputCore {
106    pub fn supported() -> bool {
107        false
108    }
109
110    pub fn new() -> Self {
111        Self {
112            state: Arc::new(Mutex::new(TerminalInputState {
113                events: VecDeque::new(),
114                closed: true,
115            })),
116            condvar: Arc::new(Condvar::new()),
117            stop: Arc::new(AtomicBool::new(false)),
118            capturing: Arc::new(AtomicBool::new(false)),
119            worker: Mutex::new(None),
120        }
121    }
122
123    pub fn next_event(&self) -> Option<TerminalInputEventRecord> {
124        self.state
125            .lock()
126            .expect("terminal input mutex poisoned")
127            .events
128            .pop_front()
129    }
130
131    pub fn available(&self) -> bool {
132        !self
133            .state
134            .lock()
135            .expect("terminal input mutex poisoned")
136            .events
137            .is_empty()
138    }
139
140    pub fn capturing(&self) -> bool {
141        self.capturing.load(Ordering::Acquire)
142    }
143
144    pub fn original_console_mode(&self) -> Option<u32> {
145        None
146    }
147
148    pub fn active_console_mode(&self) -> Option<u32> {
149        None
150    }
151
152    pub fn wait_for_event(
153        &self,
154        timeout: Option<f64>,
155    ) -> Result<TerminalInputEventRecord, TerminalInputError> {
156        match wait_for_terminal_input_event(
157            &self.state,
158            &self.condvar,
159            timeout.map(Duration::from_secs_f64),
160        ) {
161            TerminalInputWaitOutcome::Event(event) => Ok(event),
162            TerminalInputWaitOutcome::Timeout => Err(TerminalInputError::Timeout),
163            TerminalInputWaitOutcome::Closed => Err(TerminalInputError::Closed),
164        }
165    }
166
167    pub fn drain_events(&self) -> Vec<TerminalInputEventRecord> {
168        self.state
169            .lock()
170            .expect("terminal input mutex poisoned")
171            .events
172            .drain(..)
173            .collect()
174    }
175
176    pub fn stop_impl(&self) -> Result<(), std::io::Error> {
177        self.stop.store(true, Ordering::Release);
178        if let Some(worker) = self
179            .worker
180            .lock()
181            .expect("terminal input worker mutex poisoned")
182            .take()
183        {
184            let _ = worker.join();
185        }
186        self.capturing.store(false, Ordering::Release);
187        let mut state = self.state.lock().expect("terminal input mutex poisoned");
188        state.closed = true;
189        self.condvar.notify_all();
190        Ok(())
191    }
192
193    pub fn start_impl(&self) -> Result<(), std::io::Error> {
194        Err(std::io::Error::new(
195            std::io::ErrorKind::Unsupported,
196            "NativeTerminalInput is only available on Windows consoles",
197        ))
198    }
199}
200
201impl Drop for TerminalInputCore {
202    fn drop(&mut self) {
203        let _ = self.stop_impl();
204    }
205}