Repository navigation
v0.2.0
TermDOM follows the web platform specifications where they apply. Where it departs from one, these notes say so.
0.2.0 adds images and canvas, serving apps over SSH, and the keyboard and job-control defaults a terminal program is expected to have. It also settles the API: renderer options moved off the transport, names no standard defines are gone, and styling the built-in controls follows the CSS drafts browsers are implementing.
Images and canvas are mostly the work of @titanism and Forward Email, who wrote the decoders, the "2d" context and the half-block renderer in #125 for their terminal mail client, which runs on TermDOM. Thank you!
Added
-
Images and canvas (#125, #136).
<img>decodes PNG, JPEG, GIF and BMP, withobject-fit,srcsetanddecode().<canvas>has a"2d"context with paths, gradients, patterns, compositing, transforms, clipping and pixel access; text is drawn as cells, and shadows and filters draw nothing. Both draw two pixels per cell with half blocks, so they work over SSH and in tmux. Images decode on the runtime'sWorkerwhere there is one, and decoders refuse image bombs. A canvas is laid out atwidthbyheightCSS pixels: cells under the default unit cell, pixels withcellSizeset. A canvas with no size is 38 by 9 cells under the unit cell, about the size of HTML's 300 by 150 pixels. -
An image is sized as it would look in pixels, measured against the terminal's cell. Its
naturalWidthandnaturalHeightare that size in CSS pixels, as a browser reports a high-density image, anddrawImage(image, x, y)draws the whole image at that size, as Safari does.createImageBitmap()and source rectangles keep the file's pixels. -
imagesandcanvassay how each draws:"auto"(default) or"cells"in half blocks, or"none", which shows an image's alt text or a canvas's fallback content instead. A page written for a browser often reads better without its pixels. -
csp, a Content Security Policy that narrows what markup may load, for a program showing markup it doesn't trust. Left out, every load is allowed, as in a browser:new TermDOM({csp: "img-src cid: data:"}); // a mail client
Loads go to
fetchlisteners on the TermDOM, as a Service Worker'sFetchEvent, then to the network. The policy also covers inline event handlers (script-src-attr,script-src,default-src) and the page's ownfetch(connect-src), which goes through the same listeners. -
The
"termdom-cellgrid"canvas context (experimental), a grid of glyphs with box-drawing lines and 2D-context settings, with its type exported asCanvasCellGridContext. -
transportFromSSH(session)serves a TermDOM over an ssh2 session, with no dependency on ssh2.client.on("session", (accept) => { new TermDOM({transport: transportFromSSH(accept())}).attach(); });
-
Ctrl+Z suspends, as a shell's job control does, and the document is hidden until
fg. Akeydownlistener that callspreventDefault()keeps the key, for an editor's undo. Over SSH and on Windows it's an ordinary key (#84). -
installGlobals(term)puts the window onglobalThisfor React, Vue, Svelte and CodeMirror, and returns a function that puts back what was there. Call it before importing the framework, since frameworks read the environment when they load:installGlobals(term); const {createRoot} = await import("react-dom/client");
While installed, an unhandled promise rejection fires
unhandledrejectionon the window instead of ending the process, and a late handler firesrejectionhandled.localStorageandsessionStorageare the window's where the runtime's are missing orundefined, as on Node without--localstorage-file.Element.animate()returns an animation that finishes at once, so Svelte transitions run. -
The arrows move through a radio group (#84).
<input type=range>is a slider that the arrows and the mouse move. The wheel scrolls boxes sideways. Translucent backgrounds blend.window.screenis a realScreen. A summary's marker is its::marker.
Changed
- Faster. A scroll moves the terminal's lines even when other parts of the frame change, so a one-row scroll writes 528 bytes instead of 2,168. A page with a
:has()rule no longer restyles everything on each keystroke (155 ms down to 20), and adding a line to a 2,000-line editor takes 10 ms instead of 79. Real emails open about a third faster, and nested tables eight deep in 0.4 seconds instead of three. - Exit statuses follow the shell. Ctrl-C exits 130,
window.close()exits 1 if the page let an exception escape and otherwiseprocess.exitCode, andSIGINT,SIGHUPandSIGTERMexit 130, 129 and 143. All exited 0 before. A program that listens for one of those signals itself gets the session closed and decides when to exit, as in Node. dispose()stops the timers set throughterm.window, as a browser stops an unloaded page's, and waits for the terminal's late replies so they don't land in the shell.- Frames come at most once every 16 ms, and an edit and what a microtask does in answer are drawn together.
- Line height takes
floor(line-height / font-size)rows, so text spaces the same at any terminal font size. - An image that isn't showing is its alt text, inline, as HTML renders it.
Fixed
- Reading a feature a terminal lacks, such as
window.indexedDB, threw; it's left off the window, so"indexedDB" in windowis false and feature detection works.navigatorhas its standard fields,hardwareConcurrencyincluded. - An exception in a
window.setTimeout(),setInterval()orqueueMicrotask()callback ended the process; it's reported through the window'serrorevent. - A 2D canvas drew right-to-left text backwards, and ignored the canvas's
direction. - Table layouts that real email uses drew blank or wrong, lines wrapped a word early, and fractional sizes rounded inconsistently.
- Emoji that terminals measure differently left fragments under tmux, a wide character left its old background, and a zero-width non-joiner spread words out in Terminal.app.
- A terminal's reply split across reads typed into the page, and origin mode left on by another program made scrolls land a row low.
- Removed shadow trees, controls, stylesheets and scrolled boxes stayed in memory.
text-decoration: underline doubledrew a single underline.- A terminal session with no tty on stdin now ends when its parent process dies, instead of running on.
Breaking changes
-
colorDepthandcolorSchemeare options ofTermDOM, not of the transport.colorDepthtakes24,8or4, or"rgb","256"or"ansi"; any other value throws. -
TermDOM reads no environment variables. It asks the terminal instead, and uses 24-bit color only when the terminal says it has it, so Alacritty, Konsole, VS Code and Warp drop to 256 colors. To keep the old behavior:
const truecolor = process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit"; new TermDOM({colorDepth: truecolor ? 24 : "auto"});
-
"ansi"draws all 16 basic colors. -
attach()takes no transport; pass it to the constructor. A transport serves one TermDOM: attaching a second to it rejects with anInvalidStateError. -
Use after
dispose()fails.attach()andprint()reject, andrenderANSI()throws, with anInvalidStateError. Aurlthat doesn't parse and acolorSchemeother than"auto","light"or"dark"throw aTypeError. -
term.windowis typed as a browser's window,Window & typeof globalThis, soterm.window.HTMLElementand the rest type-check. The types match TypeScript 6's DOM types; on TypeScript 5, setskipLibCheck. -
Disposal is asynchronous only: use
await using, asSymbol.disposeis gone. -
The keys scroll, as in a browser. The arrows, PageUp, PageDown, Space, Home and End scroll unless a
keydownlistener callspreventDefault()(#84). -
A page's
::part()no longer reaches into built-in controls, as in browsers. Style<progress>and<meter>with::slider-track,::slider-filland:low-value,:optimal-valueand:high-value, from the CSS Form Control Styling draft, or with the stable WebKit and Firefox names such as::-webkit-progress-value. The track and fill are painted by their backgrounds, as in a browser.progress::slider-fill { background-color: green; } /* was progress::part(bar) { color: green; } */
-
--math-glyphs,--math-variant-glyphsand members no standard defines are gone, andDOMRectListis its own interface rather than anArray. A new check keeps every interface onwindowmatching TypeScript's lib.dom.
These release notes were generated by Claude Code.