github anomalyco/opentui v0.9.0
Release v0.9.0

49 minutes ago

In 0.9.0, Markdown runs on OpenTUI's native engine, the same engine the Rust widget uses. In 0.8.3, MarkdownRenderable parsed with marked in JavaScript and lexed the open end of the document again on every chunk. In a growing list, the open end is the whole list, so streaming one list took 21,430 million instructions. MarkdownRenderable also sent all prose to the Tree-sitter worker for styling. The native engine reads each chunk once and writes paragraphs and headings into their text nodes itself. Streaming the same list now takes 5.9 million instructions. Over one streamed answer, JavaScript's styled-text writes for prose went from 1,595 to 0, and the prose characters sent to the highlighter went from 56,223 to 0.

Because the engine follows CommonMark 0.31.2 and GFM, some documents look different. While streaming, a line that can still change kind, such as a possible table row or a heading underline, waits for its end before it shows. A table appears when its delimiter row arrives, and task items show ☐ and ☑. The token tree is gone, so renderNode(token, context) becomes renderBlock(block, context), and you switch on block.kind instead of token.type:

// 0.8.3
renderNode: (token) => {
  if (token.type !== "heading") return undefined
  return new TextRenderable(renderer, { content: token.text.toUpperCase() })
}
// 0.9.0
renderBlock: (block) => {
  if (block.kind !== "heading") return undefined
  return new TextRenderable(renderer, { content: block.text.toUpperCase() })
}

internalBlockMode is gone: every top-level block is its own renderable, one blank row apart. Code that read the parse state reads blocks and committedBlockCount instead, and the React and Solid prop is renderBlock too. The Markdown page has the full migration list.

The new inlineHook option replaces the private chunk callback that apps had to override to show IDs as titled links. That callback missed the streaming preview and table cells, so on a typical answer 9 of 18 frames showed the raw ID. The hook gets the spans of every paragraph, heading, and table cell and returns the spans to show. A span with a target in a paragraph or heading becomes clickable through markdown.targetAt(x, y). inlineHookFilter runs the hook only on blocks that contain one of its strings, so other blocks stay on the native path:

const markdown = new MarkdownRenderable(renderer, {
  content,
  syntaxStyle,
  inlineHookFilter: ["ses_"],
  inlineHook: (spans) =>
    spans.map((span) => {
      const title = span.code ? titles.get(span.text) : undefined
      return title ? { ...span, text: title, target: span.text } : span
    }),
})

Input now has one stdin parser, the native one. 0.8 had two: the TypeScript parser was the default, and the native parser was opt-in through experimental_nativeInput. They did not always return the same keys for the same bytes. The TypeScript parser gave ESC , no key name, dropped CSI 1;2 R instead of reporting Shift+F3, and read an SGR button-32 report as a move instead of a drag. Remove experimental_nativeInput and stdinParserMaxBufferBytes from your renderer config. To report a parser bug, attach an OTUI_STDIN_LOG capture. parseKeypress() keeps its signature but decodes with the native parser, so under Node it needs --experimental-ffi. You can feel one change: once a terminal confirms the Kitty keyboard protocol, Escape arrives at once instead of after a 20 ms wait.

The declarative composition layer is gone too: h(), delegate(), vstyles, and the construct factories such as Box() and Text(). Create renderables with their classes and add their children:

// 0.8.3
renderer.root.add(Box({ border: true }, Text({ content: "Done" })))
// 0.9.0
const box = new BoxRenderable(renderer, { border: true })
box.add(new TextRenderable(renderer, { content: "Done" }))
renderer.root.add(box)

Use the t template instead of vstyles, for example t`${bold("Done")}` , and the renderAfter option instead of Generic(). Renderable.add() now accepts only a Renderable, so narrow a nullable value before you add it.

React and Solid now handle a removed prop the same way. They used to map JSX props to renderables in separate code, and the two disagreed. In Solid, a key removed from style kept its old value, so style={on ? { border: true } : {}} left the border on screen. React wrote null for a removed prop, and opacity = null means 0, so removing opacity hid the box. Both reconcilers now call setRenderableProp in Core and write a removed prop as undefined, which restores the default. If your custom setter checks value === null to detect removal, make it accept undefined too.

Two new edit buffer methods convert between JavaScript string indexes and cell offsets. Edit buffer offsets count display cells, but JavaScript indexes a string in UTF-16 units, and the two diverge at the first tab or wide character. In ab\tcd with the default tab width of 2, d is at string index 4 but at cell offset 5, so a plugin that passed the string index deleted c. Neither method moves the cursor:

buffer.setText("ab\tcd")
const offset = buffer.stringIndexToCellOffset(buffer.getText().indexOf("d")) // 5
buffer.setCursorByOffset(offset)
buffer.cellOffsetToStringIndex(buffer.getCursorPosition().offset) // 4

The new layoutMode: "list" makes long columns cheaper to lay out. Before, a long column, such as a transcript, made Yoga run its flex pass over every row when one row changed. A list lays out its rows as a column and reuses the size of each row that did not change. Rows never grow or shrink; otherwise, a list lays out like a flex column with overflow: "scroll". For ScrollBox content, set contentOptions: { layoutMode: "list" }. In a 1,000-row select, a frame that appends one row costs 1.54 million instructions in list mode. Before list mode reused row sizes, the same frame cost 22.39 million.

Breaking changes

  • core: MarkdownRenderable parses with the native engine, and marked is no longer a dependency. renderBlock(block, context) replaces renderNode(token, context), also as a React and Solid prop. blocks and committedBlockCount replace internalBlockMode and the parse state. Paragraphs and headings are TextRenderables, so code that set onChunks or onHighlight on them moves to inlineHook. The hook runs when the content or options change, also inside a setter, not when a highlight arrives. See Markdown. (#2079, #2023)
  • core: A MarkdownCodeBlockRenderer gets a MarkdownCodeBlock instead of Tokens.Code. Its closed field tells whether the closing fence has arrived. See Markdown. (#2079)
  • core: The vnode composition API is removed: h(), VNode, delegate(), instantiate(), Generic(), VRenderable, vstyles, and the factories Box(), Text(), Input(), Select(), TabSelect(), ASCIIFont(), FrameBuffer(), Code(), and ScrollBox(). Create renderables with new and add(), use the t template instead of vstyles, and use the renderAfter option instead of Generic(). See Renderables. (#2000)
  • core: Renderable.add() and insertBefore() accept only a Renderable, and TextRenderable accepts only text children. Narrow a nullable or BaseRenderable value before you add it. See Renderables. (#2000)
  • core: Input always uses the native parser. The experimental_nativeInput and stdinParserMaxBufferBytes options, the OTUI_NATIVE_INPUT_PARSER and OTUI_NATIVE_INPUT_SHADOW variables, and StdinParser with its types are removed. Use renderer.keyInput, prependInputHandler, or subscribeOsc instead. This fixes the F3 bugs that @FasterPHP reported. See Keyboard input. (#1981)
  • core: MouseParser and nonAlphanumericKeys are removed. parseKeypress() decodes with the native parser, needs --experimental-ffi under Node, and returns null for empty input. Use renderable mouse events or createMockMouse instead of MouseParser, and terminalNamedSingleStrokeKeys instead of nonAlphanumericKeys. See Keyboard input. (#2016)
  • core: React and Solid write a removed prop or style key through the new setRenderableProp as undefined, not null, so the setter restores its default. A custom setter that checks value === null must also accept undefined. See React bindings. (#2018)
  • core: InputRenderableOptions no longer has onSubmit, which InputRenderable never called. Listen for the enter event instead. Contributed by @posva. See Input. (#1216)
  • core: ExtmarksController.adjustExtmarksAfterDeletion() is removed; the edit buffer now moves extmarks itself. For a malformed range, create() returns 0 instead of throwing an error. Because the buffer owns its extmarks, a controller's destroy() leaves them in place; call clear() to drop them. See Editing buffers and views. (#2104)
  • core: Siblings with the same zIndex paint in child order instead of the order they were added. A renderable inserted or moved before a sibling paints below it, and where they overlap, the later sibling gets the click. This also applies to a Solid <Show> before an overlapping sibling. To keep a renderable on top, give it a higher zIndex. See Renderables. (#2119)
  • native: You need Zig 0.17.1 to build the native library and the Zig module. This version fixes an LLVM miscompile of casts from signed to narrower unsigned values. See Zig. (#2045)
  • native: In the Zig module, yoga.yogaNodeGetContentBoxChecked(node, &box) is now yoga.yogaNodeGetLayoutChecked(node, &layout, &box). yoga_c.otYogaNodeGetComputedLayout takes a third argument, null or a four-float content box. See Zig. (#2090)
  • native: opentui.h has new calls and types, but OT_CONTEXT_ABI_VERSION stays 2. Build a C or Rust host with the header, bindings, and library from the same release. opentui-sys fails its build when the library's header differs from its bindings. See C. (#1949, #2049, #2104)
  • rust: The experimental crates opentui-sys, opentui, and opentui-ssh moved into the OpenTUI repository and are not published, so depend on them by Git revision. On Linux and macOS they link the static library by default; for the shared library, turn off default features in every crate that depends on them. On Windows, ship opentui.dll beside the app. See Rust. (#1946, #1949, #1984, #1992)
  • rust: Markdown parses with the native engine and follows CommonMark and GFM, so rules and setext headings can render differently. Its code fences now wrap by word; a standalone Code still does not wrap. Input::max_length counts UTF-16 units, as Core does. See Rust. (#2049, #2089, #2017)
  • rust: An app's priority-0 highlight paints under syntax colors. Textareas that share one style table share one syntax theme: the one set last on any of them. After clear_highlights, a theme change brings syntax colors back only at the next edit. See Rust. (#2026, #2030)
  • rust: On Unix, Ctrl+Z stops the app's whole process group, and a suspend or release drops the rest of the input from that read. On Windows, Host::release hands the terminal to its task as on Unix. ScrollbackSurface::commit_rows fails after a change of width or width method until the surface renders again. See Rust. (#1950, #1958, #2043, #2082)

Added

  • core: EditBuffer.stringIndexToCellOffset() and cellOffsetToStringIndex() convert between a JavaScript string index and a cell offset in the buffer. They do not move the cursor. Reported by @cameronr. See Editing buffers and views. (#2110)
  • core: MarkdownRenderable takes inlineHook and inlineHookFilter to rewrite the spans of paragraphs, headings, and table cells. rerunInlineHook() runs the hook again without reparsing. targetAt(x, y) returns the target of the paragraph or heading span that the last frame drew at that position. See Markdown. (#2023, #2079)
  • core: With extensions: ["math"], MarkdownRenderable parses inline math into spans with math set, and display math into a math code block. See Markdown. (#2079, #2078)
  • core: Markdown table columns, header included, follow the alignment in their delimiter row. TextTableRenderable takes a columnAlignments option with "left", "center", or "right" for each column. See TextTable. (#2112)
  • core: The alignContent and aspectRatio layout options work on renderables, ScrollBox content, and React and Solid props. getAlignContent() and getAspectRatio() read them. Requested by @nwidynski. See Layout. (#2096)
  • core: layoutMode: "list" lays out children as a column without a flex pass over each row, so a change to one row lays out only that row. ScrollBox content takes it through contentOptions. See Layout. (#1989, #1960, #1987)
  • core: StyleDefinition and theme token styles take strikethrough, and Code, Markdown, and Tree-sitter styles draw it. See Code. (#2027)
  • core: In a Textarea, Cmd+Backspace (super+backspace) deletes to the start of the line in terminals that report Super. If your app uses this key, override keyBindings. Contributed by @remorses. See Textarea. (#817)
  • ssh: The server accepts OPENTUI_FORCE_TRUECOLOR from the client environment. See SSH. (#1967)
  • native: OPENTUI_FORCE_TRUECOLOR=1 forces 24-bit color, for example inside GNU screen with its truecolor setting on. See Environment variables. (#1967)
  • native: zig build -Dvideo=false builds the same native API without FFmpeg. A video in such a build reports that decoding is unavailable. Requested by @AaronFeledy. See Deploy an OpenTUI application. (#2098)
  • native: The ot_markdown_* functions expose the streaming Markdown engine. They create a document, append or set its source, read changed blocks and leaves, and bind paragraphs to text nodes. OT_MD_BLOCK_ALIGN_LEFT and OT_MD_BLOCK_ALIGN_RIGHT give the alignment of a table cell. See C. (#2037, #2049, #2112)
  • native: ot_edit_buffer_mark and ot_edit_buffer_get_marks store marks in an edit buffer, which moves them with edits and restores them on undo. ot_edit_buffer_convert_offset converts between bytes, UTF-16 units, and cells. See C. (#2094, #2110)
  • native: ot_edit_buffer_set_layer and ot_text_buffer_set_layer write a highlight layer of ot_highlight_run byte runs in one call. ot_context_group_set registers a group set of up to OT_GROUP_SET_NAME_COUNT_MAX style names. See C. (#2026, #2091, #2030)
  • native: OT_STYLE_ENUM_LAYOUT_MODE with OT_LAYOUT_MODE_LIST gives a box list layout. ot_scene_get_layout with OT_LAYOUT_CONTENT returns the content rect of a text node inside its padding. See C. (#1960, #2054)
  • native: New scene calls: ot_scene_hit_test_excluding skips one node, ot_scene_get_text_byte_at returns the byte that the last frame drew at a cell, and ot_scene_clear_children detaches all children in one pass. ot_scene_painted tells whether the last frame painted a node. See Frames and output. (#2065, #2019, #1943, #2036)
  • native: OT_EDITOR_STYLE_ATTRIBUTE_MASK tells which attribute bits a syntax style supplies, so a later style name can clear bold as well as set it. See C. (#2027)
  • native: OT_INPUT_EXPECT_ESCAPE_PREFIX makes the input parser read Escape without a timeout, and OT_CONTROL_END_REPLY_SENTINEL ends the reply sentinel. See Host I/O and time. (#1948)
  • native: opentui.h defines macros for values it used to describe only in comments: text attributes, Kitty keyboard flags, cell kinds, the indexed palette, embedded terminal codes, cursor styles, limits, and Yoga enums. The values do not change. See C. (#2020)
  • native: The OT_SCENE_RECORD_CELLS paint record draws a run of ot_scene_cell cells in rows of a given width, each as a blended cell draw would. Rust widgets write it with Painter::cells. See Frames and output. (#2128)
  • rust: App::on_animation_tick and Widget::on_animation_tick run animations on Core's shared clock, so animations with equal rates tick in the same frames. An animation bound to a widget parks while the widget is hidden, detached, or clipped out of view. AnimationTick::follow adds a widget to an animation, and Widget::downgrade returns a WeakWidget that does not keep the widget alive. See Rust. (#2120, #2134)
  • rust: ScrollBox::on_scroll reports each move of a scroll box once, with its position and cause. See Rust. (#2129)
  • rust: Code::finish() frees the parse tree of a view whose text stopped growing, and the next change parses again. A transcript with 100 written files uses 36 MB less heap. See Rust. (#2126)

Changed

  • core: Markdown follows CommonMark 0.31.2 and GFM. While streaming, a line that can still change kind waits for its end, and a table shows when its delimiter row arrives. Task items show ☐ and ☑. See Markdown. (#2079, #2078)
  • core: Each top-level Markdown block is its own renderable, one blank row apart, so apps on the old "coalesced" default now see blank rows between some blocks. Set tableOptions.style: "columns" to get the old top-level table style. See Markdown. (#2079)
  • core: The default Markdown style strikes through ~~text~~ unless the theme sets strikethrough: false. See Markdown. (#2027)
  • core: A streaming code fence in Markdown sends only its new text to the Tree-sitter worker. New text shows plain until its highlight arrives, and getLineHighlights() on a streaming fence returns no syntax records. MockTreeSitterClient in @opentui/core/testing has highlightAppend(). See Syntax highlighting with Tree-sitter. (#2102)
  • core: Extmarks live in the native edit buffer. Undo and redo restore them, IDs are never reused, queries return extmarks in (start, id) order, and an edit that changes part of a virtual extmark drops it. Extmarks no longer clear the buffer's other highlights on each edit. See Editing buffers and views. (#2104, #2094)
  • core: When a terminal confirms the Kitty keyboard protocol, Escape arrives at once instead of after 20 ms. Kitty, Ghostty, foot, and Alacritty confirm it, but WezTerm by default and tmux do not. See Keyboard input. (#1948, #1971)
  • core: Theme and palette queries end at the DA1 reply instead of on timers. waitForThemeMode() resolves with null when no OSC 11 reply arrives, and getPalette() reports the default colors of a terminal that does not answer OSC 4. See Terminal capabilities. (#1969)
  • core: In split-footer mode, a frame writes up to 64 captured stdout rows instead of 8, so output at 2,000 rows per second no longer falls behind. writeToScrollback commits stay at 8 per frame. See Renderer. (#2084)
  • three: Writing ThreeRenderable.aspectRatio sets the Yoga layout ratio, which getAspectRatio() returns; reading it still returns the display ratio. An aspectRatio passed at construction no longer throws. See Three.js WebGPU. (#2101)
  • native: Inside GNU screen, OpenTUI ignores COLORTERM and WT_SESSION and writes 256-color SGR, because screen drops 24-bit colors by default. Set OPENTUI_FORCE_TRUECOLOR=1 when screen's truecolor setting is on. See Terminal capabilities. (#1967, #1982)
  • native: WT_SESSION enables OSC 8 hyperlinks on every host OS when TERM is unset, empty, or starts with xterm and no multiplexer is present. So a Windows Terminal client gets clickable links from a Linux or macOS host when a remote session forwards WT_SESSION. See Terminal capabilities. (#1976)
  • native: Notifications in tmux and GNU screen use the same passthrough as OSC 52. Inside screen, each non-ASCII character in OSC 777 and OSC 9 text becomes a space. See Notifications. (#2021)
  • native: Each Linux libopentui.a includes a private libc++ and libc++abi. A static link needs no system libc++ and also works on musl. See C. (#1991)
  • native: When two highlight ranges with the same priority cover a cell, the later range wins. Before, the winner depended on hash-map order. See Editing buffers and views. (#2025)
  • native: Layout does less work per frame. Rounding visits only the subtrees that a solve laid out, a larger height cap counts as a cache hit, and Linux builds call Yoga's inline helpers directly. A status glyph beside 10,000 rows costs 30.6% fewer instructions per frame. See Rendering pipeline. (#1951, #1966, #2040, #2090)
  • native: OpenTUI builds highlight spans at paint time, only for the lines that a view draws, and a layer write splices only the runs that change. 4,000 ranges on one 24,000-byte line cost about 47 million instructions instead of 5.95 billion. See Rendering pipeline. (#2025, #2100, #2111)
  • native: A text or Display write that changes nothing does no work, and removing a first or last child takes one search step. Destroying 40,000 children in reverse order takes 0.84 billion instructions instead of 13.2 billion. See Renderables. (#1942, #1943)
  • rust: Code, Diff, and Textarea views no longer wrap their text at width 0 before their first frame, and Code views and Diff rows share their theme and signs instead of copying them. 100 Code views mounted before their first draw take 20 MB instead of 438 MB. See Rust. (#2124, #2123)

Fixed

  • core: In a ScrollBox with 16 or more children, a Code or Markdown block that changes in the same tick that moves it out of view measures its new content at once. Descendants of a culled child report their solved layout. See ScrollBox. (#2003)
  • core: A streaming Code block keeps its highlight on screen when it scrolls away during a highlight, when rows above it go away, and through a theme or conceal change. See Code. (#2036)
  • core: Committed Markdown blocks stay final while streaming. A split-footer transcript no longer loses the paragraph after a list or near link reference definitions. See Markdown. (#2029, #2039)
  • core: Text with padding paints, wraps, hits, and selects inside its padding. Before, it painted at the outer edge of its box and wrapped at the full width. Remove any indent that you added to make up for this. See Text. (#2054)
  • core: Layout after a change now matches a fresh layout. This fixes text widened by minWidth, children one cell off in a cached box, a column in a baseline-aligned row, stale wraps after small width changes, and nodes added under display: none, which now read 0 × 0. See Layout. (#1947, #1968, #1995, #2009)
  • core: Without wrapping, the visual line end (Meta+E or Cmd+Right) moves the cursor to the end of a line that is wider than the field. See Input. (#2053)
  • core: A wheel step that moves the Textarea cursor onto a wrapped row keeps the cursor on that row, so the wheel can show the middle rows of a long paragraph. See Textarea. (#2076)
  • core: Left from the end of a virtual extmark goes to the character before it, or to its start when another virtual extmark ends there, also after a CJK character, emoji, or tab. A wheel scroll, click, or drag no longer leaves the cursor inside an extmark. See Editing buffers and views. (#2050, #2064)
  • core: A left press during a held gesture means the terminal dropped the release, so OpenTUI ends that gesture first. Clicks work again after a lost release, and a ScrollBox stops its auto-scroll. Reported by @mrbubble62. See Interaction, focus, and selection. (#2065)
  • core: Under the Kitty protocol, text typed quickly after Escape, such as OA or [TODO], arrives as typed. A bracketed paste arrives whole even when a pause splits its start marker. See Keyboard input. (#2046, #1971)
  • core: With reportText, Kitty key text longer than 8 characters arrives whole, up to about 42 CJK characters. See Keyboard input. (#1972)
  • core: A focused EmbeddedTerminalRenderable sends Ctrl+J to its program when the host terminal does not use the Kitty protocol. See Embedded terminal. (#2022)
  • core: In an InputRenderable at maxLength, typed or pasted text replaces the selection. A cut at maxLength no longer splits a character. See Input. (#1999, #2017)
  • core: DiffRenderable keeps a blank context line and numbers the lines after it correctly. See Diff. (#2017)
  • core: A click on a Slider thumb without pointer movement keeps the value and emits no change event. A click on the track emits one change event. Both fixes also apply to scrollbars. Contributed by @Kidyoh. See Slider. (#2031)
  • core: remove() accepts a child from another copy of the Core bundle, as add() already did. Contributed by @AksharP5 and reported by @letrquan. See Renderables. (#1535)
  • core: EditBuffer.getText() and TextBuffer.getPlainText() keep a leading U+FEFF. Contributed by @sean0407. See Editing buffers and views. (#1626)
  • core: Setting treeSitterClient on Diff and Markdown, scrollMargin on editors, the ASCIIFont selection colors, or the line-number gutter props after construction now takes effect, so these props work in Solid JSX. Reported by @filipsobol. See Solid bindings. (#2097)
  • core: The Tree-sitter worker frees replaced syntax trees and failed parsers and runs the messages of each buffer in order. 200 full edits of a large file no longer grow the worker from 32 MiB to 166 MiB, and a buffer removed during createBuffer() stays removed. Reported by @JosXa, @RaphaelFakhri, and @Osraka. See Syntax highlighting with Tree-sitter. (#1978, #2002, #2073)
  • core: suspend() keeps the exit-signal handlers until it settles. A SIGTERM or SIGHUP during a suspension restores the terminal. See Lifecycle and cleanup. (#2083)
  • core: With externalOutputMode: "capture-stdout", a combining mark or VS16 at the start of a write joins the previous character. Captured rows use the renderer's width method, so text after a wide cluster stays visible. See Renderer. (#2067, #1929)
  • core: A split-footer app keeps its history across an alternate-screen round trip. When the footer grows in xterm.js terminals such as VS Code, the top rows go to scrollback. Based on work by @viralcodex, and reported by @cevr. See Renderer. (#2041, #2042)
  • core: The all-available clipboard writes OSC 52 after the host write settles, so the terminal keeps the copy on Wayland and X11. Contributed by @clemenswasser. See Clipboard. (#2028)
  • core: A linked plugin package that imports a host module by name gets the host's copy, so Solid ownership, memos, and stores work across it. Reported by @cevr. See Load plugins and modules at runtime. (#2095)
  • core: A video decode failure at the end of a clip or at a loop fires onError instead of onEnd. See Video. (#2074)
  • core: An autoplay video fires its first onTimeUpdate after onReady, and reports a failed state read through onError once. See Video. (#2127)
  • core: setCursorByOffset() with an offset past the end of the text moves the cursor to the end instead of the start. Contributed by @latent-9. See Editing buffers and views. (#2131)
  • solid: The Solid transform compiles JSX files in node_modules again, so runtime plugins from npm or git get reactive props. See Load plugins and modules at runtime. (#2051)
  • react: The published bundle uses react/jsx-runtime, so the error boundary shows its fallback under NODE_ENV=production instead of a blank screen. Reported by @nwidynski. See React bindings. (#2092)
  • keymap: A leader or Neovim-style sequence gives queued input a turn before it expires. Ctrl+X then f runs the shortcut even after an input stall. Reported by @cevr. See Built-in keymap addons. (#2099)
  • native: On Windows and macOS, a clipboard write or clear that blocks in the OS call ends as timed-out at its deadline. The text can still land later. See Clipboard. (#2038)
  • native: In kitty, the OSC 66 startup probe no longer overwrites the top two history rows of a split-footer app. The main screen gets no drawing probe, and OpenTUI recognizes kitty 0.40+ and foot 1.21+ from their XTVERSION reply. See Terminal capabilities. (#2056)
  • native: Inside GNU screen, startup no longer prints +q4d73. A remote session detects screen from TERM, so OSC 52 copies are wrapped and emoji widths match. See Terminal capabilities. (#1945, #1961)
  • native: When a reply selects wcwidth, OpenTUI turns off mode 2027, and teardown restores the mode as it was before startup. Reported by @mmick66. See Terminal capabilities. (#1986)
  • native: On a Windows host, remote sessions ask the client terminal about 24-bit color and skip the ConPTY close teardown. See Terminal capabilities. (#1957, #1959)
  • native: An image whose Kitty upload does not fit one frame of output shows as Kitty graphics instead of half blocks: OpenTUI sends one scaled copy, and scrolling or resizing only places it again. Half-block cells drawn over a Kitty image go away with their image. See Image. (#2135, #2130)
  • rust: Timers, deferred tasks, and quit() calls that a frame starts run on time in an otherwise idle app. Before, they waited for the next unrelated wakeup, so a headless app could fail to end. See Rust. (#2118)
  • rust: A split-footer app whose terminal does not answer the cursor-position query sleeps between startup replies instead of using a full core for five seconds. See Rust. (#2122)
  • rust: An app that ends with deferred tasks, timers, handlers, or spawned tasks still queued no longer panics or hangs in teardown. See Rust. (#2132, #2120)

Security

  • native: An OSC 8 link target must be non-empty UTF-8 of at most 512 bytes with no control characters. Before, an ESC or BEL in a link target ended the sequence, and the terminal ran the rest as commands. Reported by @carsteneu, who also wrote the first fix. See Terminal capabilities. (#1996)
  • native: Notification text must be UTF-8, and C0, DEL, and C1 characters become spaces. OSC 9 text that starts with a digit gets a leading space, and triggerNotification() returns false in ConEmu. See Notifications. (#1998)

API changes and documentation: https://opentui.com/docs/releases/0.9.0

What's Changed

  • native: skip scene text and Display writes that change nothing by @simonklee in #1942
  • native: detach children without scanning their siblings by @simonklee in #1943
  • rust: import opentui-rust into packages/rust by @simonklee in #1946
  • native: move Yoga to upstream main and fix three layout cache bugs by @simonklee in #1947
  • rust: generate the bindings, link in-repo, follow core's version by @simonklee in #1949
  • rust: drop the rest of a read's input at suspend and release by @simonklee in #1950
  • ci: test the Rust crates and skip TypeScript CI on Rust-only PRs by @simonklee in #1952
  • rust: wait for any frame in the lifecycle tests by @simonklee in #1953
  • rust: measure text with the width method the terminal reports by @simonklee in #1955
  • native: round only the Yoga subtrees a solve laid out by @simonklee in #1951
  • native: skip the XTGETTCAP Ms query inside GNU screen by @simonklee in #1945
  • native: drop the Escape timeout once Kitty disambiguate is confirmed by @simonklee in #1948
  • ci: stop the Bun 1.3 job from caching 1.3.14 as 1.4.1 by @simonklee in #1956
  • native: ask a remote terminal for 24-bit color on Windows too by @simonklee in #1957
  • core: measure captured stdout with the renderer's width method by @simonklee in #1929
  • rust: stop the whole process group on Ctrl+Z by @simonklee in #1958
  • native: keep the ConPTY close teardown off remote sessions by @simonklee in #1959
  • rust: wait for both timers in the real-clock headless test by @simonklee in #1963
  • rust: skip the termios check after a shell exits in pty-smoke by @simonklee in #1962
  • rust: add VideoView over the native video node by @simonklee in #1964
  • native: detect GNU screen in auto-detected remote sessions by @simonklee in #1961
  • rust: remove PLAN.md by @simonklee in #1965
  • native: keep GNU screen at 256 colors, add OPENTUI_FORCE_TRUECOLOR by @simonklee in #1967
  • rust: wait for each key in the ssh idle timeout test by @simonklee in #1970
  • native: keep Kitty key text past 8 characters by @simonklee in #1972
  • rust: pace the ssh slow-client tests on a clock by @simonklee in #1974
  • native: take a larger-height-cap match as a Yoga cache hit by @simonklee in #1966
  • core: end theme and palette waits at their DA1 reply by @simonklee in #1969
  • native: align a cached column by its child's unrounded top by @simonklee in #1968
  • native: read a raw ESC by its read under confirmed Kitty by @simonklee in #1971
  • native: enable WT_SESSION hyperlinks on every host OS by @simonklee in #1976
  • rust: keep mode con output on screen in the Windows size test by @simonklee in #1979
  • ci: run Rust (macOS) on pull requests that change the crates by @simonklee in #1980
  • core: remove the legacy stdin parser by @simonklee in #1981
  • native: keep WT_SESSION from turning on 24-bit color in screen by @simonklee in #1982
  • rust: link the static archive from a folder without the dylib by @simonklee in #1983
  • core: free replaced syntax trees in the parser worker by @simonklee in #1978
  • native: add a list layout mode for boxes by @simonklee in #1960
  • native: reset mode 2027 when the width method leaves it by @simonklee in #1986
  • core: stop a timed-out session sim run from breaking the next by @simonklee in #1985
  • rust: build opentui-sys for every native platform by @simonklee in #1984
  • native: skip clean rows of a list by @simonklee in #1987
  • rust: wake ssh each second in the OpenSSH client test by @simonklee in #1988
  • core, rust: add a layoutMode option for list layout by @simonklee in #1989
  • rust: give images the terminal's pixel size by @simonklee in #1990
  • native: bundle a private libc++ in the Linux static archive by @simonklee in #1991
  • rust: link the static archive by default by @simonklee in #1992
  • native: run the C context test in pull request CI by @simonklee in #1993
  • fix(core): preserve leading BOM in text buffer reads by @sean0407 in #1626
  • native: keep control characters out of OSC 8 link targets by @simonklee in #1996
  • native: compare measured sizes exactly under absolute-edge rounding by @simonklee in #1995
  • fix(core): allow removing renderables across module copies by @AksharP5 in #1535
  • fix(core): drop dead onSubmit option from InputRenderableOptions by @posva in #1216
  • core: measure off-screen Code content changes at once by @simonklee in #1994
  • feat: add Cmd+Backspace keybinding to delete to line start by @remorses in #817
  • web: add the 0.8.4 release notes and API record by @simonklee in #2004
  • rust: wait for the warm-up reaper before counting descriptors by @simonklee in #2010
  • rust: read Markdown content without copying it by @simonklee in #2005
  • rust: stop allocating per line and per code block in Markdown by @simonklee in #2006
  • rust: add syntax::prewarm to compile grammars ahead by @simonklee in #2007
  • rust: build the keymap's active view once per key by @simonklee in #2008
  • native: zero out nodes added under display: none by @simonklee in #2009
  • core: keep culled ScrollBox content's layout exact by @simonklee in #2003
  • core: let Input typing replace a selection at maxLength by @simonklee in #1999
  • core: keep tree-sitter buffer edits in order and free failed parsers by @simonklee in #2002
  • native: keep notification text inside its OSC by @simonklee in #1998
  • core: share one prop mapping between React and Solid by @simonklee in #2018
  • core: keep streamed Markdown blocks final once they are stable by @simonklee in #2029
  • core: send Ctrl+J from a legacy host to the embedded terminal by @simonklee in #2022
  • core, rust: share conformance vectors and fix where the two disagreed by @simonklee in #2017
  • core: remove the vnode/construct composition API by @simonklee in #2000
  • core: let a Markdown hook rewrite prose spans by @simonklee in #2023
  • native: send notifications through GNU screen like OSC 52 by @simonklee in #2021
  • rust: let a Markdown hook rewrite prose spans by @simonklee in #2019
  • rust: generate key binding, key name and filetype tables from Core by @simonklee in #2014
  • build: derive or check copied versions, targets and API index by @simonklee in #2015
  • core, rust: share the CommonMark and GFM examples as one Markdown corpus by @simonklee in #2024
  • core: remove the legacy TypeScript key and mouse parsers by @simonklee in #2016
  • abi: define prose-only values in opentui.h and derive the bindings by @simonklee in #2020
  • core: import kittyKeyMap from kitty-keys.ts by @simonklee in #2032
  • ci: separate symbols when the fast Linux job misses the bottle by @simonklee in #2034
  • web: add Cyrillic to OpenTUI Mono by @iamdavidhill in #2033
  • core: keep split-footer history across an alternate-screen round trip by @simonklee in #2041
  • clipboard: write the terminal last for all-available by @clemenswasser in #2028
  • rust: write the terminal clipboard last so copies outlive the app by @simonklee in #2035
  • native: time out a clipboard write stuck in the OS call by @simonklee in #2038
  • core: keep a Slider's value when a click does not move the pointer by @Kidyoh in #2031
  • native: bind Yoga's C++ calls inside libopentui by @simonklee in #2040
  • core: keep split footer history in scrollback when the footer grows by @simonklee in #2042
  • rust: hand the terminal over on Windows in Host::release by @simonklee in #2043
  • rust: restore terminal modes on macOS and finish the Windows hand-over by @simonklee in #2044
  • solid: compile JSX in node_modules again by @kommander in #2051
  • native: build highlight spans lazily at paint by @simonklee in #2025
  • native: parse Markdown in a hidden streaming engine by @simonklee in #2037
  • rust: end a zero-width selection on Enter and deletes by @simonklee in #2048
  • core, rust: step to the character before an extmark, not into it by @simonklee in #2050
  • rust: fix one-color TextView and EditorView selections by @simonklee in #2052
  • native: end visual lines at the line end without wrapping by @simonklee in #2053
  • native: paint text inside its padding by @simonklee in #2054
  • core, rust: keep the cursor out of extmarks after the mouse and wheel by @simonklee in #2064
  • native: keep typed text after Escape and cut pastes whole by @simonklee in #2046
  • core: add property and transition rows the suite missed by @simonklee in #2068
  • rust: highlight editor syntax with one native layer call by @simonklee in #2026
  • test: add boundary rows the native and binding suites missed by @simonklee in #2069
  • test: model native handovers and bound reuse work by @simonklee in #2070
  • rust: make ScrollBox and Code follow content as Core does by @simonklee in #2071
  • core, native: draw strikethrough and compose styles per attribute by @simonklee in #2027
  • core: keep streamed Markdown blocks final around link definitions by @simonklee in #2039
  • native: fix streaming display defects in the Markdown engine by @simonklee in #2078
  • core: keep removed tree-sitter buffers removed by @simonklee in #2073
  • core, rust: end the old gesture on a left press by @simonklee in #2065
  • native: keep the wheel's cursor on the wrapped row it lands on by @simonklee in #2076
  • native: report a video decode failure that lands after the end by @simonklee in #2074
  • rust: include the start cell in Shift+arrow selections, as Core does by @simonklee in #2075
  • rust: publish a palette refresh that got only the cursor color by @simonklee in #2080
  • rust: reapply the mouse selection when Text or Code content changes by @simonklee in #2081
  • rust: commit surface rows only from a render at the width method by @simonklee in #2082
  • core: keep exit-signal handlers until suspend() settles by @simonklee in #2083
  • core: send up to 64 captured stdout rows per split frame by @simonklee in #2084
  • rust: highlight editor syntax by capture names, so themes cost no parse by @simonklee in #2030
  • test: check interaction machines and lost events in Core and Rust by @simonklee in #2087
  • rust: run Markdown on the native engine by @simonklee in #2049
  • core: run Markdown on the native engine by @simonklee in #2079
  • core: keep Code highlights on screen through culling and style changes by @simonklee in #2036
  • native: keep the OSC 66 startup probe off the main screen by @simonklee in #2056
  • core: keep a combining mark that ends up in a later stdout write by @simonklee in #2067
  • rust: parse Markdown math with an extensions option by @simonklee in #2072
  • rust: wrap long lines in Markdown code blocks, as Core does by @simonklee in #2089
  • build: update Zig to 0.17.1 by @simonklee in #2045
  • ci: give release jobs a read-only token unless they write by @simonklee in #2093
  • native: read a text node's content box with its layout by @simonklee in #2090
  • keymap: let queued keys complete timed sequences by @simonklee in #2099
  • rust: paint Code highlights as a capture-name layer by @simonklee in #2091
  • core: expose alignContent and aspectRatio by @simonklee in #2096
  • native: let apps ship OpenTUI without FFmpeg by @simonklee in #2098
  • react: use production JSX in the published bundle by @simonklee in #2092
  • three: keep camera and layout aspect ratios separate by @simonklee in #2101
  • native: keep edit marks in the edit buffer, paired with undo by @simonklee in #2094
  • core: honor renderable options assigned from Solid JSX by @simonklee in #2097
  • core: wait for renderer work instead of counting host turns by @simonklee in #2103
  • core: restore timeout margin for render model tests by @simonklee in #2106
  • native: splice set_layer runs in place by @simonklee in #2100
  • rust: keep Markdown code views when options change by @simonklee in #2105
  • core: convert between string indices and edit offsets by @simonklee in #2110
  • rust: reuse the parse tree when highlighting edits by @simonklee in #2108
  • core: send streamed Markdown fences to the highlighter as deltas by @simonklee in #2102
  • native, core, rust: align Markdown table columns by @simonklee in #2112
  • native: skip mark preparation on plain edits by @simonklee in #2111
  • core: keep extmarks in the native edit buffer by @simonklee in #2104
  • core: share host runtime modules with linked plugin packages by @simonklee in #2095
  • ci: check Three builds against Core changes by @simonklee in #2107
  • native: wait for the audio drain in the video playback test by @simonklee in #2116
  • docs: add Built with OpenCode badge by @iamdavidhill in #2121
  • rust: wake for timers and quits that a frame asks for by @simonklee in #2118
  • native: paint equal-z siblings in child order by @simonklee in #2119
  • rust: share immutable themes and diff signs by @simonklee in #2123
  • native: don't wrap text before a view has a width by @simonklee in #2124
  • rust: share Core's animation clock by @simonklee in #2120
  • rust: let split footers sleep between startup replies by @simonklee in #2122
  • rust: don't count waits for output room as idle wakeups by @simonklee in #2125
  • rust: let a finished Code free its parse tree by @simonklee in #2126
  • core: start video ticks with the native start by @simonklee in #2127
  • native: add a CELLS paint record for runs of cells by @simonklee in #2128
  • rust: add ScrollBox::on_scroll and document the frame walk order by @simonklee in #2129
  • native: clear block cells left over a Kitty image by @simonklee in #2130
  • native: clamp setCursorByOffset past the end to the document end by @latent-9 in #2131
  • rust: stop panicking when an app ends with work queued by @simonklee in #2132
  • rust: let animations follow several widgets and park when clipped by @simonklee in #2134
  • native: show large Kitty images as a copy that fits one frame by @simonklee in #2135

New Contributors

Full Changelog: v0.8.4...v0.9.0

Don't miss a new opentui release

NewReleases is sending notifications on new releases.