A GPUI app adds the phux-gpui view. Another Rust toolkit uses phux-client-runtime and paints its draw::DrawList. A Swift, C or other native app links phux-client-ffi and paints phux_client_terminal_draw_list. All three run phux's libghostty replica in your process and share one set of paint rules (ADR-0189). None of these crates is on crates.io yet; depend on them by path or git.
Pick a route
| Your app | Depend on | You write |
|---|---|---|
| GPUI (gpui-fast 0.1.9) | phux-gpui | Wake GPUI from the runtime and place the view. |
| Another Rust UI (egui, iced, Slint, a custom renderer) | phux-client-runtime | Map cells to pixels and draw the draw list. |
| Swift, Objective-C, C, C++, Zig | phux-client-ffi (C ABI) | Connect, poll on wake, paint the C draw list. |
| A phone app | phux-client-ffi (UniFFI) | See phones. |
Every route starts the same way: open a client, attach a session, and create a view of one terminal. phux does the rest of the terminal: the replica, reconnect, acknowledged input, history, selection and search.
GPUI
clients/desktop/native/phux-gpui
is the terminal view the phux desktop is built on. examples/embed.rs is a
whole app in one file:
Use an explicit disposable server socket and a non-production profile, not
an inherited pane socket. From the repository root, first run
just desktop-source to materialize the workspace’s pinned GPUI sources.
The example opens a window; run it only in an authorized graphical session.
That server must already have a session named embed-test containing a
terminal; substitute its actual session name in the final argument if different.
# /tmp/phux-embed-test.sock must belong to your disposable test server.
unset "${!PHUX_WS_@}" # Bash: clear inherited WebSocket credentials/TLS settings
export PHUX_PROFILE=embed-test
export PHUX_SOCKET=/tmp/phux-embed-test.sock
cargo run --locked --manifest-path clients/desktop/native/Cargo.toml \
-p phux-gpui --example embed -- "$PHUX_SOCKET" embed-test
Terminal::new does not own or destroy its runtime ViewId. When closing
a view on a shared Client, the host calls client.destroy_view(view);
dropping the GPUI entity alone leaves that view registered. Sibling views
and their shared client remain alive.
It builds on the published gpui-fast. Its patched-gpui feature adds retained
glyph sprites, drawable-presented receipts, macOS key repeat and exact image-source
sampling. Phux Desktop enables it; the contract below distinguishes the product
guarantee from the unpatched compatibility lane.
Frames, wakes, and input
One listener and one event drain exist per runtime client. Drain, then invalidate affected terminals. A removed slot clears stale presentation even when its generation equals the cached number. Never publish a cached frame as evidence of recovery.
Raw-send success is a local queue result, not delivery. Unknown delivery is
terminal-wide. In Phux Desktop (the patched-gpui product build),
acknowledge_projection runs only from the drawable-presented callback on
a visible, unclipped terminal. Paint and on_next_frame do not clear its
fence; a late callback must not clear a newer fence. Unpatched embedders
use a weaker next-frame-start receipt, not presentation proof; that lane
does not meet this product guarantee. See the
feature qualification.
Any Rust toolkit
Enable the runtime’s engine feature. This sketch separates connection from
UI callbacks; options.control.attach must select the intended disposable
server session. Keep the returned client/view alive between callbacks.
use phux_client_runtime::draw::{DrawList, Theme, Visibility};
use phux_client_runtime::publication::Rgb;
use phux_client_runtime::runtime::RuntimeError;
use phux_client_runtime::{Client, ClientOptions, Runtime, Target, ViewId};
fn connect(socket: std::path::PathBuf, options: ClientOptions) -> Result<Client, RuntimeError> {
Runtime::connect(Target::uds(socket), options)
}
// After attached/readiness, call client.create_view(&terminal_id)? once.
// On UI invalidation, acquire the latest immutable frame:
fn draw(client: &Client, view: ViewId, theme: &Theme<Rgb>) -> Option<DrawList<Rgb>> {
let frame = client.acquire_view(view)?;
Some(DrawList::build(&frame, theme, Visibility::default()))
}
// On closing that view, call client.destroy_view(view)? once.
Theme<C> is generic over your colour type: give it a fn(Rgb) -> C and
optional theme colours, and the list arrives in your colours. Send input
through Client::with_control the way phux-gpui’s input module does, and
resize the terminal from the view that owns its size.
Inline images
Image capabilities default to empty: text-only and ABI consumers must not
advertise protocols they cannot paint. A Rust renderer that paints Kitty images
adds the matching phux-protocol dependency and opts in before connecting:
options.control.image_protocols = phux_protocol::caps::ImageProtocolSet::with(&[
phux_protocol::caps::ImageProtocol::KittyGraphics,
]);
phux-gpui paints the image planes automatically; its embedding example opts in.
Other renderers consume GridFrame.images alongside the text draw list. Each
placement borrows immutable, shared RGBA pixels through an Arc<ImageData>,
with signed cell-space geometry, pixel-space source/crop rectangles and a signed
z-order. Apply sample_source to texture filtering independently of the visible
source slice, so Unicode placeholder cells do not introduce sampling seams.
Follow the painter layering contract.
Report actual cell pixels through Client::resize_terminal_with_cell so native
image sizes and offsets match the renderer.
Kitty consumers negotiate synthesized Raw/StateSync bootstrap: native GHOSTSNP does not include the engine’s image/placement registry. Synthesized replay includes unplaced payloads and virtual prototypes for later live placements. External file/shared-memory media stay disabled. See desktop compatibility limits. The C/Swift draw-list ABI is still text-only; no image fields were added to it.
C and Swift
include/phux/client.h
is the C ABI. For product connections use phux_client_connect: the runtime
owns socket, HELLO, framing and reconnect. Post its wake to your event loop,
then call phux_client_poll on the owning thread. Never feed frames, queue
a second HELLO or drain outgoing bytes on a connected handle. Fully
initialize the thread-safe wake context before phux_client_connect: a
registration wake may call it synchronously before the client is returned.
It must only post asynchronously to the event loop, never call the ABI
inline or wait for a mutex held across connect. Start polling only after
connect returns the handle; keep the context valid until free completes.
phux_client_new plus feed_frame/outgoing_* is the separate embedded
lane for offline use, harnesses and fault injection: the host then owns
dialing, framing, reconnect and bounds. The lanes are mutually exclusive.
Every root release attaches the
PhuxFFI xcframework (macOS and iOS, header included); on other platforms
build the static library with just ffi-release. Call
phux_client_terminal_draw_list(client, &terminal, &options, &list) after
output arrives and paint the records it lends, which stay valid until your
next mutable call on the client. The low-level phux_client_terminal_grid
and phux_client_terminal_grid_metadata are still there for a renderer that
wants every cell.
Runtime consumer contract
Native Rust consumers normally use the runtime’s Fed delivery: the
driver feeds the control plane, and the UI consumes events and immutable
GridFrame publications. A binding with owning-thread per-frame catalog
state (the C ABI) uses Queued delivery: the driver retains inbound bytes,
and the owner drains/polls and decodes them. Do not build another dialer to
move the decode point. Queues and frame publications are bounded.
Use take_observation to drain events and sample status, topology, failure
and connection_epoch under one lock. An identical terminal id after
reconnect is not the same observation. For queued batches, carry the drained
epoch and check accepts_inbound(epoch) under the same control guard used
to feed each frame; reconnect can retire a batch after it was drained.
Caller-validated recovery reads inventory and validates stable identities
before attaching, never blindly adopts reused handles. Deliberate lease
takeover is spent once, not repeated automatically on reconnect. Geometry
comes from authoritative frames; the designated size-owning view sends
resize intent, while observers do not create resize feedback loops.
A listener is a coalesced wake, not an event payload. Drain then invalidate
only affected views. Driver notifications run on the runtime thread;
registration (set_listener) and explicit wake/close can invoke the
listener synchronously on their caller’s thread. All run outside runtime
locks, not necessarily outside your locks. Initialize context before
registration, make it thread-safe, and only post asynchronously to the UI;
never block or re-enter the client from a callback.
with_control is non-reentrant and must not block or call back into the
client. Dropping the last Client closes the driver, retires callbacks,
waits out callbacks on other threads, and joins the driver (never joins
itself from its own callback). Free callback context only after retirement;
C phux_client_free performs that stop/join.
Immutable Rust grid frames can outlive a later publication. C grid/draw/effect
pointers instead last only until the next mutable call: copy before retaining.
A pointer borrow ending is not delivery acceptance. Ordinary event drains
accept agent publications; leased drains retain their journal allocation
until explicit accept_agent_publication (C: phux_client_effect_clear).
Unaccepted publications replay after replacement once catalog/attach are
ready. This acceptance is also not the drawable-presented receipt that
clears an unknown-input fence; see
GPUI presentation.
The runtime API, publication delivery seam, epoch observations and recovery tests own these contracts. The C header owns ABI versions, open enums, thread and borrow lifetimes; tolerate additive enum members rather than inventing a new binding vocabulary.
The draw list
Positions are cells; you choose the cell size. Paint in this order:
- Fill the grid with
background. - Fill each background run.
- A solid cursor: fill its cells. The glyphs on it already have the cell’s background colour.
- Each text run: one character per column from
col, at your cell width, not the font’s advance, in its face and colour. - Each block cell: fill its regions, given in eighths of the cell.
- Each decoration: underline styles, strikethrough and overline across
start..end. - Any other cursor: a bar, an underline, or a hollow block.
A faint item paints at half alpha. The colours are final: the theme, palette provenance, inverse video and selection are applied, and a hidden or blinked-off cell has no text.
Build requirements
libghostty-vt downloads its prebuilt archive only in ReleaseFast. Set
LIBGHOSTTY_VT_SYS_OPTIMIZE = "ReleaseFast" under [env] in your
.cargo/config.toml, as this repository does, or have Zig on PATH.