Skip to main content

running_process/pty/
terminal_input.rs

1use std::collections::VecDeque;
2#[cfg(windows)]
3use std::fs::OpenOptions;
4#[cfg(windows)]
5use std::io::Write;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Condvar, Mutex};
8use std::thread;
9use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
10
11use thiserror::Error;
12
13/// Environment variable name for the trace file path.
14pub const NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV: &str =
15    "RUNNING_PROCESS_NATIVE_TERMINAL_INPUT_TRACE_PATH";
16
17// ── Error type ──
18
19/// Errors returned by native terminal input capture.
20#[derive(Debug, Error)]
21pub enum TerminalInputError {
22    /// Terminal input capture has already closed.
23    #[error("terminal input is closed")]
24    Closed,
25    /// No terminal input arrived before the requested timeout.
26    #[error("no terminal input available before timeout")]
27    Timeout,
28    /// An operating-system I/O operation failed.
29    #[error("terminal input I/O error: {0}")]
30    Io(#[from] std::io::Error),
31    /// A terminal input error that does not fit a narrower variant.
32    #[error("terminal input error: {0}")]
33    Other(String),
34}
35
36// ── Pure-Rust data types ──
37
38/// A translated terminal input event ready to forward to a PTY.
39#[derive(Clone)]
40pub struct TerminalInputEventRecord {
41    /// Bytes to write to the PTY for this event.
42    pub data: Vec<u8>,
43    /// Whether this event represents an unmodified Enter submit action.
44    pub submit: bool,
45    /// Whether Shift was active when the event was captured.
46    pub shift: bool,
47    /// Whether Ctrl was active when the event was captured.
48    pub ctrl: bool,
49    /// Whether Alt was active when the event was captured.
50    pub alt: bool,
51    /// Virtual-key code for the source key event.
52    pub virtual_key_code: u16,
53    /// Number of key repeats represented by this event.
54    pub repeat_count: u16,
55}
56
57/// Shared queue state for captured terminal input events.
58pub struct TerminalInputState {
59    /// Queued translated terminal input events.
60    pub events: VecDeque<TerminalInputEventRecord>,
61    /// Whether the capture stream has been closed.
62    pub closed: bool,
63}
64
65#[cfg(windows)]
66/// Windows console state saved while native input capture is active.
67pub struct ActiveTerminalInputCapture {
68    /// Raw Windows console input handle as an integer.
69    pub input_handle: usize,
70    /// Console mode to restore when capture stops.
71    pub original_mode: u32,
72    /// Console mode installed while capture is active.
73    pub active_mode: u32,
74}
75
76#[cfg(windows)]
77/// Result of waiting for a Windows terminal input event.
78#[derive(Debug, PartialEq)]
79pub enum TerminalInputWaitOutcome {
80    /// A translated input event was received.
81    Event(TerminalInputEventRecord),
82    /// Terminal input capture closed before an event arrived.
83    Closed,
84    /// No event arrived before the timeout.
85    Timeout,
86}
87
88#[cfg(windows)]
89impl std::fmt::Debug for TerminalInputEventRecord {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("TerminalInputEventRecord")
92            .field("data", &self.data)
93            .field("submit", &self.submit)
94            .field("shift", &self.shift)
95            .field("ctrl", &self.ctrl)
96            .field("alt", &self.alt)
97            .field("virtual_key_code", &self.virtual_key_code)
98            .field("repeat_count", &self.repeat_count)
99            .finish()
100    }
101}
102
103#[cfg(windows)]
104impl PartialEq for TerminalInputEventRecord {
105    fn eq(&self, other: &Self) -> bool {
106        self.data == other.data
107            && self.submit == other.submit
108            && self.shift == other.shift
109            && self.ctrl == other.ctrl
110            && self.alt == other.alt
111            && self.virtual_key_code == other.virtual_key_code
112            && self.repeat_count == other.repeat_count
113    }
114}
115
116// ── Utility functions ──
117
118/// Returns the current Unix timestamp as fractional seconds.
119pub fn unix_now_seconds() -> f64 {
120    SystemTime::now()
121        .duration_since(UNIX_EPOCH)
122        .map(|duration| duration.as_secs_f64())
123        .unwrap_or(0.0)
124}
125
126#[cfg(windows)]
127/// Returns the configured native input trace target, if tracing is enabled.
128pub fn native_terminal_input_trace_target() -> Option<String> {
129    std::env::var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV)
130        .ok()
131        .map(|value| value.trim().to_string())
132        .filter(|value| !value.is_empty())
133}
134
135#[cfg(windows)]
136/// Appends one native input trace line to the configured target.
137pub fn append_native_terminal_input_trace_line(line: &str) {
138    let Some(target) = native_terminal_input_trace_target() else {
139        return;
140    };
141    if target == "-" {
142        eprintln!("{line}");
143        return;
144    }
145    let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&target) else {
146        return;
147    };
148    let _ = writeln!(file, "{line}");
149}
150
151#[cfg(windows)]
152/// Formats bytes as lowercase hexadecimal values for trace output.
153pub fn format_terminal_input_bytes(data: &[u8]) -> String {
154    if data.is_empty() {
155        return "[]".to_string();
156    }
157    let parts: Vec<String> = data.iter().map(|byte| format!("{byte:02x}")).collect();
158    format!("[{}]", parts.join(" "))
159}
160
161// ── Console mode / key translation helpers ──
162
163#[cfg(windows)]
164/// Builds the Windows console mode used during native input capture.
165pub fn native_terminal_input_mode(original_mode: u32) -> u32 {
166    use winapi::um::wincon::{
167        ENABLE_ECHO_INPUT, ENABLE_EXTENDED_FLAGS, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
168        ENABLE_QUICK_EDIT_MODE, ENABLE_WINDOW_INPUT,
169    };
170
171    (original_mode | ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT)
172        & !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE)
173}
174
175#[cfg(windows)]
176/// Returns the VT modifier parameter for the active modifier keys.
177pub fn terminal_input_modifier_parameter(shift: bool, alt: bool, ctrl: bool) -> Option<u8> {
178    let value = 1 + u8::from(shift) + (u8::from(alt) * 2) + (u8::from(ctrl) * 4);
179    (value > 1).then_some(value)
180}
181
182#[cfg(windows)]
183/// Repeats translated bytes according to a Windows key repeat count.
184pub fn repeat_terminal_input_bytes(chunk: &[u8], repeat_count: u16) -> Vec<u8> {
185    let repeat = usize::from(repeat_count.max(1));
186    let mut output = Vec::with_capacity(chunk.len() * repeat);
187    for _ in 0..repeat {
188        output.extend_from_slice(chunk);
189    }
190    output
191}
192
193#[cfg(windows)]
194/// Adds a VT CSI modifier parameter to a base sequence when needed and repeats it.
195pub fn repeated_modified_sequence(base: &[u8], modifier: Option<u8>, repeat_count: u16) -> Vec<u8> {
196    if let Some(value) = modifier {
197        let base_text = std::str::from_utf8(base).expect("VT sequence literal must be utf-8");
198        let body = base_text
199            .strip_prefix("\x1b[")
200            .expect("VT sequence literal must start with CSI");
201        let sequence = format!("\x1b[1;{value}{body}");
202        repeat_terminal_input_bytes(sequence.as_bytes(), repeat_count)
203    } else {
204        repeat_terminal_input_bytes(base, repeat_count)
205    }
206}
207
208#[cfg(windows)]
209/// Builds and repeats a VT CSI tilde sequence with an optional modifier.
210pub fn repeated_tilde_sequence(number: u8, modifier: Option<u8>, repeat_count: u16) -> Vec<u8> {
211    if let Some(value) = modifier {
212        let sequence = format!("\x1b[{number};{value}~");
213        repeat_terminal_input_bytes(sequence.as_bytes(), repeat_count)
214    } else {
215        let sequence = format!("\x1b[{number}~");
216        repeat_terminal_input_bytes(sequence.as_bytes(), repeat_count)
217    }
218}
219
220#[cfg(windows)]
221/// Maps a Unicode code unit to the corresponding Ctrl-key control byte.
222pub fn control_character_for_unicode(unicode: u16) -> Option<u8> {
223    let upper = char::from_u32(u32::from(unicode))?.to_ascii_uppercase();
224    match upper {
225        '@' | ' ' => Some(0x00),
226        'A'..='Z' => Some((upper as u8) - b'@'),
227        '[' => Some(0x1B),
228        '\\' => Some(0x1C),
229        ']' => Some(0x1D),
230        '^' => Some(0x1E),
231        '_' => Some(0x1F),
232        _ => None,
233    }
234}
235
236#[cfg(windows)]
237/// Writes a trace record for a translated key event and returns the event unchanged.
238pub fn trace_translated_console_key_event(
239    record: &winapi::um::wincontypes::KEY_EVENT_RECORD,
240    event: TerminalInputEventRecord,
241) -> TerminalInputEventRecord {
242    append_native_terminal_input_trace_line(&format!(
243        "[{:.6}] native_terminal_input raw bKeyDown={} vk={:#06x} scan={:#06x} unicode={:#06x} control={:#010x} repeat={} translated bytes={} submit={} shift={} ctrl={} alt={}",
244        unix_now_seconds(),
245        record.bKeyDown,
246        record.wVirtualKeyCode,
247        record.wVirtualScanCode,
248        unsafe { *record.uChar.UnicodeChar() },
249        record.dwControlKeyState,
250        record.wRepeatCount.max(1),
251        format_terminal_input_bytes(&event.data),
252        event.submit,
253        event.shift,
254        event.ctrl,
255        event.alt,
256    ));
257    event
258}
259
260#[cfg(windows)]
261/// Translates a Windows console key event into PTY input bytes.
262pub fn translate_console_key_event(
263    record: &winapi::um::wincontypes::KEY_EVENT_RECORD,
264) -> Option<TerminalInputEventRecord> {
265    use winapi::um::wincontypes::{
266        LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SHIFT_PRESSED,
267    };
268    use winapi::um::winuser::{
269        VK_BACK, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_HOME, VK_INSERT, VK_LEFT, VK_NEXT,
270        VK_PRIOR, VK_RETURN, VK_RIGHT, VK_TAB, VK_UP,
271    };
272
273    if record.bKeyDown == 0 {
274        append_native_terminal_input_trace_line(&format!(
275            "[{:.6}] native_terminal_input raw bKeyDown=0 vk={:#06x} scan={:#06x} unicode={:#06x} control={:#010x} repeat={} translated=ignored",
276            unix_now_seconds(),
277            record.wVirtualKeyCode,
278            record.wVirtualScanCode,
279            unsafe { *record.uChar.UnicodeChar() },
280            record.dwControlKeyState,
281            record.wRepeatCount,
282        ));
283        return None;
284    }
285
286    let repeat_count = record.wRepeatCount.max(1);
287    let modifiers = record.dwControlKeyState;
288    let shift = modifiers & SHIFT_PRESSED != 0;
289    let alt = modifiers & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0;
290    let ctrl = modifiers & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0;
291    let virtual_key_code = record.wVirtualKeyCode;
292    let unicode = unsafe { *record.uChar.UnicodeChar() };
293
294    // Shift+Enter: send CSI u escape sequence so downstream TUI apps
295    // (e.g. Claude Code) can distinguish Shift+Enter (newline) from
296    // plain Enter (submit).  Format: ESC [ 13 ; 2 u
297    if shift && !ctrl && !alt && virtual_key_code as i32 == VK_RETURN {
298        return Some(trace_translated_console_key_event(
299            record,
300            TerminalInputEventRecord {
301                data: repeat_terminal_input_bytes(b"\x1b[13;2u", repeat_count),
302                submit: false,
303                shift,
304                ctrl,
305                alt,
306                virtual_key_code,
307                repeat_count,
308            },
309        ));
310    }
311
312    let mut data = if ctrl {
313        control_character_for_unicode(unicode)
314            .map(|byte| repeat_terminal_input_bytes(&[byte], repeat_count))
315            .unwrap_or_default()
316    } else {
317        Vec::new()
318    };
319
320    if data.is_empty() && unicode != 0 {
321        if let Some(character) = char::from_u32(u32::from(unicode)) {
322            let text: String = std::iter::repeat_n(character, usize::from(repeat_count)).collect();
323            data = text.into_bytes();
324        }
325    }
326
327    if data.is_empty() {
328        let modifier = terminal_input_modifier_parameter(shift, alt, ctrl);
329        let sequence = match virtual_key_code as i32 {
330            VK_BACK => Some(b"\x08".as_slice()),
331            VK_TAB if shift => Some(b"\x1b[Z".as_slice()),
332            VK_TAB => Some(b"\t".as_slice()),
333            VK_RETURN => Some(b"\r".as_slice()),
334            VK_ESCAPE => Some(b"\x1b".as_slice()),
335            VK_UP => {
336                return Some(trace_translated_console_key_event(
337                    record,
338                    TerminalInputEventRecord {
339                        data: repeated_modified_sequence(b"\x1b[A", modifier, repeat_count),
340                        submit: false,
341                        shift,
342                        ctrl,
343                        alt,
344                        virtual_key_code,
345                        repeat_count,
346                    },
347                ));
348            }
349            VK_DOWN => {
350                return Some(trace_translated_console_key_event(
351                    record,
352                    TerminalInputEventRecord {
353                        data: repeated_modified_sequence(b"\x1b[B", modifier, repeat_count),
354                        submit: false,
355                        shift,
356                        ctrl,
357                        alt,
358                        virtual_key_code,
359                        repeat_count,
360                    },
361                ));
362            }
363            VK_RIGHT => {
364                return Some(trace_translated_console_key_event(
365                    record,
366                    TerminalInputEventRecord {
367                        data: repeated_modified_sequence(b"\x1b[C", modifier, repeat_count),
368                        submit: false,
369                        shift,
370                        ctrl,
371                        alt,
372                        virtual_key_code,
373                        repeat_count,
374                    },
375                ));
376            }
377            VK_LEFT => {
378                return Some(trace_translated_console_key_event(
379                    record,
380                    TerminalInputEventRecord {
381                        data: repeated_modified_sequence(b"\x1b[D", modifier, repeat_count),
382                        submit: false,
383                        shift,
384                        ctrl,
385                        alt,
386                        virtual_key_code,
387                        repeat_count,
388                    },
389                ));
390            }
391            VK_HOME => {
392                return Some(trace_translated_console_key_event(
393                    record,
394                    TerminalInputEventRecord {
395                        data: repeated_modified_sequence(b"\x1b[H", modifier, repeat_count),
396                        submit: false,
397                        shift,
398                        ctrl,
399                        alt,
400                        virtual_key_code,
401                        repeat_count,
402                    },
403                ));
404            }
405            VK_END => {
406                return Some(trace_translated_console_key_event(
407                    record,
408                    TerminalInputEventRecord {
409                        data: repeated_modified_sequence(b"\x1b[F", modifier, repeat_count),
410                        submit: false,
411                        shift,
412                        ctrl,
413                        alt,
414                        virtual_key_code,
415                        repeat_count,
416                    },
417                ));
418            }
419            VK_INSERT => {
420                return Some(trace_translated_console_key_event(
421                    record,
422                    TerminalInputEventRecord {
423                        data: repeated_tilde_sequence(2, modifier, repeat_count),
424                        submit: false,
425                        shift,
426                        ctrl,
427                        alt,
428                        virtual_key_code,
429                        repeat_count,
430                    },
431                ));
432            }
433            VK_DELETE => {
434                return Some(trace_translated_console_key_event(
435                    record,
436                    TerminalInputEventRecord {
437                        data: repeated_tilde_sequence(3, modifier, repeat_count),
438                        submit: false,
439                        shift,
440                        ctrl,
441                        alt,
442                        virtual_key_code,
443                        repeat_count,
444                    },
445                ));
446            }
447            VK_PRIOR => {
448                return Some(trace_translated_console_key_event(
449                    record,
450                    TerminalInputEventRecord {
451                        data: repeated_tilde_sequence(5, modifier, repeat_count),
452                        submit: false,
453                        shift,
454                        ctrl,
455                        alt,
456                        virtual_key_code,
457                        repeat_count,
458                    },
459                ));
460            }
461            VK_NEXT => {
462                return Some(trace_translated_console_key_event(
463                    record,
464                    TerminalInputEventRecord {
465                        data: repeated_tilde_sequence(6, modifier, repeat_count),
466                        submit: false,
467                        shift,
468                        ctrl,
469                        alt,
470                        virtual_key_code,
471                        repeat_count,
472                    },
473                ));
474            }
475            _ => None,
476        };
477        data = sequence.map(|chunk| repeat_terminal_input_bytes(chunk, repeat_count))?;
478    }
479
480    if alt && !data.starts_with(b"\x1b[") && !data.starts_with(b"\x1bO") {
481        let mut prefixed = Vec::with_capacity(data.len() + 1);
482        prefixed.push(0x1B);
483        prefixed.extend_from_slice(&data);
484        data = prefixed;
485    }
486
487    let event = TerminalInputEventRecord {
488        data,
489        submit: virtual_key_code as i32 == VK_RETURN && !shift,
490        shift,
491        ctrl,
492        alt,
493        virtual_key_code,
494        repeat_count,
495    };
496    Some(trace_translated_console_key_event(record, event))
497}
498
499// ── Worker thread ──
500
501#[cfg(windows)]
502/// Runs the Windows console input worker that queues translated key events.
503pub fn native_terminal_input_worker(
504    input_handle: usize,
505    state: Arc<Mutex<TerminalInputState>>,
506    condvar: Arc<Condvar>,
507    stop: Arc<AtomicBool>,
508    capturing: Arc<AtomicBool>,
509) {
510    use winapi::shared::minwindef::DWORD;
511    use winapi::shared::winerror::WAIT_TIMEOUT;
512    use winapi::um::consoleapi::ReadConsoleInputW;
513    use winapi::um::synchapi::WaitForSingleObject;
514    use winapi::um::winbase::WAIT_OBJECT_0;
515    use winapi::um::wincontypes::{INPUT_RECORD, KEY_EVENT};
516    use winapi::um::winnt::HANDLE;
517
518    let handle = input_handle as HANDLE;
519    let mut records: [INPUT_RECORD; 512] = unsafe { std::mem::zeroed() };
520    append_native_terminal_input_trace_line(&format!(
521        "[{:.6}] native_terminal_input worker_start handle={input_handle}",
522        unix_now_seconds(),
523    ));
524
525    while !stop.load(Ordering::Acquire) {
526        let wait_result = unsafe { WaitForSingleObject(handle, 50) };
527        match wait_result {
528            WAIT_OBJECT_0 => {
529                let mut read_count: DWORD = 0;
530                let ok = unsafe {
531                    ReadConsoleInputW(
532                        handle,
533                        records.as_mut_ptr(),
534                        records.len() as DWORD,
535                        &mut read_count,
536                    )
537                };
538                if ok == 0 {
539                    append_native_terminal_input_trace_line(&format!(
540                        "[{:.6}] native_terminal_input read_console_input_failed handle={input_handle}",
541                        unix_now_seconds(),
542                    ));
543                    break;
544                }
545                let mut batch = Vec::new();
546                for record in records.iter().take(read_count as usize) {
547                    if record.EventType != KEY_EVENT {
548                        continue;
549                    }
550                    let key_event = unsafe { record.Event.KeyEvent() };
551                    if let Some(event) = translate_console_key_event(key_event) {
552                        batch.push(event);
553                    }
554                }
555                if !batch.is_empty() {
556                    let mut guard = state.lock().expect("terminal input mutex poisoned");
557                    guard.events.extend(batch);
558                    drop(guard);
559                    condvar.notify_all();
560                }
561            }
562            WAIT_TIMEOUT => continue,
563            _ => {
564                append_native_terminal_input_trace_line(&format!(
565                    "[{:.6}] native_terminal_input wait_result={wait_result} handle={input_handle}",
566                    unix_now_seconds(),
567                ));
568                break;
569            }
570        }
571    }
572
573    capturing.store(false, Ordering::Release);
574    let mut guard = state.lock().expect("terminal input mutex poisoned");
575    guard.closed = true;
576    condvar.notify_all();
577    drop(guard);
578    append_native_terminal_input_trace_line(&format!(
579        "[{:.6}] native_terminal_input worker_stop handle={input_handle}",
580        unix_now_seconds(),
581    ));
582}
583
584// ── Wait helper ──
585
586#[cfg(windows)]
587/// Waits for the next queued terminal input event, closure, or timeout.
588pub fn wait_for_terminal_input_event(
589    state: &Arc<Mutex<TerminalInputState>>,
590    condvar: &Arc<Condvar>,
591    timeout: Option<Duration>,
592) -> TerminalInputWaitOutcome {
593    let deadline = timeout.map(|limit| Instant::now() + limit);
594    let mut guard = state.lock().expect("terminal input mutex poisoned");
595    loop {
596        if let Some(event) = guard.events.pop_front() {
597            return TerminalInputWaitOutcome::Event(event);
598        }
599        if guard.closed {
600            return TerminalInputWaitOutcome::Closed;
601        }
602        match deadline {
603            Some(deadline) => {
604                let now = Instant::now();
605                if now >= deadline {
606                    return TerminalInputWaitOutcome::Timeout;
607                }
608                let wait = deadline.saturating_duration_since(now);
609                let result = condvar
610                    .wait_timeout(guard, wait)
611                    .expect("terminal input mutex poisoned");
612                guard = result.0;
613            }
614            None => {
615                guard = condvar.wait(guard).expect("terminal input mutex poisoned");
616            }
617        }
618    }
619}
620
621// ── TerminalInputCore ──
622
623/// Shared native terminal input capture core.
624pub struct TerminalInputCore {
625    /// Shared queue and closed flag for captured input.
626    pub state: Arc<Mutex<TerminalInputState>>,
627    /// Condition variable signaled when input state changes.
628    pub condvar: Arc<Condvar>,
629    /// Stop flag observed by the capture worker.
630    pub stop: Arc<AtomicBool>,
631    /// Whether native input capture is currently active.
632    pub capturing: Arc<AtomicBool>,
633    /// Worker thread handle for active capture.
634    pub worker: Mutex<Option<thread::JoinHandle<()>>>,
635    #[cfg(windows)]
636    /// Saved Windows console capture state.
637    pub console: Mutex<Option<ActiveTerminalInputCapture>>,
638}
639
640impl Default for TerminalInputCore {
641    fn default() -> Self {
642        Self::new()
643    }
644}
645
646impl TerminalInputCore {
647    /// Creates an idle terminal input core with capture stopped.
648    pub fn new() -> Self {
649        Self {
650            state: Arc::new(Mutex::new(TerminalInputState {
651                events: VecDeque::new(),
652                closed: true,
653            })),
654            condvar: Arc::new(Condvar::new()),
655            stop: Arc::new(AtomicBool::new(false)),
656            capturing: Arc::new(AtomicBool::new(false)),
657            worker: Mutex::new(None),
658            #[cfg(windows)]
659            console: Mutex::new(None),
660        }
661    }
662
663    /// Pops the next queued terminal input event without blocking.
664    pub fn next_event(&self) -> Option<TerminalInputEventRecord> {
665        self.state
666            .lock()
667            .expect("terminal input mutex poisoned")
668            .events
669            .pop_front()
670    }
671
672    /// Returns whether at least one input event is queued.
673    pub fn available(&self) -> bool {
674        !self
675            .state
676            .lock()
677            .expect("terminal input mutex poisoned")
678            .events
679            .is_empty()
680    }
681
682    /// Returns whether native terminal input capture is active.
683    pub fn capturing(&self) -> bool {
684        self.capturing.load(Ordering::Acquire)
685    }
686
687    /// Returns the saved original Windows console mode, if capture is active.
688    pub fn original_console_mode(&self) -> Option<u32> {
689        #[cfg(windows)]
690        {
691            return self
692                .console
693                .lock()
694                .expect("terminal input console mutex poisoned")
695                .as_ref()
696                .map(|capture| capture.original_mode);
697        }
698
699        #[cfg(not(windows))]
700        {
701            None
702        }
703    }
704
705    /// Returns the active Windows console mode, if capture is active.
706    pub fn active_console_mode(&self) -> Option<u32> {
707        #[cfg(windows)]
708        {
709            return self
710                .console
711                .lock()
712                .expect("terminal input console mutex poisoned")
713                .as_ref()
714                .map(|capture| capture.active_mode);
715        }
716
717        #[cfg(not(windows))]
718        {
719            None
720        }
721    }
722
723    /// Blocks until an input event, closure, or optional timeout.
724    pub fn wait_for_event(
725        &self,
726        timeout: Option<f64>,
727    ) -> Result<TerminalInputEventRecord, TerminalInputError> {
728        let state = Arc::clone(&self.state);
729        let condvar = Arc::clone(&self.condvar);
730        let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
731        let mut guard = state.lock().expect("terminal input mutex poisoned");
732        loop {
733            if let Some(event) = guard.events.pop_front() {
734                return Ok(event);
735            }
736            if guard.closed {
737                return Err(TerminalInputError::Closed);
738            }
739            match deadline {
740                Some(deadline) => {
741                    let now = Instant::now();
742                    if now >= deadline {
743                        return Err(TerminalInputError::Timeout);
744                    }
745                    let wait = deadline.saturating_duration_since(now);
746                    let result = condvar
747                        .wait_timeout(guard, wait)
748                        .expect("terminal input mutex poisoned");
749                    guard = result.0;
750                }
751                None => {
752                    guard = condvar.wait(guard).expect("terminal input mutex poisoned");
753                }
754            }
755        }
756    }
757
758    /// Drains all queued terminal input events.
759    pub fn drain_events(&self) -> Vec<TerminalInputEventRecord> {
760        let mut guard = self.state.lock().expect("terminal input mutex poisoned");
761        guard.events.drain(..).collect()
762    }
763
764    /// Stops native terminal input capture and restores console state.
765    pub fn stop_impl(&self) -> Result<(), std::io::Error> {
766        self.stop.store(true, Ordering::Release);
767        #[cfg(windows)]
768        append_native_terminal_input_trace_line(&format!(
769            "[{:.6}] native_terminal_input stop_requested",
770            unix_now_seconds(),
771        ));
772        if let Some(worker) = self
773            .worker
774            .lock()
775            .expect("terminal input worker mutex poisoned")
776            .take()
777        {
778            let _ = worker.join();
779        }
780        self.capturing.store(false, Ordering::Release);
781
782        #[cfg(windows)]
783        let restore_result = {
784            use winapi::um::consoleapi::SetConsoleMode;
785            use winapi::um::winnt::HANDLE;
786
787            let console = self
788                .console
789                .lock()
790                .expect("terminal input console mutex poisoned")
791                .take();
792            console.map(|capture| unsafe {
793                SetConsoleMode(capture.input_handle as HANDLE, capture.original_mode)
794            })
795        };
796
797        let mut guard = self.state.lock().expect("terminal input mutex poisoned");
798        guard.closed = true;
799        self.condvar.notify_all();
800        drop(guard);
801
802        #[cfg(windows)]
803        if let Some(result) = restore_result {
804            if result == 0 {
805                return Err(std::io::Error::last_os_error());
806            }
807        }
808        Ok(())
809    }
810
811    #[cfg(windows)]
812    /// Starts native terminal input capture for the attached Windows console.
813    pub fn start_impl(&self) -> Result<(), std::io::Error> {
814        use winapi::um::consoleapi::{GetConsoleMode, SetConsoleMode};
815        use winapi::um::handleapi::INVALID_HANDLE_VALUE;
816        use winapi::um::processenv::GetStdHandle;
817        use winapi::um::winbase::STD_INPUT_HANDLE;
818
819        let mut worker_guard = self
820            .worker
821            .lock()
822            .expect("terminal input worker mutex poisoned");
823        if worker_guard.is_some() {
824            return Ok(());
825        }
826
827        let input_handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
828        if input_handle.is_null() || input_handle == INVALID_HANDLE_VALUE {
829            return Err(std::io::Error::last_os_error());
830        }
831
832        let mut original_mode = 0u32;
833        let got_mode = unsafe { GetConsoleMode(input_handle, &mut original_mode) };
834        if got_mode == 0 {
835            return Err(std::io::Error::new(
836                std::io::ErrorKind::Unsupported,
837                "TerminalInputCore requires an attached Windows console stdin",
838            ));
839        }
840
841        let active_mode = native_terminal_input_mode(original_mode);
842        let set_mode = unsafe { SetConsoleMode(input_handle, active_mode) };
843        if set_mode == 0 {
844            return Err(std::io::Error::last_os_error());
845        }
846        append_native_terminal_input_trace_line(&format!(
847            "[{:.6}] native_terminal_input start handle={} original_mode={:#010x} active_mode={:#010x}",
848            unix_now_seconds(),
849            input_handle as usize,
850            original_mode,
851            active_mode,
852        ));
853
854        self.stop.store(false, Ordering::Release);
855        self.capturing.store(true, Ordering::Release);
856        {
857            let mut state = self.state.lock().expect("terminal input mutex poisoned");
858            state.events.clear();
859            state.closed = false;
860        }
861        *self
862            .console
863            .lock()
864            .expect("terminal input console mutex poisoned") = Some(ActiveTerminalInputCapture {
865            input_handle: input_handle as usize,
866            original_mode,
867            active_mode,
868        });
869
870        let state = Arc::clone(&self.state);
871        let condvar = Arc::clone(&self.condvar);
872        let stop = Arc::clone(&self.stop);
873        let capturing = Arc::clone(&self.capturing);
874        let input_handle_raw = input_handle as usize;
875        *worker_guard = Some(thread::spawn(move || {
876            native_terminal_input_worker(input_handle_raw, state, condvar, stop, capturing);
877        }));
878        Ok(())
879    }
880}
881
882impl Drop for TerminalInputCore {
883    fn drop(&mut self) {
884        let _ = self.stop_impl();
885    }
886}