Skip to content

Add getFrameController() for application-owned text selection - #984

Open
chiga0 wants to merge 2 commits into
vadimdemedes:masterfrom
chiga0:feat/frame-controller
Open

Add getFrameController() for application-owned text selection#984
chiga0 wants to merge 2 commits into
vadimdemedes:masterfrom
chiga0:feat/frame-controller

Conversation

@chiga0

@chiga0 chiga0 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This is the first part of the split suggested in #980: a small, opt-in, read-only frame controller for apps that own their alternate-screen viewport and implement selection themselves. Selection semantics for Text (flows, boundaries, selectable and friends) are left for a follow-up once the bridge is proven by a real consumer.

What's included

  • getFrameController(stdout) with:
    • getFrame() — a read-only cells[y][x] grid; each cell exposes only value + fullWidth. Wide characters occupy two cells: the leading cell has fullWidth: true, the trailing placeholder has an empty value. No internal style data is exposed.
    • getSelection() / setSelection() — Ink highlights the region before serialization. Repaints go through the regular render throttle, and identical selections are deduplicated.
    • subscribe() — frame listener, returning an unsubscribe function.
  • Selection coordinates are zero-based cell positions in Ink's output region, normalized to reading order — reverse (right-to-left or bottom-to-top) drags select the same region as forward drags.
  • Readme docs covering the API and coordinate semantics.

Concerns from #980 addressed

  1. Per-render overhead — cells are only projected while subscribers exist, so apps that never use the frame controller pay no overhead.
  2. Subscriber reentrancy — listeners run in a microtask after the publishing render completes; a setSelection() call from a listener schedules a new render instead of re-entering the one in flight; notifications are coalesced and frames delivered in order.
  3. API surface — frame publishing stays internal (getFrameController returns the read-only view); no internal style data is exposed; frames and rows are frozen and deeply readonly-typed.
  4. Reverse selections — normalized in setSelection(), covered by tests.
  5. Tests — 14 tests covering opt-in behavior, async delivery, wide characters, multi-row regions, reverse selections, dedupe, and subscriber reentrancy. The nested-text metadata concern from Add text selection system with frame-level cell composition #980 doesn't apply here because this PR adds no semantic Text props.

Also included

renderInteractiveFrame re-asserts the app's current cursor intent before each interactive render: log-update only applies a cursor position set since the previous render, so without this a repaint triggered by setSelection() would move the cursor from its useCursor() position to the end of the output.

The alternate-screen clearTerminal from #980 is deliberately not included here: it's an unrelated behavior change and can be a separate proposal if desired.

Expose the composited frame as a read-only grid of cells that apps
owning their input (e.g. alternate-screen apps handling mouse events)
can subscribe to, and let them push a selection region that Ink
highlights before serialization.

Frames are only generated while subscribers are registered, listeners
are notified outside the render pass with coalesced notifications, and
selections are normalized to reading order so reverse drags work.
Cover the gaps found in self-review: the controller is no longer
reachable after unmount, and the selection highlight reaches the
terminal through the throttled, non-debug interactive render path
(log-update writes), not just debug mode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant