Over ssh, sshd drops COLORTERM. A terminal that draws 24-bit color but sends TERM=xterm-256color without COLORTERM therefore got every color mapped to the 256-color palette. In WezTerm and xterm over ssh, a navy #23344b turned into a gray #3a3a3a. In 0.8.2, only a kitty XTVERSION reply turned on 24-bit color. In 0.8.3, when COLORTERM does not settle the question, the startup queries also ask XTGETTCAP RGB and Tc. A positive reply turns on 24-bit color. So does an XTVERSION reply from Ghostty, WezTerm, foot, XTerm, iTerm2, or tmux, because tmux converts 24-bit colors for its client itself. The replies come back through ssh, so this works without COLORTERM. Inside GNU Screen, OpenTUI sends neither query, because Screen drops 24-bit colors and prints the query bodies as text. When nothing answers, the old TERM rule still applies. Alacritty and mosh answer none of these queries, so they still need COLORTERM=truecolor.
The width method decides how many cells an emoji sequence takes. When OpenTUI picks a different method than the terminal uses, the line overflows or leaves gaps. Before, OpenTUI kept the unicode method in terminals that do not report mode 2027, even though many of them draw a sequence one code point at a time. tmux 3.6 and later draws a sequence as one cluster but got wcwidth. An inherited TERM_PROGRAM=ghostty also forced unicode-wide in other terminals. Now the method comes from one place, with the strongest evidence first:
OPENTUI_FORCE_*, also over SSH.- The multiplexer.
- The DECRPM 2027 reply. An unrecognized or permanently reset mode means wcwidth, except in kitty and iTerm2.
- XTVERSION.
- The environment, where
TERMwins over an inheritedTERM_PROGRAM.
| Terminal | Before | After |
|---|---|---|
| Alacritty, xterm, urxvt, GNOME Terminal, VS Code, Zellij, foot (wcswidth) | unicode | wcwidth |
| tmux 3.6+ | wcwidth | unicode |
| kitty started from Ghostty | unicode-wide | unicode |
| Ghostty over SSH, libghostty | unicode | unicode-wide |
No method matches tmux when variation-selector-always-wide is off. Unicode is the closest. If the new choice is wrong for your terminal, set OPENTUI_FORCE_UNICODE or OPENTUI_FORCE_WCWIDTH. These settings still win.
Before this release, a host that started an unmodified OpenTUI app could not easily make it load a different native build. setRenderLibPath() needs code, and OTUI_ASSET_ROOT needs the whole asset tree under one root. OPENTUI_LIB_PATH names one library file, under both Bun and Node:
OPENTUI_LIB_PATH=../opentui/packages/native/lib/x86_64-linux/libopentui.so bun run src/index.tsA relative path resolves against the working directory. setRenderLibPath() takes precedence over the variable, and the variable takes precedence over OTUI_ASSET_ROOT and the platform package. If the file is missing or does not load, the error names the variable. Core now also checks the context ABI version when it loads a library, so a library built for another context ABI fails at load and not at a later call.
A host that embeds a terminal often has to know where the terminal's view is. For example, it can pass an unused wheel step to an outer scroll view or draw a scrollbar. Counting scroll steps does not give the answer. Output and resizes keep the view on its history rows, a scrollback clear jumps to the live screen, and the alternate screen has no history. EmbeddedTerminalRenderable.getScroll() reads the current view from the emulator, including the moves that the emulator makes on its own:
function scrollStatus(terminal: EmbeddedTerminalRenderable): string {
const { offset, total, visible } = terminal.getScroll()
if (offset === 0) return "live"
return `${offset} of ${total - visible} rows back`
}C and Rust hosts get two changes. First, the @opentui/core-<platform> packages now ship the opentui.h that their library was built from. Before, a binding that took the library from npm had no matching header. Second, opentui.h now declares the audio engine. Before, a host could bind it only by copying Zig signatures. With OT_AUDIO_START_NO_DEVICE, the engine mixes only when the host calls ot_audio_mix, so tests can play sounds without a device. Voice IDs now carry a generation. An old ID no longer stops a new sound that reused its slot.
Added
- core:
EmbeddedTerminalRenderable.getScroll()returns the view'soffsetfrom the live screen, thetotalrows, and thevisiblerows. See Embedded terminal. (#1934) - core:
OPENTUI_LIB_PATHnames a native library file. Under Bun and Node, Core loads it instead of theOTUI_ASSET_ROOTcopy or the platform package.setRenderLibPath()takes precedence over the variable. See Environment variables. (#1933) - native:
ot_embedded_terminal_scrollbar_getfills anot_embedded_terminal_scrollbarrecord with the offset from the live screen, the total rows, and the visible rows. See Embedded terminal. (#1934) - native:
opentui.hdeclares nineot_audio_*functions that address an audio engine byot_handleand returnot_status. With theOT_AUDIO_START_NO_DEVICEflag,ot_audio_startopens no device, and the engine mixes only inot_audio_mix. See C. (#1935) - native: The
@opentui/core-<platform>packages include theopentui.hthat the library was built from. On Windows, they also includeopentui.lib. See C. (#1932) - native: The
OT_INPUT_KEY_TYPEDkey flag marks a key whose code is a printable character that the terminal sent as text or as a CSI 27 code. See Keyboard input. (#1919)
Changed
- core: When
COLORTERMdoes not enable 24-bit color, the renderer asks the terminal with XTGETTCAPRGBandTc. A positive reply, or an XTVERSION reply from Ghostty, WezTerm, foot, XTerm, iTerm2, or tmux, enablesrgb, also over SSH. See Terminal capabilities. (#1941) - core: OpenTUI chooses the width method from
OPENTUI_FORCE_*, the multiplexer, the DECRPM 2027 reply, XTVERSION, and then the environment. Alacritty, xterm, GNOME Terminal, VS Code, and Zellij now get wcwidth, and tmux 3.6 and later gets unicode. See Terminal capabilities. (#1920) - core: When no Linux clipboard backend can do an operation, the result stays
unsupported, and the process logs once why each backend failed. The log names a library that did not load, a display that did not connect, or a missing Wayland protocol. See Clipboard. (#1921) - core: Core checks the context ABI version of the native library when it loads the library. A library built for a different context ABI throws at load. See Environment variables. (#1933)
Fixed
- core: A frame that changes only the pointer shape from
setMousePointer()no longer hides a visible text cursor. The frame sends only the OSC 22 sequence, and C hosts get the same fix. See Renderer. (#1939) - core: A clipboard read on Wayland no longer freezes the process when the compositor stops accepting connections. The read settles as
timed-outat its deadline. See Clipboard. (#1923) - core: With the native input parser, a typed character from U+E000 to U+E06E, such as a Nerd Fonts glyph, gives a key with that character instead of Escape, Return, or Backspace. A Textarea now inserts the glyph. See Keyboard input. (#1919)
- core: With the native input parser, a typed U+FEFF keeps its character in
rawandsequence, so a Textarea no longer drops it. See Keyboard input. (#1915) - core: With the native input parser, a drag in URxvt continues after a wheel tick, so a selection keeps growing. See Interaction, focus, and selection. (#1917)
- core: A node that changes size and then publishes its hooks again before its first frame still gets its first
onResizecall. This happens, for example, when you assignrenderAfter. Abuffered: truenode then resizes its buffer and does not clip its drawing to the old width. See Renderables. (#1918) - core: Layout no longer makes items millions of cells wide when every item of an overflowing flex line freezes at its minimum size. The vendored Yoga includes the upstream fix. See Layout. (#1922)
- core: Text measurement treats a width that is less than 0.0001 below a whole cell as that cell. After a resize, flex-shrink and flex-grow layouts no longer keep a text one cell wider than a fresh layout. See Layout. (#1926)
- core:
stopVoice()with the ID of a voice that ended no longer stops a newer sound. Voice IDs now carry a generation. See Audio. (#1935)
API changes and documentation: https://opentui.com/docs/releases/0.8.3
What's Changed
- core: keep a typed U+FEFF in native key events by @simonklee in #1915
- native: drag the held button in URxvt motion after a wheel tick by @simonklee in #1917
- native: keep the first resize across hook republishes by @simonklee in #1918
- native: tell typed private-use characters from functional keys by @simonklee in #1919
- native: say why a Linux clipboard backend is unavailable by @simonklee in #1921
- native: connect to the Wayland clipboard socket without blocking by @simonklee in #1923
- native: choose the width method from DECRPM 2027 and XTVERSION by @simonklee in #1920
- native: backport Yoga's fix for flex-shrink float residues by @simonklee in #1922
- native: measure a width within Yoga's tolerance of a cell as that cell by @simonklee in #1926
- native: keep failing tests from hanging after environment changes by @simonklee in #1928
- core: ship opentui.h in the platform packages by @simonklee in #1932
- core: load the native library from OPENTUI_LIB_PATH by @simonklee in #1933
- native: report the embedded terminal viewport by @simonklee in #1934
- native: export the audio engine in opentui.h by @simonklee in #1935
- native: ask the terminal for 24-bit color before falling back by @simonklee in #1941
- native: keep the cursor visible on a pointer-only frame by @simonklee in #1939
- README.md by @simonklee in #1938
Full Changelog: v0.8.2...v0.8.3