Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"] }

Expand Down
46 changes: 45 additions & 1 deletion NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
45 changes: 45 additions & 0 deletions src/bin/etr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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! {
Expand Down
Loading