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. 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, render configuration, and canonical plans for terminal glyphs that should bypass font shaping; 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
- Host-routed Kitty OSC 5522 reads and writes for arbitrary clipboard MIME data
- Wakeup notifications for event-driven render scheduling
- Display-only terminals driven by arbitrary output bytes
- Runtime cell measurement, theme colors, and cursor configuration
- Shared block-element, box-drawing, sextant, Braille, rounded-corner, and diagonal geometry
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.
Rich Rust rendering and the engine boundary
The flat TermyFrame and TermyFrameUpdate types remain the stable Rust/C
compatibility contract. Rust renderers that need complete combining text,
underline variants and colors, live palette revisions, ordered viewport scroll
damage, or generation-checked partial reads can use Terminal::render_read,
Terminal::take_render_damage_snapshot, and the visit_* methods.
visit_line_cells streams a coherent buffer range while holding the terminal
lock, so its callback must not call back into the same terminal.
The terminal engine itself is private. termy_core 0.2 removes Terminal::with_term,
TerminalOptions::term_config, and the exported raw Alacritty conversion
helpers as part of the renderer-neutral core API change. Use core-owned
TerminalColor, TerminalRenderCell, and TerminalQueryColors values plus the
public Terminal methods instead. This is an intentional Rust source break;
the existing flat frame layout remains unchanged.
Shared special-glyph plans
Do not shape block elements, box drawing, sextants, long Braille runs, rounded
corners, or diagonals as ordinary font glyphs. Call terminal_glyph_plan with
TerminalGlyphMetrics and TerminalGlyphNeighbors. The returned
TerminalGlyphPlan is allocation-free and contains normalized rectangles and
strokes. Transform those primitives into your renderer's coordinate space and
apply final device-pixel snapping according to each rectangle's snap mode.
TerminalGlyphNeighbors::from_row preserves the context-sensitive Braille
behavior: long QR-style runs use explicit geometry, while short animated
spinners remain shaped text.
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.
- Build special-glyph plans for those spans 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.
Kitty clipboard callbacks from C
Use termy_terminal_drain_events_with_clipboard to service Kitty OSC 5522 MIME
clipboard requests. The read and write callbacks run synchronously during the
drain, on the thread that called it. The host owns permission prompts and system
clipboard access. Missing callbacks fail closed: reads are denied and writes are
reported as unsupported.
Read requests include requested MIME types, location, application name, and
permission state. Invoke the supplied reply callback before the read callback
returns; libtermy copies all response slices during that invocation. Write
request slices are borrowed only for the callback. Never retain callback data,
retain a read reply function, or re-enter the same terminal handle from a
callback. Continue using the callback-enabled drain for every batch while
has_more is true.
PTY-backed terminals return protocol responses directly to their child.
Display-only hosts receive response bytes through the protocol-reply callback
and must forward them to their external transport. Use
termy_terminal_kitty_clipboard_paste_events_enabled and
termy_terminal_send_kitty_clipboard_paste_event to notify applications of an
ordinary paste with its available MIME formats.
Batched special-glyph plans from C
Keep one full row-major TermyFfiCell buffer in the host and patch frame updates
into it. Then call termy_cells_build_glyph_render_plan with that retained
buffer, its dimensions, measured cell/font metrics, and the update's dirty
spans. One call returns a sparse TermyFfiGlyphRenderPlan: entries identify
absolute cell indices and ranges in flat rectangle and stroke arrays. Passing
zero spans builds a full plan.
Coordinates are relative to one cell. Apply the host renderer's transform and
final device-pixel snapping, then release the batch with
termy_glyph_render_plan_free. This keeps TermyFfiCell compact and avoids one
FFI call per cell.
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 |
TermyFfiGlyphRenderPlan | termy_glyph_render_plan_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_ffi