hotty SDKs
Version: 0.4, with SPEC.md 0.4
Status: Draft. It changes with SPEC.md (§15 there).
Licence: CC BY 4.0
Abstract
An SDK is a library through which a program speaks hotty in one language. This document says what every SDK provides, whatever its language: its components, what each one does, the options each takes and their defaults, and the names they go by. A program written against one SDK reads the same against another, once the language's idioms are applied.
An SDK has two layers. The wire layer encodes and decodes, and does no I/O; the conformance vectors check it. The SDK layer does the I/O for a kind of program: one that prints and exits, one built on a UI framework, a test. It is described here so that SDKs agree where a program would notice, and follow their language everywhere else.
The reference implementation is hotty-go. Its names are the canonical ones, and where it falls short of this document, §5.3 says so.
Contents
- Introduction
- Principles
- The wire layer
- The SDK layer
- Conformance
Appendices: A. Names · B. Rationale
1. Introduction
SPEC.md defines the wire: what a program and a host send each other. This document defines how a library presents the wire to a program. Where the two disagree, SPEC.md is right and this document is wrong.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in SPEC.md §2.
- Program, host, surface, placement, reply, event: as in SPEC.md.
- SDK: a library that speaks hotty for programs in one language.
- Wire layer: the part of an SDK that builds commands and reads what the host sends. It does no I/O (§2.1).
- SDK layer: the part that reads and writes a terminal, or works through whatever stands for one (§4).
- Canonical name: the name this document gives a component, an operation, an option or a field. It is hotty-go's, and an SDK spells it in its language's case (Appendix A).
- Environment: what the program runs in: a process with a terminal, a UI framework that owns the screen, an editor, a page.
2. Principles
2.1 Two layers
- The wire layer is pure. It MUST NOT do I/O, start threads, or read a clock: where it needs the time (the Detector, §3.8), the caller passes it in. Given the same input, it returns the same output.
- It depends on the language's standard library only, and on base64, zlib and msgpack where that library lacks them. msgpack writes a program's deltas and menus and reads what a host sends (SPEC.md §3.3). zlib is needed only to compress what a program sends, which is optional (§3.3); a host never compresses what it sends (SPEC.md §3.3).
- The SDK layer is built on the wire layer's public interface, so a program that needs something the SDK layer does not do can do it from the same parts.
2.2 Canonical concepts, idiomatic surface
- Every component, operation, option and field in §3 has a canonical name.
An SDK MUST provide each under a name a reader maps to it at a glance:
the canonical name in the language's case (
SetText,set_text,setText). Where the name is a keyword of the language, the SDK picks the nearest word (delisdeletein Python) and says so in its documentation. - Options take the language's form: functional options in Go, keyword arguments in Python, a table in Lua, a struct or a builder in Rust, an object in TypeScript. What an option is called, and its default, are canonical.
- Errors take the language's form: an error value, an exception, a result type. What they carry (a code and a detail, §3.9) is canonical.
- Absence takes the language's form: Go's zero value and
ok, Python'sNone, Lua'snil, Rust'sOption. The vectors write itnull. - Data takes the language's form: what a message or a control holds
(§3.2, §3.9) is a field or an accessor, and a question about it
(
Has) a method or a function. hotty-go's are plain structs with no methods but what its codec generates: a field (ev.Value), and a function beside the type (hotty.Light(caps)).
2.3 Shared defaults
The defaults in this document (quiet levels, timeouts, limits, the keymap of §3.10) are the same in every SDK, so a program behaves the same in every language. An SDK MUST NOT choose other defaults; it MAY let the program change them.
2.4 Protocol only
An SDK's contract is SPEC.md. Everything a program does with the protocol that the protocol does not define, such as rendering Markdown, highlighting code, drawing charts, or building forms from a schema, is presentation, and outside this document. An SDK MAY ship such helpers beside it. They depend on the SDK; the SDK never depends on them, and nothing in §3 or §4 needs them.
What a text field does with a key is the protocol's (SPEC.md §10.2), so keymaps (§3.10) and editing a field in cells as a host edits it (§4.6) are in this document.
2.5 Three renditions
Every program has three renditions (SPEC.md §14): surfaces on a host, cells on a terminal that is not one, and plain data when there is no terminal. An SDK makes the three branch points plain:
- No terminal: opening the terminal fails (§4.1). The program writes plain data.
- Not a host: detection says no (§3.8). The program draws in cells, and sends nothing of hotty but the query.
- A host: surfaces.
2.6 Leave the terminal clean
Replies and events arrive on the program's input, and whatever reads the input after the program exits, a shell for instance, reads them as typing. An SDK makes it easy, and its documentation says how, for a program to:
- read every reply its commands caused before it exits (a fence, §4.1);
- leave no surface that still reports events: create the ones it leaves on the screen detached, or detach them (SPEC.md §5.5).
2.7 Forward compatibility
As SPEC.md §15 has programs do:
- control keys the SDK does not know are kept in the message, and change nothing;
- an event of a kind it does not know is an event all the same: its kind is reported as it came;
- fields it does not know, in the capabilities or a detail, are ignored. Reading a message MUST NOT fail because of its body: a body that does not decode leaves absent what it would have filled (§3.9).
2.8 Environments
Some environments already do part of what the SDK layer does. A UI framework's input parser may hand over complete OSC sequences; an editor may own the terminal's output and hand its input over as events (§4.2). There, the SDK layer uses what the environment provides: it feeds complete sequences to the Decoder (§3.6) and writes through the environment's output. The wire layer is the same everywhere.
2.9 Updates cost what they change
A program that redraws a surface's tree, itself or through a framework,
diffs the new tree against the last one it sent and sends what changed as
the ops of one delta (SPEC.md §6.1): a text op where a cell's text
changed, 56 bytes, where morphing the whole tree of a small table sends 930.
A whole-tree morph without a target stays the fallback, and keeps what the
user was doing. A stylesheet that several documents share goes once, as a
resource (SPEC.md §7.1), not in each document. An SDK's helpers that build
documents (§2.4) do both.
3. The wire layer
An SDK MUST provide every component of this section.
| component | does | Go | vectors |
|---|---|---|---|
| constants (§3.1) | the protocol's numbers and named values | Number, Chunk, … |
— |
| Control (§3.2) | an ordered list of keys and values | Control |
— |
| Encode (§3.3) | one command as OSC sequences | Encode |
encode |
| commands (§3.4) | a builder for each action | Doc, Place, … |
build |
| names (§3.5) | surface names | ValidName, SurfaceName |
— |
| Decoder (§3.6) | OSC sequences into messages | Decoder |
wire, decode |
| Scanner (§3.7) | hotty sequences out of a byte stream | — | scan |
| Detector (§3.8) | whether the terminal is a host | internal/detect |
detect |
| messages (§3.9) | replies, events, capabilities, errors | Reply, Event, Caps, Error |
decode |
| keys (§3.10) | key names, keymaps, the SDK's keymap | ParseKey, DecodeKeys, KeyText, Keymap, TerminalKeys |
keys, keymap |
3.1 Constants and named sets
| name | value |
|---|---|
Number |
"7279", the OSC number |
Chunk |
4096, the most base64 bytes in one sequence |
MaxSize |
1000, the most columns or rows of a surface |
MaxName |
64, the longest surface name |
Version |
"0.4", the protocol version the SDK speaks, which its query lists (SPEC.md §4) |
DefaultMax |
16777216 (16 MiB), the most bytes a Decoder decodes one message to unless its Max says otherwise (§3.6) |
And these sets, each value under its canonical name:
- event kinds:
click,change,input,submit,press,focus,blur,fit,hover,view,dragstart,drag,dragend,command,caps(SPEC.md §9); - features:
passthrough,hover,count,menu(SPEC.md §4); - modifier bits:
ModShift1,ModAlt2,ModCtrl4,ModSuper8 (SPEC.md §9); - error codes:
EINVAL,ENOENT,ENOTARGET,EDETACHED,EQUOTA,EBUDGET,EVERSION(SPEC.md §3.6); - delta ops:
morph,inner,replace,append,prepend,before,after,remove,attr,unattr,text,var,reveal(SPEC.md §6.1); - quiet levels:
ReplyAlways0,ReplyOnError1,NoReply2 (SPEC.md §3.5).
3.2 Control
A command's control is an ordered list of keys and values: keys go out
in the order they were added. It reads a key's value and whether it is
present (get), and sets one, in place if present, at the end if not
(with).
3.3 Encoding
Encode(control, payload) returns one command as one or more OSC
sequences. Its output is fixed except where the protocol leaves a choice to
the program (compression), so the encode vectors compare bytes.
- Values are cleaned. Each character a value may not hold becomes one
_::,;,=, a control character, and any character outside ASCII, one_for each Unicode code point, and one for each byte that is not UTF-8 (SPEC.md §3.2). Keys are the SDK's own and are not cleaned. - Keys go out in order, as
key=valuejoined by:. - The payload is base64, the standard alphabet, padded. An empty payload
is sent as nothing, with no
;before it. - Compression is the program's choice. An SDK MAY compress a payload
with zlib and mark it
o=z, and then only a payload of 256 bytes or more, and only when compressing makes it smaller. It compresses at zlib's default level, 6: a stylesheet of 20 KB comes out a third smaller than at level 1, for about a millisecond.o=zfollows the other keys. oandmare Encode's. A control given with either goes out without it, and Encode sets them as this section says.- Chunks. A payload whose base64 is longer than 4096 bytes is split
into chunks of exactly 4096, the last one excepted. Nothing of 4096 or
less is chunked.
- The first chunk carries the whole control, then
m=1. - Every further chunk carries only
m:1, or0on the last.
- The first chunk carries the whole control, then
- Every sequence ends with ST (
ESC \).
3.4 Commands
A builder for each action returns the command's sequences, ready to write. Each takes the reply options of §3.4.2 and has the default quiet level shown, which SPEC.md leaves to the program; these are the levels every SDK uses.
| builder | sends | parameters | default q |
|---|---|---|---|
Query |
a=q:n=<n>:v=<Version>, late=1 with the Late option, then DA1 (ESC [ c) |
n | 0 |
Doc |
a=doc:s and the HTML; d=1 with the Detached option, scroll with Scroll |
surface, html | 1 |
Place |
a=place (§3.4.1) |
surface, placement | 1 |
PlaceAt |
ESC 7, ESC [ <y+1> ; <x+1> H, Place with C=1, ESC 8 |
surface, x, y, placement | 1 |
Hide |
a=hide:s |
surface | 2 |
Delta |
a=delta:s and its ops as msgpack (§3.4.3) |
surface, ops | 2 |
SetText |
Delta with one op, Text |
surface, target, text | 2 |
SetVar |
Delta with one op, Var |
surface, target, name, value | 2 |
SetAttr |
Delta with one op, Attr |
surface, target, name, value | 2 |
RemoveAttr |
Delta with one op, Unattr |
surface, target, name | 2 |
MorphTo |
Delta with one op, Morph |
surface, target, html | 2 |
Reveal |
Delta with one op, RevealOp: where is start, end, or empty for the nearest (SPEC.md §6.1) |
surface, target, where | 2 |
Res |
a=res:id:type and the data |
id, mime, data | 2 |
DelRes |
a=del:id |
id | 2 |
Del |
a=del:s |
surface | 2 |
DelAll |
a=del |
— | 2 |
Detach |
a=detach:s |
surface | 2 |
Focus |
a=focus:s[:t]; t only when given |
surface, target | 2 |
Blur |
a=blur:s |
surface | 2 |
Sync |
CSI ? 2026 h, the commands, CSI ? 2026 l (SPEC.md §6.3) |
commands | — |
WithdrawLate |
a=q:q=2: withdraws a query that asked for a late answer (SPEC.md §4) |
— | — |
Edit |
a=edit:take=<names>, the names joined by ,, in the order given; take= empty takes none (SPEC.md §10.6) |
names | 2 |
Menu |
a=menu and the menu as msgpack (below); with no menu, no payload, which leaves the program none (SPEC.md §10.7) |
menu | 2 |
MenuShow |
a=menu:show=1 and {"seq": [str]}, the keys typed; with none, no payload, which shows the top level (SPEC.md §10.7) |
keys | 2 |
MenuHide |
a=menu:show=0 |
— | 2 |
Touch |
a=touch and {"areas": [area]} as msgpack (below); with no areas, no payload, which leaves none (SPEC.md §9.6) |
areas | 2 |
DocandPlaceanswer errors, since a refused document or placement (EQUOTA,ENOENT) is something a program must hear. Everything else answers nothing unless asked: a program that sends deltas every frame would otherwise fill its input with replies.PlaceAtplaces a surface at screen cell (x, y), counted from 0, and leaves the cursor where it was: for a full-screen program, which places surfaces where it draws them. The coordinates have no leading zeros.Synctakes commands already built, and brackets them.Menutakes aMenuTree:LabelandItems, each item aMenuItemwithKey,Label,Name,EnabledandItems, its own. It writes one msgpack map, each value in the smallest of its type's forms:labelunless it is empty,items; each itemkey,labelandnameunless they are empty,enabled, anditemsunless there are none. A program on myelin names each binding of itsTreeand hands it over. The program keeps it within eight levels (SPEC.md §10.7).Touchtakes a list ofTouchArea, each withC,R,W,HandDrag: the screen's cells it covers, counted from 0, and"x","y"or"xy". It writes each area as a map of all five, in that order. The program keeps to 256 areas,WandHat least 1, and sends them again when what it drew moves, or the screen changes (SPEC.md §9.6).
3.4.1 Placement
A Placement says where and how a surface is shown (SPEC.md §5.2):
| field | key | default | sent |
|---|---|---|---|
Cols |
c |
— | always |
Rows |
r |
0, which is auto |
always: the number, or auto |
Window |
x, y, w, h |
none | all four, or none: none when absent, when all are 0, or when it shows the whole surface (rows given, and the window is 0, 0, cols, rows) |
Z |
z |
0 | when not 0 |
Press |
p |
absent | p=1 when on, p=0 when off, nothing when absent |
Fit |
f |
absent | as Press |
Hover |
v |
absent | as Press |
KeepCursor |
C=1 |
false | when true |
Press, Fit and Hover belong to the surface (SPEC.md §5.2), so each is
absent, on or off: a placement that leaves them absent leaves them as they
were. Go spells them hotty.On and hotty.Off, with the zero value
absent.
3.4.2 Reply options
Every builder but Query, Sync and WithdrawLate takes two options:
N(n)numbers the command and asks for its reply:n=<n>, and quiet level 0 unless the program givesQ.Q(q)sets the quiet level. AQgiven wins over the levelNimplies, whatever the order in which the program gives them.
q goes out only when the wire does not imply it (SPEC.md §3.5): it is
left out where it would be 0 with n, or 1 without, and for the query. So
Doc and Place carry no q, a delta carries q=2, and Doc with N(4)
carries n=4 alone.
Query takes one option of its own, Late (late=1): the program
asks for a late answer (SPEC.md §4).
Each key goes out once. Doc also takes Detached (d=1) and
Scroll(axes) (scroll=<axes>, SPEC.md §5.1): a bitmask of
ScrollVertical (1) and ScrollHorizontal (2). 0, the default, sends
no key; any other value goes out as given, for the host to judge.
3.4.3 Ops
Delta(surface, ops…) sends one delta of the ops given, in order (SPEC.md
§6.1). An Op has Op, Target, Key and Value, and a builder for
each op makes one:
| builder | op | parameters |
|---|---|---|
Morph |
morph |
target (empty: the whole tree), html |
Inner, Replace, Append, Prepend, Before, After |
the same, in lower case | target, html |
Remove |
remove |
target |
Attr |
attr |
target, name, value |
Unattr |
unattr |
target, name |
Text |
text |
target, text |
Var |
var |
target, name, value |
RevealOp |
reveal |
target, where |
The body is one msgpack map, {"ops": [op, …]}, each value in the smallest
of its type's forms, and each op a map with its keys in this order:
op;t, unless the target is empty;k, forattr,unattrandvar;v: for an op that takes one (SPEC.md §6.1), always, even empty; forreveal, only whenwhereisstartorend.
Targets and values are not cleaned (§3.3): they travel in the body, as the
document has them. A Delta of no ops is still sent, and the host answers
it EINVAL (SPEC.md §6.1).
3.5 Names
ValidName(s): whethersis a surface name: 1 to 64 ofA–Z,a–z,0–9,_and-.SurfaceName(s):smade one: each other character becomes_, it is cut to 64, and an empty name becomes_.
3.6 Decoder
A Decoder turns OSC sequences into messages, one at a time, joining chunked ones. A new Decoder is ready to use.
-
Feed(seq)takes one OSC sequence,ESC ] …, with or without its terminator (an environment may strip it, §4.2), and returns a result and, when the result iscomplete, the message:result when not_hottythe sequence is another OSC, or another number ( ESC ] 72790). The caller handles it; a chunked message in progress is not affectedpartiala chunk of a message that is not complete yet completea whole message invalida malformed message (SPEC.md §3.7), dropped -
The control is strict. It is SPEC.md §3.2's grammar: a key that does not parse, a value outside printable ASCII, a key that appears twice, or an empty control is malformed.
-
Chunks (SPEC.md §3.4):
- A control with only
mcontinues the message in progress; with none in progress, it is malformed. - Any other message arriving before the last chunk aborts the one in progress, which counts as malformed, and is then decoded as usual. If it is malformed itself, that makes two (SPEC.md §3.7).
- The pieces of base64 are joined before they are decoded, so a chunk may end anywhere in a base64 quantum.
- A control with only
-
The payload is base64, with or without padding, whitespace ignored. Any other byte makes the message malformed. A payload marked
o=zis inflated if the SDK has zlib; one that does not reports itinvalid. Hosts never compress (SPEC.md §3.3), so only a relay meets one. -
The message has the control without
mando, and the payload as bytes. -
Invalidcounts the malformed messages dropped so far. -
Maxis the most bytes one message decodes to, after base64 and zlib; 0 isDefaultMax. Past it the message isinvalid, counted once (SPEC.md §3.4, §3.7): a chunked one holds no more base64 thanMaxbytes could take, zlib's overhead included, and its further chunks are dropped up to its last, which reportsinvalidwithout counting again. Zlib stops inflating pastMax. A host that decodes with a Decoder reports itsMaxaslimits.message(SPEC.md §13).
3.7 Scanner
A Scanner cuts hotty sequences out of a byte stream, however the reads split it. An SDK that reads a raw terminal uses one; so does a relay, on a program's output. An environment that hands over complete sequences needs none (§2.8), but the wire layer has one all the same.
Feed(bytes)returns segments, in order:pass: bytes that are not hotty's, unchanged: keys, mouse reports, other OSCs, other answers;osc: one complete hotty sequence, fromESC ] 7279 ;to its terminator (ST or BEL), ready for the Decoder;da1: optionally, an answer to Primary Device Attributes,CSI ? <digits and ;> c, for an SDK that reads DA1 itself (§4.1). A Scanner that does not look for them passes them.
- It holds only what may still become a segment of its own: bytes that
are the start of
ESC ] 7279 ;(or of a DA1 answer, when it looks for them), and a hotty sequence in progress. Everything else goes out in the sameFeedthat brought it. So the segments do not depend on how the stream was split, and a key is never kept waiting for the next read. Holding()returns what it holds, andInSequence()whether that is a hotty sequence in progress (or one too long, being dropped), which the nextFeedgoes on with, rather than the start of a segment, which a program reading keys may flush after a moment as typing.- An ESC inside a hotty sequence that does not begin its ST ends the sequence unfinished: the sequence is dropped as malformed, and the ESC begins what comes next.
- A hotty sequence longer than 64 KiB (65536 bytes, its terminator not counted) is dropped as malformed, up to its terminator. No host sends one: chunks are at most 4096 bytes of base64.
Flush()ends the stream: what is held goes out aspass, except a hotty sequence in progress, which is dropped as malformed. A program reading keys flushes a lone ESC held at the end of a read, as the Escape key; that is the SDK layer's choice (§4.1).Invalidcounts the sequences it dropped.
3.8 Detector
A Detector decides whether the terminal is a host (SPEC.md §4). It is a
state machine with the time passed in, in milliseconds, so it behaves the
same in every SDK and environment, and the detect vectors check it step
by step.
Start(now)returns the query to send:Query(n),n1 unless the program chooses another, withLatewhen the program asks for a late answer (SPEC.md §4).DA1(now): a DA1 answer arrived. It returns whether the answer was detection's.Reply(r, now): a reply arrived; it returns whether it answers the query.Tick(now): the time is now.End(now): the input ended, or the caller gave up.- It exposes
State(detecting,nativeortext),Caps(the host's, when native),Decided,Done, andDeadline: when to callTicknext, or none once done. - Every input first lets the time pass: whatever was due at or before
nowhappens before the input is taken.
The rules, with their times:
| rule | |
|---|---|
a reply that answers the query (re=q, the query's n): a=ok, its capabilities decoding (§3.9), their V the SDK's Version |
the terminal is a host: native, decided, with its capabilities. Done when the DA1 behind it arrives, or 300 ms after the reply, or at 1500 ms, whichever comes first |
any other reply that answers the query: an error (EVERSION, or another), or capabilities that do not decode or name another version |
the terminal is no host the program can use (SPEC.md §4): text, decided. Done as for a host's reply |
| a DA1 before any reply | it may answer a question asked before the query (SPEC.md §4). It opens a grace of 150 ms: a reply within it still answers the query |
| the grace ends with no reply | text: not a host. Decided and done |
| 1500 ms with no answer at all | text. Decided and done |
| the input ends | text if nothing was decided. Done |
with Late, a reply that answers the query once the state is text, and makes the terminal a host (the first rule) |
a late answer: native, with its capabilities. Still decided and done |
- Which inputs are detection's. Every DA1 from the start until done is
detection's, and the SDK layer swallows it: it answers the query's fence or
an earlier question, and is no key. After done, a DA1 is not
detection's. A reply that answers the query,
a=okora=err, is detection's whenever it arrives, even after the state istext; a late one changes nothing, unless the query asked for it (Late), and then only the first that makes the terminal a host. Replies with anothern, and everything else, are not. - Decided and done differ only when the terminal answers the query: whether it is a host the program can use is known when its reply arrives, and the detection is over when nothing more of it will arrive. A program that must not wait, such as a framework's adapter (§4.3), acts when decided; one that hands the terminal on, when done.
3.9 Messages
A decoded Message has its control (a map of keys to values) and its
payload (bytes). It reads as a reply or as an event.
The body (SPEC.md §3.3) is decoded when the reply or event is read, each field as its type.
- Fields the SDK does not know are ignored (§2.7), whatever their type.
- A body nested more than 32 levels deep does not decode. An SDK stops reading there, so that no body can exhaust its stack: TinyGo's is 64 KB, and a few hundred levels crash it.
- Nor does a body that holds, anywhere in it, a field the SDK knows or not, what SPEC.md §3.3 has no host send: a nil, a map key that is not a string, a key given twice in one map (keys compared as the strings they are, whatever their form), a string that is not UTF-8, an int further than 2^53 − 1 from zero, or a timestamp of another size, with a second's nanoseconds or more, or with seconds further than 2^53 − 1 from 1970. Each is checked as the body is read, in every SDK alike, so a body decodes in all of them or in none: an int past 2^53 − 1 is not rounded where numbers are doubles, nor kept where they are not.
- A body that does not decode leaves absent everything it would have filled, and the rest of the message stands. That is a body that is not one msgpack map, one that breaks a rule above, or one with a field the SDK knows holding another type. What it fills is the capabilities, an error's code and detail, or what an event's detail carries.
Reply (SPEC.md §3.6), when the action is ok or err:
| name | from |
|---|---|
OK |
a=ok |
Re |
re: the action answered |
N |
n, as a number; absent when the command had none |
Surface |
s; absent when the command named none |
Cols, Rows |
c and r, as numbers, in a reply to place |
Code, Detail |
the error's body, when not OK |
Op |
i, as a number, in an error answering a delta: the op at fault (SPEC.md §6.1); absent otherwise |
Caps |
the capabilities, the body of an ok reply to q |
Err |
the reply as an error, carrying Code and Detail; none when OK |
Event (SPEC.md §9), when the action is ev: Surface (absent for
command and caps, which belong to no surface), Kind (as it came,
known or not), Target (t, empty when absent), and what the detail
carries, each absent when the event does not carry it:
| name | holds | for |
|---|---|---|
Value |
value, a string |
click, change, input |
Checked |
checked, a boolean |
change |
Fields |
fields: the pairs of a name and a value, in order (SPEC.md §9) |
submit |
Mods |
mods: the modifier bits (§3.1), 0 when absent |
click, press, dragstart, drag, dragend |
Link |
href, and url if present |
click on a link |
FitRows |
r |
fit |
Drag |
c and r; the steps x and y in the element dragged, and tx and ty in a t with drop (as TX and TY where the language capitalises), each absent when the detail has none (SPEC.md §9.1) |
dragstart, drag, dragend |
Hover |
c and r, or out |
hover |
Area |
area: c, r, w and h, the element's cells |
click, press |
Count |
count: the click's place in its run of clicks (SPEC.md §9), 2 for the second click of a double click; 1 for a click whose detail has none |
click |
View |
Areas, from in: each id in view to its cells as Area holds them, leaving out one whose area lacks a field, and none when in is absent; and AtEnd, from end, false when absent (SPEC.md §9.5) |
view |
Command |
Name, Key and Text, from name, key and text, each empty when absent, and HasText, whether text is there (SPEC.md §10.6); absent when the detail has no name |
command |
Changed |
the capabilities that changed, a Caps with only the fields the detail has (SPEC.md §4) |
caps |
The detail is read by the event's kind, so a form field named value is in
Fields and nowhere else. An event of a kind the SDK does not know carries
none of these, and its payload is in its message.
Caps (SPEC.md §4) has every field SPEC.md lists: V, CellCSS
(w, h, from cellcss), Scale, Scheme, Mod, Features,
Limits, Net, Edit, Host, Version. It also answers:
Has(feature): whetherfeatureis infeatures(SPEC.md §4);Light: the scheme islight;Merge(changed): the capabilities with the fields of acapsevent'sChangedin place of their own, and without those it empties ({},[]or"": absent now, SPEC.md §9).
An error carries Code and Detail, and its message names both.
3.10 Keys
What a text field does with a key (SPEC.md §10.2, §10.4), as data: so that a program that draws its fields in cells edits them as a host edits them on a surface, and the keymap it gives a surface is the one it uses in cells. And which keys any other element gives the program, or scrolls with, for a host.
-
ParseKey(name)reads a key's name as SPEC.md §10.4 has hosts read one, with myelin key names' default reading (Shift+Ctrl+A,PageDown,esc), and returns the key, its canonical name (ctrl+shift+a,page_down,escape). A name the reading refuses (empty,meta+a,ctrl+, a modifier given twice) is absent, and so is one withmod, which only a Platform reads (ReadKey). Its vectors are that document's: the keys section carries itsreadvectors. -
Platformis how a host reads bindings:Mod,ctrlorsuper, whatmodstands for (SPEC.md §10.4), andEdit, its edit shortcuts, from each action to its keys (SPEC.md §10.2). The zero Platform isctrlwith the default edit shortcuts.PlatformOf(caps)takes both from the capabilities, each the zero Platform's when absent.DefaultEdit(mod)returns the default edit shortcuts for amod, in SPEC.md §10.2's order, each key read:ctrl+shift+zandctrl+yforredowhere it isctrl. -
ReadKey(platform, name)reads a key's name asParseKeydoes, withmodstanding for the Platform's (myelin key names §4.4): onctrl,mod+sisctrl+s, andmod+ctrl+sis absent. The keys section carries that document'smodvectors for it. -
DecodeKeys(input)reads input from the terminal, the bytes a program reads, as SPEC.md §10.4 names it: a list with one entry for each key, its name, or absent for input that is no key it names (a mouse report, a sequence it does not know). It is pure: anESCat the end of the input isescape, and the caller passes each read whole (a framework's parser has already cut the input into keys). -
KeyText(key)is the text a key types in a field (SPEC.md §10.2): a character's, a letter withshiftas its capital, a space forspace, and none for any other key, or a key withctrl,altorsuper. -
Keymapis a set of bindings, key to action, read on a Platform.ParseKeymap(platform, value)reads adata-keysvalue as SPEC.md §10.2 has hosts read it, dropping the bindings a host ignores, each key withReadKey: onctrl,mod+aandctrl+aare one key, the later binding's.Format()writes one back: each key once, where it was first bound, as its last binding wrote it, with its last action, askey=action, separated by a space. A key is written canonical, and one written withmodkeeps it, first (mod+shift+z). -
Resolve(platform, multiline, values…)builds a field's keymap: SPEC.md's default keymap for the Platform'smod, with itsEdit, for aninputor for a multi-line field, then eachdata-keysvalue in turn, the root's first. A binding to a scroll action is left out where it stands, in its own value too: it neither acts nor overrides an earlier binding of its key (SPEC.md §10.2). -
Binds(key)says whether a keymap binds the key, or, for a key withshiftthat is not a character and that it does not bind, the key withoutshift(SPEC.md §10.2). A host that has two names for a press (SPEC.md §10.4) asks what follows of the physical one where the keymap binds it, and of the other one otherwise. -
Lookup(key)on a resolved keymap returns what the field does with the key: an action;insertfor a key it types (KeyText); or absent when the key is not the field's (it reaches the program, ortabmoves focus).tab,shift+tabandescapeare never the field's, nor a key bound toprogramorprefix, or to an action the field does not have. -
Selects(key)says whether the field selects with the key (SPEC.md §10.2, Shift selects):Lookupreturns a move for it, and its name hasshift(shift+arrow_left,ctrl+shift+end, notalt+<). A field then does the move withExtend(§4.6). -
Program(key)says whether a keymap gives the key to the program: it binds the key toprogramorprefix, with the fallback ofBinds. On an element that is not a text field, a host asks it of the element's keymap,ParseKeymapof itsdata-keysvalues joined with a space, the root's first, on the host's Platform, with no default keymap; a key it gives reaches the program before the element or a scroll uses it (SPEC.md §10.2, keys for the program). -
Prefix(key)says whether a keymap binds the key toprefix, with the same fallback without Shift. A host asks it of a keyProgramgives the program, and from then on gives the program every key (SPEC.md §10.5). -
Scroll(key)returns the scroll action a keymap binds the key to, with the same fallback, or absent. A host asks it of the keymap it asksProgramof, for a key the element does not use (SPEC.md §10.2, scrolling keys). -
TerminalKeysis the SDK's keymap, the same in every SDK (§2.3): the keys of Bubble Tea's text input and text area (bubbles), which a program that edits fields in cells is likely to share, but formod+a, which selects all, as in a GUI field and SPEC.md's default keymap:ctrl+aon Linux, while on macOSctrl+astays the line's start. A program puts it in thedata-keysof an element that holds its fields, and gives its cells rendition the same keymap,Resolve(platform, multiline, TerminalKeys).action keys char-backward,char-forwardarrow_left,ctrl+b;arrow_right,ctrl+fword-backwardalt+arrow_left,ctrl+arrow_left,alt+bword-forwardalt+arrow_right,ctrl+arrow_right,alt+fline-start,line-endhome;end,ctrl+edelete-char-backwardbackspace,ctrl+hdelete-char-forwarddelete,ctrl+ddelete-word-backwardalt+backspace,ctrl+w,ctrl+backspacedelete-word-forwardalt+delete,alt+d,ctrl+deletedelete-to-line-start,delete-to-line-endctrl+u;ctrl+kline-previous,line-nextarrow_up,ctrl+p;arrow_down,ctrl+npage-up,page-downpage_up;page_downinput-start,input-endalt+<,ctrl+home;alt+>,ctrl+endselect-allmod+anewlinectrl+mIt leaves
enterto SPEC.md's default (submitin aninput,newlinein a multi-line field). In the order of this table, it is the value vectors callterminal_keys.
4. The SDK layer
What a program does with a terminal depends on the kind of program, and on what it runs in. This section describes the components an SDK SHOULD provide for the kinds of program its language is used for, by what they do, then by the shape hotty-go gives them. An SDK follows its language and its frameworks in everything else.
4.1 Terminal
For a program that is not a full-screen framework's: a command that prints
documents and exits, one that asks a question, a chart that streams. Go:
hottyterm.Term.
- Opening.
Open(name)uses the process's terminal, whatever its standard output is (a pipe, say):/dev/tty, or the platform's console. With no terminal, it fails, and the program writes plain data.New(in, out, name, size, known)makes one from streams, for a test or an environment with no file (a terminal in a page).knowntells it the terminal is a host, and with what capabilities, without asking. - One reader. Everything the terminal sends is read once, by the terminal: keys, the mouse, pastes, replies and events. What a call waits for goes to that call (detection's answers, a fence's DA1, a numbered reply); everything else goes to one event stream, in the order it arrived. Calls that wait and a reader of events may run at once.
- Detection.
Detect()runs the Detector (§3.8), once: later calls return the first answer. It returns when the Detector is done, so that nothing of the detection is left for whatever reads the terminal next. A terminal made withknownanswers at once. - A late answer. A terminal opened with
Lateasks for one (SPEC.md §4). If it comes, it goes to the event stream as the host's capabilities, andDetect()answers with it from then on. Until it comes, the terminal stays in raw mode, or the answer would be echoed, andClose()withdraws the query (WithdrawLate), so that no answer reaches whatever reads the terminal next. - Requests.
Request(build)sends one command numbered withNand waits for its reply, 3 s at most. The number is never 1, which is the query's. An error reply is a reply, not an error; no reply in time is an error. - The fence.
Fence()waits until the terminal has taken everything written before it (it sends DA1, which terminals answer in order), 1 s at most, and returns the replies nothing read: the errors of commands sent withq=1, mostly. A program calls it before it exits. - The line.
LineStart()moves the cursor to the start of a line, where a surface placed at the cursor belongs: it asks where the cursor is (CPR), and writes CR LF if it is not at column 0. A terminal that does not answer within 1 s is taken to be at one. - Printing.
Print(name, html, placement)sends a document detached and places it at the cursor, in one write, and returns the surface's full name: for a document that stays in the scrollback among the program's output. - Writing.
Send(commands…)writes commands in one write, so that nothing the program prints lands between them. - Names.
Surface(name)makes a surface name of the program's own: a prefix, a dash and the name, made valid. The prefix is the program's name and a number unique on the terminal (its process id), or what the environment provides, so that two runs never share a surface. Every name is remembered (Surfaces()), andDetachAll()detaches them all. - Closing.
Close()gives the terminal back as it was (its mode, its file). It sends nothing but a withdrawal of a late query (above): what is left on the screen, and the replies not read, are the program's to settle first (§2.6). - Raw mode. Calls that wait for the terminal put it in raw mode for themselves. A program that reads keys asks for it once.
- The polyfill.
KittyGraphics()MAY be provided: whether a terminal that is not a host shows kitty graphics, so that a program can suggest a polyfill (SPEC.md §14).
4.2 Push environments
In some environments the SDK does not read the terminal: the environment
hands it what arrives, and writes for it. Neovim is one: its TUI parses the
input and raises TermResponse with each OSC, DCS and APC sequence, without
its terminator; a plugin writes to the terminal with nvim_ui_send. There:
- Input is pushed. The SDK layer receives sequences in callbacks, and feeds the Decoder and the Detector from them. It needs no Scanner.
- Waiting is asynchronous. Detection, requests and fences complete through callbacks or coroutines, with timers the environment provides.
- What is detection's is advisory: the environment may hand the same answer to other listeners. Others ask DA1 too, so a stale DA1 is routine, and the Detector's grace (§3.8) matters.
- The terminal can change. An editor can attach another UI, or come back from suspension; the SDK layer detects again, and sends documents again, when the environment says so.
- A late answer is the environment's, since detection is: one query asks for it for every session, and it is withdrawn while a shell has the terminal (on exit and suspension).
- What the environment lacks, the SDK lacks: CPR,
LineStart,Printand raw mode may not exist. Surface names take a prefix the program supplies. - Batches do not nest. Where the environment brackets its own frames in synchronized output, the SDK sends its commands within them, and does not add brackets of its own.
4.3 Session: a framework's adapter
For a full-screen program built on a UI framework, which redraws its frame
as its state changes. Go: hottytea.Session, for Bubble Tea. Other
frameworks get their own adapter, with the same responsibilities.
- Modes.
Detectinguntil the Detector decides, thenNative(a host) orText(not one). InTextthe Session sends nothing, and the program draws everything in cells. - A late answer. A Session made with
Lateasks for one (SPEC.md §4), through its terminal when the terminal detects. If it comes while the mode isText, the mode becomesNative, ready is sent again, and then the program is asked for a layout: a program that asks for a late answer draws either rendition whenever the mode says. Until it comes,Close()withdraws the query (WithdrawLate). - Layout. With each frame, the program says which surfaces it wants
where: name, rectangle on the screen, an optional clip (the part of the
screen the surface shows in, such as a scrolling region),
Keep,Z,Press,Fit,Hover,Scroll(the axes the host scrolls the document along, §3.4.2), and a function that returns the document. The Session sends only what changed:- a document once, attached, with its
Scroll, and again only when the host has lost it or itsScrollchanged; - a placement (
PlaceAt) when its rectangle, window or options changed, withPress,FitandHoveronly when they differ from what the surface has (SPEC.md §5.2), on or off; a surface clipped to nothing is as good as not wanted; - for a surface no longer wanted,
HideifKeep, elseDel.
- a document once, attached, with its
- Order. Its commands go out with the framework's frames, in the same stream, so that a placement lands with the cells around it.
- Relayout. A framework's renderer erases, scrolls and switches screens on its own, and a host drops placements with them (SPEC.md §5.4). The Session watches the output on its way out, and asks the program for a new layout when it sees: erase in display (2 or 3), scroll up or down, insert or delete lines, a new scrolling region, reverse index, leaving the alternate screen, or a full reset. An environment that knows its own redraws (an editor) uses its events instead.
- Lost documents.
ENOENTin answer to a placement means the host lost the document (a reset, the alternate screen); the Session sends it again with the next layout. - Limits. At most 48 surfaces by default, fewer when the host's
limits.surfacessays so, or when anEQUOTAsays so. To make room, it deletes the surfaces not wanted, the longest unseen first; under a limit the host set, a new surface waits for room. - Messages. What the host sends comes back to the program as the framework's messages: ready (the mode is known), an event, an error (a command refused), an ack (the ok of a numbered command), relayout, and pong (a round trip timed). Everything else passes as it was.
- Closing.
Close()deletes the surfaces the Session sent;DetachAll()detaches them instead, for a program that leaves them on the screen.
4.4 Test host
A host that runs inside a test, so that a program's tests need no terminal.
Go: hottytest.Host.
- The terminal around it. It answers what terminals answer: DA1, the cursor's position, the background colour, and, when asked to, the kitty graphics query. It tracks the alternate screen and full resets as a host does (SPEC.md §5.4), and keeps the cells the program printed. Made as a terminal that is not a host, it answers no hotty command, until the test makes it a host, as a multiplexer's terminal attaching does: then it answers a query that asked for a late answer and was not answered (SPEC.md §4), and every command after.
- The host. It keeps every surface's document with the delta operations
and the morph of SPEC.md §6, answers as SPEC.md §3.6 has hosts do, and
passes the host vectors'
send,inspectandkeysteps. It speaks the SDK'sVersiononly: it answers a query that lists it with its capabilities, and any other withEVERSION(SPEC.md §4). It lays nothing out:r=autogets an estimate the test may replace, nothing in it scrolls, so a key bound to a scroll action goes on as if its keymap did not bind it (SPEC.md §10.2), and it sends noview. Nor can it measure where in an element the pointer is: a drag of an element withdata-stepscarries the steps the test gives, in it and int(SPEC.md §9.1). A drag goes to the element the test names, which may be one withdropin itsdata-on. So it skips the vectors markedscroll,steps,dropandview(conformance/README.md), which every other host passes. - Strict by default. A malformed message, an
EINVAL, or a hotty command other than the query sent to a terminal that is not a host fails the test. A lenient host records them instead. - The user. It plays the user: typing, pressing a key as SPEC.md §10.2 has a host take it, clicking, filling a field, checking a box, choosing an option, submitting, pressing, dragging, and any event the test makes up.
- Inspection. The test reads each surface: its document, an element's attributes and text, the focused element, whether it is detached, and where it is placed. It also reads the touch areas the program sent for each screen, which it checks as a host does and keeps per screen, though no finger reaches them (SPEC.md §9.6).
An SDK that does not build one MAY test its SDK layer against another implementation's host, over a pipe.
4.5 Relay
A relay stands between programs and a host, as a multiplexer does (SPEC.md
§2): it answers the query with the host's capabilities, gives each
program's surfaces names of their own on the host, places them where the
program's pane is, and passes events back to the program whose surface they
came from. plx's pkg/hottyrelay is one. This section is reserved: what a
relay does will be specified here, with its own vectors.
4.6 Field
A text field's value, caret and selection, edited as SPEC.md §10.2's actions
edit them, for a program that draws its fields in cells (the second
rendition, §2.5). Go: hottyedit.Field, a module of its own for its
grapheme segmentation.
- The field.
Value,Caret(a count of characters, grapheme clusters, from the start),Anchor,Multiline,Password, andRows, the rows it shows (1 when absent), forpage-upandpage-down. - The selection runs from
Anchor, the end where it began, to the caret (SPEC.md §10.2). Nothing is selected whenAnchoris absent or at the caret.Selection()returns its start and end, equal when nothing is selected, for the rendition to draw.Select(anchor, caret)sets both, for a selection the user makes with the pointer, andSelect(p, p)puts the caret atpwith nothing selected. Go keeps the anchor behindAnchor()andSelect, since aFieldliteral's zero anchor would select. - Rows are lines. A field in cells does not wrap: each line is a row, and the place along it is a count of characters. A rendition that wraps moves the caret between its rows itself.
Do(action)does an action, and reports whether the value changed, so the program knows when to report aninput. A selection comes first, as SPEC.md §10.2 says: a delete deletes it, a move starts from its start or its end and ends it, andselect-allselects the whole value.submitandprogramare the program's, and change nothing, as arecopy,cut,paste,undoandredo: a Field holds no clipboard and no history, which a program that keeps them adds around it.Extend(action)does a move as Shift does it (SPEC.md §10.2): the anchor stays, or is set where the caret is, and the caret moves from where it is. An action that is not a move isDo's.- A run of row moves,
line-previous,line-next,page-upandpage-down, withDoorExtend, keeps the place along the row it started from; any other action, typing, orSelectends it. Type(text)types text at the caret, in place of the selection.Key(keymap, key), where an SDK has it, does what the keymap says of the key: types itsKeyText, does its action, or, whenSelects(key), extends with it.
An SDK whose language has no grapheme segmentation MAY count code
points instead, CR LF still one, and says so (§5.3); it then skips the
vectors that require graphemes. So does its DecodeKeys, for a character
of more than one code point.
5. Conformance
5.1 Vector sections
conformance/vectors.json holds the vectors of hosts and of SDKs
(conformance/README.md):
| section | checks | runs it |
|---|---|---|
vectors |
a host: replies, documents, events | hosts, test hosts (§4.4) |
wire |
decoding a stream of commands | hosts and SDKs |
build |
the builders, their defaults and options (§3.4) | SDKs |
encode |
Encode: cleaning, chunks, compression (§3.3) |
SDKs |
decode |
the Decoder and the messages (§3.6, §3.9) | SDKs |
scan |
the Scanner (§3.7) | SDKs |
detect |
the Detector (§3.8) | SDKs |
keys |
key names and DecodeKeys (§3.10, SPEC.md §10.4) |
hosts and SDKs |
keymap |
ParseKeymap, Resolve, Lookup, Program, Scroll and TerminalKeys (§3.10, SPEC.md §10.2) |
hosts and SDKs |
edit |
the actions on a value: a Field (§4.6) | hosts, and SDKs with a Field |
An SDK conforms when it meets every MUST of this document and passes
every SDK section, edit where it provides a Field.
5.2 Runners
- Names in the vectors are canonical, in snake case: a runner maps
set_text,keep_cursorandfit_rowsto its own names. That mapping is what keeps SDKs aligned. requires. A vector that needs something an SDK may not have yet names it inrequires. A runner declares what it implements, and skips the vectors whoserequiresit lacks, so the list of what an SDK lacks is its runner's, not a list kept by hand.- A copy, checked. An SDK keeps a copy of
vectors.jsonin its tests, and a test that compares it with the spec's when a checkout is at hand (hotty-go:make vectors,TestVectorsAreCurrent). The runner checks that the vectors' version is the SDK's.
5.3 Implementations
| SDK | language | wire layer | SDK layer |
|---|---|---|---|
| hotty-go | Go | the reference; complete, with what a relay writes as a host (replies, events, Message.Placement, CursorBelow) |
hottyterm, hottytea, hottytest, hottyedit (§4.6) |
clients/python (this repository) |
Python | complete | Hotty, for the examples |
| hotty-lua | Lua | complete, under LuaJIT, Lua 5.1, Neovim and gopher-lua (plx's) | hotty.nvim (§4.2) |
hotty-go's runners (vectors_test.go, and hottyedit's for edit) run
every SDK section and skip nothing. hotty-lua's (tests/vectors.lua, under
each of its runtimes) and the Python client's count code points (§4.6) and
skip only the vectors that require graphemes. plx's sdk/hotty (Go) is a
relay's codec, to be replaced by hotty-go's, which has all that plexos
listed it uses. hotty-blitz's hotty-wire (Rust) is a host's; its tests
run the keys, keymap and edit sections.
Appendix A. Names
The canonical names, in each language's case. Go's are hotty-go's.
| canonical | Go | Python | Lua | Rust | TypeScript |
|---|---|---|---|---|---|
Encode |
hotty.Encode |
hotty.encode |
hotty.encode |
hotty::encode |
encode |
Query |
hotty.Query |
query |
query |
query |
query |
Doc |
Doc |
doc |
doc |
doc |
doc |
Place, PlaceAt |
Place, PlaceAt |
place, place_at |
place, place_at |
place, place_at |
place, placeAt |
Hide, Delta |
Hide, Delta |
hide, delta |
hide, delta |
hide, delta |
hide, delta |
Op and its fields |
hotty.Op{Op, Target, Key, Value} |
Op(op, target, key, value) |
a table with those keys | Op { op, target, key, value } |
{ op, target, key, value } |
Morph, Inner, Replace, Append, Prepend, Before, After, Remove, Attr, Unattr, Text, Var, RevealOp |
hotty.OpMorph, hotty.OpInner, …, hotty.OpText, hotty.OpVar, hotty.OpReveal (hotty.Text is a detector's state) |
morph, inner, …, text, var, reveal_op |
as Python | as Python | morph, inner, …, text, varOp, revealOp |
SetText, SetVar, SetAttr, RemoveAttr, MorphTo, Reveal |
the same | set_text, set_var, set_attr, remove_attr, morph_to, reveal |
as Python | as Python | setText, setVar, setAttr, removeAttr, morphTo, reveal |
Res, DelRes |
Res, DelRes |
res, del_res |
res, del_res |
res, del_res |
res, delRes |
Del, DelAll |
Del, DelAll |
delete, del_all |
del, del_all |
del, del_all |
del, delAll |
Detach, Focus, Blur, Sync |
the same | detach, focus, blur, sync |
as Python | as Python | detach, focus, blur, sync |
WithdrawLate |
hotty.WithdrawLate |
withdraw_late |
withdraw_late |
withdraw_late |
withdrawLate |
Edit, Menu, MenuShow, MenuHide |
hotty.Edit, hotty.Menu, hotty.MenuShow, hotty.MenuHide |
edit, menu, menu_show, menu_hide |
as Python | as Python | edit, menu, menuShow, menuHide |
Touch, TouchArea and its fields |
hotty.Touch, hotty.TouchArea{C, R, W, H, Drag} |
touch, TouchArea(c, r, w, h, drag) |
touch, tables with those keys |
touch, TouchArea { c, r, w, h, drag } |
touch, { c, r, w, h, drag } |
MenuTree, MenuItem and their fields |
hotty.MenuTree{Label, Items}, hotty.MenuItem{Key, Label, Name, Enabled, Items} |
MenuTree(label, items), MenuItem(key, label, name, enabled, items) |
tables with those keys | MenuTree { label, items } |
{ label, items } |
Placement and its fields |
Placement{Cols, Rows, Window, Z, Press, Fit, Hover, KeepCursor} |
Placement(cols, rows, window, z, press, fit, hover, keep_cursor) |
a table with those keys | Placement { cols, rows, … } |
{ cols, rows, window, z, press, fit, hover, keepCursor } |
a flag on, off or absent (Press, Fit, Hover) |
hotty.On, hotty.Off, the zero value |
True, False, None |
true, false, nil |
Option<bool> |
true, false, undefined |
N, Q, Detached, Scroll, Late |
hotty.N(n), hotty.Q(q), hotty.Detached(), hotty.Scroll(axes), hotty.Late() |
n=, q=, detached=, scroll=, late= |
{ n = …, q = …, detached = …, scroll = …, late = … } |
builder methods | { n, q, detached, scroll, late } |
ScrollVertical, ScrollHorizontal |
hotty.ScrollVertical, hotty.ScrollHorizontal |
SCROLL_VERTICAL, SCROLL_HORIZONTAL |
SCROLL_VERTICAL, SCROLL_HORIZONTAL |
SCROLL_VERTICAL, SCROLL_HORIZONTAL |
SCROLL_VERTICAL, SCROLL_HORIZONTAL |
Decoder.Feed, Invalid, Max |
(*Decoder).Feed, .Invalid, .Max |
Decoder.feed, .invalid, .max |
decoder:feed, .invalid, hotty.decoder({ max = … }) |
Decoder::feed, .invalid, .max |
Decoder.feed, .invalid, .max |
DefaultMax |
hotty.DefaultMax |
DEFAULT_MAX |
DEFAULT_MAX |
DEFAULT_MAX |
DEFAULT_MAX |
Scanner, Detector |
Scanner, Detector |
Scanner, Detector |
scanner, detector |
Scanner, Detector |
Scanner, Detector |
Holding, InSequence |
(*Scanner).Holding, .InSequence |
Scanner.holding, .in_sequence |
scanner:holding, :in_sequence |
Scanner::holding, ::in_sequence |
Scanner.holding, .inSequence |
Reply, Event, Caps |
Reply, Event, Caps |
the same | tables with the fields in snake case | the same | the same |
FitRows, Mods, CellCSS |
FitRows, Mods, CellCSS |
fit_rows, mods, cell_css |
as Python | as Python | fitRows, mods, cellCss |
ModShift, ModAlt, ModCtrl, ModSuper |
hotty.ModShift, … |
MOD_SHIFT, … |
MOD_SHIFT, … |
MOD_SHIFT, … |
MOD_SHIFT, … |
a message as a Reply or an Event, Err |
hotty.ReplyOf(m), hotty.EventOf(m), hotty.Err(r) |
m.reply(), m.event(), r.err() |
m:reply(), m:event(), r:err() |
methods | m.reply(), m.event(), r.err() |
Has, Light, Merge |
hotty.HasFeature(c, feature) (hotty.Has asks a control), hotty.Light(c), hotty.Merge(c, changed) |
c.has(feature), c.light(), c.merge(changed) |
c:has(feature), … |
methods | c.has(feature), … |
Command, Changed |
Event.Command, Event.Changed |
e.command, e.changed |
as Python | methods | e.command, e.changed |
a control's get and with |
hotty.Get(ctl, k), hotty.With(ctl, k, v) |
the SDK's own | the SDK's own | the SDK's own | the SDK's own |
ParseKey, DecodeKeys, KeyText |
hotty.ParseKey, hotty.DecodeKeys, hotty.KeyText |
parse_key, decode_keys, key_text |
as Python | as Python | parseKey, decodeKeys, keyText |
Keymap, ParseKeymap, Resolve, Binds, Lookup, Program, Prefix, Scroll, Format |
hotty.Keymap, hotty.ParseKeymap, hotty.Resolve, (Keymap).Binds, .Lookup, .Program, .Prefix, .Scroll, .Format |
Keymap, parse_keymap, resolve, Keymap.binds, .lookup, .program, .prefix, .scroll, .format |
parse_keymap, resolve, keymap:binds, :lookup, :program, :prefix, :scroll, :format |
as Python | Keymap, parseKeymap, resolve, .binds, .lookup, .program, .prefix, .scroll, .format |
TerminalKeys |
hotty.TerminalKeys |
TERMINAL_KEYS |
TERMINAL_KEYS |
TERMINAL_KEYS |
TERMINAL_KEYS |
Platform, PlatformOf, DefaultEdit, ReadKey |
hotty.Platform{Mod, Edit}, hotty.PlatformOf, hotty.DefaultEdit, (Platform).ReadKey |
Platform(mod, edit), platform_of, default_edit, read_key |
a table { mod = …, edit = … }, platform_of, default_edit, read_key |
as Python | { mod, edit }, platformOf, defaultEdit, readKey |
Field, Do, Type |
hottyedit.Field, (*Field).Do, .Type |
Field, .do, .type |
field, :do_action (do is a keyword), :type |
Field, do_action, type_text |
Field, .do, .type |
In the vectors, every name is in snake case: place_at, keep_cursor,
fit_rows, cell_css.
Appendix B. Rationale
- Why two layers. What goes on the wire is the same in every program; how a program reads its terminal is not. A pure wire layer can be checked as data, in every language, by the same vectors, and an environment that does its own I/O (an editor, a page) still uses all of it.
- Why shared defaults. A program ported from one language to another should not start filling its input with replies, or wait a different time for an answer. Defaults are behaviour, and behaviour is what the program's user sees.
- Why the Scanner and the Detector are in the wire layer. Both are pure: bytes in, segments out; inputs and times in, a decision out. Both are where SDKs went wrong first: a reply cut across two reads read as keys, a DA1 left for the shell, a terminal that is a host taken for one that is not. As state machines with vectors, they are right once, in every language.
- Why
DocandPlaceanswer errors and nothing else does. A refused document or placement leaves a hole in the program's screen, which the program must hear about; a delta that names a missing element is the program's bug, which a test finds. Asking for every reply would fill the input with them. - Why a
Qgiven wins overN. Options in most languages have no order: keyword arguments, a table, a struct. The rule that needs no order is the one every SDK can follow. - Why the SDK has a keymap of its own. SPEC.md's default keymap is small, so that a program that names no keys loses none. A program that edits fields in cells, though, has a keymap already, usually its framework's, and wants its surfaces' fields to edit the same. Bubble Tea's is the one the reference SDK's programs have; one keymap shared by every SDK keeps a program the same in every language (§2.3).
- Why a body decodes whole or not at all. A codec generated from the types (Go's msgp, Rust's serde) stops at the first field of the wrong type. A rule every SDK can keep is the one such a codec keeps, and a host that sends a wrong type fails the vectors instead.
- Why presentation is out. hotty carries any HTML. How one program presents its content is that program's choice, and an SDK that builds documents would make it for every program, in each language differently.
Appendix C. Moving from 0.2
Informative. hotty 0.3 breaks 0.2 (SPEC.md §15), as a fold of what 0.2 grew. A program and its SDK move together, and its tests' fake hosts with them. These are the places a port goes wrong:
- A delta is a list of ops.
Delta(surface, op, target, key, payload)becameDelta(surface, ops…), the ops built byText,Attrand the rest (§3.4.3; in GoOpText,OpAttr, …). A frame's changes to one surface go in one delta, which the host applies as one; the one-op shorthands (SetText, …) are still there for a single change. - The flags stay on the surface.
p,fandvhold until the program turns them off (SPEC.md §5.2). A program that placed a surface withoutPressto stop its presses now sendsPress: Off. A Session (§4.3) sends them when they change. - Ask by feature.
Supports,Sends,Drags,Hovers,Viewsand the capabilities'Passthrough,Scroll,Steps,Drop,CountandMenuare gone: everything a 0.3 host does, it does, andHasFeature(c, feature)answers forpassthrough,hover,countandmenu. - The cell is in CSS pixels.
Cell, in device pixels, is gone, andCellCSSis the host's own field. There is noresize: a new font is acapsevent with the newCellCSS, whichMergefolds in. - A submit's fields are pairs, in order: two fields of one name are two pairs. Code that looked a name up in a map looks it up in the pairs.
- Modifiers are bits. A drag's
Keyslist isMods, and clicks and presses carry it too:Mods & ModShiftfor Shift. - An empty target is absent.
Targetreads the same, empty, but a host'st=with nothing after it is gone from the wire, and a fake host that sends one is out of date. - No key sequences in the host.
data-keyand the capabilities'Keysare gone. A program binds its leader toprefix(SPEC.md §10.5), reads the keys after it, and sendsa=focuswhen the sequence ends. Its menu goes toMenuandMenuShow, as in the cells. - A menu is a tree.
MenuLevelisMenuTree, with noSeq, and an item has aNameand its ownItemsin place ofGroup. A program sends its tree when what it can do changes,MenuShow(seq)for each key of a sequence, andMenuHide()when it ends, where 0.2 sent a level for each key and hid it with an emptyMenu. A pick, from a multiplexer's palette or an agent, reaches it ascommandwith the item's name. - Errors only, unless asked. A builder sends no
qwhere the wire implies it, andN(n)alone asks for the reply. A fake host must answer a command withoutqonly with an error.
Appendix D. Moving from 0.3
Informative. hotty 0.4 breaks 0.3 (SPEC.md §15): it names keys as myelin key names 0.1 does (SPEC.md §10.4). A program and its SDK move together, and its tests' fake hosts with them. These are the places a port goes wrong:
- Every key's name changes.
Control+aisctrl+a,Metaissuper,ArrowLeftisarrow_left,PageDownispage_down,EnterisenterandSpaceisspace. A program'sdata-keys, its menu's keys andseq, and the keys its tests expect all move. myelinkey'sParseHottyreads the 0.3 names, for a script that ports them. mod, notMod. A binding writesmod+s, and one withmodandctrlorsuperas well is refused. The capabilities'modis"ctrl"or"super", andeditnames its keys the new way.ParseKeyrefusesmod: a binding read on a host's Platform goes throughReadKey.- A capital is no Shift. A host reads
Gasg, so a binding of Shift with a letter writesshift+g, and a 0.3Control+Aisctrl+shift+a. The text a key types is itsKeyText:shift+atypesA. - Shift falls back only from a key that is not a character. An
unbound
shift+arrow_leftis looked up asarrow_left, butshift+gis a key of its own. - A press can have two names. A host that sees the key itself names
it by its place and by what it types (
ctrl+key_w,ctrl+w), and looks the place up first (Binds). A host that reads bytes names what they carry. - The modifier bits keep their values, under myelin's names:
ModControlisModCtrl, andModMetaisModSuper.