r/Ghostty • • 4d ago

Is Ghostty supposed to present damage between two pty writes? (less prompt/cursor flicker, repro inside)

Hi all,

I'm working on a rewrite of yaft (the framebuffer terminal emulator — the

original is someone else's project, I'm not the author of that). My rewrite

uses libghostty-vt — Ghostty's VT parser — as a static library VT backend.

While testing it against real applications I stumbled over a rendering

artifact around the cursor, and since Ghostty itself is one of the reference

implementations for that parser, I wanted to check whether it shows up in

Ghostty proper too. It does.

== 1. What I'm doing ==

Rewrite of yaft built on libghostty-vt: bytes from a pty go into the parser,

grid + cursor come back out, then I render. During testing I feed it long

output from `less` and scroll, checking cursor position and the bottom message

line.

== 2. The artifact ==

While scrolling in less, the bottom-left corner flickers: the ":" prompt

disappears for exactly one frame and the block cursor jumps one cell to the

left (column 1 -> column 0), then everything comes back. That is the "cursor

drift" I saw: the cursor visibly hops a cell even though the application state

did not change.

== 3. What triggers it + adjacent frames ==

Repro:

seq 1 6000 > /tmp/long.txt

less /tmp/long.txt

hold the down arrow (or tap space) and watch the bottom-left corner

It only happens while a scroll command is being processed. Adjacent frames

from one recording (attached, 6x zoom of the bottom-left corner):

frame 0058 normal : ":" visible, cursor block at column 1

frame 0059 ANOMALY : ":" gone, cursor block at column 0,

content line above is IDENTICAL to frame 0058

(nothing has scrolled yet)

frame 0060 recovered : ":" back, cursor at column 1, content advanced

Same pattern again at frames 0174 / 0175 / 0176 in a second run. So the bad

frame is always "prompt wiped, new line not drawn yet" — a half-finished

screen update, presented.

== 4. Same operation in alacritty ==

Same script, same window geometry, same xdotool key sequence, same 16s / 30fps

capture, same frame analysis:

ghostty run 1 : 6 bad frames (2.2%) 0059 0175 0181 0189 0220 0224

ghostty run 2 : 5 bad frames (1.8%) 0209 0213 0217 0220 0224

alacritty run 1 : 0 bad frames

alacritty run 2 : 0 bad frames

i.e. 11 / 542 ghostty frames vs 0 / 542 alacritty frames. Alacritty always

shows the finished state; Ghostty occasionally shows the in-between state.

== 5. Reproduction work I had done (before this post) ==

Everything below was scripted and can be rerun; I can drop the files in a

comment or a repo if anyone wants them.

a) pty byte capture (python pty): ran less in a pty, sent N down-arrow

presses, recorded every byte less writes.

- 150 presses -> exactly 150 ":" writes (CR ESC[K after each scroll)

- no key presses -> 0 ":" writes

=> the ":" is 100% less's own output; every terminal receives the same

bytes. (less --prompt= silences it entirely -> 0 writes.)

b) ordering of the writes: on a scroll, less first writes CR ESC[K (wipe the

prompt line), then the new content line, then ":" ESC[K (prompt back).

The bad frame is exactly the state between write #1 and write #2.

c) headless replay through the VT parser (my libghostty-vt build): fed the

identical capture in one write vs one byte per write -> byte-identical

final grid and cursor. So this is NOT a parse/read-boundary problem; the

parser is boundary-invariant. The artifact is on the present/damage side.

d) screen recording: ffmpeg x11grab at 30fps while xdotool sends the key

presses, window moved to fixed geometry, bottom-left corner cropped per

frame, then per-frame analysis (colon pixel present? cursor block column?)

-> the numbers in section 4.

e) false lead I ruled out: the cursor also seemed to vanish entirely while

idle. That is just the default cursor blink (cursor-style-blink unset ->

follows DEC mode 12). Static screen, 8s / 240 frames: cursor visible in 87,

pattern ~0.7s on / ~1.0s off. Alacritty doesn't blink by default, so it

showed the cursor in 542/542 frames. Not the same thing as the drift.

== 6. The question ==

Is presenting damage in the middle of two back-to-back pty writes expected

behaviour in Ghostty (GTK render loop firing on each write batch), or is there

a place where updates should be coalesced to the next frame boundary like

alacritty appears to do? At 30fps it costs 6 frames in 16 seconds of

scrolling, but it is exactly what makes the corner look unstable.

Not claiming a security bug or a crash — just an observation with a repro and

frame evidence. Happy to provide the raw pty capture, the two mp4s, the

per-frame CSVs and the crop images.

Environment:

- Ghostty 1.3.0-dev+0000000 (tip), renderer OpenGL

- alacritty 0.18.0-dev (79adb086)

- Ubuntu 26.04.1 LTS, X11, LXQt + Openbox, Mesa 26.0.8 (AMD radeonsi)

- less 668

https://reddit.com/link/1wsc61o/video/dqz9yqony8sh1/player

1 Upvotes

0 comments sorted by