OpenTUI 0.6.1

npmGitHub release

In 0.6.0, EditBuffer.setText("a\fb") throws NativeError. The editor places its cursor by display cell, and a control character took no cells, so the cursor could not move past it or delete it. In 0.5.14, which accepted controls, the cursor in "a\fb" could not reach column 0, Backspace stopped at "a\f", and rendering "A\x07B\x1bC\fD" wrote raw BEL and ESC bytes to the terminal. Rejecting the text stopped that, but every application then had to remove these characters before it could edit a string.

In 0.6.1, editors keep control characters and show them the way Vim and Neovim do. The text "a\fb\0c\x1bd\x85e\x7ff" shows as a^Lb^@c^[d<85>e^?f. A C0 control or DEL is caret notation in two cells, and a C1 control is lowercase hex in four cells. The notation changes only how the text looks. The cursor stops before each control, one Backspace deletes it, and getText(), undo, and selection copy return the original characters. Tab, carriage return, and line feed work as before. Read-only text, such as TextRenderable, still gives controls no cells.

This has one cost. An editor offset is no longer the same as a JavaScript string width. Bun.stringWidth("\f") is 0, but ^L takes two cells. Code that calculates editor offsets from string widths, such as code that places an extmark after a match, must ask the buffer instead. measureText() measures text the way the buffer lays it out, and getTextRange() converts an offset back to a string index:

buffer.setText("\f@file")
const text = buffer.getText()
const offset = buffer.measureText(text.slice(0, text.indexOf("@"))) // 2: ^L takes two cells
buffer.getTextRange(0, offset).length // 1: the index of "@"

Added

  • core: EditBuffer.measureText(text) returns the display offset at the end of text as the buffer would lay it out. It counts each line break as one. See Editing buffers and views. (#1636)
  • native: ot_edit_buffer_measure_text() gives the same measurement through the C interface. See Editing buffers and views. (#1636)

Changed

  • core: EditBuffer.setText(), insertText(), and replaceText(), Textarea initialValue, Input.value, and placeholders accept control characters and do not throw NativeError. An editor shows C0 controls and DEL as ^@ to ^_ and ^? in two cells, and C1 controls as <80> to <9f> in four cells. See Editing buffers and views. (#1636)
  • core: Paste in Textarea and Input keeps control characters. It removes terminal escape sequences, including a lone ESC and an incomplete sequence, the same way on Bun and Node. Core no longer depends on strip-ansi. See Editing buffers and views. (#1636)

Fixed

  • core: Extmarks and the Input cursor use native display offsets. As a result, they keep the correct position in text that contains control characters. See Editing buffers and views. (#1636)

API changes#

@opentui/core#

Added

  • method EditBuffer.measureText(text: string): number
  • method FFIRenderLib.contextEditBufferMeasureText(context: NativeContextHandle, editBuffer: ContextEditBufferHandle, bytes: Uint8Array): number

opentui.h#

Added

  • function ot_edit_buffer_measure_text(ot_context *, const ot_handle *edit_buffer, const uint8_t *bytes, uint32_t byte_count, uint32_t *out_width): ot_status