Summary
Images drawn with the kitty graphics protocol come out vertically stretched,
roughly two and a half times taller than they should be on my font, whenever
the program drawing them was started as the terminal's own process. A square
image fills a tall rectangle.
The cause is the cell size query. A ghostel terminal answers CSI 16 t with a
1x1 pixel cell until it has rendered once, so a program that asks at startup,
before ghostel has drawn anything into the buffer, reads CSI 6 ; 1 ; 1 t,
concludes that the cells are square, and lays its images out on that basis. The
stretch is the real cell aspect ratio, 23/9 here. CSI 14 t (text area in
pixels) and CSI 18 t (text area in cells) are wrong in the same window and
return the same numbers as each other, because the pixel fields hold the row
and column counts.
I hit this with pi, a terminal coding agent that queries the cell size when its
interface starts and accepts any reply carrying two positive numbers. Anything
started through ghostel-exec as the terminal's own process is exposed the
same way, including timg and yazi previews. A program you launch by typing its
name at a shell prompt is unaffected, because the prompt has already forced a
render.
ghostel--init-buffer already seeds the real cell dimensions before the
process starts, and its comment says why, but the seed only takes effect at the
first redraw, which comes too late for the program the terminal was started to
run. 0.49.0 fixed a 1x1 cell size for ghostel-compile; this is the same
problem on the ordinary spawn path, except that it clears itself once anything
renders rather than waiting for a manual resize.
Steps to reproduce
(let ((buf (generate-new-buffer "*16t*")))
(display-buffer buf)
(ghostel-exec
buf "/bin/bash"
(list "-c"
(concat "q(){ printf '\\e[16t'; IFS= read -rs -t 1 -d t r;"
" printf '%s reply: %st\\n' \"$1\""
" \"$(printf '%s' \"$r\" | cat -v)\"; };"
" q cold; sleep 0.5; q warm; sleep 20"))))
The new terminal prints:
cold reply: ^[[6;1;1t
warm reply: ^[[6;23;9t
Both replies should be ^[[6;23;9t. In that buffer
(ghostel--reported-cell-width) is 9 and (ghostel--reported-cell-height) is
23, from an 8x20 logical pixel font and a (ghostel--cell-pixel-scale) of 1.14.
CSI 14 t behaves the same way. On a terminal of 37 rows by 114 columns it
answers ^[[4;37;114t before the first redraw, which is the grid size sitting
in the pixel fields, and ^[[4;851;1026t after it, which is correct (37 times
23, and 114 times 9).
Writing output in the same call as the query does not avoid the problem:
printf 'hello\e[16t' still gets ^[[6;1;1t, because ghostel generates the
reply while parsing that chunk and the redraw the output schedules runs later.
Hosting the program under an interactive shell does not avoid it either. I
start pi as $SHELL -i -c 'pi "$@"' pi ... and it reads 1x1 every time.
Root cause
Renderer.init (src/Renderer.zig:122) seeds pending_resize with
cell_w = 1, cell_h = 1 and commits it immediately, so the terminal starts out
with width_px equal to cols and height_px equal to rows.
Renderer.resize (src/Renderer.zig:153) then only records a new
pending_resize. What applies it is commitResize (src/Renderer.zig:1043),
which runs from redraw (src/Renderer.zig:182). GhostelTerm.resize
(src/GhostelTerm.zig:227) documents that deferral as deliberate, so that
shrinking the viewport promotes rows into scrollback against fully rendered
state. The cell pixel dimensions travel in the same struct and inherit the
deferral, and that is the part a size query sees.
sizeCallback (src/handler.zig:103) divides terminal.width_px by
terminal.cols and terminal.height_px by terminal.rows. That is correct
once any resize has been committed, and it yields 1x1 before the first one.
So the seeding call in ghostel--init-buffer (lisp/ghostel.el:5406) cannot do
what its comment describes. Nothing has rendered when it runs, so the
dimensions it passes are still pending when the child's first query arrives.
Suggested fix
Apply the cell pixel dimensions in Renderer.resize straight away and keep
deferring only the column and row commit. The cell size feeds size reports and
kitty graphics geometry but does not reflow the grid, so it does not need to
wait for a render. Passing the dimensions into Renderer.init would work too,
and would mean the terminal never exists with a 1x1 cell.
A related gap, if you want to close it at the same time:
PosixPtyProcess.resizePty (src/PosixPtyProcess.zig:67) sets ws_xpixel and
ws_ypixel to 0 in its TIOCSWINSZ call, so a program that reads the window
size from the tty instead of querying never learns the cell size at all.
Environment
ghostel 0.50.0 plus unreleased main (b25a55e), Emacs 31.0.91 (GUI, emacs-plus),
macOS on aarch64 (aarch64-apple-darwin25.5.0).
Summary
Images drawn with the kitty graphics protocol come out vertically stretched,
roughly two and a half times taller than they should be on my font, whenever
the program drawing them was started as the terminal's own process. A square
image fills a tall rectangle.
The cause is the cell size query. A ghostel terminal answers
CSI 16 twith a1x1 pixel cell until it has rendered once, so a program that asks at startup,
before ghostel has drawn anything into the buffer, reads
CSI 6 ; 1 ; 1 t,concludes that the cells are square, and lays its images out on that basis. The
stretch is the real cell aspect ratio, 23/9 here.
CSI 14 t(text area inpixels) and
CSI 18 t(text area in cells) are wrong in the same window andreturn the same numbers as each other, because the pixel fields hold the row
and column counts.
I hit this with pi, a terminal coding agent that queries the cell size when its
interface starts and accepts any reply carrying two positive numbers. Anything
started through
ghostel-execas the terminal's own process is exposed thesame way, including timg and yazi previews. A program you launch by typing its
name at a shell prompt is unaffected, because the prompt has already forced a
render.
ghostel--init-bufferalready seeds the real cell dimensions before theprocess starts, and its comment says why, but the seed only takes effect at the
first redraw, which comes too late for the program the terminal was started to
run. 0.49.0 fixed a 1x1 cell size for
ghostel-compile; this is the sameproblem on the ordinary spawn path, except that it clears itself once anything
renders rather than waiting for a manual resize.
Steps to reproduce
The new terminal prints:
Both replies should be
^[[6;23;9t. In that buffer(ghostel--reported-cell-width)is 9 and(ghostel--reported-cell-height)is23, from an 8x20 logical pixel font and a
(ghostel--cell-pixel-scale)of 1.14.CSI 14 tbehaves the same way. On a terminal of 37 rows by 114 columns itanswers
^[[4;37;114tbefore the first redraw, which is the grid size sittingin the pixel fields, and
^[[4;851;1026tafter it, which is correct (37 times23, and 114 times 9).
Writing output in the same call as the query does not avoid the problem:
printf 'hello\e[16t'still gets^[[6;1;1t, because ghostel generates thereply while parsing that chunk and the redraw the output schedules runs later.
Hosting the program under an interactive shell does not avoid it either. I
start pi as
$SHELL -i -c 'pi "$@"' pi ...and it reads 1x1 every time.Root cause
Renderer.init(src/Renderer.zig:122) seedspending_resizewithcell_w = 1, cell_h = 1and commits it immediately, so the terminal starts outwith
width_pxequal tocolsandheight_pxequal torows.Renderer.resize(src/Renderer.zig:153) then only records a newpending_resize. What applies it iscommitResize(src/Renderer.zig:1043),which runs from
redraw(src/Renderer.zig:182).GhostelTerm.resize(src/GhostelTerm.zig:227) documents that deferral as deliberate, so that
shrinking the viewport promotes rows into scrollback against fully rendered
state. The cell pixel dimensions travel in the same struct and inherit the
deferral, and that is the part a size query sees.
sizeCallback(src/handler.zig:103) dividesterminal.width_pxbyterminal.colsandterminal.height_pxbyterminal.rows. That is correctonce any resize has been committed, and it yields 1x1 before the first one.
So the seeding call in
ghostel--init-buffer(lisp/ghostel.el:5406) cannot dowhat its comment describes. Nothing has rendered when it runs, so the
dimensions it passes are still pending when the child's first query arrives.
Suggested fix
Apply the cell pixel dimensions in
Renderer.resizestraight away and keepdeferring only the column and row commit. The cell size feeds size reports and
kitty graphics geometry but does not reflow the grid, so it does not need to
wait for a render. Passing the dimensions into
Renderer.initwould work too,and would mean the terminal never exists with a 1x1 cell.
A related gap, if you want to close it at the same time:
PosixPtyProcess.resizePty(src/PosixPtyProcess.zig:67) setsws_xpixelandws_ypixelto 0 in itsTIOCSWINSZcall, so a program that reads the windowsize from the tty instead of querying never learns the cell size at all.
Environment
ghostel 0.50.0 plus unreleased main (b25a55e), Emacs 31.0.91 (GUI, emacs-plus),
macOS on aarch64 (aarch64-apple-darwin25.5.0).