$ termy docs
libtermy
Embed Termy's beta terminal engine in Rust, C, Swift, and other native hosts.
libtermy is in beta and ready for experiments, prototypes, and real host integrations. The Rust and C APIs are usable today, but they can still change between Termy releases. Pin a commit or release and test upgrades before shipping them.
libtermy is Termy's reusable terminal engine. It provides the PTY, VT parser,
scrollback, input protocols, shell integration, search, links, configuration,
events, and damage-scoped frames while leaving the actual UI and GPU renderer to
your application.
The same engine powers Termy's desktop surfaces, and the native macOS Swift app uses the C ABI as its terminal backend. You can build another terminal app, embed a terminal inside a native tool, render terminal output in a custom UI, or feed output into a display-only terminal without creating a child process.
Choose an API
| Host | Use | What you get |
|---|---|---|
| Rust | termy_core | Native Rust types and the smallest integration layer |
| C, C++, Swift, Objective-C | termy_ffi | Opaque handles and a C-compatible ABI |
| External output or tmux panes | Display terminal | Parser, grid, damage, and frames without a PTY |
Both APIs are renderer-neutral. libtermy produces cells, cursor state, damage, events, and render configuration; your host decides how those values become pixels and application chrome.
What is available in beta
- PTY and shell startup on macOS, Linux, and Windows
- Full frames and dirty-span incremental frame updates
- Config loading with diagnostics and resolved render settings
- Keyboard and mouse encoding for negotiated terminal modes
- Scrollback, full-buffer search, and OSC 8 hyperlink lookup
- Title, bell, exit, clipboard, working-directory, progress, and shell events
- Wakeup notifications for event-driven render scheduling
- Display-only terminals driven by arbitrary output bytes
- Runtime cell measurement, theme colors, and cursor configuration
App chrome—windows, tabs, pane layout, settings UI, and rendering—is intentionally owned by the embedder.
Rust quick start
Add termy_core from the Termy repository. During beta, pinning a known commit
or release is recommended:
[dependencies]
anyhow = "1"
flume = "0.11"
termy_core = { git = "https://github.com/lassejlv/termy", package = "termy_core" }Create a terminal, send input, and consume a renderer-neutral update:
use termy_core::{
Terminal,
TerminalSize,
load_config_from_default_path,
measure_cell_from_config,
};
use std::time::Duration;
fn main() -> anyhow::Result<()> {
let loaded = load_config_from_default_path()?;
let metrics = measure_cell_from_config(&loaded.app_config);
let (wakeup_tx, wakeup_rx) = flume::bounded(1);
let terminal = Terminal::new(
TerminalSize {
cols: 100,
rows: 30,
cell_width: metrics.cell_width,
cell_height: metrics.cell_height,
},
None, // working directory
Some(wakeup_tx), // output-ready notifications
None, // shell-integration title policy
Some(&loaded.runtime_config),
None, // startup command
)?;
terminal.write(b"printf 'hello from libtermy\\r\\n'\r");
let _ = wakeup_rx.recv_timeout(Duration::from_secs(1));
// Force the first update to contain a complete row-major frame.
let update = terminal.frame_update(true);
println!("{}x{} cells", update.cols, update.rows);
Ok(())
}Use Terminal::frame_update(false) after the first frame. Partial updates carry
cells in dirty-span order, allowing a retained renderer to patch only the rows
and rectangles that changed. Terminal::snapshot() remains useful for one-off
captures and simple prototypes.
Display-only terminals
When another process owns the session—such as a tmux control-mode client—create a grid without a PTY:
let terminal = termy_core::Terminal::new_display(size, Some(&runtime_config));
terminal.feed_output(b"\x1b[32moutput from another process\x1b[0m\r\n");
let update = terminal.frame_update(true);write is intentionally a no-op for display terminals. Route input to the
external session owner and feed its resulting output back into libtermy.
C ABI quick start
Build the shared library from the repository:
git clone https://github.com/lassejlv/termy
cd termy
cargo build --release -p termy_ffiThe build produces libtermy_ffi.dylib on macOS,
libtermy_ffi.so on Linux, or termy_ffi.dll on Windows under
target/release. The public header is
crates/ffi/include/termy.h.
#include "termy.h"
#include <string.h>
int main(void) {
TermyFfiTerminal *terminal = NULL;
TermyFfiSize size = termy_size_default();
size.cols = 100;
size.rows = 30;
if (termy_terminal_new(size, NULL, 0, &terminal) != TERMY_FFI_OK) {
return 1;
}
const char *input = "printf 'hello from libtermy\\r\\n'\r";
termy_terminal_write(
terminal,
(const uint8_t *)input,
strlen(input));
TermyFfiFrameUpdate update = {0};
if (termy_terminal_take_frame_update(terminal, true, &update) == TERMY_FFI_OK) {
/* Render update.cells_ptr, then release the returned allocation. */
termy_frame_update_free(&update);
}
termy_terminal_free(terminal);
return 0;
}For production-style hosts, prefer termy_terminal_new_with_options. It accepts
a loaded config, working directory, startup command, and environment overrides.
Use termy_config_render_config_for_appearance to obtain theme colors, font
settings, padding, cursor behavior, and measured cell dimensions.
Drive the render loop
A responsive host normally follows this sequence:
- Create the terminal and request one forced full frame.
- Wait for output using
termy_terminal_wait_for_wakeup, or schedule work from the Rust wakeup sender. - Drain terminal events.
- Call
frame_update(false)ortermy_terminal_take_frame_update(..., false, ...). - Patch dirty spans into the retained frame and redraw affected rows or rects.
- Return to the idle wait instead of polling continuously.
Full updates are row-major: index = row * cols + col. Partial-update cells are
ordered by dirty span, left to right. A partial cell does not carry its own row
or column; derive the position from its span.
Input, search, and links
Use libtermy's protocol helpers instead of rebuilding terminal negotiation in the host:
keystroke_to_input/termy_terminal_encode_keyencode_mouse_report/termy_terminal_encode_mousebracketed_paste_mode/termy_terminal_bracketed_paste_modesearch_with_options/termy_terminal_search_with_optionshyperlink_at/termy_terminal_hyperlink_at
Search covers the full terminal buffer, including scrollback. Hyperlink lookup returns OSC 8 links. Treat terminal-provided URLs as untrusted input and validate schemes before asking the operating system to open them.
Ownership and threading
A TermyFfiTerminal handle is not internally synchronized. Serialize every
call for a handle except the documented wake-channel functions. Never free a
handle while another thread is inside an FFI call.
Every returned allocation has one matching free function:
| Returned value | Release with |
|---|---|
TermyFfiTerminal * | termy_terminal_free |
TermyFfiConfig * | termy_config_free |
TermyFfiFrame | termy_frame_free |
TermyFfiFrameUpdate | termy_frame_update_free |
TermyFfiDamage | termy_damage_free |
TermyFfiEventBatch | termy_event_batch_free |
TermyFfiSearchBatch | termy_search_batch_free |
TermyFfiHyperlink | termy_hyperlink_free |
Standalone TermyFfiBytes | termy_buffer_free |
Batch-owned event payloads, search lines, and config diagnostics are released by their batch free function. Do not release those nested values individually.
The wake channel is the only concurrency exception. One thread may block in
termy_terminal_wait_for_wakeup while another serially drives the terminal. To
tear down safely:
- Stop issuing new terminal calls.
- Call
termy_terminal_notify_wakeup. - Join the waiting thread.
- Call
termy_terminal_free.
Beta expectations
libtermy is usable, but it has not reached a stable 1.0 contract yet:
- Rust types and C ABI functions may change between releases.
- Prebuilt standalone SDK packages are not published yet; build from the Termy repository and vendor the matching header and shared library.
- The host must provide rendering, focus, clipboard policy, accessibility, and platform integration.
- Validate performance with incremental frame updates; a full snapshot every display tick leaves much of libtermy's performance work unused.
Pin the Termy revision in your dependency or build system, keep the header and library from the same revision, and review release notes before updating.
Validate an integration
When working from a Termy checkout, the focused checks are:
cargo test -p termy_core
cargo test -p termy_ffi
cargo build --release -p termy_ffiThe native macOS implementation in
macos/Sources/TermySwift/Services/LibTermyTerminal.swift
is the most complete Swift integration reference.