Skip to content

@jsvision/ui / EventLoop

Interface: EventLoop

Defined in: ui/src/event/types.ts:109

The event loop: a host-agnostic engine that owns a render root, routes input and commands, manages focus, commands, and modal windows, and paints exactly one coalesced frame per dispatch tick.

You drive it entirely through dispatch() (decoded input) and the imperative methods below — no terminal is required, which is what makes it testable and embeddable. To connect it to a real terminal, either wire the on*/write* sinks to a host yourself, or use createApplication, which does that wiring for you. Create one with createEventLoop.

Properties

onCaret?

optional onCaret?: (cell) => void

Defined in: ui/src/event/types.ts:212

Called right after onFrame at every frame with the focused view's absolute caret cell, or null when nothing is focused or the focused view wants no visible caret. Wire it to move the terminal's hardware cursor. It reads the persisted view origin, so the caret position stays correct even on a partial repaint that skips the focused view. undefined ⇒ no caret output.

Parameters

cell

Point | null

Returns

void


onFrame?

optional onFrame?: (buffer) => void

Defined in: ui/src/event/types.ts:205

Called with the composed buffer after every frame (each dispatch tick, resize, and mount) so a host can paint it. Set this to host.render (or your own writer) after the host exists; createApplication wires it for you. While unset, frames are still composed but not pushed — headless tests read renderRoot.buffer() directly.

Parameters

buffer

ScreenBuffer

Returns

void


onResize?

optional onResize?: (size) => void

Defined in: ui/src/event/types.ts:219

Called inside resize after the reflow settles the new geometry, so a handler can re-anchor viewport-sized chrome against fresh bounds (the app uses it to re-fit maximized windows and re-anchor the open menu). The loop repaints once more afterward so the adjustment is visible. undefined ⇒ resize only reflows.

Parameters

size

Size2D

Returns

void


popupHost?

optional popupHost?: PopupHost

Defined in: ui/src/event/types.ts:237

The host that anchored dropdown popups (menus, combo boxes, date/color pickers) mount into. createApplication wires it to the app's overlay + focus. undefined ⇒ no host, so opening a dropdown is a safe no-op; a standalone Dialog can supply its own.


renderRoot

readonly renderRoot: RenderRoot

Defined in: ui/src/event/types.ts:111

The render root the loop builds and owns — read renderRoot.buffer() to inspect the composed frame.


writeClipboard?

optional writeClipboard?: (seq) => void

Defined in: ui/src/event/types.ts:231

Called with a ready-to-write terminal clipboard sequence when a control copies/cuts text (the loop encodes and sanitizes it for you). Wire it to your output stream. undefined ⇒ clipboard writes are dropped, so copy/cut is a safe no-op headlessly.

Parameters

seq

string

Returns

void

Methods

dispatch()

dispatch(event): void

Defined in: ui/src/event/types.ts:128

Feed one decoded input event (key/mouse/wheel/paste) into the loop; it routes and repaints in one tick.

Parameters

event

AppEvent

Returns

void


emitCommand()

emitCommand(command, arg?): void

Defined in: ui/src/event/types.ts:142

Emit a command, routing it to any handler. Dropped silently if the command is disabled.

Parameters

command

string

arg?

unknown

Returns

void


enableCommand()

enableCommand(command, on): void

Defined in: ui/src/event/types.ts:144

Enable or disable a command. While disabled, emitCommand for it is dropped.

Parameters

command

string

on

boolean

Returns

void


endModal()

endModal<R>(result): void

Defined in: ui/src/event/types.ts:153

Close the top-most modal, restore the previously focused view, and resolve its execView promise with result.

Type Parameters

R

R

Parameters

result

R

Returns

void


execView()

execView<R>(view): Promise<R>

Defined in: ui/src/event/types.ts:151

Open view as a modal: input is captured to its subtree until it closes. Returns a promise that resolves with the value passed to endModal. await it to run a dialog and read its result.

Type Parameters

R

R

Parameters

view

View

Returns

Promise<R>


focusInto()

focusInto(view): void

Defined in: ui/src/event/types.ts:138

Focus into a container: restore its last-focused child, or focus its first focusable descendant.

Parameters

view

View

Returns

void


focusNext()

focusNext(): void

Defined in: ui/src/event/types.ts:132

Move focus to the next focusable view in traversal order, wrapping at the end.

Returns

void


focusPrev()

focusPrev(): void

Defined in: ui/src/event/types.ts:134

Move focus to the previous focusable view in traversal order, wrapping at the start.

Returns

void


focusView()

focusView(view): void

Defined in: ui/src/event/types.ts:136

Focus exactly view. A no-op if view is not currently focusable.

Parameters

view

View

Returns

void


getFocused()

getFocused(): View | null

Defined in: ui/src/event/types.ts:140

The currently focused view, or null if nothing is focused.

Returns

View | null


isCommandEnabled()

isCommandEnabled(command): boolean

Defined in: ui/src/event/types.ts:146

Whether a command is currently enabled. Commands are enabled by default until disabled.

Parameters

command

string

Returns

boolean


mount()

mount(root): void

Defined in: ui/src/event/types.ts:113

Mount a view tree as the loop's root and paint the first frame. Call once before dispatching.

Parameters

root

View

Returns

void


onCommand()

onCommand(command, handler): () => void

Defined in: ui/src/event/types.ts:186

Register a handler for a named command; returns a function that unregisters it. Every handler registered for a command runs (in registration order) when that command is emitted, and a handled command is consumed there — a downstream view matching the same command does not also receive it.

Handlers run in the pre-process phase, so an onCommand handler fires before a focused view could handle the same command. One exception: while a modal (e.g. a Dialog) owns the dispatch scope, commands are confined to the modal subtree, so a general onCommand handler does not fire until the modal closes.

Parameters

command

string

The command name to handle.

handler

() => void

Called when the command is emitted.

Returns

A function that unregisters this handler (idempotent).

() => void


refreshCaret()

refreshCaret(): void

Defined in: ui/src/event/types.ts:225

Re-send the current caret cell to onCaret out of band. run() calls it once after the first frame (which is painted directly, not through a tick) to position the initial cursor. A no-op when onCaret is unset.

Returns

void


releaseCapture()

releaseCapture(): void

Defined in: ui/src/event/types.ts:198

Release the pointer capture. A no-op if nothing is captured.

Returns

void


resize()

resize(size): void

Defined in: ui/src/event/types.ts:130

Resize the viewport: reflow the tree and paint exactly one frame.

Parameters

size

Size2D

Returns

void


setAcceleratorMode()

setAcceleratorMode(on): void

Defined in: ui/src/event/types.ts:160

Turn accelerator mode on or off. When on, every reachable ~X~ hotkey is underlined and a bare letter fires the matching accelerator like Alt+letter. The reveal key (default F12) toggles this for you; call it directly to arm/dismiss the mode programmatically. A no-op when the feature is disabled (revealKey: null).

Parameters

on

boolean

Returns

void


setCapture()

setCapture(view): void

Defined in: ui/src/event/types.ts:196

Capture the pointer to view: while captured, all mouse/wheel events go to view (with view-local ev.local coordinates), bypassing hit-testing and focus-on-click — this is how a drag or resize keeps tracking even after the cursor leaves the affordance. Setting a new target replaces any current one; capture is released automatically when a modal opens/closes or the target unmounts.

Parameters

view

View

Returns

void


setTheme()

setTheme(theme): void

Defined in: ui/src/event/types.ts:171

Replace the active theme and repaint every view with the new colors in one coalesced frame. Safe to call from anywhere — a command handler, an async callback, or a bare imperative call between input ticks — because the swap runs inside the loop's own tick and reuses its trailing flush + onFrame, so the repainted frame reaches the host even outside a dispatch.

Parameters

theme

Theme

The theme to switch to.

Returns

void

Example

ts
loop.setTheme(nordTheme); // repaints immediately, from any call context

stop()

stop(): void

Defined in: ui/src/event/types.ts:126

Stop the loop's out-of-tick painter. After stop(), a mutation that would normally schedule a deferred repaint — a timer, a promise continuation, a direct call between ticks — is ignored, and any already-queued deferred paint is skipped, so a late callback during or after teardown never writes to a stopped host. Idempotent. In-tick painting (a dispatch/resize/command) is unaffected: a running loop never calls this, and run() calls it once during shutdown. It does not dispose the mounted view tree.

Returns

void

Example

ts
// Inside run()'s shutdown, after the terminal is restored:
loop.stop(); // gate the deferred painter before detaching the frame/caret sinks