docs/core-concepts/interaction.mdx
docs/core-concepts/interaction.mdxBrowse 89 files
14,588 bytes
Token encoding: o200k_base
Snapshot ac753b4
title: Interaction, focus, and selection description: Handle mouse input, renderable focus, terminal focus reports, and text selection
{/* docs-lint-disable mdx-esm-import */} import MousePointerPreview from "../../../components/MousePointerPreview.astro"
Interaction, focus, and selection
OpenTUI routes mouse input through rendered cell bounds. It tracks one focused renderable and one global text selection per renderer.
Terminal-window focus is a separate state. A terminal focus report does not give keyboard input to a renderable.
Mouse events
Mouse input is enabled by default. Set useMouse: false to disable it or enableMouseMovement: false to omit movement tracking.
Renderable options accept a catch-all onMouse handler and these specific handlers:
| Handler | event.type | Meaning |
|---|---|---|
onMouseDown | "down" | A button went down. |
onMouseUp | "up" | A button went up. |
onMouseMove | "move" | The pointer moved without a pressed button. |
onMouseDrag | "drag" | The pointer moved with a pressed button. |
onMouseDragEnd | "drag-end" | An automatically captured left-button drag ended. |
onMouseDrop | "drop" | A captured drag ended over this target. |
onMouseOver | "over" | The hit target changed to this renderable. |
onMouseOut | "out" | The hit target changed away from this renderable. |
onMouseScroll | "scroll" | The terminal reported wheel input. |
OpenTUI does not synthesize a click event. A click produces down and up. A double click produces two such pairs.
Mouse handlers receive these raw pairs. Text selection also counts repeated left-button downs, but MouseEvent has no
click-count field.
MouseButton.LEFT, MIDDLE, and RIGHT are 0, 1, and 2. For wheel input, read event.scroll.direction and event.scroll.delta instead of event.button.
Each MouseEvent also contains these fields:
xandyare zero-based global cells in the renderer's render region.modifierscontainsshift,alt, andctrl.targetis the original hit renderable.currentTargetchanges as the event moves through ancestors.sourceidentifies the captured renderable onoveranddropevents.isDraggingmarks mouse events that belong to a text-selection drag.
OpenTUI does not expose a coordinate-conversion helper. Compute local cells from the current target.
import { BoxRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const box = new BoxRenderable(renderer, {
width: 20,
height: 4,
onMouseDown(event) {
const localX = event.x - event.currentTarget!.x
const localY = event.y - event.currentTarget!.y
console.log(localX, localY)
},
})
renderer.root.add(box)
Hit order and propagation
Each rendered node writes its bounds to a native hit grid. Later writes replace earlier writes in overlapping cells.
Children render after their parent. Siblings render in ascending zIndex order, so a higher zIndex receives an overlapping hit.
The hit grid obeys clipping from overflow: "hidden" and "scroll". A layout change can emit out and over without physical pointer movement.
Mouse events start at the hit target and bubble through parent links. event.stopPropagation() prevents later ancestors from receiving that event.
In this overlapping pair, the front box receives both mouse downs. The first bubbles to parent; the second calls
stopPropagation() in the front box's handler.
┌─parent─────────────────────────────┐
│ │
│ ┌─back z=0─────────────┐ │
│ │ ┌─front z=1─────────────┐ │
│ └────────│ │ │
│ └───────────────────────┘ │
└────────────────────────────────────┘
target: front
bubble: front -> parent
stopped: front
event.preventDefault() has only defined renderer defaults. On left-button down, it prevents automatic focus and the post-dispatch selection clear.
It does not stop propagation. It also does not undo a new selection that already started on selectable text.
Hover, drag, and capture
over and out report changes to the top hit target. They are not browser enter and leave events, and they bubble like other mouse events.
After a left-button drag starts on a renderable, OpenTUI captures later drag events to that source. No public pointer-capture API exists.
On release, the source receives drag-end and up. The renderable under the pointer receives drop with event.source, then its normal up event.
Right-button and middle-button drags follow hit testing without this capture. A selectable-text drag follows the selection path instead.
Mouse pointer styles
renderer.setMousePointer() changes the mouse pointer style in the terminal pane. The style applies to the renderer,
not to an individual renderable. Use onMouseOver to set the style. Use onMouseOut to restore "default".
import { BoxRenderable, createCliRenderer, type MousePointerStyle } from "@opentui/core"
const renderer = await createCliRenderer()
function setPointer(style: MousePointerStyle) {
renderer.setMousePointer(style)
renderer.requestRender()
}
const button = new BoxRenderable(renderer, {
width: 12,
height: 3,
border: true,
onMouseOver: () => setPointer("pointer"),
onMouseOut: () => setPointer("default"),
})
renderer.root.add(button)
A demand-driven renderer writes the new style during its next frame. If the handler does not change a renderable
property, call requestRender() to request the next frame.
MousePointerStyle accepts the following CSS cursor values. Move your pointer over a value to preview the pointer from
your browser and operating system. Your terminal can show a different pointer.
{/* docs-lint-disable mdx-component-node */}
OpenTUI sends each value with OSC 22, but it does not detect OSC 22
support. A terminal can ignore an unsupported value or use the nearest available style. "none" hides the pointer if
the terminal supports this value.
The style changes only the pointer appearance. It does not change hit testing or mouse event delivery. Keep visible
focus states and keyboard alternatives for every pointer interaction. renderer.destroy() resets the terminal pointer.
Renderable focus
A renderable receives keyboard and paste input only while its focused property is true. Calling focus() has no effect unless focusable is true.
Input, Textarea, Select, TabSelect, ScrollBox, and ScrollBar are focusable by default. A Box becomes focusable with focusable: true.
Each renderer tracks at most one focused renderable. Focusing another renderable blurs the previous one.
Use focus() and blur() for explicit control. Listen for RenderableEvents.FOCUSED and RenderableEvents.BLURRED on the instance.
Focusing an input does not clear a selection in another renderable:
input.focus()
input.value = "deploy --check"
Select text, then focus input
Command
deploy --check
By default, a left-button down focuses the nearest focusable target or ancestor. Set renderer autoFocus: false to disable this behavior.
OpenTUI Core has no automatic Tab traversal or focus-order property. Your application must choose the next renderable and call focus().
Focused components apply their own key bindings before editing or changing local state. See Keyboard input.
Terminal focus reports
The renderer emits focus and blur when the terminal sends window-focus reports. Capability detection controls whether those reports are available.
These events can pause application work when the terminal window loses focus. They do not change currentFocusedRenderable.
Use focused_renderable for renderable focus changes. See Terminal capabilities for report support and timing.
Text selection
Text-buffer renderables are selectable by default. Set selectable: false on Text or related content to disable selection.
A left-button down on selectable content starts a global selection. Dragging updates its endpoints and extends the selection across selectable descendants in the active container.
Text-buffer renderables, including Text, Code, Input, and Textarea, use repeated left-button presses for these selection behaviors:
| Gesture | Result |
|---|---|
| First press and drag | Start at zero width, then select by cells |
| Second press | Select the word at the pointer |
| Third press | Select the logical source line, including its soft wraps |
| Drag after the second press | Extend by words and include the text between the outer words |
| Drag after the third press | Extend by logical source lines |
Each repeated press must hit the same renderable within 500 ms. Its x and y coordinates can each differ by at most one cell from the prior press. OpenTUI does not expose options for this interval or for the selection-word boundary set.
OpenTUI groups adjacent graphemes by whether their first code point is in this boundary set:
space tab ' " │ ` | : ; , ( ) [ ] { } < > $
Adjacent boundary graphemes form one selectable run. Adjacent non-boundary graphemes form another. Thus, double-clicking
a run of spaces or tabs selects that run. /, \, -, and . are not boundaries, so foo/bar is one selection word.
The same rule treats 日本語abc as one selection word. A hard line break ends a word, but a soft wrap does not.
These boundaries apply only to pointer selection. Textarea word movement and word deletion use editor boundaries, so they can stop at different positions.
Line selection excludes the line break. It trims leading and trailing ASCII spaces and tabs, unless the complete line contains only spaces and tabs.
ASCII Font, TextTable, and Embedded Terminal keep their component-specific cell selection behavior.
The active container expands to an ancestor when the pointer leaves its current subtree.
Releasing the button ends the drag and emits renderer selection. Ctrl+left-click extends an existing selection from its original anchor.
A normal left-button down that does not start or extend selection clears it. A mouse handler can prevent that clear with preventDefault().
You can also call renderer.clearSelection().
Use these public values:
renderer.hasSelectionreports whether a global selection object exists.renderer.getSelection()returns thatSelectionornull.Selection.behavioris"cell","word", or"line"and records how the selection expands its endpoints.Selection.anchor,focus, andboundsuse global gesture cells. The rectangularboundsincludes both endpoint cells.Selection.selectedRenderableslists renderables with selected text.Selection.getSelectedText()joins selected text in top-to-bottom, left-to-right order.
A word or line selection can extend beyond Selection.bounds. The bounds describe the pointer gesture, not the expanded
text range.
Each text buffer converts the global cell rectangle to local coordinates. TextBufferRenderable.getSelection() then returns { start, end }.
These offsets form a half-open range from the start of that text buffer. They count terminal display width, and each line break adds one unit.
The offsets are not UTF-16 indexes. Selection boundaries snap around complete grapheme clusters, including a cluster that spans multiple cells.
A drag can span lines. Here, the selected text includes the line break after app, and the local range ends after Test.
Build the app
Test the input
Selected: "the app\nTest"
Range: [6, 18)
Read Text and terminal cells before you combine selection offsets with JavaScript string methods.
Test interaction
createTestRenderer() returns mockMouse and mockInput. Both send terminal sequences through the real parser.
import { InputRenderable, TextRenderable } from "@opentui/core"
import { createTestRenderer } from "@opentui/core/testing"
const setup = await createTestRenderer({ width: 30, height: 6 })
try {
const text = new TextRenderable(setup.renderer, { content: "select this", width: 11 })
const input = new InputRenderable(setup.renderer, { position: "absolute", top: 2, width: 20 })
setup.renderer.root.add(text)
setup.renderer.root.add(input)
await setup.renderOnce()
await setup.mockMouse.drag(text.x, text.y, text.x + 5, text.y)
input.focus()
await setup.mockInput.typeText("hello")
console.log(setup.renderer.getSelection()?.getSelectedText())
console.log(input.value)
} finally {
setup.renderer.destroy()
}
See Testing for MockMouse, MockInput, frame capture, and capability fixtures.
Terminal limitations
Mouse protocols, focus reports, hyperlinks, and modifier detail vary by terminal. Keyboard-only operation must not depend on pointer hover or drag.
OpenTUI does not create a browser accessibility tree. Supply visible focus state, keyboard alternatives, and text labels in your application.
Next
Read Keyboard input for event ownership. See Input, Textarea, Text, Box, and Embedded terminal for component behavior.