summaryrefslogtreecommitdiff
path: root/src/tty
diff options
context:
space:
mode:
Diffstat (limited to 'src/tty')
-rw-r--r--src/tty/tty.zig56
1 files changed, 55 insertions, 1 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 007abd49..b4bd4175 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -161,7 +161,47 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
// resize watcher: plain detached thread (not io.concurrent — teardown
// joins those, and sigwait never returns); dies with the process
(try std.Thread.spawn(.{}, winchWatch, .{ &loop, &vx, &tty })).detach();
- vx.queryTerminal(tty.writer(), std.Io.Duration.fromMilliseconds(2)) catch {};
+ // Capability handshake — SEND the probes, do not wait on them. This was
+ // queryTerminal(2ms), which blocks on a futex until DA1 comes back. The
+ // number has to beat one terminal round trip: a local terminal answers in
+ // microseconds, `ssh localhost` in under 1ms, and any real link never.
+ // Measured over sshd on :22 with the replies delayed to model the wire,
+ // 2ms already loses at 5ms RTT and everything above.
+ //
+ // Losing it is worse than never probing, because vaxis splits detect from
+ // enable and only detect respects the deadline. queryTerminal sets
+ // queries_done the moment the futex times out, and the two replies vaxis
+ // gates on that flag — explicit width and scaled text, both answered as a
+ // cursor-position report — stop being recognised as probe replies and are
+ // handed to US as shift-F3/alt-F3 keypresses. The replies it does NOT
+ // gate (mode 2027, kitty keyboard/graphics, sgr-pixels) keep landing and
+ // keep mutating vx.caps from the reader thread, long after
+ // enableDetectedFeatures ran and declined to switch those modes on. So
+ // over ssh the terminal sat in its default modes while caps claimed
+ // otherwise — kitty keyboard was never actually pushed, ever. Raising the
+ // timeout only moves the link speed at which that happens.
+ //
+ // Resolve it on the loop instead. DA1 is last in the probe string and
+ // terminals answer in order, so when vaxis's reader flips queries_done
+ // every earlier reply is already applied — no window left to miss at any
+ // latency, and the enable lands before the next frame (see caps_pending
+ // in the render path). A terminal that never answers keeps the defaults,
+ // which is what the 2ms timeout produced anyway, and with no caps
+ // enableDetectedFeatures writes no bytes — the snapshot goldens, where
+ // nothing ever answers, do not move. Startup gets 2ms faster, not slower.
+ //
+ // ponytail: nothing wakes the loop for DA1 alone. In practice the reply
+ // burst carries the mode-2048 size report too, which posts a winsize and
+ // turns the loop; a terminal idle from boot that lands DA1 between two
+ // parks keeps the defaults until the user's first keystroke (decoded
+ // legacy, which vaxis handles). Ceiling accepted because nothing in the
+ // render path reads the missing caps: pardes writes one codepoint per
+ // cell plus an explicit blank spacer under a wide glyph, and vaxis's
+ // Cell.width defaults to 1, so gwidth — the only consumer of
+ // caps.unicode — is never called. If that ever changes, wake the loop on
+ // vx.query_futex from a one-shot thread instead.
+ try vx.queryTerminalSend(tty.writer());
+ var caps_pending = true;
// now threads are fine: start a reader task per pty
for (&ptys, 0..) |*slot, id| if (slot.*) |*pt| {
@@ -287,6 +327,20 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
if (look.shellCwd(pt.pid, &lbuf)) |cwd| core.setCwd(id, cwd);
};
+ // The handshake landed (vaxis's reader flips queries_done on DA1, the
+ // last probe answered): put the terminal into the modes the caps now
+ // claim, before anything is drawn under them. Polled here rather than
+ // done where the replies arrive because that is the reader thread, and
+ // this is the only thread allowed to touch the tty writer. The repaint
+ // matters as much as the enable: earlier frames were drawn under the
+ // pre-handshake caps, and vaxis's shadow grid has to be re-established
+ // under the new ones or it keeps skipping cells it thinks are current.
+ if (caps_pending and vx.queries_done.load(.unordered)) {
+ caps_pending = false;
+ vx.enableDetectedFeatures(tty.writer()) catch {};
+ vx.queueRefresh();
+ }
+
// ---- render: surface -> vaxis, cell for cell ----
_ = frame_arena.reset(.retain_capacity);
const tz_core = tracy.zone(@src(), "core.render");