mitchellh.com/go-libghostty

10 min read Original article ↗

Configure Feed

Select the types of activity you want to include in your feed.

Configure Feed

Select the types of activity you want to include in your feed.

62 1 0

Clone this repository

https://tangled.org/mitchellh.com/go-libghostty https://tangled.org/did:plc:s6vfrnebhxb6gqrmlspmbmr7

git@knot.mitchellh.com:mitchellh.com/go-libghostty git@knot.mitchellh.com:did:plc:s6vfrnebhxb6gqrmlspmbmr7

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz Download .zip

Commits 62

Advance the pinned Ghostty revision to upstream main. The previous
bindings referenced removed mode getter and setter functions and did not
expose the new terminal configuration and effect APIs.

Use the generic terminal data and option paths for modes, add default
mode configuration, title reporting, terminfo names, and unknown-sequence
callbacks, and align snapshot documentation with the revised format.

Add Go benchmarks covering empty, ASCII, Unicode, styled, and 10K-line history states across allocating and reused-buffer encoding plus full and READY-prefix decoding.

Return C out parameters through small value structs to avoid cgo-forced heap escapes. Pin immutable byte input for zero-copy decoding, and retain an explicit copying constructor for callers that need mutation isolation.

Update the pinned Ghostty revision to main and expose the new snapshot,
stream continuation, reader, and writer APIs through idiomatic Go wrappers.
The bindings previously had no way to persist and incrementally restore
complete terminal state.

Preserve callback errors across cgo, support incremental history restoration,
and adapt Kitty graphics metadata to pending payloads. Add coverage for
buffer, callback, lifecycle, and validation paths.

Update the pinned Ghostty revision to the latest main commit. The new
revision introduces desktop notification and progress report effects.

Expose both effects through creation options and live setters. Copy
borrowed notification strings into Go memory, translate progress states,
and validate sized callback structs before access.

Cover OSC 9, OSC 777, and OSC 9;4 callback behavior, clearing,
split writes, and all progress states.

Update the pinned Ghostty revision and Zig overlay for the upstream
Zig 0.16 requirement. Adapt terminal construction and configuration
to the new scrollback limit ABI and temporary-file policy.

Bind the remaining native OSC, SGR, color, Unicode, formatter,
render, reporting, compression, and metadata APIs. Add typed accessors
and tests, leaving only WebAssembly-only allocation helpers unbound.

ghostty-org/ghostty#13182

Update the pinned libghostty revision to include the clipboard write
API and expose its protocol-neutral descriptors through Go callbacks.

Clipboard effects copy borrowed C descriptors into Go-owned MIME
representations, support constructor and live registration, and return
the upstream result enum. Cover OSC 52 and iTerm2 writes, clears,
ignored reads, binary payloads, and callback removal.

Update the pinned ghostty source to pick up the new render-cell
GRAPHEMES_UTF8 data getter and GhosttyBuffer type.

AppendGraphemes no longer carries a binding-local implementation of
grapheme UTF-8 encoding. It now calls the upstream getter through a
small C-stack wrapper, which keeps the GhosttyBuffer out-parameter off
the Go heap while letting libghostty own the grapheme extraction and
encoding behavior.

This preserves the allocation-avoiding Go API while reducing the
binding-side code to a thin adapter over the upstream API.

AppendGraphemes previously queried grapheme length into a Go local
and used a Go stack scratch buffer for the common short-grapheme case.
Both values were passed through cgo, which forced them to escape to the
heap on every non-empty cell.

Move the hot path into a small C helper that keeps the length and
codepoint scratch storage on the C side, encodes the grapheme cluster
as UTF-8, and returns the result metadata by value. The Go wrapper now
only passes the caller byte buffer through cgo and grows that buffer
when the helper reports GHOSTTY_OUT_OF_SPACE.

This preserves the existing AppendGraphemes API while avoiding the
extra per-cell heap allocation that made the direct UTF-8 path slower
than caller-reused GraphemesInto scratch.

RenderCellStyle used pointer fields for foreground and background colors
that pointed back into storage owned by the same struct. Reusing a single
style value in a per-cell render loop still forced the destination to
escape, producing heap allocations for heavily styled output.

Store resolved colors as values with explicit presence booleans instead
and make StyleInto fill that shape directly. The binding helper now
returns its snapshot by value rather than writing through a Go pointer, so
cgo does not force a temporary snapshot allocation on every call.

Add RenderCellStyle plus StyleInto and ResolvedStyleInto for row-cell
rendering. The new API lets callers reuse one destination value while
reading resolved foreground, background, has-styling, and common text
style flags.

Previously renderers had to call Style, FgColor, BgColor, and
HasStyling separately. That allocated Go wrapper values and crossed cgo
multiple times per styled cell. The new binding-side helper batches the
existing libghostty queries into one cgo transition and treats absent
foreground or background colors as nil fields instead of errors.

RenderStateRowCells.Graphemes previously allocated a fresh codepoint
slice for every text cell. That made plaintext renderers pay allocation
and conversion costs even when most cells contained a single ASCII
codepoint.

Add GraphemesInto so callers can reuse their own codepoint scratch
buffer, and add AppendGraphemes for renderers that want UTF-8 bytes
directly. The existing Graphemes API now delegates through the reusable
path while preserving its nil result for empty cells.

Bind the libghostty selection APIs for deriving word, line,
select-all, and command-output selections from terminal grid refs. The
new Go entry points use option structs so future C option fields can be
added without changing method signatures.

Add one-shot selection formatting helpers and selection inspection
operations for adjustment, ordering, containment, and equality. Render
state rows can now expose their row-local selected cell range, matching
the upstream row selection data API.

Expose the remaining partially-bound C constants listed in TODO.md.

Build info now includes version pre-release metadata, modes expose
DECBKM, and terminal bindings expose active selection data plus APC
buffer and selection setters. The terminal selection accessor maps
GHOSTTY_NO_VALUE to a nil selection to match existing optional getter
patterns.

Remove the completed constants from TODO.md.

Add human-friendly text serialization to the input enum types via
the standard encoding.TextMarshaler and encoding.TextUnmarshaler
interfaces. This makes them work transparently with encoding/json,
TOML, YAML, XML, and flag.TextVar.

Each type gains:

- String() string returning the canonical snake_case name
(e.g. "key_a", "arrow_down", "shift+ctrl", "middle",
"gained").
- MarshalText() ([]byte, error) returning the same bytes as
String. Mods(0) marshals to an empty byte slice.
- UnmarshalText([]byte) error mutating the receiver.
- A top-level ParseKey, ParseMods, ParseMouseButton, and
ParseFocusEvent function as an ergonomic alternative to the
pointer-receiver UnmarshalText.

The Key and MouseButton names follow the upstream Zig source
naming. Mods is rendered as a stable "+"-joined list of
individual flag names; UnmarshalText accepts either "+" or ","
as separators and supports the upstream aliases (cmd/command for
super, opt/option for alt, control for ctrl).

add String/FromString for Key, Mods, MouseButton, FocusEvent

Add human-friendly string conversion to the input enum types so they
can be serialized to and parsed from text formats like JSON or
configuration files.

Each type gains a String() method returning the canonical
snake_case name (e.g. "key_a", "arrow_down", "shift+ctrl",
"middle", "gained") and a pointer-receiver FromString method
that mutates the receiver. A top-level NewKeyFromString,
NewModsFromString, NewMouseButtonFromString, and
NewFocusEventFromString constructor is also provided as a more
ergonomic alternative.

Extensive notes on concurrency

Document and test cross-compilation

One of the common stigmas of cgo in Go projects is that it makes
cross-compilation overly difficult. Its more difficult than pure Go projects,
to be sure, but with the right shape of libraries and build scripts, it
can be made to work well. libghostty is a good example of this.

libghostty only depends on libc and the Zig compiler (tool, not language)
as a drop-in replacement for c/c++ compilation means we can easily
cross-compile!

This commit adds documentation, tests, and examples on how to do this.

terminal: fix checkptr failure under -race for cgo.Handle userdata

Converting a cgo.Handle (uintptr) directly to unsafe.Pointer in
NewTerminal triggered a checkptr "bad pointer value" panic when
running tests with -race. The handle is an opaque integer, not a
real Go pointer, so checkptr incorrectly rejects it.

Extract the conversion into a small handleToPointer helper
annotated with //go:nocheckptr to suppress the false positive.

terminal: pass *Terminal as first parameter to effect callbacks

Previously, effect callback function types (WritePtyFn, BellFn,
TitleChangedFn, etc.) did not receive the terminal that triggered
them. This forced callers using the functional option pattern to
pre-declare a var and use a split assignment so closures could
capture it, as seen in the effects example.

All effect callback types now take *Terminal as their first
parameter. The C trampolines already recovered the *Terminal from
userdata, so they now simply forward it to the Go handler. This
lets callers define callbacks inline in NewTerminal without any
pre-declaration workaround.

encoding: add key, mouse, and focus encoding bindings

Add Go bindings for the upstream key, mouse, and focus encoding
APIs from key/, mouse/, and focus.h headers.

Key encoding provides KeyEvent (action, key, mods, UTF-8 text,
composing state, unshifted codepoint) and KeyEncoder with setopt
methods for cursor key application mode, Kitty keyboard protocol
flags, macOS option-as-alt, and other terminal modes. The encoder
also supports syncing options from a Terminal instance.

Mouse encoding provides MouseEvent (action, button, mods, position)
and MouseEncoder with tracking mode, format, size geometry, and
terminal state syncing. The encoder supports X10, UTF-8, SGR, URxvt,
and SGR-Pixels protocols.

Focus encoding exposes FocusEncode as a standalone function producing
CSI I (gained) or CSI O (lost) sequences.

KittyKeyFlags moves from terminal.go to key_encoder.go since it is
part of the key encoding API and used by both the encoder and the
terminal.

Update libghostty to not depend on libc++

README.md

Go Libghostty Bindings#

Go bindings for libghostty-vt.

This project uses cgo but libghostty-vt only depends on libc, so it is very easy to static link and very easy to cross-compile. The bindings default to static linking for this reason.

WARNING

I'm not promising any API stability yet. This is a new project and the API may change as necessary. The underlying functionality is very stable, but the Go API is still being designed.

Example#

package main

import (
 "fmt"
 "log"

 "go.mitchellh.com/libghostty"
)

func main() {
 term, err := libghostty.NewTerminal(libghostty.WithSize(80, 24))
 if err != nil {
  log.Fatal(err)
 }
 defer term.Close()

 // Feed VT data — bold green "world", then plain text.
 fmt.Fprintf(term, "Hello, \033[1;32mworld\033[0m!\r\n")

 // Format the terminal contents as plain text.
 f, err := libghostty.NewFormatter(term,
  libghostty.WithFormatterFormat(libghostty.FormatterFormatPlain),
  libghostty.WithFormatterTrim(true),
 )
 if err != nil {
  log.Fatal(err)
 }
 defer f.Close()

 output, _ := f.FormatString()
 fmt.Println(output) // Hello, world!
}

More examples are in the examples/ directory.

Usage#

Add the module to your Go project:

go get go.mitchellh.com/libghostty

This is a cgo package that links libghostty-vt via pkg-config. By default it links statically. Before building your project, you need the library installed. Either install it system-wide or set PKG_CONFIG_PATH to point to a local checkout:

export PKG_CONFIG_PATH=/path/to/libghostty-vt/share/pkgconfig

To link dynamically instead (requires the shared library at runtime, so you'll also need to set the library path):

go build -tags dynamic

See the Ghostty docs for building libghostty-vt from source.

Cross-Compilation#

Because libghostty-vt only depends on libc, cross-compilation is straightforward using Zig as the C compiler. Zig is already required to build libghostty-vt, so no extra tooling is needed. You don't need to write any Zig code, we're just using Zig as a C/C++ compiler.

First, build libghostty-vt for your target (from the ghostty source tree):

zig build -Demit-lib-vt -Dtarget=x86_64-linux-gnu --prefix /tmp/ghostty-linux-amd64

Then cross-compile your Go project with zig cc:

CGO_ENABLED=1 \
GOOS=linux GOARCH=amd64 \
CC="zig cc -target x86_64-linux-gnu" \
CXX="zig c++ -target x86_64-linux-gnu" \
CGO_CFLAGS="-I/tmp/ghostty-linux-amd64/include -DGHOSTTY_STATIC" \
CGO_LDFLAGS="-L/tmp/ghostty-linux-amd64/lib -lghostty-vt" \
go build ./...

Supported targets include x86_64-linux-gnu, aarch64-linux-gnu, x86_64-macos, aarch64-macos, x86_64-windows-gnu, and aarch64-windows-gnu.

If you are using ghostty's CMake integration via FetchContent, the ghostty_vt_add_target() function handles the zig build for you:

FetchContent_MakeAvailable(ghostty)
ghostty_vt_add_target(NAME linux-amd64 ZIG_TARGET x86_64-linux-gnu)

See the ghostty CMakeLists.txt for full documentation of ghostty_vt_add_target().

Development#

CMake fetches and builds libghostty-vt automatically. CMake is only required and used for development of this module. For actual downstream usage, you can get libghostty-vt available however you like (e.g. system package, local checkout, etc.).

You need Zig and CMake on your PATH.

make build
make test

# If in a Nix dev shell:
go build
go test

If you use the Nix dev shell (nix develop), go build and go test work directly — the shell configures all paths automatically.