Skip to content

v0.2.0

Choose a tag to compare

@brainkim brainkim released this 12 Oct 00:19
· 583 commits to main since this release

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, with object-fit, srcset and decode(). <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's Worker where there is one, and decoders refuse image bombs. A canvas is laid out at width by height CSS pixels: cells under the default unit cell, pixels with cellSize set. 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 naturalWidth and naturalHeight are that size in CSS pixels, as a browser reports a high-density image, and drawImage(image, x, y) draws the whole image at that size, as Safari does. createImageBitmap() and source rectangles keep the file's pixels.

  • images and canvas say 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 fetch listeners on the TermDOM, as a Service Worker's FetchEvent, then to the network. The policy also covers inline event handlers (script-src-attr, script-src, default-src) and the page's own fetch (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 as CanvasCellGridContext.

  • 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. A keydown listener that calls preventDefault() keeps the key, for an editor's undo. Over SSH and on Windows it's an ordinary key (#84).

  • installGlobals(term) puts the window on globalThis for 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 unhandledrejection on the window instead of ending the process, and a late handler fires rejectionhandled. localStorage and sessionStorage are the window's where the runtime's are missing or undefined, 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.screen is a real Screen. 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 otherwise process.exitCode, and SIGINT, SIGHUP and SIGTERM exit 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 through term.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 window is false and feature detection works. navigator has its standard fields, hardwareConcurrency included.
  • An exception in a window.setTimeout(), setInterval() or queueMicrotask() callback ended the process; it's reported through the window's error event.
  • 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 double drew a single underline.
  • A terminal session with no tty on stdin now ends when its parent process dies, instead of running on.

Breaking changes

  • colorDepth and colorScheme are options of TermDOM, not of the transport. colorDepth takes 24, 8 or 4, 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 an InvalidStateError.

  • Use after dispose() fails. attach() and print() reject, and renderANSI() throws, with an InvalidStateError. A url that doesn't parse and a colorScheme other than "auto", "light" or "dark" throw a TypeError.

  • term.window is typed as a browser's window, Window & typeof globalThis, so term.window.HTMLElement and the rest type-check. The types match TypeScript 6's DOM types; on TypeScript 5, set skipLibCheck.

  • Disposal is asynchronous only: use await using, as Symbol.dispose is gone.

  • The keys scroll, as in a browser. The arrows, PageUp, PageDown, Space, Home and End scroll unless a keydown listener calls preventDefault() (#84).

  • A page's ::part() no longer reaches into built-in controls, as in browsers. Style <progress> and <meter> with ::slider-track, ::slider-fill and :low-value, :optimal-value and :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-glyphs and members no standard defines are gone, and DOMRectList is its own interface rather than an Array. A new check keeps every interface on window matching TypeScript's lib.dom.

These release notes were generated by Claude Code.