aboutsummaryrefslogtreecommitdiff
path: root/src/raw_input.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/raw_input.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (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.rs766
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");