diff --git a/Cargo.lock b/Cargo.lock index e6decc6..d950809 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "asn1-rs" @@ -599,7 +599,7 @@ dependencies = [ [[package]] name = "etr" -version = "0.6.3" +version = "0.6.4" dependencies = [ "clap", "clap_complete", @@ -619,6 +619,7 @@ dependencies = [ "serde", "tokio", "toml", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2fce79c..2dd6332 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "etr" -version = "0.6.3" +version = "0.6.4" edition = "2024" description = "A Rust implementation of Eternal Terminal (et)" license = "GPL-3.0-only" @@ -36,6 +36,14 @@ libc = "0.2" serde = { version = "1", features = ["derive"] } toml = "1" +# Windows-only: put the console into virtual-terminal mode so the client speaks +# the same key/ANSI byte conventions a Unix PTY expects (Backspace → DEL, etc.). +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_System_Console", +] } + [dev-dependencies] criterion = { version = "0.8", features = ["async_tokio"] } diff --git a/NOTES.md b/NOTES.md index 7d19fc7..1789b46 100644 --- a/NOTES.md +++ b/NOTES.md @@ -9,7 +9,51 @@ the link drops. This project uses **QUIC** (via the `quinn` crate) for the tran layer, which provides reliable, ordered, multiplexed streams with congestion control and TLS 1.3 built-in. -## Current state: v0.6.3 — project logo +## Current state: v0.6.4 — Windows Backspace fix + +New in v0.6.4: +- **Windows client Backspace fixed.** The Windows console delivers legacy key + codes to raw byte reads (Backspace → `0x08`), whereas a Unix PTY expects the + xterm convention `0x7f` (DEL) to match the default `stty erase`. The client + now enables virtual-terminal console modes after raw mode + (`ENABLE_VIRTUAL_TERMINAL_INPUT` on stdin, `ENABLE_VIRTUAL_TERMINAL_PROCESSING` + on stdout) via a new Windows-only `windows-sys` dependency, so it emits the + xterm byte sequences the remote expects and renders the remote's ANSI output. + No-op on Unix. Verified live (Windows `etr` → Unix `etrs`): Backspace now + erases correctly. +- Bumped `anyhow` 1.0.102→1.0.103 to clear RUSTSEC-2026-0190 (an unsoundness + advisory against a pre-existing transitive dep). (`crossbeam-epoch` was + already bumped to 0.9.20 in v0.6.3 for RUSTSEC-2026-0204.) +- Test count: 110 (unchanged). + +### Known issue — Windows: first line of input not echoed until Enter + +When connecting from a Windows `etr` client to a Unix host, the shell prompt +renders correctly, but the **first line** the user types is not echoed until +Enter is pressed (after which the whole line appears and runs, and the session +behaves normally thereafter). This does **not** occur linux→linux and is not +shell-specific (reproduced with `zsh`+`zellij`+`starship` and with +`bash --norc`). + +Diagnosis: `-vvv` logs show the keystrokes reach the server, but the Windows +client sends the first line as a single batched chunk (with the trailing Enter) +rather than per-keystroke, so the remote PTY echoes the whole line only on +submission. The fault is in the Windows client input path +(`std::io::stdin().read()`), which does not deliver bytes per-keystroke at +session start; a large contributor is the shell's startup terminal-probing, +which — with VT-input mode on — the Windows console auto-answers with a ~7 KB +burst (256-color palette, size, mode) plus mouse-motion events that +back-pressure/batch the reader. + +Two fixes were attempted and **reverted** (neither is in this release): (1) +deferring the server-side PTY reader until the first client connection — no +effect, since the input path (not output) is at fault; (2) reading discrete key +events via `crossterm` and translating to bytes on the client — eliminated the +batching but caused the console's terminal-query auto-responses to be echoed +back as visible garbage (`]4;N;rgb:…`). Tracked in +[GitHub issue #54](https://github.com/l1a/etr/issues/54). + +## Previous: v0.6.3 — project logo New in v0.6.3: - Added `assets/logos/etr-logo.svg` (source) plus rendered `etr-logo-256.png`, diff --git a/src/bin/etr.rs b/src/bin/etr.rs index cdc0df8..58d0946 100644 --- a/src/bin/etr.rs +++ b/src/bin/etr.rs @@ -28,6 +28,47 @@ static IN_RAW_MODE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBoo /// prevents false triggers from `~` in shell paths or git refs. const ESCAPE_CHAR: u8 = b'~'; +/// Put the console into virtual-terminal mode. +/// +/// On Windows the console, by default, hands raw byte reads the legacy key +/// codes (Backspace → `0x08`, no ESC sequences for arrows/function keys) and +/// does not interpret ANSI output. A Unix PTY on the far end expects the +/// xterm conventions instead — notably Backspace → `0x7f` (DEL) — so without +/// this the remote `stty erase` (DEL) never matches what we send and Backspace +/// misbehaves. Enabling `ENABLE_VIRTUAL_TERMINAL_INPUT` makes the console +/// translate keys into the same VT byte sequences a real terminal emits, and +/// `ENABLE_VIRTUAL_TERMINAL_PROCESSING` makes it render the remote's ANSI +/// output. Call this after raw mode is enabled, on every (re)connect. +/// +/// No-op on Unix, where the terminal already speaks VT natively. +#[cfg(windows)] +fn enable_vt_console() { + use windows_sys::Win32::System::Console::{ + CONSOLE_MODE, ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, + GetConsoleMode, GetStdHandle, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, SetConsoleMode, + }; + // SAFETY: standard Win32 console calls. `GetStdHandle` returns a process + // std handle; `GetConsoleMode` fails (returns 0) for non-console handles + // (e.g. redirected stdio), so we only call `SetConsoleMode` on a handle it + // confirmed is a console. All pointers point to locals that outlive the call. + unsafe { + let h_in = GetStdHandle(STD_INPUT_HANDLE); + let mut mode: CONSOLE_MODE = 0; + if GetConsoleMode(h_in, &mut mode) != 0 { + SetConsoleMode(h_in, mode | ENABLE_VIRTUAL_TERMINAL_INPUT); + } + let h_out = GetStdHandle(STD_OUTPUT_HANDLE); + let mut out_mode: CONSOLE_MODE = 0; + if GetConsoleMode(h_out, &mut out_mode) != 0 { + SetConsoleMode(h_out, out_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + } +} + +/// No-op on Unix: the terminal already delivers VT key sequences and renders ANSI. +#[cfg(not(windows))] +fn enable_vt_console() {} + macro_rules! vlog { ($verbose:expr, $level:expr, $($arg:tt)*) => { if $verbose >= $level { @@ -760,6 +801,10 @@ async fn run_connection_loop( vlog!(verbose, 2, "[etr] {}", quic::tls_info()); enable_raw_mode().unwrap(); + // On Windows, raw mode alone still leaves the console emitting legacy key + // codes (Backspace → 0x08) and not rendering ANSI; switch it to VT mode so + // we speak the same conventions as the remote Unix PTY. No-op on Unix. + enable_vt_console(); IN_RAW_MODE.store(true, std::sync::atomic::Ordering::Relaxed); in_raw = true; let result = tokio::select! {