Repository navigation
Expose the probed cell size (cell aspect) through the public API #154
Description
Not a bug report — an API ask, grown out of the image support in #82.
The problem. Any app drawing proportional content onto a termdom-cellgrid canvas — album art, charts, QR codes, thumbnails — must size its grid against the terminal's real cell aspect (cell height ÷ width in pixels), or the rendering distorts. That aspect is not a constant: the classic 8×16 cell gives 2.0, JetBrains Mono's natural line box gives ~2.2, and line spacing / integer-pixel rounding push real terminals further. On my setup (Konsole + JetBrains Mono) the terminal answers the CSI 16 t query with CSI 6;25;11t — 11×25 px cells, aspect ≈ 2.27 — so a square image fitted at the customary 2:1 renders ~13% too tall, and users resort to hand-tuned magic constants per terminal.
TermDOM already measures this: the framebuffer starts from a guessed 8×16, then negotiateCellPixels() asks CSI 16 t at attach and adoptCellPixels() stores the answer — and intrinsic <img> sizing (the #82 feature) lays images out at round(px ÷ cell) through it. But the measurement is unreachable from app code:
- the package exports map blocks
@b9g/termdom/internal/cssom, wheregetImageCellSize()lives; - the TermDOM instance, document and window expose no property or event for it, and
window.devicePixelRatioreturns a hardcoded1(fair — CSS px are cells under the unit cell — but it leaves no public access at all); - the
termdom-cellgridcontext keeps itscellPixels()behind a private symbol.
The workaround we ship (simplified; the engine sizes a replaced <img> with the probed cell pixels, so a hidden probe leaks the measurement back through public DOM geometry):
// Hidden probe: a 640×200 fully transparent PNG (base64 data URL).
const probe = document.createElement("img");
probe.alt = "";
probe.src = PROBE_PNG_DATA_URL;
probe.setAttribute(
"style",
"position:fixed;visibility:hidden;width:auto;height:auto",
);
document.body.appendChild(probe);
// Read the cell aspect back — the box is round(640/cellW) × round(200/cellH).
const box = probe.getBoundingClientRect();
const cellW = 640 / box.width;
const cellH = 200 / box.height;
const cellAspect = cellH / cellW; // ≈ 2.27 once the probe answer lands,
// 2.0 while the 8×16 guess is still standingCaveats that a real accessor would not have: it costs an image decode and a DOM node per app; precision is bound by the integer rounding of the laid-out box (±0.5 cell per axis — a larger probe narrows it); and it reads the guess until the CSI 16 t answer arrives, so consumers must read it lazily rather than once at startup.
The ask — any one of these would do, smallest first:
- A getter on the TermDOM instance or its window, e.g.
term.cellPixels: {width, height}— the framebuffer's current value, guessed or probed. - An accessor on the
termdom-cellgridcontext (ctx.cellPixelsorctx.cellAspect) — cellgrid consumers are exactly the apps that need it. - Export
getImageCellSize/getCellPixelsfrom the package root (the exports map currently blocks the internal path).
Two bonus behaviors that would complete it:
- A signal when the value changes — an event, or dispatching a window
resizefromadoptCellPixelseven under the defaultcellSize: "unit"(today theterminalresizere-probe listener is only installed whenmeasuresCell, i.e.cellSize: "auto"). - Installing that re-probe unconditionally, so a font-size zoom mid-session refreshes image sizing under the default setting too.
Environment: @b9g/termdom 0.2.0, Node 24, primary dev terminal Konsole. All engine internals cited from the 0.2.0 bundle (GUESSED_CELL_PIXELS, negotiateCellPixels, getNaturalSize, getPixelCellSize, the exports map in package.json).
Activity
Measure the terminal's cell on request, and announce a new one
brainkim commentedon Oct 15, 2026
Thanks for the detailed write-up, and for the probe workaround, which showed exactly what was missing!
0.2.1 adds
term.measureCell()(#155). It asks the terminal for its cell (XTWINOPS 16) and resolves to{width, height}in the terminal's pixels, whatever thecellSizeoption says. So under the default unit cell you get the real 11×25, not 1×1:A few differences from what you proposed:
It's async, and never returns the 8×16 guess. The real value exists only once the terminal answers, so
measureCell()waits for it. It resolves tonullwhen there's no terminal to ask (beforeattach(), afterdispose(), output not a TTY) or the terminal doesn't answer, so you pick the fallback yourself. Returning a guess for this would be counterproductive.Changes come as an event.
cellchangefires on the TermDOM withwidthandheightwhen the cell is measured at a different size: when the first answer afterattach()replaces the assumed size, and after a font zoom.No context accessor or internal exports. One method on the TermDOM serves the cellgrid and every other consumer, without making internals public API.
Your second bonus is in too: TermDOM now asks for the cell again after every resize, not only under
cellSize: "auto". A font zoom resizes images under the default setting as well. Before, they kept the size the old cell gave them until restart.Closing, but feel free to reopen if there are additional issues.
Trippnology commentedon Oct 16, 2026
Thanks for sorting this so quickly, that worked perfectly. 👍