Skip to content

Skip synchronized-update markers inside tmux (fixes stray glyphs on scroll)#2400

Open
Agrejus wants to merge 1 commit into
sinelaw:masterfrom
Agrejus:agrejus/tmux-synchronized-update
Open

Skip synchronized-update markers inside tmux (fixes stray glyphs on scroll)#2400
Agrejus wants to merge 1 commit into
sinelaw:masterfrom
Agrejus:agrejus/tmux-synchronized-update

Conversation

@Agrejus

@Agrejus Agrejus commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

The bug

When scrolling inside tmux, stray box-drawing glyphs (e.g. a leftover ) get left behind on otherwise-blank lines and stay there until a full redraw (Ctrl-L).

Root cause

Every frame is wrapped in DEC private mode 2026 synchronized-update markers (Begin/EndSynchronizedUpdate) in the render loop, sent straight to stdout:

stdout().execute(crossterm::terminal::BeginSynchronizedUpdate)?;
terminal.draw(|frame| editor.render(frame))?;
stdout().execute(crossterm::terminal::EndSynchronizedUpdate)?;

Inside tmux those markers pass through to the outer terminal unwrapped, and tmux's handling can drop individual cell-clear updates from a frame diff. The composed frame buffer is correct, but the cell that should have been cleared never reaches the screen — so the old glyph persists until the whole screen is repainted.

How I narrowed it down

  • Reproduced live in tmux; characterized it: stray cell sits at a decoration's column, on blank lines, and clears on Ctrl-L.
  • Proved the composed buffer is always correct — a headless ratatui-backend test (which reflects real terminal state via diffs, vt100-cross-checked) stayed clean across 60 scroll steps. So it isn't a logic/layout bug.
  • Disabled the synchronized-update markers, rebuilt, scrolled again → strays gone. That isolates the markers as the cause.

The fix

Gate the markers on not running inside tmux (TMUX env unset):

let synchronized_update = std::env::var_os("TMUX").is_none();
// ...
if synchronized_update { stdout().execute(BeginSynchronizedUpdate)?; }
terminal.draw(...)?;
if synchronized_update { stdout().execute(EndSynchronizedUpdate)?; }

tmux already batches its own pane refreshes per display cycle, so synchronized update buys nothing there. Outside tmux, the tear-free behavior is unchanged.

Risk / testing

Low — one boolean gate around two escape writes; the actual frame draw is untouched, and non-tmux behavior is identical.

No automated test: this is terminal-output (escape-sequence) behavior under tmux specifically. The headless backend used in tests composes a fresh, correct buffer every frame and so cannot reproduce a tmux passthrough drop (that's exactly why the bug was invisible to CI). Verified manually: built a release binary, ran inside tmux, scrolled a decoration-heavy buffer, and confirmed zero stray cells across many scroll passes (vs. reliably reproducible before).


Split out of #2325 (closed); this is one of the non-graphics pieces broken out for easier review. Independent of the other splits — branches from master and can merge in any order.

Each frame is wrapped in DEC private mode 2026 (Begin/EndSynchronizedUpdate)
to avoid tearing. Inside tmux those markers are passed through to the outer
terminal unwrapped, and tmux's handling can drop individual cell-clear updates
from a frame diff — leaving stale cells (a leftover box-drawing border glyph,
etc.) on otherwise-blank lines until a full redraw (Ctrl-L).

Gate the markers on not running inside tmux (TMUX unset). tmux already batches
its own pane refreshes, so synchronized update buys nothing there; outside
tmux the tear-free behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sinelaw

sinelaw commented Jun 19, 2026

Copy link
Copy Markdown
Owner

I use tmux all the time and haven't seen an issue.
Can you explain step by step how to reproduce this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants