// ZPWR-EMBED-TERMINAL — SHARED EMBEDDED PTY TERMINAL

private internal library · rlib + staticlib + cdylib · 6 zet_* C ABI exports · native rlib for the Tauri apps · C ABI for zpwr-daw · one PTY login-shell core

Report GitHub
// Color scheme

>_ZPWR-EMBED-TERMINAL

One terminal, every app, Rust core + C ABI. The shared embedded PTY terminal behind the MenkeTechnologies app stack — one PTY-backed login-shell session (xterm.js front end) extracted verbatim from the Audio-Haxor backend (src-tauri/src/terminal.rs) with the Tauri app.emit(...) calls replaced by host-supplied on_output / on_exit callbacks. Every app runs byte-for-byte identical PTY spawning, reading and resizing against the same core.

Two link models, four apps

Audio-Haxor / traderview / ztranslator (Tauri / Rust)link it natively as an rlib, wrap a TerminalSession in a tauri::State, and forward on_outputapp.emit("terminal-output", ...)
zpwr-daw (JUCE / C++)link libzpwr_embed_terminal.a (or the cdylib) over the C ABI in include/zpwr_embed_terminal.h and drive the WebView over JUCE native functions

A session owns one PTY master + child shell. Re-spawning kills the previous one. No Tauri or JUCE types appear in src/lib.rs — the core is framework-agnostic.

Spawn · Stream · Resize

Spawnspawn(rows, cols, on_output, on_exit) opens a PTY via portable-pty (native_pty_system()), launches $SHELL -l (login shell, defaults to /bin/zsh), inherits HOME and sets cwd to it, exports TERM=xterm-256color, and stores the child PID
Streama named terminal-reader background thread reads 4 KiB chunks; it tracks the last valid UTF-8 boundary and carries an incomplete multi-byte tail to the next read instead of emitting U+FFFD; each chunk is handed to on_output; on EOF it fires on_exit
Resizeresize(rows, cols) notifies the PTY master of the new viewport; write(data) sends raw keystrokes to the PTY writer; kill() drops the writer/master and SIGKILLs the child on Unix

Native (Rust) usage

The Tauri apps link the rlib and call the API directly. spawn drives the PTY + reader thread; the host forwards the callbacks to its event bus.

use zpwr_embed_terminal::TerminalSession;

let session = TerminalSession::new();
session.spawn(
    24, 80,
    move |chunk: &str| {
        // forward to the front end, e.g. app.emit("terminal-output", chunk)
    },
    move || {
        // shell exited, e.g. app.emit("terminal-exit", ())
    },
)?;

session.write("ls -la\r")?;
session.resize(40, 120)?;
session.kill();

spawn tears down any existing session before opening the new PTY, so a re-spawn is always clean.

C ABI usage (zpwr-daw)

zpwr-daw links libzpwr_embed_terminal.a (or the cdylib) and includes include/zpwr_embed_terminal.h — hand-written, no cbindgen. PTY output is delivered to a C callback from the reader thread.

#include "zpwr_embed_terminal.h"

static void on_output(const char *text, void *user) {
    /* marshal to the UI thread, then feed xterm.js — text valid for this call only */
}
static void on_exit(void *user) { /* shell exited */ }

TerminalHandle *h = zet_new();
zet_spawn(h, 24, 80, on_output, on_exit, /*user*/ NULL);

zet_write(h, "ls -la\r");
zet_resize(h, 40, 120);

zet_kill(h);   /* handle stays valid, can be re-spawned */
zet_free(h);

The text passed to the output callback is a NUL-terminated C string valid only for the duration of the call — copy it if you need it later. Callbacks fire on the reader thread, not the caller's thread, so marshal to your UI thread before touching the WebView.

C ABI surface

zet_new / zet_freecreate an opaque TerminalHandle; release it (kills the session first)
zet_spawnspawn the login shell; OutputCb + ExitCb + a verbatim user pointer; returns 0 / -1
zet_writewrite NUL-terminated UTF-8 keystrokes to the PTY; returns 0 / -1
zet_resizeresize the PTY viewport; returns 0 / -1
zet_killkill the session; the handle stays valid and can be re-spawned; returns 0

Six extern "C" exports plus two callback typedefs (ZetOutputCb / ZetExitCb). The *mut c_void user pointer is shuttled across the reader thread as a usize and never dereferenced by Rust.

The xterm.js front end

webui/terminal.js is the shared front end. It auto-detects its transport at runtime and abstracts the IPC, so the same file backs every host:

Tauri transportwhen window.__TAURI__ is present, calls map to invoke('terminal_spawn'|'terminal_write'|'terminal_resize'|'terminal_kill') and events to listen('terminal-output'|'terminal-exit')
JUCE transportwhen window.Juce.getNativeFunction is present, the same commands resolve to JUCE native functions and backend events
prefs fallbacktermPrefs uses the host's global window.prefs if present, else a localStorage shim
toggleCtrl+` (Backquote) or Cmd/Ctrl+T toggles the embedded terminal popup (toggleTerminalPopup)

The host provides a #terminalPane > #terminalContainer mount point. webui/xterm.js and webui/xterm.css are vendored xterm.js.

Backend contract (each host implements)

Commandsterminal_spawn(rows, cols) · terminal_write(data) · terminal_resize(rows, cols) · terminal_kill()
Eventsterminal-output(string) · terminal-exit()

Build & test

cargo build          # produces rlib + staticlib + cdylib
cargo test           # headless; spawns a PTY, runs a command, streams output
cargo fmt --all --check
cargo clippy --all-targets -- -D warnings

Builds on macOS aarch64 and Linux x86_64. portable-pty abstracts the PTY across macOS/Linux/Windows; the kill() SIGKILL path is gated on #[cfg(unix)].

Source layout

src/lib.rsframework-agnostic TerminalSession (new / spawn / write / resize / kill); reader thread + UTF-8 boundary carry
src/ffi.rsthe C ABI (zet_* exports + OutputCb / ExitCb)
include/zpwr_embed_terminal.hhand-written C header (no cbindgen)
webui/terminal.js / webui/terminal.cssthe xterm.js front end + its styling (transport auto-detect)
webui/xterm.js / webui/xterm.cssvendored xterm.js
tests/integration.rsheadless PTY integration tests

The MenkeTechnologies stack

zpwr-embed-terminal is a shared component of the MenkeTechnologies app stack. Browse the rest via the MenkeTechnologiesMeta umbrella repo:

  • Audio-Haxor — Tauri v2 desktop app; links zpwr-embed-terminal as an rlib
  • traderview — Tauri v2 desktop app; links the rlib
  • ztranslator — Tauri v2 desktop app; links the rlib
  • zpwr-daw — DAW arranger; links the C ABI
  • zpwr-crate — shared sample-library crate browser