//! Stdin input reading for the thin client. //! //! On Unix, reads stdin bytes and forwards framed input to the main event loop. //! The server handles semantic parsing. On Windows, crossterm may surface //! terminal control strings as character key events, so the reader re-frames //! those control bytes before forwarding semantic client input events. //! //! This is simpler and more reliable because: //! - The server has the same input parsing code //! - We avoid duplicating parsing logic in the client //! - Host terminal control replies can be buffered or discarded before they leak use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; #[cfg(unix)] use std::io::{self, Read}; #[cfg(unix)] use std::os::fd::AsRawFd; #[cfg(windows)] use std::time::Duration; use tokio::sync::mpsc; use super::ClientLoopEvent; #[cfg(any(windows, test))] mod windows_vti; // --------------------------------------------------------------------------- // Stdin reader thread // --------------------------------------------------------------------------- /// Reads raw bytes from stdin and sends them to the main event loop. /// /// This runs on a dedicated thread because stdin reading is blocking. /// The main loop receives the raw bytes and forwards them as /// `ClientMessage::Input` to the server. pub fn stdin_reader_loop( event_tx: mpsc::Sender, should_quit: &Arc, host_color_query_sent: bool, host_mouse_capture_active: Arc, ) { #[cfg(windows)] { let _ = (host_color_query_sent, host_mouse_capture_active); windows_stdin_reader_loop(event_tx, should_quit); } #[cfg(unix)] unix_stdin_reader_loop( event_tx, should_quit, host_color_query_sent, host_mouse_capture_active, ); } #[cfg(unix)] fn unix_stdin_reader_loop( event_tx: mpsc::Sender, should_quit: &Arc, host_color_query_sent: bool, host_mouse_capture_active: Arc, ) { let stdin = io::stdin(); let mut reader = stdin.lock(); let mut scratch = [0u8; 4096]; let mut framer = crate::raw_input::RawInputByteFramer::for_host_input(); if host_color_query_sent { framer.host_color_query_sent(); framer.enable_host_color_scheme_change_tracking(); } let mut pending_palette = Vec::new(); while !should_quit.load(Ordering::Acquire) { match reader.read(&mut scratch) { Ok(0) => break, Ok(n) => { if !send_unix_input_chunks( framer.push(&scratch[..n]), &event_tx, &mut pending_palette, ) { return; } let timeout_ms = idle_flush_timeout_ms( &framer, host_mouse_capture_active.load(Ordering::Acquire), ); if stdin_read_ready(&reader, timeout_ms) == Some(false) { let had_pending = framer.has_pending_input(); let chunks = framer.flush_timeout(); let held_escape = had_pending && chunks.is_empty(); if !send_unix_input_chunks(chunks, &event_tx, &mut pending_palette) || !flush_unix_palette_input(&event_tx, &mut pending_palette) { return; } if held_escape && stdin_read_ready( &reader, crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS, ) == Some(false) && !send_unix_input_chunks( framer.flush_timeout(), &event_tx, &mut pending_palette, ) { return; } } } Err(err) => { if err.kind() == io::ErrorKind::Interrupted { continue; } break; } } } } #[cfg(unix)] fn send_unix_input_chunks( chunks: Vec>, event_tx: &mpsc::Sender, pending_palette: &mut Vec>, ) -> bool { for data in chunks { let palette_response = std::str::from_utf8(&data) .ok() .and_then(crate::terminal_theme::parse_palette_color_response) .is_some(); if palette_response { pending_palette.push(data); if pending_palette.len() == 256 && !flush_unix_palette_input(event_tx, pending_palette) { return false; } continue; } let default_color_response = std::str::from_utf8(&data) .ok() .and_then(crate::terminal_theme::parse_default_color_response) .is_some(); if !default_color_response && !flush_unix_palette_input(event_tx, pending_palette) { return false; } if event_tx .blocking_send(ClientLoopEvent::StdinInput(data)) .is_err() { return false; } } true } #[cfg(unix)] fn flush_unix_palette_input( event_tx: &mpsc::Sender, pending_palette: &mut Vec>, ) -> bool { if pending_palette.is_empty() { return true; } let data = std::mem::take(pending_palette).concat(); event_tx .blocking_send(ClientLoopEvent::StdinInput(data)) .is_ok() } #[cfg(unix)] fn idle_flush_timeout_ms( framer: &crate::raw_input::RawInputByteFramer, host_mouse_capture_active: bool, ) -> i32 { if host_mouse_capture_active && (framer.has_pending_lone_escape() || framer.has_pending_incomplete_sgr_mouse_sequence()) { crate::raw_input::MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS } else { crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS } } #[cfg(windows)] fn windows_stdin_reader_loop( event_tx: mpsc::Sender, should_quit: &Arc, ) { if !super::windows_vti_input_backend_enabled() { windows_crossterm_reader_loop(event_tx, should_quit); } else { match windows_vti::console_input_handle() { Ok(handle) if windows_vti::virtual_terminal_input_enabled(handle) => { windows_vti::raw_console_reader_loop(handle, event_tx, should_quit); } _ => windows_crossterm_reader_loop(event_tx, should_quit), } } } #[cfg(windows)] fn windows_crossterm_reader_loop( event_tx: mpsc::Sender, should_quit: &Arc, ) { let mut framer = crate::raw_input::RawInputFramer::for_host_input(); while !should_quit.load(Ordering::Acquire) { match crossterm::event::poll(Duration::from_millis(10)) { Ok(true) => {} Ok(false) => { if framer.has_pending_input() { tracing::debug!("windows input raw sequence timed out; flushing"); if !send_windows_raw_events(framer.flush_timeout(), &event_tx) { return; } } continue; } Err(_) => break, } let event = match crossterm::event::read() { Ok(event) => event, Err(_) => break, }; let raw_sequence_pending = framer.has_pending_input(); if let Some(bytes) = windows_key_raw_bytes(&event, raw_sequence_pending) { tracing::debug!( bytes = ?bytes, pending_before = raw_sequence_pending, "windows input routed through raw framer" ); if !send_windows_raw_events(framer.push(&bytes), &event_tx) { return; } continue; } if raw_sequence_pending { tracing::debug!("windows input raw sequence interrupted by semantic event; flushing"); if !send_windows_raw_events(framer.flush_timeout(), &event_tx) { return; } } if windows_event_is_control_key(&event) { tracing::debug!(event = ?event, "windows control key forwarded as semantic input"); } let Some(event) = windows_crossterm_input_event(event) else { continue; }; if event_tx .blocking_send(ClientLoopEvent::StdinEvents(vec![event])) .is_err() { return; } } if framer.has_pending_input() { let _ = send_windows_raw_events(framer.flush_timeout(), &event_tx); } } #[cfg(any(windows, test))] fn windows_crossterm_input_event( event: crossterm::event::Event, ) -> Option { let event = crate::protocol::ClientInputEvent::from_crossterm(event)?; match event { crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Char(codepoint), modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, source, .. } => Some(crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Char(codepoint), modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, repeat_count: 1, generated_text: Some(codepoint.to_string()), source, }), event => Some(event), } } #[cfg(windows)] fn windows_event_is_control_key(event: &crossterm::event::Event) -> bool { use crossterm::event::{Event, KeyModifiers}; matches!( event, Event::Key(key) if key.modifiers.contains(KeyModifiers::CONTROL) || matches!(key.code, crossterm::event::KeyCode::Char(ch) if ch.is_control()) ) } #[cfg(any(windows, test))] fn windows_key_raw_bytes( event: &crossterm::event::Event, raw_sequence_pending: bool, ) -> Option> { use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers}; let Event::Key(key) = event else { return None; }; if key.kind == KeyEventKind::Release { return None; } match key.code { KeyCode::Esc if key.modifiers.is_empty() => Some(vec![0x1b]), KeyCode::Char('[') if !raw_sequence_pending && key.modifiers == KeyModifiers::CONTROL => { Some(vec![0x1b]) } KeyCode::Char(ch) if !raw_sequence_pending && matches!(ch, 'i' | 'I') && key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) => { let mut buf = [0; 4]; Some(ch.encode_utf8(&mut buf).as_bytes().to_vec()) } KeyCode::Char(ch) if raw_sequence_pending || ch.is_control() => { let mut bytes = Vec::new(); if key.modifiers.contains(KeyModifiers::ALT) { bytes.push(0x1b); } let mut buf = [0; 4]; bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes()); Some(bytes) } _ => None, } } #[cfg(windows)] fn send_windows_raw_events( events: Vec, event_tx: &mpsc::Sender, ) -> bool { let raw_event_count = events.len(); let events = events .into_iter() .filter_map(windows_client_input_event_from_raw) .collect::>(); if events.is_empty() { return true; } tracing::debug!( raw_event_count, forwarded_event_count = events.len(), "windows raw-framed input events forwarded" ); event_tx .blocking_send(ClientLoopEvent::StdinEvents(events)) .is_ok() } #[cfg(any(windows, test))] fn windows_client_input_event_from_raw( event: crate::raw_input::RawInputEvent, ) -> Option { match event { crate::raw_input::RawInputEvent::Text(text) => Some( crate::protocol::ClientInputEvent::TextCommit(text.into_string()), ), crate::raw_input::RawInputEvent::Key(key) => { let code = crate::protocol::ClientKeyCode::from_crossterm(key.code)?; let modifiers = key.modifiers.bits(); let kind = crate::protocol::ClientKeyKind::from_crossterm(key.kind); let source = if let Some(bytes) = key.vt_bytes() { crate::protocol::ClientKeySource::Vt { bytes: bytes.to_vec(), } } else if let Some(record) = key.windows_record() { crate::protocol::ClientKeySource::WindowsConsole { record } } else { crate::protocol::ClientKeySource::Synthesized }; Some(crate::protocol::ClientInputEvent::Key { code, modifiers, kind, repeat_count: key.repeat_count, generated_text: key.generated_text.clone(), source, }) } crate::raw_input::RawInputEvent::Mouse(mouse) => { Some(crate::protocol::ClientInputEvent::Mouse { kind: crate::protocol::ClientMouseKind::from_crossterm(mouse.kind)?, column: mouse.column, row: mouse.row, modifiers: mouse.modifiers.bits(), }) } crate::raw_input::RawInputEvent::Paste(text) => { Some(crate::protocol::ClientInputEvent::Paste { text }) } crate::raw_input::RawInputEvent::OuterFocusGained => { Some(crate::protocol::ClientInputEvent::FocusGained) } crate::raw_input::RawInputEvent::OuterFocusLost => { Some(crate::protocol::ClientInputEvent::FocusLost) } crate::raw_input::RawInputEvent::HostDefaultColor { .. } | crate::raw_input::RawInputEvent::HostPaletteColors { .. } | crate::raw_input::RawInputEvent::HostColorSchemeChanged(_) | crate::raw_input::RawInputEvent::Unsupported => None, } } #[cfg(unix)] fn stdin_read_ready(reader: &R, timeout_ms: i32) -> Option { poll_read_ready(reader.as_raw_fd(), timeout_ms) } #[cfg(unix)] fn poll_read_ready(fd: i32, timeout_ms: i32) -> Option { #[repr(C)] struct PollFd { fd: i32, events: i16, revents: i16, } unsafe extern "C" { fn poll(fds: *mut PollFd, nfds: usize, timeout: i32) -> i32; } const POLLIN: i16 = 0x0001; let mut pfd = PollFd { fd, events: POLLIN, revents: 0, }; let result = unsafe { poll(&mut pfd as *mut PollFd, 1, timeout_ms) }; if result < 0 { None } else { Some(result > 0) } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(all(test, unix))] mod tests { // The stdin reader thread is hard to unit test since it reads from actual stdin. // Integration tests will verify the full client→server input flow. // Here we test the event type construction. use super::*; #[cfg(unix)] #[test] fn stdin_input_event_carries_raw_bytes() { let data = vec![0x1b, b'[', b'A']; // Up arrow escape sequence let event = ClientLoopEvent::StdinInput(data.clone()); match event { ClientLoopEvent::StdinInput(d) => assert_eq!(d, data), _ => panic!("expected StdinInput event"), } } #[test] fn palette_replies_are_forwarded_as_one_input_batch() { let (tx, mut rx) = mpsc::channel(4); let mut pending = Vec::new(); assert!(send_unix_input_chunks( vec![ b"\x1b]4;0;rgb:1111/2222/3333\x1b\\".to_vec(), b"\x1b]4;1;rgb:4444/5555/6666\x1b\\".to_vec(), ], &tx, &mut pending, )); assert!(rx.try_recv().is_err()); assert!(flush_unix_palette_input(&tx, &mut pending)); let ClientLoopEvent::StdinInput(data) = rx.try_recv().unwrap() else { panic!("expected palette input batch"); }; assert_eq!( data.windows(4) .filter(|window| *window == b"\x1b]4;") .count(), 2 ); assert!(pending.is_empty()); } #[test] fn raw_input_idle_flush_timeout_keeps_escape_responsive() { let timeout_ms = std::hint::black_box(crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS); assert!(timeout_ms <= 20); } #[cfg(not(target_os = "macos"))] #[test] fn windows_repeated_escape_keeps_second_escape_pending() { let mut framer = crate::raw_input::RawInputFramer::for_host_input(); let events = framer.push(b"\x1b\x1b"); assert_eq!(events.len(), 1); assert!(framer.has_pending_input()); assert_eq!(framer.flush_timeout().len(), 1); } #[test] fn mouse_active_escape_sequences_get_longer_reassembly_window() { let mut escape = crate::raw_input::RawInputByteFramer::default(); assert!(escape.push(b"\x1b").is_empty()); let mut mouse = crate::raw_input::RawInputByteFramer::default(); assert!(mouse.push(b"\x1b[<3").is_empty()); let mut unrelated = crate::raw_input::RawInputByteFramer::default(); assert!(unrelated.push(b"\x1b[49:33;2:").is_empty()); for framer in [&escape, &mouse, &unrelated] { assert_eq!( idle_flush_timeout_ms(framer, false), crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS ); } for framer in [&escape, &mouse] { assert_eq!( idle_flush_timeout_ms(framer, true), crate::raw_input::MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS ); } assert_eq!( idle_flush_timeout_ms(&unrelated, true), crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS ); let mouse_timeout_ms = std::hint::black_box(crate::raw_input::MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS); assert!(mouse_timeout_ms > 100); } } #[cfg(test)] mod windows_tests { use super::*; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; #[test] fn windows_control_chars_are_reframed_as_raw_bytes() { let escape = Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())); assert_eq!( windows_key_raw_bytes(&escape, false).as_deref(), Some(b"\x1b".as_slice()) ); let enter = Event::Key(KeyEvent::new(KeyCode::Char('\r'), KeyModifiers::empty())); assert_eq!( windows_key_raw_bytes(&enter, false).as_deref(), Some(b"\r".as_slice()) ); let printable = Event::Key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::empty())); assert_eq!(windows_key_raw_bytes(&printable, false), None); let pending_arrow_tail = Event::Key(KeyEvent::new(KeyCode::Char('['), KeyModifiers::empty())); assert_eq!( windows_key_raw_bytes(&pending_arrow_tail, true).as_deref(), Some(b"[".as_slice()) ); } #[test] fn windows_crossterm_printable_press_keeps_key_semantics_and_text() { let event = Event::Key(KeyEvent::new(KeyCode::Char('你'), KeyModifiers::empty())); assert_eq!( windows_crossterm_input_event(event), Some(crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Char('你'), modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, repeat_count: 1, generated_text: Some("你".to_string()), source: crate::protocol::ClientKeySource::Synthesized, }) ); } #[test] fn windows_ctrl_bracket_starts_raw_escape_sequence() { let ctrl_bracket = Event::Key(KeyEvent::new(KeyCode::Char('['), KeyModifiers::CONTROL)); assert_eq!( windows_key_raw_bytes(&ctrl_bracket, false).as_deref(), Some(b"\x1b".as_slice()) ); let mut framer = crate::raw_input::RawInputFramer::default(); assert!(framer.push(b"\x1b").is_empty()); let events = framer.push(b"[<35;48;26M"); assert_eq!(events.len(), 1); let event = windows_client_input_event_from_raw(events.into_iter().next().unwrap()) .expect("raw mouse converts"); assert!(matches!( event, crate::protocol::ClientInputEvent::Mouse { kind: crate::protocol::ClientMouseKind::Moved, column: 47, row: 25, modifiers: _, } )); } #[test] fn windows_ctrl_shift_bracket_stays_semantic() { let ctrl_shift_bracket = Event::Key(KeyEvent::new( KeyCode::Char('['), KeyModifiers::CONTROL | KeyModifiers::SHIFT, )); assert_eq!(windows_key_raw_bytes(&ctrl_shift_bracket, false), None); } #[cfg(windows)] #[test] fn windows_ctrl_d_semantic_event_encodes_to_eot() { let event = Event::Key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL)); assert_eq!(windows_key_raw_bytes(&event, false), None); let event = crate::protocol::ClientInputEvent::from_crossterm(event).expect("ctrl-d converts"); let raw = event.to_raw_input_event(); let crate::raw_input::RawInputEvent::Key(key) = raw else { panic!("expected key"); }; assert_eq!(key.code, KeyCode::Char('d')); assert_eq!(key.modifiers, KeyModifiers::CONTROL); assert_eq!( crate::input::encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy), b"\x04" ); } #[test] fn windows_pasted_printable_ctrl_i_routes_as_literal_i() { let event = Event::Key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::CONTROL)); assert_eq!( windows_key_raw_bytes(&event, false).as_deref(), Some(b"i".as_slice()) ); let event = Event::Key(KeyEvent::new( KeyCode::Char('I'), KeyModifiers::CONTROL | KeyModifiers::SHIFT, )); assert_eq!( windows_key_raw_bytes(&event, false).as_deref(), Some(b"I".as_slice()) ); } #[test] fn windows_eot_control_char_normalizes_to_ctrl_d() { let event = Event::Key(KeyEvent::new(KeyCode::Char('\u{4}'), KeyModifiers::empty())); let bytes = windows_key_raw_bytes(&event, false).expect("eot routes through raw framer"); assert_eq!(bytes, b"\x04"); let mut framer = crate::raw_input::RawInputFramer::default(); let events = framer.push(&bytes); assert_eq!(events.len(), 1); let event = windows_client_input_event_from_raw(events.into_iter().next().unwrap()) .expect("raw eot converts"); assert_eq!( event, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Char('d'), modifiers: KeyModifiers::CONTROL.bits(), kind: crate::protocol::ClientKeyKind::Press, repeat_count: 1, generated_text: None, source: crate::protocol::ClientKeySource::Vt { bytes: vec![4] }, } ); } #[test] fn windows_pending_escape_sequence_converts_to_semantic_arrow() { let mut framer = crate::raw_input::RawInputFramer::default(); assert!(framer.push(b"\x1b").is_empty()); assert!(framer.push(b"[").is_empty()); let events = framer.push(b"A"); assert_eq!(events.len(), 1); let event = windows_client_input_event_from_raw(events.into_iter().next().unwrap()) .expect("raw arrow converts"); assert_eq!( event, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Up, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, repeat_count: 1, generated_text: None, source: crate::protocol::ClientKeySource::Vt { bytes: b"\x1b[A".to_vec() }, } ); } #[test] fn windows_bare_escape_flushes_to_semantic_escape() { let mut framer = crate::raw_input::RawInputFramer::default(); assert!(framer.push(b"\x1b").is_empty()); let events = framer.flush_timeout(); assert_eq!(events.len(), 1); let event = windows_client_input_event_from_raw(events.into_iter().next().unwrap()) .expect("raw escape converts"); assert_eq!( event, crate::protocol::ClientInputEvent::Key { code: crate::protocol::ClientKeyCode::Esc, modifiers: 0, kind: crate::protocol::ClientKeyKind::Press, repeat_count: 1, generated_text: None, source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] }, } ); } }