// STRYKE-TERMINAL — HEADLESS TERMINAL EMULATOR FOR STRYKE // VT100 + VT220 + LINUX SCREEN MODEL

stryke package · cdylib libstryke_terminal · loaded in-process via dlopen on first use Terminal · faithful port of pyte 0.8.2 · v0.1.0

Report GitHub Issues
// Color scheme

>_STRYKE-TERMINAL

pyte, one stryke pipe away. A headless VT100 / VT220 / TERM=linux terminal emulator for stryke — a faithful port of pyte. Feed it the raw byte stream a program writes (colors, cursor moves, erases, scroll regions, insert/delete, charsets, titles) and it maintains a full screen model: the character grid, cursor, per-cell colors and attributes, terminal modes, scroll margins, tab stops, and scrollback history. Then read the rendered screen instead of escape-laden bytes. The stryke side is a thin JSON-over-FFI wrapper; the emulator lives in the libstryke_terminal cdylib and sessions persist across calls.

Why this exists

strykelang already ships Tcl/Expect-style PTY automation as built-in functions — pty_spawn, pty_read, pty_send, pty_expect, pty_close, pty_strip_ansi. Those hand you the raw bytes a program writes, with only a dumb pty_strip_ansi to make sense of them — no cursor movement, no erase, no scroll, no insert/delete, no color/attribute state, no scrollback.

stryke-terminal is the missing piece: the screen model those bytes render into. Drive htop / vim / less headlessly through the pty_* builtins, pump their output into a Terminal, and read the exact screen a human would see. It does not re-implement PTY spawning — it consumes pty_read output.

 pty_spawn("htop")  —bytes—>  Terminal::feed  —>  Screen model  —>  Terminal::display
   (strykelang builtin)          (this package)      (grid+cursor+SGR)     (rendered lines)

Install

The emulator is pure Rust — no network, no OS-side build deps beyond a Rust toolchain.

s pkg install -g github.com/MenkeTechnologies/stryke-terminal

Pin a specific release:

s pkg install -g github.com/MenkeTechnologies/stryke-terminal@v0.1.0

This fetches the prebuilt tarball for your host triple (aarch64-apple-darwin, x86_64-apple-darwin, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu), verifies its SHA-256, extracts into ~/.stryke/store/stryke-terminal@<version>/, and registers the cdylib for use Terminal. Verify the whole stack, permission-free, with terminal-test.

Quick start

Parse a stream of terminal output and read the rendered screen:

use Terminal

val $t = Terminal::new(columns => 80, lines => 24)

# feed the kind of bytes a program emits: colored text + a cursor move
Terminal::feed($t, "\x1b[1;32mBUILD OK\x1b[0m")
Terminal::feed($t, "\x1b[3;1Hline three")

for $line (Terminal::display($t)) {
    p $line                       # the rendered screen, one string per row
}

val $cell = Terminal::cell($t, 0, 0)
p "$cell->{data} fg=$cell->{fg} bold=$cell->{bold}"   # B fg=green bold=1

Terminal::destroy($t)

Driving a real program headless

Combine the strykelang pty_* builtins (which spawn and talk to the process) with Terminal (which renders what it draws). This is what "control a full terminal headless" means:

use Terminal

val $h = pty_spawn("vim -u NONE")     # strykelang builtin — allocates a PTY
val $t = Terminal::new(columns => 80, lines => 24)

Terminal::drain($t, $h, 1)            # pump pty_read -> Terminal::feed until idle
pty_send($h, "ihello\x1b")            # type into vim
Terminal::drain($t, $h, 1)

p Terminal::display($t)               # what vim's screen now shows

# some programs query the terminal (DA / DSR / cursor position); forward the
# emulator's replies back to the child so they keep working:
pty_send($h, Terminal::take_input($t))

pty_send($h, ":q!\x0d")
pty_close($h)

Terminal::drain is the core loop of headless terminal automation: spawn, drain, read the screen, act, repeat.

API surface

Every function takes a session id $t from Terminal::new. Full reference in the README.

Terminal::new(columns, lines, kind, history, ratio)create a session → id (kind => "history" for scrollback)
Terminal::feed($t, $data)feed terminal output into the parser
Terminal::feed_bytes($t, $base64)feed raw bytes when output is not valid UTF-8
Terminal::display($t)rendered screen, one string per line
Terminal::cell($t, $x, $y){ data, fg, bg, bold, italics, underscore, strikethrough, reverse, blink }
Terminal::buffer($t)full styled matrix (rows of cells)
Terminal::cursor($t){ x, y, hidden, attrs }
Terminal::dirty($t) / clear_dirty($t)changed line numbers / clear the set
Terminal::title($t) / icon_name($t)OSC-set strings
Terminal::take_input($t)drain device-report replies to forward with pty_send
Terminal::prev_page / next_page / historyscrollback paging + state (history sessions)
Terminal::pump / drain ($t, $pty, $timeout)read one chunk / drain to EOF from a pty_spawn handle
Terminal::dis($data)disassemble an escape string → [name, [args], {kwargs}] events

Direct screen commands (drive the grid without escape sequences) are also exposed: draw, cursor_position, the cursor_* family, erase_in_line / erase_in_display, insert_lines / delete_lines, insert_characters / delete_characters, select_graphic_rendition (alias sgr), set_mode / reset_mode, set_margins, define_charset, and more.

Fidelity

This is a faithful port of pyte 0.8.2 — VT100 / VT220 / TERM=linux. The test suite generates golden fixtures by feeding the reference pyte the same inputs and asserts the Rust port reproduces the display, cursor, styled buffer, titles, device reports, and scrollback byte-for-byte. See the engineering report for coverage.