# Advanced (/docs/customize/advanced)
## Updates [#updates]
| Key | Default | Description |
| ------------- | ------- | ------------------------------- |
| `auto_update` | `true` | Enable automatic update checks. |
## Startup [#startup]
| Key | Default | Description |
| --------------------------- | ------------------------------------- | --------------------------------------------------------------- |
| `working_dir` | unset | Initial directory for new sessions. |
| `working_dir_fallback` | `home` (macOS/Win), `process` (Linux) | Used when `working_dir` is unset. Alias: `default_working_dir`. |
| `native_tab_persistence` | `false` | Restore native tabs and splits across restarts. |
| `native_layout_autosave` | `false` | Auto-save into the currently loaded named layout. |
| `native_buffer_persistence` | `false` | Replay saved buffer text when restoring layouts. |
On launch, Termy starts shells for the selected restored workspace only. Inactive
workspaces remain dormant until you first select them.
## Safety [#safety]
| Key | Default | Description |
| ----------------------------------- | ------- | -------------------------------------- |
| `warn_on_quit` | `false` | Warn every time you try to quit. |
| `warn_on_quit_with_running_process` | `true` | Warn when a tab has an active process. |
## Debug & UI [#debug--ui]
| Key | Default | Description |
| --------------------- | ------- | -------------------------------------------------------------- |
| `show_debug_overlay` | `false` | Show FPS, CPU, and memory in the terminal corner. |
| `simple_mode` | `false` | Open config file instead of Settings; disable command palette. |
| `onboarding_complete` | `true` | First-run welcome flow; set `false` to see it again. |
# Appearance (/docs/customize/appearance)
## Theme [#theme]
| Key | Default | Description |
| ----------------- | ------- | ---------------------------------------------- |
| `theme` | `termy` | Current color scheme name. |
| `chrome_contrast` | `false` | Increase contrast of non-terminal UI surfaces. |
## Font [#font]
| Key | Default | Description |
| ------------- | ----------------------------------------------------- | ----------------------------------- |
| `font_family` | system monospace (`Menlo` / `Consolas` / `monospace`) | Font family used in terminal UI. |
| `font_size` | `14` | Terminal font size in pixels. |
| `line_height` | `1.40` | Line height multiplier (0.8 – 2.5). |
## Window [#window]
| Key | Default | Description |
| -------------------------- | ------- | ------------------------------------------------------- |
| `background_opacity` | `1.0` | Window background opacity (0.0 – 1.0). |
| `background_opacity_cells` | `false` | Apply window transparency to terminal cell backgrounds. |
| `background_blur` | `false` | Enable blur for transparent backgrounds. |
| `window_width` | `1280` | Startup window width in pixels. |
| `window_height` | `820` | Startup window height in pixels. |
## Padding [#padding]
| Key | Default | Description |
| ----------- | ------- | -------------------------------- |
| `padding_x` | `12` | Left and right terminal padding. |
| `padding_y` | `8` | Top and bottom terminal padding. |
# Colors (/docs/customize/colors)
Use the `[colors]` block to override the active theme. Values are 6-digit hex (`#RRGGBB`). For full themes, see [Themes](/docs/customize/themes).
```txt
[colors]
foreground = #e7ebf5
background = #0b1020
cursor = #a7e9a3
```
## Core [#core]
| Key | Aliases | Description |
| ------------ | ------- | -------------------- |
| `foreground` | `fg` | Default text color. |
| `background` | `bg` | Terminal background. |
| `cursor` | | Cursor color. |
## ANSI 0–7 [#ansi-07]
| Key | Aliases | Description |
| --------- | -------- | ------------- |
| `black` | `color0` | ANSI black. |
| `red` | `color1` | ANSI red. |
| `green` | `color2` | ANSI green. |
| `yellow` | `color3` | ANSI yellow. |
| `blue` | `color4` | ANSI blue. |
| `magenta` | `color5` | ANSI magenta. |
| `cyan` | `color6` | ANSI cyan. |
| `white` | `color7` | ANSI white. |
## ANSI 8–15 (bright) [#ansi-815-bright]
| Key | Aliases | Description |
| ---------------- | -------------------------- | --------------- |
| `bright_black` | `brightblack`, `color8` | Bright black. |
| `bright_red` | `brightred`, `color9` | Bright red. |
| `bright_green` | `brightgreen`, `color10` | Bright green. |
| `bright_yellow` | `brightyellow`, `color11` | Bright yellow. |
| `bright_blue` | `brightblue`, `color12` | Bright blue. |
| `bright_magenta` | `brightmagenta`, `color13` | Bright magenta. |
| `bright_cyan` | `brightcyan`, `color14` | Bright cyan. |
| `bright_white` | `brightwhite`, `color15` | Bright white. |
# Customize Termy (/docs/customize)
Use Settings for normal changes. Edit `~/.config/termy/config.txt` directly when
you want a portable, version-controlled setup.
## Common goals [#common-goals]
| I want to… | Go to |
| -------------------------- | ------------------------------------------------------------------ |
| Change the font or cursor | [Appearance](/docs/customize/appearance) |
| Use a different shell | [Terminal behavior](/docs/customize/terminal-behavior) |
| Rebind a shortcut | [Keybindings](/docs/customize/keybindings) |
| Find an exact setting name | [Configuration reference](/docs/reference/configuration-reference) |
# Keybindings (/docs/customize/keybindings)
Repeated `keybind` lines in `~/.config/termy/config.txt`:
```txt
keybind = clear
keybind = secondary-p=toggle_command_palette
keybind = secondary-t=new_tab
keybind = secondary-w=unbind
keybind = secondary-g=plugin:git-tools/status
```
* Lines apply in order; later lines win for the same trigger.
* `clear` removes all defaults before later lines apply.
* `unbind` removes a mapping.
* `secondary` → `cmd` on macOS, `ctrl` elsewhere.
* `plugin:/` runs a plugin command. Commands with inputs open their input form.
* Termy refreshes plugins before a plugin shortcut runs, so changed manifests and source are picked up.
Defaults: [Keybinding defaults](/docs/reference/keybinding-defaults). Actions: [Actions](/docs/reference/actions).
# Tabs and Titlebar (/docs/customize/tabs-titlebar)
## Tab Titles [#tab-titles]
| Key | Default | Description |
| ----------------------------- | ----------------------------------- | --------------------------------------------- |
| `tab_title_mode` | `smart` | `smart` \| `shell` \| `explicit` \| `static`. |
| `tab_title_priority` | `manual, explicit, shell, fallback` | Exact source priority. |
| `tab_title_fallback` | `Terminal` | Title when no source available. |
| `tab_title_explicit_prefix` | `termy:tab:` | Prefix for explicit OSC title payloads. |
| `tab_title_shell_integration` | `true` | Export `TERMY_*` env vars for shell hooks. |
| `tab_title_prompt_format` | `{cwd}` | Template for prompt-derived titles. |
| `tab_title_command_format` | `{command}` | Template for command-derived titles. |
## Tab Strip [#tab-strip]
| Key | Default | Description |
| --------------------------- | -------------------- | -------------------------------------------------- |
| `tab_close_visibility` | `active_hover` | `active_hover` \| `hover` \| `always`. |
| `tab_width_mode` | `active_grow_sticky` | `stable` \| `active_grow` \| `active_grow_sticky`. |
| `tab_switch_modifier_hints` | `true` | Show secondary+1..9 badges on first nine tabs. |
| `auto_hide_tabbar` | `true` | Hide tab bar when only one tab is open. |
## Title Bar [#title-bar]
| Key | Default | Description |
| ------------------------ | ------- | -------------------------------------------- |
| `show_termy_in_titlebar` | `true` | Show or hide Termy branding in the titlebar. |
# Terminal Behavior (/docs/customize/terminal-behavior)
## Shell [#shell]
| Key | Default | Description |
| --------------------------- | ---------------- | ---------------------------------------- |
| `shell` | login shell | Executable used for new sessions. |
| `term` | `xterm-256color` | `TERM` value exposed to child apps. |
| `colorterm` | `truecolor` | `COLORTERM` value exposed to child apps. |
| `shell_integration_enabled` | `true` | OSC 133 prompt and command tracking. |
## Cursor [#cursor]
| Key | Default | Description |
| -------------- | ------- | --------------------------------- |
| `cursor_style` | `block` | `block` or `line`. |
| `cursor_blink` | `true` | Enable blinking cursor animation. |
## Scrolling [#scrolling]
| Key | Default | Description |
| ------------------------- | ----------- | --------------------------------------------------- |
| `mouse_scroll_multiplier` | `3` | Mouse wheel scroll speed multiplier. |
| `scrollbar_visibility` | `on_scroll` | `off` \| `always` \| `on_scroll`. |
| `scrollbar_style` | `neutral` | `neutral` \| `muted_theme` \| `theme`. |
| `scrollback_history` | `2000` | Lines retained in scrollback (alias: `scrollback`). |
| `inactive_tab_scrollback` | unset | Scrollback limit for inactive tabs. |
## Panes [#panes]
| Key | Default | Description |
| --------------------- | ---------------- | ------------------------------------------------------ |
| `pane_focus_effect` | `soft_spotlight` | `off` \| `soft_spotlight` \| `cinematic` \| `minimal`. |
| `pane_focus_strength` | `0.6` | Strength of active pane emphasis (0.0 – 2.0). |
## Clipboard [#clipboard]
| Key | Default | Description |
| ---------------------- | ------- | ----------------------------------------- |
| `copy_on_select` | `false` | Auto-copy selected text. |
| `copy_on_select_toast` | `true` | Show toast when text is copied on select. |
Termy supports the Kitty clipboard protocol (OSC 5522) out of the box. Terminal
applications can list, read, and write multiple clipboard formats—including
images and other non-text data—and can receive MIME-aware paste events. Reads
that are not covered by a short-lived paste token or a remembered application
grant require confirmation; format listing does not expose clipboard contents
and does not prompt. The primary selection is supported on Linux. Native and
tmux terminals use the same behavior.
## UI [#ui]
Full-screen terminal apps such as OpenCode use the entire content area when
running in a single pane. Termy removes the shell's horizontal and vertical
padding while the alternate screen is active, then restores your configured
padding when the app exits. Opaque app backgrounds extend into Termy's title
bar, and matching colors fill the window edges so a different terminal theme
does not leave a contrasting border. Window controls and tabs remain available.
| Key | Default | Description |
| ------------------------------- | ------- | --------------------------------------------- |
| `command_palette_show_keybinds` | `true` | Show shortcut badges in command palette rows. |
| `progress_indicator_enabled` | `true` | Show OSC 9;4 progress in tab badges. |
## Tmux [#tmux]
| Key | Default | Description |
| ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------ |
| `tmux_enabled` | `false` | Enable tmux runtime integration. |
| `tmux_persistence` | `true` | Reuse tmux tabs and panes across restarts. |
| `tmux_exclusive` | `false` | Stay in tmux control mode; restart it instead of falling back to a classic terminal when control mode exits. |
| `tmux_binary` | `tmux` | tmux executable path or binary name. |
| `tmux_show_active_pane_border` | `false` | Show active tmux pane border highlight. |
# Themes (/docs/customize/themes)
Drop `.json` files in `~/.config/termy/themes/` (Windows: `%APPDATA%\termy\themes\`). The filename (without `.json`) is the theme slug:
```txt
theme = my-theme
```
Or pick one from **Switch Theme** in the command palette.
## Format [#format]
```json
{
"background": "#0b1020",
"foreground": "#e7ebf5",
"cursor": "#a7e9a3",
"black": "#0b1020",
"red": "#f1b8c5",
"green": "#a7e9a3"
}
```
All keys are 6-digit hex. See [Colors](/docs/customize/colors) for the full list.
## Import without switching theme [#import-without-switching-theme]
**Import Colors** in the command palette writes values into `[colors]` while keeping your current `theme` — useful for one-off tweaks.
Delete `[colors]` to fall back to the pure theme file.
# Command Boundary (/docs/developer/command-boundary)
## Ownership [#ownership]
`termy_command_core` owns:
* Command IDs and config-facing command names
* Command config-name parsing and normalization
* Keybind defaults
* Keybind directive parsing (`clear`, bind, unbind)
* Deterministic keybind resolution order
App/CLI adapters own:
* UI labels, keywords, and command-palette presentation
* Platform-specific visibility policy for palette entries
* UI trigger canonicalization and validation (e.g. GPUI keystroke parsing)
## Dependency Rule [#dependency-rule]
`termy_command_core` must remain a pure domain crate. It must **not** depend on:
* `termy_config_core`
* `gpui`
* Other UI or presentation crates
## Integration Pattern [#integration-pattern]
* Adapters convert parsed config keybind lines into `termy_command_core::KeybindLineRef`.
* Adapters call `parse_keybind_directives_from_iter`; trigger canonicalization happens in `termy_command_core`.
* Adapters call `resolve_keybinds` over `default_resolved_keybinds`.
One canonical command/keybind engine; thin, readable adapter code.
# Develop and embed (/docs/developer)
Choose the path that matches what you are building.
## Pick the correct layer [#pick-the-correct-layer]
| Change | Owning layer |
| -------------------------------------------- | ------------------- |
| Terminal parsing, PTY, frames, input, search | `termy_core` |
| C, Swift, or Objective-C integration | `termy_ffi` |
| Shared tabs, panes, and terminal UI behavior | `termy_terminal_ui` |
| GPUI desktop application | `termy` |
# libtermy (/docs/developer/libtermy)
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 [#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 [#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 [#rust-quick-start]
Add `termy_core` from the Termy repository. During beta, pinning a known commit
or release is recommended:
```toml title="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:
```rust
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 [#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 [#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 [#display-only-terminals]
When another process owns the session—such as a tmux control-mode client—create
a grid without a PTY:
```rust
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 [#c-abi-quick-start]
Build the shared library from the repository:
```bash
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`](https://github.com/lassejlv/termy/blob/main/crates/ffi/include/termy.h).
```c
#include "termy.h"
#include
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 [#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 [#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 [#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 [#input-search-and-links]
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 [#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:
1. Stop issuing new terminal calls.
2. Call `termy_terminal_notify_wakeup`.
3. Join the waiting thread.
4. Call `termy_terminal_free`.
## Beta expectations [#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 [#validate-an-integration]
When working from a Termy checkout, the focused checks are:
```bash
cargo test -p termy_core
cargo test -p termy_ffi
cargo build --release -p termy_ffi
```
# Render Metrics (/docs/developer/render-metrics)
Termy ships a debug-only render metrics overlay for diagnosing rendering churn.
## Enable [#enable]
```bash
RUST_LOG=info TERMY_RENDER_METRICS=1 cargo run -p termy
```
Metrics logs are gated on `debug_assertions`; `--release` builds will not emit `render_metrics` lines.
## Counters [#counters]
| Counter | Meaning |
| --------------------------- | ----------------------------------------------------- |
| `full` | Full per-pane cell cache rebuild decisions |
| `partial` | Dirty-span patch decisions |
| `reuse` | No cell cache update needed |
| `dirty_span` | Number of dirty spans consumed during partial updates |
| `patched_cell` | Number of cells patched from dirty spans |
| `grid_paint` / `shape_line` | Paint + text shaping work that interval |
## Sanity check [#sanity-check]
During cursor blink, `full` should stay near `0`. Small `partial` or `reuse` values are expected depending on reported terminal damage.
## Benchmarks (macOS only) [#benchmarks-macos-only]
```bash
cargo run -p xtask -- benchmark-compare
```
Requires `xctrace` / Activity Monitor. Not supported on Linux or Windows.
# Config file (/docs/getting-started/config-file)
| Platform | Path |
| ------------- | ---------------------------- |
| macOS / Linux | `~/.config/termy/config.txt` |
| Windows | `%APPDATA%\termy\config.txt` |
Open **Settings** (`Cmd ,` / `Ctrl ,`) or edit the file directly. Each line is `key = value`. Comments start with `#`.
```txt
theme = termy
font_family = JetBrains Mono
font_size = 14
shell = /opt/homebrew/bin/fish
keybind = cmd-p=toggle_command_palette
```
Termy does not bundle custom fonts. A `font_family` value must name a family installed on your system; if it is missing or not fixed-pitch, Termy shows a warning and falls back to an installed monospace font (preferring Consolas on Windows, Menlo on macOS, or your Fontconfig monospace preference on Linux). On Linux, `font_family = monospace` follows that system preference and resolves it to a concrete font before rendering. On Windows, if an installed font is still not found, try reinstalling it for all users rather than only your account.
## Colors [#colors]
Only `[colors]` uses a section header:
```txt
[colors]
foreground = #e7ebf5
background = #0b1020
```
## Tasks [#tasks]
```txt
task.build.command = cargo build
task.build.working_dir = crates/cli
```
## Common tweaks [#common-tweaks]
| Goal | Setting |
| ------------------- | --------------------------- |
| Transparent window | `background_opacity = 0.88` |
| Blur | `background_blur = true` |
| Always show tab bar | `auto_hide_tabbar = false` |
Full key list: [Configuration reference](/docs/reference/configuration-reference). Keybind syntax: [Keybindings](/docs/customize/keybindings).
# Getting started (/docs/getting-started)
## 1. Install the app [#1-install-the-app]
Download the build for your platform, install it, and complete the first launch.
[Install Termy →](/docs/getting-started/installation)
macOS builds are not signed yet. If macOS says Termy is damaged or refuses to
open it, follow the one-command fix in [Troubleshooting](/docs/getting-started/troubleshooting).
## 2. Confirm the essentials [#2-confirm-the-essentials]
On first launch, Termy uses your login shell and a system monospace font. Open
Settings with `Cmd ,` on macOS or `Ctrl ,` on Linux and Windows.
| Check | What to do next |
| ------------------------------------- | ----------------------------------------------------------- |
| Shell opens correctly | Continue to daily workflows |
| Font or theme feels wrong | Open [Appearance](/docs/customize/appearance) |
| Shell or working directory is wrong | Open [Terminal behavior](/docs/customize/terminal-behavior) |
| A shortcut conflicts with another app | Open [Keybindings](/docs/customize/keybindings) |
## 3. Learn the command palette [#3-learn-the-command-palette]
Press `Cmd P` or `Ctrl P`. The palette exposes Termy's actions without requiring
you to memorize every shortcut first.
[Use the command palette →](/docs/using-termy/command-palette)
## 4. Shape your workspace [#4-shape-your-workspace]
Continue with the feature that matches how you work:
# Install (/docs/getting-started/installation)
The fastest path is the [Termy download page](/download). It detects your platform and links to the current GitHub release.
## macOS [#macos]
1. On the [download page](/download), choose **Apple silicon** for M-series Macs or **Intel** for older Macs.
2. Open the `.dmg` and drag Termy to `/Applications`.
3. Try to open Termy once.
If macOS blocks the first launch, remove its quarantine attribute:
```bash
sudo xattr -d com.apple.quarantine /Applications/Termy.app
```
Then open Termy again. [See the full troubleshooting guide](/docs/getting-started/troubleshooting) if it still does not launch.
## Linux [#linux]
Download the AppImage or `.tar.gz` from [GitHub Releases](https://github.com/lassejlv/termy/releases).
* **AppImage:** mark it executable, then run it.
* **Archive:** unpack it and run the included Termy binary.
Linux release launchers prefer X11/XWayland when it is available so the desktop
window manager can provide the native title bar and window controls. To run the
Wayland backend instead, launch Termy with `TERMY_LINUX_BACKEND=wayland`.
Both terminal and Settings windows request system-managed decorations, including
KDE's title bar, window controls, and resize borders.
## Windows [#windows]
Download and run the `.msi` installer from [GitHub Releases](https://github.com/lassejlv/termy/releases).
## From source [#from-source]
```bash
git clone https://github.com/lassejlv/termy
cd termy
cargo run --release -p termy
```
This route requires Rust and the platform dependencies used by the desktop app. Prefer a release binary unless you are developing Termy itself.
## First launch [#first-launch]
| | |
| --------------- | ------------------------------------------------------------ |
| Theme | `termy` |
| Shell | your login shell (`$SHELL`) |
| Font | system monospace (Menlo on macOS, Consolas on Windows), 14px |
| Settings | `Cmd ,` (macOS) · `Ctrl ,` (Linux / Windows) |
| Command palette | `Cmd P` · `Ctrl P` |
| Open a folder | file manager **Open new Termy tab here**, or `termy /path` |
# Troubleshooting (/docs/getting-started/troubleshooting)
## macOS: app won't open [#macos-app-wont-open]
Termy is not notarized yet. After downloading, macOS may show that the app is damaged or refuse to open it.
First, make sure `Termy.app` is in `/Applications`. Then remove the quarantine attribute:
```bash
sudo xattr -d com.apple.quarantine /Applications/Termy.app
```
Open Termy again when the command finishes.
### The command cannot find Termy [#the-command-cannot-find-termy]
The command assumes Termy is installed at `/Applications/Termy.app`. If you put it somewhere else, replace that path with the real location.
The simplest fix is to open the downloaded `.dmg`, drag Termy into **Applications**, and run the command again.
### Termy is still blocked [#termy-is-still-blocked]
1. Remove any older copy of Termy from Applications.
2. Download the newest `.dmg` from the [Termy download page](/download).
3. Drag Termy into Applications.
4. Run the quarantine command again.
## Report an issue [#report-an-issue]
If the problem continues, [open a GitHub issue](https://github.com/lassejlv/termy/issues/new) with:
* your operating system and architecture;
* the Termy release you downloaded;
* the exact error message;
* what you expected to happen.
Do not include tokens, shell history, or other secrets in screenshots or logs.
# Termy documentation (/docs)
New to Termy? Start with [Install Termy](/docs/getting-started/installation),
then keep [Troubleshooting](/docs/getting-started/troubleshooting) nearby for
the current unsigned macOS build.
## Start here [#start-here]
These steps take you from download to a useful daily setup.
## Find an answer by goal [#find-an-answer-by-goal]
## Useful shortcuts [#useful-shortcuts]
| Goal | macOS | Linux / Windows |
| ---------------------- | ------- | --------------- |
| Open Settings | `Cmd ,` | `Ctrl ,` |
| Open command palette | `Cmd P` | `Ctrl P` |
| Search terminal output | `Cmd F` | `Ctrl F` |
Use the search button in the sidebar when you know a setting or feature name but
not which section owns it.
# Actions (/docs/reference/actions)
## Tabs [#tabs]
`new_tab`, `close_tab`, `close_pane_or_tab`, `move_tab_left`, `move_tab_right`,
`switch_tab_left`, `switch_tab_right`, `switch_to_tab_1` … `switch_to_tab_9`,
`rename_tab`, `toggle_tab_bar_visibility`.
## Panes [#panes]
`split_pane_vertical`, `split_pane_horizontal`, `close_pane`,
`focus_pane_left`, `focus_pane_right`, `focus_pane_up`, `focus_pane_down`,
`focus_pane_next`, `focus_pane_previous`,
`resize_pane_left`, `resize_pane_right`, `resize_pane_up`, `resize_pane_down`,
`toggle_pane_zoom`.
## Window [#window]
`minimize_window`, `zoom_in`, `zoom_out`, `zoom_reset`, `quit`, `restart_app`, `app_info`.
## Search [#search]
`open_search`, `close_search`, `search_next`, `search_previous`,
`toggle_search_case_sensitive`, `toggle_search_regex`.
## Clipboard [#clipboard]
`copy`, `paste`.
## Terminal [#terminal]
`clear_screen` sends the same form-feed control character as `Ctrl+L` to the
active pane, clearing the viewport while preserving scrollback in shells that
use the conventional `Ctrl+L` behavior.
```txt
keybind = secondary-k=clear_screen
```
## App [#app]
`toggle_command_palette`, `open_settings`, `open_config`, `prettify_config`,
`switch_theme`, `import_colors`, `check_for_updates`, `install_cli`.
## Workflows [#workflows]
`manage_tmux_sessions`, `manage_saved_layouts`, `run_task`, `toggle_agent_sidebar`.
# Configuration Reference (/docs/reference/configuration-reference)
The canonical configuration reference is generated from the Rust config schema.
```bash
just generate-config-doc
```
The generated source lives at [`docs/configuration.md`](https://github.com/lassejlv/termy/blob/main/docs/configuration.md). The website's [Customize](/docs/customize) pages group the same settings by task and explain common workflows.
## Common Sections [#common-sections]
* [Appearance](/docs/customize/appearance)
* [Terminal Behavior](/docs/customize/terminal-behavior)
* [Tabs and Titlebar](/docs/customize/tabs-titlebar)
* [Colors](/docs/customize/colors)
* [Themes](/docs/customize/themes)
* [Advanced](/docs/customize/advanced)
## Maintenance Rule [#maintenance-rule]
Do not hand-copy new settings into reference tables without updating the generated config docs first. Generated docs are the source of truth for key names and defaults.
# Reference (/docs/reference)
Reference pages are for lookup. If you are deciding what to change, start in
[Customize Termy](/docs/customize) instead.
## How the pieces connect [#how-the-pieces-connect]
```txt
key press → keybinding → action → Termy behavior
config key → parsed setting → runtime or appearance change
```
When creating a shortcut, find the behavior in [Actions](/docs/reference/actions),
then map that action using [Keybindings](/docs/customize/keybindings).
# Defaults (/docs/reference/keybinding-defaults)
`secondary` is `cmd` on macOS and `ctrl` on Linux/Windows. Copy/paste differs per platform.
## macOS [#macos]
```txt
secondary-q quit
secondary-, open_settings
secondary-p toggle_command_palette
secondary-t new_tab
secondary-w close_pane_or_tab
secondary-1..9 switch_to_tab_1..9
secondary-d split_pane_vertical
secondary-shift-d split_pane_horizontal
secondary-o focus_pane_next
secondary-alt- focus_pane_
secondary-alt-shift- resize_pane_
secondary-enter toggle_pane_zoom
secondary-= / - zoom_in / zoom_out
secondary-0 zoom_reset
secondary-f open_search
secondary-g search_next
secondary-shift-g search_previous
secondary-m minimize_window
secondary-c copy
secondary-v paste
```
## Linux [#linux]
Identical to macOS except clipboard uses `ctrl-shift-c` / `ctrl-shift-v`. No `minimize_window` binding.
## Windows [#windows]
Identical to Linux. `secondary-c` and `secondary-v` map to copy/paste through the `secondary` modifier (`ctrl`).
## Notes [#notes]
* `toggle_agent_sidebar` is currently unavailable on Windows.
# Build plugins with an agent (/docs/skills/build-termy-plugins)
Install the skill:
```sh
npx skills add lassejlv/termy --skill build-termy-plugins
```
Then ask your agent:
```text
Use build-termy-plugins to create a plugin named git-summary.
Add one command that shows Git status. Do not install it yet.
```
The skill includes Termy's API reference, security rules, templates, and validation
steps. It supports commands, settings, events, storage, keybindings, and native UI.
Plugins are trusted Bun code with your user account's access. Read the source
before installing one.
When the source is ready, load it in development mode:
```sh
termy plugin dev ./git-summary
```
See the [plugin guide](/docs/using-termy/plugins) for the full API and installation
details.
# Migrate from Ghostty with an agent (/docs/skills/ghostty-to-termy)
Install the skill:
```sh
npx skills add lassejlv/termy --skill ghostty-to-termy
```
Then ask your agent:
```text
Use ghostty-to-termy to convert ~/.config/ghostty/config.
Do not replace my live Termy config.
```
The skill creates a Termy config and a compatibility report. Unsupported or lossy
Ghostty settings are reported instead of guessed.
## Run the converter directly [#run-the-converter-directly]
From a Termy checkout:
```sh
python3 skills/ghostty-to-termy/scripts/convert_ghostty_to_termy.py \
~/.config/ghostty/config \
--output /tmp/termy-config.txt \
--report /tmp/termy-compatibility.json \
--strict
```
Check both output files before replacing your live
`~/.config/termy/config.txt`.
See the [configuration reference](/docs/reference/configuration-reference) for
manual changes.
# Agent skills (/docs/skills)
Termy includes two skills for coding agents:
## Install [#install]
Install both skills:
```sh
npx skills add lassejlv/termy
```
Or install one:
```sh
npx skills add lassejlv/termy --skill ghostty-to-termy
npx skills add lassejlv/termy --skill build-termy-plugins
```
Add `-g` to install globally instead of in the current project.
The complete skill sources are also available in the repository's
[`skills`](https://github.com/lassejlv/termy/tree/main/skills) folder.
# Command palette (/docs/using-termy/command-palette)
Open with `secondary-p` (`Cmd P` / `Ctrl P`).
Includes every [action](/docs/reference/actions), theme (preview on hover), setting, [task](/docs/using-termy/tasks-layouts), and tmux session when `tmux_enabled = true`.
Hide shortcut badges:
```txt
command_palette_show_keybinds = false
```
Disable the palette and open the raw config from Settings instead:
```txt
simple_mode = true
```
# Using Termy (/docs/using-termy)
Start with the command palette if you prefer discovering actions before learning
shortcuts. Use tabs and panes for local organization; add tasks, layouts, or tmux
when work needs to be repeatable or persistent.
## Choose the right workspace model [#choose-the-right-workspace-model]
| Need | Start with |
| ---------------------------------------- | ---------------------------------------------------- |
| A few temporary shells | [Tabs](/docs/using-termy/tabs) |
| Related commands visible together | [Splits and panes](/docs/using-termy/splits-panes) |
| Repeat the same setup later | [Tasks and layouts](/docs/using-termy/tasks-layouts) |
| Open a terminal on a saved remote host | [SSH hosts](/docs/using-termy/ssh-hosts) |
| Keep work alive after closing the window | [Tmux sessions](/docs/using-termy/tmux-sessions) |
# Commands and context (/docs/using-termy/plugins/commands-context)
Commands are searchable actions that run inside the plugin's Bun Worker. They can
collect native inputs, inspect a read-only Termy context, emit notifications, and
return typed actions to the app.
```ts title="plugin.ts"
export default definePlugin({
settings: {
greeting: {
type: "text",
title: "Greeting",
defaultValue: "Hello from Termy",
},
token: {
type: "secret",
title: "API token",
},
},
commands: [
{
id: "greet",
title: "Hello: Greet me",
placements: ["commandPalette", "terminalContextMenu"],
keywords: ["hello", "example"],
icon: "info",
inputs: [
{
id: "style",
type: "select",
label: "Greeting style",
options: [
{ value: "short", label: "Short" },
{ value: "friendly", label: "Friendly" },
],
},
{
id: "confirmed",
type: "confirm",
label: "Show greeting?",
defaultValue: true,
},
],
async run({ inputs, context }) {
if (inputs.confirmed !== true) return;
const message =
inputs.style === "friendly"
? `Welcome to Termy ${context.appVersion}`
: context.settings.get("greeting") ?? "Hello from Termy";
context.toasts.success(message);
},
},
],
} satisfies TermyPlugin);
```
## Command fields and inputs [#command-fields-and-inputs]
Commands require `id`, `title`, and `run`. They may also declare search `keywords`,
`status`, `enabled`, `disabledReason`, `timeoutMs`, and one of Termy's built-in
`icon` names.
`placements` controls where a command is listed. It accepts `commandPalette`,
`terminalContextMenu`, and `tabContextMenu`, and defaults to
`["commandPalette"]`. Context-menu placements are currently available on Linux
and Windows. Commands with inputs use the same palette input flow from every
surface. Use `placements: []` for a keybinding-only command.
Use `when` to make a command available only in matching context. Supported filters
are `hasSelection`, `hasWorkingDirectory`, `runtimes`, and `platforms`. Termy
checks the condition when listing the command and again immediately before running
it.
Inputs appear sequentially before the handler runs:
* `text` returns a string and supports a placeholder, default, required flag, and maximum length.
* `select` returns the value of one fixed option.
* `pick` calls `loadOptions({ query, context })` as the user types and returns the selected option value.
* `confirm` returns a boolean.
Commands without inputs run immediately. Inputs are delivered as
`inputs.` to `run`.
Pick loaders may be async and return the same `{ value, label, keywords?, status? }`
shape as select options. Termy debounces requests and ignores stale results. Keep
loaders side-effect free; they cannot return actions.
## Read the active context [#read-the-active-context]
Every handler receives a read-only snapshot with `platform`, `appVersion`, the
resolved session launch `shell`, and the active `runtime` (`native` or `tmux`). It
may also include `workingDirectory`, `activeCommand`, `selectedText`, `activeTab`,
and `activePane`.
`context.origin` contains stable `windowId`, `tabId`, and `paneId` values captured
when the request starts. Use it to keep an async action aimed at its original
terminal even if the user changes tabs. Active tab and pane snapshots include the
same stable IDs.
Tab and pane indexes are zero-based. Selected text is capped at 64 KiB on a UTF-8
boundary; `selectedTextTruncated` reports whether Termy shortened it.
```ts
run({ context }) {
if (!context.selectedText) {
context.toasts.info("Select terminal text first");
return;
}
return { type: "clipboard.write", text: context.selectedText };
}
```
The context also exposes `context.toasts.info(message)`, `success(message)`,
`warning(message)`, and `error(message)`. These emit notifications directly and do
not require an SDK import or returned action.
Long-running handlers should stop when `context.signal` aborts. They can update the
cancellable loading toast with
`context.progress.report({ message?, percentage? })`; percentages range from 0 to
100\.
## Plugin settings [#plugin-settings]
Declare typed `toggle`, `text`, `select`, and `secret` settings beside `commands`.
Termy renders them under **Settings → Plugins**, and handlers read the resolved
values with `context.settings.get("settingName")`.
Ordinary overrides live in the plugin data directory. Secrets are masked and stored
through the operating-system credential store instead of `settings.json`. Changes
apply to the next invocation without restarting the Worker.
## Return actions [#return-actions]
A command, lifecycle handler, or native-view action handler can return one action,
an action array, `{ actions: [...] }`, or nothing. Handlers may be async.
| Type | Purpose |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `terminal.run` | Legacy action that opens a tab and runs a shell command. |
| `terminal.sendText` | Send text, optionally followed by Enter, to `origin`, `active`, or an exact terminal target. |
| `terminal.open` | Open a tab, right/down split, or window with an optional working directory and launch. |
| `termy.command` | Invoke a built-in Termy command. |
| `clipboard.write` | Copy text to the system clipboard. |
| `url.open` | Open an `http` or `https` URL. |
| `view.open` | Open a native view with optional JSON `params`. Add `target: "commandPalette"` to use the palette surface; the default is a modal. Requires `native-ui`. |
| `view.replace` | Replace the current plugin view and params in place. |
| `view.close` | Close the current plugin view. |
| `toast` | Show an info, success, warning, or error notification. |
Exact terminal targets are scoped to the Termy window that delivered the
invocation. `focus: false` restores the previous pane after opening a tab or
split; a newly created OS window necessarily becomes focused.
`terminal.open.launch` accepts `{ type: "shell", command }` or
`{ type: "program", program, args? }`. Prefer structured programs when exact argv
boundaries matter. Structured program launches require the native runtime; tmux
rejects them because preserving argv would otherwise require shell parsing.
Never interpolate free-form text directly into `terminal.run` or a shell launch.
Map selected values to fixed commands or quote values correctly for the target
shell.
## Bind a plugin command [#bind-a-plugin-command]
Use the manifest and command IDs in `~/.config/termy/config.txt`:
```txt
keybind = secondary-g=plugin:git-tools/status
```
Commands with inputs open their input form. Termy refreshes the plugin catalog
before invoking a shortcut, so saved plugin changes are picked up. Normal keybinding
ordering applies: later lines win, `unbind` removes the shortcut, and a task
keybinding takes priority on conflicts.
See [Native UI](/docs/using-termy/plugins/native-ui) for `view.open` and
[Lifecycle and storage](/docs/using-termy/plugins/lifecycle-storage) for events and
persistence.
# Getting started (/docs/using-termy/plugins/getting-started)
Install [Bun](https://bun.sh/), then scaffold a plugin:
```sh
termy plugin init my-plugin
termy plugin dev ./my-plugin
```
`dev` installs or updates Termy's managed copy and watches the development folder.
Open the command palette after saving to load the latest valid version.
## Plugin files [#plugin-files]
A minimal plugin has a manifest and a TypeScript entrypoint:
```text
my-plugin/plugin.json
my-plugin/plugin.ts
```
`plugin.json` owns identity and API metadata:
```json title="plugin.json"
{
"$schema": "https://termy.sh/schemas/plugin.schema.json",
"apiVersion": 1,
"id": "hello",
"name": "Hello",
"version": "1.0.0",
"capabilities": []
}
```
`version` is optional metadata shown in Settings. Set `main` to a relative `.ts` or
`.tsx` path when the entrypoint is not `plugin.ts`; it must stay inside the plugin
directory and cannot be a symlink.
`$schema` gives compatible editors validation and autocomplete from Termy's public
[plugin manifest schema](/schemas/plugin.schema.json). `termy plugin init` adds it
automatically.
`capabilities` declares the Termy host APIs a plugin uses. Supported values are
`storage` for `context.storage` and `context.paths`, and `native-ui` for views and
`view.open`. Omitting the field means no capabilities; unknown and duplicate values
are rejected.
Capabilities gate Termy APIs. They are not operating-system permissions: plugins
are still trusted Bun code with normal file, network, and process access.
The entrypoint exports a plugin definition without repeating manifest metadata:
```ts title="plugin.ts"
export default definePlugin({
commands: [
{
id: "greet",
title: "Hello: Greet me",
keywords: ["hello", "example"],
icon: "info",
run({ context }) {
context.toasts.success(`Welcome to Termy ${context.appVersion}`);
},
},
],
} satisfies TermyPlugin);
```
Termy manages `plugins/termy.d.ts`, which supplies the global `definePlugin`,
`TermyPlugin`, `TermyPluginContext`, and JSX types. Do not import an SDK or run a
build step.
## Install and manage plugins [#install-and-manage-plugins]
Use **Settings → Plugins → Install from folder** to select a plugin directory.
Termy validates it and copies it into the managed plugins directory. The same screen
shows Bun status and lets you refresh, open the folder, enable, disable, or uninstall
plugins.
The CLI supports local folders and trusted GitHub sources:
```sh
termy plugin add ./my-plugin
termy plugin add https://github.com/example/termy-plugins --path my-plugin
termy plugin status my-plugin
termy plugin disable my-plugin
termy plugin enable my-plugin
termy plugin update my-plugin
termy plugin uninstall my-plugin
```
`add` also accepts the `install` alias, while `remove` accepts `uninstall`. Local
installs copy the source into Termy's managed directory and leave the development
folder untouched.
GitHub repository URLs, `/tree/[/` URLs, `--ref`, and `--path` are
supported. Termy resolves refs to full commits and stores source metadata for
predictable status and updates. A repository with multiple valid plugins requires
`--path` instead of guessing.
The installer downloads regular files as data. It does not clone the repository,
run scripts, evaluate plugin code, or install dependencies while installing. The
plugin becomes trusted Bun code when Termy loads it, so interactive CLI installs
require confirmation and automation must pass `--yes`.
## Develop safely [#develop-safely]
`termy plugin dev ./my-plugin` validates the source tree and atomically swaps each
valid update into place. A validation or copy failure leaves the managed copy
untouched; Bun load errors appear when the command palette refreshes. `Ctrl C` stops
the watcher without uninstalling the plugin.
Dev mode preserves enabled state and storage. It refuses to replace a GitHub-tracked
installation with a local source, so uninstall that copy first when intentionally
changing its origin.
The managed plugin directory is `$XDG_CONFIG_HOME/termy/plugins` when
`XDG_CONFIG_HOME` is set, `~/.config/termy/plugins` otherwise on macOS and Linux,
and `%APPDATA%\termy\plugins` on Windows.
Next, define richer [commands and context](/docs/using-termy/plugins/commands-context)
or switch the entrypoint to `.tsx` for [native UI](/docs/using-termy/plugins/native-ui).
# Plugins (/docs/using-termy/plugins)
Plugins extend Termy with TypeScript running in Bun. A plugin can add command-palette
commands, react to terminal lifecycle events, store local data, and open small native
tools rendered by Termy.
Plugins are trusted local code. They run with your user account's file, network,
and process access, so read the source before installing one.
## What plugins can do [#what-plugins-can-do]
| Feature | Use it for |
| -------------------- | ------------------------------------------------------------------------------------------------ |
| Commands | Add actions to the command palette, context menus, and keybindings. |
| Context | Read the active terminal, selection, working directory, shell, tab, and pane. |
| Native UI | Return allowlisted JSX that Termy validates and renders with GPUI. |
| Lifecycle events | Respond when the terminal is ready, a tab changes, the directory changes, or a command finishes. |
| Settings and storage | Expose typed settings and persist plugin-specific JSON or files. |
Install [Bun](https://bun.sh/) before loading plugins. Then open **Settings →
Plugins** to install, enable, disable, refresh, or uninstall them. Developers can
start with [Getting started](/docs/using-termy/plugins/getting-started).
# Lifecycle and storage (/docs/using-termy/plugins/lifecycle-storage)
Plugins can explicitly subscribe to active-terminal lifecycle events beside
`commands`. Use `commands: []` for an event-only plugin.
```ts title="plugin.ts"
export default definePlugin({
commands: [],
events: {
"terminal.ready"({ context }) {
context.toasts.info("Terminal ready");
},
"tab.activated"({ event, context }) {
console.log(event.previousTabId, context.activeTab?.id);
},
"pane.activated"({ event, context }) {
console.log(event.previousPaneId, context.activePane?.id);
},
"tab.created"({ event }) {
console.log("created", event.tabId);
},
"terminal.bell"() {
console.log("bell");
},
"workingDirectory.changed"({ event }) {
console.log(event.previousWorkingDirectory, event.workingDirectory);
},
"command.finished"({ event }) {
console.log(event.command, event.exitCode, event.durationMs);
},
},
} satisfies TermyPlugin);
```
Handlers receive the same read-only [context](/docs/using-termy/plugins/commands-context#read-the-active-context)
as commands. They can emit toasts or return the same actions.
`terminal.ready` runs once after the plugin catalog is ready. Other subscriptions
are `tab.activated`, `pane.activated`, `tab.created`, `tab.closed`,
`terminal.bell`, `workingDirectory.changed`, `command.started`, and
`command.finished`. Native shell integration provides command exit codes, measured
durations, and bell events. Tmux completion is inferred and its bell state is not
available, so unavailable fields/events are omitted instead of guessed.
Events remain ordered within one plugin. Separate plugins may handle them
concurrently under the normal execution timeout.
## Store JSON values [#store-json-values]
Declare the storage capability in `plugin.json`:
```json title="plugin.json"
{
"$schema": "https://termy.sh/schemas/plugin.schema.json",
"apiVersion": 1,
"id": "todos",
"name": "Todos",
"capabilities": ["storage"]
}
```
Then use the asynchronous storage API for small persistent values:
```ts
const todos = await context.storage.get("todos") ?? [];
await context.storage.set("todos", todos);
await context.storage.delete("todos");
await context.storage.clear();
```
Each plugin can store up to 512 JSON values totaling 1 MiB. Storage is plain local
JSON; use a declared `secret` setting for credentials.
## Store larger files [#store-larger-files]
`context.paths.dataDirectory` is for larger persistent files, while
`context.paths.cacheDirectory` is for disposable data. Plugins may use Bun or
Node-compatible file APIs to read and write these paths.
Both locations are isolated per plugin, live outside the content-hashed source
tree, and survive reloads and updates. Uninstalling a plugin removes its storage,
data, and cache. Disabling it keeps them.
Use [Native UI](/docs/using-termy/plugins/native-ui) to turn stored data into a
small native tool, or review the [security model](/docs/using-termy/plugins/security-limits)
before handling sensitive data.
# Native UI (/docs/using-termy/plugins/native-ui)
Plugins can open small native tools from `.tsx` entrypoints. JSX is compiled into a
strictly validated Termy document and rendered with GPUI; it is not HTML or React,
and plugins never receive native renderer access.
Declare `native-ui` in `plugin.json`; add `storage` too when the view persists data.
## Open a view [#open-a-view]
Point the manifest at a `.tsx` entrypoint:
```json title="plugin.json"
{
"$schema": "https://termy.sh/schemas/plugin.schema.json",
"apiVersion": 1,
"id": "todos",
"name": "Todos",
"main": "plugin.tsx",
"capabilities": ["storage", "native-ui"]
}
```
Add the Termy JSX pragmas, declare the view, and return `view.open` from a command:
```tsx title="plugin.tsx"
/** @jsxRuntime classic */
/** @jsx TermyUI.createElement */
/** @jsxFrag TermyUI.Fragment */
export default definePlugin({
commands: [{
id: "open",
title: "Todos: Open",
run() {
return { type: "view.open", view: "todos" };
},
}],
views: {
todos: {
title: "Todos",
async render({ params, context }) {
const todos = await context.storage.get>("todos") ?? [];
return (
Add
{todos.slice(0, 24).map((todo) => (
{todo.title}
))}
);
},
async onAction({ action, values, context }) {
// Persist changes using action.id, action.payload, and the form values.
// Termy rerenders the view after this handler finishes.
},
},
},
} satisfies TermyPlugin);
```
The view key (`todos`) must match the `view` in the `view.open` action. The managed
`termy.d.ts` file supplies `TermyUI`, JSX, view, and action types globally.
`view.open` accepts a bounded JSON `params` object. Termy freezes it and passes the
same values to `render` and `onAction`. Return `view.replace` to navigate to another
declared view in place, or `view.close` to dismiss the current view.
Views use a centered modal by default. Add `target: "commandPalette"` to render
the same bounded native document inside the palette results area while keeping
the palette search and footer:
```ts
return { type: "view.open", view: "todos", target: "commandPalette" };
```
## Handle interactions [#handle-interactions]
Interactive controls use named actions instead of JavaScript callbacks. Any view
that renders an interactive control must define `onAction`; passive views may omit
it.
* `Button`, `Checkbox`, `Select`, `List`, and `ListItem` use named actions; `payload` carries optional item data.
* `TextInput` and `TextArea` require an `id`; `submit` names an optional submit action.
* Every control ID in one rendered document must be unique.
Termy supplies Tab/Shift-Tab focus traversal, arrow-key selection for selects and
lists, and Enter/Space activation for controls. A filtering list also accepts text
while focused and filters its items locally.
`onAction` receives `action.id`, `action.controlId`, optional `action.payload` and
`action.value`, plus a `values` snapshot keyed by control ID. It can update storage,
emit toasts, or return any normal [plugin action](/docs/using-termy/plugins/commands-context#return-actions).
After it finishes, Termy calls `render` again and replaces the document.
```tsx
type Todo = { id: string; title: string; done: boolean };
async onAction({ action, values, context }) {
if (action.id === "add") {
const title = String(values.title ?? "").trim();
if (!title) {
context.toasts.info("Give the todo a title first");
return;
}
const todos = await context.storage.get("todos") ?? [];
await context.storage.set("todos", [
...todos,
{ id: crypto.randomUUID(), title, done: false },
]);
}
}
```
## Component allowlist [#component-allowlist]
| Component | Supported props |
| --------------- | ---------------------------------------------------------------------------------------------------- |
| `Column`, `Row` | `gap`, `align`, and children |
| `Text` | `variant`, `tone`, and text children |
| `TextInput` | `id`, `label`, `placeholder`, `value`, `maxLength`, `submit`, `disabled` |
| `TextArea` | TextInput props plus `rows` |
| `Select` | `id`, `label`, `placeholder`, `value`, `options`, `action`, `disabled` |
| `List` | `id`, `action`, `selectedId`, `searchPlaceholder`, `filtering`, `isLoading`, and `ListItem` children |
| `ListItem` | `id`, `title`, `subtitle`, `keywords`, `status`, `payload`, `action`, `disabled` |
| `EmptyState` | `title`, `description` |
| `Progress` | `label`, `value` from 0 through 100 |
| `Button` | `id`, `action`, `payload`, `variant`, `disabled`, and text children |
| `Checkbox` | `id`, `action`, `payload`, `checked`, `disabled`, and text children |
| `Divider` | No props |
| `Spacer` | `size` |
Gaps and spacer sizes use `none`, `small`, `medium`, or `large`. Alignment uses
`start`, `center`, `end`, or `stretch`. Text variants are `heading`, `body`,
`caption`, and `code`; tones are `default`, `muted`, `success`, and `danger`.
Button variants are `secondary`, `primary`, and `danger`.
Styling is semantic and theme-aware. Arbitrary CSS, GPUI properties, assets,
callbacks, and colors are rejected. Read
[Security and limits](/docs/using-termy/plugins/security-limits) for document and
runtime boundaries. Paginate dynamic lists so large saved collections stay inside
the node, child, and value-control limits.
# Security and limits (/docs/using-termy/plugins/security-limits)
Plugins are trusted Bun code, not sandboxed extensions. A handler can use async
functions, `fetch`, `Bun.*`, Node-compatible standard-library APIs, files,
subprocesses, network requests, and local relative TypeScript imports.
Worker isolation protects Termy from many crashes and timeouts, but it does not
protect your account from a malicious plugin. Read the source before installing.
Manifest capabilities gate Termy-owned APIs, not operating-system access. A plugin
needs `storage` for `context.storage` and `context.paths`, and `native-ui` for views
and `view.open`, but those declarations do not restrict Bun, Node built-ins,
subprocesses, files, or the network.
## Imports and source validation [#imports-and-source-validation]
V1 supports local relative imports that remain inside the plugin directory, plus
Bun and Node built-ins such as `bun` and `node:fs`. Package imports, out-of-root
paths, and symlinks are rejected. Termy never installs packages automatically or
runs plugin build hooks.
GitHub installation downloads regular files without cloning the repository,
running scripts, evaluating code, or installing dependencies. Code executes only
when the plugin is loaded.
## Workers, timeouts, and processes [#workers-timeouts-and-processes]
Termy uses one external Bun host and isolates each plugin in its own Worker.
Lifecycle subscribers keep the host warm; eventless plugins restart it on demand
after idle suspension. Commands, events, and native views use execution timeouts;
a crash or timeout is contained to the failed Worker.
Subprocesses started by plugin code can outlive that Worker, so the plugin must stop
its own child processes when cancellation matters. If the shared host transport
exits, Termy rebuilds it and reloads Workers on the next refresh without taking down
the app.
User cancellation aborts `context.signal`; long-running handlers should observe it
and can report a bounded status message and percentage through `context.progress`.
Persist durable state through `context.storage` or managed files. Module globals
may reset when an eventless host sleeps.
The default handler timeout is 10 seconds. A command or view may set `timeoutMs`
from 100 to 30,000 milliseconds.
## Runtime limits [#runtime-limits]
| Area | Limit |
| ----------------- | --------------------------------------------------------------- |
| Installed plugins | 32 |
| Source tree | 4,096 files and 16 MiB total |
| Definitions | 512 commands, 64 settings, and 32 views per plugin |
| Command input | 16 inputs per command and 128 options per select or pick result |
| Returned actions | 32 per invocation |
| View params | 64 KiB JSON object |
| Structured launch | 128 args and 4,096 characters per program/arg |
Small persistent storage has separate limits documented under
[Lifecycle and storage](/docs/using-termy/plugins/lifecycle-storage#store-json-values).
## Native UI boundaries [#native-ui-boundaries]
JSX is data, not renderer access. Termy accepts only the documented
[native components](/docs/using-termy/plugins/native-ui#component-allowlist),
semantic props, and string or boolean control values. It rejects unknown nodes and
props before GPUI renders anything.
A plugin may declare at most 32 views. Each rendered document is limited to 256
nodes, 16 levels of nesting, 64 children per node, and 64 value-bearing controls.
Text input values are limited to 4,096 characters. View params must be a JSON object
and are limited to 64 KiB.
## Bundles and reloads [#bundles-and-reloads]
Termy checks plugin content whenever the command palette opens. It fingerprints the
manifest and source tree, then bundles each changed plugin once with
`Bun.build({ target: "bun" })` into
`plugins/.termy-cache/bundles//.mjs`. Local relative imports are
included in both the content hash and bundle; unchanged plugins reuse their cached
module.
Saving a manifest or source file replaces the changed Worker and refreshes its
commands without restarting Termy. Disabling a plugin keeps its managed files and
storage but removes its commands on the next refresh. Uninstalling removes the
managed copy, storage, and cache while leaving the original source folder untouched.
Actions and native-view updates from requests already running when a plugin
changes, is disabled, or is removed are rejected.
Termy resolves Bun from `TERMY_BUN_PATH`, beside the Termy executable, `PATH`,
Bun's default user install, Homebrew, and `/usr/local/bin`. Set `TERMY_BUN_PATH` to
an absolute executable path when automatic resolution misses it.
V1 supports command-palette commands, lifecycle events, plugin keybindings, and
allowlisted native JSX views. It does not support arbitrary React, HTML, or GPUI,
build hooks, package imports, or automatic package installation.
# Search (/docs/using-termy/search)
| Action | Default |
| ------------------------------ | ------------------- |
| `open_search` | `secondary-f` |
| `search_next` | `secondary-g` |
| `search_previous` | `secondary-shift-g` |
| `close_search` | bind manually |
| `toggle_search_case_sensitive` | bind manually |
| `toggle_search_regex` | bind manually |
Deeper history: raise `scrollback_history` in [Terminal behavior](/docs/customize/terminal-behavior).
# Splits and panes (/docs/using-termy/splits-panes)
## Split [#split]
| Action | Default |
| ----------------------- | ------------------- |
| `split_pane_vertical` | `secondary-d` |
| `split_pane_horizontal` | `secondary-shift-d` |
| `close_pane` | bind manually |
## Focus and resize [#focus-and-resize]
| Action | Default |
| ------------------- | ----------------------------- |
| `focus_pane_next` | `secondary-o` |
| `focus_pane_` | `secondary-alt-` |
| `resize_pane_` | `secondary-alt-shift-` |
| `toggle_pane_zoom` | `secondary-enter` |
## Inactive panes [#inactive-panes]
`pane_focus_effect`: `off` · `soft_spotlight` (default) · `cinematic` · `minimal`
Intensity: `pane_focus_strength` (`0.0` – `2.0`)
# SSH hosts (/docs/using-termy/ssh-hosts)
Open **Settings → SSH** to add, edit, or delete a host. Each host keeps a
display name, hostname, port, username, and either an SSH key path or password
authentication.
After saving a host, open the new-tab menu and choose it under **SSH Hosts**.
Termy launches your system OpenSSH client in a normal terminal tab, so
connection and authentication messages remain visible there.
## Credentials [#credentials]
Leave the password or private-key passphrase blank to enter it interactively
when OpenSSH asks. If you enter one in Settings, Termy stores it in the system
Keychain and never in `ssh_hosts.json`, process arguments, logs, or the shell
command line.
Termy stores only an identity-file path, not a copy of your private key.
## Host verification [#host-verification]
OpenSSH still manages host fingerprints and `known_hosts`. Termy does not
disable strict host-key checks. Review the fingerprint before accepting a new
host, and treat a changed-host-key warning as a security error.
# Tabs (/docs/using-termy/tabs)
| Action | Default |
| -------------------------------------- | ----------------------------- |
| `new_tab` | `secondary-t` |
| `close_pane_or_tab` | `secondary-w` |
| `switch_to_tab_1` … `switch_to_tab_9` | `secondary-1` … `secondary-9` |
| `switch_tab_left` / `switch_tab_right` | bind manually |
| `move_tab_left` / `move_tab_right` | bind manually |
| `rename_tab` | bind manually |
Tab titles: [Tabs and titlebar](/docs/customize/tabs-titlebar).
## Open a tab from your file manager [#open-a-tab-from-your-file-manager]
On macOS, go to **Settings → General → System integration → Default terminal**
and click **Set as default**. This sets Termy as the system's terminal handler.
Run the installed `Termy.app` to use this option. Apps with their own external
terminal preference must be configured separately. Finder's **Open new Termy tab
here** service remains available; opening a file in Termy opens its containing
folder without running the file.
After Termy is installed, file managers expose **Open new Termy tab here**:
On macOS, launch Termy once after updating to refresh the Finder service.
The action finds the installed app by its identifier, so it continues to work
after the app bundle is replaced or moved.
| Platform | Where it appears |
| -------- | ---------------------------------------------------------------------------- |
| Windows | Explorer folder, folder background, and drive context menus |
| macOS | Finder Services, and Open With on a folder |
| Linux | Nautilus/Caja Scripts, Nemo actions, KDE/Dolphin folder menus, and Open With |
If Termy is already running, the action adds a tab in that folder instead of starting a second window. You can also do this from a shell:
```bash
termy /path/to/project
termy --working-directory /path/to/project
```
# Tasks and layouts (/docs/using-termy/tasks-layouts)
Pattern: `task..`
| Field | Required | Description |
| ------------- | -------- | ------------------------------------------ |
| `command` | yes | Shell command for a new tab |
| `layout` | no | Only show when this saved layout is active |
| `working_dir` | no | Working directory |
```txt
task.build.command = cargo build
task.build.working_dir = crates/cli
task.dev_server.layout = dashboard
task.dev_server.command = cargo run
```
Run from the palette (**Run Task**) or bind `run_task` to a key.
# Tmux sessions (/docs/using-termy/tmux-sessions)
Requires `tmux` ≥ 3.3 on `PATH` (macOS / Linux), or reachable through a command prefix (Windows / remote).
```txt
tmux_enabled = true
tmux_persistence = true
tmux_exclusive = false
tmux_binary = tmux
tmux_command_prefix = none
tmux_show_active_pane_border = false
```
| Key | Effect |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tmux_enabled` | New tabs use tmux instead of a raw PTY |
| `tmux_persistence` | Sessions survive app restarts |
| `tmux_exclusive` | Stay in tmux control mode; restart it instead of falling back to a classic terminal when control mode exits (for example after Ctrl+D closes the last pane) |
| `tmux_binary` | Path or name of the tmux binary |
| `tmux_command_prefix` | Command that tmux runs through, e.g. `wsl.exe -e` or `ssh myhost` |
Open **Manage tmux Sessions** from the command palette, or bind `manage_tmux_sessions`.
Termy creates one session per window (`termy-`). Splits map to real tmux panes.
## Windows (WSL) and remote hosts [#windows-wsl-and-remote-hosts]
tmux cannot run natively on Windows, but Termy can drive a tmux that lives inside WSL
or on a remote machine. Set `tmux_command_prefix` to the command that reaches it:
```txt
# tmux inside WSL (default distro)
tmux_enabled = true
tmux_command_prefix = wsl.exe -e
# tmux on a remote host over SSH (needs key-based auth)
tmux_enabled = true
tmux_command_prefix = ssh myhost
```
Termy then launches `wsl.exe -e tmux ...` (or `ssh myhost tmux ...`) for the control
channel and all session management commands. The prefix is split on whitespace, so it
cannot contain arguments with embedded spaces. On Windows, tmux mode stays off until a
prefix is configured. New sessions start in the default directory of the WSL distro or
remote shell — the host working directory is not forwarded.
## Troubleshooting [#troubleshooting]
| Symptom | Fix |
| ---------------------------------------- | ---------------------------------------------------------------------- |
| Blank tabs | Set `tmux_binary` to an absolute path |
| `protocol version mismatch` | Point `tmux_binary` at the tmux version you intend to use |
| Sessions lost on quit | Set `tmux_persistence = true` |
| Control mode exits to a classic terminal | Set `tmux_exclusive = true` to restart control mode instead |
| Double active-pane border | Set `tmux_show_active_pane_border = false` |
| tmux mode ignored on Windows | Set `tmux_command_prefix` (e.g. `wsl.exe -e`) |
| `ssh` prefix hangs at startup | Use key-based auth; the control channel cannot answer password prompts |
]