diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-31 03:51:34 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:08 -0300 |
| commit | 32b4245962b75916be0ebe89225375430e03c36f (patch) | |
| tree | 1d2ccc02338a5da4f92a9c31267468bd54bd1a4b | |
| parent | 6cd33661064df4163f3bd98db432526ad2165b7f (diff) | |
| download | pardes-32b4245962b75916be0ebe89225375430e03c36f.tar.gz pardes-32b4245962b75916be0ebe89225375430e03c36f.zip | |
the capability handshake cannot lose a race it never runs
queryTerminal(2ms) blocks on a futex until DA1 comes back, and the number has
to beat one terminal round trip: local answers in microseconds, ssh localhost
under a millisecond, any real link never. Measured against sshd with the
replies delayed to model the wire, 2ms already loses at 5ms RTT.
Losing it is worse than never probing, because vaxis splits detect from enable
and only detect respects the deadline. The flag flips the moment the futex
times out, so the two replies gated on it — explicit width and scaled text,
both spelled as a cursor-position report — stop being read as probe replies and
arrive at the app as shift-F3 and alt-F3 keypresses, while the ungated ones
keep mutating caps from the reader thread long after enable already declined to
switch those modes on. Over ssh the terminal sat in its default modes while
caps claimed otherwise: kitty keyboard was never actually pushed, ever.
So send the probes and resolve them on the loop. DA1 is last and terminals
answer in order, so when the reader flips the flag every earlier reply is
applied — no window to miss at any latency. Verified over real ssh at 5 through
500ms RTT: 7/7 caps and kitty keyboard actually enabled at every one, where
before it was 5/7 and never. Startup is 2ms faster, no golden moves (nothing
answers in the harness, and with no caps enable writes no bytes).
Honest scope: I could not reproduce the reported stale characters, only the
handshake bug behind them. The width half of the theory is inert — vaxis's
Cell.width defaults to 1 and pardes writes one codepoint per cell with an
explicit spacer, so gwidth, the only consumer of caps.unicode, is never called.
That is written down so nobody re-derives it. If the dirty screen survives,
the next suspect is vaxis's own carry-over for an escape sequence split across
a read boundary (Loop.zig:174-190, wrong length and an off-by-one): four wheel
events sent whole scroll four notches, the same four split at a `;` with a
60ms gap scroll zero. Network framing is exactly what makes those gaps. It is
an input bug in a vendored dep and wants its own change.
| -rw-r--r-- | next-steps.txt | 2 | ||||
| -rw-r--r-- | src/tty/tty.zig | 56 |
2 files changed, 56 insertions, 2 deletions
diff --git a/next-steps.txt b/next-steps.txt index 250908c6..a068041c 100644 --- a/next-steps.txt +++ b/next-steps.txt @@ -6,7 +6,7 @@ + Scrolling on a touchpad moves horizontally accidentally way too much, add some heuristics to prioritize vertical scrolling and make horizontal scrolling work only when the user is a bit more explicit about it - for example by measuring if there was recent high-enough vertical movement, etc. + Add a new builtin to list the themes and create an output window to select those themes; each theme now must live on its own .zig file, when moving with n/N on out ThemeSel output window it will select that theme so it's a interactive way to select the themes. We're being clever now with how these output windows will work, this feature depends on the new semantics for output windows but basically now the ThemeSel output panes will set the Command flag that will basically tell that when doing n/N movement on the output pane instead of Look-ing it will Execute the line, so the ThemeSel output pane will be like: (Theme acme, Theme a, Theme b) seperated by lines so the n/N will automatically consider selecting the Theme + argument (this is not output pane specific, it should be generalized to work on every pane kind), and will Exec it. Add new themes that will be automatically exported from helix and zed, a new binary will execute that will generate .zig files, this binary will be added as a execute step on the build.zig so when building it will regenarate those .zig that will be then included on the runtime as themes. - Use zig's 0.16 new threaded io and the std functions that the build system uses to listen for file changes, it needs to use something from the os so it's not a busy wait. It will only track files on open panes. when a file is updated, doing Undo will return to the state that it was before so if a file automatically updates the user won't lose the unsaved state as it was before and the editor won't need to handle with merging the unsaved changes and the new update. -- There's some bug when rendering from a ssh session - the screen gets dirty with old chars when scrolling. ++ There's some bug when rendering from a ssh session - the screen gets dirty with old chars when scrolling. - There's some bad performance when dealing with specially large files or long lines; write a perf harness for this and optimize the performance. + The Look and Exec should be regular semantic entities, i.e. their only special feature should be the keys/shortcuts they're assigned so we could execute Look main.zig and it would call the regular look on it, but also we could Look some string like @`ls -la` (you can change the syntax, but this also depends on that syntax change so it's centralized and easy to change later). This basically will allow you to make some big refactorings and cut a lot of code (hopefully) and also allow some funny things like @`Look .` (basically allowing nested things like this for no good reason other than, it's elegant and it's fun!) + We'll make the focus history a meaningful and strong semantic entity: instead of being a stack of Panes, it'll be a stack of locations. This will allow us to implement vim's ctrl-i and ctrl-o that will just move the current focus up or down on that stack. there'll be also a new builtin that will toggle the focus of the latest panes, basically moving the current focus by changing the current pointer to the location on the stack. The Jumplist (new builtin) will be an Output pane with each line being a location that can be Looked, n/N should work. 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"); |
