In 0.8.1, suspend() turned off raw mode at once, before the Session wrote the bytes that stop mouse reporting. If you moved the mouse while you pressed Ctrl+Z or opened $EDITOR, the terminal echoed the reports at the prompt. The next program then read the queued reports as typed text. After Ctrl+Z, bash ran them as commands. In 0.8.2, raw mode stays on until the promise from suspend() settles, whether it resolves or rejects. destroy() already worked this way. When the promise settles, OpenTUI also discards the input that the kernel queued on process stdin. In a pty test with mouse motion during the transition, the number of echoed and queued reports went from 4 to 0. With a 15 ms link delay, it went from 60 to 0. Keys typed during the transition, about 20 ms, are discarded too. The fix works only if you await suspend() before you give the terminal to another program:
await renderer.suspend()
spawnSync(process.env.EDITOR ?? "vi", [file], { stdio: "inherit" })
await renderer.resume()If you do not await it, the next program starts while stdin is still in raw mode and mouse reporting is still on.
One layout change can show in existing apps. Native text measurement used to clamp a text's height to Yoga's "at most" height, but only when the width was "at most" too. Yoga reuses a size that it measured at an "at most" width when the same width later comes back as exact. As a result, after an edit a text could keep a clamped height that a fresh layout would not give it, and it lost rows. The same clamp cut text in a flex-start ScrollBox to the height of the viewport, and it cut text in an auto-height absolute box to one row. Now a text's height is its wrapped line count under any height mode, the same way TextTable already measured it. Stretched text and text with an explicit width already kept their full height. Now text that is taller than its space also keeps its full height in centered and flex-start columns, so its siblings shrink or move. In a short centered dialog, the row below a long body can move onto the bottom border. To keep that row in place, give the dialog room for the body, or put the body in a ScrollBox.
MarkdownRenderable also changes how it uses renderNode. In the default coalesced mode, a change to syntaxStyle, fg, conceal, or streaming used to restyle every block as if the default renderer had made it. A custom heading from renderNode became a CodeRenderable, and custom blockquote and rule boxes lost their border color. Now coalesced mode does what top-level mode already did. When a style changes, renderNode runs again for each block with a custom node. Its result replaces the old node, and the old node is destroyed. So renderNode must return a new renderable each time it is called. If it caches a renderable and returns it again, it returns a renderable that was already destroyed:
const markdown = new MarkdownRenderable(renderer, {
content: "# Title\n\nHello",
syntaxStyle,
renderNode: (token) => {
if (token.type !== "heading") return undefined
return new TextRenderable(renderer, { content: token.text.toUpperCase() })
},
})Blocks from context.defaultRender() are still restyled in place, and they cause no extra renderNode call.
Changed
- core: A text keeps its full wrapped height under any height mode. It no longer loses rows after an edit, and it shows all its rows in a flex-start ScrollBox or an auto-height absolute box. In centered and flex-start columns, text that is taller than its space makes its siblings shrink or move. See Layout. (#1906)
Fixed
- core:
suspend()leaves raw mode when its promise settles, and then it discards the input queued on stdin. Mouse reports no longer show at the prompt or reach the next program as typed text. See Lifecycle and cleanup. (#1905) - core: When
streamingis off, acontentchange onMarkdownRenderablelexes the whole content again. The last block no longer splits in two when the new text extends the old text, as when> onebecomes> one\n> two. See Markdown. (#1903) - core: In the default coalesced mode, a style change on
MarkdownRenderablerunsrenderNodeagain for blocks with a custom node, so custom headings, blockquotes, and rules keep their renderable and border color. A style change no longer parses unchanged content again. See Markdown. (#1904) - core: In split-footer mode with
externalOutputMode: "capture-stdout", the renderer removes escape sequences from captured writes, so colors and OSC 8 links no longer leave text such as[31min scrollback. Captured rows are plain text. See Renderer. (#1902) - core: On Node 26.9.0 and later with
--permission, the audio recorder keeps a recording when the Permission Model deniesFileHandle.sync(). The recorder closes the file without the sync. See Audio capture and recording. (#1907) - ssh: A session with a late close that wrote the terminal restore exits its channel with status 0 instead of 1, and
onErrorstill gets the late close. If the renderer cleanup fails during a late close,closedrejects with that failure and the channel exits with status 1. See SSH. (#1900)
API changes and documentation: https://opentui.com/docs/releases/0.8.2
What's Changed
- core: lex all markdown again when an edit is not streaming by @simonklee in #1903
- ssh: exit 0 after a late close that wrote the restore by @simonklee in #1900
- core: strip escape sequences from captured stdout by @simonklee in #1902
- core: rerun renderNode for custom Markdown blocks on restyle by @simonklee in #1904
- core: keep recordings when the Permission Model blocks fsync by @simonklee in #1907
- ci: pin Node 26.13.0, which fixes a worker teardown crash by @simonklee in #1908
- native: measure text height without the at-most clamp by @simonklee in #1906
- core: leave raw mode when suspend() settles by @simonklee in #1905
Full Changelog: v0.8.1...v0.8.2