hottyHTML over the TTY

☰ Contents · Reference › Changes

Changes

0.4

A draft, as every 0.x is, released 2026-10-18 across the stack. It breaks 0.3 (SPEC.md §15): a 0.4 host answers a 0.3 program with EVERSION, and the program falls back (§4, §14).

What changed

  • Keys are named by myelin key names 0.1 (SPEC.md §10.4), which this document cites at the tag myelinkey/v0.1.0 rather than naming keys itself. The modifiers are super, ctrl, alt and shift. A physical key is its W3C code in snake case (arrow_left, page_down, key_w). A character is what the key types, a letter written small with shift as a modifier (shift+g). The W3C key values (Control+a, ArrowLeft, Meta+Enter) are gone.
  • mod is myelin's command modifier (its §4.4): a binding writes mod+s. The capabilities' mod is "ctrl" or "super", and edit names its keys the new way (§4).
  • A host reads a key a program writes with myelin's default reading, so G is g, and meta is refused (§10.4).
  • A press can have two names, its physical key and the character it types, and a keymap is looked up by the physical one first (§10.4). A host that reads bytes names what they carry.
  • Shift falls back only from a key that is not a character (§10.2): shift+g is a key of its own. A field types what a key types, a letter with shift as its capital.
  • mods keeps its bits, and 8 is super (§9).
  • A press given to the program as pressed stays the program's when it comes again as bytes, though the field binds its bytes' name, and a prefix binding starts its prefix (§10.4): ctrl+key_a=program in a field sends ctrl+a on.
  • A modifier tapped alone is named on its release (shift_left, shift) and goes no further, so a tap bound to program or prefix does nothing (§10.4).
  • A command's key is the one name its bytes have, ctrl+c, never ctrl+key_c (§10.6).
  • A finger can drag in the cells (§9.6). a=touch names areas of the screen, each with the way it drags. There, while the program reads the mouse, a touch drag that way and a tap reach the program as the mouse's primary button. Elsewhere, and the other way, a touch drag still scrolls. It adds to 0.3 and changes nothing a program already does.
  • The name is written hotty, in lowercase, even at the start of a sentence: "hotty 0.4", "a hotty host". Nothing on the wire changes, and neither do identifiers, environment variables or task ids. The text of 0.1 to 0.3 stays as it was released.

Upgrading from 0.3

A program:

  • Write keys in the new names: in data-keys, in a menu's item keys and seq, and wherever you compare a command's key. Mod+s is mod+s, and Control+A is ctrl+shift+a (§10.4).
  • Read the capabilities' mod as "ctrl" or "super", and edit in the new names (§4).
  • Bind Shift with a letter as shift+<letter>: a host reads G as g.
  • If you read the mouse to drag something in your cells, such as a slider, send its cells with a=touch so that a finger drags it too, and send them again when it moves (§9.6).

A host:

  • Read a written key with myelin's default reading, mod standing for your mod. Refuse meta, and a binding with mod and ctrl or super as well (§10.4).
  • Name a press seen as pressed by its physical key and by the character it types, and look the physical one up first. Name keys from bytes as §10.4's table has them.
  • Report mod, edit and a command's key in the new names, the key as its bytes name it (§10.6).
  • Keep a press you give the program as pressed, by either name, the program's when its bytes come, and start a prefix binding's prefix then. Offer a modifier tapped alone to the surface, and send it nowhere else (§10.4).
  • Fall back without shift only from a key that is not a character (§10.2).
  • Take a=touch, and keep its areas per screen. In an area, while the program reads the mouse, report a touch drag the area's way, and a tap, as the mouse's primary button (§9.6).

A multiplexer does both lists. A keymap of its own that is myelin's already speaks these names. One that relays the mouse gives the terminal the areas of the panes it shows, moved to where each pane is (§9.6).

0.3

A draft, as every 0.x is. It breaks 0.2 (SPEC.md §15): it folds what 0.2 grew, for bandwidth and for fewer rules, and programs and hosts move to it together. The list it folds is vault/tasks/fold-0.3.md.

What changed

  • A delta is one list of ops (SPEC.md §6.1): a=delta:s=<name> and one msgpack map, {"ops": [{"op", "t", "k", "v"}, …]}, applied in order and shown as one. The control keys op, t and k, and the text payload, are gone. A frame of fifteen small changes is 184 bytes, not 755. An op whose target is missing fails alone, and the reply names it by its index, i; a malformed op rejects the whole delta. Targets travel as the document has them, so an id outside ASCII is addressed as it is.
  • p, f and v belong to the surface (SPEC.md §5.2): 1 on, 0 off, held until the program sends them again. A placement without them leaves them as they were. One table in §9 says, for every event, what asks for it, how long and how often.
  • Features (SPEC.md §4): a 0.3 host does every op and event, scrolling, steps, drops and view. One list, features, names what a host may lack: passthrough, hover, count, menu. The capabilities' ops, events, passthrough, scroll, steps, drop, count, keys and menu are gone.
  • The cell is in CSS pixels (SPEC.md §4, §5.3, §8): cellcss takes the place of cell, resize is gone, and a new font sends caps with the new cellcss. --hotty-px is one device pixel, current through zooms. scale stays, as a hint for images.
  • caps carries any field that changed (SPEC.md §4, §9), not only mod and edit: a multiplexer whose program moves to another viewer's host tells it everything that differs.
  • Event details (SPEC.md §9): a submit's fields are pairs, {"fields": [[name, value], …]}; mods, one int of modifier bits, goes on clicks, presses and drags in place of a drag's keys; t is absent, not empty, where no element is meant; a detail with no field is no payload.
  • Errors only, unless asked (SPEC.md §3.5, §3.6): q defaults to 1, and to 0 for a query and a command with n. Continuation chunks carry only m.
  • Key sequences are the program's (SPEC.md §10.5): data-key, the capabilities' keys, and the hosts' sequence engines are gone. A keymap action, prefix, sends the program every key after it, whatever is focused, until it sends a=focus.
  • A menu is what a program can do (SPEC.md §10.7, which takes menus out of §10.5): a=menu sends the program's actions as a tree, kept per screen, each item with a key where one reaches it and a name; a=menu:show=1 shows the level the keys typed reach, a few bytes a key, and show=0 hides it. A pick, from a multiplexer's palette or an agent, sends command with the item's name. A 0.2 menu was one level, shown when sent. A host that takes menus has the feature menu, for programs on surfaces as in the cells.

Upgrading from 0.2

A program:

  • Send a frame's changes to a surface as one a=delta whose ops list holds them all (§6.1), not one message per change. Deltas no longer take op, t or k in the control.
  • Send p, f and v when they change, not with every placement: they hold until the program sends them again (§5.2).
  • Lay out with cellcss, in CSS pixels, and stop handling resize: a new font sends caps with the new cellcss (§4). A document that needs a device pixel reads --hotty-px (§8).
  • Ask features for passthrough, hover, count and menu only, and count on every op, every event, scrolling, steps and drops.
  • Read a submit's fields as pairs, a click's, press's or drag's mods as bits, and an absent t as no element (§9).
  • Leave q out unless you want more than errors (§3.5).
  • Resolve key sequences yourself: bind the keys that start one to prefix in the keymap, and the keys after it come to you, whatever is focused, until you send a=focus (§10.5). data-key is gone.
  • Send what you can do as a menu tree with a name per item, show a level with a=menu:show=1 and hide it with show=0, and handle command with a name from a pick (§10.7).

A host:

  • Decode a delta's op list and apply it as one unit. Reply to an op whose target is missing with its index i, and reject a malformed op's whole delta (§6.1).
  • Keep p, f and v per surface (§5.2).
  • Report cellcss and features, and send caps with every field that changed. Send no resize (§4).
  • Take q as 1 when it is absent, and read only m on continuation chunks (§3.5, §3.6).
  • Send event details as §9 has them.
  • Route every key after a prefix to the program, until it sends a=focus or the surface loses the keyboard (§10.5).
  • Keep a program's menu per screen and show the level its keys reach. Report the feature menu only if both hold (§10.7). Add no palette for it: where you already list commands of your own, you may list its named items there, and send a pick as command with the item's name.

A multiplexer is a host to the programs in its panes and a program to its terminal, so it does both lists. Besides them:

  • Answer a pane's query for the terminal whose keys reach that pane, and when that terminal changes, send the pane caps with every field that differs from what it last heard (§4).
  • Replay a surface with its p, f and v: they are the surface's now, not a placement's.
  • Keep each pane's menu and show its levels, or leave menu out of the features you report and answer a pane's a=menu with EINVAL (§10.7). You may offer the menu in a palette of your own, or to whatever drives the pane, and send picks to the pane as command.
  • While a surface you relay has the keyboard, give its pane every key you read, after your own shortcuts. A prefix asks nothing of you, since the keys the surface uses never reach you (§10.5).

0.2

A draft, as every 0.x is. It breaks 0.1 (SPEC.md §15): programs and hosts move to it together. It ships with hottyterm's release that pins hotty-blitz a70065f and with xterm-addon-hotty 0.1.0-next.8.

What changed

  • A host's bodies are msgpack (SPEC.md §3.3): the capabilities, an error's body, and every event's detail are one msgpack map, with nothing after it, nested at most 32 deep. What a program sends is unchanged: text and bytes.
  • Each field has a type, and keeps it from 0.2 on: int, float, str, bool, arrays and maps of them. A float is sent as a float even when whole. A known field of another type fails the body; unknown fields are skipped.
  • A body holds nothing a host does not send, anywhere in it: no nil, keys that are strings and each given once, UTF-8 strings, ints within 2^53 − 1 of zero, and timestamps msgpack defines. A body that breaks one does not decode, and the message stands without it (SDK.md §3.9).
  • A query lists the versions the program speaks (SPEC.md §4): a=q:n=1:v=0.2. A host answers in the highest it shares, or with the new error EVERSION, its detail the versions it speaks. A program takes as a host only an answer whose capabilities decode and name a version it speaks. A 0.1 program on a 0.2 host, and a 0.2 program on a 0.1 host, each find no host and draw in cells.

Added within 0.2

What follows came after the first 0.2 hosts, and keeps the version: a host that lacks it says so in its capabilities.

  • reveal (SPEC.md §6.1), a delta that scrolls an element into view: its top to the top with start, its bottom to the bottom with end, and by as little as it takes with neither. It moves no focus. A program asks Supports(c, "reveal").

  • view (SPEC.md §9.5), an event: the elements with data-on~=view and an id that are in view, each with its area, and whether the document shows its end. A host sends it once the document is laid out, then on each change, once a scroll comes to rest. A program asks Views(c).

  • limits.message (SPEC.md §13): a host may limit the decoded size of one reassembled message and reports it under limits (§4). Past it a message is malformed (§3.7), not EQUOTA, and its further chunks are dropped; a host that reports none enforces none, and one with a limit should accept at least 1 MiB decoded. A resource is one message, so this limit also caps the largest resource. A program may limit what it decodes from its host the same way: each SDK's Decoder takes Max, 16 MiB by default (SDK.md §3.6).

  • A click's count (SPEC.md §9): a click that quickly follows another on the same element carries its place in that run, 2 for the second click of a double click, as the platform counts them by the user's own settings. Every click of a run is reported. A host that counts reports count: true in its capabilities.

  • Drops (SPEC.md §9.1): an element with drop in its data-on can be where a drag goes, t, without being dragged, and keeps its text selection. When such a t has data-steps, drag and dragend say where in it the pointer is, tx and ty, and a drag goes out when that step changes: a row with "0 1" splits into halves, a folder with "0 2" into before, into and after. A host that does both reports drop: true.

  • Copying (SPEC.md §10.2, §11): the new action copy, which the default keymap binds to Control+c and Meta+c (Command+c). A text field's keymap copies its selection; a selection elsewhere in a document is copied by the keymap of the element it starts in, which the host offers each key first, keyboard or not. With nothing selected, in a password field, or under Control+c=program, the key goes to the program: Control+c still interrupts. The terminal's own copy keys and menu copy whatever the keymaps say. A double click selects a word, a triple click a block, and Linux hosts put a selection in the primary selection. There is one selection across the host. No wire change and no capability.

  • Cutting, pasting and undo (SPEC.md §10.2, §11): a text field's default keymap binds Control+x and Meta+x to the new action cut, Control+v and Meta+v to paste, Control+z and Meta+z to undo, and Control+Z, Control+y and Meta+Z to redo. Each field keeps its own history, as a browser's does; a value the program sets clears it. cut with nothing selected, or in a password field, leaves the key to the program. The terminal's own paste types into a field that has the keyboard. A host that does not know the actions leaves the keys to the program, as before; no capability.

  • Mod and the edit shortcuts (SPEC.md §10.2, §10.4): a binding's Mod is Meta on macOS and Control elsewhere, reported as mod. The default keymap now binds each edit action once, to the host's edit shortcuts (Mod+a, Mod+c, Mod+x, Mod+v, Mod+z, Mod+Z, and Control+y where Mod is Control), which the user may change and the host reports as edit. Its other rows follow the platform: Control+ArrowLeft moves by a word where Mod is Control; Meta+ArrowLeft and Control+a go to the line's start where it is Meta. A Linux host no longer binds Meta+c, nor a macOS host Control+c. Breaks 0.2 in place.

  • Key sequences (SPEC.md §10.5): data-key="<leader> f n" gives an element a sequence that clicks or focuses it, with groups, <leader> and a timeout reported as keys, by myelin's rules. While one waits the host shows a which-key menu of the elements' labels.

  • Commands in the cells (SPEC.md §10.6): a=edit:take=copy,paste,… makes the edit shortcuts reach a program as the event command, with their key and, for a paste, the clipboard's text, per screen. The terminal's selection is still copied first, and its menus send commands without a key. a=menu shows a program's which-key menu, a msgpack map as myelin's levels are, reported as menu. caps says when mod, edit or keys change, and sends one that became absent empty ("edit": {}). command and caps belong to no surface.

Each SDK has the builder Reveal, the event's View (Areas and AtEnd, from in and end), Views, the event's Count, the drag's tx and ty, and the capabilities' Count and Drop.

Porting

A port that compiles can still be wrong. SDK.md Appendix C lists what ports got wrong; the two that cost most:

  • A clicked link is Link, not a field. 0.1's hotty-go read any detail through Event.Fields(), so programs found a link's href as Fields()["href"]. In 0.2 Fields holds a submit's fields only, and a link click carries Link (Href, URL). After a mechanical port, link clicks silently go nowhere.
  • Read the event's fields, never its detail. Code that unmarshals ev.Detail as JSON reads zero values from msgpack: a checkbox that is always unchecked. Use Checked, Value and the rest.

And a test that plays the host answers with msgpack capabilities naming 0.2 (hotty-go: hottytest.Body, or hottytest.Host), or the program under test falls back to text.

In each SDK

  • hotty-go (Go): the wire types are data, with functions beside them. m.Reply() is hotty.ReplyOf(m), m.Event() is hotty.EventOf(m), r.Caps() is the field r.Caps (nil when none) or hotty.CapsOf(body), r.Err() is hotty.Err(r), c.Supports(op) is hotty.Supports(c, op), and an event's accessors (Value(), Checked(), Link(), Size(), FitRows(), Drag(), Hover(), Area(), Fields()) are its fields. A host or relay answers a query with hotty.ReplyQuery.
  • hotty-ts (TypeScript) and the Python client: the reader keeps an int apart from a float; query() sends v.
  • hotty-lua: hotty.msgpack.decode(s) returns value, kinds, where kinds[t][k] tells a float, bin, array, time or extension apart from what Lua alone cannot.
  • hotty-blitz and xterm-addon-hotty, the hosts: they send msgpack and answer queries by version.

Starting the terminal…