Hooks and the Console REPL

useInput, useFocus, useViewport, useConsoleStats and window.text.

Hooks connect components to input, focus and the viewport.

useInput

useInput((e) => {  if (e.key === "up") setIndex((i) => i - 1);  if (e.ctrl && e.key === "p") setPaletteOpen(true);});
FieldDescription
key"up" "return" "escape" "tab"…
inputText produced by the key
ctrl meta shift altModifiers
stopPropagation()Stop lower-priority handlers

Browser shortcuts like ⌘⌥J and ⌘R always pass through.

usePaste and useWheel take the same options. ambient has no effect on useWheel.

OptionDescription
isActivefalse turns the handler off without unmounting (default true)
priorityHigher priorities run first (default 0). Focused components use FOCUSED_INPUT_PRIORITY (10)
globalKeep receiving input while a <Modal> outside this component is open (default false)
ambientDon't count this handler toward the focus hint (default false)

The Dev Overlay's shortcut (F2 or Ctrl+`) is ambient, so a page with no other keyboard input doesn't show the hint.

useInput((e) => {  if (e.key === "f2") toggleOverlay();}, { ambient: true });

usePaste and useWheel

usePaste((e) => insert(e.text));useWheel((e) => scrollBy(e.rows));

useKeyState (experimental)

@textjs/react/experimental exports useKeyState to track held physical keys by KeyboardEvent.code ("KeyW", "ArrowUp", "ControlLeft"). Polling keys.isDown(code) or reading keys.pressed does not trigger React re-renders. Pass a callback to react to key presses and releases as they happen.

import { useKeyState } from "@textjs/react/experimental";const keys = useKeyState((e) => {  if (e.code === "Space" && e.down) fire();});

Held keys are released automatically when the page loses focus. See Breaking changes for how experimental exports are versioned.

useViewport

const { cols, rows, focused, paused } = useViewport();

usePageFocus

usePageFocus() returns whether the page has focus. Keystrokes reach the app only while it does. It's the same value as useViewport().focused, but it re-renders only when focus changes.

const focused = usePageFocus();

While the page doesn't have focus and the app has a useInput or usePaste handler, a useKeyState hook or a focus target that Tab can reach, the console frame's last row becomes an amber bar that reads "Click the page to use the keyboard". Handlers marked ambient don't count. The hint doesn't appear in a terminal, in tests or in inline mirrors. Each time the hint appears, it blinks for about 6 seconds, unless the system's reduced motion setting is on. To decide yourself when it shows, set focusHint: false in text.config.ts or createConsoleRoot and render <FocusHint>:

<FocusHint active={editing} />

useConsoleStats

const { totalMs, spanCount } = useConsoleStats({ interval: 250 });

useApp

const { exit, pause, resume } = useApp();

The window.text REPL

Every app adds helpers to window.text. Type them in the DevTools prompt.

text.help()text.resize(120, 36)   // pin the grid sizetext.responsive(true)  // fit the docked console againtext.pause()text.stats()text.wireframe()text.copyText()

Loading on console intent (experimental)

@textjs/core/experimental exports onConsoleIntent for pages that download their console app only when a visitor wants it. It calls your function once, with the reason:

ReasonWhen
dockDevTools is docked when the page loads, or docks later. Text.js reads it from the window size
shortcutThe visitor presses F12 or Ctrl+Shift+J, I, C or K, or ⌘⌥J, I, C or K on a Mac. The browser still opens DevTools
engageA primary click on an element matching engageSelector, Enter or Space on a [data-textjs-engage] button, or a size picked in <ViewportControl> before the app starts. Clicks on links, buttons and form fields inside a matching element don't count unless they have data-textjs-engage too
sessionOne of the above already happened in this tab, so a reload boots at once
manualYou called trigger() on the returned handle
import { onConsoleIntent } from "@textjs/core/experimental";onConsoleIntent(  async (e) => {    const { start } = await import("./console");    start({ focus: e.reason === "engage" });  },  { prefetch: () => void import("./console") },);

e.away() says whether focus has left the page for DevTools since the intent. Pass it to createHostInput({ away: e.away() }) when the app's input starts, so a visitor who clicked back into the page while the app downloaded gets its keys with their next click on the page.

DevTools opened in its own window from a browser menu doesn't show up in the window size. Those visitors start the app from an engage element, and session keeps it starting on reloads in that tab.

Nothing fires automatically on touch screens. Call trigger() from your own control.

OptionDescription
engageSelectorElements that start the app when clicked (default [data-textjs-engage], [data-textjs-focus-pad], text-console). false adds no listeners
persistRemember intent for the tab in sessionStorage under CONSOLE_INTENT_KEY (default true)
prefetchCalled at most once, when the pointer or focus enters an engageSelector element
idlePrefetchAlso call prefetch when the browser is idle after load (default false)

prefetch never runs on touch screens, with Data Saver on, or on connections slower than 4G. canPrefetchConsole() returns the same check. The handle's dispose() removes every listener.

<ViewportControl> works before the console root exists. Picking a size saves it, fires engage so onConsoleIntent starts the app, and applies the size once root is set. See Breaking changes for how experimental exports are versioned.

Edit this page on GitHub