Terminal emulator library for the BEAM.
Wraps libghostty-vt — the virtual terminal extracted from Ghostty. SIMD-optimized VT parsing, full Unicode, 24-bit color, scrollback with text reflow. Terminals are GenServers.
def deps do
[{:ghostty, "~> 0.2"}]
endPrecompiled terminal and PTY NIF binaries are downloaded automatically for x86_64 Linux, aarch64 Linux, and aarch64 macOS.
{:ok, term} = Ghostty.Terminal.start_link(cols: 120, rows: 40)
Ghostty.Terminal.write(term, "Hello, \e[1;32mworld\e[0m!\r\n")
{:ok, text} = Ghostty.Terminal.snapshot(term)
# => "Hello, world!"
{:ok, html} = Ghostty.Terminal.snapshot(term, :html)
# => HTML with inline color styles
{col, row} = Ghostty.Terminal.cursor(term)
# => {0, 1}children = [
{Ghostty.Terminal, name: :console, cols: 120, rows: 40},
{Ghostty.Terminal, name: :logs, id: :logs, cols: 200, rows: 100,
max_scrollback: 100_000}
]
Supervisor.start_link(children, strategy: :one_for_one)
Ghostty.Terminal.write(:console, data)
{:ok, html} = Ghostty.Terminal.snapshot(:console, :html)Effect messages are sent to the process that called start_link/1:
| Message | Trigger |
|---|---|
{:pty_write, binary} |
Query responses to write back to the PTY |
:bell |
BEL character (\a) |
:title_changed |
Title change via OSC 2 |
Ghostty.PTY sends messages to the process that called start_link/1:
| Message | Trigger |
|---|---|
{:data, binary} |
Subprocess stdout/stderr |
{:exit, status} |
Subprocess exit |
Ghostty.Terminal.write(term, String.duplicate("x", 120) <> "\r\n")
Ghostty.Terminal.resize(term, 40, 24)
{:ok, text} = Ghostty.Terminal.snapshot(term)
# Long line is now wrapped across 3 lines{:ok, term} = Ghostty.Terminal.start_link(cols: 200, rows: 500)
Ghostty.Terminal.write(term, ansi_output)
{:ok, plain} = Ghostty.Terminal.snapshot(term)Read the screen as a grid of cells for building custom renderers (LiveView, Scenic, etc.):
rows = Ghostty.Terminal.cells(term)
for row <- rows do
for {grapheme, fg, bg, flags} <- row do
if Ghostty.Terminal.Cell.bold?({grapheme, fg, bg, flags}) do
IO.write(IO.ANSI.bright())
end
IO.write(grapheme)
end
IO.puts("")
endRun interactive programs in a real pseudo-terminal:
{:ok, term} = Ghostty.Terminal.start_link(cols: 80, rows: 24)
{:ok, pty} = Ghostty.PTY.start_link(cmd: "/bin/bash", cols: 80, rows: 24)
# PTY output arrives as messages
receive do
{:data, data} -> Ghostty.Terminal.write(term, data)
end
# Send keyboard input
Ghostty.PTY.write(pty, "ls --color\n")
# Resize the PTY (reflows in the terminal too)
Ghostty.PTY.resize(pty, 120, 40)
Ghostty.Terminal.resize(term, 120, 40)Install the LiveView hook into a Phoenix app with:
mix igniter.install ghosttyThen drop in a terminal with Ghostty.LiveTerminal.Component — it handles
keyboard events internally so your LiveView only manages the terminal
and PTY lifecycle:
defmodule MyAppWeb.TerminalLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, term} = Ghostty.Terminal.start_link(cols: 80, rows: 24)
{:ok, assign(socket, term: term, pty: nil)}
end
def render(assigns) do
~H"""
<.live_component
module={Ghostty.LiveTerminal.Component}
id="term"
term={@term}
pty={@pty}
fit={true}
autofocus={true}
/>
"""
end
def handle_info({:terminal_ready, "term", cols, rows}, socket) do
{:ok, pty} = Ghostty.PTY.start_link(cmd: "/bin/bash", cols: cols, rows: rows)
{:noreply, assign(socket, pty: pty)}
end
def handle_info({:data, data}, socket) do
Ghostty.Terminal.write(socket.assigns.term, data)
send_update(Ghostty.LiveTerminal.Component, id: "term", refresh: true)
{:noreply, socket}
end
def handle_info({:exit, _status}, socket), do: {:noreply, socket}
end| Assign | Default | Description |
|---|---|---|
:term |
required | Ghostty.Terminal pid |
:pty |
nil |
Ghostty.PTY pid; key input is written here when present |
:fit |
false |
Auto-fit terminal size to the rendered container |
:autofocus |
false |
Focus the hidden terminal input on mount |
:class |
"" |
CSS class for the container div |
When fit is enabled, the hook measures the container and sends a "ready"
event with the computed cols and rows. The component resizes the terminal
and notifies the parent with {:terminal_ready, id, cols, rows} —
use this to defer PTY startup until the real container size is known.
For full control, use the helpers directly:
Ghostty.LiveTerminal.key_event_from_params(params) # parse browser key event
Ghostty.LiveTerminal.handle_key(term, params) # parse + encode
Ghostty.LiveTerminal.push_render(socket, "term-id", term) # push cells to clientmix igniter.install ghostty vendors ghostty.js into your app assets and wires
GhosttyTerminal into assets/js/app.js automatically.
TypeScript source lives in priv/ts/ and is bundled at compile time via
OXC — no Node.js or Bun required for end users.
Contributors can run TypeScript quality checks:
bun install && bun run lint && bun run format:checkSee examples/live_terminal/
for a complete runnable app with Playwright browser tests. It includes a control
panel with preset commands, fit/banner toggles, and sets TERM=xterm-256color
for colorized shell output.
See the examples/ directory:
| Example | What it does |
|---|---|
hello.exs |
Write with colors, read back plain + HTML |
ansi_stripper.exs |
Pipe stdin, strip ANSI codes |
html_recorder.exs |
Capture command output as styled HTML |
progress_bar.exs |
\r overwrites → final screen state only |
reflow.exs |
Text reflow on resize |
supervised.exs |
Named terminals in a supervision tree |
diff.exs |
Terminal-aware Myers diff |
expect.exs |
Expect-like automation with pattern matching |
pool.exs |
Reusable terminal pool for concurrent processing |
live_terminal/ |
Phoenix LiveView terminal renderer with Playwright browser tests |
Zig 0.15+ is only required for source builds.
git clone https://github.com/dannote/ghostty_ex
cd ghostty_ex
mix deps.get
mix ghostty.setup # clones Ghostty, builds libghostty-vt
GHOSTTY_BUILD=1 mix testTo use an existing Ghostty checkout:
GHOSTTY_SOURCE_DIR=~/code/ghostty mix ghostty.setupXcode 26.4 breaks Zig builds on macOS. Downgrade to Xcode 26.3 CLT:
# Download from https://developer.apple.com/download/all/?q=Command+Line+Tools+for+Xcode+26.3
sudo xcode-select --switch /Library/Developer/CommandLineToolsSee ziglang/zig#31658 for details.
MIT