@opentui/core
0.6.6 · 14 Oct 2026MIT677.5k / weekRepositoryHomepagenpmAtom feed
160 stable releases, newest first, by release line. Notes are quoted as the maintainers published them.
- 0.6.x · latest line
0.6.6
14 Oct 2026core: A custom renderable can override
protected defaultDimension(kind)to set thewidthorheightthat a nullish write restores. See Custom renderables.core: Assigning
nullorundefinedto a layout property restores the default for an omitted option, and removing a React or Solid style prop does the same. See Layout.core: Assigning
nullorundefinedtovisibleshows the node. Before,nullhid the node, so assignfalseto hide it.
Previously, most layout setters ignored
null,undefined, or both, so the node kept its old style. As a result, a React or Solid component that removed a style prop kept the old layout. A plugin that readUnit.Undefinedfrom a getter also could not write that value back. In 0.6.6, a nullish write restores the default that the constructor uses when the option is omitted:const box = new BoxRenderable(renderer, { paddingX: 2 }) box.paddingLeft = 5 // pads the left side by 5 box.paddingLeft = undefined // pads it by 2 again, from paddingXMargin, padding, gap, min and max sizes, and
flexBasisbecome unset, so an edge falls back to its shorthand again.top,right,bottom, andleftalso unset the native edge. Before, the getter reportedundefinedwhile layout kept the old offset.widthandheightbecome"auto",positionbecomes"relative",overflowbecomes"visible", andvisiblebecomestrue. Invalid values are still ignored. Not every default comes back. Class defaults such as the row direction ofScrollBoxRenderablestay as they are, andflexShrinkbecomes1.If a custom renderable passes its own
widthorheighttosuper(), it should override the newprotected defaultDimension(kind)method. A nullish write then restores that size instead of"auto". Built-in classes that size themselves, such asInputRenderable,TabSelectRenderable, andASCIIFontRenderable, already override it.Added
core: A custom renderable can override
protected defaultDimension(kind)to set thewidthorheightthat a nullish write restores. See Custom renderables.
Changed
core: Assigning
nullorundefinedto a layout property restores the default for an omitted option, and removing a React or Solid style prop does the same. See Layout.core: Assigning
nullorundefinedtovisibleshows the node. Before,nullhid the node, so assignfalseto hide it.
Fixed
core:
fillRect,drawBox,setCell,drawChar, and other calls with u32 fields now say "must be a non-negative integer" for NaN, Infinity, fractional, and negative values. Before, the error said that the value exceeded the u32 limit. See Buffer API.core: A box
titleorbottomTitleover 64 KiB throws the sameRangeErrorasdrawTextinstead of aNativeErrorwith theInvalidArgumentstatus. See Box.core: Nesting deeper than 256 levels, counting the root, throws a
NativeErrorwith the message "Scene nesting depth exceeds the native limit". Before, the message gave only theObjectLimitstatus. See Layout.
API changes and documentation: opentui.com/docs/releases/0.6.6
What's Changed
core: name the real cause in three boundary errors (u32 fields, box titles, nesting depth)
core: restore layout defaults on nullish writes
0.6.5
14 Oct 2026core: Renderables have the remaining Yoga
Nodestyle getters, such asgetHeight(),getMaxWidth(),getPadding(edge),getGap(gutter),getFlexDirection(), andgetAlignSelf(). Use them instead of style reads throughgetLayoutNode(). See Layout.core: read declared layout styles on renderables
OpenTUI 0.6 removed
Renderable.getLayoutNode(). Before 0.6, plugins used it to read the layout styles of host renderables that they did not create. Some plugins saved a container'sgetMaxWidth()andgetAlignSelf()so they could restore the container after they restyled it. Others checked the host layout withgetFlexDirection(), or sized a panel inside a padded sidebar withgetPadding(). 0.6 kept onlygetWidth()andgetMargin(edge), so this code had nothing to call.In 0.6.5, renderables have the other style getters of a Yoga
Node, with the same names and return types. To move, remove thegetLayoutNode()call:// Before 0.6 const saved = host.getLayoutNode().getMaxWidth() // 0.6.5 const saved = host.getMaxWidth()
These getters return the declared style, not the computed layout. A write shows up at once, before the next layout pass. After
box.maxWidth = 40,box.getMaxWidth()returns{ unit: Unit.Point, value: 40 }, butbox.widthstill holds the computed width from the last pass. Afterdestroy(), the getters do not throw. They return Yoga's unset values, asgetWidth()does.An edge getter reads only the value set for that exact edge, so this case needs care. The
paddingoption setsEdge.All,paddingXsetsEdge.Horizontal, andpaddingLeftsetsEdge.Left. Afterpadding: 2,getPadding(Edge.Left)returns aUnit.Undefinedvalue, but layout still pads the left side by 2. That is because Yoga falls back fromEdge.LefttoEdge.Horizontaland then toEdge.All. To get the declared left padding, check the edges in that same order:import { Edge, Unit } from "@opentui/core/yoga" const left = [Edge.Left, Edge.Horizontal, Edge.All] .map((edge) => sidebar.getPadding(edge)) .find((value) => value.unit !== Unit.Undefined)Renderables still have no getter for computed padding, margin, or border. For padding declared in points, the declared value and the computed value are the same.
Added
core: Renderables have the remaining Yoga
Nodestyle getters, such asgetHeight(),getMaxWidth(),getPadding(edge),getGap(gutter),getFlexDirection(), andgetAlignSelf(). Use them instead of style reads throughgetLayoutNode(). See Layout.
API changes and documentation: opentui.com/docs/releases/0.6.5
What's Changed
core: read declared layout styles on renderables
0.6.4
13 Oct 2026core: Kitty images in iTerm2 no longer go black or blank after a move, a scroll, or a layout change. The renderer now deletes each old placement by its placement id, because iTerm2 also frees the image data when it gets an image-wide delete.
native: delete Kitty placements by id so iTerm2 keeps the image
Fixed
core: Kitty images in iTerm2 no longer go black or blank after a move, a scroll, or a layout change. The renderer now deletes each old placement by its placement id, because iTerm2 also frees the image data when it gets an image-wide delete.
API changes and documentation: opentui.com/docs/releases/0.6.4
What's Changed
native: delete Kitty placements by id so iTerm2 keeps the image
0.6.3
13 Oct 2026core:
renderer.setProgramStatus(status)andNativeSession.setProgramStatus(status)send one OSC 7501 program status
report. They returnfalsewhen OpenTUI does not queue the report, and they
throw for an invalid report. See
Program status.core:
TerminalCapabilities.program_statusistruewhen the terminal
replies to theOSC 7501 ; ?query. The renderer sends this query at startup
with the other capability queries, but not to foot. See
Terminal capabilities.native:
ot_session_control()withOT_CONTROL_PROGRAM_STATUSand anot_session_program_statuspayload sends a program status report.OT_CAP_PROGRAM_STATUSis set when the terminal replies to the query. See
Program status.
A coding agent such as opencode can wait for minutes at a permission prompt in a
background tab. Before this release, the terminal could not tell the user about it.
In 0.6.3, the agent can report its state to the terminal:renderer.setProgramStatus({ state: "blocked", kind: "permission", app: "opencode", message: "Allow bash?" })This call writes one report in the
Program Status Protocol,
OSC 7501. The terminal decides how to show the report. It can use a tab
indicator, an inbox entry, or a desktop notification. A terminal that does not
implement the protocol ignores the sequence, sosetProgramStatus()does not
wait for capability detection.capabilities.program_statusonly tells you that
a reply arrived. Afalsevalue can also mean that the reply has not arrived
yet.Each report replaces one record, and a field that a report omits is gone from
the record. Sendappandtitlewith every report that needs them. A program
that runs several tasks can give each task its own record with anid. It can
then remove that record withclear:renderer.setProgramStatus({ state: "working", id: "subagent-1", title: "Explore", progress: 40 }) renderer.setProgramStatus({ state: "clear", id: "subagent-1" })Records belong to the terminal, not to the renderer. OpenTUI does not clear them
when the renderer is destroyed or suspended. When the process exits, the
terminal dropsworkingandblockedrecords. It keepsdoneanderror
records for the user to find. If the user should see the result, reportdoneorerrorbefore the program exits. OpenTUI also does not wrap reports in tmux or
GNU Screen passthrough. The outer terminal cannot tell panes apart or see the
program exit, so aworkingrecord from a pane would never clear.C hosts send the same report with
ot_session_control()andOT_CONTROL_PROGRAM_STATUS. The payload is anot_session_program_statusstruct,
followed by the id, app, title, and message bytes. Native code checks every
report on both paths before it writes anything. A report that a terminal would
discard or only partly read fails at the call.Added
core:
renderer.setProgramStatus(status)andNativeSession.setProgramStatus(status)send one OSC 7501 program status
report. They returnfalsewhen OpenTUI does not queue the report, and they
throw for an invalid report. See
Program status.core:
TerminalCapabilities.program_statusistruewhen the terminal
replies to theOSC 7501 ; ?query. The renderer sends this query at startup
with the other capability queries, but not to foot. See
Terminal capabilities.native:
ot_session_control()withOT_CONTROL_PROGRAM_STATUSand anot_session_program_statuspayload sends a program status report.OT_CAP_PROGRAM_STATUSis set when the terminal replies to the query. See
Program status.
Changed
native:
ot_session_control()now checks the payload before the terminal
phase. While the terminal is not active, an invalid payload returnsOT_INVALID_ARGUMENTinstead ofOT_INVALID_PHASE. A title with control
characters is one example. See Frames and output.
API changes and documentation: opentui.com/docs/releases/0.6.3
What's Changed
core: report program status with OSC 7501
0.6.2
12 Oct 2026core:
measureEditText(text, { widthMethod, tabWidth })returns the
editor offset at the end oftextwithout anEditBuffer.tabWidth
defaults to the tab width of a new edit buffer. See
Editing buffers and views.native:
ot_edit_measure_text()gives the same measurement through the C
interface. Atab_widthover 255 fails. See
Editing buffers and views.native: A bold, dim, italic, underlined, blinking, inverse, hidden, or
struck-through cell in the last column of a row no longer gives its
attributes to the next cells that the renderer draws. A row that ends in such
a cell writes 4 more bytes. See Frames and output.
In 0.6.1, editors started to show control characters as notation. As a result,
an editor offset is no longer the same as a JavaScript string width. The string"\f"has a width of 0, but the editor shows it as^Lin two cells.EditBuffer.measureText()gives the correct offset, but only for a buffer you
already have. To get the offset of text that was not in an editor yet, code had
to create anEditBufferonly to measure that text.In 0.6.2,
measureEditText()measures text without a buffer. It returns the
same value asmeasureText()on an edit buffer with the same width method and
tab width. Pass the settings of the editor that will hold the text.Textarea
andInputuse the renderer's width method and the default tab width, which is
2:import { createCliRenderer, measureEditText } from "@opentui/core" const renderer = await createCliRenderer() measureEditText("\f@file", { widthMethod: renderer.widthMethod }) // 7: ^L takes two cells measureEditText("a\tb", { widthMethod: renderer.widthMethod, tabWidth: 4 }) // 6C programs get the same measurement from
ot_edit_measure_text(). It takes a
context, but no edit buffer handle.Added
core:
measureEditText(text, { widthMethod, tabWidth })returns the
editor offset at the end oftextwithout anEditBuffer.tabWidth
defaults to the tab width of a new edit buffer. See
Editing buffers and views.native:
ot_edit_measure_text()gives the same measurement through the C
interface. Atab_widthover 255 fails. See
Editing buffers and views.
Fixed
native: A bold, dim, italic, underlined, blinking, inverse, hidden, or
struck-through cell in the last column of a row no longer gives its
attributes to the next cells that the renderer draws. A row that ends in such
a cell writes 4 more bytes. See Frames and output.native: If you type an emoji with a skin-tone modifier, a ZWJ sequence,
Hangul jamo, or U+FE0F in an editor, the editor now stores its correct width.
Before, 👍🏽 took 4 cells instead of 2, so the cursor, wrapping, selection, and
Right key were wrong. See Textarea.native: A combining mark that starts a new text chunk now draws on the
character before it, for example U+0301 typed afterabcin an editor or
added withTextBuffer.append. The renderer drops a mark that would make a
1-cell character 2 cells wide, such as U+FE0F. See
Text and terminal cells.native: An embedded terminal redraws all rows when the child changes the
default foreground or background color (OSC 10, 11, 110, or 111) or sets
reverse video on or off (CSI ? 5 horCSI ? 5 l). Before, rows on the
screen kept the old colors. See
Embedded terminal.core: If the host terminal uses the Kitty keyboard protocol, an embedded
terminal now sends keypad Enter, keypad navigation, lock, Print Screen, Pause,
and left and right modifier keys to the child. Keypad digits reach a Kitty
child as keypad keys, and Ctrl+keypad 0 no longer arrives as a bare0. See
Embedded terminal.core:
parseKeypress()no longer changes the caller'sBufferwhen it
decodes a single byte above 127 as a meta key. The input parser of the
renderer did not have this problem. See
Keyboard input.
API changes and documentation: opentui.com/docs/releases/0.6.2
What's Changed
bench: time only drawBox in the box benchmark
native: draw a combining mark that starts a chunk
native: redraw embedded terminal when default colors change
core: encode embedded terminal Kitty keypad, lock, and modifier keys
native: measure merged editor chunks instead of adding their widths
core: keep parseKeypress from changing the caller's Buffer
native: close an attributed run at the end of its row
0.6.1
11 Oct 2026core:
EditBuffer.measureText(text)returns the display offset at the
end oftextas the buffer would lay it out. It counts each line break as
one. See Editing buffers and views.native:
ot_edit_buffer_measure_text()gives the same measurement
through the C interface. See
Editing buffers and views.core:
EditBuffer.setText(),insertText(), andreplaceText(),
TextareainitialValue,Input.value, and placeholders accept control
characters and do not throwNativeError. 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.
In 0.6.0,
EditBuffer.setText("a\fb")throwsNativeError. 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 asa^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, andgetText(),
undo, and selection copy return the original characters. Tab, carriage
return, and line feed work as before. Read-only text, such asTextRenderable, 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^Ltakes 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, andgetTextRange()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 oftextas the buffer would lay it out. It counts each line break as
one. See Editing buffers and views.native:
ot_edit_buffer_measure_text()gives the same measurement
through the C interface. See
Editing buffers and views.
Changed
core:
EditBuffer.setText(),insertText(), andreplaceText(),
TextareainitialValue,Input.value, and placeholders accept control
characters and do not throwNativeError. 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.core: Paste in
TextareaandInputkeeps 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 onstrip-ansi. See Editing buffers and views.
Fixed
core: Extmarks and the
Inputcursor use native display offsets. As a
result, they keep the correct position in text that contains control
characters. See Editing buffers and views.
API changes and documentation: opentui.com/docs/releases/0.6.1
What's Changed
editor: show control characters as notation
0.6.0
10 Oct 2026- breaking
core:
Renderable.render(),updateLayout(),updateFromLayout(),canReuseRenderCommandList(),getLayoutNode(), the protectedyogaNode,
theRenderCommandtype, andRootRenderable.calculateLayout()are
removed. Draw inrenderSelf(),renderBefore, orrenderAfter, and usesetMeasureProvider(),invalidateIntrinsicSize(), andgetLayout()for
size and layout. Protected members such as_x,_y,getScissorRect(),
and_getVisibleChildren()are also removed. Usex,y,screenX, andscreenY, and theviewportCullingoption ofScrollBox. See
Custom renderables. - breaking
core: The
enableLayout: falseoption throws.on("resized")andon("layout-changed")throw on a renderable that is not the root, where
these listeners never ran. UseonSizeChangeinstead. See
Layout. - breaking
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.
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. EachRenderablehas 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 asenableLayout: false.
Custom renderables may need a little work.Renderableno longer hasrender(),getLayoutNode(), or a protectedyogaNode. Draw inrenderSelf(),renderBefore, orrenderAfter, and report content size withsetMeasureProvider(). 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. Withbuffered: 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, passrenderer.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
ResourceContextas theirowner. For the
renderer's Context, passrenderer.nativeScene.resourceContext. Without a
renderer, create a Context withnew ResourceContext({ objectCapacity, renderCellsMax }), and call itsdestroy()method when you are done.objectCapacityis the initial number
of object slots, andrenderCellsMaxlimits the cells in each buffer.
API and symbol index lists each removed or
changed API with its replacement.Terminal transitions are now asynchronous.
suspend()andresume()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, awaitrenderer.closed:await renderer.suspend() // Run an editor or a shell in the terminal. await renderer.resume() renderer.destroy() await renderer.closed
The native library now has a C interface.
opentui.hdeclares a C ABI ofot_*functions, and theopentuiZig 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 readTERMandCOLORTERM. 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 withTERM=xterm-256colorand noCOLORTERMused to get exact colors by accident.
Examples are iTerm2 and WezTerm over a stock macOS ssh client, which does not
forwardCOLORTERM. They now get 256-color approximations. A Kitty XTVERSION
reply still turns on truecolor. To keep truecolor in other terminals, exportCOLORTERM=truecoloron the server, forward it withSendEnv COLORTERMandAcceptEnv COLORTERM, or pass it in the renderer'senvironmentoption.Breaking changes
- breaking
core:
Renderable.render(),updateLayout(),updateFromLayout(),canReuseRenderCommandList(),getLayoutNode(), the protectedyogaNode,
theRenderCommandtype, andRootRenderable.calculateLayout()are
removed. Draw inrenderSelf(),renderBefore, orrenderAfter, and usesetMeasureProvider(),invalidateIntrinsicSize(), andgetLayout()for
size and layout. Protected members such as_x,_y,getScissorRect(),
and_getVisibleChildren()are also removed. Usex,y,screenX, andscreenY, and theviewportCullingoption ofScrollBox. See
Custom renderables. - breaking
core: The
enableLayout: falseoption throws.on("resized")andon("layout-changed")throw on a renderable that is not the root, where
these listeners never ran. UseonSizeChangeinstead. See
Layout. - breaking
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. - breaking
core:
addToHitGrid(),pushHitGridScissorRect(),popHitGridScissorRect(), andclearHitGridScissorRects()are removed fromCliRendererandRenderContext. Native code adds a hit area for each
visible node, andrenderer.hitTest(x, y)queries the hit grid. A customRenderContextmust implementnativeScene,requestAnimationFrame(), andcancelAnimationFrame(). See
Interaction, focus, and selection. - breaking
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 aResourceContextwhen 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. - breaking
core:
suspend()andresume()return aPromise<void>, anddestroy()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. - breaking
core: The
useThreadoption,renderer.useThread,renderer.rendererPtr,dumpBuffers(),dumpOutputBuffer(), and theOTUI_NO_NATIVE_RENDERenvironment variable are removed. RemoveuseThread. Instead ofdumpBuffers(), use test frame capture orrenderer.currentRenderBuffer.withBuffers(). Instead ofdumpOutputBuffer(), give the renderer a customstdout. Instead ofOTUI_NO_NATIVE_RENDER, usecreateTestRenderer(). See
Rendering diagnostics. - breaking
core: The
Clipboardclass is removed. Userenderer.copyToClipboardOSC52()andrenderer.clearClipboardOSC52(), orcreateClipboard()withcreateHostClipboard()andcreateRendererClipboardAdapter(renderer). See
Clipboard. - breaking
core: OpenTUI no longer installs a process-global
requestAnimationFrame,cancelAnimationFrame, orwindow. Userenderer.requestAnimationFrame()andrenderer.cancelAnimationFrame().
See Animation and Timeline. - breaking
core: After
destroy(), renderable constructors, the timelineengine.attach(), and most renderer methods throw, for examplesetTerminalTitle(),copyToClipboardOSC52(),getStats(), andhitTest().requestRender()andresize()do nothing. Checkrenderer.isDestroyedfirst. See
Lifecycle and cleanup. - breaking
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. - breaking
core: Drawing methods throw
RangeErrorfor fractional,NaN, or
infinite coordinates and sizes.drawText()and box titles throwRangeErrorfor text larger than 64 KiB of UTF-8.setCell(),setCellWithAlphaBlending(),drawChar(), anddrawText()throwNativeErrorfor attribute bits 8 to 31. In a paint hook, the frame fails
instead. See Buffer API. - breaking
core:
EditBuffer.setText(),insertText(),insertChar(), andreplaceText(), the same Textarea and Input methods, TextareainitialValue,Input.value, and bothplaceholderoptions throwNativeErrorfor 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. - breaking
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. - breaking
core: The pointer-based
RenderLibmethods, such ascreateRenderer
andbufferDrawText, and the handle types, such asRendererHandleandOptimizedBufferHandle, are removed. Use the public classes and theContext*Handle,SessionHandle, andSceneNodeHandletypes. See
How Core uses native. - breaking
core: Standalone
YogathrowsYogaErrorfor invalid arguments, andConfig.free()throws while nodes from that config remain. Measure and
dirtied functions must return synchronously and must not change Yoga nodes.
See Yoga API. - breaking
ssh:
session.write()can throwOutputPressureErrororRangeError.colsandrowshold the requested size until the renderer attaches, andonCloseruns at logical close, before the renderer finishes closing. See
SSH. - breaking
native: The legacy native exports, such as
createRendererandbufferDrawText, are removed. Use theot_*functions inopentui.h. In
Zig, the global grapheme and link pools are removed, soOptimizedBuffer.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.
Added
core:
renderer.requestAnimationFrame()andrenderer.cancelAnimationFrame()schedule one-shot frame callbacks for one
renderer.getTimelineEngine(renderer)andcreateTimeline(options, renderer)give each renderer its own timeline
engine. See Animation and Timeline.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.core:
renderer.requestResize(width, height)debounces size changes
from a custom terminal and applies only the latest size. See
Renderer.core: The
nativeSceneWorkBudgetoption lets a frame yield to the event
loop during preparation. Painting still runs in one native call. See
Renderer.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 setremote: true. See
Terminal capabilities.core:
ResourceContextowns native resources without a renderer, andNativeErrorreports a failed native call with itsoperationandstatus. Image factories accept aResourceContextin theirowner
option, such asrenderer.nativeScene.resourceContext. See
How Core uses native and
NativeImage.core: Custom renderables get
setMeasureProvider(),invalidateIntrinsicSize(),getLayout(),refreshHooks(), anddefineNativeIntegration().TextRenderableandCodeRenderablegetdrawToBuffer(), andOptimizedBuffergetswithBuffers()for scoped cell
access. See Custom renderables.core:
TestRendererSetup.dispose()and[Symbol.asyncDispose]()
destroy the test renderer and wait forrenderer.closed.ManualClock.pendingTimerCountcounts the scheduled timeouts and active
intervals. See Testing.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.ssh: The server accepts
COLORTERM,TERM_PROGRAM, andTERM_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 theTERMprefix. A client withTERM=xterm-256colorand noCOLORTERMnow
gets 256-color output instead of truecolor. To keep truecolor, addSendEnv COLORTERMto the client's ssh configuration. See
SSH.
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.native: Text buffers build rope slices in one allocation. Setting and
appending text use less time and memory. See
Editing buffers and views.native: Buffers write cells that are not grapheme clusters on a faster
path, anddrawText()writes printable ASCII without copying it. See
Buffer API.core:
EmbeddedTerminalRenderableencodesmetaas Alt, and onlysuperencodes 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.solid:
render()does not mount when the renderer is destroyed, for
example afterexitOnCtrlC. See Solid bindings.
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.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()andreplaceText()drop highlights on lines past the new line count, sogetHighlightCount()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()andmoveCursorDown(),EditorView.moveUpVisual()andmoveDownVisual(), the
Textarea arrow keys, andEditorView.setViewport()withmoveCursor. See
Textarea.native: Down does nothing in an empty Textarea while it shows a
placeholder of more than one row. The next edit no longer fails withInvalidArgument. See Textarea.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.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 matchesexitOnCtrlC, and Ctrl+ㅊ still matches Ctrl+C. See
Keyboard input.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.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 thetypeandconstantcaptures. See
Syntax highlighting with Tree-sitter.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.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.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.
API changes and documentation: opentui.com/docs/releases/0.6.0
What's Changed
Terminology supports hyperlinks (osc8) and notifications through osc777
native: move the render tree into Zig
skills: simplify hardening skill
skills: keep the opentui skill installable
native: keep the last cell of a split scrollback row that fills the width
rope: build slice trees in one allocation
buffer: optimize plain ASCII writes
native: reply to device attribute queries in the embedded terminal
native: count a trailing wide character in a split scrollback row's end
core: pin the ASCII boundary of the base-layout fallback rows
native: bound word-wrap layout memory after a width change
native: snap vertical cursor moves to the start of a wide character
native: check highlight counts after shrinking setText
ssh: send the client's terminal environment to the renderer
solid, react: skip the mount when the renderer is destroyed
core: highlight TSX with the tree-sitter TSX grammar
rope: simplify marker cache storage
core: encode embedded terminal function keys and SS3 keys
native: set embedded terminal default colors
core: cut captured stdout rows where the terminal row continues
native: keep split-footer output off the footer when fewer than two rows remain
native: keep Down on the empty buffer while a placeholder is shown
native: stop highlights from spreading onto text appended at the line end
core: evaluate #lua-match? predicates in tree-sitter queries
api: record the API history of every release
native: grow the object handle table instead of failing at object_capacity
core: add input parser conformance vectors
native: keep TERM and COLORTERM color depth in auto remote sessions
web: let release notes inspect source
- 0.5.x
0.5.17
8 Oct 2026native: detect OSC 8 hyperlinks in more terminals
0.5.16
7 Oct 2026ot npm served timeout
What's Changed
ot npm served timeout
0.5.15
7 Oct 2026ot readd memory
release: publish dependencies first and resume failed publishes
native: reject invalid blended cell coordinates
What's Changed
ot readd memory
release: publish dependencies first and resume failed publishes
native: reject invalid blended cell coordinates
core: treat non-positive draw extents as empty
core: preserve host-provided bare specifiers in runtime plugin
Release v0.5.15
0.5.14
30 Sep 2026core: repaint after net-zero resize bursts
What's Changed
core: repaint after net-zero resize bursts
0.5.13
30 Sep 2026native: fix Zig dependency extraction on Windows
Fix/kitty z above cell background
ssh: disable Nagle's algorithm on accepted connections
What's Changed
native: fix Zig dependency extraction on Windows
Fix/kitty z above cell background
ssh: disable Nagle's algorithm on accepted connections
fix portal teardown leak
terminal: keep tmux capability replies in the pane that asked
core: skip native resets of a text view with no selection
native: resize buffers in place while the cells fit
native: round color bytes without a libm call
core: encode draw text into reused storage
native: release replaced text buffer ropes
renderable: defer layout for late children
0.5.12
22 Sep 2026add comprehensive mouse pointer style support and update related functionality
renderer: fix split-footer resize cleanup
editor: allow cursor reads after destruction
What's Changed
add comprehensive mouse pointer style support and update related functionality
renderer: fix split-footer resize cleanup
editor: allow cursor reads after destruction
core: pass markdown fg through to table cells
implement per-line text alignment support textAlign
core: keep worker error listener on termination
Docs website Scrollbar fix
core: support transparent background in embedded terminals
text-buffer: preserve inline continuation alignment
0.5.11
7 Sep 2026core: bound large diff rendering work
core: realign split diffs after resizing
core: preserve keyboard layouts in embedded terminals
What's Changed
core: bound large diff rendering work
core: realign split diffs after resizing
core: preserve keyboard layouts in embedded terminals
web: add another color theme
runtime: support Bun 1.4
native: allow line wrapping between CJK characters
markdown: fix hyperlink rendering and detection
core: support PCM through the shared audio stream interface
0.5.10
1 Sep 2026renderer: reduce image streaming overhead
core: retry the final automatic frame after backpressure
core: preserve markdown during stream completion
What's Changed
renderer: reduce image streaming overhead
core: retry the final automatic frame after backpressure
core: preserve markdown during stream completion
renderer: preserve native scrollback images
native: ship stripped libraries with separate release symbols
0.5.9
27 Aug 2026core: normalize embedded terminal keys
native: silence standard library logs
native: filter Ghostty logs
What's Changed
core: normalize embedded terminal keys
native: silence standard library logs
native: filter Ghostty logs
renderer: prevent backpressure revive
renderer: resolve snapshot links from snapshot pool
ot ghostty vignette
native: export audio module to Zig consumers
renderer: ignore invalid terminal dimensions
core: align bordered hit-grid clipping
ffi: preserve viewport lookup success result
ssh: assert error code for occupied ports
native: prove styled text without JavaScript
web: add rendered terminal examples to docs
core: limit embedded terminal selection highlights to text
native: preserve truncated copy ranges
native: preserve mixed whitespace graphemes
core: skip tab refresh for tab-free roots
core: preserve edit cursors across tab widths
native: expose Yoga C API to Zig consumers
native: reduce word layout work with eager view metadata
0.5.8
24 Aug 2026utf8: count Thai spacing marks in widths
mouse: track buttons independently
ot readme docs
What's Changed
utf8: count Thai spacing marks in widths
mouse: track buttons independently
ot readme docs
terminal: match Ghostty wide grapheme widths
0.5.7
23 Aug 2026ot web font
web: update font
core: add configurable selection occupancy
What's Changed
ot web font
web: update font
core: add configurable selection occupancy
renderer: reject forced Sixel for non-hosts
core: selection no longer collapses with y below, x left of text
web: add OpenTUI Mono typeface
core: add double/triple click support
core: fix portable boolean FFI calls
node: support Node.js 26.4 and later
core: test u32 cell attributes on all platforms
core: correlate OSC 66 probe responses
0.5.6
20 Aug 2026Maintenance only: docs, tests, build or refactoring. Nothing changes in how you use it.
0.5.4
18 Aug 2026ot web blue
web: add package index
web: improve documentation readability
What's Changed
ot web blue
web: add package index
web: improve documentation readability
packages: add remote facts for community entries
packages: declutter package headers
scrollback: add package index text
core: use blocks fallback for forced Sixel placements on Kitty
renderer: clear stale mouse input
core: fix GNU Screen OSC 52 passthrough
0.5.3
13 Aug 2026core: clip glyphs left of the buffer
web: rebuild site + add blog
ot fix web mobile
0.5.2
12 Aug 2026bench: add JS benchmark
web: upgrade Astro
web: add pixel OpenTUI logo mark
What's Changed
bench: add JS benchmark
web: upgrade Astro
web: add pixel OpenTUI logo mark
bench: add Node JS benchmark runner
core: migrate to zig 0.16
web: pin Bun types to 1.3.14
core: add embedded terminal runtime
image: accept retained NativeImage sources
Clipboard and paste
render: bound streaming code highlights
0.5.1
4 Aug 2026core: accept trailing PNG whitespace
core: support ICC PNG profiles
0.5.0
3 Aug 2026core: speed up Node FFI layout reads
core: add raw stdin logging
core: reuse FFI struct storage
What's Changed
core: speed up Node FFI layout reads
core: add raw stdin logging
core: reuse FFI struct storage
runtime: bundled asset path fallback
docs/audio: clarify engine lifecycle
core(renderer): write Windows console output with WriteConsoleW
core: add audio input capture
core: prevent duplicate live frame timers
three: load Jimp only for image operations
core: emit input handler errors
react: support Node ESM imports
core: emit render errors
core: harden renderer resolution lifecycle
core: add native image rendering
- 0.4.x
0.4.5
17 Jul 2026Reading the note…
0.4.4
16 Jul 2026Reading the note…
0.4.3
3 Jul 2026Reading the note…
0.4.2
24 Jun 2026Reading the note…
0.4.1
11 Jun 2026Reading the note…
0.4.0
9 Jun 2026Reading the note…
- 0.3.x
0.3.4
7 Jun 2026Reading the note…
0.3.3
7 Jun 2026Reading the note…
0.3.2
4 Jun 2026Reading the note…
0.3.1
1 Jun 2026Reading the note…
0.3.0
28 May 2026Reading the note…
- 0.2.x
0.2.16
26 May 2026Reading the note…
0.2.15
20 May 2026Reading the note…
0.2.14
18 May 2026Reading the note…
0.2.13
17 May 2026Reading the note…
0.2.12
16 May 2026Reading the note…
0.2.11
16 May 2026Reading the note…
0.2.10
14 May 2026Reading the note…
0.2.9
13 May 2026Reading the note…
0.2.8
13 May 2026Reading the note…
0.2.7
12 May 2026Reading the note…
0.2.6
8 May 2026Reading the note…
0.2.5
8 May 2026Reading the note…
0.2.4
7 May 2026Reading the note…
0.2.3
6 May 2026Reading the note…
0.2.2
2 May 2026Reading the note…