Termy
Developer

$ 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

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

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.

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 and redraw affected rows or rects.
  6. 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.

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

The native macOS implementation in macos/Sources/TermySwift/Services/LibTermyTerminal.swift is the most complete Swift integration reference.

On this page