Skip to main content

max / shop

shop-grid: parse DECSET/DECRST 2026, expose sync_update() flag Some ratatui-based tools frame a full-screen paint in \e[?2026h ... \e[?2026l to say "hold the next frame until I'm done". Grid now tracks that state on the ?h/?l private-mode arm and exposes Grid::sync_update() -> bool for the binary to gate its redraw scheduler. Grid stays pure state; deferring the actual wl_surface.frame callback + the safety timeout is the binary's job (task 076904d already coalesces via frame callbacks, so the hook is just "skip requesting a new one while sync_update is true, plus a wall-clock timeout ~150ms").
Author: Max Johnson <me@maxj.phd> · 2026-07-24 15:23 UTC
Signed with PGP, not checked
Commit: 7cc692ac1a0528f165411d13b8a7daefaf3e685c
Parent: 914e901
1 file changed, +30 insertions, -0 deletions
@@ -255,6 +255,10 @@
255 255 // first use of a print run; any non-print Perform entry point (execute /
256 256 // csi / esc / osc), newline, screen-swap, DECSTBM, or resize invalidates.
257 257 cur_row_start: u32,
258 + // DECSET 2026: true between `\e[?2026h` and `\e[?2026l`. Purely
259 + // reflected via [`Grid::sync_update`] — the binary decides whether/how
260 + // long to defer redraws (typical timeout is ~150ms).
261 + sync_update: bool,
258 262 pending_title: Option<String>,
259 263 // Damage tracking — accumulated between take_damage() calls.
260 264 row_dirty: Vec<bool>,
@@ -294,6 +298,7 @@
294 298 pending_fg_word: 0,
295 299 pending_bg_word: 0,
296 300 cur_row_start: CUR_ROW_INVALID,
301 + sync_update: false,
297 302 pending_title: None,
298 303 // Initial state: everything dirty so first render populates the
299 304 // per-row cache.
@@ -342,6 +347,13 @@
342 347 self.cursor_shape
343 348 }
344 349
350 + /// True while a DECSET 2026 synchronized-update batch is open. The
351 + /// renderer should hold off on presenting a frame until this clears
352 + /// (matching `\e[?2026l`) or its own safety timeout expires.
353 + pub fn sync_update(&self) -> bool {
354 + self.sync_update
355 + }
356 +
345 357 /// Consume any window title set by the shell via OSC 0/2 since the last
346 358 /// call. Binary polls after each `parser.advance` and forwards to
347 359 /// `xdg_window.set_title`.
@@ -1128,6 +1140,12 @@
1128 1140 match code {
1129 1141 25 => self.cursor.visible = action == 'h',
1130 1142 1049 | 47 | 1047 => self.swap_alt(action == 'h'),
1143 + // DECSET/DECRST 2026: synchronized update. `h`
1144 + // begins a batch (renderer should hold frames
1145 + // until `l` or the caller's timeout); `l` ends
1146 + // it. Grid just tracks the state — the binary
1147 + // is what actually defers the redraw.
1148 + 2026 => self.sync_update = action == 'h',
1131 1149 _ => {}
1132 1150 }
1133 1151 }
@@ -1420,6 +1438,18 @@
1420 1438 assert_eq!(g.cursor_shape(), CursorShape::Block);
1421 1439 }
1422 1440
1441 + // ---- synchronized update (DECSET 2026) -----------------------------
1442 +
1443 + #[test]
1444 + fn sync_update_toggles_on_2026() {
1445 + let mut g = Grid::new(10, 1);
1446 + assert!(!g.sync_update());
1447 + feed(&mut g, b"\x1b[?2026h");
1448 + assert!(g.sync_update());
1449 + feed(&mut g, b"\x1b[?2026l");
1450 + assert!(!g.sync_update());
1451 + }
1452 +
1423 1453 // ---- alt screen ----------------------------------------------------
1424 1454
1425 1455 #[test]