❯_termy
Developerlibtermy

Input, search, and navigation

Translate host input, handle bracketed paste and mouse modes, and navigate scrollback with correct coordinates.

Input flows from the host to the application running inside the terminal. Output flows from that application into libtermy. Keeping those directions separate is especially important for display-only sessions.

Keyboard input

Use the current terminal keyboard mode when encoding a key. It can change while an application runs, so do not cache one mode forever or hardcode arrow-key escape sequences.

use termy_core::{Terminal, TerminalKeyEventKind, TermyKeystroke, keystroke_to_input};

pub fn send_key(terminal: &Terminal, key: &TermyKeystroke, kind: TerminalKeyEventKind) {
    if let Some(bytes) = keystroke_to_input(key, kind, terminal.keyboard_mode(), true) {
        terminal.write(&bytes);
    }
}

This helper is for an owned PTY. For display-only sessions, send the returned bytes to the external transport instead. Translate native events into TermyKeystroke: key is the logical key name, key_char is optional produced text, and modifiers describes control, alt, shift, platform, and function keys. Use names such as enter, backspace, left, and right supported by the encoder.

In C, fill TermyFfiKeystroke and call termy_terminal_encode_key or termy_terminal_encode_key_with_options. event_kind is 1 press, 2 repeat, and 3 release. The encoder returns an owned TermyFfiBytes: write or forward it, then call termy_buffer_free. An empty result is valid, for example when a release event does not need reporting.

Use the options variant for macOS Option-as-Alt behavior. Do not bypass the mode-aware encoder with an unconditional ESC prefix; enhanced keyboard modes can require a different representation. Handle application shortcuts and IME composition in the host so committed text is not sent twice.

Paste

Query bracketed_paste_mode() or termy_terminal_bracketed_paste_mode at paste time. If enabled, wrap the approved paste payload in these delimiters:

ESC [ 2 0 0 ~   payload   ESC [ 2 0 1 ~

In byte notation, those are \x1b[200~ and \x1b[201~. If disabled, send the payload without wrappers. Writing bytes does not automatically add these markers. Decide how to handle newlines and control characters at the host's paste-policy layer, and send one consistently ordered paste operation.

If Kitty clipboard paste notifications are enabled, report the available MIME formats as described in events and clipboard. A paste notification does not replace the actual paste payload.

Mouse input

Convert pointer coordinates to zero-based viewport cell coordinates after subtracting padding and host chrome. Clamp or reject positions outside the grid. libtermy handles the terminal protocol's coordinate encoding.

Rust uses encode_mouse_report with terminal.mouse_mode(), event kind, position, and modifiers. C uses termy_terminal_encode_mouse, which reads the current mode internally.

C TermyFfiMouseInput.kindMeaning
1Press
2Release
3Drag
4Move
5, 6Wheel up, down
7, 8Wheel left, right

For press/release/drag, button is 1 left, 2 middle, or 3 right. An empty encoded result means nothing should be sent for that event/mode combination. The host decides whether a gesture should instead select text or scroll history. Mouse reporting and local scrollback navigation are separate actions.

Scrollback and coordinate spaces

scroll_display(delta) / termy_terminal_scroll_display moves the viewport: positive values move into older history, negative values move toward the live bottom. Use scroll_to_bottom to return to live output. Repaint when the method reports a change.

CoordinateMeaning
Frame/dirty-span/mouse rowZero-based row in the visible viewport
display_offsetDistance in rows above the live bottom; zero is live output
history_sizeNumber of retained history rows
Terminal search result rowZero-based row in history plus the live screen, oldest row first
Rust buffer-line visitorsSigned engine-neutral line coordinates; see line_bounds()

For a coherent buffer state, the oldest visible search row is history_size - display_offset. Convert a search result to a viewport row with:

viewport_row = result.row - (history_size - display_offset)

Do that subtraction in a signed type or check bounds first; a result above the viewport must not underflow an unsigned index. Draw a highlight only when the result lies within the visible row range. Re-run searches after output, resize, or history trimming changes the buffer; result rows are not persistent IDs.

Terminal::search and termy_terminal_search search history and the live screen. The default is case-insensitive literal matching. Use search_with_options / termy_terminal_search_with_options for case-sensitive or regular-expression queries.

Results contain the buffer row, inclusive start_col and end_col, and line text. C result lines are owned by the search batch; release the entire result with termy_search_batch_free. Rust's search_shared_with_options shares line strings between matches on the same row, reducing duplication for large results.

Empty queries and invalid regular expressions currently return no matches. If your UI needs to distinguish invalid syntax from zero results, validate the pattern separately. Searches operate on individual buffer rows; do not promise multiline matching across wrapped rows.

The standalone Rust search_frame helpers only search the frame you pass in. That narrower behavior is different from searching a Terminal.

hyperlink_at(row, col) / termy_terminal_hyperlink_at looks up OSC 8 hyperlinks at a viewport position. C reports whether a link was found separately from its status code; release a returned link with termy_hyperlink_free. Rust also exposes link_at for broader detected-link information.

Opening a link is a host action. Validate terminal-provided URL schemes against your allowed schemes before passing a target to the OS. A terminal-provided working directory or link should not automatically execute a command.

Selection gestures, highlight painting, copy behavior, and accessibility are host responsibilities. Preserve wide-cell and wrapping information when assembling selected text; slicing UTF-8 by terminal column is not valid.

On this page