Events and Input

Input arrives as DOM events, through addEventListener.

Keyboard #

document.addEventListener("keydown", (ev) => {
	if (ev.key === "j") select(selected + 1);
	if (ev.key === "Enter") open(rows()[selected]);
});

Escape sequences from the terminal are decoded into KeyboardEvents with key, ctrlKey, altKey, and shiftKey set.

What a terminal can report #

A terminal delivers bytes, not key states, and several distinct keystrokes arrive as the same byte. These limits come from the terminal, not from TermDOM, and they apply to every terminal application.

Editing chords in text fields #

Because a terminal user's hands expect them, <input> and <textarea> bind the readline motions and cuts as their default action, and an author's preventDefault on keydown suppresses them like any other:

ChordEffect
Ctrl+A / Ctrl+EStart / end of the line
Ctrl+B / Ctrl+FBack / forward one character
Ctrl+K / Ctrl+UCut to the end / start of the line
Ctrl+WCut the word before the caret
Ctrl+DDelete forward
Ctrl+JInsert a newline, in a <textarea>

Ctrl+A is a caret motion here rather than the browser's select-all, which has no terminal equivalent to inherit.

Mouse #

Mouse events dispatch at the element under the cell: mousedown moves focus, click clicks, wheel scrolls. Coordinates are in cells.

row.addEventListener("click", () => open(row.dataset.path!));

A terminal reports a mouse position only while a button is held, or when motion reporting is on, so :hover is not implemented. Mouse reporting also belongs to the application while it runs: a terminal's own text selection is usually available by holding Shift, which the terminal handles itself and TermDOM never sees.

Focus #

Tab traverses focusable elements in document order, :focus styles apply, and element.focus() works. Typing goes to the focused element.

Form controls #

<input> (text, checkbox, radio), <textarea>, <select> and <button> are all implemented, and they fire input and change events:

<div class="field">
	<div class="label">Name</div><input id="name">
</div>
field.addEventListener("input", updatePreview);

The controls are UA shadow trees, so ::placeholder and ::part() styling apply. The caret is the real terminal cursor, and IME composition works: CJK input methods compose in the field. <input type="password"> masks its value.

Selection and the clipboard #

Drag to select, in the document or inside a field. Selection is styled through ::selection. Copying is explicit: navigator.clipboard.writeText(text) carries the text to the system clipboard over OSC 52, which travels in-band and works across SSH. The terminal's own select-to-copy remains available as Shift+drag, which bypasses mouse reporting.

Scrolling and the camera #

Output starts at the command line and flows down. When the document outgrows the terminal, earlier rows scroll into the terminal's scrollback.

A camera scrolls over the document:

window.scrollTo(0, 0);
element.scrollIntoView();

window.scrollY and pageYOffset report where the camera is.

Fullscreen #

await element.requestFullscreen();

requestFullscreen() takes the alternate screen and applies :fullscreen styles. Exiting restores the main screen and scrollback.

Resizing #

A terminal resize re-evaluates @media rules and fires change on live MediaQueryList objects:

const wide = window.matchMedia("(min-width: 80ch)");
wide.addEventListener("change", relayout);
Edit this page on GitHub