diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-07 00:07:24 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-07 16:29:44 +0300 |
| commit | 3c2b46d24aa6c8011a1e198143752f2aa414d585 (patch) | |
| tree | a52280bccc824ca656f8bc986a756f30901974e4 /src/client | |
| parent | 5d7e56737ef30d9de5802b0684a9428c40131dda (diff) | |
feat: add native windows beta support
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/input.rs | 310 | ||||
| -rw-r--r-- | src/client/mod.rs | 170 |
2 files changed, 432 insertions, 48 deletions
diff --git a/src/client/input.rs b/src/client/input.rs index e9592db8..bbc37a58 100644 --- a/src/client/input.rs +++ b/src/client/input.rs @@ -1,22 +1,24 @@ //! Stdin input reading for the thin client. //! -//! Reads stdin bytes and forwards framed input to the main event loop. -//! Unlike the monolithic herdr, the thin client does NOT parse input into -//! key/mouse/paste events. It keeps enough byte-framing state to avoid splitting -//! terminal control strings, then sends bytes to the server as `ClientMessage::Input`. -//! The server handles semantic parsing. +//! 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::io::{self, Read}; 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; @@ -31,6 +33,15 @@ use super::ClientLoopEvent; /// 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<ClientLoopEvent>, should_quit: &Arc<AtomicBool>) { + #[cfg(windows)] + return windows_stdin_reader_loop(event_tx, should_quit); + + #[cfg(unix)] + unix_stdin_reader_loop(event_tx, should_quit); +} + +#[cfg(unix)] +fn unix_stdin_reader_loop(event_tx: mpsc::Sender<ClientLoopEvent>, should_quit: &Arc<AtomicBool>) { let stdin = io::stdin(); let mut reader = stdin.lock(); let mut scratch = [0u8; 4096]; @@ -70,16 +81,179 @@ pub fn stdin_reader_loop(event_tx: mpsc::Sender<ClientLoopEvent>, should_quit: & } } +#[cfg(windows)] +fn windows_stdin_reader_loop( + event_tx: mpsc::Sender<ClientLoopEvent>, + should_quit: &Arc<AtomicBool>, +) { + let mut framer = crate::raw_input::RawInputFramer::default(); + let mut raw_sequence_pending = false; + + while !should_quit.load(Ordering::Acquire) { + match crossterm::event::poll(Duration::from_millis(10)) { + Ok(true) => {} + Ok(false) => { + if raw_sequence_pending { + tracing::debug!("windows input raw sequence timed out; flushing"); + if !send_windows_raw_events(framer.flush_timeout(), &event_tx) { + return; + } + raw_sequence_pending = false; + } + continue; + } + Err(_) => break, + } + + let event = match crossterm::event::read() { + Ok(event) => event, + Err(_) => break, + }; + + 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" + ); + let events = framer.push(&bytes); + raw_sequence_pending = events.is_empty(); + if !send_windows_raw_events(events, &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; + } + raw_sequence_pending = false; + } + + if windows_event_is_control_key(&event) { + tracing::debug!(event = ?event, "windows control key forwarded as semantic input"); + } + + let Some(event) = crate::protocol::ClientInputEvent::from_crossterm(event) else { + continue; + }; + if event_tx + .blocking_send(ClientLoopEvent::StdinEvents(vec![event])) + .is_err() + { + return; + } + } + + if raw_sequence_pending { + let _ = send_windows_raw_events(framer.flush_timeout(), &event_tx); + } +} + +#[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(windows)] +fn windows_key_raw_bytes( + event: &crossterm::event::Event, + raw_sequence_pending: bool, +) -> Option<Vec<u8>> { + 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(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<crate::raw_input::RawInputEvent>, + event_tx: &mpsc::Sender<ClientLoopEvent>, +) -> bool { + let raw_event_count = events.len(); + let events = events + .into_iter() + .filter_map(windows_client_input_event_from_raw) + .collect::<Vec<_>>(); + 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(windows)] +fn windows_client_input_event_from_raw( + event: crate::raw_input::RawInputEvent, +) -> Option<crate::protocol::ClientInputEvent> { + match event { + crate::raw_input::RawInputEvent::Key(key) => Some(crate::protocol::ClientInputEvent::Key { + code: crate::protocol::ClientKeyCode::from_crossterm(key.code)?, + modifiers: key.modifiers.bits(), + kind: crate::protocol::ClientKeyKind::from_crossterm(key.kind), + }), + 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::Unsupported => None, + } +} + #[cfg(unix)] fn stdin_read_ready<R: AsRawFd>(reader: &R, timeout_ms: i32) -> Option<bool> { poll_read_ready(reader.as_raw_fd(), timeout_ms) } -#[cfg(not(unix))] -fn stdin_read_ready<R>(_reader: &R, _timeout_ms: i32) -> Option<bool> { - None -} - #[cfg(unix)] fn poll_read_ready(fd: i32, timeout_ms: i32) -> Option<bool> { #[repr(C)] @@ -113,7 +287,7 @@ fn poll_read_ready(fd: i32, timeout_ms: i32) -> Option<bool> { // Tests // --------------------------------------------------------------------------- -#[cfg(test)] +#[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. @@ -121,6 +295,7 @@ mod tests { use super::*; + #[cfg(unix)] #[test] fn stdin_input_event_carries_raw_bytes() { let data = vec![0x1b, b'[', b'A']; // Up arrow escape sequence @@ -131,3 +306,114 @@ mod tests { } } } + +#[cfg(all(test, windows))] +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_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_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, + } + ); + } + + #[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, + } + ); + } + + #[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, + } + ); + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 564b5e27..a895dae0 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -16,25 +16,31 @@ mod input; use std::collections::HashSet; use std::io::{self, Write as _}; -use std::os::unix::net::UnixStream; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; use crossterm::event::{ DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, - EnableFocusChange, EnableMouseCapture, KeyCode, KeyEventKind, KeyModifiers, MouseEventKind, - PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, + EnableFocusChange, EnableMouseCapture, }; +#[cfg(unix)] +use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers, MouseEventKind}; +#[cfg(not(windows))] +use crossterm::event::{PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}; use crossterm::execute; +use interprocess::local_socket::traits::Stream as _; +use interprocess::TryClone as _; use tracing::{debug, info, warn}; +use crate::ipc::LocalStream; use crate::protocol::render_ansi; use crate::protocol::{ - self, AttachScrollDirection, AttachScrollSource, ClientKeybindings, ClientLaunchMode, - ClientMessage, NotifyKind, RenderEncoding, ServerMessage, MAX_CLIPBOARD_IMAGE_PAYLOAD, - MAX_FRAME_SIZE, MAX_GRAPHICS_FRAME_SIZE, PROTOCOL_VERSION, + self, ClientKeybindings, ClientLaunchMode, ClientMessage, NotifyKind, RenderEncoding, + ServerMessage, MAX_FRAME_SIZE, MAX_GRAPHICS_FRAME_SIZE, PROTOCOL_VERSION, }; +#[cfg(unix)] +use crate::protocol::{AttachScrollDirection, AttachScrollSource, MAX_CLIPBOARD_IMAGE_PAYLOAD}; use crate::server::socket_paths::client_socket_path; static RECEIVED_KITTY_GRAPHICS_IDS: OnceLock<Mutex<HashSet<u32>>> = OnceLock::new(); @@ -58,17 +64,24 @@ struct ClientState { /// Direct attach prefix escape state. None for full-app clients. attach_escape: Option<AttachEscapeState>, /// Rows scrolled for one direct-attach wheel notch. + #[cfg(unix)] mouse_scroll_lines: usize, /// Whether outer focus gain should force a full host-terminal redraw. redraw_on_focus_gained: bool, } #[derive(Debug, Default)] +#[cfg(windows)] +struct AttachEscapeState; + +#[derive(Debug, Default)] +#[cfg(unix)] struct AttachEscapeState { pending_prefix: bool, } #[derive(Debug)] +#[cfg(unix)] enum AttachInputAction { Forward(Vec<u8>), Scroll { @@ -84,6 +97,7 @@ enum AttachInputAction { } impl AttachEscapeState { + #[cfg(unix)] fn filter_input( &mut self, data: Vec<u8>, @@ -126,6 +140,7 @@ impl AttachEscapeState { } } +#[cfg(unix)] fn attach_scroll_action( data: &[u8], viewport_rows: u16, @@ -304,20 +319,16 @@ fn setup_terminal_with_capabilities( if enable_client_protocols { if mouse_capture { - execute!(io::stdout(), EnableMouseCapture)?; + set_mouse_capture(true)?; } else { - execute!(io::stdout(), DisableMouseCapture)?; + set_mouse_capture(false)?; } - execute!( - io::stdout(), - EnableBracketedPaste, - EnableFocusChange, - PushKeyboardEnhancementFlags(crate::input::ime_compatible_keyboard_enhancement_flags()) - )?; + execute!(io::stdout(), EnableBracketedPaste, EnableFocusChange)?; + push_keyboard_enhancement_flags()?; } else if mouse_capture { - execute!(io::stdout(), EnableMouseCapture)?; + set_mouse_capture(true)?; } else { - execute!(io::stdout(), DisableMouseCapture)?; + set_mouse_capture(false)?; } let modify_other_keys_mode = enable_client_protocols @@ -354,7 +365,12 @@ fn set_mouse_capture(enabled: bool) -> io::Result<()> { if enabled { execute!(io::stdout(), EnableMouseCapture) } else { - execute!(io::stdout(), DisableMouseCapture) + match execute!(io::stdout(), DisableMouseCapture) { + Ok(()) => Ok(()), + #[cfg(windows)] + Err(err) if err.to_string() == "Initial console modes not set" => Ok(()), + Err(err) => Err(err), + } } } @@ -367,9 +383,9 @@ fn restore_terminal_state(reset_modify_other_keys: bool) { let _ = io::stdout().flush(); } + let _ = pop_keyboard_enhancement_flags(); let _ = execute!( io::stdout(), - PopKeyboardEnhancementFlags, DisableFocusChange, DisableBracketedPaste, DisableMouseCapture @@ -378,6 +394,29 @@ fn restore_terminal_state(reset_modify_other_keys: bool) { let _ = write_terminal_restore_postlude(&mut io::stdout()); } +#[cfg(not(windows))] +fn push_keyboard_enhancement_flags() -> io::Result<()> { + execute!( + io::stdout(), + PushKeyboardEnhancementFlags(crate::input::ime_compatible_keyboard_enhancement_flags()) + ) +} + +#[cfg(windows)] +fn push_keyboard_enhancement_flags() -> io::Result<()> { + Ok(()) +} + +#[cfg(not(windows))] +fn pop_keyboard_enhancement_flags() -> io::Result<()> { + execute!(io::stdout(), PopKeyboardEnhancementFlags) +} + +#[cfg(windows)] +fn pop_keyboard_enhancement_flags() -> io::Result<()> { + Ok(()) +} + impl Drop for TerminalGuard { fn drop(&mut self) { restore_terminal_state(self.reset_modify_other_keys); @@ -414,7 +453,7 @@ fn requested_keybindings() -> ClientKeybindings { /// Sends Hello with the terminal size and protocol version, reads the Welcome /// response. Returns Ok(()) on success, or an error if the server rejects us. fn do_handshake( - stream: &mut UnixStream, + stream: &mut LocalStream, cols: u16, rows: u16, cell_width_px: u32, @@ -445,13 +484,13 @@ fn do_handshake( .map_err(|e| ClientError::ConnectionFailed(io::Error::other(e.to_string())))?; // Read Welcome. - stream - .set_read_timeout(Some(Duration::from_secs(5))) - .map_err(ClientError::ConnectionFailed)?; + if let Err(err) = stream.set_recv_timeout(Some(Duration::from_secs(5))) { + debug!(err = %err, "client handshake read timeout unavailable"); + } let welcome: ServerMessage = protocol::read_message(stream, MAX_FRAME_SIZE)?; - stream - .set_read_timeout(None) - .map_err(ClientError::ConnectionFailed)?; + if let Err(err) = stream.set_recv_timeout(None) { + debug!(err = %err, "failed to clear client handshake read timeout"); + } match welcome { ServerMessage::Welcome { @@ -478,7 +517,11 @@ fn do_handshake( /// Internal events for the client event loop. enum ClientLoopEvent { /// Raw input bytes from stdin. + #[cfg(unix)] StdinInput(Vec<u8>), + /// Structured input events from platforms without Unix-style stdin bytes. + #[cfg(windows)] + StdinEvents(Vec<crate::protocol::ClientInputEvent>), /// Terminal resize detected. Resize(u16, u16, u32, u32), /// Server message received. @@ -503,6 +546,7 @@ pub fn run_client() -> io::Result<()> { } /// Runs a direct terminal attach client. +#[cfg(unix)] pub fn run_terminal_attach(terminal_id: String, takeover: bool) -> io::Result<()> { run_client_with_mode( RenderEncoding::TerminalAnsi, @@ -512,6 +556,16 @@ pub fn run_terminal_attach(terminal_id: String, takeover: bool) -> io::Result<() ) } +/// Direct terminal attach is Unix raw-byte input only until Windows gets a semantic attach path. +#[cfg(windows)] +pub fn run_terminal_attach(_terminal_id: String, _takeover: bool) -> io::Result<()> { + debug_assert!(!crate::platform::capabilities().direct_terminal_attach); + Err(io::Error::new( + io::ErrorKind::Unsupported, + "direct terminal attach is not supported on Windows yet", + )) +} + fn run_client_with_mode( requested_encoding: RenderEncoding, attach_request: Option<(String, bool)>, @@ -521,6 +575,7 @@ fn run_client_with_mode( init_logging(); let loaded_config = crate::config::Config::load(); + let mouse_capture = loaded_config.config.ui.mouse_capture; let mouse_scroll_lines = loaded_config.config.ui.mouse_scroll_lines(); let redraw_on_focus_gained = loaded_config.config.ui.redraw_on_focus_gained; let sound_config = loaded_config.config.ui.sound; @@ -533,7 +588,7 @@ fn run_client_with_mode( info!(path = %socket_path.display(), "{log_message}"); // Try to connect to the server. - let mut stream = match UnixStream::connect(&socket_path) { + let mut stream = match crate::ipc::connect_local_stream(&socket_path) { Ok(s) => s, Err(err) => { // Server unreachable — show clear error and exit. @@ -578,10 +633,10 @@ fn run_client_with_mode( // Now set up the terminal. This must happen AFTER the handshake succeeds, // so we don't leave the terminal in raw mode if the server rejects us. let direct_attach = attach_escape.is_some(); - let _guard = if direct_attach { + let terminal_guard = if direct_attach { setup_direct_attach_terminal() } else { - setup_terminal(false) + setup_terminal(mouse_capture) } .map_err(|err| { eprintln!("herdr: failed to set up terminal: {err}"); @@ -589,10 +644,10 @@ fn run_client_with_mode( })?; // Install a panic hook to restore the terminal on panic (same as monolithic). - let in_tmux = std::env::var("TMUX").is_ok(); + let panic_resets_modify_other_keys = terminal_guard.reset_modify_other_keys; let original_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - restore_terminal_state(in_tmux); + restore_terminal_state(panic_resets_modify_other_keys); original_hook(info); })); @@ -620,7 +675,7 @@ fn run_client_with_mode( mouse_scroll_lines, redraw_on_focus_gained, kitty_graphics_enabled, - false, + mouse_capture, negotiated_encoding, attach_escape, ) @@ -628,7 +683,7 @@ fn run_client_with_mode( }); // Restore the terminal before printing any final status message. - drop(_guard); + drop(terminal_guard); if let Err(err) = result { eprintln!("herdr: {err}"); @@ -660,7 +715,7 @@ fn run_client_with_mode( /// - server reader thread → reads ServerMessages and sends to main loop /// - main loop: coordinates input, output, and server communication async fn run_client_loop( - stream: UnixStream, + stream: LocalStream, cols: u16, rows: u16, should_quit: Arc<AtomicBool>, @@ -672,6 +727,9 @@ async fn run_client_loop( negotiated_encoding: RenderEncoding, attach_escape: Option<AttachEscapeState>, ) -> Result<(), ClientError> { + #[cfg(windows)] + let _ = mouse_scroll_lines; + let mut state = ClientState { blit_encoder: render_ansi::BlitEncoder::new(), mouse_capture_active, @@ -679,6 +737,7 @@ async fn run_client_loop( sound_config, kitty_graphics_enabled, attach_escape, + #[cfg(unix)] mouse_scroll_lines, redraw_on_focus_gained, }; @@ -694,7 +753,7 @@ async fn run_client_loop( input::stdin_reader_loop(stdin_tx, &stdin_quit); }); - if state.attach_escape.is_none() { + if state.attach_escape.is_none() && should_query_host_terminal_theme() { query_host_terminal_theme(); } @@ -739,6 +798,7 @@ async fn run_client_loop( }; match event { + #[cfg(unix)] ClientLoopEvent::StdinInput(data) => { let data = if let Some(attach_escape) = &mut state.attach_escape { match attach_escape.filter_input( @@ -817,6 +877,26 @@ async fn run_client_loop( return Err(ClientError::ConnectionLost(e)); } } + #[cfg(windows)] + ClientLoopEvent::StdinEvents(events) => { + if state.attach_escape.is_some() { + continue; + } + let raw_events = events + .iter() + .map(crate::protocol::ClientInputEvent::to_raw_input_event) + .collect::<Vec<_>>(); + if crate::raw_input::events_require_host_surface_redraw( + &raw_events, + state.redraw_on_focus_gained, + ) { + state.request_full_redraw(); + } + let msg = ClientMessage::InputEvents { events }; + if let Err(e) = write_to_server(&mut write_stream, &msg) { + return Err(ClientError::ConnectionLost(e)); + } + } ClientLoopEvent::Resize(new_cols, new_rows, cell_width_px, cell_height_px) => { state.reported_size = (new_cols, new_rows); let msg = ClientMessage::Resize { @@ -917,7 +997,7 @@ async fn run_client_loop( /// Blocking thread that reads ServerMessages from the server and sends them /// to the main event loop. fn server_reader_thread( - mut stream: UnixStream, + mut stream: LocalStream, event_tx: tokio::sync::mpsc::Sender<ClientLoopEvent>, should_quit: &Arc<AtomicBool>, max_frame_size: usize, @@ -970,7 +1050,7 @@ fn server_reader_thread( // --------------------------------------------------------------------------- /// Writes a message to the server stream (blocking). -fn write_to_server(stream: &mut UnixStream, msg: &ClientMessage) -> io::Result<()> { +fn write_to_server(stream: &mut LocalStream, msg: &ClientMessage) -> io::Result<()> { protocol::write_message(stream, msg).map_err(|e| io::Error::other(e.to_string())) } @@ -1063,6 +1143,7 @@ fn sound_from_notify_message(message: &str) -> Option<crate::sound::Sound> { } } +#[cfg(unix)] fn should_bridge_clipboard_image_paste(data: &[u8]) -> bool { if data == b"\x1b[200~\x1b[201~" { return true; @@ -1254,6 +1335,10 @@ fn query_host_terminal_theme() { let _ = write_host_terminal_theme_query(io::stdout()); } +fn should_query_host_terminal_theme() -> bool { + !cfg!(windows) +} + fn write_host_terminal_theme_query(mut writer: impl io::Write) -> io::Result<()> { writer.write_all(crate::terminal_theme::HOST_COLOR_QUERY_SEQUENCE.as_bytes())?; writer.flush() @@ -1330,6 +1415,7 @@ mod tests { } } + #[cfg(unix)] #[test] fn clipboard_image_paste_bridge_triggers_on_ctrl_v_and_empty_paste() { assert!(should_bridge_clipboard_image_paste(&[0x16])); @@ -1400,12 +1486,18 @@ mod tests { } #[test] + fn host_terminal_theme_query_is_disabled_on_windows() { + assert_eq!(should_query_host_terminal_theme(), !cfg!(windows)); + } + + #[test] fn terminal_restore_postlude_restores_visible_default_cursor() { let mut output = Vec::new(); write_terminal_restore_postlude(&mut output).unwrap(); assert_eq!(output, b"\x1b[?25h\x1b[0 q"); } + #[cfg(unix)] #[test] fn attach_escape_detaches_on_prefix_q() { let mut escape = AttachEscapeState::default(); @@ -1419,6 +1511,7 @@ mod tests { )); } + #[cfg(unix)] #[test] fn attach_escape_sends_literal_prefix_on_double_prefix() { let mut escape = AttachEscapeState::default(); @@ -1432,6 +1525,7 @@ mod tests { } } + #[cfg(unix)] #[test] fn attach_escape_forwards_prefix_before_non_escape_key() { let mut escape = AttachEscapeState::default(); @@ -1445,6 +1539,7 @@ mod tests { } } + #[cfg(unix)] #[test] fn attach_escape_turns_wheel_into_scroll_action() { let mut escape = AttachEscapeState::default(); @@ -1467,6 +1562,7 @@ mod tests { } } + #[cfg(unix)] #[test] fn attach_escape_swallows_non_wheel_mouse_reports() { let mut escape = AttachEscapeState::default(); @@ -1476,6 +1572,7 @@ mod tests { )); } + #[cfg(unix)] #[test] fn attach_escape_turns_plain_page_keys_into_scroll_actions() { let mut escape = AttachEscapeState::default(); @@ -1518,6 +1615,7 @@ mod tests { } } + #[cfg(unix)] #[test] fn attach_escape_forwards_modified_page_key() { let mut escape = AttachEscapeState::default(); |
