>_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_output → app.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
| Spawn | spawn(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 |
| Stream | a 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 |
| Resize | resize(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_free | create an opaque TerminalHandle; release it (kills the session first) |
zet_spawn | spawn the login shell; OutputCb + ExitCb + a verbatim user pointer; returns 0 / -1 |
zet_write | write NUL-terminated UTF-8 keystrokes to the PTY; returns 0 / -1 |
zet_resize | resize the PTY viewport; returns 0 / -1 |
zet_kill | kill 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 transport | when 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 transport | when window.Juce.getNativeFunction is present, the same commands resolve to JUCE native functions and backend events |
| prefs fallback | termPrefs uses the host's global window.prefs if present, else a localStorage shim |
| toggle | Ctrl+` (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)
| Commands | terminal_spawn(rows, cols) · terminal_write(data) · terminal_resize(rows, cols) · terminal_kill() |
| Events | terminal-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.rs | framework-agnostic TerminalSession (new / spawn / write / resize / kill); reader thread + UTF-8 boundary carry |
src/ffi.rs | the C ABI (zet_* exports + OutputCb / ExitCb) |
include/zpwr_embed_terminal.h | hand-written C header (no cbindgen) |
webui/terminal.js / webui/terminal.css | the xterm.js front end + its styling (transport auto-detect) |
webui/xterm.js / webui/xterm.css | vendored xterm.js |
tests/integration.rs | headless 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