Rebuild effects when the terminal is resized - #4
Conversation
4e66c58 to
67c0c0f
Compare
Newly created terminals can briefly expose the pty default of 80x24 before the compositor assigns the real window size. Since effect state is built from the measured canvas, that leaves fullscreen animations painting into one corner. Record SIGWINCH, verify that the dimensions actually changed, and end the current render pass without advancing the cursor. The CLI then rebuilds the same selected effect against the new dimensions, carrying the RNG state forward and reusing the existing output area. Normal runs and the existing library runner remain unchanged.
Six fixes from the review of omacom#4, plus a pty-driven regression test that fails on all of them without these changes. Only react to SIGWINCH when stdout is a terminal. The signal reaches every process in the terminal's foreground group whatever its stdout points at, and terminal_size() falls back to stderr, so `ttfx pour | less` in a window being resized restarted mid-stream: the consumer saw a truncated first run followed by a complete second one. A file sink drains too fast to show it, which is why the test drives a deliberately slow reader. Compare the canvas geometry, not the raw terminal size. With an input-sized canvas and no anchor offsets, most resizes cannot move a single rendered cell, and restarting for those is pure loss. compute_layout is now factored out of Terminal::new so a resize can re-derive the geometry from the stored line lengths and compare. Wait for the size to settle before rebuilding. Dragging a window edge emits a SIGWINCH per step; rebuilding for each one pinned the animation at its opening frames for the whole drag and started it over on release. A 6-step drag now costs at most 3 rebuilds instead of 7. Wipe the old area instead of reusing the canvas. Reusing it moved up by the new visible_top from an anchor that no longer had that much room above it, so the blank-line loop ran past the anchor and scrolled a line of scrollback away per rebuild. The resize path now returns to the top of the area it allocated, erases to the end of the screen, and lays the new canvas out from there. Leave the cursor hidden across a rebuild. Restoring it between runs strobed the cursor 20-40 times a second through a drag. Drop the libc dependency. Only signal(2) and a handful of constants were needed, and signal was already hand-declared; the pacing sleep now slices rather than reaching for nanosleep, which also makes it portable. Rebasing onto master also had to reconcile the run loop with the arena teardown skip that landed in omacom#5: forgetting the engine now happens on the exit paths inside the loop, never on a resize rebuild, which still drops its engine so a long session of resizes cannot accumulate them.
67c0c0f to
6db0138
Compare
|
Thanks for this — the flag protocol is right (the take-then-re-read ordering means no resize can be lost or double-counted), and the restart loop itself is sound. I reviewed it, pushed fixes for six things onto your branch, and rebased it onto current What changedOnly react to SIGWINCH when stdout is a terminal. The signal goes to every process in the terminal's foreground group regardless of where stdout points, and Compare canvas geometry, not raw terminal size. With the default input-sized canvas and Coalesce the winch storm. Dragging a window edge emits a SIGWINCH per step; rebuilding for each one pinned the animation at its opening frames for the whole drag and only started over on release. A 6-step drag went from 7 rebuilds to 3. Wipe the old area instead of Keep the cursor hidden across a rebuild. Dropped the TestAdded One rebase noteThe rebase applied with zero conflicts and produced code that didn't compile: this PR moves engine construction into a restart loop, and the arena-teardown skip from #5 sits after that region and referenced what are now loop-local bindings. Reconciled by forgetting the engine on the exit paths inside the loop — a resize rebuild deliberately still drops its engine, so a long session of resizes can't accumulate them. That fix landed in my commit, so the two earlier commits don't build in isolation on the new base; squash-merging would give a clean bisect. 354 effect-parity checks, 41 tty byte-stream checks, the CLI corpus and — 🤖 Claude, posting on behalf of @dhh |
The restart machinery had grown three layers that each cost more than they returned. Fold the debounce into the run loop. wait_for_resize_to_settle polled from main with a 40ms sleep, and it ran after the screen had already been wiped, so every rebuild showed a blank hole for at least that long. Terminal now restarts a 50ms quiet window on each SIGWINCH and checks it where the frame loop already checks for interrupts: the old canvas keeps animating until the window expires, so the wait costs nothing on screen. A 6-step drag now rebuilds twice instead of three times. Drop sleep_until_signal. Slicing the frame delay into 4ms pieces bought at most one frame of latency — the default frame rate is 60, so the delay it was slicing is 16.7ms — on a path that then deliberately waits 50ms for the size to settle. thread::sleep is back. Collapse run_effect, run_effect_resize_aware and run_effect_inner into one run_effect(effect, ctx, stop_on_resize). The split existed to keep the old signature "unchanged for library callers" that do not exist, and main was branching on resize_aware to choose between two wrappers that branch on the same bool again. dump_effect now shares the single match, which also puts forget_engine on one exit path instead of three. Co-Authored-By: Claude <noreply@anthropic.com>
Makes
ttfxrespond to terminal resizes by rebuilding the current effect at the new canvas size. This removes the 80x24 startup problem currently worked around in Omarchy and also handles later window resizes.<<< AI wording below >>>>
Problem
Newly created terminal windows can briefly expose the pty default of 80x24 before the compositor assigns the real size.
ttfxmeasures once while constructing the engine, so a fullscreen animation that starts in that window keeps an 80x24 canvas and paints into one corner.Omarchy currently works around this by polling
stty sizefor up to two seconds before startingttfx: omacom/omarchy@354c2f0Fix
Record
SIGWINCHand verify that the terminal dimensions differ from those used to build the current canvas. When they do, finish that render pass without advancing the cursor, then rebuild the same selected effect against the new dimensions while carrying the RNG state forward and reusing the existing output area. This handles both the initial compositor resize and later window resizes.The existing
run_effectAPI remains unchanged for library callers, and no effect logic or CLI surface changes.Verification
ESC[24AtoESC[40Ain the same process, then exited cleanly on SIGINT.The full test command on this Linux/aarch64 machine still has the existing exact-easing failure at
OutExpo p=0.03(4 ULP); it is unchanged frommasterand unrelated to this patch.