❯_termy
Developer

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

HostUseWhat you get
Rusttermy_coreNative Rust types and the smallest integration layer
C, C++, Swift, Objective-Ctermy_ffiOpaque handles and a C-compatible ABI
External output or tmux panesDisplay terminalParser, 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:

Cargo.toml
[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_ffi

The 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:

  1. Create the terminal and request one forced full frame.
  2. Wait for output using termy_terminal_wait_for_wakeup, or schedule work from the Rust wakeup sender.
  3. Drain terminal events.
  4. Call frame_update(false) or termy_terminal_take_frame_update(..., false, ...).
  5. Patch dirty spans into the retained frame.
  6. Build special-glyph plans for those spans and redraw affected rows or rects.
  7. 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.

Use libtermy's protocol helpers instead of rebuilding terminal negotiation in the host:

  • keystroke_to_input / termy_terminal_encode_key
  • encode_mouse_report / termy_terminal_encode_mouse
  • bracketed_paste_mode / termy_terminal_bracketed_paste_mode
  • search_with_options / termy_terminal_search_with_options
  • hyperlink_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 valueRelease with
TermyFfiTerminal *termy_terminal_free
TermyFfiConfig *termy_config_free
TermyFfiFrametermy_frame_free
TermyFfiFrameUpdatetermy_frame_update_free
TermyFfiGlyphRenderPlantermy_glyph_render_plan_free
TermyFfiDamagetermy_damage_free
TermyFfiEventBatchtermy_event_batch_free
TermyFfiSearchBatchtermy_search_batch_free
TermyFfiHyperlinktermy_hyperlink_free
Standalone TermyFfiBytestermy_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:

  1. Stop issuing new terminal calls.
  2. Call termy_terminal_notify_wakeup.
  3. Join the waiting thread.
  4. 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

On this page