Skip to content

Add an inline Cursor element - #1002

Closed
dhleong wants to merge 18 commits into
vadimdemedes:masterfrom
dhleong:dhleong/inline-cursor-element
Closed

dhleong wants to merge 18 commits into
vadimdemedes:masterfrom
dhleong:dhleong/inline-cursor-element

Conversation

@dhleong

@dhleong dhleong commented Sep 15, 2026

Copy link
Copy Markdown

This is an alternative to #872 that also address #870 and #251.

<Cursor /> renders inline within <Text />, following it naturally with Ink's word wraps. Wherever <Cursor /> is rendered within the output is where Ink will place the system cursor.

How

<Cursor /> renders a zero-width ANSI hyperlink that Output detects and extracts, so (once we upgrade to https://github.com/AlCalzone/ansi-tokenize/releases/tag/v0.3.1; wasn't sure if you'd want that package noise in this PR) there should be no leakage into the rendered content. With the extracted cursor in hand, Ink simply uses the existing cursor management to place it where it needs to be.

<Cursor /> also supports a shape param, which gets carried by the hyperlink URI, so you can declaratively set the cursor shape as well (see discussion below).

Details

  • <Cursor /> is mutually exclusive with useCursor, enforced at runtime. In component mode, the absence of a rendered Cursor in the output means to hide the cursor, but useCursor will never render one in the output so clearing the component cursor would also clear any set by useCursor. There might be a clever way to use the CursorContext to indicate to Ink that a useCursor hook is mounted, but that didn't seem worth it.
  • I've also added cursor shape support here because I needed it in my project. We only render cursor shape changes if needed, so apps used in a context not supporting the cursor shape protocol (using only the default "block" cursor) shouldn't see any ANSI noise. It would be a little annoying to separate out perhaps, however if you like this feature but want to land them separately I could do that.
  • Since we're no longer explicitly placing the cursor in this mode, it may be useful to know where it's ended up. I've added a cursor property to the params of onRender. Not married to this API—the param name is "Metrics" so feels unintentional—but we do get the position on each render so it felt natural. I could also see a dedicated onCursorPositionChanged callback that does diffing and only emits if it actually changed, but that wasn't necessary for my purposes so didn't want to invest in it.
  • I've included a cursor-ime-component example that also has basic cursor movement support via the arrow keys, for playing with the wrapping behavior.

This feels cleaner, and makes for a better API in onRender
We *shouldn't* get other values, but if we do we fall back to 'block'
If users always use the default block shape, writing the cursor shape
sequence is wasteful---and potentially a bug, if they are trying to
use Cursor in an environment that does not support cursor shape
sequences.
Potentially, changing shape while not visible could result in the
terminal not actually updating, so we update previousCursorShape now
only on when the cursor was actually active
@sindresorhus

Copy link
Copy Markdown
Collaborator

I agree that Ink should support an inline cursor, and the <Cursor /> API makes sense for text inputs and IME. I don't think the marker-based implementation is safe to merge yet.

I manually verified several correctness issues: padding is counted twice when positioning the cursor, the marker lets text escape overflow="hidden", all three truncation modes lose a visible character, and screen-reader mode writes the internal OSC hyperlink verbatim. Cursor shape cleanup is also incorrect: removing a shaped cursor before exit leaves that shape active, while non-interactive output emits a reset despite never setting one. Resetting to steady block also overrides the user's terminal default.

The common problem is representing renderer state as an OSC hyperlink plus a Unicode character. Wrapping, truncation, clipping, transforms, and accessibility all process the fake character before it is extracted, so each path needs special handling.

I think cursor position should remain out-of-band metadata. Either put a cursor offset on the containing Text node, as the Gemini Ink fork does, or make <Cursor /> a special internal child that text flattening converts into an offset without serializing it into the content. I would also move cursor shape into a seperate PR, track whether Ink actually emitted a shape, and restore DECSCUSR 0 on cleanup. The onRender addition can probably wait as well.

@dhleong

dhleong commented Sep 16, 2026 via email

Copy link
Copy Markdown
Author

@dhleong

dhleong commented Sep 19, 2026

Copy link
Copy Markdown
Author

Think we can probably close this in favor of #1004

@dhleong dhleong closed this Sep 19, 2026
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.

2 participants