OpenTUI 0.5 kept the renderable tree in JavaScript. On each frame, JavaScript
walked the tree and read each node's position back from Yoga. It then made one
native call for each draw, so the cost of a frame grew with the number of
nodes and draws. In 0.6.0, native code owns the tree. Each Renderable has a
node in a native scene, which holds the node's Yoga node and paint state.
Layout and paint setters stage their changes, and Core sends all staged
changes to native code in one call before each frame step. Native code then
lays out, clips, and paints the scene and fills the hit grid. It draws most
built-in renderables without a call to JavaScript. When no visible node has a
JavaScript hook, one native frame step lays out and paints the whole frame.
Rendering pipeline follows a
complete frame.
The built-in renderables and the React and Solid bindings keep their option
names, but a few option values now throw, such as enableLayout: false.
Custom renderables may need a little work. Renderable no longer has
render(), getLayoutNode(), or a protected yogaNode. Draw in
renderSelf(), renderBefore, or renderAfter, and report content size with
setMeasureProvider(). Paint hooks also work differently. A hook of an
unbuffered renderable now receives a recording buffer: OpenTUI stores the
hook's draw calls, and native code plays them back at the node's place in
paint order. Painting never stops to call JavaScript, so a hook cannot tear
the frame. But a hook can no longer read the frame's cells. To read cells, a
hook must draw into a buffer that the renderable owns and compose that
buffer, or the work must move to a post-process function. With
buffered: true, the hooks draw into such a buffer, and OpenTUI composes it.
Custom renderables shows a complete
example.
Native rendering resources now belong to one owner, a native Context.
Buffers, text buffers, views, syntax styles, images, and scene nodes cross the
native boundary as generational handles instead of raw pointers. A stale
handle fails with an error instead of touching freed memory, and destroying a
Context releases everything that it owns. The factories that used to create
resources without an owner now take one. For a resource that a renderer
draws, pass renderer.nativeScene:
// 0.5: SyntaxStyle.fromStyles(styles)
const syntaxStyle = SyntaxStyle.fromStyles(styles, renderer.nativeScene)
// 0.5: OptimizedBuffer.create(40, 10, "unicode")
const buffer = OptimizedBuffer.create(40, 10, "unicode", {
owner: renderer.nativeScene,
})Image factories take only a ResourceContext as their owner. For the
renderer's Context, pass renderer.nativeScene.resourceContext. Without a
renderer, create a Context with
new ResourceContext({ objectCapacity, renderCellsMax }), and call its
destroy() method when you are done. objectCapacity is the initial number
of object slots, and renderCellsMax limits the cells in each buffer.
API and symbol index lists each removed or
changed API with its replacement.
Terminal transitions are now asynchronous. suspend() and resume() return
promises. destroy() still returns at once, but the terminal restoration and
the remaining output finish later. Before you reuse the streams, or before a
deliberate exit, await renderer.closed:
await renderer.suspend()
// Run an editor or a shell in the terminal.
await renderer.resume()
renderer.destroy()
await renderer.closedThe native library now has a C interface. opentui.h declares a C ABI of
ot_* functions, and the opentui Zig module gains the same Context, scene,
and Session model. A C or Zig program can build a scene and write frames with
no JavaScript runtime. The ABI is experimental, so use the header, library,
and bindings from the same revision.
Native rendering starts with a box and a label.
One fix changes colors in some SSH sessions. Inside an SSH or mosh login,
OpenTUI detects remote mode automatically. In that mode, it used to stop
reading the environment before it read TERM and COLORTERM. As a result,
every remote terminal got truecolor output, and a 256-color terminal such as
GNU screen 4.x showed the UI without color. OpenTUI now reads these two keys.
This has a cost. A truecolor terminal that reaches the server with
TERM=xterm-256color and no COLORTERM used to get exact colors by accident.
Examples are iTerm2 and WezTerm over a stock macOS ssh client, which does not
forward COLORTERM. They now get 256-color approximations. A Kitty XTVERSION
reply still turns on truecolor. To keep truecolor in other terminals, export
COLORTERM=truecolor on the server, forward it with SendEnv COLORTERM and
AcceptEnv COLORTERM, or pass it in the renderer's environment option.
Breaking changes
- core:
Renderable.render(),updateLayout(),updateFromLayout(),
canReuseRenderCommandList(),getLayoutNode(), the protectedyogaNode,
theRenderCommandtype, andRootRenderable.calculateLayout()are
removed. Draw inrenderSelf(),renderBefore, orrenderAfter, and use
setMeasureProvider(),invalidateIntrinsicSize(), andgetLayout()for
size and layout. Protected members such as_x,_y,getScissorRect(),
and_getVisibleChildren()are also removed. Usex,y,screenX, and
screenY, and theviewportCullingoption ofScrollBox. See
Custom renderables.
(#1479) - core: The
enableLayout: falseoption throws.on("resized")and
on("layout-changed")throw on a renderable that is not the root, where
these listeners never ran. UseonSizeChangeinstead. See
Layout.
(#1479) - core: Paint hooks of an unbuffered renderable get a recording buffer
that cannot read cells, andrenderer.nextRenderBufferaccepts drawing only
in a post-process function.buffer.buffersworks only in a post-process
function. Elsewhere, usebuffer.withBuffers(callback). To read or change
cells in a hook, setbuffered: true, or draw into a buffer that you own and
compose it. For the whole frame, use a post-process function. See
Rendering pipeline.
(#1479) - core:
addToHitGrid(),pushHitGridScissorRect(),
popHitGridScissorRect(), andclearHitGridScissorRects()are removed from
CliRendererandRenderContext. Native code adds a hit area for each
visible node, andrenderer.hitTest(x, y)queries the hit grid. A custom
RenderContextmust implementnativeScene,requestAnimationFrame(), and
cancelAnimationFrame(). See
Interaction, focus, and selection.
(#1479) - core:
OptimizedBuffer.create(),TextBuffer.create(),
EditBuffer.create(),SyntaxStyle.create(),SyntaxStyle.fromStyles(),
andSyntaxStyle.fromTheme()require an owner, and theptrproperties of
buffers, views, and styles are removed. Passrenderer.nativeScene, or a
ResourceContextwhen no renderer exists. Resources from different owners
cannot be used together, and the resources that a renderer owns stop
working when the renderer is destroyed. See
How Core uses native.
(#1479) - core:
suspend()andresume()return aPromise<void>, and
destroy()restores the terminal asynchronously. Await the transitions, and
awaitrenderer.closedbefore you reuse the streams. Both methods throw
while another transition is pending, and when the terminal is not set up,
for example on a test renderer or afterdestroy(). See
Lifecycle and cleanup.
(#1479) - core: The
useThreadoption,renderer.useThread,
renderer.rendererPtr,dumpBuffers(),dumpOutputBuffer(), and the
OTUI_NO_NATIVE_RENDERenvironment variable are removed. Remove
useThread. Instead ofdumpBuffers(), use test frame capture or
renderer.currentRenderBuffer.withBuffers(). Instead of
dumpOutputBuffer(), give the renderer a customstdout. Instead of
OTUI_NO_NATIVE_RENDER, usecreateTestRenderer(). See
Rendering diagnostics.
(#1479) - core: The
Clipboardclass is removed. Use
renderer.copyToClipboardOSC52()andrenderer.clearClipboardOSC52(), or
createClipboard()withcreateHostClipboard()and
createRendererClipboardAdapter(renderer). See
Clipboard.
(#1479) - core: OpenTUI no longer installs a process-global
requestAnimationFrame,cancelAnimationFrame, orwindow. Use
renderer.requestAnimationFrame()andrenderer.cancelAnimationFrame().
See Animation and Timeline.
(#1479) - core: After
destroy(), renderable constructors, the timeline
engine.attach(), and most renderer methods throw, for example
setTerminalTitle(),copyToClipboardOSC52(),getStats(), and
hitTest().requestRender()andresize()do nothing. Check
renderer.isDestroyedfirst. See
Lifecycle and cleanup.
(#1479) - core: When the output stream emits
error, or closes or finishes before
the renderer closes it, the renderer destroys itself andrenderer.closed
rejects. Handle the rejection ofclosed. See
Lifecycle and cleanup.
(#1479) - core: Drawing methods throw
RangeErrorfor fractional,NaN, or
infinite coordinates and sizes.drawText()and box titles throw
RangeErrorfor text larger than 64 KiB of UTF-8.setCell(),
setCellWithAlphaBlending(),drawChar(), anddrawText()throw
NativeErrorfor attribute bits 8 to 31. In a paint hook, the frame fails
instead. See Buffer API.
(#1479) - core:
EditBuffer.setText(),insertText(),insertChar(), and
replaceText(), the same Textarea and Input methods, Textarea
initialValue,Input.value, and bothplaceholderoptions throw
NativeErrorfor control characters other than tab, carriage return, and
line feed. DEL and C1 control characters also throw. Remove these
characters first. See Editing buffers and views.
(#1479) - core: The minimum Bun version is 1.3.14 (was 1.3.0), the oldest version
that CI tests. Bun 1.3.0 to 1.3.3 pass some FFI pointer arguments as wrong
addresses, Bun 1.3.4 to 1.3.6 can crash after a Worker exits, and Bun 1.3.7
can hang while it loads modules. See
Runtime and platform support.
(#1479) - core: The pointer-based
RenderLibmethods, such ascreateRenderer
andbufferDrawText, and the handle types, such asRendererHandleand
OptimizedBufferHandle, are removed. Use the public classes and the
Context*Handle,SessionHandle, andSceneNodeHandletypes. See
How Core uses native.
(#1479) - core: Standalone
YogathrowsYogaErrorfor invalid arguments, and
Config.free()throws while nodes from that config remain. Measure and
dirtied functions must return synchronously and must not change Yoga nodes.
See Yoga API.
(#1479) - ssh:
session.write()can throwOutputPressureErrororRangeError.
colsandrowshold the requested size until the renderer attaches, and
onCloseruns at logical close, before the renderer finishes closing. See
SSH.
(#1479) - native: The legacy native exports, such as
createRendererand
bufferDrawText, are removed. Use theot_*functions inopentui.h. In
Zig, the global grapheme and link pools are removed, so
OptimizedBuffer.init()andCliRenderer.create()require alink_pool.
GraphemePool.acquire()andLinkPool.acquire()replacealloc(). They
return an ID that holds one reference, whichdecref()releases. See
C and Zig.
(#1479)
Added
- core:
renderer.requestAnimationFrame()and
renderer.cancelAnimationFrame()schedule one-shot frame callbacks for one
renderer.getTimelineEngine(renderer)and
createTimeline(options, renderer)give each renderer its own timeline
engine. See Animation and Timeline.
(#1479) - core:
renderer.closedresolves after destruction writes the remaining
output and native teardown finishes. It rejects when the output fails or
does not finish within one second. See
Lifecycle and cleanup.
(#1479) - core:
renderer.requestResize(width, height)debounces size changes
from a custom terminal and applies only the latest size. See
Renderer.
(#1479) - core: The
nativeSceneWorkBudgetoption lets a frame yield to the event
loop during preparation. Painting still runs in one native call. See
Renderer.
(#1479) - core: The
environmentoption gives terminal environment values to one
renderer, for example{ TERM: pty.term }from an SSH client. These values
replace theforwardEnvKeysvalues with the same names. In an automatically
detected SSH login, onlyTERMandCOLORTERMtake effect unless you set
remote: true. See
Terminal capabilities.
(#1603) - core:
ResourceContextowns native resources without a renderer, and
NativeErrorreports a failed native call with itsoperationand
status. Image factories accept aResourceContextin theirowner
option, such asrenderer.nativeScene.resourceContext. See
How Core uses native and
NativeImage.
(#1479) - core: Custom renderables get
setMeasureProvider(),
invalidateIntrinsicSize(),getLayout(),refreshHooks(), and
defineNativeIntegration().TextRenderableandCodeRenderableget
drawToBuffer(), andOptimizedBuffergetswithBuffers()for scoped cell
access. See Custom renderables.
(#1479) - core:
TestRendererSetup.dispose()and[Symbol.asyncDispose]()
destroy the test renderer and wait forrenderer.closed.
ManualClock.pendingTimerCountcounts the scheduled timeouts and active
intervals. See Testing.
(#1479) - native:
opentui.hdeclares an experimental C ABI, and theopentui
Zig module gains the same Context, scene, and Session model. A
Context's object table starts atobject_capacityslots and doubles when it
is full, up to 4,194,304 slots orobject_capacity, whichever is larger.
Past that limit, object creation fails withOT_OBJECT_LIMIT. See
Native rendering.
(#1479,
#1622) - native: Terminology gets OSC 8 hyperlinks and OSC 777 notifications when
its XTVERSION reply names it. See
Terminal capabilities.
(#1566) - ssh: The server accepts
COLORTERM,TERM_PROGRAM, and
TERM_PROGRAM_VERSIONenv requests that arrive before the shell request,
with values of at most 256 bytes. It rejects other keys. The renderer gets
these values with the client's PTYTERM, so capability detection sees the
client's color depth and hyperlink support, and GNU Screen or tmux from the
TERMprefix. A client withTERM=xterm-256colorand noCOLORTERMnow
gets 256-color output instead of truecolor. To keep truecolor, add
SendEnv COLORTERMto the client's ssh configuration. See
SSH.
(#1603)
Changed
- core: Native code lays out, culls, and paints the scene and fills the
hit grid, so JavaScript no longer walks the tree on each frame. JavaScript
work in a frame now grows with changes and hooks, not with the number of
renderables. Some built-in renderables, such as Select and ScrollBox, still
run JavaScript hooks. See
Rendering pipeline.
(#1479) - native: Text buffers build rope slices in one allocation. Setting and
appending text use less time and memory. See
Editing buffers and views.
(#1582) - native: Buffers write cells that are not grapheme clusters on a faster
path, anddrawText()writes printable ASCII without copying it. See
Buffer API.
(#1591) - core:
EmbeddedTerminalRenderableencodesmetaas Alt, and only
superencodes as Super. Core's key parser setsmetafor Alt, so Alt+b
now reaches a legacy child asESC b, notb. On macOS, an Alt modifier
reaches the child as Alt, because the host terminal already applied its
Option setting. See Embedded terminal.
(#1594,
1b25377b6,
f0d7d1d3b,
be14991e6) - react:
root.render()does nothing when the renderer is destroyed, for
example afterexitOnCtrlC. See React bindings.
(#1604) - solid:
render()does not mount when the renderer is destroyed, for
example afterexitOnCtrlC. See Solid bindings.
(#1604)
Fixed
- native: In an SSH or mosh login, automatically detected remote mode
reads the color depth fromTERMandCOLORTERM. A 256-color terminal such
as GNU screen 4.x gets 256-color output instead of truecolor that it cannot
show. See Terminal capabilities.
(#1624) - core: In split-footer mode, a write that continues a scrollback row no
longer loses or overwrites a wide character at the end of the row. Tabs in
captured stdout count from the start of the terminal row and stop at the
last column. See Renderer.
(#1597,
#1617) - native: In split-footer mode, a scrollback row that fills the terminal
width keeps its last cell. Output no longer erases the footer when fewer
than two rows remain above it, for example with the default footer in a
terminal of 13 rows or fewer. See Renderer.
(#1581,
#1618) - native: A highlight at the end of a line no longer colors text that an
append, insert, or undo adds to that line later.setText()and
replaceText()drop highlights on lines past the new line count, so
getHighlightCount()andgetLineHighlights()no longer report them. See
Editing buffers and views.
(#1621,
#1601) - native: Up and Down move the cursor to the start of a wide character or
tab, not inside it. This applies toEditBuffer.moveCursorUp()and
moveCursorDown(),EditorView.moveUpVisual()andmoveDownVisual(), the
Textarea arrow keys, andEditorView.setViewport()withmoveCursor. See
Textarea.
(#1596) - native: Down does nothing in an empty Textarea while it shows a
placeholder of more than one row. The next edit no longer fails with
InvalidArgument. See Textarea.
(#1619) - native: A width change in word-wrap mode releases the layout memory of a
narrower wrap. Before this fix, a 94 KiB text wrapped at width 1 and then at
width 80 kept about 46 MB. See Text.
(#1602) - core: Key bindings use the Kitty base-layout key only when the typed key
is one non-ASCII character. On Dvorak, Ctrl+J no longer matches
exitOnCtrlC, and Ctrl+ㅊ still matches Ctrl+C. See
Keyboard input.
(#1599) - core:
.tsxfiles andtsxMarkdown fences use the tree-sitter TSX
grammar through a separatetypescriptreactparser. JSX closing tags no
longer highlight as regular expressions. See
Syntax highlighting with Tree-sitter.
(#1598) - core: The tree-sitter worker changes
#lua-match?predicates to
#match?in bundled and registered queries when it can translate the
pattern. TypeScript and Zig identifiers no longer all get thetypeand
constantcaptures. See
Syntax highlighting with Tree-sitter.
(#1620) - core:
EmbeddedTerminalRenderablesends F1 to F12 with or without
modifiers, keys that the host terminal sends in SS3 form, and the keypad
Begin key. It sends F13 to F25 to a child in Kitty keyboard mode. See
Embedded terminal.
(#1616) - native: The embedded terminal answers OSC 10, 11, and 12 and Kitty OSC
21 color queries, and it applies an OSC 10 or OSC 11 that sets only one
default color. OSC 110 resets the foreground to white, and OSC 111 resets the
background to black. See
Embedded terminal.
(#1615) - native: The embedded terminal answers Primary, Secondary, and Tertiary
Device Attributes queries. fish 4.1 and later no longer stall for about two
seconds at startup. See
Embedded terminal.
(#1595)
API changes and documentation: https://opentui.com/docs/releases/0.6.0
What's Changed
- Terminology supports hyperlinks (osc8) and notifications through osc777 by @borisfaure in #1566
- native: move the render tree into Zig by @simonklee in #1479
- skills: simplify hardening skill by @simonklee in #1576
- docs: clarify runtime API contracts by @simonklee in #1577
- skills: keep the opentui skill installable by @simonklee in #1579
- native: keep the last cell of a split scrollback row that fills the width by @simonklee in #1581
- rope: build slice trees in one allocation by @simonklee in #1582
- buffer: optimize plain ASCII writes by @simonklee in #1591
- native: reply to device attribute queries in the embedded terminal by @simonklee in #1595
- native: count a trailing wide character in a split scrollback row's end by @simonklee in #1597
- core: pin the ASCII boundary of the base-layout fallback rows by @simonklee in #1599
- native: bound word-wrap layout memory after a width change by @simonklee in #1602
- native: snap vertical cursor moves to the start of a wide character by @simonklee in #1596
- native: check highlight counts after shrinking setText by @simonklee in #1601
- ssh: send the client's terminal environment to the renderer by @simonklee in #1603
- solid, react: skip the mount when the renderer is destroyed by @simonklee in #1604
- core: highlight TSX with the tree-sitter TSX grammar by @simonklee in #1598
- rope: simplify marker cache storage by @simonklee in #1610
- core: encode embedded terminal function keys and SS3 keys by @simonklee in #1616
- native: set embedded terminal default colors by @simonklee in #1615
- core: cut captured stdout rows where the terminal row continues by @simonklee in #1617
- native: keep split-footer output off the footer when fewer than two rows remain by @simonklee in #1618
- native: keep Down on the empty buffer while a placeholder is shown by @simonklee in #1619
- native: stop highlights from spreading onto text appended at the line end by @simonklee in #1621
- core: evaluate #lua-match? predicates in tree-sitter queries by @simonklee in #1620
- api: record the API history of every release by @simonklee in #1625
- native: grow the object handle table instead of failing at object_capacity by @simonklee in #1622
- core: add input parser conformance vectors by @simonklee in #1623
- native: keep TERM and COLORTERM color depth in auto remote sessions by @simonklee in #1624
- web: let release notes inspect source by @simonklee in #1627
New Contributors
- @borisfaure made their first contribution in #1566
Full Changelog: v0.5.17...v0.6.0