Files
opentui/packages/native
Simon Klee 7221adcbd9 native: keep split-footer output off the footer when fewer than two rows remain (#1618)
In split-footer mode, native keeps appended output above the footer with
a temporary scroll region (`ESC[1;Nr`, N = rows above the footer). With
one row above the footer it sent `ESC[1;1r`, which terminals ignore,
because a scroll region needs two rows; with no row it sent no region.
Each CRLF on row 1 then moved into the footer and erased it, later
frames
did not repaint it, and nothing reached the scrollback. The default
footer height (12) hits this in any terminal of 13 rows or fewer, for
example a small tmux pane or after a resize.

With fewer than two rows above the footer, native now uses the region
`1;2`, which borrows the footer's top row, sends one more `ESC D` after
the payload so that the last line is back on row 1, and repaints the
footer. When a first row wraps onto the borrowed row, the existing
pre-scroll also clears that row. With no row above the footer, a row
that
fills the width ends there and scrolls into the scrollback, and the next
commit starts at column 1. A one-row terminal skips the extra `ESC D`.

One case stays lossy: when the footer covers the whole terminal, a write
shorter than the width and without a newline has no row to stay on, so
it is lost if a later write continues it.

The screen table in `renderer.native-scene-split.test.ts` gets rows with
a terminal size: panes of two rows, one row (also under the default
footer and in a two-row terminal), no row, a one-row terminal, and a
footer that shrinks after a lost partial row. They write captured lines,
a two-row writer snapshot, a wrapping first row, and a full row, then
grow the VT with the cursor on the bottom row so the scrollback shows
above the screen. The native ordering test runs at pinned offsets 2, 1,
and 0. Reverting the region, dropping the extra `ESC D`, the footer
repaint, the borrowed-row clear, the full-row end, the tail reset, or
the
one-row guard each makes at least one row fail.

Fixes #1607
2026-10-10 07:32:59 +02:00
..

Native API

OpenTUI uses Context-owned scenes and Sessions for production rendering, including text, editors, custom paint hooks, images, detached surfaces, and split output. Standalone resources use the same checked ownership model without a terminal.

Read Resources and ownership for resource bindings, text copy units, and scoped framebuffer access. Frames and output defines mutation admission, painted drafts, and transport completion. How Core uses native explains TypeScript staging and resource ownership. Host I/O and time defines Context file operations, Session deadlines, and native diagnostic clocks.

API surfaces

  • src/opentui.zig is the public Zig module. It exports the checked Context API and raw Zig primitives, including CliRenderer, NativeRenderable, OptimizedBuffer, text buffers, and pools. Callers manage the lifetimes of raw primitives. Importing a raw primitive does not add Context checks to it.
  • include/opentui.h defines the checked ot_* C ABI for Contexts, Sessions, scenes, drawing, text, editors, styles, leases, clipboard, and diagnostics. The ABI is experimental and at version 1. Core renders through it.
  • ../core/src/zig.ts supplies TypeScript wrappers over that checked ABI. Its checked signatures, callbacks, constants, and record layouts come from native-abi.generated.ts.

ABI generation and builds

Use C headers and libraries from the same revision. Set each versioned record's exact struct_size and abi_version, and leave unused flags and reserved fields zero. Follow the output and failure contract of each operation in the header.

From packages/core:

bun run generate:abi
bun run check:abi
bun run test:abi

scripts/native-abi.ts uses Zig Translate-C and scripts/native-abi.zig reflection to derive scalar widths, signatures, callback types, constants, record sizes, alignment, and field offsets from the header. Pointer nullability, retention, address fields, and portable buffer/ptr policy live in scripts/native-abi-pointers.ts, because C types cannot prove lifetimes. Review that metadata when ownership contracts change. Do not edit generated bindings. check:abi detects stale output. Use bun run check:abi --all-targets to also compare the layouts of all supported targets. An unsupported record shape or calling convention fails the script. It does not produce partial metadata. C compiler assertions also check complete function and callback prototypes, record layouts, field types, and constant values. Translate-C alone is not enough, because it can drop callback calling-convention attributes and ignore #pragma pack.

From packages/native, bun run build installs the header and libraries under lib/<target>/ for the host target. Linux and macOS produce libopentui.a beside the shared library. Windows produces opentui-static.lib, opentui.lib for DLL imports, and opentui.dll. Static linkage also needs the platform and C++ runtime libraries. zig build -Dall builds all supported targets. zig build -Dlibrary-target=<target> builds one target.

zig build test-abi --summary all

This command runs the C fixture against the static and shared libraries on the host. On a glibc Linux host, the fixture targets glibc 2.17. check:abi --all-targets checks layouts for all eight supported targets. It does not test macOS or Windows runtime linkage or terminal behavior.

The external examples/hello package imports the public Zig module without JavaScript.