Skip to content

Stale cell data visible on new rows after scrolling with default cursor style #138

Closed

Description

@darrinm

Description

When repeatedly writing escape-heavy output (SGR sequences, 256-color, truecolor) to the terminal followed by scrolling, new rows can display stale cell data from previously used page memory. This manifests as content from old rows appearing horizontally merged into current rows.

Root cause

In Screen.zig, cursorDownScroll() only clears new row cells when the cursor has a non-default background color (bg_color != .none). When programs reset attributes with ESC[0m (which sets bg_color = .none), pages.grow() extends existing pages by appending rows whose cell memory still contains data from previously erased rows. These stale cells are never cleared and become visible on lines that aren't fully overwritten (e.g., empty lines from bare \r\n sequences with default cursor style).

Symptoms

  • Content from previous rows appears merged horizontally into current rows
  • Bug is transient — self-corrects on the next write to that row
  • Bug is periodic — frequency depends on column width (e.g., ~every 11 writes at cols=160)
  • Affects all column widths at different frequencies

Reproduction

  1. Create a terminal (any reasonable size, e.g. 160x39)
  2. Repeatedly write escape-heavy output containing SGR color sequences followed by ESC[0m resets
  3. After enough repetitions to trigger pages.grow(), inspect viewport rows
  4. Some rows will contain horizontally concatenated content from two different terminal lines

Expected behavior

New rows created during scrolling should always have clean/empty cells regardless of cursor style.

Fix

Draft PR with fix and regression tests: #134

Activity

  1. diegosouzapw commented on May 24, 2026

    @diegosouzapw

    Hi @darrinm! 👋

    Just wanted to let you know: this issue is fixed in diegosouzapw/ghostty-web v0.4.0 (see commit).
    cursorDownScroll in Screen.zig now unconditionally clears new rows, eliminating stale cell data visible after scroll with default cursor style.

    If you're looking for a fork with this and many other upstream fixes, feel free to give it a try!

    I'm also building OmniRoute, an open project that provides free LLM model access — I plan to use ghostty-web there, so the more reliable it gets, the better. 🙏 Contributions welcome!

  2. self-assigned this
    on Jun 26, 2026
  3. ThomasK33 commented on Jun 26, 2026

    @ThomasK33
    Member

    Note

    This triage report is AI-generated using Mux

    1. Classification — bug. No status:could-not-reproduce outcome: the behavior reproduced locally.

    2. Summary — Repeated escape-heavy writes followed by scrolling can expose stale cell contents in newly grown rows when the cursor style is back at the default background after ESC[0m. The visible symptom is row text that intermittently contains horizontally merged/stale content from previous rows. This matches the issue description and is reproducible against the current checkout.

    3. Evidence / reproduction — I reproduced this with a local WASM build from the current repository state.

      Environment / setup used:

      The test constructs a 160x39 terminal, repeatedly writes a large escape-heavy payload containing SGR resets, and compares the viewport text after each write to the first stable baseline. Result:

      Targeted reproduction output
      Corrupt at reps: [2, 7, 12, 14, 19, 24, 27, 29]
      error: expect(received).toBe(expected)
      
      Expected: 0
      Received: 8
      (fail) Viewport row-merge bug > viewport text is stable after repeated writes
      
      scrollback=10000: corrupt at [2, 7, 12, 14]
      scrollback=1000000: corrupt at [2, 7, 12, 14]
      scrollback=50000000: corrupt at [2, 7, 12, 14]
      
      Original: 101479 bytes, patched: 177107 bytes
      With workaround: corrupt at [] (0/30)
      
      5 pass
      2 fail
      Ran 7 tests across 1 files.
      

      The passing checks are also useful: the corruption was non-consecutive/transient, independent of the tested scrollback limits, and getViewport() and getLine() agreed at the corrupt state. That narrows the failure to the terminal/WASM state rather than the TypeScript viewport extraction layer. Replacing ESC[0m with an explicit reset plus background color (ESC[0;48;2;10;10;10m) avoided the corruption in the same test loop, which supports the default-background branch as the trigger.

    4. Relevant code / likely cause — The current ghostty/src/terminal/Screen.zig implementation only clears the newly grown row when the cursor background is non-default:

      // Clear the new row so it gets our bg color. We only do this
      // if we have a bg color at all.
      if (self.cursor.style.bg_color != .none) {
          const page: *Page = &page_pin.node.data;
          self.clearCells(
              page,
              self.cursor.page_row,
              page.getCells(self.cursor.page_row),
          );
      }

      After ESC[0m, self.cursor.style.bg_color is .none, so pages.grow() can leave the new row's cell memory uncleared. If the following output does not fully overwrite that row, stale cells can remain visible. The workaround result above is consistent with this branch: keeping an explicit background color causes row clearing and prevents the corruption.

    5. Recommendation — Treat this as a confirmed bug and proceed with an implementation fix. The smallest code-level direction is to clear newly grown rows unconditionally in cursorDownScroll() while preserving the later background-fill behavior for styled rows. PR fix: always clear new row cells during scroll to prevent stale data #134 already contains this shape of fix plus regression coverage and should be reviewed or used as the implementation starting point. The regression test should stay focused on stable viewport text after repeated escape-heavy writes and on the default-background ESC[0m case.

    6. Adjacent issues — I searched for duplicate issue/PR text around stale cells, row merge, scrolling, and cursorDownScroll. No duplicate issue was found. The related open implementation PR is fix: always clear new row cells during scroll to prevent stale data #134. No adjacent issue was created.


    Generated with mux • Model: openai:gpt-5.5 • Thinking: xhigh

  4. added
    bugConfirmed or likely defect
    triage:doneMux triage report has been posted
    acceptedHuman-approved for implementation
    and removed
    needs-triageIssue should be considered by the Mux triage loop
    triage:in-progressMux triage workspace has been launched
    on Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

acceptedHuman-approved for implementationbugConfirmed or likely defecttriage:doneMux triage report has been posted

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions