running_process_platform_internal/platform/
terminal_input.rs1use 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}