This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch add-term-sync
in repository terminology.

View the commit online.

commit bd2f56584936d477e26a329a83ecabe794f17f58
Author: [email protected] <[email protected]>
AuthorDate: Mon Apr 27 11:11:31 2026 -0600

    feat: add Synchronized Output (DECSET/DECRST 2026) with atomic frame snapshot
    
    Modern TUI applications (Claude Code, neovim, htop, lazygit, etc.) use the
    Synchronized Output protocol to frame each logical update in ESC[?2026h…
    ESC[?2026l brackets. Terminals that support it (kitty, foot, alacritty,
    wezterm, ghostty, contour, iTerm2, Windows Terminal) buffer between Begin
    and End markers and flip the frame atomically. Without support, terminals
    render every intermediate state—partial cursor moves, erasures, writes—
    leaving visible glyph fragments scattered during live TUI sessions.
    
    Protocol surface (per the Synchronized Output proposal):
    - ESC[?2026h (BSU): begin sync window; snapshot visible cells + cursor
    - ESC[?2026l (ESU): end sync window; drop snapshot, schedule render tick
    - ESC[?2026$p (DECRQM): query sync state
    - 150 ms watchdog: force end-of-sync if app forgets ESU, with one warning
    - Soft/hard reset clears stuck sync state (covers RIS via delegation)
    - Alt-buffer switch (DECSET 1047/1049) forces ESU across the buffer flip
    
    Atomic frame semantics require a consistent view of the entire screen
    across a single render cycle. We achieve this with an eager full-screen
    snapshot at BSU. The const cellrow accessor returns snapshot rows while
    sync is active (sy.sync_output.active && shadow_rows != NULL), allowing
    the renderer to read a frozen view. Meanwhile, live cells continue to
    mutate freely under the parser (which calls cellrow_get_writeable); the
    snapshot is dropped at ESU, watchdog timeout, resize, soft-reset (DECSTR),
    RIS, or alt-buffer-switch, and a normal render is scheduled.
    
    Why eager rather than copy-on-write: the parser writes go through the
    TERMPTY_SCREEN(ty,x,y) macro and bypass the cellrow accessor entirely,
    so a COW hook on the accessor would never trigger. Eager snapshot at BSU
    is ~h*w*sizeof(Termcell) bytes (~15 KB at 80×24) — acceptable.
    
    The cursor position is also snapshotted (shadow_cur_x/y/hidden) and
    read by the rendering path during sync, so the preedit overlay and
    cursor are anchored to the snapshot state, not the live cursor that
    may have advanced past.
    
    Out of scope: backlog rows pushed by scroll-during-sync still reflect
    live state (rare in practice; apps that bracket panels with 2026 don't
    typically scroll inside the bracket).
    
    Adds 5 unit tests (tytest_sync_frame_coherence, tytest_sync_watchdog_teardown,
    tytest_sync_nested, tytest_sync_resize, tytest_sync_soft_reset) covering
    snapshot lifecycle: coherence, watchdog teardown, nested windows, resize
    invalidation, and soft reset (DECSTR) invalidation.
    
    Zero behavior change for apps that don't send mode 2026.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---
 src/bin/termio.c     |  38 +++++--
 src/bin/termpty.c    | 284 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/bin/termpty.h    |  12 +++
 src/bin/termptyesc.c | 117 +++++++++++++++++++++
 src/bin/termptyops.c |   1 +
 src/bin/tytest.c     |   5 +
 src/bin/unit_tests.h |   5 +
 7 files changed, 456 insertions(+), 6 deletions(-)

diff --git a/src/bin/termio.c b/src/bin/termio.c
index 2598a249..53fe01f5 100644
--- a/src/bin/termio.c
+++ b/src/bin/termio.c
@@ -3178,6 +3178,21 @@ _smart_apply(Evas_Object *obj)
 
    evas_object_geometry_get(obj, &ox, &oy, &ow, &oh);
 
+   /* Set cursor base position before rendering so that the preedit overlay
+    * in termio_internal_render uses the right (snapshot vs live) coordinates
+    * as its starting point.  During sync the preedit must be anchored to the
+    * snapshot cursor, not the live cursor that may have advanced past it. */
+   if (sd->pty->sync_output.active)
+     {
+        sd->cursor.x = sd->pty->sync_output.shadow_cur_x;
+        sd->cursor.y = sd->pty->sync_output.shadow_cur_y;
+     }
+   else
+     {
+        sd->cursor.x = sd->pty->cursor_state.cx;
+        sd->cursor.y = sd->pty->cursor_state.cy;
+     }
+
    termio_internal_render(sd,
                           ox, oy,
                           &preedit_x, &preedit_y);
@@ -3192,12 +3207,23 @@ _smart_apply(Evas_Object *obj)
                (sd->pty->block.active, l);
           }
      }
-   if ((sd->scroll != 0) || (sd->pty->termstate.hide_cursor))
-     evas_object_hide(sd->cursor.obj);
+   if (sd->pty->sync_output.active)
+     {
+        /* During sync: present the pre-BSU cursor position. */
+        if ((sd->scroll != 0) || (sd->pty->sync_output.shadow_cur_hidden))
+          evas_object_hide(sd->cursor.obj);
+        else
+          evas_object_show(sd->cursor.obj);
+        /* sd->cursor.x/y already set to snapshot values above. */
+     }
    else
-     evas_object_show(sd->cursor.obj);
-   sd->cursor.x = sd->pty->cursor_state.cx;
-   sd->cursor.y = sd->pty->cursor_state.cy;
+     {
+        if ((sd->scroll != 0) || (sd->pty->termstate.hide_cursor))
+          evas_object_hide(sd->cursor.obj);
+        else
+          evas_object_show(sd->cursor.obj);
+        /* sd->cursor.x/y already set to live values above. */
+     }
    evas_object_move(sd->cursor.obj,
                     ox + ((sd->cursor.x + preedit_x) * sd->font.chw),
                     oy + ((sd->cursor.y + preedit_y) * sd->font.chh));
@@ -3361,7 +3387,7 @@ void
 termio_smart_update_queue(Termio *sd)
 {
    if (sd->anim)
-       return;
+     return;
    sd->anim = ecore_animator_add(_smart_cb_change, sd->self);
 }
 
diff --git a/src/bin/termpty.c b/src/bin/termpty.c
index 8b515b9e..d4ab1e38 100644
--- a/src/bin/termpty.c
+++ b/src/bin/termpty.c
@@ -829,6 +829,24 @@ err:
    return NULL;
 }
 
+void
+termpty_sync_output_snapshot_free(Termpty *ty)
+{
+   int y;
+
+   if (!ty->sync_output.shadow_rows)
+     return;
+   for (y = 0; y < ty->sync_output.shadow_h; y++)
+     {
+        free(ty->sync_output.shadow_rows[y]);
+        ty->sync_output.shadow_rows[y] = NULL;
+     }
+   free(ty->sync_output.shadow_rows);
+   ty->sync_output.shadow_rows = NULL;
+   ty->sync_output.shadow_h = 0;
+   ty->sync_output.shadow_w = 0;
+}
+
 void
 termpty_free(Termpty *ty)
 {
@@ -880,6 +898,14 @@ termpty_free(Termpty *ty)
              ty->pid = -1;
           }
      }
+   /* Reset active first so accessor fall-through during teardown is safe. */
+   ty->sync_output.active = EINA_FALSE;
+   if (ty->sync_output.watchdog)
+     {
+        ecore_timer_del(ty->sync_output.watchdog);
+        ty->sync_output.watchdog = NULL;
+     }
+   termpty_sync_output_snapshot_free(ty);
    if (ty->hand_exe_exit)
      ecore_event_handler_del(ty->hand_exe_exit);
    if (ty->hand_fd)
@@ -909,6 +935,20 @@ termpty_free(Termpty *ty)
    free(ty);
 }
 
+void
+termpty_sync_output_reset(Termpty *ty)
+{
+   /* active must go FALSE before freeing the snapshot so that any accessor
+    * call during teardown falls through to the live path. */
+   ty->sync_output.active = EINA_FALSE;
+   if (ty->sync_output.watchdog)
+     {
+        ecore_timer_del(ty->sync_output.watchdog);
+        ty->sync_output.watchdog = NULL;
+     }
+   termpty_sync_output_snapshot_free(ty);
+}
+
 void
 termpty_config_update(Termpty *ty, Config *config)
 {
@@ -1185,6 +1225,16 @@ _termpty_cellrow_live_get(Termpty *ty, int y_requested, ssize_t *wret)
 const Termcell *
 termpty_cellrow_get(Termpty *ty, int y_requested, ssize_t *wret)
 {
+   if (ty->sync_output.active &&
+       ty->sync_output.shadow_rows &&
+       y_requested >= 0 &&
+       y_requested < ty->sync_output.shadow_h)
+     {
+        if (wret)
+          *wret = termpty_line_length(ty->sync_output.shadow_rows[y_requested],
+                                     ty->sync_output.shadow_w);
+        return ty->sync_output.shadow_rows[y_requested];
+     }
    return _termpty_cellrow_live_get(ty, y_requested, wret);
 }
 
@@ -1373,6 +1423,10 @@ termpty_resize(Termpty *ty, int new_w, int new_h)
    struct screen_info new_si = {.screen = NULL};
    if ((ty->w == new_w) && (ty->h == new_h)) return;
 
+   /* A resize invalidates any in-flight sync window; the app will re-bracket. */
+   if (ty->sync_output.active)
+     termpty_sync_output_reset(ty);
+
    termpty_backlog_lock();
 
    if (ty->altbuf)
@@ -1817,3 +1871,233 @@ term_link_free(Termpty *ty, Term_Link *link)
    /* Remove from bitmap */
    hl_bitmap_clear_bit(ty, id);
 }
+
+#if defined(BINARY_TYFUZZ) || defined(BINARY_TYTEST)
+
+/* Feed a NUL-terminated ASCII string as if the PTY sent it. */
+static void
+_ty_feed(Termpty *ty, const char *str)
+{
+   Eina_Unicode buf[256];
+   int i, j = 0;
+
+   for (i = 0; str[i] && j < 255; i++, j++)
+     buf[j] = (unsigned char)str[i];
+   buf[j] = 0;
+   termpty_handle_buf(ty, buf, j);
+}
+
+/* Minimal Termpty allocator for unit tests — no fd, no EFL object.
+ * Requires eina+ecore init so that ecore_timer_add (watchdog) works. */
+static void
+_ty_test_init(Termpty *ty, int w, int h)
+{
+   eina_init();
+   ecore_init();
+
+   memset(ty, 0, sizeof(*ty));
+   ty->w = w;
+   ty->h = h;
+   ty->fd = -1;
+   ty->slavefd = -1;
+   ty->pid = -1;
+   ty->backsize = 50;
+   termpty_resize_tabs(ty, 0, w);
+   termpty_reset_state(ty);
+   ty->screen  = calloc(1, sizeof(Termcell) * w * h);
+   ty->screen2 = calloc(1, sizeof(Termcell) * w * h);
+   ty->hl.bitmap = calloc(1, HL_LINKS_MAX / 8);
+   ty->hl.bitmap[0] = 1; /* id 0 reserved */
+   assert(ty->screen && ty->screen2 && ty->hl.bitmap);
+}
+
+static void
+_ty_test_shutdown(Termpty *ty)
+{
+   termpty_sync_output_reset(ty);
+   termpty_backlog_free(ty);
+   free(ty->screen);
+   free(ty->screen2);
+   free(ty->tabs);
+   free(ty->hl.bitmap);
+   free(ty->buf);
+
+   ecore_shutdown();
+   eina_shutdown();
+}
+
+/* Helper: read codepoint of cell (x,y) via the read accessor. */
+static Eina_Unicode
+_ty_cell_cp(Termpty *ty, int x, int y)
+{
+   ssize_t w = 0;
+   const Termcell *cells = termpty_cellrow_get(ty, y, &w);
+   if (!cells || x >= w) return 0;
+   return cells[x].codepoint;
+}
+
+/* Test 1: Frame coherence.
+ * BSU; write "foo"; ESU.  The read accessor must return the pre-BSU (blank)
+ * row until ESU is received, and the live "foo" after. */
+int
+tytest_sync_frame_coherence(void)
+{
+   Termpty ty;
+   const Termcell *row;
+   ssize_t w = 0;
+
+   _ty_test_init(&ty, 80, 24);
+
+   /* Pre-BSU: row 0 is blank. */
+   row = termpty_cellrow_get(&ty, 0, &w);
+   assert(row);
+   assert(row[0].codepoint == 0);
+
+   /* Begin Synchronized Update. */
+   _ty_feed(&ty, "\x1b[?2026h");
+   assert(ty.sync_output.active == EINA_TRUE);
+   assert(ty.sync_output.shadow_rows != NULL);
+
+   /* Write "foo" to row 0.  Writes to live row; the eager snapshot taken at BSU is unaffected. */
+   _ty_feed(&ty, "\r\x1b[2Kfoo");
+
+   /* Renderer sees old blank row (snapshot). */
+   row = termpty_cellrow_get(&ty, 0, &w);
+   assert(row);
+   assert(row[0].codepoint == 0); /* snapshot: still blank */
+
+   /* Writeable accessor sees live row with 'f'. */
+   {
+      Termcell *live = termpty_cellrow_get_writeable(&ty, 0, &w);
+      assert(live);
+      assert(live[0].codepoint == 'f');
+   }
+
+   /* End Synchronized Update. */
+   _ty_feed(&ty, "\x1b[?2026l");
+   assert(ty.sync_output.active == EINA_FALSE);
+   assert(ty.sync_output.shadow_rows == NULL);
+
+   /* After ESU the read accessor returns the live row. */
+   assert(_ty_cell_cp(&ty, 0, 0) == 'f');
+   assert(_ty_cell_cp(&ty, 1, 0) == 'o');
+   assert(_ty_cell_cp(&ty, 2, 0) == 'o');
+
+   _ty_test_shutdown(&ty);
+   return 0;
+}
+
+/* Test 2: Watchdog teardown.
+ * BSU + partial write, then force watchdog expiry by calling reset directly.
+ * Snapshot freed, no leaks, subsequent read returns live (partial) state. */
+int
+tytest_sync_watchdog_teardown(void)
+{
+   Termpty ty;
+
+   _ty_test_init(&ty, 80, 24);
+
+   _ty_feed(&ty, "\x1b[?2026h");
+   assert(ty.sync_output.active == EINA_TRUE);
+   _ty_feed(&ty, "\r\x1b[2Kpartial");
+
+   /* Simulate watchdog by calling reset (same effect as timer firing). */
+   termpty_sync_output_reset(&ty);
+   assert(ty.sync_output.active == EINA_FALSE);
+   assert(ty.sync_output.shadow_rows == NULL);
+   assert(ty.sync_output.watchdog == NULL);
+
+   /* Live state has the partial write. */
+   assert(_ty_cell_cp(&ty, 0, 0) == 'p');
+
+   _ty_test_shutdown(&ty);
+   return 0;
+}
+
+/* Test 3: Nested ESU/BSU.
+ * BSU "A" ESU "B" BSU "C" ESU.  After first ESU the renderer sees "A".
+ * After second ESU it sees "A...B...C" (all live). */
+int
+tytest_sync_nested(void)
+{
+   Termpty ty;
+
+   _ty_test_init(&ty, 80, 24);
+
+   /* First sync window: write 'A' to col 0. */
+   _ty_feed(&ty, "\x1b[?2026h");
+   _ty_feed(&ty, "\r\x1b[2KA");
+
+   /* Renderer still sees blank (snapshot). */
+   assert(_ty_cell_cp(&ty, 0, 0) == 0);
+
+   _ty_feed(&ty, "\x1b[?2026l"); /* ESU */
+   assert(ty.sync_output.active == EINA_FALSE);
+   /* Now renderer sees live: "A". */
+   assert(_ty_cell_cp(&ty, 0, 0) == 'A');
+
+   /* Write "B" outside sync (visible immediately). */
+   _ty_feed(&ty, "\x1b[2GB"); /* cursor to col 2, write 'B' */
+
+   /* Second sync window: write 'C' at col 4. */
+   _ty_feed(&ty, "\x1b[?2026h");
+   _ty_feed(&ty, "\x1b[4GC");
+
+   /* Renderer still sees pre-second-BSU state: 'A'@0, 'B'@1, nothing at 3. */
+   assert(_ty_cell_cp(&ty, 0, 0) == 'A');
+   assert(_ty_cell_cp(&ty, 3, 0) == 0);
+
+   _ty_feed(&ty, "\x1b[?2026l"); /* second ESU */
+   assert(ty.sync_output.active == EINA_FALSE);
+   assert(_ty_cell_cp(&ty, 1, 0) == 'B'); /* inter-window write survives second snapshot/restore */
+   assert(_ty_cell_cp(&ty, 3, 0) == 'C');
+
+   _ty_test_shutdown(&ty);
+   return 0;
+}
+
+/* Test 4: Resize during sync.
+ * BSU + writes + termpty_resize.  Snapshot freed, sync inactive. */
+int
+tytest_sync_resize(void)
+{
+   Termpty ty;
+
+   _ty_test_init(&ty, 80, 24);
+
+   _ty_feed(&ty, "\x1b[?2026h");
+   assert(ty.sync_output.active == EINA_TRUE);
+   _ty_feed(&ty, "\r\x1b[2Khello");
+
+   /* Resize invalidates the sync window. */
+   termpty_resize(&ty, 40, 24);
+   assert(ty.sync_output.active == EINA_FALSE);
+   assert(ty.sync_output.shadow_rows == NULL);
+
+   _ty_test_shutdown(&ty);
+   return 0;
+}
+
+/* Test 5: Soft reset during sync.
+ * BSU + writes + DECSTR (\e[!p). Snapshot freed. */
+int
+tytest_sync_soft_reset(void)
+{
+   Termpty ty;
+
+   _ty_test_init(&ty, 80, 24);
+
+   _ty_feed(&ty, "\x1b[?2026h");
+   assert(ty.sync_output.active == EINA_TRUE);
+   _ty_feed(&ty, "\r\x1b[2Kworld");
+
+   /* DECSTR — soft reset. */
+   _ty_feed(&ty, "\x1b[!p");
+   assert(ty.sync_output.active == EINA_FALSE);
+   assert(ty.sync_output.shadow_rows == NULL);
+
+   _ty_test_shutdown(&ty);
+   return 0;
+}
+
+#endif /* BINARY_TYFUZZ || BINARY_TYTEST */
diff --git a/src/bin/termpty.h b/src/bin/termpty.h
index 34389b86..c24a25b7 100644
--- a/src/bin/termpty.h
+++ b/src/bin/termpty.h
@@ -155,6 +155,15 @@ struct tag_Termpty
          void *data;
       } change, set_title, set_icon, cancel_sel, exited, bell, command;
    } cb;
+   struct {
+      Ecore_Timer *watchdog;
+      Termcell   **shadow_rows;     /* size shadow_h, each entry NULL or malloc'd row copy */
+      int          shadow_h;
+      int          shadow_w;
+      int          shadow_cur_x, shadow_cur_y;
+      Eina_Bool    shadow_cur_hidden : 1;
+      Eina_Bool    active : 1;
+   } sync_output;
    struct {
       const char *icon;
       /* dynamic title set by xterm, keep it in case user don't want a
@@ -291,6 +300,9 @@ Termpty   *termpty_new(const char *cmd, Eina_Bool login_shell, const char *cd,
                        int w, int h, Config *config, const char *title,
                        Ecore_Window window_id);
 void       termpty_free(Termpty *ty);
+void       termpty_sync_output_reset(Termpty *ty);
+/* private — called from termptyesc.c _sync_output_end only */
+void       termpty_sync_output_snapshot_free(Termpty *ty);
 void       termpty_config_update(Termpty *ty, Config *config);
 
 const Termcell *termpty_cellrow_get(Termpty *ty, int y, ssize_t *wret);
diff --git a/src/bin/termptyesc.c b/src/bin/termptyesc.c
index e80c7bd5..2eb26efb 100644
--- a/src/bin/termptyesc.c
+++ b/src/bin/termptyesc.c
@@ -253,9 +253,15 @@ _handle_cursor_control(Termpty *ty, const Eina_Unicode *cc)
      }
 }
 
+/* Forward declaration: defined in the Synchronized Output section below. */
+static void _sync_output_end(Termpty *ty);
+
 static void
 _switch_to_alternative_screen(Termpty *ty, int mode)
 {
+   /* An alt-buffer switch invalidates the sync window; the app will re-bracket. */
+   if (ty->sync_output.active)
+     _sync_output_end(ty);
    // swap screen content now
    if (mode != ty->altbuf)
      termpty_screen_swap(ty);
@@ -279,6 +285,86 @@ _move_cursor_to_origin(Termpty *ty)
 }
 
 
+/* --- Synchronized Output (DECSET/DECRST 2026) helpers --- */
+
+static void
+_sync_output_end(Termpty *ty)
+{
+   if (!ty->sync_output.active) return;
+
+   /* Drop active first: accessor calls during teardown fall through to live. */
+   ty->sync_output.active = EINA_FALSE;
+   if (ty->sync_output.watchdog)
+     {
+        ecore_timer_del(ty->sync_output.watchdog);
+        ty->sync_output.watchdog = NULL;
+     }
+   termpty_sync_output_snapshot_free(ty);
+   /* Nudge the change pipeline so the renderer paints the now-live cells. */
+   if (ty->cb.change.func)
+     ty->cb.change.func(ty->cb.change.data);
+}
+
+static Eina_Bool
+_sync_output_watchdog_cb(void *data)
+{
+   Termpty *ty = data;
+
+   WRN("Synchronized Output watchdog fired — ESU missing after 150 ms, "
+       "forcing render (mode 2026)");
+   ty->sync_output.watchdog = NULL; /* timer owns itself; NULL before _end */
+   _sync_output_end(ty);
+   return ECORE_CALLBACK_CANCEL;
+}
+
+static void
+_sync_output_begin(Termpty *ty)
+{
+   if (ty->sync_output.active)
+     {
+        /* Nested BSU: ignore but reset the watchdog deadline. */
+        if (ty->sync_output.watchdog)
+          ecore_timer_reset(ty->sync_output.watchdog);
+        return;
+     }
+
+   {
+      int y;
+      Termcell **rows = calloc(ty->h, sizeof(Termcell *));
+      if (!rows)
+        {
+           /* Allocation failure: skip sync, render live. */
+           return;
+        }
+      for (y = 0; y < ty->h; y++)
+        {
+           Termcell *snap = malloc(ty->w * sizeof(Termcell));
+           if (!snap)
+             {
+                /* Partial snapshot: free what we have and bail out. */
+                int k;
+                for (k = 0; k < y; k++) free(rows[k]);
+                free(rows);
+                return;
+             }
+           /* Snapshot row y from the live circular screen buffer. */
+           memcpy(snap, &(TERMPTY_SCREEN(ty, 0, y)), ty->w * sizeof(Termcell));
+           rows[y] = snap;
+        }
+      ty->sync_output.shadow_rows = rows;
+   }
+   ty->sync_output.shadow_h = ty->h;
+   ty->sync_output.shadow_w = ty->w;
+   ty->sync_output.shadow_cur_x = ty->cursor_state.cx;
+   ty->sync_output.shadow_cur_y = ty->cursor_state.cy;
+   ty->sync_output.shadow_cur_hidden = ty->termstate.hide_cursor;
+   ty->sync_output.active = EINA_TRUE;
+   ty->sync_output.watchdog =
+      ecore_timer_add(0.150, _sync_output_watchdog_cb, ty);
+}
+
+/* --------------------------------------------------------- */
+
 static void
 _handle_esc_csi_reset_mode(Termpty *ty, Eina_Unicode cc, Eina_Unicode *b,
                            const Eina_Unicode * const end)
@@ -540,6 +626,12 @@ _handle_esc_csi_reset_mode(Termpty *ty, Eina_Unicode cc, Eina_Unicode *b,
                 case 2004:
                    ty->bracketed_paste = mode;
                    break;
+                case 2026: /* Synchronized Output (BSU/ESU) */
+                   if (mode)
+                     _sync_output_begin(ty);
+                   else
+                     _sync_output_end(ty);
+                   break;
                 case 7700: // ignore
                    WRN("TODO: ambiguous width reporting %i", mode);
                    ty->decoding_error = EINA_TRUE;
@@ -3570,6 +3662,31 @@ _handle_esc_csi(Termpty *ty, const Eina_Unicode *c, const Eina_Unicode *ce)
              DBG("soft reset (DECSTR)");
              termpty_soft_reset_state(ty);
           }
+        else if (*(cc-1) == '$' && b && *b == '?')
+          {
+             /* DECRQM — DEC Request Mode (private).
+              * Parse ?<mode>$p and reply \e[?<mode>;<status>$y
+              * Status: 1 = set, 2 = reset, 0 = not recognized.
+              */
+             char bf[32];
+             int len, qarg;
+             b++; /* skip '?' */
+             qarg = _csi_arg_get(ty, &b);
+             if (qarg < 0) break; /* malformed: no response */
+             if (qarg == 2026)
+               {
+                  /* Known mode: report current state */
+                  len = snprintf(bf, sizeof(bf), "\033[?2026;%d$y",
+                                 ty->sync_output.active ? 1 : 2);
+                  termpty_write(ty, bf, len);
+               }
+             else
+               {
+                  /* Unknown/unrecognized mode: report 0 */
+                  len = snprintf(bf, sizeof(bf), "\033[?%d;0$y", qarg);
+                  termpty_write(ty, bf, len);
+               }
+          }
         else
           {
              goto unhandled;
diff --git a/src/bin/termptyops.c b/src/bin/termptyops.c
index de7fa64a..2e0fec93 100644
--- a/src/bin/termptyops.c
+++ b/src/bin/termptyops.c
@@ -456,6 +456,7 @@ termpty_soft_reset_state(Termpty *ty)
    ty->mouse_mode = MOUSE_OFF;
    ty->mouse_ext = MOUSE_EXT_NONE;
    ty->bracketed_paste = 0;
+   termpty_sync_output_reset(ty);
 
    termio_reset_main_colors(ty->obj);
    termpty_clear_tabs_on_screen(ty);
diff --git a/src/bin/tytest.c b/src/bin/tytest.c
index 02903830..fb4c5d29 100644
--- a/src/bin/tytest.c
+++ b/src/bin/tytest.c
@@ -42,6 +42,11 @@ static struct {
        { "color_parse_css_hsl", tytest_color_parse_css_hsl},
        { "extn_matching", tytest_extn_matching},
        { "base64", tytest_base64},
+       { "sync_frame_coherence", tytest_sync_frame_coherence},
+       { "sync_watchdog_teardown", tytest_sync_watchdog_teardown},
+       { "sync_nested", tytest_sync_nested},
+       { "sync_resize", tytest_sync_resize},
+       { "sync_soft_reset", tytest_sync_soft_reset},
        { NULL, NULL},
 };
 
diff --git a/src/bin/unit_tests.h b/src/bin/unit_tests.h
index ed3ddd94..52ad917c 100644
--- a/src/bin/unit_tests.h
+++ b/src/bin/unit_tests.h
@@ -19,5 +19,10 @@ int tytest_color_parse_css_rgb(void);
 int tytest_color_parse_css_hsl(void);
 int tytest_extn_matching(void);
 int tytest_base64(void);
+int tytest_sync_frame_coherence(void);
+int tytest_sync_watchdog_teardown(void);
+int tytest_sync_nested(void);
+int tytest_sync_resize(void);
+int tytest_sync_soft_reset(void);
 
 #endif

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to