diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-01 15:48:22 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 15:48:22 +0300 |
| commit | 207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch) | |
| tree | d7b037ecdf629224a048c841d11659efdc23507d /src/raw_input.rs | |
| parent | 8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff) | |
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell
* feat: add client-owned shell mouse controls
* feat: complete client-owned shell chrome
* feat: add client-owned shell settings and notifications
* feat: run client shell custom commands
* feat: render popup terminals in client shell
* feat: add client-owned scrollback and copy mode
* fix: preserve client worktree trust defaults
* feat: render startup diagnostics in client shell
* feat: render onboarding in client shell
* feat: render product announcements in client shell
* feat: render release notes in client shell
* feat: render mobile switcher in client shell
* feat: render kitty graphics in client shell
* fix: preserve client shell lifecycle coherence
* refactor: route client shell commands through endpoint connections
* refactor: make the client-rendered shell the default
* refactor: remove monolithic launch mode
* refactor: complete client-rendered shell cutover
* perf: retain client pane surface updates
* perf: scale retained pane surface updates
* perf: remove remote pane input latency
* test: make client shell checks platform-independent
* fix: prevent client timer starvation
* test: fix client shell platform gates
* test: accept retained surfaces in cross-area checks
* test: make client mode assertions platform-neutral
* fix: badge only outdated integrations
Diffstat (limited to 'src/raw_input.rs')
| -rw-r--r-- | src/raw_input.rs | 766 |
1 files changed, 111 insertions, 655 deletions
diff --git a/src/raw_input.rs b/src/raw_input.rs index 5f750ca8..3f2e102e 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -1,88 +1,10 @@ -use std::io::Read; - use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; /// Parse raw terminal input bytes into a list of `RawInputEvent`s. /// -/// This is used by the headless server to route client input through the -/// same parsing pipeline that the monolithic binary uses for stdin. -/// Incomplete sequences at the end of the buffer are flushed as best-effort -/// (same logic as the live input reader). -#[allow(dead_code)] -pub fn parse_raw_input_bytes(data: &[u8]) -> Vec<RawInputEvent> { - // Delegate to the sync version which actually works. - parse_raw_input_bytes_sync(data) -} - -/// A raw input event paired with the byte range it consumed from the original buffer. -#[cfg(test)] -#[derive(Debug)] -pub struct RawInputEventWithRange { - /// The parsed event. - pub event: RawInputEvent, - /// Byte offset where this event starts in the original buffer. - pub start: usize, - /// Number of bytes this event consumed from the original buffer. - /// For events generated from flushed incomplete bytes, `len` may be 0 - /// (synthetic events that don't map to original bytes). - pub len: usize, -} - -/// Parse raw terminal input bytes into a list of `RawInputEventWithRange`s (synchronous version). -/// -/// Unlike `parse_raw_input_bytes_sync`, this preserves the byte offset for each -/// event, allowing callers to write only the specific bytes for each event -/// instead of the entire input buffer. -#[cfg(test)] -pub fn parse_raw_input_bytes_with_ranges(data: &[u8]) -> Vec<RawInputEventWithRange> { - let mut buffer = data.to_vec(); - let mut events = Vec::new(); - let mut offset = 0usize; - - while let Some((event, consumed)) = extract_one_event(&buffer) { - buffer.drain(..consumed); - events.push(RawInputEventWithRange { - event, - start: offset, - len: consumed, - }); - offset += consumed; - } - - // Flush remaining incomplete bytes. - if !buffer.is_empty() { - if buffer.as_slice() == [ESC] { - events.push(RawInputEventWithRange { - event: RawInputEvent::Key( - TerminalKey::new(crossterm::event::KeyCode::Esc, KeyModifiers::empty()) - .with_vt_bytes(vec![ESC]), - ), - start: offset, - len: 1, - }); - } else if matches!( - control_string(&buffer), - Some(ControlString::Incomplete { .. }) - ) { - return events; - } else if let Ok(text) = std::str::from_utf8(&buffer) { - if let Some(key) = parse_terminal_key_sequence(text) { - events.push(RawInputEventWithRange { - event: RawInputEvent::Key(key.with_text_commit().with_vt_bytes(buffer.clone())), - start: offset, - len: buffer.len(), - }); - } - } - } - - events -} - -/// Parse raw terminal input bytes into a list of `RawInputEvent`s (synchronous version). -/// -/// Unlike `parse_raw_input_bytes`, this directly extracts events without -/// going through a channel, making it suitable for synchronous use. +/// This directly extracts events without going through a channel, making it +/// suitable for synchronous use. +#[cfg(any(unix, test))] pub fn parse_raw_input_bytes_sync(data: &[u8]) -> Vec<RawInputEvent> { let mut framer = RawInputFramer::default(); let mut events = framer.push(data); @@ -90,10 +12,6 @@ pub fn parse_raw_input_bytes_sync(data: &[u8]) -> Vec<RawInputEvent> { events } -#[cfg(unix)] -use std::os::fd::AsRawFd; -use tokio::sync::mpsc; - use crate::input::{parse_terminal_key_sequence, TerminalKey, TextCommit}; use crate::terminal_theme::{ parse_default_color_response, parse_palette_color_response, DefaultColorKind, HostAppearance, @@ -101,7 +19,9 @@ use crate::terminal_theme::{ }; const ESC: u8 = 0x1b; +#[cfg(any(unix, test))] pub(crate) const RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS: i32 = 10; +#[cfg(any(unix, test))] pub(crate) const MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS: i32 = 150; pub(crate) const GHOSTTY_COLOR_SCHEME_DARK_REPORT: &[u8] = b"\x1b[?997;1n"; pub(crate) const GHOSTTY_COLOR_SCHEME_LIGHT_REPORT: &[u8] = b"\x1b[?997;2n"; @@ -157,6 +77,7 @@ pub(crate) struct RawInputFramer { } impl RawInputFramer { + #[cfg(any(windows, test))] pub(crate) fn for_host_input() -> Self { Self { byte_framer: RawInputByteFramer::for_host_input(), @@ -167,27 +88,11 @@ impl RawInputFramer { Self::events_from_chunks(self.byte_framer.push(data)) } - pub(crate) fn host_color_query_sent(&mut self) { - self.byte_framer.host_color_query_sent(); - } - - pub(crate) fn enable_host_color_scheme_change_tracking(&mut self) { - self.byte_framer.enable_host_color_scheme_change_tracking(); - } - - #[cfg(any(not(windows), test))] - pub(crate) fn enable_host_appearance_query_on_focus(&mut self) { - self.byte_framer.enable_host_appearance_query_on_focus(); - } - + #[cfg(any(windows, all(test, not(target_os = "macos"))))] pub(crate) fn has_pending_input(&self) -> bool { self.byte_framer.has_pending_input() } - pub(crate) fn has_pending_incomplete_mouse_sequence(&self) -> bool { - self.byte_framer.has_pending_incomplete_mouse_sequence() - } - #[cfg(any(windows, test))] pub(crate) fn has_pending_bracketed_paste(&self) -> bool { self.byte_framer.has_pending_bracketed_paste() @@ -281,6 +186,7 @@ impl RawInputByteFramer { || self.host_appearance_reply_awaited } + #[cfg(any(unix, test))] pub(crate) fn enable_host_color_scheme_change_tracking(&mut self) { self.host_color_scheme_change_tracking = true; } @@ -301,6 +207,7 @@ impl RawInputByteFramer { self.buffer.as_slice() == [ESC] } + #[cfg(any(unix, test))] pub(crate) fn has_pending_incomplete_mouse_sequence(&self) -> bool { starts_with_incomplete_sgr_mouse_sequence(&self.buffer) || starts_with_incomplete_default_mouse_sequence(&self.buffer) @@ -612,6 +519,7 @@ fn plausible_control_string_tail(family: ControlStringFamily, buffer: &[u8]) -> } } +#[cfg(any(unix, test))] pub(crate) fn events_require_host_surface_redraw( events: &[RawInputEvent], redraw_on_focus_gained: bool, @@ -636,191 +544,6 @@ pub(crate) fn events_require_host_terminal_theme_query(events: &[RawInputEvent]) .any(|event| matches!(event, RawInputEvent::HostColorSchemeChanged(_))) } -fn input_flush_timeout_ms(framer: &RawInputFramer) -> i32 { - if framer.has_pending_incomplete_mouse_sequence() { - MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS - } else { - RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS - } -} - -pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> { - let (tx, rx) = mpsc::channel(256); - - std::thread::spawn(move || { - let stdin = std::io::stdin(); - let mut reader = stdin.lock(); - let mut scratch = [0u8; 1024]; - let mut framer = RawInputFramer::for_host_input(); - framer.host_color_query_sent(); - framer.enable_host_color_scheme_change_tracking(); - #[cfg(not(windows))] - framer.enable_host_appearance_query_on_focus(); - let mut pending_palette = Vec::new(); - - loop { - match reader.read(&mut scratch) { - Ok(0) => break, - Ok(n) => { - send_raw_input_events(framer.push(&scratch[..n]), &tx, &mut pending_palette); - - if stdin_read_ready(&reader, input_flush_timeout_ms(&framer)) == Some(false) { - let had_pending = framer.has_pending_input(); - let events = framer.flush_timeout(); - let held_escape = had_pending && events.is_empty(); - send_raw_input_events(events, &tx, &mut pending_palette); - flush_host_palette_events(&tx, &mut pending_palette); - if held_escape - && stdin_read_ready(&reader, RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS) - == Some(false) - { - send_raw_input_events( - framer.flush_timeout(), - &tx, - &mut pending_palette, - ); - flush_host_palette_events(&tx, &mut pending_palette); - } - } - } - Err(_) => break, - } - } - }); - - rx -} - -fn send_raw_input_events( - events: Vec<RawInputEvent>, - tx: &mpsc::Sender<RawInputEvent>, - pending_palette: &mut Vec<(u8, RgbColor)>, -) { - for event in events { - match event { - RawInputEvent::HostPaletteColors { colors } => { - pending_palette.extend(colors); - if pending_palette.len() == 256 { - flush_host_palette_events(tx, pending_palette); - } - } - event @ RawInputEvent::HostDefaultColor { .. } => { - let _ = tx.blocking_send(event); - } - event => { - flush_host_palette_events(tx, pending_palette); - let _ = tx.blocking_send(event); - } - } - } -} - -fn flush_host_palette_events( - tx: &mpsc::Sender<RawInputEvent>, - pending_palette: &mut Vec<(u8, RgbColor)>, -) { - if pending_palette.is_empty() { - return; - } - let colors = std::mem::take(pending_palette); - let _ = tx.blocking_send(RawInputEvent::HostPaletteColors { colors }); -} - -#[cfg(test)] -fn drain_buffer(buffer: &mut Vec<u8>, tx: &mpsc::Sender<RawInputEvent>) { - for bytes in drain_complete_input_bytes(buffer) { - let Some((event, _consumed)) = extract_one_event(&bytes) else { - continue; - }; - tracing::debug!(raw_bytes = ?bytes, event = ?event, "raw input event parsed"); - let _ = tx.blocking_send(event); - } -} - -#[cfg(test)] -pub(crate) fn drain_complete_input_bytes(buffer: &mut Vec<u8>) -> Vec<Vec<u8>> { - let mut chunks = Vec::new(); - - while let Some((_event, consumed)) = extract_one_event(buffer) { - chunks.push(buffer[..consumed].to_vec()); - buffer.drain(..consumed); - } - - chunks -} - -#[cfg(test)] -fn flush_incomplete_buffer(buffer: &mut Vec<u8>, tx: &mpsc::Sender<RawInputEvent>) { - if let Some(bytes) = flush_incomplete_input_bytes(buffer) { - if bytes.as_slice() == [ESC] { - let _ = tx.blocking_send(RawInputEvent::Key( - TerminalKey::new(crossterm::event::KeyCode::Esc, KeyModifiers::empty()) - .with_vt_bytes(bytes), - )); - return; - } - - let Some((event, _consumed)) = extract_one_event(&bytes) else { - return; - }; - let _ = tx.blocking_send(event); - } -} - -#[cfg(test)] -pub(crate) fn flush_incomplete_input_bytes(buffer: &mut Vec<u8>) -> Option<Vec<u8>> { - let mut framer = RawInputByteFramer { - buffer: std::mem::take(buffer), - ..Default::default() - }; - let mut chunks = framer.flush_timeout(); - *buffer = framer.buffer; - chunks.pop() -} - -#[cfg(unix)] -fn stdin_read_ready<R: AsRawFd>(_reader: &R, _timeout_ms: i32) -> Option<bool> { - #[cfg(unix)] - { - let fd = _reader.as_raw_fd(); - poll_read_ready(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)] - 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) - } -} - fn extract_one_event(buffer: &[u8]) -> Option<(RawInputEvent, usize)> { if buffer.is_empty() { return None; @@ -1089,6 +812,7 @@ fn starts_with_incomplete_sgr_mouse_sequence(buffer: &[u8]) -> bool { .all(|byte| byte.is_ascii_digit() || *byte == b';') } +#[cfg(any(unix, test))] fn starts_with_incomplete_default_mouse_sequence(buffer: &[u8]) -> bool { buffer.starts_with(b"\x1b[M") && buffer.len() < 6 } @@ -1388,19 +1112,6 @@ mod tests { modifiers } - fn collect_events(rx: &mut mpsc::Receiver<RawInputEvent>) -> Vec<RawInputEvent> { - let mut events = Vec::new(); - while let Ok(event) = rx.try_recv() { - events.push(event); - } - events - } - - fn drain_chunk(buffer: &mut Vec<u8>, tx: &mpsc::Sender<RawInputEvent>, chunk: &[u8]) { - buffer.extend_from_slice(chunk); - drain_buffer(buffer, tx); - } - #[test] fn parses_kitty_shift_letter_release() { let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b[108:76;2:3u").unwrap() @@ -1456,7 +1167,8 @@ mod tests { #[test] fn parses_default_mouse_encoding() { - let events = parse_raw_input_bytes_sync(b"\x1b[MCN1"); + let mut framer = RawInputFramer::default(); + let events = framer.push(b"\x1b[MCN1"); let [RawInputEvent::Mouse(mouse)] = events.as_slice() else { panic!("expected one mouse event"); }; @@ -1467,13 +1179,16 @@ mod tests { #[test] fn rejected_default_mouse_frame_preserves_trailing_input() { - let events = parse_raw_input_bytes_with_ranges(b"\x1b[M\x82AAx"); + let mut framer = RawInputFramer::default(); + let events = framer.push(b"\x1b[M\x82AAx"); assert_eq!(events.len(), 2); - assert!(matches!(events[0].event, RawInputEvent::Unsupported)); - assert_eq!((events[0].start, events[0].len), (0, 6)); - assert!(matches!(events[1].event, RawInputEvent::Key(_))); - assert_eq!((events[1].start, events[1].len), (6, 1)); + assert!(matches!(events[0], RawInputEvent::Unsupported)); + assert_raw_key( + events.into_iter().nth(1).unwrap(), + KeyCode::Char('x'), + KeyModifiers::empty(), + ); } #[test] @@ -1822,15 +1537,16 @@ mod tests { #[test] fn flushes_lone_escape_after_timeout() { - let (tx, mut rx) = mpsc::channel(4); - let mut buffer = vec![ESC]; - flush_incomplete_buffer(&mut buffer, &tx); - assert!(buffer.is_empty()); - let event = rx.try_recv().unwrap(); - let RawInputEvent::Key(key) = event else { - panic!("expected key"); - }; - assert_eq!(key.code, KeyCode::Esc); + let mut framer = RawInputFramer::default(); + assert!(framer.push(&[ESC]).is_empty()); + + let events = framer.flush_timeout(); + assert_eq!(events.len(), 1); + assert_raw_key( + events.into_iter().next().unwrap(), + KeyCode::Esc, + KeyModifiers::empty(), + ); } #[test] @@ -1944,16 +1660,10 @@ mod tests { #[test] fn chunked_legacy_arrow_waits_for_completion() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); - - drain_chunk(&mut buffer, &tx, b"\x1b"); - assert_eq!(buffer, b"\x1b"); - assert!(collect_events(&mut rx).is_empty()); + let mut framer = RawInputFramer::default(); - drain_chunk(&mut buffer, &tx, b"[A"); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(b"\x1b").is_empty()); + let events = framer.push(b"[A"); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -1964,16 +1674,10 @@ mod tests { #[test] fn lone_escape_is_buffered_until_timeout_flush() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); - - drain_chunk(&mut buffer, &tx, b"\x1b"); - assert_eq!(buffer, b"\x1b"); - assert!(collect_events(&mut rx).is_empty()); + let mut framer = RawInputFramer::default(); - flush_incomplete_buffer(&mut buffer, &tx); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(b"\x1b").is_empty()); + let events = framer.flush_timeout(); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -1984,16 +1688,10 @@ mod tests { #[test] fn escape_followed_by_arrow_before_flush_does_not_emit_escape() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); - - drain_chunk(&mut buffer, &tx, b"\x1b"); - assert_eq!(buffer, b"\x1b"); - assert!(collect_events(&mut rx).is_empty()); + let mut framer = RawInputFramer::default(); - drain_chunk(&mut buffer, &tx, b"[B"); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(b"\x1b").is_empty()); + let events = framer.push(b"[B"); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -2101,37 +1799,6 @@ mod tests { } #[test] - fn legacy_reader_extends_incomplete_mouse_timeouts() { - let mut sgr_mouse = RawInputFramer::default(); - assert!(sgr_mouse.push(b"\x1b[<3").is_empty()); - assert_eq!( - input_flush_timeout_ms(&sgr_mouse), - MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS - ); - - let report = b"\x1b[MCN1"; - for split in 3..report.len() { - let mut default_mouse = RawInputFramer::default(); - assert!(default_mouse.push(&report[..split]).is_empty()); - assert_eq!( - input_flush_timeout_ms(&default_mouse), - MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS - ); - assert!(matches!( - default_mouse.push(&report[split..]).as_slice(), - [RawInputEvent::Mouse(_)] - )); - } - - let mut escape = RawInputFramer::default(); - assert!(escape.push(b"\x1b").is_empty()); - assert_eq!( - input_flush_timeout_ms(&escape), - RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS - ); - } - - #[test] fn sgr_mouse_sequence_split_after_button_prefix_is_reassembled_before_timeout() { let mut framer = RawInputFramer::default(); @@ -2244,16 +1911,10 @@ mod tests { #[test] fn escape_followed_by_alt_char_before_flush_becomes_alt_key() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); - - drain_chunk(&mut buffer, &tx, b"\x1b"); - assert_eq!(buffer, b"\x1b"); - assert!(collect_events(&mut rx).is_empty()); + let mut framer = RawInputFramer::default(); - drain_chunk(&mut buffer, &tx, b"b"); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(b"\x1b").is_empty()); + let events = framer.push(b"b"); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -2264,16 +1925,10 @@ mod tests { #[test] fn chunked_kitty_sequence_waits_for_completion() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); - - drain_chunk(&mut buffer, &tx, b"\x1b[49:33;2:"); - assert_eq!(buffer, b"\x1b[49:33;2:"); - assert!(collect_events(&mut rx).is_empty()); + let mut framer = RawInputFramer::default(); - drain_chunk(&mut buffer, &tx, b"1u"); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(b"\x1b[49:33;2:").is_empty()); + let events = framer.push(b"1u"); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -2284,16 +1939,10 @@ mod tests { #[test] fn chunked_bracketed_paste_waits_for_terminator() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); - - drain_chunk(&mut buffer, &tx, b"\x1b[200~hello"); - assert_eq!(buffer, b"\x1b[200~hello"); - assert!(collect_events(&mut rx).is_empty()); + let mut framer = RawInputFramer::default(); - drain_chunk(&mut buffer, &tx, b"\x1b[201~"); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(b"\x1b[200~hello").is_empty()); + let events = framer.push(b"\x1b[201~"); assert_eq!(events.len(), 1); let RawInputEvent::Paste(text) = &events[0] else { panic!("expected paste"); @@ -2303,20 +1952,11 @@ mod tests { #[test] fn incomplete_bracketed_paste_is_not_flushed_on_timeout() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); - - drain_chunk(&mut buffer, &tx, b"\x1b[200~hello\nworld"); - assert_eq!(buffer, b"\x1b[200~hello\nworld"); - assert!(collect_events(&mut rx).is_empty()); - - flush_incomplete_buffer(&mut buffer, &tx); - assert_eq!(buffer, b"\x1b[200~hello\nworld"); - assert!(collect_events(&mut rx).is_empty()); + let mut framer = RawInputFramer::default(); - drain_chunk(&mut buffer, &tx, b"\x1b[201~"); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(b"\x1b[200~hello\nworld").is_empty()); + assert!(framer.flush_timeout().is_empty()); + let events = framer.push(b"\x1b[201~"); assert_eq!(events.len(), 1); let RawInputEvent::Paste(text) = &events[0] else { panic!("expected paste"); @@ -2326,45 +1966,48 @@ mod tests { #[test] fn complete_utf8_char_before_incomplete_char_is_drained() { - let mut buffer = "你".as_bytes().to_vec(); - buffer.push("好".as_bytes()[0]); - - let chunks = drain_complete_input_bytes(&mut buffer); + let mut framer = RawInputByteFramer::default(); + let mut input = "你".as_bytes().to_vec(); + input.push("好".as_bytes()[0]); - assert_eq!(chunks, vec!["你".as_bytes().to_vec()]); - assert_eq!(buffer, vec!["好".as_bytes()[0]]); + assert_eq!(framer.push(&input), vec!["你".as_bytes().to_vec()]); + assert_eq!(framer.push(&[]), Vec::<Vec<u8>>::new()); } #[test] fn incomplete_utf8_prefix_is_not_flushed_on_timeout() { - let mut buffer = vec!["好".as_bytes()[0]]; + let mut framer = RawInputByteFramer::default(); + let prefix = &"好".as_bytes()[..1]; - assert_eq!(flush_incomplete_input_bytes(&mut buffer), None); - assert_eq!(buffer, vec!["好".as_bytes()[0]]); + assert!(framer.push(prefix).is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert_eq!( + framer.push(&"好".as_bytes()[1..]), + vec!["好".as_bytes().to_vec()] + ); } #[test] fn invalid_utf8_lead_byte_is_flushed_instead_of_buffered_forever() { - let mut buffer = vec![0xC0]; + let mut framer = RawInputByteFramer::default(); - assert_eq!(flush_incomplete_input_bytes(&mut buffer), None); - assert!(buffer.is_empty()); + assert!(framer.push(&[0xC0]).is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert!(!framer.has_pending_input()); } #[test] fn complete_utf8_char_before_incomplete_char_survives_timeout_and_next_chunk() { - let mut buffer = "你".as_bytes().to_vec(); - buffer.push("好".as_bytes()[0]); - - let chunks = drain_complete_input_bytes(&mut buffer); - assert_eq!(chunks, vec!["你".as_bytes().to_vec()]); - assert_eq!(flush_incomplete_input_bytes(&mut buffer), None); - assert_eq!(buffer, vec!["好".as_bytes()[0]]); + let mut framer = RawInputByteFramer::default(); + let mut input = "你".as_bytes().to_vec(); + input.push("好".as_bytes()[0]); - buffer.extend_from_slice(&"好".as_bytes()[1..]); - let chunks = drain_complete_input_bytes(&mut buffer); - assert_eq!(chunks, vec!["好".as_bytes().to_vec()]); - assert!(buffer.is_empty()); + assert_eq!(framer.push(&input), vec!["你".as_bytes().to_vec()]); + assert!(framer.flush_timeout().is_empty()); + assert_eq!( + framer.push(&"好".as_bytes()[1..]), + vec!["好".as_bytes().to_vec()] + ); } #[test] @@ -2380,20 +2023,12 @@ mod tests { #[test] fn chunked_alt_utf8_waits_for_continuation_byte_after_escape() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); + let mut framer = RawInputFramer::default(); let bytes = "\x1bé".as_bytes(); - drain_chunk(&mut buffer, &tx, &bytes[..2]); - assert_eq!(buffer, bytes[..2]); - assert!(collect_events(&mut rx).is_empty()); - flush_incomplete_buffer(&mut buffer, &tx); - assert_eq!(buffer, bytes[..2]); - assert!(collect_events(&mut rx).is_empty()); - - drain_chunk(&mut buffer, &tx, &bytes[2..]); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(&bytes[..2]).is_empty()); + assert!(framer.flush_timeout().is_empty()); + let events = framer.push(&bytes[2..]); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -2404,16 +2039,10 @@ mod tests { #[test] fn chunked_utf8_waits_for_continuation_byte() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); - - drain_chunk(&mut buffer, &tx, "é".as_bytes().get(..1).unwrap()); - assert_eq!(buffer, vec![0xC3]); - assert!(collect_events(&mut rx).is_empty()); + let mut framer = RawInputFramer::default(); - drain_chunk(&mut buffer, &tx, "é".as_bytes().get(1..).unwrap()); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(&"é".as_bytes()[..1]).is_empty()); + let events = framer.push(&"é".as_bytes()[1..]); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -2424,27 +2053,14 @@ mod tests { #[test] fn chunked_cjk_utf8_waits_for_all_continuation_bytes() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); + let mut framer = RawInputFramer::default(); let bytes = "好".as_bytes(); - drain_chunk(&mut buffer, &tx, &bytes[..1]); - assert_eq!(buffer, bytes[..1]); - assert!(collect_events(&mut rx).is_empty()); - flush_incomplete_buffer(&mut buffer, &tx); - assert_eq!(buffer, bytes[..1]); - assert!(collect_events(&mut rx).is_empty()); - - drain_chunk(&mut buffer, &tx, &bytes[1..2]); - assert_eq!(buffer, bytes[..2]); - assert!(collect_events(&mut rx).is_empty()); - flush_incomplete_buffer(&mut buffer, &tx); - assert_eq!(buffer, bytes[..2]); - assert!(collect_events(&mut rx).is_empty()); - - drain_chunk(&mut buffer, &tx, &bytes[2..]); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + assert!(framer.push(&bytes[..1]).is_empty()); + assert!(framer.flush_timeout().is_empty()); + assert!(framer.push(&bytes[1..2]).is_empty()); + assert!(framer.flush_timeout().is_empty()); + let events = framer.push(&bytes[2..]); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -2455,22 +2071,15 @@ mod tests { #[test] fn chunked_four_byte_utf8_waits_for_all_continuation_bytes() { - let (tx, mut rx) = mpsc::channel(8); - let mut buffer = Vec::new(); + let mut framer = RawInputFramer::default(); let bytes = "🙂".as_bytes(); for split in 1..bytes.len() { - drain_chunk(&mut buffer, &tx, &bytes[split - 1..split]); - assert_eq!(buffer, bytes[..split]); - assert!(collect_events(&mut rx).is_empty()); - flush_incomplete_buffer(&mut buffer, &tx); - assert_eq!(buffer, bytes[..split]); - assert!(collect_events(&mut rx).is_empty()); + assert!(framer.push(&bytes[split - 1..split]).is_empty()); + assert!(framer.flush_timeout().is_empty()); } - drain_chunk(&mut buffer, &tx, &bytes[bytes.len() - 1..]); - assert!(buffer.is_empty()); - let events = collect_events(&mut rx); + let events = framer.push(&bytes[bytes.len() - 1..]); assert_eq!(events.len(), 1); assert_raw_key( events.into_iter().next().unwrap(), @@ -2486,114 +2095,39 @@ mod tests { text.len() > 4096, "test input should exceed the client read buffer" ); - let mut buffer = text.as_bytes().to_vec(); + let mut framer = RawInputByteFramer::default(); - let chunks = drain_complete_input_bytes(&mut buffer); + let chunks = framer.push(text.as_bytes()); let rebuilt: Vec<u8> = chunks.into_iter().flatten().collect(); - assert!(buffer.is_empty()); + assert!(!framer.has_pending_input()); assert_eq!(rebuilt, text.as_bytes()); } #[test] fn long_multilingual_burst_survives_one_byte_chunks_and_timeouts() { let text = "中文かなカナ한글🙂,。".repeat(64); - let mut buffer = Vec::new(); + let mut framer = RawInputByteFramer::default(); let mut rebuilt = Vec::new(); for byte in text.as_bytes() { - buffer.push(*byte); - for chunk in drain_complete_input_bytes(&mut buffer) { - rebuilt.extend(chunk); - } - if !buffer.is_empty() { - assert_eq!(flush_incomplete_input_bytes(&mut buffer), None); + rebuilt.extend( + framer + .push(std::slice::from_ref(byte)) + .into_iter() + .flatten(), + ); + if framer.has_pending_input() { + assert!(framer.flush_timeout().is_empty()); } } - for chunk in drain_complete_input_bytes(&mut buffer) { - rebuilt.extend(chunk); - } - - assert!(buffer.is_empty()); + rebuilt.extend(framer.flush_timeout().into_iter().flatten()); + assert!(!framer.has_pending_input()); assert_eq!(rebuilt, text.as_bytes()); } #[test] - fn parse_with_ranges_tracks_byte_offsets() { - use super::parse_raw_input_bytes_with_ranges; - - // Input: Up arrow (3 bytes) + 'a' (1 byte) + Down arrow (3 bytes) - let input = b"\x1b[Aa\x1b[B".to_vec(); - let ranges = parse_raw_input_bytes_with_ranges(&input); - - assert_eq!(ranges.len(), 3, "should parse three events"); - - // Up arrow: \x1b[A at offset 0, length 3 - assert_eq!(ranges[0].start, 0); - assert_eq!(ranges[0].len, 3); - assert!(matches!( - &ranges[0].event, - RawInputEvent::Key(k) if k.code == KeyCode::Up - )); - - // 'a' at offset 3, length 1 - assert_eq!(ranges[1].start, 3); - assert_eq!(ranges[1].len, 1); - assert!(matches!( - &ranges[1].event, - RawInputEvent::Key(k) if k.code == KeyCode::Char('a') - )); - - // Down arrow: \x1b[B at offset 4, length 3 - assert_eq!(ranges[2].start, 4); - assert_eq!(ranges[2].len, 3); - assert!(matches!( - &ranges[2].event, - RawInputEvent::Key(k) if k.code == KeyCode::Down - )); - - // Verify the raw bytes for each event slice correctly. - assert_eq!( - &input[ranges[0].start..ranges[0].start + ranges[0].len], - b"\x1b[A" - ); - assert_eq!( - &input[ranges[1].start..ranges[1].start + ranges[1].len], - b"a" - ); - assert_eq!( - &input[ranges[2].start..ranges[2].start + ranges[2].len], - b"\x1b[B" - ); - } - - #[test] - fn parse_with_ranges_handles_single_event() { - use super::parse_raw_input_bytes_with_ranges; - - let input = b"a".to_vec(); - let ranges = parse_raw_input_bytes_with_ranges(&input); - - assert_eq!(ranges.len(), 1); - assert_eq!(ranges[0].start, 0); - assert_eq!(ranges[0].len, 1); - } - - #[test] - fn parse_with_ranges_handles_mouse_event() { - use super::parse_raw_input_bytes_with_ranges; - - let input = b"\x1b[<0;20;10M".to_vec(); - let ranges = parse_raw_input_bytes_with_ranges(&input); - - assert_eq!(ranges.len(), 1); - assert_eq!(ranges[0].start, 0); - assert_eq!(ranges[0].len, input.len()); - assert!(matches!(&ranges[0].event, RawInputEvent::Mouse(_))); - } - - #[test] fn parses_ghostty_default_background_response() { let events = parse_raw_input_bytes_sync(b"\x1b]11;rgb:2828/2a2a/3636\x07"); @@ -2612,84 +2146,6 @@ mod tests { } #[test] - fn drain_complete_input_bytes_keeps_split_default_background_response_buffered() { - let mut buffer = b"\x1b]11;rgb:2828".to_vec(); - - let chunks = drain_complete_input_bytes(&mut buffer); - - assert!(chunks.is_empty()); - assert_eq!(buffer, b"\x1b]11;rgb:2828"); - } - - #[test] - fn flush_incomplete_input_bytes_keeps_split_default_background_response_buffered() { - let mut buffer = b"\x1b]11;rgb:2828".to_vec(); - - let flushed = flush_incomplete_input_bytes(&mut buffer); - - assert!(flushed.is_none()); - assert_eq!(buffer, b"\x1b]11;rgb:2828"); - } - - #[test] - fn flush_incomplete_input_bytes_keeps_default_background_response_split_after_command() { - let mut buffer = b"\x1b]11;".to_vec(); - - let flushed = flush_incomplete_input_bytes(&mut buffer); - - assert!(flushed.is_none()); - assert_eq!(buffer, b"\x1b]11;"); - } - - #[test] - fn flush_incomplete_input_bytes_keeps_default_background_response_split_inside_st() { - let mut buffer = b"\x1b]11;rgb:2828/2a2a/3636\x1b".to_vec(); - - let flushed = flush_incomplete_input_bytes(&mut buffer); - - assert!(flushed.is_none()); - assert_eq!(buffer, b"\x1b]11;rgb:2828/2a2a/3636\x1b"); - } - - #[test] - fn drain_complete_input_bytes_keeps_bare_osc_introducer_buffered() { - let mut buffer = b"\x1b]".to_vec(); - - let chunks = drain_complete_input_bytes(&mut buffer); - - assert!(chunks.is_empty()); - assert_eq!(buffer, b"\x1b]"); - } - - #[test] - fn flush_incomplete_input_bytes_drops_bare_osc_introducer_after_timeout() { - let mut buffer = b"\x1b]".to_vec(); - - let flushed = flush_incomplete_input_bytes(&mut buffer); - - assert!(flushed.is_none()); - assert!(buffer.is_empty()); - } - - #[test] - fn flush_incomplete_input_bytes_drops_string_introducers_after_timeout() { - for bytes in [ - b"\x1b]".as_slice(), - b"\x1bP".as_slice(), - b"\x1b_".as_slice(), - b"\x1b^".as_slice(), - b"\x1bX".as_slice(), - ] { - let mut buffer = bytes.to_vec(); - - let flushed = flush_incomplete_input_bytes(&mut buffer); - - assert!(flushed.is_none(), "flushed {bytes:?}"); - assert!(buffer.is_empty(), "kept {bytes:?}"); - } - } - - #[test] fn raw_input_framer_reassembles_split_default_background_response() { let mut framer = RawInputFramer::default(); @@ -2781,7 +2237,7 @@ mod tests { } #[test] - fn flush_incomplete_input_bytes_does_not_hold_non_osc_default_color_text() { + fn byte_framer_does_not_hold_non_osc_default_color_text() { let mut framer = RawInputByteFramer::default(); let chunks = framer.push(b"11;rgb:2828"); |
