From 3256167f48c3b33d929df6840559383e41028eb3 Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 1 May 2026 17:11:08 +0000 Subject: [PATCH 1/3] test: add PTY integration test harness Add a pty-mock workspace crate (echo/alt-screen/paste-echo/buffer-fill/ sync-blocks subcommands) plus an in-process integration test suite that drives Proxy directly via a new_for_test constructor and #[cfg(test)] state accessors. Sets up the regression net needed before swapping the underlying VT emulator from vt100 to termwiz. Pulled from upstream draft PR #35; the alt-screen feed-order semantic change is intentionally deferred (its dedicated test is left #[ignore]). --- .github/workflows/ci.yml | 1 + Cargo.lock | 7 + Cargo.toml | 1 + crates/claude-chill/src/integration_tests.rs | 539 +++++++++++++++++++ crates/claude-chill/src/lib.rs | 2 + crates/claude-chill/src/proxy.rs | 103 +++- crates/pty-mock/Cargo.toml | 12 + crates/pty-mock/src/bin.rs | 146 +++++ flake.nix | 1 + 9 files changed, 810 insertions(+), 2 deletions(-) create mode 100644 crates/claude-chill/src/integration_tests.rs create mode 100644 crates/pty-mock/Cargo.toml create mode 100644 crates/pty-mock/src/bin.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05fed9a..3cc2bfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - run: cargo build -p pty-mock - run: cargo test --all-targets test: diff --git a/Cargo.lock b/Cargo.lock index d2a7bc9..bd068f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,6 +750,13 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pty-mock" +version = "0.1.4" +dependencies = [ + "clap", +] + [[package]] name = "quote" version = "1.0.43" diff --git a/Cargo.toml b/Cargo.toml index 916c086..290bd39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "crates/claude-chill", + "crates/pty-mock", ] [workspace.package] diff --git a/crates/claude-chill/src/integration_tests.rs b/crates/claude-chill/src/integration_tests.rs new file mode 100644 index 0000000..259c224 --- /dev/null +++ b/crates/claude-chill/src/integration_tests.rs @@ -0,0 +1,539 @@ +#[cfg(test)] +mod tests { + use crate::proxy::{Proxy, ProxyConfig}; + use nix::fcntl::{FcntlArg, OFlag, fcntl}; + use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; + use nix::pty::openpty; + use nix::sys::termios::{self, SetArg}; + use nix::unistd::read; + use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd}; + use std::os::unix::process::CommandExt; + use std::process::{Child, Command}; + use std::time::Duration; + + fn pty_mock_binary() -> Option { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let bin = format!("{}/../../target/debug/pty-mock", manifest_dir); + if std::path::Path::new(&bin).exists() { + return Some(bin); + } + None + } + + fn spawn_command(cmd: &str, args: &[&str]) -> (OwnedFd, Child) { + let pty = openpty(None, None).expect("openpty failed"); + + let slave_fd = pty.slave.as_raw_fd(); + let mut termios_settings = termios::tcgetattr(&pty.slave).expect("tcgetattr failed"); + termios::cfmakeraw(&mut termios_settings); + termios::tcsetattr(&pty.slave, SetArg::TCSANOW, &termios_settings) + .expect("tcsetattr failed"); + + let child = unsafe { + Command::new(cmd) + .args(args) + .stdin(pty.slave.try_clone().expect("clone slave")) + .stdout(pty.slave.try_clone().expect("clone slave")) + .stderr(pty.slave.try_clone().expect("clone slave")) + .pre_exec(move || { + if libc::setsid() == -1 { + return Err(std::io::Error::last_os_error()); + } + if libc::ioctl(slave_fd, libc::TIOCSCTTY as libc::c_ulong, 0) == -1 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }) + .spawn() + .expect("failed to spawn command") + }; + + drop(pty.slave); + + // Set master to non-blocking + let flags = fcntl(&pty.master, FcntlArg::F_GETFL).expect("F_GETFL"); + let flags = OFlag::from_bits_truncate(flags); + fcntl(&pty.master, FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK)).expect("F_SETFL"); + + (pty.master, child) + } + + fn spawn_mock(subcommand: &str) -> Option<(OwnedFd, Child)> { + let bin = pty_mock_binary()?; + Some(spawn_command(&bin, &[subcommand])) + } + + fn read_pty_output(master: &OwnedFd, timeout_ms: u16) -> Vec { + let mut result = Vec::new(); + let mut buf = [0u8; 65536]; + let mut poll_fds = [PollFd::new(master.as_fd(), PollFlags::POLLIN)]; + + loop { + match poll(&mut poll_fds, PollTimeout::from(timeout_ms)) { + Ok(0) => break, + Ok(_) => match read(master.as_fd(), &mut buf) { + Ok(0) => break, + Ok(n) => result.extend_from_slice(&buf[..n]), + Err(nix::errno::Errno::EAGAIN) => break, + Err(_) => break, + }, + Err(_) => break, + } + } + result + } + + fn make_proxy(master: OwnedFd, child: Child) -> Proxy { + let config = ProxyConfig::default(); + Proxy::new_for_test(master, child, config, 24, 80) + } + + fn dev_null() -> OwnedFd { + use std::fs::OpenOptions; + let f = OpenOptions::new() + .write(true) + .open("/dev/null") + .expect("open /dev/null"); + OwnedFd::from(f) + } + + fn dup_master(proxy: &Proxy) -> OwnedFd { + let raw = proxy.pty_master_fd_raw(); + unsafe { OwnedFd::from_raw_fd(libc::dup(raw)) } + } + + #[test] + fn test_echo_baseline() { + let Some((master, child)) = spawn_mock("echo") else { + return; + }; + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + proxy + .process_input(b"hello world\r\n", &sink) + .expect("process_input"); + proxy.flush_drain_buffer(&sink).expect("flush drain"); + + std::thread::sleep(Duration::from_millis(50)); + + let master_dup = dup_master(&proxy); + let output = read_pty_output(&master_dup, 100); + assert!(!output.is_empty(), "expected output from echo child"); + proxy + .process_output(&output, &sink) + .expect("process_output"); + + let mut history = Vec::new(); + proxy.history().append_all(&mut history); + let history_str = String::from_utf8_lossy(&history); + assert!( + history_str.contains("hello world"), + "history should contain echoed text, got: {:?}", + history_str + ); + } + + #[test] + fn test_alt_screen_tracking() { + let Some((master, child)) = spawn_mock("alt-screen") else { + return; + }; + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + std::thread::sleep(Duration::from_millis(50)); + + let master_dup = dup_master(&proxy); + let output = read_pty_output(&master_dup, 100); + if !output.is_empty() { + proxy + .process_output(&output, &sink) + .expect("process_output"); + } + + assert!( + proxy.is_in_alternate_screen(), + "proxy should be in alt screen" + ); + + proxy.process_input(b"x", &sink).expect("process_input"); + proxy.flush_drain_buffer(&sink).expect("flush drain"); + + std::thread::sleep(Duration::from_millis(50)); + + let master_dup = dup_master(&proxy); + let output = read_pty_output(&master_dup, 100); + if !output.is_empty() { + proxy + .process_output(&output, &sink) + .expect("process_output"); + } + + assert!( + !proxy.is_in_alternate_screen(), + "proxy should have exited alt screen" + ); + } + + #[test] + fn test_bracketed_paste_flow() { + let Some((master, child)) = spawn_mock("paste-echo") else { + return; + }; + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + std::thread::sleep(Duration::from_millis(50)); + let master_dup = dup_master(&proxy); + let output = read_pty_output(&master_dup, 100); + if !output.is_empty() { + proxy + .process_output(&output, &sink) + .expect("process_output"); + } + + let mut paste = Vec::new(); + paste.extend_from_slice(b"\x1b[200~"); + paste.extend_from_slice(b"pasted content\r\n"); + paste.extend_from_slice(b"\x1b[201~"); + + proxy.process_input(&paste, &sink).expect("process_input"); + proxy.flush_drain_buffer(&sink).expect("flush drain"); + + assert!( + !proxy.is_in_bracketed_paste(), + "paste mode should be off after complete paste" + ); + } + + #[test] + fn test_split_paste_end_marker() { + let Some((master, child)) = spawn_mock("paste-echo") else { + return; + }; + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + std::thread::sleep(Duration::from_millis(50)); + let master_dup = dup_master(&proxy); + let output = read_pty_output(&master_dup, 100); + if !output.is_empty() { + proxy + .process_output(&output, &sink) + .expect("process_output"); + } + + let mut chunk1 = Vec::new(); + chunk1.extend_from_slice(b"\x1b[200~"); + chunk1.extend_from_slice(b"split test\r\n"); + chunk1.extend_from_slice(b"\x1b[20"); // partial end marker + + proxy + .process_input(&chunk1, &sink) + .expect("process_input chunk1"); + proxy.flush_drain_buffer(&sink).expect("flush drain"); + + assert!( + proxy.is_in_bracketed_paste(), + "should still be in paste mode after partial end marker" + ); + + proxy + .process_input(b"1~", &sink) + .expect("process_input chunk2"); + proxy.flush_drain_buffer(&sink).expect("flush drain"); + + assert!( + !proxy.is_in_bracketed_paste(), + "paste mode should be off after completing split end marker" + ); + } + + #[test] + fn test_alt_screen_exit_restores_main_content() { + let Some((master, child)) = spawn_mock("echo") else { + return; + }; + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + // Simulate child writing main screen content + let main_content = b"Claude Code v2.1.84\r\nOpus 4.6\r\n~/public/claude-chill\r\n"; + proxy + .process_output(main_content, &sink) + .expect("process_output main content"); + + let screen_before = proxy.vt_screen_text(); + assert!( + screen_before.contains("Claude Code v2.1.84"), + "main content should be on screen before alt screen, got: {:?}", + screen_before + ); + + // Child enters alt screen + proxy + .process_output(b"\x1b[?1049h", &sink) + .expect("process_output alt enter"); + assert!(proxy.is_in_alternate_screen()); + + // Child writes alt screen content (e.g. vim, less) + proxy + .process_output(b"alt screen editor content\r\nline 2\r\n", &sink) + .expect("process_output alt content"); + + // Child exits alt screen + proxy + .process_output(b"\x1b[?1049l", &sink) + .expect("process_output alt exit"); + assert!(!proxy.is_in_alternate_screen()); + + // The VT parser should have restored the main screen content + let screen_after = proxy.vt_screen_text(); + assert!( + screen_after.contains("Claude Code v2.1.84"), + "main content should be restored after alt screen exit, got: {:?}", + screen_after + ); + } + + // Skipped on this branch: depends on the alt-screen feed-order fix from + // upstream PR #35 (incremental VT feeding around alt enter/exit). We're + // pulling in only the test harness now; the semantic fix is a separate + // step. Re-enable by removing #[ignore] once that change lands. + #[test] + #[ignore] + fn test_alt_screen_exit_restores_after_clear_screen_sync_block() { + // Reproduces bug: sync block containing clear screen clears VT parser, + // but render_vt_screen never fires before alt-screen-enter arrives + // in the next chunk. VT parser saves blank screen. After alt exit, + // contents_formatted() paints blank over the real terminal which + // still had the original content (it never got the clear render). + let Some((master, child)) = spawn_mock("echo") else { + return; + }; + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + // Initial content via sync block — this gets rendered + let mut initial_sync = Vec::new(); + initial_sync.extend_from_slice(b"\x1b[?2026h"); + initial_sync.extend_from_slice(b"\x1b[H"); + initial_sync.extend_from_slice(b"Claude Code v2.1.84\r\n"); + initial_sync.extend_from_slice(b"Opus 4.6\r\n"); + initial_sync.extend_from_slice(b"~/public/claude-chill\r\n"); + initial_sync.extend_from_slice(b"\x1b[?2026l"); + + proxy + .process_output(&initial_sync, &sink) + .expect("initial sync block"); + + let screen_before = proxy.vt_screen_text(); + assert!( + screen_before.contains("Claude Code v2.1.84"), + "header should be on screen initially" + ); + + // Sync block with clear screen (simulates ctrl-g response) — + // VT parser processes the clear but render never fires + let mut clear_sync = Vec::new(); + clear_sync.extend_from_slice(b"\x1b[?2026h"); + clear_sync.extend_from_slice(b"\x1b[2J"); // clear screen + clear_sync.extend_from_slice(b"\x1b[H"); + clear_sync.extend_from_slice(b"\x1b[?2026l"); + + proxy + .process_output(&clear_sync, &sink) + .expect("clear sync block"); + + // VT parser screen is now blank (clear screen was processed) + let screen_after_clear = proxy.vt_screen_text(); + assert!( + !screen_after_clear.contains("Claude Code"), + "VT screen should be blank after clear sync block" + ); + + // Alt screen enter in the NEXT chunk — no render_vt_screen fired + // between the clear sync and this. VT parser saves blank screen. + proxy + .process_output(b"\x1b[?1049h", &sink) + .expect("alt screen enter"); + assert!(proxy.is_in_alternate_screen()); + + // Editor content during alt screen + proxy + .process_output(b"editor content\r\n", &sink) + .expect("alt content"); + + // Alt screen exit — VT restores saved (blank) screen + proxy + .process_output(b"\x1b[?1049l", &sink) + .expect("alt screen exit"); + assert!(!proxy.is_in_alternate_screen()); + + // Clear screen wiped the main buffer before alt-screen saved it. + // Correct VT100 behavior: restored screen should be blank. + let screen_after_exit = proxy.vt_screen_text(); + assert!( + !screen_after_exit.contains("Claude Code v2.1.84"), + "header should NOT survive clear + alt screen round-trip, got: {:?}", + screen_after_exit + ); + } + + #[test] + fn test_sync_block_tracking() { + let Some((master, child)) = spawn_mock("echo") else { + return; + }; + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + // Start a sync block without ending it + proxy + .process_output(b"\x1b[?2026h some content", &sink) + .expect("partial sync block"); + assert!( + proxy.is_in_sync_block(), + "should be in sync block after SYNC_START without SYNC_END" + ); + + // End the sync block + proxy + .process_output(b" more content \x1b[?2026l", &sink) + .expect("sync block end"); + assert!( + !proxy.is_in_sync_block(), + "should not be in sync block after SYNC_END" + ); + } + + #[test] + fn test_lookback_mode_toggle() { + let Some((master, child)) = spawn_mock("echo") else { + return; + }; + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + // Generate some output so lookback has content + proxy + .process_output(b"line 1\r\nline 2\r\nline 3\r\n", &sink) + .expect("output"); + + assert!(!proxy.is_in_lookback_mode()); + + // Send legacy lookback key (0x1E = Ctrl+^) + send_input(&mut proxy, &sink, &[0x1E]); + assert!( + proxy.is_in_lookback_mode(), + "should enter lookback mode after Ctrl+^" + ); + + // Send it again to exit + send_input(&mut proxy, &sink, &[0x1E]); + assert!( + !proxy.is_in_lookback_mode(), + "should exit lookback mode after second Ctrl+^" + ); + } + + fn pump_proxy(proxy: &mut Proxy, sink: &OwnedFd, timeout_ms: u16) { + let master_dup = dup_master(proxy); + let output = read_pty_output(&master_dup, timeout_ms); + if !output.is_empty() { + proxy.process_output(&output, sink).expect("process_output"); + } + } + + fn send_input(proxy: &mut Proxy, sink: &OwnedFd, data: &[u8]) { + proxy.process_input(data, sink).expect("process_input"); + proxy.flush_drain_buffer(sink).expect("flush drain"); + } + + fn wait_for_screen(proxy: &mut Proxy, sink: &OwnedFd, text: &str, timeout: Duration) -> bool { + let start = std::time::Instant::now(); + while start.elapsed() < timeout { + pump_proxy(proxy, sink, 100); + if proxy.vt_screen_text().contains(text) { + return true; + } + } + false + } + + #[test] + #[ignore] // requires claude CLI to be installed and authenticated + fn test_claude_alt_screen_restore() { + let (master, child) = spawn_command("claude", &[]); + let mut proxy = make_proxy(master, child); + let sink = dev_null(); + + // Wait for Claude Code to start up and show its UI + assert!( + wait_for_screen(&mut proxy, &sink, "Claude Code", Duration::from_secs(10)), + "Claude Code did not start, screen: {:?}", + proxy.vt_screen_text() + ); + + let screen_before = proxy.vt_screen_text(); + eprintln!("Screen before alt screen:\n{}", screen_before); + assert!( + screen_before.contains("▐▛███▜▌"), + "logo should be present before alt screen" + ); + + // Send Ctrl-G to open the editor (enters alt screen) + send_input(&mut proxy, &sink, &[0x07]); // Ctrl-G + + // Wait for alt screen to be entered + let start = std::time::Instant::now(); + while start.elapsed() < Duration::from_secs(5) { + pump_proxy(&mut proxy, &sink, 100); + if proxy.is_in_alternate_screen() { + break; + } + } + assert!( + proxy.is_in_alternate_screen(), + "should have entered alt screen after Ctrl-G" + ); + + // Pump until editor has rendered + pump_proxy(&mut proxy, &sink, 500); + + // Send :wq to close the editor (exits alt screen) + send_input(&mut proxy, &sink, b"\x1b"); + pump_proxy(&mut proxy, &sink, 100); + send_input(&mut proxy, &sink, b":wq\r"); + + // Wait for alt screen to be exited + let start = std::time::Instant::now(); + while start.elapsed() < Duration::from_secs(5) { + pump_proxy(&mut proxy, &sink, 100); + if !proxy.is_in_alternate_screen() { + break; + } + } + + // Drain remaining output after alt screen exit + pump_proxy(&mut proxy, &sink, 500); + + let vt_screen_after = proxy.vt_screen_text(); + + eprintln!("VT parser screen after alt exit:\n{}", vt_screen_after); + + // Claude Code bug: after editor exit, the logo (▐▛███▜▌) is not + // redrawn. This happens with or without claude-chill. + // Update this assertion if Claude Code fixes the redraw. + assert!( + !vt_screen_after.contains("▐▛███▜▌"), + "logo should NOT appear after alt screen exit (known Claude Code bug), got: {:?}", + vt_screen_after + ); + + // Send /exit to quit + send_input(&mut proxy, &sink, b"/exit\r"); + pump_proxy(&mut proxy, &sink, 1000); + } +} diff --git a/crates/claude-chill/src/lib.rs b/crates/claude-chill/src/lib.rs index 72f918b..1219fef 100644 --- a/crates/claude-chill/src/lib.rs +++ b/crates/claude-chill/src/lib.rs @@ -2,6 +2,8 @@ pub mod config; pub mod escape_filter; pub mod escape_sequences; pub mod history_filter; +#[cfg(test)] +mod integration_tests; pub mod key_parser; pub mod line_buffer; pub mod proxy; diff --git a/crates/claude-chill/src/proxy.rs b/crates/claude-chill/src/proxy.rs index f294dce..071c15b 100644 --- a/crates/claude-chill/src/proxy.rs +++ b/crates/claude-chill/src/proxy.rs @@ -322,6 +322,105 @@ impl Proxy { }) } + #[cfg(test)] + pub(crate) fn new_for_test( + pty_master: OwnedFd, + child: Child, + config: ProxyConfig, + rows: u16, + cols: u16, + ) -> Self { + let vt_parser = vt100::Parser::new(rows, cols, 0); + let mut history = LineBuffer::new(config.max_history_lines); + history.push_bytes(CLEAR_SCREEN); + history.push_bytes(CURSOR_HOME); + let auto_lookback_timeout = Duration::from_millis(config.auto_lookback_timeout_ms); + + Self { + history, + history_filter: HistoryFilter::new(), + config, + pty_master, + child, + original_termios: None, + vt_parser, + vt_prev_screen: None, + last_output_time: None, + last_render_time: None, + last_stdin_time: None, + last_auto_lookback_time: None, + auto_lookback_timeout, + sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY), + in_sync_block: false, + in_lookback_mode: false, + in_alternate_screen: false, + in_bracketed_paste: false, + kitty_mode_supported: false, + kitty_mode_stack: 0, + kitty_output_parser: TermwizParser::new(), + vt_render_pending: false, + lookback_cache: Vec::new(), + lookback_input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY), + output_buffer: Vec::with_capacity(OUTPUT_BUFFER_CAPACITY), + sync_start_finder: memmem::Finder::new(SYNC_START), + sync_end_finder: memmem::Finder::new(SYNC_END), + clear_screen_finder: memmem::Finder::new(CLEAR_SCREEN), + cursor_home_finder: memmem::Finder::new(CURSOR_HOME), + alt_screen_enter_finder: memmem::Finder::new(ALT_SCREEN_ENTER), + alt_screen_exit_finder: memmem::Finder::new(ALT_SCREEN_EXIT), + alt_screen_enter_legacy_finder: memmem::Finder::new(ALT_SCREEN_ENTER_LEGACY), + alt_screen_exit_legacy_finder: memmem::Finder::new(ALT_SCREEN_EXIT_LEGACY), + paste_start_finder: memmem::Finder::new(BRACKETED_PASTE_START), + paste_end_finder: memmem::Finder::new(BRACKETED_PASTE_END), + pty_drain_buffer: Vec::new(), + paste_remainder: Vec::new(), + } + } + + #[cfg(test)] + pub(crate) fn history(&self) -> &LineBuffer { + &self.history + } + + #[cfg(test)] + pub(crate) fn is_in_alternate_screen(&self) -> bool { + self.in_alternate_screen + } + + #[cfg(test)] + pub(crate) fn is_in_bracketed_paste(&self) -> bool { + self.in_bracketed_paste + } + + #[cfg(test)] + pub(crate) fn is_in_sync_block(&self) -> bool { + self.in_sync_block + } + + #[cfg(test)] + pub(crate) fn is_in_lookback_mode(&self) -> bool { + self.in_lookback_mode + } + + #[cfg(test)] + pub(crate) fn vt_screen_text(&self) -> String { + self.vt_parser.screen().contents() + } + + #[cfg(test)] + pub(crate) fn pty_master_fd_raw(&self) -> i32 { + self.pty_master.as_raw_fd() + } + + #[cfg(test)] + pub(crate) fn flush_drain_buffer(&mut self, stdout_fd: &F) -> Result<()> { + if !self.pty_drain_buffer.is_empty() { + let drained = std::mem::take(&mut self.pty_drain_buffer); + self.process_output(&drained, stdout_fd)?; + } + Ok(()) + } + pub fn run(&mut self) -> Result { let stdin_fd = io::stdin(); let stdout_fd = io::stdout(); @@ -406,7 +505,7 @@ impl Proxy { self.wait_child() } - fn process_output(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> { + pub(crate) fn process_output(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> { self.process_output_inner(data, stdout_fd, true) } @@ -842,7 +941,7 @@ impl Proxy { Ok(()) } - fn process_input(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> { + pub(crate) fn process_input(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> { self.last_stdin_time = Some(Instant::now()); debug!( diff --git a/crates/pty-mock/Cargo.toml b/crates/pty-mock/Cargo.toml new file mode 100644 index 0000000..838f839 --- /dev/null +++ b/crates/pty-mock/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pty-mock" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[[bin]] +name = "pty-mock" +path = "src/bin.rs" + +[dependencies] +clap = { version = "4", features = ["derive"] } diff --git a/crates/pty-mock/src/bin.rs b/crates/pty-mock/src/bin.rs new file mode 100644 index 0000000..f3c3b30 --- /dev/null +++ b/crates/pty-mock/src/bin.rs @@ -0,0 +1,146 @@ +use clap::{Parser, Subcommand}; +use std::io::{self, BufRead, Read, Write}; + +#[derive(Parser)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Echo, + AltScreen, + PasteEcho, + BufferFill, + SyncBlocks, +} + +fn cmd_echo() { + let stdin = io::stdin(); + let mut stdout = io::stdout(); + for line in stdin.lock().lines() { + let line = match line { + Ok(l) => l, + Err(_) => break, + }; + if writeln!(stdout, "{}", line).is_err() { + break; + } + if stdout.flush().is_err() { + break; + } + } +} + +fn cmd_alt_screen() { + let mut stdout = io::stdout(); + let mut stdin = io::stdin(); + + // Enter alt screen + let _ = stdout.write_all(b"\x1b[?1049h"); + let _ = stdout.flush(); + + // Write some content while in alt screen + let _ = stdout.write_all(b"alt screen content\r\n"); + let _ = stdout.flush(); + + // Wait for any byte on stdin as trigger to exit + let mut trigger = [0u8; 1]; + let _ = stdin.read_exact(&mut trigger); + + // Exit alt screen + let _ = stdout.write_all(b"\x1b[?1049l"); + let _ = stdout.flush(); + + // Write content after alt screen exit + let _ = stdout.write_all(b"back to normal\r\n"); + let _ = stdout.flush(); +} + +fn cmd_paste_echo() { + let mut stdout = io::stdout(); + let stdin = io::stdin(); + + // Enable bracketed paste mode + let _ = stdout.write_all(b"\x1b[?2004h"); + let _ = stdout.flush(); + + // Read lines, prefix paste content + for line in stdin.lock().lines() { + let line = match line { + Ok(l) => l, + Err(_) => break, + }; + if writeln!(stdout, "pasted: {}", line).is_err() { + break; + } + if stdout.flush().is_err() { + break; + } + } +} + +fn cmd_buffer_fill() { + let mut stdout = io::stdout(); + let stdin = io::stdin(); + let mut stdin_lock = stdin.lock(); + + // Write large output to fill the PTY buffer + let chunk = "X".repeat(1024) + "\r\n"; + for _ in 0..256 { + if stdout.write_all(chunk.as_bytes()).is_err() { + break; + } + } + let _ = stdout.flush(); + + // Now read stdin and echo it back + let mut buf = [0u8; 4096]; + loop { + match stdin_lock.read(&mut buf) { + Ok(0) => break, + Ok(n) => { + if stdout.write_all(&buf[..n]).is_err() { + break; + } + if stdout.flush().is_err() { + break; + } + } + Err(_) => break, + } + } +} + +fn cmd_sync_blocks() { + let stdin = io::stdin(); + let mut stdout = io::stdout(); + + for line in stdin.lock().lines() { + let line = match line { + Ok(l) => l, + Err(_) => break, + }; + // Wrap response in sync update block + let _ = stdout.write_all(b"\x1b[?2026h"); + if writeln!(stdout, "sync: {}", line).is_err() { + break; + } + let _ = stdout.write_all(b"\x1b[?2026l"); + if stdout.flush().is_err() { + break; + } + } +} + +fn main() { + let cli = Cli::parse(); + match cli.command { + Commands::Echo => cmd_echo(), + Commands::AltScreen => cmd_alt_screen(), + Commands::PasteEcho => cmd_paste_echo(), + Commands::BufferFill => cmd_buffer_fill(), + Commands::SyncBlocks => cmd_sync_blocks(), + } +} diff --git a/flake.nix b/flake.nix index 89ffe1c..fe4f428 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,7 @@ cargoLock = { lockFile = ./Cargo.lock; }; + cargoBuildFlags = [ "-p" "claude-chill" ]; }; in -- 2.49.1 From 1be6e4954eb402eab5cb9374b776ce5f2d7e3fd4 Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 1 May 2026 17:20:35 +0000 Subject: [PATCH 2/3] test: add failing color/SGR/hyperlink preservation tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin the baseline: feed OSC 8 hyperlinks, strikethrough, curly underline, underline color, and double underline through Proxy, force a render, and capture what the proxy emits to its stdout side. Each test asserts the styling survives the round-trip. All five fail on the current vt100 0.16 backend because its cell model only stores fg/bg color, bold, dim, italic, underline, inverse — every other attribute is dropped at render time. Tests are #[ignore]-gated so CI stays green; run with --ignored to see the baseline failures. Once the emulator is swapped to termwiz, remove the gates. A truecolor-foreground sanity test runs by default and confirms the parts of the SGR space vt100 does support are still working. Adds Proxy::force_render as a #[cfg(test)] wrapper around the private render_vt_screen method. --- .../src/color_preservation_tests.rs | 216 ++++++++++++++++++ crates/claude-chill/src/lib.rs | 2 + crates/claude-chill/src/proxy.rs | 5 + 3 files changed, 223 insertions(+) create mode 100644 crates/claude-chill/src/color_preservation_tests.rs diff --git a/crates/claude-chill/src/color_preservation_tests.rs b/crates/claude-chill/src/color_preservation_tests.rs new file mode 100644 index 0000000..6fed42a --- /dev/null +++ b/crates/claude-chill/src/color_preservation_tests.rs @@ -0,0 +1,216 @@ +//! Color / SGR / hyperlink preservation tests. +//! +//! These tests feed modern terminal styling bytes through a real `Proxy`, +//! force a render, capture what the proxy emits to its stdout side, and +//! assert that the styling survives the round-trip. +//! +//! They currently FAIL on master because the underlying `vt100 0.16` +//! emulator only models a handful of attributes (fg/bg color, bold, dim, +//! italic, underline, inverse). Anything else — OSC 8 hyperlinks, +//! strikethrough, fancy underline styles, underline color — is silently +//! dropped on render. +//! +//! They are gated behind `#[ignore]` so CI stays green. Run them with: +//! cargo test -p claude-chill --lib color_preservation -- --ignored +//! +//! The expectation is that they pass once the emulator is swapped to +//! termwiz's `Surface`. Remove the `#[ignore]` attributes at that point. + +#[cfg(test)] +mod tests { + use crate::proxy::{Proxy, ProxyConfig}; + use nix::fcntl::{FcntlArg, OFlag, fcntl}; + use nix::pty::openpty; + use nix::sys::termios::{self, SetArg}; + use nix::unistd::{pipe, read}; + use std::os::fd::{AsFd, AsRawFd, OwnedFd}; + use std::os::unix::process::CommandExt; + use std::process::{Child, Command}; + + fn pty_mock_binary() -> Option { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let bin = format!("{}/../../target/debug/pty-mock", manifest_dir); + if std::path::Path::new(&bin).exists() { + Some(bin) + } else { + None + } + } + + fn spawn_echo_child() -> Option<(OwnedFd, Child)> { + let bin = pty_mock_binary()?; + let pty = openpty(None, None).expect("openpty failed"); + + let slave_fd = pty.slave.as_raw_fd(); + let mut termios_settings = termios::tcgetattr(&pty.slave).expect("tcgetattr failed"); + termios::cfmakeraw(&mut termios_settings); + termios::tcsetattr(&pty.slave, SetArg::TCSANOW, &termios_settings) + .expect("tcsetattr failed"); + + let child = unsafe { + Command::new(&bin) + .arg("echo") + .stdin(pty.slave.try_clone().expect("clone slave")) + .stdout(pty.slave.try_clone().expect("clone slave")) + .stderr(pty.slave.try_clone().expect("clone slave")) + .pre_exec(move || { + if libc::setsid() == -1 { + return Err(std::io::Error::last_os_error()); + } + if libc::ioctl(slave_fd, libc::TIOCSCTTY as libc::c_ulong, 0) == -1 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }) + .spawn() + .expect("failed to spawn pty-mock echo") + }; + + drop(pty.slave); + + let flags = fcntl(&pty.master, FcntlArg::F_GETFL).expect("F_GETFL"); + let flags = OFlag::from_bits_truncate(flags); + fcntl(&pty.master, FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK)).expect("F_SETFL"); + + Some((pty.master, child)) + } + + fn make_proxy(master: OwnedFd, child: Child) -> Proxy { + Proxy::new_for_test(master, child, ProxyConfig::default(), 24, 80) + } + + /// Make a non-blocking pipe to capture proxy output. + fn capture_pipe() -> (OwnedFd, OwnedFd) { + let (rx, tx) = pipe().expect("pipe failed"); + let flags = fcntl(&rx, FcntlArg::F_GETFL).expect("F_GETFL"); + let flags = OFlag::from_bits_truncate(flags); + fcntl(&rx, FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK)).expect("F_SETFL"); + (rx, tx) + } + + fn drain(rx: &OwnedFd) -> Vec { + let mut buf = [0u8; 65536]; + let mut out = Vec::new(); + loop { + match read(rx.as_fd(), &mut buf) { + Ok(0) => break, + Ok(n) => out.extend_from_slice(&buf[..n]), + Err(_) => break, + } + } + out + } + + /// Feed `input` through the proxy, force a render to a captured pipe, + /// and return what the proxy emitted. + fn render_and_capture(input: &[u8]) -> Option> { + let (master, child) = spawn_echo_child()?; + let mut proxy = make_proxy(master, child); + let (rx, tx) = capture_pipe(); + + proxy.process_output(input, &tx).expect("process_output"); + proxy.force_render(&tx).expect("force_render"); + + Some(drain(&rx)) + } + + fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { + haystack.windows(needle.len()).any(|w| w == needle) + } + + #[test] + #[ignore] // FIXME: passes once VT emulator swap to termwiz lands + fn osc8_hyperlink_survives_render() { + // OSC 8 hyperlinks — used for clickable file paths / URLs. + // Format: ESC ] 8 ; ; URL ESC \ ... ESC ] 8 ; ; ESC \ + let mut input = Vec::new(); + input.extend_from_slice(b"\x1b]8;;https://example.com/foo\x1b\\"); + input.extend_from_slice(b"link text"); + input.extend_from_slice(b"\x1b]8;;\x1b\\"); + + let captured = render_and_capture(&input).expect("pty-mock binary missing"); + assert!( + contains_bytes(&captured, b"\x1b]8;;https://example.com/foo"), + "OSC 8 hyperlink open should survive render, got: {:?}", + String::from_utf8_lossy(&captured) + ); + assert!( + contains_bytes(&captured, b"\x1b]8;;\x1b\\"), + "OSC 8 hyperlink close should survive render, got: {:?}", + String::from_utf8_lossy(&captured) + ); + } + + #[test] + #[ignore] // FIXME: passes once VT emulator swap to termwiz lands + fn strikethrough_survives_render() { + // SGR 9 = strikethrough. vt100 0.16 has no field for it. + let input = b"\x1b[9mstruck through text\x1b[0m"; + let captured = render_and_capture(input).expect("pty-mock binary missing"); + assert!( + contains_bytes(&captured, b"\x1b[9m") || contains_bytes(&captured, b";9m"), + "SGR 9 strikethrough should survive render, got: {:?}", + String::from_utf8_lossy(&captured) + ); + } + + #[test] + #[ignore] // FIXME: passes once VT emulator swap to termwiz lands + fn curly_underline_survives_render() { + // SGR 4:3 = curly underline (kitty/wezterm/ghostty all support). + let input = b"\x1b[4:3mcurly underline\x1b[0m"; + let captured = render_and_capture(input).expect("pty-mock binary missing"); + assert!( + contains_bytes(&captured, b"4:3"), + "SGR 4:3 curly underline should survive render, got: {:?}", + String::from_utf8_lossy(&captured) + ); + } + + #[test] + #[ignore] // FIXME: passes once VT emulator swap to termwiz lands + fn underline_color_survives_render() { + // SGR 58:2::R:G:B = truecolor underline color. + let input = b"\x1b[4m\x1b[58:2::255:0:0mcolored underline\x1b[0m"; + let captured = render_and_capture(input).expect("pty-mock binary missing"); + assert!( + contains_bytes(&captured, b"58:2") || contains_bytes(&captured, b"58;2"), + "SGR 58 underline color should survive render, got: {:?}", + String::from_utf8_lossy(&captured) + ); + } + + #[test] + #[ignore] // FIXME: passes once VT emulator swap to termwiz lands + fn double_underline_survives_render() { + // SGR 21 = doubly underlined. + let input = b"\x1b[21mdouble underlined\x1b[0m"; + let captured = render_and_capture(input).expect("pty-mock binary missing"); + assert!( + contains_bytes(&captured, b"\x1b[21m") + || contains_bytes(&captured, b";21m") + || contains_bytes(&captured, b"4:2"), + "SGR 21 double underline should survive render, got: {:?}", + String::from_utf8_lossy(&captured) + ); + } + + /// Sanity check: confirm a styling attribute that vt100 *does* support + /// already survives the render. If this ever fails, something more + /// fundamental is broken than just attribute coverage. + #[test] + fn truecolor_foreground_survives_render() { + // SGR 38;2;R;G;B = truecolor foreground. vt100 supports this. + let input = b"\x1b[38;2;255;128;0morange text\x1b[0m"; + let Some(captured) = render_and_capture(input) else { + // pty-mock not built — skip. + return; + }; + assert!( + contains_bytes(&captured, b"38;2;255;128;0") + || contains_bytes(&captured, b"38:2::255:128:0"), + "truecolor foreground should survive render (sanity), got: {:?}", + String::from_utf8_lossy(&captured) + ); + } +} diff --git a/crates/claude-chill/src/lib.rs b/crates/claude-chill/src/lib.rs index 1219fef..3fc02d7 100644 --- a/crates/claude-chill/src/lib.rs +++ b/crates/claude-chill/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +mod color_preservation_tests; pub mod config; pub mod escape_filter; pub mod escape_sequences; diff --git a/crates/claude-chill/src/proxy.rs b/crates/claude-chill/src/proxy.rs index 071c15b..a4a29d7 100644 --- a/crates/claude-chill/src/proxy.rs +++ b/crates/claude-chill/src/proxy.rs @@ -412,6 +412,11 @@ impl Proxy { self.pty_master.as_raw_fd() } + #[cfg(test)] + pub(crate) fn force_render(&mut self, stdout_fd: &F) -> Result<()> { + self.render_vt_screen(stdout_fd) + } + #[cfg(test)] pub(crate) fn flush_drain_buffer(&mut self, stdout_fd: &F) -> Result<()> { if !self.pty_drain_buffer.is_empty() { -- 2.49.1 From 8ca1e806039fbd89c8f8f342af8e2bf5a188ab4f Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 1 May 2026 18:56:29 +0000 Subject: [PATCH 3/3] feat: replace vt100 with wezterm-term for the screen emulator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the rendering core. The previous backend (vt100 0.16) only stored six attributes — fg/bg color, bold, dim, italic, underline, inverse — and silently dropped everything else when re-emitting the screen. With modern Claude Code that meant losing OSC 8 hyperlinks, strikethrough, fancy underline styles, underline color, and so on. New stack: * wezterm-term::Terminal handles the byte-driven VT emulation. Its cell model preserves the full modern attribute set including hyperlinks and underline color. * termwiz::surface::Surface is used as a "mirror of last emitted state." Each render diffs the terminal's current visible lines against this mirror to compute the minimum change set. * termwiz::render::TerminfoRenderer encodes those Changes as bytes using a TrueColor + hyperlink-capable capability profile. A defensive OSC 8 close is appended to every render to handle the case where the diff ends inside a hyperlinked region (the renderer only emits the close on transition to a non-hyperlinked cell). Both termwiz and wezterm-term are pulled from the wezterm git repo at a pinned rev so the Line types unify across the dep graph. Test results, against the color-preservation harness from the previous commit (run via cargo test color_preservation -- --ignored on master): baseline (vt100): 0 / 5 passing after this commit: 3 / 5 passing (osc8, strikethrough, double underline) The two still-ignored tests (curly underline, underline color) are upstream renderer limitations: termwiz's TerminfoRenderer only emits Single/Double underline and never emits SGR 58. The cell model stores both correctly; only the emit step drops them. Tests are kept #[ignore] with comments calling out the upstream gap. Total: 118 passed, 4 ignored, 0 failed. --- Cargo.lock | 1315 +++++++++++++++-- crates/claude-chill/Cargo.toml | 4 +- .../src/color_preservation_tests.rs | 28 +- crates/claude-chill/src/lib.rs | 1 + crates/claude-chill/src/proxy.rs | 62 +- crates/claude-chill/src/screen_engine.rs | 264 ++++ 6 files changed, 1525 insertions(+), 149 deletions(-) create mode 100644 crates/claude-chill/src/screen_engine.rs diff --git a/Cargo.lock b/Cargo.lock index bd068f8..14d8452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,6 +23,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "0.6.21" @@ -67,6 +103,23 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -74,12 +127,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "atomic" -version = "0.6.1" +name = "as-slice" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" dependencies = [ - "bytemuck", + "stable_deref_trait", ] [[package]] @@ -88,6 +141,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "base64" version = "0.22.1" @@ -96,18 +192,24 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" @@ -121,6 +223,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -130,6 +241,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.19.1" @@ -142,6 +259,24 @@ version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -209,9 +344,15 @@ dependencies = [ "serde", "termwiz", "toml", - "vt100", + "wezterm-term", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -227,6 +368,46 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -284,6 +465,29 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "env_filter" version = "0.1.4" @@ -307,6 +511,26 @@ dependencies = [ "log", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -333,20 +557,50 @@ dependencies = [ ] [[package]] -name = "fancy-regex" -version = "0.11.0" +name = "exr" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ "bit-set", - "regex", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", ] [[package]] name = "filedescriptor" version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "libc", "thiserror 1.0.69", @@ -354,10 +608,15 @@ dependencies = [ ] [[package]] -name = "finl_unicode" -version = "1.4.0" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.3.0" +source = "git+https://github.com/wez/finl_unicode.git?branch=no_std#a1892f26245529f2ef3877a9ebd610c96cec07a6" [[package]] name = "fixedbitset" @@ -365,12 +624,37 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.9", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -404,11 +688,37 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -422,6 +732,157 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + [[package]] name = "indexmap" version = "2.13.0" @@ -432,6 +893,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -439,10 +911,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "itoa" -version = "1.0.17" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] [[package]] name = "jiff" @@ -468,6 +943,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -490,12 +975,34 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.12" @@ -506,6 +1013,12 @@ dependencies = [ "libc", ] +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "log" version = "0.4.29" @@ -513,13 +1026,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "mac_address" -version = "1.1.8" +name = "loop9" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" dependencies = [ - "nix 0.29.0", - "winapi", + "imgref", +] + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", ] [[package]] @@ -534,21 +1065,47 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.29.0" @@ -559,7 +1116,6 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", - "memoffset", ] [[package]] @@ -574,6 +1130,15 @@ dependencies = [ "libc", ] +[[package]] +name = "no_std_io2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -584,6 +1149,31 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -595,6 +1185,26 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -602,6 +1212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -631,6 +1242,24 @@ dependencies = [ "num-traits", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.8.5" @@ -701,7 +1330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -726,6 +1355,19 @@ dependencies = [ "siphasher", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "portable-atomic" version = "1.13.0" @@ -741,6 +1383,24 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.105" @@ -750,6 +1410,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.114", +] + [[package]] name = "pty-mock" version = "0.1.4" @@ -757,6 +1436,27 @@ dependencies = [ "clap", ] +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.43" @@ -778,7 +1478,27 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -787,6 +1507,85 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.17", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -827,6 +1626,12 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "rustversion" version = "1.0.22" @@ -883,6 +1688,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" @@ -903,12 +1714,39 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -937,6 +1775,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "terminfo" version = "0.9.0" @@ -944,7 +1793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" dependencies = [ "fnv", - "nom", + "nom 7.1.3", "phf", "phf_codegen", ] @@ -960,19 +1809,16 @@ dependencies = [ [[package]] name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +version = "0.24.0" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "anyhow", - "base64", "bitflags 2.10.0", "fancy-regex", "filedescriptor", "finl_unicode", "fixedbitset", - "hex", - "lazy_static", + "image", "libc", "log", "memmem", @@ -994,9 +1840,13 @@ dependencies = [ "vtparse", "wezterm-bidi", "wezterm-blob-leases", + "wezterm-cell", + "wezterm-char-props", "wezterm-color-types", "wezterm-dynamic", + "wezterm-escape-parser", "wezterm-input-types", + "wezterm-surface", "winapi", ] @@ -1040,6 +1890,45 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "toml" version = "0.8.23" @@ -1099,6 +1988,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -1106,10 +2004,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] -name = "unicode-width" -version = "0.2.2" +name = "url" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" @@ -1123,44 +2033,32 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "atomic", "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vt100" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9" -dependencies = [ - "itoa", - "unicode-width", - "vte", -] - -[[package]] -name = "vte" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" -dependencies = [ - "arrayvec", - "memchr", -] - [[package]] name = "vtparse" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +version = "0.7.0" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "utf8parse", ] @@ -1225,11 +2123,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "wezterm-bidi" version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "log", "wezterm-dynamic", @@ -1238,65 +2141,166 @@ dependencies = [ [[package]] name = "wezterm-blob-leases" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "getrandom 0.3.4", - "mac_address", "sha2", "thiserror 1.0.69", "uuid", ] +[[package]] +name = "wezterm-cell" +version = "0.1.0" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" +dependencies = [ + "finl_unicode", + "image", + "log", + "ordered-float", + "serde", + "sha2", + "thiserror 1.0.69", + "wezterm-blob-leases", + "wezterm-char-props", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-escape-parser", +] + +[[package]] +name = "wezterm-char-props" +version = "0.1.3" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" +dependencies = [ + "phf", + "serde", + "ucd-trie", +] + [[package]] name = "wezterm-color-types" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "csscolorparser", "deltae", - "lazy_static", + "num-traits", + "serde", "wezterm-dynamic", ] [[package]] name = "wezterm-dynamic" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "log", "ordered-float", "strsim", - "thiserror 1.0.69", + "thiserror 2.0.17", "wezterm-dynamic-derive", ] [[package]] name = "wezterm-dynamic-derive" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "wezterm-escape-parser" +version = "0.1.0" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" +dependencies = [ + "base64", + "bitflags 2.10.0", + "hex", + "image", + "log", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "sha2", + "thiserror 2.0.17", + "vtparse", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + [[package]] name = "wezterm-input-types" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" dependencies = [ "bitflags 1.3.2", "euclid", - "lazy_static", "serde", "wezterm-dynamic", ] +[[package]] +name = "wezterm-surface" +version = "0.1.0" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" +dependencies = [ + "bitflags 2.10.0", + "fancy-regex", + "finl_unicode", + "fixedbitset", + "ordered-float", + "siphasher", + "unicode-segmentation", + "wezterm-bidi", + "wezterm-cell", + "wezterm-char-props", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-escape-parser", + "wezterm-input-types", +] + +[[package]] +name = "wezterm-term" +version = "0.1.0" +source = "git+https://github.com/wezterm/wezterm?rev=577474d89ee61aef4a48145cdec82a638d874751#577474d89ee61aef4a48145cdec82a638d874751" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "csscolorparser", + "downcast-rs", + "finl_unicode", + "hex", + "humansize", + "image", + "lazy_static", + "log", + "lru", + "miniz_oxide 0.7.4", + "num-traits", + "ordered-float", + "serde", + "terminfo", + "termwiz", + "unicode-normalization", + "url", + "wezterm-bidi", + "wezterm-cell", + "wezterm-dynamic", + "wezterm-escape-parser", + "wezterm-surface", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1348,3 +2352,136 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/crates/claude-chill/Cargo.toml b/crates/claude-chill/Cargo.toml index 376cf37..ac147d7 100644 --- a/crates/claude-chill/Cargo.toml +++ b/crates/claude-chill/Cargo.toml @@ -25,7 +25,7 @@ nix = { version = "0.30", features = ["term", "signal", "poll", "process", "fs"] libc = "0.2" serde = { version = "1", features = ["derive"] } toml = "0.8" -vt100 = "0.16" -termwiz = "0.23" +termwiz = { git = "https://github.com/wezterm/wezterm", rev = "577474d89ee61aef4a48145cdec82a638d874751" } +wezterm-term = { git = "https://github.com/wezterm/wezterm", rev = "577474d89ee61aef4a48145cdec82a638d874751" } log = "0.4" env_logger = "0.11" diff --git a/crates/claude-chill/src/color_preservation_tests.rs b/crates/claude-chill/src/color_preservation_tests.rs index 6fed42a..260c8c8 100644 --- a/crates/claude-chill/src/color_preservation_tests.rs +++ b/crates/claude-chill/src/color_preservation_tests.rs @@ -4,17 +4,20 @@ //! force a render, capture what the proxy emits to its stdout side, and //! assert that the styling survives the round-trip. //! -//! They currently FAIL on master because the underlying `vt100 0.16` -//! emulator only models a handful of attributes (fg/bg color, bold, dim, -//! italic, underline, inverse). Anything else — OSC 8 hyperlinks, -//! strikethrough, fancy underline styles, underline color — is silently -//! dropped on render. +//! After the swap from `vt100 0.16` to `wezterm-term` + `termwiz` Surface +//! diffing, most of the modern attribute set is preserved end-to-end: +//! OSC 8 hyperlinks, strikethrough, double underline, and truecolor all +//! round-trip. Two known gaps remain — both are limitations of termwiz's +//! `TerminfoRenderer`, not of the cell model: //! -//! They are gated behind `#[ignore]` so CI stays green. Run them with: -//! cargo test -p claude-chill --lib color_preservation -- --ignored +//! * Curly / dotted / dashed underline styles (`SGR 4:3`, `4:4`, `4:5`): +//! stored on the cell, never emitted on render. +//! * Underline color (`SGR 58`): stored on the cell, never emitted on +//! render. //! -//! The expectation is that they pass once the emulator is swapped to -//! termwiz's `Surface`. Remove the `#[ignore]` attributes at that point. +//! Tests for those two are kept `#[ignore]` so CI stays green. They will +//! pass if/when termwiz's renderer learns to emit them (or we replace +//! the emit step with a custom serializer). #[cfg(test)] mod tests { @@ -119,7 +122,6 @@ mod tests { } #[test] - #[ignore] // FIXME: passes once VT emulator swap to termwiz lands fn osc8_hyperlink_survives_render() { // OSC 8 hyperlinks — used for clickable file paths / URLs. // Format: ESC ] 8 ; ; URL ESC \ ... ESC ] 8 ; ; ESC \ @@ -142,7 +144,6 @@ mod tests { } #[test] - #[ignore] // FIXME: passes once VT emulator swap to termwiz lands fn strikethrough_survives_render() { // SGR 9 = strikethrough. vt100 0.16 has no field for it. let input = b"\x1b[9mstruck through text\x1b[0m"; @@ -155,7 +156,7 @@ mod tests { } #[test] - #[ignore] // FIXME: passes once VT emulator swap to termwiz lands + #[ignore] // termwiz TerminfoRenderer only emits Single/Double underline. fn curly_underline_survives_render() { // SGR 4:3 = curly underline (kitty/wezterm/ghostty all support). let input = b"\x1b[4:3mcurly underline\x1b[0m"; @@ -168,7 +169,7 @@ mod tests { } #[test] - #[ignore] // FIXME: passes once VT emulator swap to termwiz lands + #[ignore] // termwiz TerminfoRenderer doesn't emit SGR 58 underline color. fn underline_color_survives_render() { // SGR 58:2::R:G:B = truecolor underline color. let input = b"\x1b[4m\x1b[58:2::255:0:0mcolored underline\x1b[0m"; @@ -181,7 +182,6 @@ mod tests { } #[test] - #[ignore] // FIXME: passes once VT emulator swap to termwiz lands fn double_underline_survives_render() { // SGR 21 = doubly underlined. let input = b"\x1b[21mdouble underlined\x1b[0m"; diff --git a/crates/claude-chill/src/lib.rs b/crates/claude-chill/src/lib.rs index 3fc02d7..9e9d64e 100644 --- a/crates/claude-chill/src/lib.rs +++ b/crates/claude-chill/src/lib.rs @@ -10,3 +10,4 @@ pub mod key_parser; pub mod line_buffer; pub mod proxy; pub mod redraw_throttler; +pub mod screen_engine; diff --git a/crates/claude-chill/src/proxy.rs b/crates/claude-chill/src/proxy.rs index a4a29d7..5e21a25 100644 --- a/crates/claude-chill/src/proxy.rs +++ b/crates/claude-chill/src/proxy.rs @@ -6,6 +6,7 @@ use crate::escape_sequences::{ }; use crate::history_filter::HistoryFilter; use crate::line_buffer::LineBuffer; +use crate::screen_engine::ScreenEngine; use anyhow::{Context, Result}; use log::debug; use memchr::memmem; @@ -102,8 +103,7 @@ pub struct Proxy { original_termios: Option, history: LineBuffer, history_filter: HistoryFilter, - vt_parser: vt100::Parser, - vt_prev_screen: Option, + screen: ScreenEngine, last_output_time: Option, last_render_time: Option, last_stdin_time: Option, @@ -270,7 +270,7 @@ impl Proxy { let stdout_fd = io::stdout(); write_all(&stdout_fd, BRACKETED_PASTE_ENABLE)?; - let vt_parser = vt100::Parser::new(winsize.ws_row, winsize.ws_col, 0); + let screen = ScreenEngine::new(winsize.ws_row, winsize.ws_col); // Seed history with clear screen so replay starts fresh let mut history = LineBuffer::new(config.max_history_lines); @@ -288,8 +288,7 @@ impl Proxy { pty_master: pty.master, child, original_termios: terminal_guard.take(), - vt_parser, - vt_prev_screen: None, + screen, last_output_time: None, last_render_time: None, last_stdin_time: None, @@ -330,7 +329,7 @@ impl Proxy { rows: u16, cols: u16, ) -> Self { - let vt_parser = vt100::Parser::new(rows, cols, 0); + let screen = ScreenEngine::new(rows, cols); let mut history = LineBuffer::new(config.max_history_lines); history.push_bytes(CLEAR_SCREEN); history.push_bytes(CURSOR_HOME); @@ -343,8 +342,7 @@ impl Proxy { pty_master, child, original_termios: None, - vt_parser, - vt_prev_screen: None, + screen, last_output_time: None, last_render_time: None, last_stdin_time: None, @@ -403,8 +401,8 @@ impl Proxy { } #[cfg(test)] - pub(crate) fn vt_screen_text(&self) -> String { - self.vt_parser.screen().contents() + pub(crate) fn vt_screen_text(&mut self) -> String { + self.screen.screen_text() } #[cfg(test)] @@ -532,7 +530,7 @@ impl Proxy { // Feed VT but NOT history while in alt screen // Alt screen content (TUI editors, etc.) shouldn't be in lookback history if feed_vt { - self.vt_parser.process(data); + self.screen.process(data); } return self.process_output_alt_screen(data, stdout_fd); } @@ -545,7 +543,7 @@ impl Proxy { // Feed data to VT emulator (unless already fed by caller) if feed_vt { - self.vt_parser.process(data); + self.screen.process(data); } self.vt_render_pending = true; self.last_output_time = Some(Instant::now()); @@ -621,7 +619,7 @@ impl Proxy { // Force full VT render to restore main screen content debug!("process_output_alt_screen: rendering VT screen after alt exit"); - self.vt_prev_screen = None; + self.screen.force_full_next(); self.render_vt_screen(stdout_fd)?; // Data after ALT_EXIT was already fed to VT and history when we processed @@ -828,32 +826,14 @@ impl Proxy { } fn render_vt_screen(&mut self, stdout_fd: &F) -> Result<()> { - let is_diff = self.vt_prev_screen.is_some(); + let body = self.screen.render()?; self.output_buffer.clear(); self.output_buffer.extend_from_slice(SYNC_START); - - match &self.vt_prev_screen { - Some(prev) => { - // Diff-based render: only send changes - self.output_buffer - .extend_from_slice(&self.vt_parser.screen().contents_diff(prev)); - } - None => { - // First render: full screen - self.output_buffer - .extend_from_slice(&self.vt_parser.screen().contents_formatted()); - } - } - - self.output_buffer - .extend_from_slice(&self.vt_parser.screen().cursor_state_formatted()); + self.output_buffer.extend_from_slice(&body); self.output_buffer.extend_from_slice(SYNC_END); - debug!( - "render_vt_screen: diff={} output_len={}\n", - is_diff, - self.output_buffer.len() - ); + debug!("render_vt_screen: output_len={}", self.output_buffer.len()); + // Can't use write_to_terminal here due to borrow checker - can't pass // &self.output_buffer while also taking &mut self Self::update_kitty_mode_helper( @@ -864,8 +844,6 @@ impl Proxy { ); write_all(stdout_fd, &self.output_buffer)?; - // Store current screen for next diff - self.vt_prev_screen = Some(self.vt_parser.screen().clone()); self.vt_render_pending = false; self.last_render_time = Some(Instant::now()); Ok(()) @@ -942,7 +920,7 @@ impl Proxy { write_all(stdout_fd, &self.output_buffer)?; // Force full VT render on next output since terminal now shows history - self.vt_prev_screen = None; + self.screen.force_full_next(); Ok(()) } @@ -1164,7 +1142,7 @@ impl Proxy { // Force full render since terminal was showing history debug!("exit_lookback_mode: rendering VT screen"); - self.vt_prev_screen = None; + self.screen.force_full_next(); self.render_vt_screen(stdout_fd)?; Ok(()) @@ -1177,11 +1155,7 @@ impl Proxy { winsize.ws_row, winsize.ws_col ); // Resize VT emulator - self.vt_parser - .screen_mut() - .set_size(winsize.ws_row, winsize.ws_col); - // Force full render on next frame since size changed - self.vt_prev_screen = None; + self.screen.resize(winsize.ws_row, winsize.ws_col); // Forward to child process unsafe { libc::ioctl( diff --git a/crates/claude-chill/src/screen_engine.rs b/crates/claude-chill/src/screen_engine.rs new file mode 100644 index 0000000..2302812 --- /dev/null +++ b/crates/claude-chill/src/screen_engine.rs @@ -0,0 +1,264 @@ +//! Screen emulator backed by `wezterm-term`. +//! +//! This replaces the previous `vt100` backing. wezterm-term gives us full +//! modern attribute coverage — OSC 8 hyperlinks, strikethrough, fancy +//! underline styles, underline color — that vt100 0.16 dropped on render. +//! +//! Architecture: +//! - `wezterm_term::Terminal` is the byte-driven VT emulator. Feed bytes +//! via `advance_bytes`; query state via `screen().visible_lines()`. +//! - A `termwiz::surface::Surface` mirrors the last bytes we *emitted* +//! downstream; the next render diffs current visible lines against +//! this Surface to compute the minimum change set. +//! - `termwiz::render::TerminfoRenderer` serializes those Changes into +//! bytes using a TrueColor / hyperlink-capable capability set. + +use anyhow::Result; +use std::io::Write; +use std::sync::Arc; +use termwiz::caps::{Capabilities, ColorLevel, ProbeHints}; +use termwiz::render::RenderTty; +use termwiz::render::terminfo::TerminfoRenderer; +use termwiz::surface::{Change, Position, Surface}; +use wezterm_term::color::ColorPalette; +use wezterm_term::{Terminal, TerminalConfiguration, TerminalSize}; + +#[derive(Debug)] +struct ProxyTermConfig; + +impl TerminalConfiguration for ProxyTermConfig { + fn color_palette(&self) -> ColorPalette { + ColorPalette::default() + } +} + +/// `Write + RenderTty` adapter that buffers everything in memory. +struct CapturedTty { + buf: Vec, + cols: usize, + rows: usize, +} + +impl Write for CapturedTty { + fn write(&mut self, data: &[u8]) -> std::io::Result { + self.buf.extend_from_slice(data); + Ok(data.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl RenderTty for CapturedTty { + fn get_size_in_cells(&mut self) -> termwiz::Result<(usize, usize)> { + Ok((self.cols, self.rows)) + } +} + +pub struct ScreenEngine { + terminal: Terminal, + /// Mirror of what we've *already emitted* downstream. Diffing the + /// current terminal screen against this gives us the minimal change + /// set to send. + mirror: Surface, + renderer: TerminfoRenderer, + rows: usize, + cols: usize, + /// When set, the next render emits the full screen instead of a diff. + force_full: bool, +} + +impl ScreenEngine { + pub fn new(rows: u16, cols: u16) -> Self { + let rows = rows as usize; + let cols = cols as usize; + + let size = TerminalSize { + rows, + cols, + ..Default::default() + }; + let config: Arc = Arc::new(ProxyTermConfig); + // We don't currently surface terminal responses (DA queries, cursor + // position reports, etc) back upstream. Matches the previous vt100 + // behavior, which simply ignored them. + let writer: Box = Box::new(std::io::sink()); + let terminal = Terminal::new( + size, + config, + "claude-chill", + env!("CARGO_PKG_VERSION"), + writer, + ); + + let mirror = Surface::new(cols, rows); + + let caps = Capabilities::new_with_hints( + ProbeHints::default() + .color_level(Some(ColorLevel::TrueColor)) + .hyperlinks(Some(true)) + .bracketed_paste(Some(true)), + ) + .expect("Capabilities::new_with_hints"); + let renderer = TerminfoRenderer::new(caps); + + Self { + terminal, + mirror, + renderer, + rows, + cols, + force_full: true, + } + } + + /// Feed bytes from the child pty into the emulator. + pub fn process(&mut self, data: &[u8]) { + self.terminal.advance_bytes(data); + } + + pub fn resize(&mut self, rows: u16, cols: u16) { + let rows = rows as usize; + let cols = cols as usize; + self.terminal.resize(TerminalSize { + rows, + cols, + ..Default::default() + }); + self.mirror.resize(cols, rows); + self.rows = rows; + self.cols = cols; + self.force_full = true; + } + + /// Mark the next `render()` call as a full screen redraw rather than a diff. + pub fn force_full_next(&mut self) { + self.force_full = true; + } + + /// Build the change stream needed to bring downstream from `mirror` to + /// the current emulator state, encode it as bytes, and update `mirror`. + pub fn render(&mut self) -> Result> { + let lines = { + let screen = self.terminal.screen(); + let rows = self.rows as i64; + let start = screen.phys_row(0); + let end = screen.phys_row(rows - 1) + 1; + screen.lines_in_phys_range(start..end) + }; + let line_refs: Vec<&_> = lines.iter().collect(); + + // If forced, reset the mirror so diff_lines emits the full screen. + if self.force_full { + self.mirror = Surface::new(self.cols, self.rows); + self.force_full = false; + } + + let mut changes = self.mirror.diff_lines(line_refs); + + // Position the downstream cursor where the emulator's cursor is. + let cursor = self.terminal.cursor_pos(); + let row = cursor.y.max(0) as usize; + let col = cursor.x; + changes.push(Change::CursorPosition { + x: Position::Absolute(col), + y: Position::Absolute(row), + }); + + // Keep the mirror in sync with what we just emitted (minus the final + // cursor reposition, which isn't a content change). + self.mirror.add_changes(changes.clone()); + + let mut tty = CapturedTty { + buf: Vec::new(), + cols: self.cols, + rows: self.rows, + }; + self.renderer + .render_to(&changes, &mut tty) + .map_err(|e| anyhow::anyhow!("renderer: {}", e))?; + + // Defensive OSC 8 close: TerminfoRenderer only emits the hyperlink + // close when transitioning to a non-hyperlinked cell. If our diff + // ends inside the hyperlinked region (rest of the row was blank + // and didn't need updating), the close is never emitted and the + // downstream terminal may keep the hyperlink active across the + // boundary. Appending a close here is a no-op when there's no + // active hyperlink. + tty.buf.extend_from_slice(b"\x1b]8;;\x1b\\"); + + Ok(tty.buf) + } + + /// Plain-text contents of the visible screen, for tests / lookback. + #[cfg(test)] + pub fn screen_text(&mut self) -> String { + let lines = { + let screen = self.terminal.screen(); + let rows = self.rows as i64; + let start = screen.phys_row(0); + let end = screen.phys_row(rows - 1) + 1; + screen.lines_in_phys_range(start..end) + }; + let mut out = String::new(); + for (i, line) in lines.iter().enumerate() { + if i > 0 { + out.push('\n'); + } + out.push_str(&line.as_str()); + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn contains(haystack: &[u8], needle: &[u8]) -> bool { + haystack.windows(needle.len()).any(|w| w == needle) + } + + fn render(input: &[u8]) -> Vec { + let mut e = ScreenEngine::new(24, 80); + e.process(input); + e.render().expect("render") + } + + #[test] + fn osc8_hyperlink_round_trip() { + let mut input = Vec::new(); + input.extend_from_slice(b"\x1b]8;;https://example.com\x1b\\"); + input.extend_from_slice(b"link"); + input.extend_from_slice(b"\x1b]8;;\x1b\\"); + let out = render(&input); + assert!( + contains(&out, b"\x1b]8;;https://example.com"), + "OSC 8 missing in: {:?}", + String::from_utf8_lossy(&out) + ); + } + + #[test] + fn strikethrough_round_trip() { + let out = render(b"\x1b[9mstruck\x1b[0m"); + assert!( + contains(&out, b"[9m") || contains(&out, b";9m"), + "SGR 9 missing in: {:?}", + String::from_utf8_lossy(&out) + ); + } + + #[test] + fn truecolor_round_trip() { + let out = render(b"\x1b[38;2;255;128;0morange\x1b[0m"); + assert!( + contains(&out, b"38:2::255:128:0") + || contains(&out, b"38;2;255;128;0") + || contains(&out, b"38:2:255:128:0"), + "truecolor fg missing in: {:?}", + String::from_utf8_lossy(&out) + ); + } +} -- 2.49.1