OpenHarmony terminal component that embeds libghostty-vt behind an XComponent surface.
The native addon is built entirely by Zig. This workspace consumes the adjacent
../ohos-zig-binding checkout for XComponent, Native Window, Native VSync,
Native Drawing, and IME wrappers. Zig builds wgpu-native and the official
libghostty-vt source; the HAR does not vendor prebuilt .a files.
Terminal behavior is sourced from the pinned official Ghostty commit in
build.zig.zon; the wrapper includes Ghostty's public headers and does not
maintain a second ABI. The OHOS renderer follows Ghostty's generic renderer
model—dirty viewport rows, immutable frame data, GPU cell/background buffers,
color-aware glyph atlas, and a dedicated render thread—on top of wgpu-native.
ohpm install @ohos-rs/terminalexample/ (entry HAP demo)
└─ Terminal / TerminalController (@ohos-rs/terminal HAR)
└─ XComponent(libraryname = "terminal")
└─ libterminal.so zig build
├─ zig-napi
├─ ohos-zig-binding (XComponent / window / VSync / drawing / IME)
├─ raw-window-handle
├─ wgpu-native-zig (builds wgpu-native)
└─ ghostty (builds libghostty-vt)
The library does not start a shell. The example echoes keyboard input locally. A real app owns PTY/SSH and wires controller.write() / controller.feed().
src/: Zig addonpackage/: reusable HAR (Terminal,TerminalController)example/: Stage-model entry HAP demo (pages/Index)
example/ is the HarmonyOS entry module. After zig build dist, open this repository in DevEco Studio and run the entry HAP.
The demo page hosts Terminal and a cooked local shell (same host split as arkit). Tap the surface to open the native IME. Typed bytes go on_input → demo shell → feed(). Hello, Colors, and Clear inject focused VT samples; Stress injects 1,200 long, colored, Unicode lines for soft-wrap and scrollback verification.
Requires Zig 0.16, an OpenHarmony API 20+ NDK, the adjacent
ohos-zig-binding checkout on branch feat/native-vsync-binding, and a network
fetch of the remaining git dependencies on the first build.
export OHOS_NDK_HOME=/path/to/ohos-sdk
./scripts/build-ohos.shOHOS_NDK_HOME is normalized to the SDK root by the build script. Passing the
native/ child is also accepted.
zig build compiles libterminal.so by building the git dependencies. zig build dist writes the addon into package/libs/<abi>/ and example/libs/<abi>/ so the HAR and entry module can package it.
Native builds default to ReleaseFast; use -Doptimize=Debug explicitly when
debugging. Running the terminal performance demo with Debug-built Ghostty or
wgpu-native is not representative and can trip HarmonyOS main-thread watchdogs.
Useful flags:
-Dnative=false— host tests only, do not build the OHOS addon
The first zig build fetches git archives and compiles wgpu-native (Rust/cargo) plus libghostty-vt. Later builds reuse the cache.
Install ohpm-rs, configure OHOS_NDK_HOME, then run:
./scripts/pack-ohpm.shThe script builds and verifies libterminal.so for arm64-v8a,
armeabi-v7a, and x86_64 before invoking ohpm-rs prepublish and
ohpm-rs pack. The resulting HAR is written to dist/. package/README.md
and package/LICENSE are relative symlinks to their repository-root files so
the published package always carries the canonical documentation and license.
The terminal follows the same ownership split used by gpui-ghostty and arkit_terminal:
- ArkTS owns component lifecycle and the host I/O boundary. Typed input goes
to the app through
on_input; only host output is fed back to Ghostty. Hardware keys are encoded by Ghostty's ownGhosttyKeyEncoder, synchronized from current terminal modes rather than a local escape-sequence table. - The render worker owns hot-path libghostty-vt stream mutation and captures
immutable viewport updates.
feed()only enqueues bytes, so large PTY bursts never parse on ArkUI. Parsing is chunked and coalesced to display ticks; Ghostty's terminal and per-row dirty state is retained instead of copying the full viewport for each PTY chunk. Surface size and font density determine rows and columns; the grid is reflowed instead of stretching glyphs. - The render worker exclusively owns wgpu, the retained
OHNativeWindow, and a window-associatedOH_NativeVSync. It keeps one one-shot frame request in flight and presents only the newest snapshot on that surface's display tick. Distinct graphemes are rasterized once into a persistent GPU atlas; backgrounds, cached glyph quads, and the cursor are submitted as GPU instances. There is no CPU-composited fullscreen bitmap. CPU-side GPU cell lists retain one allocation per row, matching Ghostty'srenderer.cell.Contents: only dirty rows are rebuilt, while the resulting buffers are batch-uploaded and drawn by wgpu. Frame publication merges unconsumed dirty rows so slow frames cannot drop or replay stale states. - Renderer readiness means that a frame was successfully presented, not just that a device was created. Surface loss and validation errors remain visible through the ArkTS diagnostic overlay.
Native VSync callbacks only enqueue a generation-tagged worker message. If a surface is replaced, late callbacks from its old VSync connection are ignored. If associated VSync creation fails, XComponent frame callbacks retain the previous fallback behavior. A busy terminal lock skips only that display tick, and the latest immutable frame still wins. Touch scrolling accumulates atomically and is applied on the next available frame, preventing engine-lock contention from blocking the ArkUI thread.
Surface and input callbacks only retain native resources or enqueue work. The render worker serializes VT parsing, pending viewport movement, cursor timing, dirty-row capture, GPU cell-list rebuild, and presentation. Configuration and query APIs briefly share the terminal-state lock but never mutate renderer GPU state.
As in official Ghostty, font discovery and a glyph's first rasterization are CPU/font-system operations. Once cached, terminal frame composition and presentation are GPU-only; normal frames do not create or upload a fullscreen CPU bitmap.
import { Terminal, TerminalController } from '@ohos-rs/terminal';
@Entry
@Component
struct TerminalPage {
private controller: TerminalController = new TerminalController();
aboutToAppear(): void {
this.controller.updateConfig({ fontSize: 16 });
this.controller.setInputListener((data: string) => {
this.driver.write(data);
});
}
build() {
Terminal({
controller: this.controller,
surfaceId: 'main-terminal',
surfaceColor: '#0B0D10'
})
}
}Declared in build.zig.zon and built by Zig:
openharmony-zig/zig-napiopenharmony-zig/raw-window-handleopenharmony-zig/wgpu_native_zigghostty-org/ghostty(ghostty-vt-staticartifact)
ohos-zig-binding is a local workspace dependency until the new native_vsync
module and NativeWindow.surfaceId() are published.