Skip to content

0.2.0: images and canvas follow-up, and a DOM that matches lib.dom - #141

Merged
brainkim merged 30 commits into
mainfrom
one-cell-guess
Oct 8, 2026
Merged

brainkim merged 30 commits into
mainfrom
one-cell-guess

Conversation

@brainkim

@brainkim brainkim commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

The follow-up to the images and canvas work in #136, and the changes for 0.2.0: what a program configures, what a page can reach, and how unloaded images, control styling and the cell canvas behave. It also carries a cleanup of code that had grown roundabout. Every commit passes the full suite on Node, Bun and Deno.

Breaking changes

  • colorDepth is an option of TermDOM, not of its transport. new TermDOM({colorDepth: 24}) replaces transport.colorDepth and transportFromProcess(proc, {colorDepth}). It takes the bits screen.colorDepth reports, 24, 8 or 4, or "auto", the default, which asks the terminal. "rgb", "256" and "ansi" still name the same three. Any other value throws.
  • "ansi" draws with all 16 SGR colors and reports 4 bits. A color goes to the nearest of xterm's 16 default colors, so bright colors go out as SGR 90–97 and 100–107. screen.colorDepth is 4 and (color-index: 16) matches.
  • The cell canvas is getContext("termdom-cellgrid"). It is TermDOM's own, so it carries TermDOM's prefix, as "moz-webgl" and "webkit-3d" carried theirs, and each cell carries a glyph and its styling. Its interface, CanvasCellGridContext, is exported from the package and is no longer on window. (The old name never shipped in a release.)
  • A page's ::part() no longer reaches into the built-in controls, as in every browser. A page styles them through the pseudo-elements browsers ship: ::placeholder, ::details-content, ::picker(select) and ::picker-icon. A <progress> draws its fill in its accent-color. The rest of a control's insides, such as a meter's level colors and a button's brackets, are the UA sheet's alone. The data-level, data-highlighted, data-disabled and data-grouped attributes on control parts are gone.
  • --math-glyphs and --math-variant-glyphs are gone. They were custom properties the MathML renderer read from the author's own namespace. MathML always draws with the Unicode pieces and terminal italics it used by default.
  • DOMRectList is its own interface, no longer an Array, with length, item(), indexed properties and the standard iterator. As in browsers, a page cannot construct one.
  • Members a page could see that no standard defines are gone, among them colorAt on CanvasGradient and CanvasPattern, the live collections' compute, namedProperties and getChangedMembers, the CSS rules' prelude, atKeyword and static atRule, a computed style's getComputedValue, innerText on MathMLElement, and takeRecords on ResizeObserver.

The version is 0.2.0.

New options

  • graphics: how a document draws pixels for <img> and a canvas's "2d" context. "auto", the default, uses the best way the terminal has, which today is always cells. "cells" forces half blocks. "none" draws no pixels: an <img> loads nothing and shows its alt text, as HTML's "images are disabled" does, and getContext("2d") returns null, so a canvas shows its fallback content.
  • colorDepth, above.

The images guide now opens its loading section with the three csp recipes: images from the web, a mail client's cid: attachments, and "" for no policy.

An image that won't load is its alt text

HTML's rendering rules (§15.4.2) keep an image that isn't showing a box only when it has a size and may still load, has no alt text, or is in a quirks-mode document. Otherwise its alt text is ordinary inline text. TermDOM drew every such image as a bordered box three rows tall, which made a mail client's link lists with blocked thumbnails much longer. The alt text lives in the image's UA shadow tree, as Chromium keeps it.

The DOM a page sees matches lib.dom

The drift script builds a TermDOM and walks every interface on its window, the objects a page reaches without a constructor (navigator, screen, the 2D context, a computed style), and what calls such as getElementsByClassName() return. It fails on any member lib.dom doesn't declare and on any prototype in a chain that isn't a standard interface. Members a current standard defines that this lib.dom lacks are listed with where they're defined. The members it found are removed, listed above.

The engine's own classes are out of every prototype chain a page can see. LayoutObserver is gone: each observer keeps its own state and calls three functions. The CSS rules' shared bases are init functions. getComputedStyle() returns a plain CSSStyleProperties that reads through to the engine's computed declaration. getElementsBy*() return a plain HTMLCollection. LiveList's methods are copied onto each list interface.

Fixes

  • A style change reaches the shadow trees under it, closed and built-in ones included. Setting color on a form's container now recolors its fields' text.

Cleanup

These commits change no behavior, except the whitespace fix noted under Duplication.

Per-document values now live on the attached document. attachDocument takes one object of named parts, instead of eight positional arguments plus side registries.

  • The cell size: the 8×16 guess and the terminal's reported cell live only in the framebuffer, read through getCellPixels. The cellSize option is stored on the document, and getCellSize reads it there. Gone: the FALLBACK_CELL re-export from cssom, the second "not interactive" choice in index.ts, the setScreenCellSource and setCellSizeSource callback registries, and the separate kMeasuresCell flag. The renderANSI engine is built with its parent's cell, and cells are compared by size, not by object identity.
  • The color scheme is decided once, in Exchange.colorScheme: the terminal's answer, then the transport's, then light. cssom reads it through the document's exchange. The setColorSchemeSource registry and index.ts's copy are gone.
  • The resource loader and its abort signal go through attachDocument, in place of resources.ts's setResourceLoader registry.
  • Hover listeners are counted in one map, not two maps and a closure.

Imports and exports.

  • layout.ts and painter.ts import isReplacedElement from images.ts, where it lives, not through replaced.ts's re-export.
  • Three files that imported one module in two statements now use one.
  • getNestedRules was used nowhere and is deleted.
  • 25 functions, constants and types that only their own module uses are no longer exported.

Duplication.

  • isControlByte lives in text.ts, not in both framebuffer.ts and exchange.ts.
  • The 80×24 default terminal size and the 1 s terminal-question timeout are named constants in exchange.ts, not literals written 5 and 8 times.
  • Trimming and splitting HTML's ASCII whitespace uses one pair of helpers in text.ts, in place of nine hand-written trims in dom.ts, two split functions, and a trim in hints.ts. The CSP parser now trims only ASCII whitespace, as CSP3 says, instead of String.prototype.trim, which also strips Unicode spaces.

Comments and names.

  • dom.ts no longer claims its isTextControl is the only definition. editing.ts's different test (an input, textarea or select, which keeps its keys) is renamed isFormControl.
  • A layout comment named a nonexistent bidi.ts.
  • Comments describing past bugs or past code now describe what the code does.

Looked at and left as is:

  • hints.ts's local HTML_NAMESPACE, which keeps a leaf module from importing dom.ts.
  • The namespace literals in dom.ts, which are type-level overloads mirroring lib.dom.
  • The two isShadowRoot functions, which agree in practice.
  • The two domException helpers, which build exceptions in different realms.

🤖 Generated with Claude Code

https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D

brainkim and others added 9 commits October 6, 2026 13:13
The 8 by 16 guess at a cell's pixels was defined in the framebuffer,
re-exported from cssom so index.ts's old import still worked, chosen
again in index.ts for a transport that is not a terminal, though that
framebuffer never leaves the guess anyway, and used a third time as
cssom's own fallback for image sizes, read through a callback index.ts
registered with cssom.

The framebuffer now holds the guess privately and the cell it adopts
from the terminal. dom.ts reads that cell from a document's framebuffer
with getCellPixels, image sizing reads it there, and "auto" reads the
framebuffer directly. The re-export, the second choice and the callback
registry are gone. A document no TermDOM draws draws no image, so its
image cell is the page's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The per-document hover listener count was kept in one WeakMap, wrapped
by watchHoverListeners in a closure that read it, and that closure kept
in a second WeakMap for hoverListenerCount to call. hoverListenerCount
now reads the count, and attachDocument sets the count's onChange
itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The cellSize option became a closure in index.ts, registered with cssom
for getCellSize to call, while index.ts decided "auto" a second time in
its own flag. The engine behind renderANSI then had its closure written
over after it was built, and two cell sizes were told apart by object
identity.

attachDocument now keeps the option, checked, on the document, and
getCellSize reads it there: the unit cell, the framebuffer's cell for
"auto", or the size given. TermDOM reads "auto" from the same place, and
a cell measured anew changes the page exactly when the setting is
"auto". The renderANSI engine is built with its parent's cell, and cells
are compared by their sizes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Whether the terminal is light or dark was resolved in index.ts, from
the exchange's answer, then the transport's, then light, and handed to
cssom through a callback registered per document, where cssom fell back
to light a second time. The exchange already has both the answer and
the transport, so its colorScheme now resolves all three, and cssom
reads it through the document's exchange. The callback registry and
index.ts's copy are gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The document's loader and the signal that ends its loads went to
resources.ts through setResourceLoader, kept in a registry there for
dom.ts to look up, while everything else a TermDOM hands its document
goes through attachDocument. attachDocument now takes them with the
rest, as one object of named parts in place of eight positional
arguments, and the image load reads them from the attached document.
The registry is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
replaced.ts re-exported isReplacedElement from images.ts for layout.ts
and painter.ts, which now import it from images.ts. cssom.ts and
cssselectors.ts each imported dom.ts in two statements, and cssvalues.ts
imported cssselectors.ts's types in a second one; each is one statement
now. getNestedRules was used nowhere and is gone. Twenty-five functions,
constants and types that only their own module uses are no longer
exported.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
dom.ts called its isTextControl the single definition of a text
control while editing.ts had another isTextControl testing something
else: whether an element is an input, textarea or select, which keeps
its keys. That one is isFormControl now, and dom.ts's comment no longer
claims to be the only one. A layout comment named a bidi.ts that does
not exist. Comments that told of past bugs or past code now say what
the code does, and the list gutter's explanation is kept in one place.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
isControlByte was written out in both the framebuffer and the exchange;
it lives in text.ts now. The 80 by 24 a silent terminal is taken to be
was written five times, and the second a question to the terminal waits
eight times; each is a named constant in exchange.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
dom.ts wrote the trim of HTML's ASCII whitespace out nine times and
had its own split, cssselectors.ts had a second split, hints.ts a third
trim, and the CSP parser trimmed with String.prototype.trim, which also
strips Unicode spaces that CSP3 does not. text.ts, which already owns
toASCIILowercase, now has trimASCIIWhitespace and splitOnASCIIWhitespace,
and each of them uses those.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
@brainkim brainkim changed the title Clean up how the cell's pixel size is kept and read Clean up indirection, duplication and stale comments Oct 6, 2026
brainkim and others added 14 commits October 6, 2026 15:37
HTML's rendering rules (§15.4.2) keep an image that is not showing a
box only when it has a size and may still load, has no alt text, or is
in a quirks-mode document. Otherwise its alt text is laid out as inline
text. The text lives in the image's UA shadow tree, as Chromium keeps
it, and layout and painting ask one function whether an element is
replaced.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The drift script now builds a TermDOM and walks every interface object
on its window, and the objects a page reaches without a constructor
(navigator, screen, the 2D context, a computed style), failing on any
member lib.dom does not declare and on any prototype in a chain that is
not a standard interface. Members a current standard defines that this
lib.dom lacks are listed with where they are defined.

The members it found are gone: the live lists' compute, namedProperties
and getChangedMembers, colorAt on CanvasGradient and CanvasPattern, the
CSS rules' prelude, atKeyword and static atRule, a computed style's
getComputedValue and declaredCustomProperties, and innerText on
MathMLElement, which the DOM puts on HTMLElement only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
A page could reach classes the engine made for itself through
Object.getPrototypeOf: LiveList and HTMLCollectionBase under every
collection, LayoutObserver under both observers, the CSS rules' shared
bases, ComputedStyleDeclaration under getComputedStyle(), and
MatchingCollection for getElementsBy*. DOMRectList inherited every
Array method, and ResizeObserver had IntersectionObserver's
takeRecords().

LayoutObserver is gone: each observer keeps its own state and calls
three functions. The CSS rules call init functions and keep their own
style and cssText, and @counter-style, @Property and
@font-palette-values lose the style attribute their spec does not give
them. getComputedStyle() returns a plain CSSStyleProperties reading
through to the engine's computed declaration. getElementsBy* return a
plain HTMLCollection that carries its test. LiveList's methods are
copied onto each list interface. DOMRectList is its own interface.

The drift script now also checks that what a page gets from a call is
an instance of the interface itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The renderer read two custom properties, in the author's own namespace,
to pick ASCII or box-drawing pieces for stretched operators and
radicals, and the Mathematical Alphanumeric block for mathvariant. Both
are gone with the tables and branches only they reached.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
… them

No browser lets a page's ::part() into a built-in control. A page now
styles one through ::field-content, ::slider-track and ::slider-fill
from the CSS Form Control Styling draft, the customizable select's
::picker(select) and ::picker-icon, and a meter's level through
::-termdom-meter-optimum-value and its two siblings, after WebKit's
prefixed ones. The UA sheet uses the same names. The other parts, a
button's brackets, a gauge's groove and a picker's rows, are the UA
sheet's alone, and the data-* flags on them are gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
::field-content, ::slider-track and ::slider-fill are in a draft no
browser ships, and the meter's level pseudo-elements were TermDOM's
own. A page styles a control through ::placeholder, ::details-content,
::picker(select) and ::picker-icon, and the rest stays the UA sheet's,
as it does in a browser.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
…indow

The context type is TermDOM's own, so it carries TermDOM's prefix, as
"moz-webgl" and "webkit-3d" carried theirs. Its interface,
CanvasCharGridContext, is no longer a global; the package exports it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
css-ui-4 has a <progress> draw in its accent-color, as browsers do, so a
page can color the fill without reaching into the control. With auto
the fill keeps the UA sheet's blue.

A style change reached a shadow tree only through element.shadowRoot,
which is null for a closed or UA tree, and never the shadow trees of
the element's descendants. A control's insides kept what they had
inherited, so setting color on a form's container left its fields'
text as it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
true, the default, draws pixels the best way the terminal has, which is
cells until a pixel protocol is negotiated. "cells" forces cells. false
draws none: an <img> ends unavailable without loading and shows its alt
text, as HTML's "update the image data" does with images disabled, and
getContext("2d") returns null, so a canvas shows its fallback content.
The chargrid context draws text and stays.

The images guide leads with the three csp recipes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The transport is the connection: input, output, size and close. How
TermDOM draws is its own option, asked of the terminal when the program
leaves it out, beside graphics and cellSize. The values stay "rgb",
"256" and "ansi". A transport, or transportFromProcess's options, that
still names a colorDepth is refused with where it went.

Tests that check the exact bytes, or render before attaching, name
"rgb" as a program that knows its terminal would. MockProcess answers
the 24-bit color question as a terminal that has it does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
TermDOM ignores a property it does not know, on its options and on a
transport alike, and TypeScript rejects one written in place.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
graphics takes "auto", "cells" or "none", and later the pixel
protocols, so the one that means detect is not true beside a list of
strings. colorDepth takes "auto" too, so detection can be written out,
as cellSize's "auto" already is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
24, 8 and 3 are what a page reads back from screen.colorDepth, so a
program can hand one TermDOM's depth to another. "rgb", "256" and
"ansi" name the same three, and any other value is refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
@brainkim brainkim changed the title Clean up indirection, duplication and stale comments 0.2.0: images and canvas follow-up, options without booleans, a DOM that matches lib.dom Oct 8, 2026
@brainkim brainkim changed the title 0.2.0: images and canvas follow-up, options without booleans, a DOM that matches lib.dom 0.2.0: images and canvas follow-up, and a DOM that matches lib.dom Oct 8, 2026
brainkim and others added 4 commits October 8, 2026 16:13
Each cell it holds carries a glyph and its styling, so it is a grid of
cells, as TermDOM calls them everywhere else, not of characters. Its
interface is CanvasCellGridContext. COMPATIBILITY.md is regenerated,
which also catches up line-height.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The compatibility table says which standards TermDOM implements. The
cell grid context is TermDOM's own, documented in the images and canvas
guide, so it has no row there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
A 2d canvas and a cell grid each clip to a scroller and paint the rows
scrolled into view, a cell grid narrower than its columns is cut at its
content box, and a 2d canvas wider than a box that clips it paints
nothing past the box.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
A scroller taller than the rows it moves is shifted under scroll
margins and only the rows scrolled in are written. A 2d canvas's
pixels and a cell grid's text land on the right rows after the shift.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
brainkim and others added 3 commits October 8, 2026 16:46
A color goes to the nearest of xterm's 16 default colors, so bright
forms use SGR 90-97 and 100-107. screen.colorDepth reports 4, the
colorDepth option takes 4 in place of 3, and color-index is 16.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
The progress examples use accent-color, the website names its color
depth on TermDOM, a page cannot construct a DOMRectList, part names
split on ASCII whitespace, and the guides cover color-index 16.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Bw3yEu4cfkuDkaZ3RSy4D
@brainkim
brainkim merged commit 5a3b9a3 into main Oct 8, 2026
6 of 7 checks passed
@brainkim
brainkim deleted the one-cell-guess branch October 9, 2026 16:28
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.

1 participant