Select the types of activity you want to include in your feed.
initial commit
This was originally broken up over many commits but I extracted this from another project. I think a lot of people assume "one big commit" means AI slop nowadays and while I use AI assistance, that wasn't the case here.
···11+# Libghosty Go Bindings
22+33+## Commands
44+55+- **Build:** `make build` (runs CMake + `go build`)
66+- **Test:** `make test`
77+- **Clean:** `make clean` (removes `build/`, re-triggers CMake fetch on next build)
88+99+## Build
1010+1111+- CMake fetches ghostty source via `FetchContent`; pinned commit is in `CMakeLists.txt`
1212+- Never modify anything under `build/`
1313+- cgo links static library by default; `-tags dynamic` links shared
1414+1515+## Code Guidelines
1616+1717+- Ghostty headers are at `build/_deps/ghostty-src/zig-out/include/ghostty/`
1818+- Use a heavy commenting style that explains what/why clearly.
1919+- Comment every exported function, type, field, etc.
2020+- Types should be in the same filename as the C header, by default.
2121+ If they're already moved, keep them where they are. For example,
2222+ `color.h` types should go in `color.go`.
2323+- If a type is a thin layer over a C type, comment with what
2424+ C type it corresponds to, e.g. `C: GhosttyResult`
2525+- When a C API uses `GhosttyString` with `len=0`, libghostty treats
2626+ the pointer as garbage, so no nil special-casing is needed.
2727+- Define constants by their C reference, e.g.
2828+ `DAConformanceVT100 = C.GHOSTTY_DA_CONFORMANCE_VT100`. Don't
2929+ hardcode values.
+9
CMakeLists.txt
···11+cmake_minimum_required(VERSION 3.19)
22+project(go-libghostty LANGUAGES C)
33+44+include(FetchContent)
55+FetchContent_Declare(ghostty
66+ GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git
77+ GIT_TAG 48a01b8bd51b0cf4ba3ed281a6662ae131ee8239
88+)
99+FetchContent_MakeAvailable(ghostty)
+21
LICENSE
···11+MIT License
22+33+Copyright (c) 2026 Mitchell Hashimoto
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+26
Makefile
···11+BUILD_DIR := build
22+33+# FetchContent places the ghostty source here.
44+GHOSTTY_ZIG_OUT := $(CURDIR)/$(BUILD_DIR)/_deps/ghostty-src/zig-out
55+PKG_CONFIG_PATH := $(GHOSTTY_ZIG_OUT)/share/pkgconfig
66+DYLD_LIBRARY_PATH := $(GHOSTTY_ZIG_OUT)/lib
77+LD_LIBRARY_PATH := $(GHOSTTY_ZIG_OUT)/lib
88+99+# Stamp file to track whether the cmake build has run.
1010+STAMP := $(BUILD_DIR)/.ghostty-built
1111+1212+.PHONY: build test clean
1313+1414+$(STAMP):
1515+ cmake -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release
1616+ cmake --build $(BUILD_DIR)
1717+ @touch $(STAMP)
1818+1919+build: $(STAMP)
2020+ PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) go build ./...
2121+2222+test: $(STAMP)
2323+ PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) DYLD_LIBRARY_PATH=$(DYLD_LIBRARY_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH) go test ./...
2424+2525+clean:
2626+ rm -rf $(BUILD_DIR)
+101
README.md
···11+# Go Libghostty Bindings
22+33+Go bindings for `libghostty-vt`.
44+55+This project uses [cgo](https://pkg.go.dev/cmd/cgo) but `libghostty-vt`
66+only depends on libc/libc++, so it is very easy to static link and very
77+easy to cross-compile. The bindings default to static linking for this
88+reason.
99+1010+> [!WARNING]
1111+>
1212+> **I'm not promising any API stability yet.** This is a new project and the
1313+> API may change as necessary. The underlying functionality is very stable,
1414+> but the Go API is still being designed.
1515+1616+## Example
1717+1818+```go
1919+package main
2020+2121+import (
2222+ "fmt"
2323+ "log"
2424+2525+ "github.com/mitchellh/go-libghostty"
2626+)
2727+2828+func main() {
2929+ term, err := libghostty.NewTerminal(libghostty.WithSize(80, 24))
3030+ if err != nil {
3131+ log.Fatal(err)
3232+ }
3333+ defer term.Close()
3434+3535+ // Feed VT data — bold green "world", then plain text.
3636+ fmt.Fprintf(term, "Hello, \033[1;32mworld\033[0m!\r\n")
3737+3838+ // Format the terminal contents as plain text.
3939+ f, err := libghostty.NewFormatter(term,
4040+ libghostty.WithFormatterFormat(libghostty.FormatterFormatPlain),
4141+ libghostty.WithFormatterTrim(true),
4242+ )
4343+ if err != nil {
4444+ log.Fatal(err)
4545+ }
4646+ defer f.Close()
4747+4848+ output, _ := f.FormatString()
4949+ fmt.Println(output) // Hello, world!
5050+}
5151+```
5252+5353+More examples are in the [`examples/`](examples/) directory.
5454+5555+## Usage
5656+5757+Add the module to your Go project:
5858+5959+```shell
6060+go get github.com/mitchellh/go-libghostty
6161+```
6262+6363+This is a cgo package that links `libghostty-vt` via `pkg-config`. By
6464+default it links statically. Before building your project, you need the
6565+library installed. Either install it system-wide or set `PKG_CONFIG_PATH`
6666+to point to a local checkout:
6767+6868+```shell
6969+export PKG_CONFIG_PATH=/path/to/libghostty-vt/share/pkgconfig
7070+```
7171+7272+To link dynamically instead (requires the shared library at runtime,
7373+so you'll also need to set the library path):
7474+7575+```shell
7676+go build -tags dynamic
7777+```
7878+7979+See the [Ghostty docs](https://ghostty.org/docs/install/build) for
8080+building `libghostty-vt` from source.
8181+8282+## Development
8383+8484+CMake fetches and builds `libghostty-vt` automatically. CMake is only
8585+required and used for development of this module. For actual downstream
8686+usage, you can get `libghostty-vt` available however you like (e.g. system
8787+package, local checkout, etc.).
8888+8989+You need [Zig](https://ghostty.org/docs/install/build) and CMake on your PATH.
9090+9191+```shell
9292+make build
9393+make test
9494+9595+# If in a Nix dev shell:
9696+go build
9797+go test
9898+```
9999+100100+If you use the Nix dev shell (`nix develop`), `go build` and `go test`
101101+work directly — the shell configures all paths automatically.
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+#include <ghostty/vt/build_info.h>
66+*/
77+import "C"
88+99+import "unsafe"
1010+1111+// OptimizeMode identifies the optimization mode the library was built with.
1212+// C: GhosttyOptimizeMode
1313+type OptimizeMode int
1414+1515+const (
1616+ // OptimizeDebug is the debug optimization mode.
1717+ OptimizeDebug OptimizeMode = C.GHOSTTY_OPTIMIZE_DEBUG
1818+1919+ // OptimizeReleaseSafe is the release-safe optimization mode.
2020+ OptimizeReleaseSafe OptimizeMode = C.GHOSTTY_OPTIMIZE_RELEASE_SAFE
2121+2222+ // OptimizeReleaseSmall is the release-small optimization mode.
2323+ OptimizeReleaseSmall OptimizeMode = C.GHOSTTY_OPTIMIZE_RELEASE_SMALL
2424+2525+ // OptimizeReleaseFast is the release-fast optimization mode.
2626+ OptimizeReleaseFast OptimizeMode = C.GHOSTTY_OPTIMIZE_RELEASE_FAST
2727+)
2828+2929+// BuildInfo holds compile-time build configuration of libghostty-vt.
3030+// All values are constant for the lifetime of the process.
3131+// C: GhosttyBuildInfo (query enum)
3232+type BuildInfo struct {
3333+ // SIMD reports whether SIMD-accelerated code paths are enabled.
3434+ SIMD bool
3535+3636+ // KittyGraphics reports whether Kitty graphics protocol support
3737+ // is available.
3838+ KittyGraphics bool
3939+4040+ // TmuxControlMode reports whether tmux control mode support
4141+ // is available.
4242+ TmuxControlMode bool
4343+4444+ // Optimize is the optimization mode the library was built with.
4545+ Optimize OptimizeMode
4646+4747+ // VersionString is the full version string
4848+ // (e.g. "1.2.3" or "1.2.3-dev+abcdef").
4949+ VersionString string
5050+5151+ // VersionMajor is the major version number.
5252+ VersionMajor uint
5353+5454+ // VersionMinor is the minor version number.
5555+ VersionMinor uint
5656+5757+ // VersionPatch is the patch version number.
5858+ VersionPatch uint
5959+6060+ // VersionBuild is the build metadata string (e.g. commit hash).
6161+ // Empty if no build metadata is present.
6262+ VersionBuild string
6363+}
6464+6565+// GetBuildInfo queries all compile-time build configuration values
6666+// and returns them in a single BuildInfo struct.
6767+func GetBuildInfo() (BuildInfo, error) {
6868+ var info BuildInfo
6969+7070+ var simd C.bool
7171+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_SIMD, unsafe.Pointer(&simd))); err != nil {
7272+ return info, err
7373+ }
7474+ info.SIMD = bool(simd)
7575+7676+ var kitty C.bool
7777+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_KITTY_GRAPHICS, unsafe.Pointer(&kitty))); err != nil {
7878+ return info, err
7979+ }
8080+ info.KittyGraphics = bool(kitty)
8181+8282+ var tmux C.bool
8383+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE, unsafe.Pointer(&tmux))); err != nil {
8484+ return info, err
8585+ }
8686+ info.TmuxControlMode = bool(tmux)
8787+8888+ var opt C.GhosttyOptimizeMode
8989+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_OPTIMIZE, unsafe.Pointer(&opt))); err != nil {
9090+ return info, err
9191+ }
9292+ info.Optimize = OptimizeMode(opt)
9393+9494+ var verStr C.GhosttyString
9595+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_STRING, unsafe.Pointer(&verStr))); err != nil {
9696+ return info, err
9797+ }
9898+ info.VersionString = C.GoStringN((*C.char)(unsafe.Pointer(verStr.ptr)), C.int(verStr.len))
9999+100100+ var major C.size_t
101101+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_MAJOR, unsafe.Pointer(&major))); err != nil {
102102+ return info, err
103103+ }
104104+ info.VersionMajor = uint(major)
105105+106106+ var minor C.size_t
107107+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_MINOR, unsafe.Pointer(&minor))); err != nil {
108108+ return info, err
109109+ }
110110+ info.VersionMinor = uint(minor)
111111+112112+ var patch C.size_t
113113+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_PATCH, unsafe.Pointer(&patch))); err != nil {
114114+ return info, err
115115+ }
116116+ info.VersionPatch = uint(patch)
117117+118118+ var verBuild C.GhosttyString
119119+ if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_BUILD, unsafe.Pointer(&verBuild))); err != nil {
120120+ return info, err
121121+ }
122122+ info.VersionBuild = C.GoStringN((*C.char)(unsafe.Pointer(verBuild.ptr)), C.int(verBuild.len))
123123+124124+ return info, nil
125125+}
+27
build_info_test.go
···11+package libghostty
22+33+import "testing"
44+55+func TestGetBuildInfo(t *testing.T) {
66+ info, err := GetBuildInfo()
77+ if err != nil {
88+ t.Fatal(err)
99+ }
1010+1111+ // Version string should be non-empty.
1212+ if info.VersionString == "" {
1313+ t.Fatal("expected non-empty version string")
1414+ }
1515+1616+ // At least one version component should be non-zero.
1717+ if info.VersionMajor == 0 && info.VersionMinor == 0 && info.VersionPatch == 0 {
1818+ t.Fatal("expected at least one non-zero version component")
1919+ }
2020+2121+ // Optimize mode should be a known value.
2222+ switch info.Optimize {
2323+ case OptimizeDebug, OptimizeReleaseSafe, OptimizeReleaseSmall, OptimizeReleaseFast:
2424+ default:
2525+ t.Fatalf("unexpected optimize mode: %d", info.Optimize)
2626+ }
2727+}
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+*/
66+import "C"
77+88+// ColorRGB represents an RGB color value.
99+// C: GhosttyColorRgb
1010+type ColorRGB struct {
1111+ R uint8
1212+ G uint8
1313+ B uint8
1414+}
1515+1616+// PaletteSize is the number of entries in a terminal color palette.
1717+const PaletteSize = 256
1818+1919+// Palette is a 256-color palette.
2020+type Palette [PaletteSize]ColorRGB
2121+2222+// Named color palette indices.
2323+// C: GHOSTTY_COLOR_NAMED_*
2424+const (
2525+ ColorNamedBlack = C.GHOSTTY_COLOR_NAMED_BLACK
2626+ ColorNamedRed = C.GHOSTTY_COLOR_NAMED_RED
2727+ ColorNamedGreen = C.GHOSTTY_COLOR_NAMED_GREEN
2828+ ColorNamedYellow = C.GHOSTTY_COLOR_NAMED_YELLOW
2929+ ColorNamedBlue = C.GHOSTTY_COLOR_NAMED_BLUE
3030+ ColorNamedMagenta = C.GHOSTTY_COLOR_NAMED_MAGENTA
3131+ ColorNamedCyan = C.GHOSTTY_COLOR_NAMED_CYAN
3232+ ColorNamedWhite = C.GHOSTTY_COLOR_NAMED_WHITE
3333+ ColorNamedBrightBlack = C.GHOSTTY_COLOR_NAMED_BRIGHT_BLACK
3434+ ColorNamedBrightRed = C.GHOSTTY_COLOR_NAMED_BRIGHT_RED
3535+ ColorNamedBrightGreen = C.GHOSTTY_COLOR_NAMED_BRIGHT_GREEN
3636+ ColorNamedBrightYellow = C.GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW
3737+ ColorNamedBrightBlue = C.GHOSTTY_COLOR_NAMED_BRIGHT_BLUE
3838+ ColorNamedBrightMagenta = C.GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA
3939+ ColorNamedBrightCyan = C.GHOSTTY_COLOR_NAMED_BRIGHT_CYAN
4040+ ColorNamedBrightWhite = C.GHOSTTY_COLOR_NAMED_BRIGHT_WHITE
4141+)
+127
device.go
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+*/
66+import "C"
77+88+// ColorScheme identifies the terminal color scheme (light or dark).
99+// C: GhosttyColorScheme
1010+type ColorScheme int
1111+1212+const (
1313+ // ColorSchemeLight indicates a light color scheme.
1414+ ColorSchemeLight ColorScheme = C.GHOSTTY_COLOR_SCHEME_LIGHT
1515+1616+ // ColorSchemeDark indicates a dark color scheme.
1717+ ColorSchemeDark ColorScheme = C.GHOSTTY_COLOR_SCHEME_DARK
1818+)
1919+2020+// DA1 conformance levels (Pp parameter).
2121+// C: GHOSTTY_DA_CONFORMANCE_*
2222+const (
2323+ DAConformanceVT100 = C.GHOSTTY_DA_CONFORMANCE_VT100
2424+ DAConformanceVT101 = C.GHOSTTY_DA_CONFORMANCE_VT101
2525+ DAConformanceVT102 = C.GHOSTTY_DA_CONFORMANCE_VT102
2626+ DAConformanceVT125 = C.GHOSTTY_DA_CONFORMANCE_VT125
2727+ DAConformanceVT131 = C.GHOSTTY_DA_CONFORMANCE_VT131
2828+ DAConformanceVT132 = C.GHOSTTY_DA_CONFORMANCE_VT132
2929+ DAConformanceVT220 = C.GHOSTTY_DA_CONFORMANCE_VT220
3030+ DAConformanceVT240 = C.GHOSTTY_DA_CONFORMANCE_VT240
3131+ DAConformanceVT320 = C.GHOSTTY_DA_CONFORMANCE_VT320
3232+ DAConformanceVT340 = C.GHOSTTY_DA_CONFORMANCE_VT340
3333+ DAConformanceVT420 = C.GHOSTTY_DA_CONFORMANCE_VT420
3434+ DAConformanceVT510 = C.GHOSTTY_DA_CONFORMANCE_VT510
3535+ DAConformanceVT520 = C.GHOSTTY_DA_CONFORMANCE_VT520
3636+ DAConformanceVT525 = C.GHOSTTY_DA_CONFORMANCE_VT525
3737+ DAConformanceLevel2 = C.GHOSTTY_DA_CONFORMANCE_LEVEL_2
3838+ DAConformanceLevel3 = C.GHOSTTY_DA_CONFORMANCE_LEVEL_3
3939+ DAConformanceLevel4 = C.GHOSTTY_DA_CONFORMANCE_LEVEL_4
4040+ DAConformanceLevel5 = C.GHOSTTY_DA_CONFORMANCE_LEVEL_5
4141+)
4242+4343+// DA1 feature codes (Ps parameters).
4444+// C: GHOSTTY_DA_FEATURE_*
4545+const (
4646+ DAFeatureColumns132 = C.GHOSTTY_DA_FEATURE_COLUMNS_132
4747+ DAFeaturePrinter = C.GHOSTTY_DA_FEATURE_PRINTER
4848+ DAFeatureReGIS = C.GHOSTTY_DA_FEATURE_REGIS
4949+ DAFeatureSixel = C.GHOSTTY_DA_FEATURE_SIXEL
5050+ DAFeatureSelectiveErase = C.GHOSTTY_DA_FEATURE_SELECTIVE_ERASE
5151+ DAFeatureUserDefinedKeys = C.GHOSTTY_DA_FEATURE_USER_DEFINED_KEYS
5252+ DAFeatureNationalReplacement = C.GHOSTTY_DA_FEATURE_NATIONAL_REPLACEMENT
5353+ DAFeatureTechnicalCharacters = C.GHOSTTY_DA_FEATURE_TECHNICAL_CHARACTERS
5454+ DAFeatureLocator = C.GHOSTTY_DA_FEATURE_LOCATOR
5555+ DAFeatureTerminalState = C.GHOSTTY_DA_FEATURE_TERMINAL_STATE
5656+ DAFeatureWindowing = C.GHOSTTY_DA_FEATURE_WINDOWING
5757+ DAFeatureHorizontalScrolling = C.GHOSTTY_DA_FEATURE_HORIZONTAL_SCROLLING
5858+ DAFeatureANSIColor = C.GHOSTTY_DA_FEATURE_ANSI_COLOR
5959+ DAFeatureRectangularEditing = C.GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING
6060+ DAFeatureANSITextLocator = C.GHOSTTY_DA_FEATURE_ANSI_TEXT_LOCATOR
6161+ DAFeatureClipboard = C.GHOSTTY_DA_FEATURE_CLIPBOARD
6262+)
6363+6464+// DA2 device type identifiers (Pp parameter).
6565+// C: GHOSTTY_DA_DEVICE_TYPE_*
6666+const (
6767+ DADeviceTypeVT100 = C.GHOSTTY_DA_DEVICE_TYPE_VT100
6868+ DADeviceTypeVT220 = C.GHOSTTY_DA_DEVICE_TYPE_VT220
6969+ DADeviceTypeVT240 = C.GHOSTTY_DA_DEVICE_TYPE_VT240
7070+ DADeviceTypeVT330 = C.GHOSTTY_DA_DEVICE_TYPE_VT330
7171+ DADeviceTypeVT340 = C.GHOSTTY_DA_DEVICE_TYPE_VT340
7272+ DADeviceTypeVT320 = C.GHOSTTY_DA_DEVICE_TYPE_VT320
7373+ DADeviceTypeVT382 = C.GHOSTTY_DA_DEVICE_TYPE_VT382
7474+ DADeviceTypeVT420 = C.GHOSTTY_DA_DEVICE_TYPE_VT420
7575+ DADeviceTypeVT510 = C.GHOSTTY_DA_DEVICE_TYPE_VT510
7676+ DADeviceTypeVT520 = C.GHOSTTY_DA_DEVICE_TYPE_VT520
7777+ DADeviceTypeVT525 = C.GHOSTTY_DA_DEVICE_TYPE_VT525
7878+)
7979+8080+// DeviceAttributes holds the response data for all three DA levels.
8181+// The terminal fills whichever sub-struct matches the request type.
8282+// C: GhosttyDeviceAttributes
8383+type DeviceAttributes struct {
8484+ // Primary is the DA1 response data (CSI c).
8585+ Primary DeviceAttributesPrimary
8686+8787+ // Secondary is the DA2 response data (CSI > c).
8888+ Secondary DeviceAttributesSecondary
8989+9090+ // Tertiary is the DA3 response data (CSI = c).
9191+ Tertiary DeviceAttributesTertiary
9292+}
9393+9494+// DeviceAttributesPrimary holds primary device attributes (DA1).
9595+// C: GhosttyDeviceAttributesPrimary
9696+type DeviceAttributesPrimary struct {
9797+ // ConformanceLevel is the Pp parameter. E.g. 62 for VT220.
9898+ ConformanceLevel uint16
9999+100100+ // Features contains the DA1 feature codes (Ps parameters).
101101+ // Only the first NumFeatures entries are valid.
102102+ Features [64]uint16
103103+104104+ // NumFeatures is the number of valid entries in Features.
105105+ NumFeatures int
106106+}
107107+108108+// DeviceAttributesSecondary holds secondary device attributes (DA2).
109109+// C: GhosttyDeviceAttributesSecondary
110110+type DeviceAttributesSecondary struct {
111111+ // DeviceType is the terminal type identifier (Pp). E.g. 1 for VT220.
112112+ DeviceType uint16
113113+114114+ // FirmwareVersion is the firmware/patch version number (Pv).
115115+ FirmwareVersion uint16
116116+117117+ // ROMCartridge is the ROM cartridge registration number (Pc).
118118+ // Always 0 for emulators.
119119+ ROMCartridge uint16
120120+}
121121+122122+// DeviceAttributesTertiary holds tertiary device attributes (DA3).
123123+// C: GhosttyDeviceAttributesTertiary
124124+type DeviceAttributesTertiary struct {
125125+ // UnitID is encoded as 8 uppercase hex digits in the response.
126126+ UnitID uint32
127127+}
+51
doc.go
···11+// Package libghostty provides Go bindings for libghostty-vt, a
22+// virtual terminal emulator library from the Ghostty project.
33+//
44+// # Getting Started
55+//
66+// Create a terminal with [NewTerminal], feed it input with
77+// [Terminal.VTWrite] (or [Terminal.Write] for an [io.Writer]), and
88+// inspect state through data getters such as [Terminal.CursorX],
99+// [Terminal.Title], and [Terminal.ActiveScreen]. When finished, call
1010+// [Terminal.Close] to release resources.
1111+//
1212+// term, err := libghostty.NewTerminal(
1313+// libghostty.WithSize(80, 24),
1414+// libghostty.WithMaxScrollback(1000),
1515+// )
1616+// if err != nil {
1717+// log.Fatal(err)
1818+// }
1919+// defer term.Close()
2020+//
2121+// term.VTWrite([]byte("Hello, world!\r\n"))
2222+//
2323+// # Effects
2424+//
2525+// The terminal communicates side-effects back to the host through
2626+// effect callbacks. Register them at creation time with functional
2727+// options like [WithWritePty], [WithBell], and [WithEnquiry], or
2828+// on a live terminal with [Terminal.SetEffectWritePty] and friends.
2929+//
3030+// [WithWritePty] is the most common effect — it delivers data that
3131+// the terminal wants to send back to the pty (e.g. query responses):
3232+//
3333+// term, _ := libghostty.NewTerminal(
3434+// libghostty.WithSize(80, 24),
3535+// libghostty.WithWritePty(func(data []byte) {
3636+// os.Stdout.Write(data)
3737+// }),
3838+// )
3939+//
4040+// # Terminal Options
4141+//
4242+// Terminal properties can be changed after creation with setter
4343+// methods such as [Terminal.SetColorForeground],
4444+// [Terminal.SetColorBackground], [Terminal.SetColorPalette],
4545+// [Terminal.SetTitle], and [Terminal.SetPwd].
4646+//
4747+// # Linking
4848+//
4949+// This is a cgo package. By default it links the shared library via
5050+// pkg-config. Build with "-tags static" to link statically instead.
5151+package libghostty
···11+// Command colors demonstrates the libghostty color APIs: setting and
22+// querying foreground, background, cursor, and palette colors, as well
33+// as the distinction between "effective" (OSC-overridden) and "default"
44+// values.
55+package main
66+77+import (
88+ "fmt"
99+ "log"
1010+1111+ ghostty "github.com/mitchellh/go-libghostty"
1212+)
1313+1414+func main() {
1515+ // Step 1: Create an 80×24 terminal with no scrollback.
1616+ t, err := ghostty.NewTerminal(
1717+ ghostty.WithSize(80, 24),
1818+ ghostty.WithMaxScrollback(0),
1919+ )
2020+ if err != nil {
2121+ log.Fatal(err)
2222+ }
2323+ defer t.Close()
2424+2525+ // Step 2: Print colors before any configuration — everything is unset.
2626+ fmt.Println("=== Before setting colors ===")
2727+ printColors(t)
2828+2929+ // Step 3: Apply a Catppuccin-inspired dark theme via the config API.
3030+ if err := t.SetColorForeground(&ghostty.ColorRGB{R: 205, G: 214, B: 244}); err != nil {
3131+ log.Fatal(err)
3232+ }
3333+ if err := t.SetColorBackground(&ghostty.ColorRGB{R: 30, G: 30, B: 46}); err != nil {
3434+ log.Fatal(err)
3535+ }
3636+ if err := t.SetColorCursor(&ghostty.ColorRGB{R: 245, G: 224, B: 220}); err != nil {
3737+ log.Fatal(err)
3838+ }
3939+4040+ // Override the first 8 palette entries with Catppuccin colors.
4141+ palette, err := t.ColorPalette()
4242+ if err != nil {
4343+ log.Fatal(err)
4444+ }
4545+ palette[ghostty.ColorNamedBlack] = ghostty.ColorRGB{R: 69, G: 71, B: 90}
4646+ palette[ghostty.ColorNamedRed] = ghostty.ColorRGB{R: 243, G: 139, B: 168}
4747+ palette[ghostty.ColorNamedGreen] = ghostty.ColorRGB{R: 166, G: 227, B: 161}
4848+ palette[ghostty.ColorNamedYellow] = ghostty.ColorRGB{R: 249, G: 226, B: 175}
4949+ palette[ghostty.ColorNamedBlue] = ghostty.ColorRGB{R: 137, G: 180, B: 250}
5050+ palette[ghostty.ColorNamedMagenta] = ghostty.ColorRGB{R: 245, G: 194, B: 231}
5151+ palette[ghostty.ColorNamedCyan] = ghostty.ColorRGB{R: 148, G: 226, B: 213}
5252+ palette[ghostty.ColorNamedWhite] = ghostty.ColorRGB{R: 186, G: 194, B: 222}
5353+ if err := t.SetColorPalette(palette); err != nil {
5454+ log.Fatal(err)
5555+ }
5656+5757+ // Step 4: Print colors after applying the theme.
5858+ fmt.Println("\n=== After setting Catppuccin theme ===")
5959+ printColors(t)
6060+6161+ // Step 5: Use OSC 10 to override the foreground color to red via VT
6262+ // input. This changes the "effective" color but leaves the "default"
6363+ // unchanged.
6464+ t.VTWrite([]byte("\x1b]10;rgb:ff/00/00\x1b\\"))
6565+6666+ fmt.Println("\n=== After OSC 10 override (fg → red) ===")
6767+ printColors(t)
6868+6969+ // Step 7: Clear the default foreground by passing nil.
7070+ if err := t.SetColorForeground(nil); err != nil {
7171+ log.Fatal(err)
7272+ }
7373+7474+ fmt.Println("\n=== After clearing default foreground ===")
7575+ printColors(t)
7676+}
7777+7878+// printColors prints the effective and default values for foreground,
7979+// background, cursor, and palette[0].
8080+func printColors(t *ghostty.Terminal) {
8181+ type colorPair struct {
8282+ label string
8383+ eff, def func() (*ghostty.ColorRGB, error)
8484+ }
8585+8686+ pairs := []colorPair{
8787+ {"Foreground", t.ColorForeground, t.ColorForegroundDefault},
8888+ {"Background", t.ColorBackground, t.ColorBackgroundDefault},
8989+ {"Cursor", t.ColorCursor, t.ColorCursorDefault},
9090+ }
9191+9292+ for _, p := range pairs {
9393+ eff, err := p.eff()
9494+ if err != nil {
9595+ log.Fatal(err)
9696+ }
9797+ def, err := p.def()
9898+ if err != nil {
9999+ log.Fatal(err)
100100+ }
101101+ fmt.Printf(" %-12s effective=%-12s default=%s\n",
102102+ p.label, formatColor(eff), formatColor(def))
103103+ }
104104+105105+ // Print palette entry 0 (black).
106106+ palette, err := t.ColorPalette()
107107+ if err != nil {
108108+ log.Fatal(err)
109109+ }
110110+ paletteDefault, err := t.ColorPaletteDefault()
111111+ if err != nil {
112112+ log.Fatal(err)
113113+ }
114114+ fmt.Printf(" %-12s effective=%-12s default=%s\n",
115115+ "Palette[0]",
116116+ formatColor(&palette[0]),
117117+ formatColor(&paletteDefault[0]))
118118+}
119119+120120+// formatColor formats a *ColorRGB as "#RRGGBB" or "(not set)" if nil.
121121+func formatColor(c *ghostty.ColorRGB) string {
122122+ if c == nil {
123123+ return "(not set)"
124124+ }
125125+ return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
126126+}
+64
examples/effects/main.go
···11+// Example program demonstrating terminal effect callbacks.
22+//
33+// It registers write_pty, bell, and title_changed effect handlers, then
44+// feeds VT sequences that trigger each one. Output shows how the
55+// callbacks fire and how terminal state can be queried from within them.
66+package main
77+88+import (
99+ "fmt"
1010+ "log"
1111+1212+ ghostty "github.com/mitchellh/go-libghostty"
1313+)
1414+1515+func main() {
1616+ // Bell counter, captured by the bell handler closure.
1717+ bellCount := 0
1818+1919+ // We declare term here so the title_changed closure can capture it.
2020+ var term *ghostty.Terminal
2121+2222+ var err error
2323+ term, err = ghostty.NewTerminal(
2424+ ghostty.WithSize(80, 24),
2525+2626+ // write_pty: called when the terminal writes data back (e.g. query responses).
2727+ ghostty.WithWritePty(func(data []byte) {
2828+ fmt.Printf("write_pty: %d bytes: %q\n", len(data), data)
2929+ }),
3030+3131+ // bell: called on BEL (0x07).
3232+ ghostty.WithBell(func() {
3333+ bellCount++
3434+ fmt.Printf("bell: count=%d\n", bellCount)
3535+ }),
3636+3737+ // title_changed: called when the terminal title changes via OSC 0/2.
3838+ ghostty.WithTitleChanged(func() {
3939+ x, err := term.CursorX()
4040+ if err != nil {
4141+ log.Fatal(err)
4242+ }
4343+ fmt.Printf("title_changed: cursor_x=%d\n", x)
4444+ }),
4545+ )
4646+ if err != nil {
4747+ log.Fatal(err)
4848+ }
4949+ defer term.Close()
5050+5151+ // BEL → triggers bell handler.
5252+ term.VTWrite([]byte{0x07})
5353+5454+ // OSC 2 (set title) → triggers title_changed handler.
5555+ term.VTWrite([]byte("\x1b]2;hello\x1b\\"))
5656+5757+ // DECRQM query → triggers write_pty with the response.
5858+ term.VTWrite([]byte("\x1b[?7$p"))
5959+6060+ // Another BEL → triggers bell handler again.
6161+ term.VTWrite([]byte{0x07})
6262+6363+ fmt.Printf("total bell count: %d\n", bellCount)
6464+}
+55
examples/formatter/main.go
···11+// Example program demonstrating the Formatter API from libghostty.
22+// It creates a terminal, writes various VT sequences to it, then
33+// formats the terminal contents as plain text with trimming enabled.
44+package main
55+66+import (
77+ "fmt"
88+ "log"
99+1010+ lg "github.com/mitchellh/go-libghostty"
1111+)
1212+1313+func main() {
1414+ // Create an 80x24 terminal.
1515+ term, err := lg.NewTerminal(lg.WithSize(80, 24))
1616+ if err != nil {
1717+ log.Fatal(err)
1818+ }
1919+ defer term.Close()
2020+2121+ // Write some content with VT formatting.
2222+ fmt.Fprintf(term, "Line 1: Hello World!\r\n")
2323+ fmt.Fprintf(term, "Line 2: \033[1mBold\033[0m and \033[4mUnderline\033[0m\r\n")
2424+ fmt.Fprintf(term, "Line 3: placeholder\r\n")
2525+2626+ // Move to row 3, col 1 and overwrite line 3.
2727+ fmt.Fprintf(term, "\033[3;1H") // CUP row 3 col 1
2828+ fmt.Fprintf(term, "\033[2K") // Erase entire line
2929+ fmt.Fprintf(term, "Line 3: Overwritten!\r\n")
3030+3131+ // Place text at specific positions.
3232+ fmt.Fprintf(term, "\033[5;10H") // CUP row 5 col 10
3333+ fmt.Fprintf(term, "Placed at (5,10)")
3434+ fmt.Fprintf(term, "\033[1;72H") // CUP row 1 col 72
3535+ fmt.Fprintf(term, "RIGHT->")
3636+3737+ // Create a plain-text formatter with trimming enabled.
3838+ f, err := lg.NewFormatter(term,
3939+ lg.WithFormatterFormat(lg.FormatterFormatPlain),
4040+ lg.WithFormatterTrim(true),
4141+ )
4242+ if err != nil {
4343+ log.Fatal(err)
4444+ }
4545+ defer f.Close()
4646+4747+ // Format and print the output.
4848+ output, err := f.FormatString()
4949+ if err != nil {
5050+ log.Fatal(err)
5151+ }
5252+5353+ fmt.Printf("%s\n", output)
5454+ fmt.Printf("(%d bytes)\n", len(output))
5555+}
+93
examples/grid-traverse/main.go
···11+// Example grid-traverse demonstrates walking the terminal grid
22+// cell-by-cell using the GridRef API to inspect content and style.
33+package main
44+55+import (
66+ "fmt"
77+ "log"
88+99+ "github.com/mitchellh/go-libghostty"
1010+)
1111+1212+func main() {
1313+ term, err := libghostty.NewTerminal(libghostty.WithSize(10, 3))
1414+ if err != nil {
1515+ log.Fatal(err)
1616+ }
1717+ defer term.Close()
1818+1919+ // Write some content: two plain lines and one bold line.
2020+ term.VTWrite([]byte("Hello!\r\n"))
2121+ term.VTWrite([]byte("World\r\n"))
2222+ term.VTWrite([]byte("\033[1mBold"))
2323+2424+ cols, err := term.Cols()
2525+ if err != nil {
2626+ log.Fatal(err)
2727+ }
2828+ rows, err := term.Rows()
2929+ if err != nil {
3030+ log.Fatal(err)
3131+ }
3232+3333+ for row := range rows {
3434+ fmt.Printf("Row %d: ", row)
3535+3636+ for col := range cols {
3737+ ref, err := term.GridRef(libghostty.Point{
3838+ Tag: libghostty.PointTagActive,
3939+ X: col,
4040+ Y: uint32(row),
4141+ })
4242+ if err != nil {
4343+ log.Fatal(err)
4444+ }
4545+4646+ cell, err := ref.Cell()
4747+ if err != nil {
4848+ log.Fatal(err)
4949+ }
5050+5151+ hasText, err := cell.HasText()
5252+ if err != nil {
5353+ log.Fatal(err)
5454+ }
5555+5656+ if hasText {
5757+ cp, err := cell.Codepoint()
5858+ if err != nil {
5959+ log.Fatal(err)
6060+ }
6161+ fmt.Printf("%c", rune(cp))
6262+ } else {
6363+ fmt.Print(".")
6464+ }
6565+ }
6666+6767+ // Print wrap and bold state for the first cell in the row.
6868+ ref, err := term.GridRef(libghostty.Point{
6969+ Tag: libghostty.PointTagActive,
7070+ X: 0,
7171+ Y: uint32(row),
7272+ })
7373+ if err != nil {
7474+ log.Fatal(err)
7575+ }
7676+7777+ rowData, err := ref.Row()
7878+ if err != nil {
7979+ log.Fatal(err)
8080+ }
8181+ wrap, err := rowData.Wrap()
8282+ if err != nil {
8383+ log.Fatal(err)
8484+ }
8585+8686+ style, err := ref.Style()
8787+ if err != nil {
8888+ log.Fatal(err)
8989+ }
9090+9191+ fmt.Printf(" (wrap=%t, bold=%t)\n", wrap, style.Bold())
9292+ }
9393+}
+19
examples/modes/main.go
···11+// Example: modes demonstrates the Mode API from libghostty.
22+// It prints the value, ANSI flag, and packed hex for a couple of modes.
33+package main
44+55+import (
66+ "fmt"
77+88+ ghostty "github.com/mitchellh/go-libghostty"
99+)
1010+1111+func main() {
1212+ // DEC mode 25: cursor visible (DECTCEM)
1313+ m := ghostty.ModeCursorVisible
1414+ fmt.Printf("value=%d ansi=%v packed=0x%04x\n", m.Value(), m.ANSI(), uint16(m))
1515+1616+ // ANSI mode 4: insert mode
1717+ m = ghostty.ModeInsert
1818+ fmt.Printf("value=%d ansi=%v packed=0x%04x\n", m.Value(), m.ANSI(), uint16(m))
1919+}
+191
examples/render/main.go
···11+// Example render demonstrates the RenderState API by creating a terminal,
22+// writing styled VT content, and iterating over the resulting rows and
33+// cells to produce ANSI-colored output.
44+package main
55+66+import (
77+ "fmt"
88+ "log"
99+1010+ "github.com/mitchellh/go-libghostty"
1111+)
1212+1313+// resolveColor converts a StyleColor to a concrete ColorRGB using the
1414+// render state's palette and a fallback for unset colors.
1515+func resolveColor(sc libghostty.StyleColor, colors *libghostty.RenderStateColors, fallback libghostty.ColorRGB) libghostty.ColorRGB {
1616+ switch sc.Tag {
1717+ case libghostty.StyleColorRGB:
1818+ return sc.RGB
1919+ case libghostty.StyleColorPalette:
2020+ return colors.Palette[sc.Palette]
2121+ default:
2222+ return fallback
2323+ }
2424+}
2525+2626+// cursorStyleName returns a human-readable name for a cursor visual style.
2727+func cursorStyleName(s libghostty.CursorVisualStyle) string {
2828+ switch s {
2929+ case libghostty.CursorVisualStyleBar:
3030+ return "bar"
3131+ case libghostty.CursorVisualStyleBlock:
3232+ return "block"
3333+ case libghostty.CursorVisualStyleUnderline:
3434+ return "underline"
3535+ case libghostty.CursorVisualStyleBlockHollow:
3636+ return "block_hollow"
3737+ default:
3838+ return "unknown"
3939+ }
4040+}
4141+4242+func main() {
4343+ // 1. Create terminal 40x5 with scrollback 10000.
4444+ term, err := libghostty.NewTerminal(
4545+ libghostty.WithSize(40, 5),
4646+ libghostty.WithMaxScrollback(10000),
4747+ )
4848+ if err != nil {
4949+ log.Fatal(err)
5050+ }
5151+ defer term.Close()
5252+5353+ // 2. Create render state.
5454+ rs, err := libghostty.NewRenderState()
5555+ if err != nil {
5656+ log.Fatal(err)
5757+ }
5858+ defer rs.Close()
5959+6060+ // 3. Write styled VT content.
6161+ term.VTWrite([]byte("Hello, \033[1;32mworld\033[0m!\r\n"))
6262+ term.VTWrite([]byte("\033[4munderlined\033[0m text\r\n"))
6363+ term.VTWrite([]byte("\033[38;2;255;128;0morange\033[0m\r\n"))
6464+6565+ // 4. Update render state from terminal.
6666+ if err := rs.Update(term); err != nil {
6767+ log.Fatal(err)
6868+ }
6969+7070+ // 5. Check and print dirty state.
7171+ dirty, err := rs.Dirty()
7272+ if err != nil {
7373+ log.Fatal(err)
7474+ }
7575+ fmt.Printf("dirty: %d\n", dirty)
7676+7777+ // 6. Get and print colors.
7878+ colors, err := rs.Colors()
7979+ if err != nil {
8080+ log.Fatal(err)
8181+ }
8282+ fmt.Printf("bg: #%02x%02x%02x\n", colors.Background.R, colors.Background.G, colors.Background.B)
8383+ fmt.Printf("fg: #%02x%02x%02x\n", colors.Foreground.R, colors.Foreground.G, colors.Foreground.B)
8484+8585+ // 7. Cursor information.
8686+ cursorVisible, err := rs.CursorVisible()
8787+ if err != nil {
8888+ log.Fatal(err)
8989+ }
9090+ cursorHasValue, err := rs.CursorViewportHasValue()
9191+ if err != nil {
9292+ log.Fatal(err)
9393+ }
9494+ if cursorVisible && cursorHasValue {
9595+ cx, err := rs.CursorViewportX()
9696+ if err != nil {
9797+ log.Fatal(err)
9898+ }
9999+ cy, err := rs.CursorViewportY()
100100+ if err != nil {
101101+ log.Fatal(err)
102102+ }
103103+ style, err := rs.CursorVisualStyle()
104104+ if err != nil {
105105+ log.Fatal(err)
106106+ }
107107+ fmt.Printf("cursor: x=%d y=%d style=%s\n", cx, cy, cursorStyleName(style))
108108+ } else {
109109+ fmt.Printf("cursor: not visible\n")
110110+ }
111111+112112+ // 8. Iterate rows and cells.
113113+ ri, err := libghostty.NewRenderStateRowIterator()
114114+ if err != nil {
115115+ log.Fatal(err)
116116+ }
117117+ defer ri.Close()
118118+119119+ rc, err := libghostty.NewRenderStateRowCells()
120120+ if err != nil {
121121+ log.Fatal(err)
122122+ }
123123+ defer rc.Close()
124124+125125+ if err := rs.RowIterator(ri); err != nil {
126126+ log.Fatal(err)
127127+ }
128128+129129+ for ri.Next() {
130130+ rowDirty, err := ri.Dirty()
131131+ if err != nil {
132132+ log.Fatal(err)
133133+ }
134134+ _ = rowDirty
135135+136136+ if err := ri.Cells(rc); err != nil {
137137+ log.Fatal(err)
138138+ }
139139+140140+ for rc.Next() {
141141+ graphemes, err := rc.Graphemes()
142142+ if err != nil {
143143+ log.Fatal(err)
144144+ }
145145+ if len(graphemes) == 0 {
146146+ continue
147147+ }
148148+149149+ style, err := rc.Style()
150150+ if err != nil {
151151+ log.Fatal(err)
152152+ }
153153+154154+ // Resolve foreground color.
155155+ fg := resolveColor(style.FgColor(), colors, colors.Foreground)
156156+157157+ // Emit ANSI true-color escape for foreground.
158158+ fmt.Printf("\033[38;2;%d;%d;%dm", fg.R, fg.G, fg.B)
159159+160160+ // Bold marker.
161161+ if style.Bold() {
162162+ fmt.Printf("\033[1m")
163163+ }
164164+165165+ // Underline marker.
166166+ if style.Underline() != libghostty.UnderlineNone {
167167+ fmt.Printf("\033[4m")
168168+ }
169169+170170+ // Print codepoints.
171171+ for _, cp := range graphemes {
172172+ fmt.Printf("%c", rune(cp))
173173+ }
174174+175175+ // Reset style after each cell.
176176+ fmt.Printf("\033[0m")
177177+ }
178178+179179+ // Clear row dirty flag.
180180+ if err := ri.SetDirty(false); err != nil {
181181+ log.Fatal(err)
182182+ }
183183+184184+ fmt.Println()
185185+ }
186186+187187+ // 9. Reset global dirty state.
188188+ if err := rs.SetDirty(libghostty.RenderStateDirtyFalse); err != nil {
189189+ log.Fatal(err)
190190+ }
191191+}
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+66+// Helper to create a properly initialized GhosttyGridRef (sized struct).
77+static inline GhosttyGridRef init_grid_ref() {
88+ GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef);
99+ return ref;
1010+}
1111+*/
1212+import "C"
1313+1414+import "unsafe"
1515+1616+// initCGridRef returns a zero-initialized C GhosttyGridRef with its
1717+// size field set (GHOSTTY_INIT_SIZED). Used by terminal.go to pass
1818+// a grid ref to C APIs.
1919+func initCGridRef() C.GhosttyGridRef {
2020+ return C.init_grid_ref()
2121+}
2222+2323+// GridRef is a resolved reference to a specific cell position in the
2424+// terminal's internal page structure. Obtain a GridRef from
2525+// Terminal.GridRef, then extract cell or row data from it.
2626+//
2727+// A GridRef is only valid until the next update to the terminal
2828+// instance. Read and cache any needed information immediately.
2929+// C: GhosttyGridRef
3030+type GridRef struct {
3131+ ref C.GhosttyGridRef
3232+}
3333+3434+// Cell returns the cell at the grid reference's position.
3535+func (g *GridRef) Cell() (*Cell, error) {
3636+ var cell C.GhosttyCell
3737+ if err := resultError(C.ghostty_grid_ref_cell(&g.ref, &cell)); err != nil {
3838+ return nil, err
3939+ }
4040+ return &Cell{c: cell}, nil
4141+}
4242+4343+// Row returns the row at the grid reference's position.
4444+func (g *GridRef) Row() (*Row, error) {
4545+ var row C.GhosttyRow
4646+ if err := resultError(C.ghostty_grid_ref_row(&g.ref, &row)); err != nil {
4747+ return nil, err
4848+ }
4949+ return &Row{c: row}, nil
5050+}
5151+5252+// Graphemes returns the full grapheme cluster codepoints for the cell
5353+// at the grid reference's position. Returns nil if the cell has no text.
5454+func (g *GridRef) Graphemes() ([]uint32, error) {
5555+ // First call to get the required length.
5656+ var outLen C.size_t
5757+ err := resultError(C.ghostty_grid_ref_graphemes(&g.ref, nil, 0, &outLen))
5858+ if err != nil {
5959+ // OUT_OF_SPACE means we need a bigger buffer; outLen has the size.
6060+ ge, ok := err.(*Error)
6161+ if !ok || ge.Result != ResultOutOfSpace {
6262+ return nil, err
6363+ }
6464+ }
6565+6666+ if outLen == 0 {
6767+ return nil, nil
6868+ }
6969+7070+ buf := make([]uint32, uint(outLen))
7171+ if err := resultError(C.ghostty_grid_ref_graphemes(
7272+ &g.ref,
7373+ (*C.uint32_t)(unsafe.Pointer(&buf[0])),
7474+ C.size_t(len(buf)),
7575+ &outLen,
7676+ )); err != nil {
7777+ return nil, err
7878+ }
7979+8080+ return buf[:uint(outLen)], nil
8181+}
8282+8383+// Style returns the style of the cell at the grid reference's position.
8484+func (g *GridRef) Style() (*Style, error) {
8585+ cs := initCStyle()
8686+ if err := resultError(C.ghostty_grid_ref_style(&g.ref, &cs)); err != nil {
8787+ return nil, err
8888+ }
8989+ return &Style{c: cs}, nil
9090+}
+170
mode.go
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+*/
66+import "C"
77+88+// Mode is a packed 16-bit terminal mode identifier. It encodes a mode
99+// value (bits 0–14) and an ANSI flag (bit 15). DEC private modes have
1010+// the ANSI bit clear; standard ANSI modes have it set.
1111+// C: GhosttyMode
1212+type Mode uint16
1313+1414+// Value returns the numeric mode value (0–32767).
1515+func (m Mode) Value() uint16 {
1616+ return uint16(m) & 0x7FFF
1717+}
1818+1919+// ANSI reports whether this is a standard ANSI mode. If false, it is
2020+// a DEC private mode (?-prefixed).
2121+func (m Mode) ANSI() bool {
2222+ return (m >> 15) != 0
2323+}
2424+2525+// ANSI modes.
2626+var (
2727+ // ModeKAM is keyboard action mode (disable keyboard).
2828+ ModeKAM = Mode(C.GHOSTTY_MODE_KAM)
2929+3030+ // ModeInsert is insert mode.
3131+ ModeInsert = Mode(C.GHOSTTY_MODE_INSERT)
3232+3333+ // ModeSRM is send/receive mode.
3434+ ModeSRM = Mode(C.GHOSTTY_MODE_SRM)
3535+3636+ // ModeLinefeed is linefeed/new line mode.
3737+ ModeLinefeed = Mode(C.GHOSTTY_MODE_LINEFEED)
3838+)
3939+4040+// DEC private modes.
4141+var (
4242+ // ModeDECCKM is cursor keys mode.
4343+ ModeDECCKM = Mode(C.GHOSTTY_MODE_DECCKM)
4444+4545+ // Mode132Column is 132/80 column mode.
4646+ Mode132Column = Mode(C.GHOSTTY_MODE_132_COLUMN)
4747+4848+ // ModeSlowScroll is slow scroll mode.
4949+ ModeSlowScroll = Mode(C.GHOSTTY_MODE_SLOW_SCROLL)
5050+5151+ // ModeReverseColors is reverse video mode.
5252+ ModeReverseColors = Mode(C.GHOSTTY_MODE_REVERSE_COLORS)
5353+5454+ // ModeOrigin is origin mode.
5555+ ModeOrigin = Mode(C.GHOSTTY_MODE_ORIGIN)
5656+5757+ // ModeWraparound is auto-wrap mode.
5858+ ModeWraparound = Mode(C.GHOSTTY_MODE_WRAPAROUND)
5959+6060+ // ModeAutorepeat is auto-repeat keys mode.
6161+ ModeAutorepeat = Mode(C.GHOSTTY_MODE_AUTOREPEAT)
6262+6363+ // ModeX10Mouse is X10 mouse reporting mode.
6464+ ModeX10Mouse = Mode(C.GHOSTTY_MODE_X10_MOUSE)
6565+6666+ // ModeCursorBlinking is cursor blink mode.
6767+ ModeCursorBlinking = Mode(C.GHOSTTY_MODE_CURSOR_BLINKING)
6868+6969+ // ModeCursorVisible is cursor visible mode (DECTCEM).
7070+ ModeCursorVisible = Mode(C.GHOSTTY_MODE_CURSOR_VISIBLE)
7171+7272+ // ModeEnableMode3 allows 132 column mode.
7373+ ModeEnableMode3 = Mode(C.GHOSTTY_MODE_ENABLE_MODE_3)
7474+7575+ // ModeReverseWrap is reverse wrap mode.
7676+ ModeReverseWrap = Mode(C.GHOSTTY_MODE_REVERSE_WRAP)
7777+7878+ // ModeAltScreenLegacy is alternate screen (legacy, mode 47).
7979+ ModeAltScreenLegacy = Mode(C.GHOSTTY_MODE_ALT_SCREEN_LEGACY)
8080+8181+ // ModeKeypadKeys is application keypad mode.
8282+ ModeKeypadKeys = Mode(C.GHOSTTY_MODE_KEYPAD_KEYS)
8383+8484+ // ModeLeftRightMargin is left/right margin mode.
8585+ ModeLeftRightMargin = Mode(C.GHOSTTY_MODE_LEFT_RIGHT_MARGIN)
8686+8787+ // ModeNormalMouse is normal mouse tracking mode.
8888+ ModeNormalMouse = Mode(C.GHOSTTY_MODE_NORMAL_MOUSE)
8989+9090+ // ModeButtonMouse is button-event mouse tracking mode.
9191+ ModeButtonMouse = Mode(C.GHOSTTY_MODE_BUTTON_MOUSE)
9292+9393+ // ModeAnyMouse is any-event mouse tracking mode.
9494+ ModeAnyMouse = Mode(C.GHOSTTY_MODE_ANY_MOUSE)
9595+9696+ // ModeFocusEvent enables focus in/out events.
9797+ ModeFocusEvent = Mode(C.GHOSTTY_MODE_FOCUS_EVENT)
9898+9999+ // ModeUTF8Mouse is UTF-8 mouse format mode.
100100+ ModeUTF8Mouse = Mode(C.GHOSTTY_MODE_UTF8_MOUSE)
101101+102102+ // ModeSGRMouse is SGR mouse format mode.
103103+ ModeSGRMouse = Mode(C.GHOSTTY_MODE_SGR_MOUSE)
104104+105105+ // ModeAltScroll is alternate scroll mode.
106106+ ModeAltScroll = Mode(C.GHOSTTY_MODE_ALT_SCROLL)
107107+108108+ // ModeURxvtMouse is URxvt mouse format mode.
109109+ ModeURxvtMouse = Mode(C.GHOSTTY_MODE_URXVT_MOUSE)
110110+111111+ // ModeSGRPixelsMouse is SGR-Pixels mouse format mode.
112112+ ModeSGRPixelsMouse = Mode(C.GHOSTTY_MODE_SGR_PIXELS_MOUSE)
113113+114114+ // ModeNumlockKeypad ignores keypad with NumLock.
115115+ ModeNumlockKeypad = Mode(C.GHOSTTY_MODE_NUMLOCK_KEYPAD)
116116+117117+ // ModeAltEscPrefix makes Alt key send ESC prefix.
118118+ ModeAltEscPrefix = Mode(C.GHOSTTY_MODE_ALT_ESC_PREFIX)
119119+120120+ // ModeAltSendsEsc makes Alt send escape.
121121+ ModeAltSendsEsc = Mode(C.GHOSTTY_MODE_ALT_SENDS_ESC)
122122+123123+ // ModeReverseWrapExt is extended reverse wrap mode.
124124+ ModeReverseWrapExt = Mode(C.GHOSTTY_MODE_REVERSE_WRAP_EXT)
125125+126126+ // ModeAltScreen is alternate screen mode (mode 1047).
127127+ ModeAltScreen = Mode(C.GHOSTTY_MODE_ALT_SCREEN)
128128+129129+ // ModeSaveCursor saves cursor position (DECSC, mode 1048).
130130+ ModeSaveCursor = Mode(C.GHOSTTY_MODE_SAVE_CURSOR)
131131+132132+ // ModeAltScreenSave is alt screen + save cursor + clear (mode 1049).
133133+ ModeAltScreenSave = Mode(C.GHOSTTY_MODE_ALT_SCREEN_SAVE)
134134+135135+ // ModeBracketedPaste is bracketed paste mode.
136136+ ModeBracketedPaste = Mode(C.GHOSTTY_MODE_BRACKETED_PASTE)
137137+138138+ // ModeSyncOutput is synchronized output mode.
139139+ ModeSyncOutput = Mode(C.GHOSTTY_MODE_SYNC_OUTPUT)
140140+141141+ // ModeGraphemeCluster is grapheme cluster mode.
142142+ ModeGraphemeCluster = Mode(C.GHOSTTY_MODE_GRAPHEME_CLUSTER)
143143+144144+ // ModeColorSchemeReport enables color scheme reporting.
145145+ ModeColorSchemeReport = Mode(C.GHOSTTY_MODE_COLOR_SCHEME_REPORT)
146146+147147+ // ModeInBandResize enables in-band size reports.
148148+ ModeInBandResize = Mode(C.GHOSTTY_MODE_IN_BAND_RESIZE)
149149+)
150150+151151+// ModeReportState represents DECRPM report state values (Ps2 parameter).
152152+// C: GhosttyModeReportState
153153+type ModeReportState int
154154+155155+const (
156156+ // ModeReportNotRecognized means the mode is not recognized.
157157+ ModeReportNotRecognized ModeReportState = C.GHOSTTY_MODE_REPORT_NOT_RECOGNIZED
158158+159159+ // ModeReportSet means the mode is set (enabled).
160160+ ModeReportSet ModeReportState = C.GHOSTTY_MODE_REPORT_SET
161161+162162+ // ModeReportReset means the mode is reset (disabled).
163163+ ModeReportReset ModeReportState = C.GHOSTTY_MODE_REPORT_RESET
164164+165165+ // ModeReportPermanentlySet means the mode is permanently set.
166166+ ModeReportPermanentlySet ModeReportState = C.GHOSTTY_MODE_REPORT_PERMANENTLY_SET
167167+168168+ // ModeReportPermanentlyReset means the mode is permanently reset.
169169+ ModeReportPermanentlyReset ModeReportState = C.GHOSTTY_MODE_REPORT_PERMANENTLY_RESET
170170+)
+23
mode_test.go
···11+package libghostty
22+33+import "testing"
44+55+func TestModeValueANSI(t *testing.T) {
66+ m := ModeInsert
77+ if m.Value() != 4 {
88+ t.Fatalf("expected value 4, got %d", m.Value())
99+ }
1010+ if !m.ANSI() {
1111+ t.Fatal("expected ANSI mode")
1212+ }
1313+}
1414+1515+func TestModeValueDEC(t *testing.T) {
1616+ m := ModeCursorVisible
1717+ if m.Value() != 25 {
1818+ t.Fatalf("expected value 25, got %d", m.Value())
1919+ }
2020+ if m.ANSI() {
2121+ t.Fatal("expected DEC private mode")
2222+ }
2323+}
+51
point.go
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+*/
66+import "C"
77+88+import "unsafe"
99+1010+// PointTag determines which coordinate system a point uses.
1111+// C: GhosttyPointTag
1212+type PointTag int
1313+1414+const (
1515+ // PointTagActive references the active area where the cursor can move.
1616+ PointTagActive PointTag = C.GHOSTTY_POINT_TAG_ACTIVE
1717+1818+ // PointTagViewport references the visible viewport (changes when scrolled).
1919+ PointTagViewport PointTag = C.GHOSTTY_POINT_TAG_VIEWPORT
2020+2121+ // PointTagScreen references the full screen including scrollback.
2222+ PointTagScreen PointTag = C.GHOSTTY_POINT_TAG_SCREEN
2323+2424+ // PointTagHistory references scrollback history only (before active area).
2525+ PointTagHistory PointTag = C.GHOSTTY_POINT_TAG_HISTORY
2626+)
2727+2828+// Point is a tagged position in the terminal grid. The Tag determines
2929+// which coordinate system X and Y refer to.
3030+// C: GhosttyPoint
3131+type Point struct {
3232+ // Tag determines the coordinate system.
3333+ Tag PointTag
3434+3535+ // X is the column (0-indexed).
3636+ X uint16
3737+3838+ // Y is the row (0-indexed). May exceed page size for screen/history tags.
3939+ Y uint32
4040+}
4141+4242+// toC converts a Go Point to a C GhosttyPoint.
4343+func (p Point) toC() C.GhosttyPoint {
4444+ var cp C.GhosttyPoint
4545+ cp.tag = C.GhosttyPointTag(p.Tag)
4646+ // Set the coordinate in the value union.
4747+ coord := (*C.GhosttyPointCoordinate)(unsafe.Pointer(&cp.value[0]))
4848+ coord.x = C.uint16_t(p.X)
4949+ coord.y = C.uint32_t(p.Y)
5050+ return cp
5151+}
+103
render_state.go
···11+package libghostty
22+33+// Render state for creating high-performance renderers.
44+// Wraps the GhosttyRenderState C APIs (excluding row/cell iterators).
55+66+/*
77+#include <ghostty/vt.h>
88+*/
99+import "C"
1010+1111+// RenderState holds the state required to render a visible screen
1212+// (viewport) of a terminal instance. It is stateful and optimized
1313+// for repeated updates from a single terminal, only updating dirty
1414+// regions of the screen.
1515+//
1616+// Basic usage:
1717+// 1. Create an empty render state with NewRenderState.
1818+// 2. Update it from a terminal via Update whenever needed.
1919+// 3. Read from the render state to get data for drawing.
2020+//
2121+// C: GhosttyRenderState
2222+type RenderState struct {
2323+ ptr C.GhosttyRenderState
2424+}
2525+2626+// RenderStateDirty describes the dirty state after an update.
2727+// C: GhosttyRenderStateDirty
2828+type RenderStateDirty int
2929+3030+const (
3131+ // RenderStateDirtyFalse means not dirty; rendering can be skipped.
3232+ RenderStateDirtyFalse RenderStateDirty = C.GHOSTTY_RENDER_STATE_DIRTY_FALSE
3333+3434+ // RenderStateDirtyPartial means some rows changed; renderer can
3535+ // redraw incrementally.
3636+ RenderStateDirtyPartial RenderStateDirty = C.GHOSTTY_RENDER_STATE_DIRTY_PARTIAL
3737+3838+ // RenderStateDirtyFull means global state changed; renderer should
3939+ // redraw everything.
4040+ RenderStateDirtyFull RenderStateDirty = C.GHOSTTY_RENDER_STATE_DIRTY_FULL
4141+)
4242+4343+// CursorVisualStyle describes the visual style of the cursor.
4444+// C: GhosttyRenderStateCursorVisualStyle
4545+type CursorVisualStyle int
4646+4747+const (
4848+ // CursorVisualStyleBar is a bar cursor (DECSCUSR 5, 6).
4949+ CursorVisualStyleBar CursorVisualStyle = C.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR
5050+5151+ // CursorVisualStyleBlock is a block cursor (DECSCUSR 1, 2).
5252+ CursorVisualStyleBlock CursorVisualStyle = C.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK
5353+5454+ // CursorVisualStyleUnderline is an underline cursor (DECSCUSR 3, 4).
5555+ CursorVisualStyleUnderline CursorVisualStyle = C.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE
5656+5757+ // CursorVisualStyleBlockHollow is a hollow block cursor.
5858+ CursorVisualStyleBlockHollow CursorVisualStyle = C.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW
5959+)
6060+6161+// RenderStateColors holds all color information from a render state,
6262+// retrieved in a single call via the sized-struct API.
6363+// C: GhosttyRenderStateColors
6464+type RenderStateColors struct {
6565+ // Background is the default/current background color.
6666+ Background ColorRGB
6767+6868+ // Foreground is the default/current foreground color.
6969+ Foreground ColorRGB
7070+7171+ // Cursor is the cursor color when explicitly set by terminal state.
7272+ // Only valid when CursorHasValue is true.
7373+ Cursor ColorRGB
7474+7575+ // CursorHasValue is true when Cursor contains a valid explicit
7676+ // cursor color value.
7777+ CursorHasValue bool
7878+7979+ // Palette is the active 256-color palette.
8080+ Palette Palette
8181+}
8282+8383+// NewRenderState creates a new empty render state.
8484+func NewRenderState() (*RenderState, error) {
8585+ var ptr C.GhosttyRenderState
8686+ if err := resultError(C.ghostty_render_state_new(nil, &ptr)); err != nil {
8787+ return nil, err
8888+ }
8989+ return &RenderState{ptr: ptr}, nil
9090+}
9191+9292+// Close frees the underlying render state handle. After this call,
9393+// the render state must not be used.
9494+func (rs *RenderState) Close() {
9595+ C.ghostty_render_state_free(rs.ptr)
9696+}
9797+9898+// Update updates the render state from a terminal instance. This
9999+// consumes terminal/screen dirty state. The terminal must not be
100100+// used concurrently during this call.
101101+func (rs *RenderState) Update(t *Terminal) error {
102102+ return resultError(C.ghostty_render_state_update(rs.ptr, t.ptr))
103103+}
+133
render_state_cell.go
···11+package libghostty
22+33+// Render-state row cell iterator wrapping the
44+// GhosttyRenderStateRowCells C APIs.
55+66+/*
77+#include <ghostty/vt.h>
88+*/
99+import "C"
1010+1111+import (
1212+ "errors"
1313+ "unsafe"
1414+)
1515+1616+// RenderStateRowCells iterates over cells in a render-state row.
1717+// Create one with NewRenderStateRowCells, populate it via
1818+// RenderStateRowIterator.Cells, then advance with Next (or jump
1919+// with Select) and read data with getter methods.
2020+//
2121+// A single instance can be reused across rows to avoid repeated
2222+// allocation. Cell data is only valid until the next call to
2323+// RenderState.Update.
2424+//
2525+// C: GhosttyRenderStateRowCells
2626+type RenderStateRowCells struct {
2727+ ptr C.GhosttyRenderStateRowCells
2828+}
2929+3030+// NewRenderStateRowCells creates a new row cells instance. The
3131+// instance is empty until populated via RenderStateRowIterator.Cells.
3232+func NewRenderStateRowCells() (*RenderStateRowCells, error) {
3333+ var ptr C.GhosttyRenderStateRowCells
3434+ if err := resultError(C.ghostty_render_state_row_cells_new(nil, &ptr)); err != nil {
3535+ return nil, err
3636+ }
3737+ return &RenderStateRowCells{ptr: ptr}, nil
3838+}
3939+4040+// Close frees the underlying row cells handle. After this call,
4141+// the instance must not be used.
4242+func (rc *RenderStateRowCells) Close() {
4343+ C.ghostty_render_state_row_cells_free(rc.ptr)
4444+}
4545+4646+// Next advances the iterator to the next cell. Returns true if the
4747+// iterator moved successfully and cell data is available. Returns
4848+// false when there are no more cells.
4949+func (rc *RenderStateRowCells) Next() bool {
5050+ return bool(C.ghostty_render_state_row_cells_next(rc.ptr))
5151+}
5252+5353+// Select positions the iterator at the given column index (0-based)
5454+// so that subsequent reads return data for that cell.
5555+func (rc *RenderStateRowCells) Select(x uint16) error {
5656+ return resultError(C.ghostty_render_state_row_cells_select(rc.ptr, C.uint16_t(x)))
5757+}
5858+5959+// Raw returns the raw Cell value for the current iterator position.
6060+// The returned Cell can be used with the same getter methods as cells
6161+// obtained from GridRef.
6262+func (rc *RenderStateRowCells) Raw() (*Cell, error) {
6363+ var v C.GhosttyCell
6464+ if err := resultError(C.ghostty_render_state_row_cells_get(rc.ptr, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, unsafe.Pointer(&v))); err != nil {
6565+ return nil, err
6666+ }
6767+ return &Cell{c: v}, nil
6868+}
6969+7070+// Style returns the style for the current cell.
7171+func (rc *RenderStateRowCells) Style() (*Style, error) {
7272+ cs := initCStyle()
7373+ if err := resultError(C.ghostty_render_state_row_cells_get(rc.ptr, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_STYLE, unsafe.Pointer(&cs))); err != nil {
7474+ return nil, err
7575+ }
7676+ return &Style{c: cs}, nil
7777+}
7878+7979+// Graphemes returns the full grapheme cluster codepoints for the
8080+// current cell. The base codepoint is first, followed by any extra
8181+// codepoints. Returns nil if the cell has no text.
8282+func (rc *RenderStateRowCells) Graphemes() ([]uint32, error) {
8383+ // Get the number of codepoints.
8484+ var n C.uint32_t
8585+ if err := resultError(C.ghostty_render_state_row_cells_get(rc.ptr, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, unsafe.Pointer(&n))); err != nil {
8686+ return nil, err
8787+ }
8888+ if n == 0 {
8989+ return nil, nil
9090+ }
9191+9292+ // Read codepoints into a buffer.
9393+ buf := make([]uint32, uint32(n))
9494+ if err := resultError(C.ghostty_render_state_row_cells_get(rc.ptr, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, unsafe.Pointer(&buf[0]))); err != nil {
9595+ return nil, err
9696+ }
9797+ return buf, nil
9898+}
9999+100100+// BgColor returns the resolved background color for the current cell.
101101+// Returns nil (without error) when the cell has no background color,
102102+// in which case the caller should use the terminal default background.
103103+func (rc *RenderStateRowCells) BgColor() (*ColorRGB, error) {
104104+ var v C.GhosttyColorRgb
105105+ err := resultError(C.ghostty_render_state_row_cells_get(rc.ptr, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, unsafe.Pointer(&v)))
106106+ if err != nil {
107107+ var ge *Error
108108+ if errors.As(err, &ge) && ge.Result == ResultInvalidValue {
109109+ return nil, nil
110110+ }
111111+ return nil, err
112112+ }
113113+ c := ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}
114114+ return &c, nil
115115+}
116116+117117+// FgColor returns the resolved foreground color for the current cell.
118118+// Returns nil (without error) when the cell has no explicit foreground
119119+// color, in which case the caller should use the terminal default
120120+// foreground. Bold color handling is not applied.
121121+func (rc *RenderStateRowCells) FgColor() (*ColorRGB, error) {
122122+ var v C.GhosttyColorRgb
123123+ err := resultError(C.ghostty_render_state_row_cells_get(rc.ptr, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, unsafe.Pointer(&v)))
124124+ if err != nil {
125125+ var ge *Error
126126+ if errors.As(err, &ge) && ge.Result == ResultInvalidValue {
127127+ return nil, nil
128128+ }
129129+ return nil, err
130130+ }
131131+ c := ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}
132132+ return &c, nil
133133+}
+334
render_state_cell_test.go
···11+package libghostty
22+33+import "testing"
44+55+func TestRenderStateRowCells(t *testing.T) {
66+ term, err := NewTerminal(WithSize(80, 24))
77+ if err != nil {
88+ t.Fatal(err)
99+ }
1010+ defer term.Close()
1111+1212+ term.VTWrite([]byte("hello"))
1313+1414+ rs, err := NewRenderState()
1515+ if err != nil {
1616+ t.Fatal(err)
1717+ }
1818+ defer rs.Close()
1919+2020+ if err := rs.Update(term); err != nil {
2121+ t.Fatal(err)
2222+ }
2323+2424+ ri, err := NewRenderStateRowIterator()
2525+ if err != nil {
2626+ t.Fatal(err)
2727+ }
2828+ defer ri.Close()
2929+3030+ if err := rs.RowIterator(ri); err != nil {
3131+ t.Fatal(err)
3232+ }
3333+3434+ // Advance to first row.
3535+ if !ri.Next() {
3636+ t.Fatal("expected at least one row")
3737+ }
3838+3939+ rc, err := NewRenderStateRowCells()
4040+ if err != nil {
4141+ t.Fatal(err)
4242+ }
4343+ defer rc.Close()
4444+4545+ if err := ri.Cells(rc); err != nil {
4646+ t.Fatal(err)
4747+ }
4848+4949+ // Iterate cells and count them.
5050+ count := 0
5151+ for rc.Next() {
5252+ count++
5353+ }
5454+ if count != 80 {
5555+ t.Fatalf("expected 80 cells, got %d", count)
5656+ }
5757+}
5858+5959+func TestRenderStateRowCellsSelect(t *testing.T) {
6060+ term, err := NewTerminal(WithSize(80, 24))
6161+ if err != nil {
6262+ t.Fatal(err)
6363+ }
6464+ defer term.Close()
6565+6666+ term.VTWrite([]byte("ABCDE"))
6767+6868+ rs, err := NewRenderState()
6969+ if err != nil {
7070+ t.Fatal(err)
7171+ }
7272+ defer rs.Close()
7373+7474+ if err := rs.Update(term); err != nil {
7575+ t.Fatal(err)
7676+ }
7777+7878+ ri, err := NewRenderStateRowIterator()
7979+ if err != nil {
8080+ t.Fatal(err)
8181+ }
8282+ defer ri.Close()
8383+8484+ if err := rs.RowIterator(ri); err != nil {
8585+ t.Fatal(err)
8686+ }
8787+8888+ if !ri.Next() {
8989+ t.Fatal("expected at least one row")
9090+ }
9191+9292+ rc, err := NewRenderStateRowCells()
9393+ if err != nil {
9494+ t.Fatal(err)
9595+ }
9696+ defer rc.Close()
9797+9898+ if err := ri.Cells(rc); err != nil {
9999+ t.Fatal(err)
100100+ }
101101+102102+ // Select column 2 (should be 'C').
103103+ if err := rc.Select(2); err != nil {
104104+ t.Fatal(err)
105105+ }
106106+107107+ graphemes, err := rc.Graphemes()
108108+ if err != nil {
109109+ t.Fatal(err)
110110+ }
111111+ if len(graphemes) != 1 || graphemes[0] != 'C' {
112112+ t.Fatalf("expected ['C'], got %v", graphemes)
113113+ }
114114+}
115115+116116+func TestRenderStateRowCellsGraphemes(t *testing.T) {
117117+ term, err := NewTerminal(WithSize(80, 24))
118118+ if err != nil {
119119+ t.Fatal(err)
120120+ }
121121+ defer term.Close()
122122+123123+ term.VTWrite([]byte("AB"))
124124+125125+ rs, err := NewRenderState()
126126+ if err != nil {
127127+ t.Fatal(err)
128128+ }
129129+ defer rs.Close()
130130+131131+ if err := rs.Update(term); err != nil {
132132+ t.Fatal(err)
133133+ }
134134+135135+ ri, err := NewRenderStateRowIterator()
136136+ if err != nil {
137137+ t.Fatal(err)
138138+ }
139139+ defer ri.Close()
140140+141141+ if err := rs.RowIterator(ri); err != nil {
142142+ t.Fatal(err)
143143+ }
144144+145145+ if !ri.Next() {
146146+ t.Fatal("expected at least one row")
147147+ }
148148+149149+ rc, err := NewRenderStateRowCells()
150150+ if err != nil {
151151+ t.Fatal(err)
152152+ }
153153+ defer rc.Close()
154154+155155+ if err := ri.Cells(rc); err != nil {
156156+ t.Fatal(err)
157157+ }
158158+159159+ // First cell should be 'A'.
160160+ if !rc.Next() {
161161+ t.Fatal("expected at least one cell")
162162+ }
163163+ graphemes, err := rc.Graphemes()
164164+ if err != nil {
165165+ t.Fatal(err)
166166+ }
167167+ if len(graphemes) != 1 || graphemes[0] != 'A' {
168168+ t.Fatalf("expected ['A'], got %v", graphemes)
169169+ }
170170+171171+ // Second cell should be 'B'.
172172+ if !rc.Next() {
173173+ t.Fatal("expected second cell")
174174+ }
175175+ graphemes, err = rc.Graphemes()
176176+ if err != nil {
177177+ t.Fatal(err)
178178+ }
179179+ if len(graphemes) != 1 || graphemes[0] != 'B' {
180180+ t.Fatalf("expected ['B'], got %v", graphemes)
181181+ }
182182+183183+ // Empty cell should return nil.
184184+ if !rc.Next() {
185185+ t.Fatal("expected third cell")
186186+ }
187187+ graphemes, err = rc.Graphemes()
188188+ if err != nil {
189189+ t.Fatal(err)
190190+ }
191191+ if graphemes != nil {
192192+ t.Fatalf("expected nil graphemes for empty cell, got %v", graphemes)
193193+ }
194194+}
195195+196196+func TestRenderStateRowCellsStyle(t *testing.T) {
197197+ term, err := NewTerminal(WithSize(80, 24))
198198+ if err != nil {
199199+ t.Fatal(err)
200200+ }
201201+ defer term.Close()
202202+203203+ // Write bold text.
204204+ term.VTWrite([]byte("\x1b[1mX"))
205205+206206+ rs, err := NewRenderState()
207207+ if err != nil {
208208+ t.Fatal(err)
209209+ }
210210+ defer rs.Close()
211211+212212+ if err := rs.Update(term); err != nil {
213213+ t.Fatal(err)
214214+ }
215215+216216+ ri, err := NewRenderStateRowIterator()
217217+ if err != nil {
218218+ t.Fatal(err)
219219+ }
220220+ defer ri.Close()
221221+222222+ if err := rs.RowIterator(ri); err != nil {
223223+ t.Fatal(err)
224224+ }
225225+226226+ if !ri.Next() {
227227+ t.Fatal("expected at least one row")
228228+ }
229229+230230+ rc, err := NewRenderStateRowCells()
231231+ if err != nil {
232232+ t.Fatal(err)
233233+ }
234234+ defer rc.Close()
235235+236236+ if err := ri.Cells(rc); err != nil {
237237+ t.Fatal(err)
238238+ }
239239+240240+ if !rc.Next() {
241241+ t.Fatal("expected at least one cell")
242242+ }
243243+244244+ style, err := rc.Style()
245245+ if err != nil {
246246+ t.Fatal(err)
247247+ }
248248+ if !style.Bold() {
249249+ t.Fatal("expected bold style")
250250+ }
251251+}
252252+253253+func TestRenderStateRowCellsColors(t *testing.T) {
254254+ term, err := NewTerminal(WithSize(80, 24))
255255+ if err != nil {
256256+ t.Fatal(err)
257257+ }
258258+ defer term.Close()
259259+260260+ // Write text with explicit fg/bg colors (SGR 38;2;R;G;B and 48;2;R;G;B).
261261+ term.VTWrite([]byte("\x1b[38;2;255;0;0;48;2;0;0;255mX"))
262262+263263+ rs, err := NewRenderState()
264264+ if err != nil {
265265+ t.Fatal(err)
266266+ }
267267+ defer rs.Close()
268268+269269+ if err := rs.Update(term); err != nil {
270270+ t.Fatal(err)
271271+ }
272272+273273+ ri, err := NewRenderStateRowIterator()
274274+ if err != nil {
275275+ t.Fatal(err)
276276+ }
277277+ defer ri.Close()
278278+279279+ if err := rs.RowIterator(ri); err != nil {
280280+ t.Fatal(err)
281281+ }
282282+283283+ if !ri.Next() {
284284+ t.Fatal("expected at least one row")
285285+ }
286286+287287+ rc, err := NewRenderStateRowCells()
288288+ if err != nil {
289289+ t.Fatal(err)
290290+ }
291291+ defer rc.Close()
292292+293293+ if err := ri.Cells(rc); err != nil {
294294+ t.Fatal(err)
295295+ }
296296+297297+ if !rc.Next() {
298298+ t.Fatal("expected at least one cell")
299299+ }
300300+301301+ fg, err := rc.FgColor()
302302+ if err != nil {
303303+ t.Fatal(err)
304304+ }
305305+ if fg == nil {
306306+ t.Fatal("expected non-nil fg color")
307307+ }
308308+ if *fg != (ColorRGB{R: 255, G: 0, B: 0}) {
309309+ t.Fatalf("expected red fg, got %+v", *fg)
310310+ }
311311+312312+ bg, err := rc.BgColor()
313313+ if err != nil {
314314+ t.Fatal(err)
315315+ }
316316+ if bg == nil {
317317+ t.Fatal("expected non-nil bg color")
318318+ }
319319+ if *bg != (ColorRGB{R: 0, G: 0, B: 255}) {
320320+ t.Fatalf("expected blue bg, got %+v", *bg)
321321+ }
322322+323323+ // Empty cell should have nil colors (use default).
324324+ if !rc.Next() {
325325+ t.Fatal("expected second cell")
326326+ }
327327+ fg, err = rc.FgColor()
328328+ if err != nil {
329329+ t.Fatal(err)
330330+ }
331331+ if fg != nil {
332332+ t.Fatalf("expected nil fg for unstyled cell, got %+v", *fg)
333333+ }
334334+}
+226
render_state_data.go
···11+package libghostty
22+33+// Render state data getters and setters wrapping
44+// ghostty_render_state_get() and ghostty_render_state_set().
55+// Functions are ordered alphabetically.
66+77+/*
88+#include <ghostty/vt.h>
99+1010+// Helper to create a properly initialized GhosttyRenderStateColors (sized struct).
1111+static inline GhosttyRenderStateColors init_render_state_colors() {
1212+ GhosttyRenderStateColors c = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);
1313+ return c;
1414+}
1515+*/
1616+import "C"
1717+1818+import (
1919+ "errors"
2020+ "unsafe"
2121+)
2222+2323+// Cols returns the viewport width in cells.
2424+func (rs *RenderState) Cols() (uint16, error) {
2525+ var v C.uint16_t
2626+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLS, unsafe.Pointer(&v))); err != nil {
2727+ return 0, err
2828+ }
2929+ return uint16(v), nil
3030+}
3131+3232+// ColorBackground returns the default/current background color.
3333+func (rs *RenderState) ColorBackground() (ColorRGB, error) {
3434+ var v C.GhosttyColorRgb
3535+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_BACKGROUND, unsafe.Pointer(&v))); err != nil {
3636+ return ColorRGB{}, err
3737+ }
3838+ return ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}, nil
3939+}
4040+4141+// ColorCursor returns the cursor color when explicitly set by terminal
4242+// state. Returns nil (without error) when no explicit cursor color is set.
4343+func (rs *RenderState) ColorCursor() (*ColorRGB, error) {
4444+ // Check whether a cursor color is set first.
4545+ var has C.bool
4646+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE, unsafe.Pointer(&has))); err != nil {
4747+ return nil, err
4848+ }
4949+ if !bool(has) {
5050+ return nil, nil
5151+ }
5252+5353+ var v C.GhosttyColorRgb
5454+ err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR, unsafe.Pointer(&v)))
5555+ if err != nil {
5656+ var ge *Error
5757+ if errors.As(err, &ge) && ge.Result == ResultInvalidValue {
5858+ return nil, nil
5959+ }
6060+ return nil, err
6161+ }
6262+ c := ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}
6363+ return &c, nil
6464+}
6565+6666+// ColorForeground returns the default/current foreground color.
6767+func (rs *RenderState) ColorForeground() (ColorRGB, error) {
6868+ var v C.GhosttyColorRgb
6969+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_FOREGROUND, unsafe.Pointer(&v))); err != nil {
7070+ return ColorRGB{}, err
7171+ }
7272+ return ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}, nil
7373+}
7474+7575+// ColorPalette returns the active 256-color palette.
7676+func (rs *RenderState) ColorPalette() (*Palette, error) {
7777+ var cp [PaletteSize]C.GhosttyColorRgb
7878+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE, unsafe.Pointer(&cp[0]))); err != nil {
7979+ return nil, err
8080+ }
8181+ var p Palette
8282+ for i, c := range cp {
8383+ p[i] = ColorRGB{R: uint8(c.r), G: uint8(c.g), B: uint8(c.b)}
8484+ }
8585+ return &p, nil
8686+}
8787+8888+// Colors returns all color information from the render state in a
8989+// single call using the sized-struct API.
9090+func (rs *RenderState) Colors() (*RenderStateColors, error) {
9191+ cc := C.init_render_state_colors()
9292+ if err := resultError(C.ghostty_render_state_colors_get(rs.ptr, &cc)); err != nil {
9393+ return nil, err
9494+ }
9595+9696+ result := &RenderStateColors{
9797+ Background: ColorRGB{R: uint8(cc.background.r), G: uint8(cc.background.g), B: uint8(cc.background.b)},
9898+ Foreground: ColorRGB{R: uint8(cc.foreground.r), G: uint8(cc.foreground.g), B: uint8(cc.foreground.b)},
9999+ Cursor: ColorRGB{R: uint8(cc.cursor.r), G: uint8(cc.cursor.g), B: uint8(cc.cursor.b)},
100100+ CursorHasValue: bool(cc.cursor_has_value),
101101+ }
102102+103103+ for i, c := range cc.palette {
104104+ result.Palette[i] = ColorRGB{R: uint8(c.r), G: uint8(c.g), B: uint8(c.b)}
105105+ }
106106+ return result, nil
107107+}
108108+109109+// CursorBlinking reports whether the cursor should blink based on
110110+// terminal modes.
111111+func (rs *RenderState) CursorBlinking() (bool, error) {
112112+ var v C.bool
113113+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING, unsafe.Pointer(&v))); err != nil {
114114+ return false, err
115115+ }
116116+ return bool(v), nil
117117+}
118118+119119+// CursorPasswordInput reports whether the cursor is at a password
120120+// input field.
121121+func (rs *RenderState) CursorPasswordInput() (bool, error) {
122122+ var v C.bool
123123+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT, unsafe.Pointer(&v))); err != nil {
124124+ return false, err
125125+ }
126126+ return bool(v), nil
127127+}
128128+129129+// CursorVisible reports whether the cursor is visible based on
130130+// terminal modes.
131131+func (rs *RenderState) CursorVisible() (bool, error) {
132132+ var v C.bool
133133+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, unsafe.Pointer(&v))); err != nil {
134134+ return false, err
135135+ }
136136+ return bool(v), nil
137137+}
138138+139139+// CursorVisualStyle returns the visual style of the cursor.
140140+func (rs *RenderState) CursorVisualStyle() (CursorVisualStyle, error) {
141141+ var v C.GhosttyRenderStateCursorVisualStyle
142142+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, unsafe.Pointer(&v))); err != nil {
143143+ return 0, err
144144+ }
145145+ return CursorVisualStyle(v), nil
146146+}
147147+148148+// CursorViewportHasValue reports whether the cursor is visible within
149149+// the viewport. If false, the cursor viewport position values are
150150+// undefined.
151151+func (rs *RenderState) CursorViewportHasValue() (bool, error) {
152152+ var v C.bool
153153+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, unsafe.Pointer(&v))); err != nil {
154154+ return false, err
155155+ }
156156+ return bool(v), nil
157157+}
158158+159159+// CursorViewportWideTail reports whether the cursor is on the tail
160160+// of a wide character. Only valid when CursorViewportHasValue
161161+// returns true.
162162+func (rs *RenderState) CursorViewportWideTail() (bool, error) {
163163+ var v C.bool
164164+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL, unsafe.Pointer(&v))); err != nil {
165165+ return false, err
166166+ }
167167+ return bool(v), nil
168168+}
169169+170170+// CursorViewportX returns the cursor viewport x position in cells.
171171+// Only valid when CursorViewportHasValue returns true.
172172+func (rs *RenderState) CursorViewportX() (uint16, error) {
173173+ var v C.uint16_t
174174+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, unsafe.Pointer(&v))); err != nil {
175175+ return 0, err
176176+ }
177177+ return uint16(v), nil
178178+}
179179+180180+// CursorViewportY returns the cursor viewport y position in cells.
181181+// Only valid when CursorViewportHasValue returns true.
182182+func (rs *RenderState) CursorViewportY() (uint16, error) {
183183+ var v C.uint16_t
184184+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, unsafe.Pointer(&v))); err != nil {
185185+ return 0, err
186186+ }
187187+ return uint16(v), nil
188188+}
189189+190190+// Dirty returns the current dirty state.
191191+func (rs *RenderState) Dirty() (RenderStateDirty, error) {
192192+ var v C.GhosttyRenderStateDirty
193193+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_DIRTY, unsafe.Pointer(&v))); err != nil {
194194+ return 0, err
195195+ }
196196+ return RenderStateDirty(v), nil
197197+}
198198+199199+// RowIterator populates a pre-allocated row iterator with row data
200200+// from the render state. The iterator can then be advanced with Next
201201+// and queried with getter methods.
202202+//
203203+// The iterator can be reused across multiple calls. Row data is only
204204+// valid until the next call to Update.
205205+func (rs *RenderState) RowIterator(ri *RenderStateRowIterator) error {
206206+ return resultError(C.ghostty_render_state_get(
207207+ rs.ptr,
208208+ C.GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR,
209209+ unsafe.Pointer(&ri.ptr),
210210+ ))
211211+}
212212+213213+// Rows returns the viewport height in cells.
214214+func (rs *RenderState) Rows() (uint16, error) {
215215+ var v C.uint16_t
216216+ if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_ROWS, unsafe.Pointer(&v))); err != nil {
217217+ return 0, err
218218+ }
219219+ return uint16(v), nil
220220+}
221221+222222+// SetDirty sets the dirty state.
223223+func (rs *RenderState) SetDirty(dirty RenderStateDirty) error {
224224+ v := C.GhosttyRenderStateDirty(dirty)
225225+ return resultError(C.ghostty_render_state_set(rs.ptr, C.GHOSTTY_RENDER_STATE_OPTION_DIRTY, unsafe.Pointer(&v)))
226226+}
+89
render_state_row.go
···11+package libghostty
22+33+// Render-state row iterator wrapping the
44+// GhosttyRenderStateRowIterator C APIs.
55+66+/*
77+#include <ghostty/vt.h>
88+*/
99+import "C"
1010+1111+import "unsafe"
1212+1313+// RenderStateRowIterator iterates over rows in a render state.
1414+// Create one with NewRenderStateRowIterator, populate it via
1515+// RenderState.RowIterator, then advance with Next and read data
1616+// with getter methods.
1717+//
1818+// Row data is only valid as long as the underlying render state
1919+// is not updated. It is unsafe to use row data after calling
2020+// RenderState.Update.
2121+//
2222+// C: GhosttyRenderStateRowIterator
2323+type RenderStateRowIterator struct {
2424+ ptr C.GhosttyRenderStateRowIterator
2525+}
2626+2727+// NewRenderStateRowIterator creates a new row iterator instance.
2828+// The iterator is empty until populated via RenderState.RowIterator.
2929+func NewRenderStateRowIterator() (*RenderStateRowIterator, error) {
3030+ var ptr C.GhosttyRenderStateRowIterator
3131+ if err := resultError(C.ghostty_render_state_row_iterator_new(nil, &ptr)); err != nil {
3232+ return nil, err
3333+ }
3434+ return &RenderStateRowIterator{ptr: ptr}, nil
3535+}
3636+3737+// Close frees the underlying row iterator handle. After this call,
3838+// the iterator must not be used.
3939+func (ri *RenderStateRowIterator) Close() {
4040+ C.ghostty_render_state_row_iterator_free(ri.ptr)
4141+}
4242+4343+// Next advances the iterator to the next row. Returns true if the
4444+// iterator moved successfully and row data is available. Returns
4545+// false when there are no more rows.
4646+func (ri *RenderStateRowIterator) Next() bool {
4747+ return bool(C.ghostty_render_state_row_iterator_next(ri.ptr))
4848+}
4949+5050+// Dirty reports whether the current row is dirty and requires a
5151+// redraw.
5252+func (ri *RenderStateRowIterator) Dirty() (bool, error) {
5353+ var v C.bool
5454+ if err := resultError(C.ghostty_render_state_row_get(ri.ptr, C.GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY, unsafe.Pointer(&v))); err != nil {
5555+ return false, err
5656+ }
5757+ return bool(v), nil
5858+}
5959+6060+// SetDirty sets the dirty state for the current row.
6161+func (ri *RenderStateRowIterator) SetDirty(dirty bool) error {
6262+ v := C.bool(dirty)
6363+ return resultError(C.ghostty_render_state_row_set(ri.ptr, C.GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY, unsafe.Pointer(&v)))
6464+}
6565+6666+// Raw returns the raw Row value for the current iterator position.
6767+// The returned Row can be used with the same getter methods as rows
6868+// obtained from GridRef.
6969+func (ri *RenderStateRowIterator) Raw() (*Row, error) {
7070+ var v C.GhosttyRow
7171+ if err := resultError(C.ghostty_render_state_row_get(ri.ptr, C.GHOSTTY_RENDER_STATE_ROW_DATA_RAW, unsafe.Pointer(&v))); err != nil {
7272+ return nil, err
7373+ }
7474+ return &Row{c: v}, nil
7575+}
7676+7777+// Cells populates a pre-allocated row cells instance with cell data
7878+// for the current row. The cells instance can then be advanced with
7979+// Next or positioned with Select.
8080+//
8181+// The cells instance can be reused across rows. Cell data is only
8282+// valid until the next call to RenderState.Update.
8383+func (ri *RenderStateRowIterator) Cells(rc *RenderStateRowCells) error {
8484+ return resultError(C.ghostty_render_state_row_get(
8585+ ri.ptr,
8686+ C.GHOSTTY_RENDER_STATE_ROW_DATA_CELLS,
8787+ unsafe.Pointer(&rc.ptr),
8888+ ))
8989+}
+138
render_state_row_test.go
···11+package libghostty
22+33+import "testing"
44+55+func TestRenderStateRowIterator(t *testing.T) {
66+ term, err := NewTerminal(WithSize(80, 24))
77+ if err != nil {
88+ t.Fatal(err)
99+ }
1010+ defer term.Close()
1111+1212+ rs, err := NewRenderState()
1313+ if err != nil {
1414+ t.Fatal(err)
1515+ }
1616+ defer rs.Close()
1717+1818+ if err := rs.Update(term); err != nil {
1919+ t.Fatal(err)
2020+ }
2121+2222+ ri, err := NewRenderStateRowIterator()
2323+ if err != nil {
2424+ t.Fatal(err)
2525+ }
2626+ defer ri.Close()
2727+2828+ if err := rs.RowIterator(ri); err != nil {
2929+ t.Fatal(err)
3030+ }
3131+3232+ // Iterate all rows and count them.
3333+ count := 0
3434+ for ri.Next() {
3535+ count++
3636+ }
3737+ if count != 24 {
3838+ t.Fatalf("expected 24 rows, got %d", count)
3939+ }
4040+}
4141+4242+func TestRenderStateRowIteratorDirty(t *testing.T) {
4343+ term, err := NewTerminal(WithSize(80, 24))
4444+ if err != nil {
4545+ t.Fatal(err)
4646+ }
4747+ defer term.Close()
4848+4949+ rs, err := NewRenderState()
5050+ if err != nil {
5151+ t.Fatal(err)
5252+ }
5353+ defer rs.Close()
5454+5555+ if err := rs.Update(term); err != nil {
5656+ t.Fatal(err)
5757+ }
5858+5959+ ri, err := NewRenderStateRowIterator()
6060+ if err != nil {
6161+ t.Fatal(err)
6262+ }
6363+ defer ri.Close()
6464+6565+ if err := rs.RowIterator(ri); err != nil {
6666+ t.Fatal(err)
6767+ }
6868+6969+ // First row after initial update should be dirty.
7070+ if !ri.Next() {
7171+ t.Fatal("expected at least one row")
7272+ }
7373+ dirty, err := ri.Dirty()
7474+ if err != nil {
7575+ t.Fatal(err)
7676+ }
7777+ if !dirty {
7878+ t.Fatal("expected first row to be dirty after initial update")
7979+ }
8080+8181+ // Clear dirty and verify.
8282+ if err := ri.SetDirty(false); err != nil {
8383+ t.Fatal(err)
8484+ }
8585+ dirty, _ = ri.Dirty()
8686+ if dirty {
8787+ t.Fatal("expected row not dirty after clearing")
8888+ }
8989+}
9090+9191+func TestRenderStateRowIteratorRaw(t *testing.T) {
9292+ term, err := NewTerminal(WithSize(80, 24))
9393+ if err != nil {
9494+ t.Fatal(err)
9595+ }
9696+ defer term.Close()
9797+9898+ // Write some text so the first row has content.
9999+ term.VTWrite([]byte("hello"))
100100+101101+ rs, err := NewRenderState()
102102+ if err != nil {
103103+ t.Fatal(err)
104104+ }
105105+ defer rs.Close()
106106+107107+ if err := rs.Update(term); err != nil {
108108+ t.Fatal(err)
109109+ }
110110+111111+ ri, err := NewRenderStateRowIterator()
112112+ if err != nil {
113113+ t.Fatal(err)
114114+ }
115115+ defer ri.Close()
116116+117117+ if err := rs.RowIterator(ri); err != nil {
118118+ t.Fatal(err)
119119+ }
120120+121121+ if !ri.Next() {
122122+ t.Fatal("expected at least one row")
123123+ }
124124+125125+ row, err := ri.Raw()
126126+ if err != nil {
127127+ t.Fatal(err)
128128+ }
129129+130130+ // The first row should not be wrapped.
131131+ wrap, err := row.Wrap()
132132+ if err != nil {
133133+ t.Fatal(err)
134134+ }
135135+ if wrap {
136136+ t.Fatal("expected first row not to be wrapped")
137137+ }
138138+}
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+*/
66+import "C"
77+88+import "fmt"
99+1010+// Result represents a Ghostty result code.
1111+//
1212+// C: GhosttyResult
1313+type Result int
1414+1515+const (
1616+ ResultSuccess Result = C.GHOSTTY_SUCCESS
1717+ ResultOutOfMemory Result = C.GHOSTTY_OUT_OF_MEMORY
1818+ ResultInvalidValue Result = C.GHOSTTY_INVALID_VALUE
1919+ ResultOutOfSpace Result = C.GHOSTTY_OUT_OF_SPACE
2020+ ResultNoValue Result = C.GHOSTTY_NO_VALUE
2121+)
2222+2323+// Error holds a non-success Ghostty result.
2424+type Error struct {
2525+ Result Result
2626+}
2727+2828+func (e *Error) Error() string {
2929+ switch e.Result {
3030+ case ResultOutOfMemory:
3131+ return "ghostty: out of memory"
3232+ case ResultInvalidValue:
3333+ return "ghostty: invalid value"
3434+ case ResultOutOfSpace:
3535+ return "ghostty: out of space"
3636+ case ResultNoValue:
3737+ return "ghostty: no value"
3838+ default:
3939+ return fmt.Sprintf("ghostty: result=%d", int(e.Result))
4040+ }
4141+}
4242+4343+// Convert a result code to an error, returning nil on success.
4444+func resultError(result C.GhosttyResult) error {
4545+ if result == C.GHOSTTY_SUCCESS {
4646+ return nil
4747+ }
4848+4949+ return &Error{Result: Result(result)}
5050+}
+50
result_test.go
···11+package libghostty
22+33+import (
44+ "errors"
55+ "testing"
66+)
77+88+func TestErrorMessage(t *testing.T) {
99+ tests := []struct {
1010+ result Result
1111+ want string
1212+ }{
1313+ {ResultOutOfMemory, "ghostty: out of memory"},
1414+ {ResultInvalidValue, "ghostty: invalid value"},
1515+ {ResultOutOfSpace, "ghostty: out of space"},
1616+ {ResultNoValue, "ghostty: no value"},
1717+ {Result(9999), "ghostty: result=9999"},
1818+ }
1919+2020+ for _, tt := range tests {
2121+ e := &Error{Result: tt.result}
2222+ if got := e.Error(); got != tt.want {
2323+ t.Errorf("Error{%d}.Error() = %q, want %q", tt.result, got, tt.want)
2424+ }
2525+ }
2626+}
2727+2828+func TestErrorAs(t *testing.T) {
2929+ var err error = &Error{Result: ResultOutOfMemory}
3030+3131+ var target *Error
3232+ if !errors.As(err, &target) {
3333+ t.Fatal("expected errors.As to succeed")
3434+ }
3535+ if target.Result != ResultOutOfMemory {
3636+ t.Fatalf("expected ResultOutOfMemory, got %d", target.Result)
3737+ }
3838+}
3939+4040+func TestErrorAsType(t *testing.T) {
4141+ var err error = &Error{Result: ResultInvalidValue}
4242+4343+ target, ok := errors.AsType[*Error](err)
4444+ if !ok {
4545+ t.Fatal("expected errors.AsType to succeed")
4646+ }
4747+ if target.Result != ResultInvalidValue {
4848+ t.Fatalf("expected ResultInvalidValue, got %d", target.Result)
4949+ }
5050+}
+272
screen.go
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+*/
66+import "C"
77+88+import "unsafe"
99+1010+// Cell is a wrapper around an opaque terminal grid cell value.
1111+// Use getter methods to extract data from it.
1212+// C: GhosttyCell
1313+type Cell struct {
1414+ c C.GhosttyCell
1515+}
1616+1717+// Row is a wrapper around an opaque terminal grid row value.
1818+// Use getter methods to extract data from it.
1919+// C: GhosttyRow
2020+type Row struct {
2121+ c C.GhosttyRow
2222+}
2323+2424+// CellContentTag describes what kind of content a cell holds.
2525+// C: GhosttyCellContentTag
2626+type CellContentTag int
2727+2828+const (
2929+ // CellContentCodepoint means a single codepoint (may be zero for empty).
3030+ CellContentCodepoint CellContentTag = C.GHOSTTY_CELL_CONTENT_CODEPOINT
3131+3232+ // CellContentCodepointGrapheme means a codepoint that is part of a
3333+ // multi-codepoint grapheme cluster.
3434+ CellContentCodepointGrapheme CellContentTag = C.GHOSTTY_CELL_CONTENT_CODEPOINT_GRAPHEME
3535+3636+ // CellContentBgColorPalette means no text; background color from palette.
3737+ CellContentBgColorPalette CellContentTag = C.GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE
3838+3939+ // CellContentBgColorRGB means no text; background color as RGB.
4040+ CellContentBgColorRGB CellContentTag = C.GHOSTTY_CELL_CONTENT_BG_COLOR_RGB
4141+)
4242+4343+// CellWide describes the width behavior of a cell.
4444+// C: GhosttyCellWide
4545+type CellWide int
4646+4747+const (
4848+ // CellWideNarrow means not a wide character, cell width 1.
4949+ CellWideNarrow CellWide = C.GHOSTTY_CELL_WIDE_NARROW
5050+5151+ // CellWideWide means wide character, cell width 2.
5252+ CellWideWide CellWide = C.GHOSTTY_CELL_WIDE_WIDE
5353+5454+ // CellWideSpacerTail means spacer after wide character (do not render).
5555+ CellWideSpacerTail CellWide = C.GHOSTTY_CELL_WIDE_SPACER_TAIL
5656+5757+ // CellWideSpacerHead means spacer at end of soft-wrapped line for a
5858+ // wide character.
5959+ CellWideSpacerHead CellWide = C.GHOSTTY_CELL_WIDE_SPACER_HEAD
6060+)
6161+6262+// CellSemanticContent is the semantic content type of a cell,
6363+// as set by OSC 133 sequences.
6464+// C: GhosttyCellSemanticContent
6565+type CellSemanticContent int
6666+6767+const (
6868+ // CellSemanticOutput means regular output content (e.g. command output).
6969+ CellSemanticOutput CellSemanticContent = C.GHOSTTY_CELL_SEMANTIC_OUTPUT
7070+7171+ // CellSemanticInput means content that is part of user input.
7272+ CellSemanticInput CellSemanticContent = C.GHOSTTY_CELL_SEMANTIC_INPUT
7373+7474+ // CellSemanticPrompt means content that is part of a shell prompt.
7575+ CellSemanticPrompt CellSemanticContent = C.GHOSTTY_CELL_SEMANTIC_PROMPT
7676+)
7777+7878+// RowSemanticPrompt indicates whether any cells in a row are part of
7979+// a shell prompt, as reported by OSC 133 sequences.
8080+// C: GhosttyRowSemanticPrompt
8181+type RowSemanticPrompt int
8282+8383+const (
8484+ // RowSemanticNone means no prompt cells in this row.
8585+ RowSemanticNone RowSemanticPrompt = C.GHOSTTY_ROW_SEMANTIC_NONE
8686+8787+ // RowSemanticPromptPrimary means prompt cells exist and this is a
8888+ // primary prompt line.
8989+ RowSemanticPromptPrimary RowSemanticPrompt = C.GHOSTTY_ROW_SEMANTIC_PROMPT
9090+9191+ // RowSemanticPromptContinuation means prompt cells exist and this is
9292+ // a continuation line.
9393+ RowSemanticPromptContinuation RowSemanticPrompt = C.GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION
9494+)
9595+9696+// Codepoint returns the codepoint of the cell (0 if empty).
9797+func (c *Cell) Codepoint() (uint32, error) {
9898+ var v C.uint32_t
9999+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_CODEPOINT, unsafe.Pointer(&v))); err != nil {
100100+ return 0, err
101101+ }
102102+ return uint32(v), nil
103103+}
104104+105105+// ContentTag returns the content tag describing what kind of content
106106+// the cell holds.
107107+func (c *Cell) ContentTag() (CellContentTag, error) {
108108+ var v C.GhosttyCellContentTag
109109+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_CONTENT_TAG, unsafe.Pointer(&v))); err != nil {
110110+ return 0, err
111111+ }
112112+ return CellContentTag(v), nil
113113+}
114114+115115+// Wide returns the wide property of the cell.
116116+func (c *Cell) Wide() (CellWide, error) {
117117+ var v C.GhosttyCellWide
118118+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_WIDE, unsafe.Pointer(&v))); err != nil {
119119+ return 0, err
120120+ }
121121+ return CellWide(v), nil
122122+}
123123+124124+// HasText reports whether the cell has text to render.
125125+func (c *Cell) HasText() (bool, error) {
126126+ var v C.bool
127127+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_HAS_TEXT, unsafe.Pointer(&v))); err != nil {
128128+ return false, err
129129+ }
130130+ return bool(v), nil
131131+}
132132+133133+// HasStyling reports whether the cell has non-default styling.
134134+func (c *Cell) HasStyling() (bool, error) {
135135+ var v C.bool
136136+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_HAS_STYLING, unsafe.Pointer(&v))); err != nil {
137137+ return false, err
138138+ }
139139+ return bool(v), nil
140140+}
141141+142142+// StyleID returns the style ID for the cell.
143143+func (c *Cell) StyleID() (uint16, error) {
144144+ var v C.uint16_t
145145+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_STYLE_ID, unsafe.Pointer(&v))); err != nil {
146146+ return 0, err
147147+ }
148148+ return uint16(v), nil
149149+}
150150+151151+// HasHyperlink reports whether the cell has a hyperlink.
152152+func (c *Cell) HasHyperlink() (bool, error) {
153153+ var v C.bool
154154+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_HAS_HYPERLINK, unsafe.Pointer(&v))); err != nil {
155155+ return false, err
156156+ }
157157+ return bool(v), nil
158158+}
159159+160160+// Protected reports whether the cell is protected.
161161+func (c *Cell) Protected() (bool, error) {
162162+ var v C.bool
163163+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_PROTECTED, unsafe.Pointer(&v))); err != nil {
164164+ return false, err
165165+ }
166166+ return bool(v), nil
167167+}
168168+169169+// Semantic returns the semantic content type of the cell.
170170+func (c *Cell) Semantic() (CellSemanticContent, error) {
171171+ var v C.GhosttyCellSemanticContent
172172+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_SEMANTIC_CONTENT, unsafe.Pointer(&v))); err != nil {
173173+ return 0, err
174174+ }
175175+ return CellSemanticContent(v), nil
176176+}
177177+178178+// ColorPalette returns the palette index for the cell's background
179179+// color. Only valid when the cell's content tag is CellContentBgColorPalette.
180180+func (c *Cell) ColorPalette() (uint8, error) {
181181+ var v C.GhosttyColorPaletteIndex
182182+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_COLOR_PALETTE, unsafe.Pointer(&v))); err != nil {
183183+ return 0, err
184184+ }
185185+ return uint8(v), nil
186186+}
187187+188188+// ColorRGB returns the RGB color for the cell's background color.
189189+// Only valid when the cell's content tag is CellContentBgColorRGB.
190190+func (c *Cell) ColorRGB() (ColorRGB, error) {
191191+ var v C.GhosttyColorRgb
192192+ if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_COLOR_RGB, unsafe.Pointer(&v))); err != nil {
193193+ return ColorRGB{}, err
194194+ }
195195+ return ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}, nil
196196+}
197197+198198+// Wrap reports whether the row is soft-wrapped.
199199+func (r *Row) Wrap() (bool, error) {
200200+ var v C.bool
201201+ if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_WRAP, unsafe.Pointer(&v))); err != nil {
202202+ return false, err
203203+ }
204204+ return bool(v), nil
205205+}
206206+207207+// WrapContinuation reports whether the row is a continuation of
208208+// a soft-wrapped row.
209209+func (r *Row) WrapContinuation() (bool, error) {
210210+ var v C.bool
211211+ if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_WRAP_CONTINUATION, unsafe.Pointer(&v))); err != nil {
212212+ return false, err
213213+ }
214214+ return bool(v), nil
215215+}
216216+217217+// Grapheme reports whether any cells in the row have grapheme clusters.
218218+func (r *Row) Grapheme() (bool, error) {
219219+ var v C.bool
220220+ if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_GRAPHEME, unsafe.Pointer(&v))); err != nil {
221221+ return false, err
222222+ }
223223+ return bool(v), nil
224224+}
225225+226226+// Styled reports whether any cells in the row have styling
227227+// (may have false positives).
228228+func (r *Row) Styled() (bool, error) {
229229+ var v C.bool
230230+ if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_STYLED, unsafe.Pointer(&v))); err != nil {
231231+ return false, err
232232+ }
233233+ return bool(v), nil
234234+}
235235+236236+// Hyperlink reports whether any cells in the row have hyperlinks
237237+// (may have false positives).
238238+func (r *Row) Hyperlink() (bool, error) {
239239+ var v C.bool
240240+ if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_HYPERLINK, unsafe.Pointer(&v))); err != nil {
241241+ return false, err
242242+ }
243243+ return bool(v), nil
244244+}
245245+246246+// Semantic returns the semantic prompt state of the row.
247247+func (r *Row) Semantic() (RowSemanticPrompt, error) {
248248+ var v C.GhosttyRowSemanticPrompt
249249+ if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_SEMANTIC_PROMPT, unsafe.Pointer(&v))); err != nil {
250250+ return 0, err
251251+ }
252252+ return RowSemanticPrompt(v), nil
253253+}
254254+255255+// KittyVirtualPlaceholder reports whether the row contains a
256256+// Kitty virtual placeholder.
257257+func (r *Row) KittyVirtualPlaceholder() (bool, error) {
258258+ var v C.bool
259259+ if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_KITTY_VIRTUAL_PLACEHOLDER, unsafe.Pointer(&v))); err != nil {
260260+ return false, err
261261+ }
262262+ return bool(v), nil
263263+}
264264+265265+// Dirty reports whether the row is dirty and requires a redraw.
266266+func (r *Row) Dirty() (bool, error) {
267267+ var v C.bool
268268+ if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_DIRTY, unsafe.Pointer(&v))); err != nil {
269269+ return false, err
270270+ }
271271+ return bool(v), nil
272272+}
+40
size_report.go
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+*/
66+import "C"
77+88+// SizeReportStyle determines the output format for a terminal size report.
99+// C: GhosttySizeReportStyle
1010+type SizeReportStyle int
1111+1212+const (
1313+ // SizeReportMode2048 is the in-band size report (mode 2048).
1414+ SizeReportMode2048 SizeReportStyle = C.GHOSTTY_SIZE_REPORT_MODE_2048
1515+1616+ // SizeReportCSI14T is the XTWINOPS text area size in pixels.
1717+ SizeReportCSI14T SizeReportStyle = C.GHOSTTY_SIZE_REPORT_CSI_14_T
1818+1919+ // SizeReportCSI16T is the XTWINOPS cell size in pixels.
2020+ SizeReportCSI16T SizeReportStyle = C.GHOSTTY_SIZE_REPORT_CSI_16_T
2121+2222+ // SizeReportCSI18T is the XTWINOPS text area size in characters.
2323+ SizeReportCSI18T SizeReportStyle = C.GHOSTTY_SIZE_REPORT_CSI_18_T
2424+)
2525+2626+// SizeReportSize holds terminal geometry for XTWINOPS size queries.
2727+// C: GhosttySizeReportSize
2828+type SizeReportSize struct {
2929+ // Rows is the terminal row count in cells.
3030+ Rows uint16
3131+3232+ // Columns is the terminal column count in cells.
3333+ Columns uint16
3434+3535+ // CellWidth is the width of a single terminal cell in pixels.
3636+ CellWidth uint32
3737+3838+ // CellHeight is the height of a single terminal cell in pixels.
3939+ CellHeight uint32
4040+}
+148
style.go
···11+package libghostty
22+33+/*
44+#include <ghostty/vt.h>
55+66+// Helper to create a properly initialized GhosttyStyle (sized struct).
77+static inline GhosttyStyle init_style() {
88+ GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle);
99+ return style;
1010+}
1111+*/
1212+import "C"
1313+1414+import "unsafe"
1515+1616+// initCStyle returns a zero-initialized C GhosttyStyle with its size
1717+// field set (GHOSTTY_INIT_SIZED). Used by other files that need to
1818+// pass a style to C APIs.
1919+func initCStyle() C.GhosttyStyle {
2020+ return C.init_style()
2121+}
2222+2323+// StyleColorTag identifies the type of color in a style color.
2424+// C: GhosttyStyleColorTag
2525+type StyleColorTag int
2626+2727+const (
2828+ // StyleColorNone means no color is set.
2929+ StyleColorNone StyleColorTag = C.GHOSTTY_STYLE_COLOR_NONE
3030+3131+ // StyleColorPalette means the color is a palette index.
3232+ StyleColorPalette StyleColorTag = C.GHOSTTY_STYLE_COLOR_PALETTE
3333+3434+ // StyleColorRGB means the color is a direct RGB value.
3535+ StyleColorRGB StyleColorTag = C.GHOSTTY_STYLE_COLOR_RGB
3636+)
3737+3838+// StyleColor is a tagged union representing a color in a style attribute.
3939+// Check Tag to determine which field is valid.
4040+// C: GhosttyStyleColor
4141+type StyleColor struct {
4242+ // Tag identifies the type of color.
4343+ Tag StyleColorTag
4444+4545+ // Palette is the palette index (valid when Tag is StyleColorPalette).
4646+ Palette uint8
4747+4848+ // RGB is the direct RGB color (valid when Tag is StyleColorRGB).
4949+ RGB ColorRGB
5050+}
5151+5252+// Underline style constants.
5353+// C: GhosttySgrUnderline
5454+const (
5555+ UnderlineNone = C.GHOSTTY_SGR_UNDERLINE_NONE
5656+ UnderlineSingle = C.GHOSTTY_SGR_UNDERLINE_SINGLE
5757+ UnderlineDouble = C.GHOSTTY_SGR_UNDERLINE_DOUBLE
5858+ UnderlineCurly = C.GHOSTTY_SGR_UNDERLINE_CURLY
5959+ UnderlineDotted = C.GHOSTTY_SGR_UNDERLINE_DOTTED
6060+ UnderlineDashed = C.GHOSTTY_SGR_UNDERLINE_DASHED
6161+)
6262+6363+// Style is a thin wrapper around the C GhosttyStyle. It provides
6464+// getter methods to access individual style attributes without
6565+// copying the entire struct upfront.
6666+// C: GhosttyStyle
6767+type Style struct {
6868+ c C.GhosttyStyle
6969+}
7070+7171+// IsDefault reports whether the style is the default style
7272+// (no colors, no flags).
7373+func (s *Style) IsDefault() bool {
7474+ return bool(C.ghostty_style_is_default(&s.c))
7575+}
7676+7777+// FgColor returns the foreground color.
7878+func (s *Style) FgColor() StyleColor {
7979+ return styleColorFromC(s.c.fg_color)
8080+}
8181+8282+// BgColor returns the background color.
8383+func (s *Style) BgColor() StyleColor {
8484+ return styleColorFromC(s.c.bg_color)
8585+}
8686+8787+// UnderlineColor returns the underline color.
8888+func (s *Style) UnderlineColor() StyleColor {
8989+ return styleColorFromC(s.c.underline_color)
9090+}
9191+9292+// Bold reports whether bold is set.
9393+func (s *Style) Bold() bool {
9494+ return bool(s.c.bold)
9595+}
9696+9797+// Italic reports whether italic is set.
9898+func (s *Style) Italic() bool {
9999+ return bool(s.c.italic)
100100+}
101101+102102+// Faint reports whether faint (dim) is set.
103103+func (s *Style) Faint() bool {
104104+ return bool(s.c.faint)
105105+}
106106+107107+// Blink reports whether blink is set.
108108+func (s *Style) Blink() bool {
109109+ return bool(s.c.blink)
110110+}
111111+112112+// Inverse reports whether inverse video is set.
113113+func (s *Style) Inverse() bool {
114114+ return bool(s.c.inverse)
115115+}
116116+117117+// Invisible reports whether invisible is set.
118118+func (s *Style) Invisible() bool {
119119+ return bool(s.c.invisible)
120120+}
121121+122122+// Strikethrough reports whether strikethrough is set.
123123+func (s *Style) Strikethrough() bool {
124124+ return bool(s.c.strikethrough)
125125+}
126126+127127+// Overline reports whether overline is set.
128128+func (s *Style) Overline() bool {
129129+ return bool(s.c.overline)
130130+}
131131+132132+// Underline returns the underline style (one of the Underline* constants).
133133+func (s *Style) Underline() int {
134134+ return int(s.c.underline)
135135+}
136136+137137+// styleColorFromC converts a C GhosttyStyleColor to a Go StyleColor.
138138+func styleColorFromC(c C.GhosttyStyleColor) StyleColor {
139139+ sc := StyleColor{Tag: StyleColorTag(c.tag)}
140140+ switch sc.Tag {
141141+ case StyleColorPalette:
142142+ sc.Palette = uint8(*(*C.GhosttyColorPaletteIndex)(unsafe.Pointer(&c.value[0])))
143143+ case StyleColorRGB:
144144+ rgb := *(*C.GhosttyColorRgb)(unsafe.Pointer(&c.value[0]))
145145+ sc.RGB = ColorRGB{R: uint8(rgb.r), G: uint8(rgb.g), B: uint8(rgb.b)}
146146+ }
147147+ return sc
148148+}
+417
terminal.go
···11+package libghostty
22+33+/*
44+#include <stdlib.h>
55+#include <ghostty/vt.h>
66+*/
77+import "C"
88+99+import (
1010+ "runtime/cgo"
1111+ "unsafe"
1212+)
1313+1414+// Terminal wraps a Ghostty VT terminal handle.
1515+// C: GhosttyTerminal
1616+type Terminal struct {
1717+ ptr C.GhosttyTerminal
1818+1919+ // handle is a cgo.Handle pointing back to this Terminal. It is
2020+ // stored as the C-side userdata (GHOSTTY_TERMINAL_OPT_USERDATA)
2121+ // so that C effect trampolines can recover the *Terminal and
2222+ // dispatch to the appropriate Go effect handler.
2323+ handle cgo.Handle
2424+2525+ onWritePty WritePtyFn
2626+ onBell BellFn
2727+ onTitleChanged TitleChangedFn
2828+ onEnquiry EnquiryFn
2929+ onXtversion XtversionFn
3030+ onSize SizeFn
3131+ onColorScheme ColorSchemeFn
3232+ onDeviceAttributes DeviceAttributesFn
3333+3434+ // effectBuf holds C-allocated memory for the most recent response
3535+ // returned by an effect trampoline (e.g. enquiry, xtversion).
3636+ // libghostty copies the data immediately, so a single buffer
3737+ // shared across effects is sufficient.
3838+ effectBuf unsafe.Pointer
3939+}
4040+4141+// TerminalOption is a functional option for configuring a Terminal.
4242+type TerminalOption func(*TerminalConfig)
4343+4444+// TerminalConfig holds the configuration for creating a Terminal.
4545+// It can be passed directly to NewTerminal or built up using
4646+// functional options like WithSize and WithMaxScrollback.
4747+// C: GhosttyTerminalOptions
4848+type TerminalConfig struct {
4949+ // Cols is the terminal width in cells. Must be greater than zero.
5050+ Cols uint16
5151+5252+ // Rows is the terminal height in cells. Must be greater than zero.
5353+ Rows uint16
5454+5555+ // MaxScrollback is the maximum number of lines to keep in scrollback
5656+ // history. Defaults to 0 (no scrollback).
5757+ MaxScrollback uint
5858+5959+ // Effect handlers applied after terminal creation.
6060+ onWritePty WritePtyFn
6161+ onBell BellFn
6262+ onTitleChanged TitleChangedFn
6363+ onEnquiry EnquiryFn
6464+ onXtversion XtversionFn
6565+ onSize SizeFn
6666+ onColorScheme ColorSchemeFn
6767+ onDeviceAttributes DeviceAttributesFn
6868+}
6969+7070+// WritePtyFn is called when the terminal writes data back to the pty
7171+// (e.g. query responses). The data is only valid for the call duration.
7272+// C: GhosttyTerminalWritePtyFn
7373+type WritePtyFn func(data []byte)
7474+7575+// BellFn is called when the terminal receives a BEL character (0x07).
7676+// C: GhosttyTerminalBellFn
7777+type BellFn func()
7878+7979+// TitleChangedFn is called when the terminal title changes via OSC 0/2.
8080+// C: GhosttyTerminalTitleChangedFn
8181+type TitleChangedFn func()
8282+8383+// EnquiryFn is called when the terminal receives ENQ (0x05).
8484+// Return the response bytes; nil or empty means no response.
8585+// C: GhosttyTerminalEnquiryFn
8686+type EnquiryFn func() []byte
8787+8888+// XtversionFn is called for XTVERSION queries (CSI > q).
8989+// Return the version string; empty uses the default "libghostty".
9090+// C: GhosttyTerminalXtversionFn
9191+type XtversionFn func() string
9292+9393+// SizeFn is called for XTWINOPS size queries (CSI 14/16/18 t).
9494+// Return the size and true, or zero value and false to ignore the query.
9595+// C: GhosttyTerminalSizeFn
9696+type SizeFn func() (SizeReportSize, bool)
9797+9898+// ColorSchemeFn is called for color scheme queries (CSI ? 996 n).
9999+// Return the scheme and true, or zero value and false to ignore the query.
100100+// C: GhosttyTerminalColorSchemeFn
101101+type ColorSchemeFn func() (ColorScheme, bool)
102102+103103+// DeviceAttributesFn is called for device attributes queries
104104+// (CSI c / CSI > c / CSI = c). Return the attributes and true,
105105+// or zero value and false to ignore the query.
106106+// C: GhosttyTerminalDeviceAttributesFn
107107+type DeviceAttributesFn func() (DeviceAttributes, bool)
108108+109109+// WithSize sets the terminal dimensions in cells.
110110+// Both cols and rows must be greater than zero.
111111+func WithSize(cols, rows uint16) TerminalOption {
112112+ return func(c *TerminalConfig) {
113113+ c.Cols = cols
114114+ c.Rows = rows
115115+ }
116116+}
117117+118118+// WithMaxScrollback sets the maximum number of lines to keep in
119119+// scrollback history. Defaults to 0 (no scrollback).
120120+func WithMaxScrollback(lines uint) TerminalOption {
121121+ return func(c *TerminalConfig) {
122122+ c.MaxScrollback = lines
123123+ }
124124+}
125125+126126+// WithWritePty registers an effect handler invoked when the terminal
127127+// writes data back to the pty (e.g. query responses). The data slice
128128+// is only valid for the duration of the call.
129129+func WithWritePty(fn WritePtyFn) TerminalOption {
130130+ return func(c *TerminalConfig) {
131131+ c.onWritePty = fn
132132+ }
133133+}
134134+135135+// WithBell registers an effect handler invoked when the terminal
136136+// receives a BEL character (0x07).
137137+func WithBell(fn BellFn) TerminalOption {
138138+ return func(c *TerminalConfig) {
139139+ c.onBell = fn
140140+ }
141141+}
142142+143143+// WithTitleChanged registers an effect handler invoked when the
144144+// terminal title changes via OSC 0 or OSC 2.
145145+func WithTitleChanged(fn TitleChangedFn) TerminalOption {
146146+ return func(c *TerminalConfig) {
147147+ c.onTitleChanged = fn
148148+ }
149149+}
150150+151151+// WithEnquiry registers an effect handler invoked when the terminal
152152+// receives an ENQ character (0x05). Return the response bytes; nil
153153+// or empty means no response.
154154+func WithEnquiry(fn EnquiryFn) TerminalOption {
155155+ return func(c *TerminalConfig) {
156156+ c.onEnquiry = fn
157157+ }
158158+}
159159+160160+// WithXtversion registers an effect handler invoked for XTVERSION
161161+// queries (CSI > q). Return the version string; empty uses the
162162+// default "libghostty" version.
163163+func WithXtversion(fn XtversionFn) TerminalOption {
164164+ return func(c *TerminalConfig) {
165165+ c.onXtversion = fn
166166+ }
167167+}
168168+169169+// WithSizeReport registers an effect handler invoked for XTWINOPS
170170+// size queries (CSI 14/16/18 t). Return the size and true, or
171171+// zero value and false to silently ignore the query.
172172+func WithSizeReport(fn SizeFn) TerminalOption {
173173+ return func(c *TerminalConfig) {
174174+ c.onSize = fn
175175+ }
176176+}
177177+178178+// WithColorScheme registers an effect handler invoked for color
179179+// scheme queries (CSI ? 996 n). Return the scheme and true, or
180180+// zero value and false to silently ignore the query.
181181+func WithColorScheme(fn ColorSchemeFn) TerminalOption {
182182+ return func(c *TerminalConfig) {
183183+ c.onColorScheme = fn
184184+ }
185185+}
186186+187187+// WithDeviceAttributes registers an effect handler invoked for
188188+// device attributes queries (CSI c / CSI > c / CSI = c). Return
189189+// the attributes and true, or zero value and false to silently
190190+// ignore the query.
191191+func WithDeviceAttributes(fn DeviceAttributesFn) TerminalOption {
192192+ return func(c *TerminalConfig) {
193193+ c.onDeviceAttributes = fn
194194+ }
195195+}
196196+197197+// NewTerminal creates a new terminal with the given options.
198198+// WithSize is required; cols and rows must both be greater than zero.
199199+func NewTerminal(opts ...TerminalOption) (*Terminal, error) {
200200+ // Apply defaults and user options.
201201+ cfg := TerminalConfig{}
202202+ for _, opt := range opts {
203203+ opt(&cfg)
204204+ }
205205+206206+ options := C.GhosttyTerminalOptions{
207207+ cols: C.uint16_t(cfg.Cols),
208208+ rows: C.uint16_t(cfg.Rows),
209209+ max_scrollback: C.size_t(cfg.MaxScrollback),
210210+ }
211211+212212+ var cterm C.GhosttyTerminal
213213+ if err := resultError(C.ghostty_terminal_new(nil, &cterm, options)); err != nil {
214214+ return nil, err
215215+ }
216216+217217+ t := &Terminal{
218218+ ptr: cterm,
219219+ onWritePty: cfg.onWritePty,
220220+ onBell: cfg.onBell,
221221+ onTitleChanged: cfg.onTitleChanged,
222222+ onEnquiry: cfg.onEnquiry,
223223+ onXtversion: cfg.onXtversion,
224224+ onSize: cfg.onSize,
225225+ onColorScheme: cfg.onColorScheme,
226226+ onDeviceAttributes: cfg.onDeviceAttributes,
227227+ }
228228+229229+ // Always set userdata to our handle so trampolines can find us.
230230+ t.handle = cgo.NewHandle(t)
231231+ C.ghostty_terminal_set(
232232+ t.ptr,
233233+ C.GHOSTTY_TERMINAL_OPT_USERDATA,
234234+ unsafe.Pointer(t.handle),
235235+ )
236236+237237+ // Register any effects that were provided via options.
238238+ t.syncEffects()
239239+240240+ return t, nil
241241+}
242242+243243+// Close frees the underlying terminal handle and releases the cgo.Handle.
244244+// After this call, the terminal must not be used.
245245+func (t *Terminal) Close() {
246246+ t.handle.Delete()
247247+ C.ghostty_terminal_free(t.ptr)
248248+ if t.effectBuf != nil {
249249+ C.free(t.effectBuf)
250250+ }
251251+}
252252+253253+// Reset performs a full terminal reset (RIS).
254254+// All state is reset to initial configuration (modes, scrollback,
255255+// scrolling region, screen contents). Dimensions are preserved.
256256+func (t *Terminal) Reset() {
257257+ C.ghostty_terminal_reset(t.ptr)
258258+}
259259+260260+// Resize changes the terminal dimensions.
261261+// Both cols and rows must be greater than zero. cellWidthPx and
262262+// cellHeightPx specify the pixel dimensions of a single cell, used
263263+// for image protocols and size reports.
264264+func (t *Terminal) Resize(cols, rows uint16, cellWidthPx, cellHeightPx uint32) error {
265265+ return resultError(C.ghostty_terminal_resize(
266266+ t.ptr,
267267+ C.uint16_t(cols),
268268+ C.uint16_t(rows),
269269+ C.uint32_t(cellWidthPx),
270270+ C.uint32_t(cellHeightPx),
271271+ ))
272272+}
273273+274274+// VTWrite feeds raw VT-encoded bytes through the terminal's parser,
275275+// updating terminal state. Malformed input is handled gracefully and
276276+// will not cause an error.
277277+func (t *Terminal) VTWrite(data []byte) {
278278+ if len(data) == 0 {
279279+ return
280280+ }
281281+ C.ghostty_terminal_vt_write(t.ptr, (*C.uint8_t)(&data[0]), C.size_t(len(data)))
282282+}
283283+284284+// Write implements io.Writer by feeding data through the terminal's
285285+// VT parser. It always consumes all bytes and never returns an error.
286286+func (t *Terminal) Write(p []byte) (int, error) {
287287+ t.VTWrite(p)
288288+ return len(p), nil
289289+}
290290+291291+// ModeGet returns the current value of a terminal mode.
292292+func (t *Terminal) ModeGet(mode Mode) (bool, error) {
293293+ var val C.bool
294294+ if err := resultError(C.ghostty_terminal_mode_get(t.ptr, C.GhosttyMode(mode), &val)); err != nil {
295295+ return false, err
296296+ }
297297+ return bool(val), nil
298298+}
299299+300300+// ModeSet sets a terminal mode to the given value.
301301+func (t *Terminal) ModeSet(mode Mode, value bool) error {
302302+ return resultError(C.ghostty_terminal_mode_set(t.ptr, C.GhosttyMode(mode), C.bool(value)))
303303+}
304304+305305+// ScrollViewportTag describes the scroll behavior.
306306+// C: GhosttyTerminalScrollViewportTag
307307+type ScrollViewportTag int
308308+309309+const (
310310+ // ScrollViewportTop scrolls to the top of scrollback.
311311+ ScrollViewportTop ScrollViewportTag = C.GHOSTTY_SCROLL_VIEWPORT_TOP
312312+313313+ // ScrollViewportBottom scrolls to the bottom (active area).
314314+ ScrollViewportBottom ScrollViewportTag = C.GHOSTTY_SCROLL_VIEWPORT_BOTTOM
315315+316316+ // ScrollViewportDelta scrolls by a delta amount (up is negative).
317317+ ScrollViewportDelta ScrollViewportTag = C.GHOSTTY_SCROLL_VIEWPORT_DELTA
318318+)
319319+320320+// ScrollViewport scrolls the terminal viewport to the top of scrollback.
321321+func (t *Terminal) ScrollViewportTop() {
322322+ var sv C.GhosttyTerminalScrollViewport
323323+ sv.tag = C.GHOSTTY_SCROLL_VIEWPORT_TOP
324324+ C.ghostty_terminal_scroll_viewport(t.ptr, sv)
325325+}
326326+327327+// ScrollViewportBottom scrolls the terminal viewport to the bottom
328328+// (active area).
329329+func (t *Terminal) ScrollViewportBottom() {
330330+ var sv C.GhosttyTerminalScrollViewport
331331+ sv.tag = C.GHOSTTY_SCROLL_VIEWPORT_BOTTOM
332332+ C.ghostty_terminal_scroll_viewport(t.ptr, sv)
333333+}
334334+335335+// ScrollViewportDelta scrolls the terminal viewport by the given delta
336336+// (negative for up, positive for down).
337337+func (t *Terminal) ScrollViewportDelta(delta int) {
338338+ var sv C.GhosttyTerminalScrollViewport
339339+ sv.tag = C.GHOSTTY_SCROLL_VIEWPORT_DELTA
340340+ // Set the delta in the value union. The delta field is at offset 0.
341341+ *(*C.intptr_t)(unsafe.Pointer(&sv.value[0])) = C.intptr_t(delta)
342342+ C.ghostty_terminal_scroll_viewport(t.ptr, sv)
343343+}
344344+345345+// TerminalScreen identifies which screen buffer is active.
346346+// C: GhosttyTerminalScreen
347347+type TerminalScreen int
348348+349349+const (
350350+ // ScreenPrimary is the primary (normal) screen.
351351+ ScreenPrimary TerminalScreen = C.GHOSTTY_TERMINAL_SCREEN_PRIMARY
352352+353353+ // ScreenAlternate is the alternate screen.
354354+ ScreenAlternate TerminalScreen = C.GHOSTTY_TERMINAL_SCREEN_ALTERNATE
355355+)
356356+357357+// Scrollbar holds the scrollbar state for the terminal viewport.
358358+// C: GhosttyTerminalScrollbar
359359+type Scrollbar struct {
360360+ // Total is the total size of the scrollable area in rows.
361361+ Total uint64
362362+363363+ // Offset is the offset into the total area that the viewport is at.
364364+ Offset uint64
365365+366366+ // Len is the length of the visible area in rows.
367367+ Len uint64
368368+}
369369+370370+// KittyKeyFlags holds the current Kitty keyboard protocol flags.
371371+// These can be combined using bitwise OR.
372372+// C: GhosttyKittyKeyFlags (uint8_t)
373373+type KittyKeyFlags uint8
374374+375375+// Kitty keyboard protocol flag constants.
376376+const (
377377+ // KittyKeyDisabled means the Kitty keyboard protocol is disabled.
378378+ KittyKeyDisabled KittyKeyFlags = C.GHOSTTY_KITTY_KEY_DISABLED
379379+380380+ // KittyKeyDisambiguate enables disambiguating escape codes.
381381+ KittyKeyDisambiguate KittyKeyFlags = C.GHOSTTY_KITTY_KEY_DISAMBIGUATE
382382+383383+ // KittyKeyReportEvents enables reporting key press and release events.
384384+ KittyKeyReportEvents KittyKeyFlags = C.GHOSTTY_KITTY_KEY_REPORT_EVENTS
385385+386386+ // KittyKeyReportAlternates enables reporting alternate key codes.
387387+ KittyKeyReportAlternates KittyKeyFlags = C.GHOSTTY_KITTY_KEY_REPORT_ALTERNATES
388388+389389+ // KittyKeyReportAll enables reporting all key events including those
390390+ // normally handled by the terminal.
391391+ KittyKeyReportAll KittyKeyFlags = C.GHOSTTY_KITTY_KEY_REPORT_ALL
392392+393393+ // KittyKeyReportAssociated enables reporting associated text with
394394+ // key events.
395395+ KittyKeyReportAssociated KittyKeyFlags = C.GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED
396396+397397+ // KittyKeyAll has all Kitty keyboard protocol flags enabled.
398398+ KittyKeyAll KittyKeyFlags = C.GHOSTTY_KITTY_KEY_ALL
399399+)
400400+401401+// GridRef resolves a point in the terminal grid to a grid reference.
402402+// The returned GridRef is only valid until the next terminal update.
403403+//
404404+// Lookups using PointTagActive and PointTagViewport are fast.
405405+// PointTagScreen and PointTagHistory may be expensive for large
406406+// scrollback buffers.
407407+func (t *Terminal) GridRef(point Point) (*GridRef, error) {
408408+ ref := initCGridRef()
409409+ if err := resultError(C.ghostty_terminal_grid_ref(
410410+ t.ptr,
411411+ point.toC(),
412412+ &ref,
413413+ )); err != nil {
414414+ return nil, err
415415+ }
416416+ return &GridRef{ref: ref}, nil
417417+}
+263
terminal_data.go
···11+package libghostty
22+33+// Terminal data getters wrapping ghostty_terminal_get().
44+// Functions are ordered alphabetically.
55+66+/*
77+#include <ghostty/vt.h>
88+*/
99+import "C"
1010+1111+import (
1212+ "errors"
1313+ "unsafe"
1414+)
1515+1616+// ActiveScreen returns which screen buffer is currently active.
1717+func (t *Terminal) ActiveScreen() (TerminalScreen, error) {
1818+ var v C.GhosttyTerminalScreen
1919+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN, unsafe.Pointer(&v))); err != nil {
2020+ return 0, err
2121+ }
2222+ return TerminalScreen(v), nil
2323+}
2424+2525+// Cols returns the terminal width in cells.
2626+func (t *Terminal) Cols() (uint16, error) {
2727+ var v C.uint16_t
2828+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_COLS, unsafe.Pointer(&v))); err != nil {
2929+ return 0, err
3030+ }
3131+ return uint16(v), nil
3232+}
3333+3434+// ColorBackground returns the effective background color (OSC override
3535+// or default). Returns nil if no background color is set.
3636+func (t *Terminal) ColorBackground() (*ColorRGB, error) {
3737+ return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND)
3838+}
3939+4040+// ColorBackgroundDefault returns the default background color, ignoring
4141+// any OSC override. Returns nil if no default is set.
4242+func (t *Terminal) ColorBackgroundDefault() (*ColorRGB, error) {
4343+ return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT)
4444+}
4545+4646+// ColorCursor returns the effective cursor color (OSC override or
4747+// default). Returns nil if no cursor color is set.
4848+func (t *Terminal) ColorCursor() (*ColorRGB, error) {
4949+ return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_CURSOR)
5050+}
5151+5252+// ColorCursorDefault returns the default cursor color, ignoring any
5353+// OSC override. Returns nil if no default is set.
5454+func (t *Terminal) ColorCursorDefault() (*ColorRGB, error) {
5555+ return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT)
5656+}
5757+5858+// ColorForeground returns the effective foreground color (OSC override
5959+// or default). Returns nil if no foreground color is set.
6060+func (t *Terminal) ColorForeground() (*ColorRGB, error) {
6161+ return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND)
6262+}
6363+6464+// ColorForegroundDefault returns the default foreground color, ignoring
6565+// any OSC override. Returns nil if no default is set.
6666+func (t *Terminal) ColorForegroundDefault() (*ColorRGB, error) {
6767+ return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT)
6868+}
6969+7070+// ColorPalette returns the current 256-color palette (with any OSC
7171+// overrides applied).
7272+func (t *Terminal) ColorPalette() (*Palette, error) {
7373+ return t.getPalette(C.GHOSTTY_TERMINAL_DATA_COLOR_PALETTE)
7474+}
7575+7676+// ColorPaletteDefault returns the default 256-color palette, ignoring
7777+// any OSC overrides.
7878+func (t *Terminal) ColorPaletteDefault() (*Palette, error) {
7979+ return t.getPalette(C.GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT)
8080+}
8181+8282+// CursorPendingWrap reports whether the cursor has a pending wrap
8383+// (the next printed character will soft-wrap to the next line).
8484+func (t *Terminal) CursorPendingWrap() (bool, error) {
8585+ var v C.bool
8686+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP, unsafe.Pointer(&v))); err != nil {
8787+ return false, err
8888+ }
8989+ return bool(v), nil
9090+}
9191+9292+// CursorStyle returns the current SGR style of the cursor. This is
9393+// the style that will be applied to newly printed characters.
9494+func (t *Terminal) CursorStyle() (*Style, error) {
9595+ cs := initCStyle()
9696+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_STYLE, unsafe.Pointer(&cs))); err != nil {
9797+ return nil, err
9898+ }
9999+ return &Style{c: cs}, nil
100100+}
101101+102102+// CursorVisible reports whether the cursor is visible (DEC mode 25).
103103+func (t *Terminal) CursorVisible() (bool, error) {
104104+ var v C.bool
105105+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE, unsafe.Pointer(&v))); err != nil {
106106+ return false, err
107107+ }
108108+ return bool(v), nil
109109+}
110110+111111+// CursorX returns the cursor column position (0-indexed).
112112+func (t *Terminal) CursorX() (uint16, error) {
113113+ var v C.uint16_t
114114+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_X, unsafe.Pointer(&v))); err != nil {
115115+ return 0, err
116116+ }
117117+ return uint16(v), nil
118118+}
119119+120120+// CursorY returns the cursor row position within the active area
121121+// (0-indexed).
122122+func (t *Terminal) CursorY() (uint16, error) {
123123+ var v C.uint16_t
124124+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_Y, unsafe.Pointer(&v))); err != nil {
125125+ return 0, err
126126+ }
127127+ return uint16(v), nil
128128+}
129129+130130+// HeightPx returns the total terminal height in pixels
131131+// (rows * cell_height_px as set by Resize).
132132+func (t *Terminal) HeightPx() (uint32, error) {
133133+ var v C.uint32_t
134134+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_HEIGHT_PX, unsafe.Pointer(&v))); err != nil {
135135+ return 0, err
136136+ }
137137+ return uint32(v), nil
138138+}
139139+140140+// KittyKeyboardFlags returns the current Kitty keyboard protocol flags.
141141+func (t *Terminal) KittyKeyboardFlags() (KittyKeyFlags, error) {
142142+ var v C.uint8_t
143143+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS, unsafe.Pointer(&v))); err != nil {
144144+ return 0, err
145145+ }
146146+ return KittyKeyFlags(v), nil
147147+}
148148+149149+// MouseTracking reports whether any mouse tracking mode is active.
150150+func (t *Terminal) MouseTracking() (bool, error) {
151151+ var v C.bool
152152+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING, unsafe.Pointer(&v))); err != nil {
153153+ return false, err
154154+ }
155155+ return bool(v), nil
156156+}
157157+158158+// Pwd returns the terminal's current working directory as set by
159159+// escape sequences (e.g. OSC 7). Returns an empty string if unset.
160160+// The returned string is copied; it remains valid after subsequent
161161+// calls to VTWrite or Reset.
162162+func (t *Terminal) Pwd() (string, error) {
163163+ var s C.GhosttyString
164164+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_PWD, unsafe.Pointer(&s))); err != nil {
165165+ return "", err
166166+ }
167167+ return C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len)), nil
168168+}
169169+170170+// Rows returns the terminal height in cells.
171171+func (t *Terminal) Rows() (uint16, error) {
172172+ var v C.uint16_t
173173+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_ROWS, unsafe.Pointer(&v))); err != nil {
174174+ return 0, err
175175+ }
176176+ return uint16(v), nil
177177+}
178178+179179+// Scrollbar returns the scrollbar state for the terminal viewport.
180180+// This may be expensive to calculate depending on the viewport position;
181181+// call only as needed.
182182+func (t *Terminal) Scrollbar() (Scrollbar, error) {
183183+ var v C.GhosttyTerminalScrollbar
184184+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_SCROLLBAR, unsafe.Pointer(&v))); err != nil {
185185+ return Scrollbar{}, err
186186+ }
187187+ return Scrollbar{
188188+ Total: uint64(v.total),
189189+ Offset: uint64(v.offset),
190190+ Len: uint64(v.len),
191191+ }, nil
192192+}
193193+194194+// ScrollbackRows returns the number of scrollback rows (total rows
195195+// minus viewport rows).
196196+func (t *Terminal) ScrollbackRows() (uint, error) {
197197+ var v C.size_t
198198+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS, unsafe.Pointer(&v))); err != nil {
199199+ return 0, err
200200+ }
201201+ return uint(v), nil
202202+}
203203+204204+// Title returns the terminal title as set by escape sequences
205205+// (e.g. OSC 0/2). Returns an empty string if unset. The returned
206206+// string is copied; it remains valid after subsequent calls to
207207+// VTWrite or Reset.
208208+func (t *Terminal) Title() (string, error) {
209209+ var s C.GhosttyString
210210+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_TITLE, unsafe.Pointer(&s))); err != nil {
211211+ return "", err
212212+ }
213213+ return C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len)), nil
214214+}
215215+216216+// TotalRows returns the total number of rows in the active screen
217217+// including scrollback.
218218+func (t *Terminal) TotalRows() (uint, error) {
219219+ var v C.size_t
220220+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_TOTAL_ROWS, unsafe.Pointer(&v))); err != nil {
221221+ return 0, err
222222+ }
223223+ return uint(v), nil
224224+}
225225+226226+// WidthPx returns the total terminal width in pixels
227227+// (cols * cell_width_px as set by Resize).
228228+func (t *Terminal) WidthPx() (uint32, error) {
229229+ var v C.uint32_t
230230+ if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_WIDTH_PX, unsafe.Pointer(&v))); err != nil {
231231+ return 0, err
232232+ }
233233+ return uint32(v), nil
234234+}
235235+236236+// getColorRGB is a helper that reads a single ColorRGB value from the
237237+// terminal. Returns nil (without error) when the result is NO_VALUE.
238238+func (t *Terminal) getColorRGB(data C.GhosttyTerminalData) (*ColorRGB, error) {
239239+ var c C.GhosttyColorRgb
240240+ err := resultError(C.ghostty_terminal_get(t.ptr, data, unsafe.Pointer(&c)))
241241+ if err != nil {
242242+ var ge *Error
243243+ if errors.As(err, &ge) && ge.Result == ResultNoValue {
244244+ return nil, nil
245245+ }
246246+ return nil, err
247247+ }
248248+ return &ColorRGB{R: uint8(c.r), G: uint8(c.g), B: uint8(c.b)}, nil
249249+}
250250+251251+// getPalette is a helper that reads a full 256-color palette from the
252252+// terminal.
253253+func (t *Terminal) getPalette(data C.GhosttyTerminalData) (*Palette, error) {
254254+ var cp [PaletteSize]C.GhosttyColorRgb
255255+ if err := resultError(C.ghostty_terminal_get(t.ptr, data, unsafe.Pointer(&cp[0]))); err != nil {
256256+ return nil, err
257257+ }
258258+ var p Palette
259259+ for i, c := range cp {
260260+ p[i] = ColorRGB{R: uint8(c.r), G: uint8(c.g), B: uint8(c.b)}
261261+ }
262262+ return &p, nil
263263+}
+268
terminal_data_test.go
···11+package libghostty
22+33+import "testing"
44+55+func TestTerminalColsRows(t *testing.T) {
66+ term, err := NewTerminal(WithSize(80, 24))
77+ if err != nil {
88+ t.Fatal(err)
99+ }
1010+ defer term.Close()
1111+1212+ cols, err := term.Cols()
1313+ if err != nil {
1414+ t.Fatal(err)
1515+ }
1616+ if cols != 80 {
1717+ t.Fatalf("expected 80 cols, got %d", cols)
1818+ }
1919+2020+ rows, err := term.Rows()
2121+ if err != nil {
2222+ t.Fatal(err)
2323+ }
2424+ if rows != 24 {
2525+ t.Fatalf("expected 24 rows, got %d", rows)
2626+ }
2727+2828+ // Resize and verify.
2929+ if err := term.Resize(120, 40, 8, 16); err != nil {
3030+ t.Fatal(err)
3131+ }
3232+ cols, _ = term.Cols()
3333+ rows, _ = term.Rows()
3434+ if cols != 120 || rows != 40 {
3535+ t.Fatalf("expected 120x40 after resize, got %dx%d", cols, rows)
3636+ }
3737+}
3838+3939+func TestTerminalCursorPosition(t *testing.T) {
4040+ term, err := NewTerminal(WithSize(80, 24))
4141+ if err != nil {
4242+ t.Fatal(err)
4343+ }
4444+ defer term.Close()
4545+4646+ // Cursor starts at 0,0.
4747+ x, err := term.CursorX()
4848+ if err != nil {
4949+ t.Fatal(err)
5050+ }
5151+ y, err := term.CursorY()
5252+ if err != nil {
5353+ t.Fatal(err)
5454+ }
5555+ if x != 0 || y != 0 {
5656+ t.Fatalf("expected cursor at 0,0, got %d,%d", x, y)
5757+ }
5858+5959+ // Move cursor to col 5, row 3 (1-indexed in VT: CSI 4;6H).
6060+ term.VTWrite([]byte("\x1b[4;6H"))
6161+ x, _ = term.CursorX()
6262+ y, _ = term.CursorY()
6363+ if x != 5 || y != 3 {
6464+ t.Fatalf("expected cursor at 5,3, got %d,%d", x, y)
6565+ }
6666+}
6767+6868+func TestTerminalTitle(t *testing.T) {
6969+ term, err := NewTerminal(WithSize(80, 24))
7070+ if err != nil {
7171+ t.Fatal(err)
7272+ }
7373+ defer term.Close()
7474+7575+ // Set title via SetTitle and read it back.
7676+ if err := term.SetTitle("hello"); err != nil {
7777+ t.Fatal(err)
7878+ }
7979+ title, err := term.Title()
8080+ if err != nil {
8181+ t.Fatal(err)
8282+ }
8383+ if title != "hello" {
8484+ t.Fatalf("expected title %q, got %q", "hello", title)
8585+ }
8686+8787+ // Set title via VT escape and read back.
8888+ term.VTWrite([]byte("\x1b]2;world\x07"))
8989+ title, _ = term.Title()
9090+ if title != "world" {
9191+ t.Fatalf("expected title %q, got %q", "world", title)
9292+ }
9393+}
9494+9595+func TestTerminalPwd(t *testing.T) {
9696+ term, err := NewTerminal(WithSize(80, 24))
9797+ if err != nil {
9898+ t.Fatal(err)
9999+ }
100100+ defer term.Close()
101101+102102+ if err := term.SetPwd("/home/user"); err != nil {
103103+ t.Fatal(err)
104104+ }
105105+ pwd, err := term.Pwd()
106106+ if err != nil {
107107+ t.Fatal(err)
108108+ }
109109+ if pwd != "/home/user" {
110110+ t.Fatalf("expected pwd %q, got %q", "/home/user", pwd)
111111+ }
112112+}
113113+114114+func TestTerminalTotalScrollbackRows(t *testing.T) {
115115+ term, err := NewTerminal(WithSize(80, 24), WithMaxScrollback(100))
116116+ if err != nil {
117117+ t.Fatal(err)
118118+ }
119119+ defer term.Close()
120120+121121+ total, err := term.TotalRows()
122122+ if err != nil {
123123+ t.Fatal(err)
124124+ }
125125+ if total < 24 {
126126+ t.Fatalf("expected at least 24 total rows, got %d", total)
127127+ }
128128+129129+ sb, err := term.ScrollbackRows()
130130+ if err != nil {
131131+ t.Fatal(err)
132132+ }
133133+ // Fresh terminal has no scrollback content.
134134+ if sb != 0 {
135135+ t.Fatalf("expected 0 scrollback rows, got %d", sb)
136136+ }
137137+}
138138+139139+func TestTerminalPixelDimensions(t *testing.T) {
140140+ term, err := NewTerminal(WithSize(80, 24))
141141+ if err != nil {
142142+ t.Fatal(err)
143143+ }
144144+ defer term.Close()
145145+146146+ // Before resize with pixel sizes, dimensions are 0.
147147+ if err := term.Resize(80, 24, 10, 20); err != nil {
148148+ t.Fatal(err)
149149+ }
150150+ w, err := term.WidthPx()
151151+ if err != nil {
152152+ t.Fatal(err)
153153+ }
154154+ h, err := term.HeightPx()
155155+ if err != nil {
156156+ t.Fatal(err)
157157+ }
158158+ if w != 800 {
159159+ t.Fatalf("expected width 800px, got %d", w)
160160+ }
161161+ if h != 480 {
162162+ t.Fatalf("expected height 480px, got %d", h)
163163+ }
164164+}
165165+166166+func TestTerminalMouseTracking(t *testing.T) {
167167+ term, err := NewTerminal(WithSize(80, 24))
168168+ if err != nil {
169169+ t.Fatal(err)
170170+ }
171171+ defer term.Close()
172172+173173+ tracking, err := term.MouseTracking()
174174+ if err != nil {
175175+ t.Fatal(err)
176176+ }
177177+ if tracking {
178178+ t.Fatal("expected no mouse tracking by default")
179179+ }
180180+181181+ // Enable normal mouse tracking.
182182+ term.VTWrite([]byte("\x1b[?1000h"))
183183+ tracking, _ = term.MouseTracking()
184184+ if !tracking {
185185+ t.Fatal("expected mouse tracking after enabling")
186186+ }
187187+}
188188+189189+func TestTerminalColorRoundTrip(t *testing.T) {
190190+ term, err := NewTerminal(WithSize(80, 24))
191191+ if err != nil {
192192+ t.Fatal(err)
193193+ }
194194+ defer term.Close()
195195+196196+ // No color set initially.
197197+ fg, err := term.ColorForeground()
198198+ if err != nil {
199199+ t.Fatal(err)
200200+ }
201201+ if fg != nil {
202202+ t.Fatal("expected nil foreground before setting")
203203+ }
204204+205205+ // Set and read back.
206206+ red := &ColorRGB{R: 255, G: 0, B: 0}
207207+ if err := term.SetColorForeground(red); err != nil {
208208+ t.Fatal(err)
209209+ }
210210+ fg, err = term.ColorForeground()
211211+ if err != nil {
212212+ t.Fatal(err)
213213+ }
214214+ if fg == nil || *fg != *red {
215215+ t.Fatalf("expected %+v, got %+v", red, fg)
216216+ }
217217+218218+ // Default getter should also return the value.
219219+ fgDef, err := term.ColorForegroundDefault()
220220+ if err != nil {
221221+ t.Fatal(err)
222222+ }
223223+ if fgDef == nil || *fgDef != *red {
224224+ t.Fatalf("expected default %+v, got %+v", red, fgDef)
225225+ }
226226+227227+ // Clear and verify nil again.
228228+ if err := term.SetColorForeground(nil); err != nil {
229229+ t.Fatal(err)
230230+ }
231231+ fg, _ = term.ColorForeground()
232232+ if fg != nil {
233233+ t.Fatal("expected nil foreground after clearing")
234234+ }
235235+}
236236+237237+func TestTerminalPaletteRoundTrip(t *testing.T) {
238238+ term, err := NewTerminal(WithSize(80, 24))
239239+ if err != nil {
240240+ t.Fatal(err)
241241+ }
242242+ defer term.Close()
243243+244244+ // Read default palette.
245245+ p, err := term.ColorPalette()
246246+ if err != nil {
247247+ t.Fatal(err)
248248+ }
249249+ if p == nil {
250250+ t.Fatal("expected non-nil palette")
251251+ }
252252+253253+ // Set a custom palette and read it back.
254254+ var custom Palette
255255+ for i := range custom {
256256+ custom[i] = ColorRGB{R: uint8(i), G: 0, B: 0}
257257+ }
258258+ if err := term.SetColorPalette(&custom); err != nil {
259259+ t.Fatal(err)
260260+ }
261261+ p, _ = term.ColorPalette()
262262+ if p[0] != (ColorRGB{R: 0, G: 0, B: 0}) {
263263+ t.Fatalf("expected palette[0] = {0,0,0}, got %+v", p[0])
264264+ }
265265+ if p[128] != (ColorRGB{R: 128, G: 0, B: 0}) {
266266+ t.Fatalf("expected palette[128] = {128,0,0}, got %+v", p[128])
267267+ }
268268+}
+235
terminal_effect.go
···11+package libghostty
22+33+// C trampolines for terminal effects.
44+//
55+// Each exported Go function here is passed to the C side as a function
66+// pointer. The C library calls it with a userdata void*, which is a
77+// cgo.Handle pointing back to the owning *Terminal. The trampoline
88+// recovers the Terminal and dispatches to the user-supplied Go effect handler.
99+1010+/*
1111+#include <stdlib.h>
1212+#include <ghostty/vt.h>
1313+1414+// Forward declarations for the Go trampolines so we can take their
1515+// addresses on the C side.
1616+extern void goWritePtyTrampoline(GhosttyTerminal, void*, uint8_t*, size_t);
1717+extern void goBellTrampoline(GhosttyTerminal, void*);
1818+extern void goTitleChangedTrampoline(GhosttyTerminal, void*);
1919+extern GhosttyString goEnquiryTrampoline(GhosttyTerminal, void*);
2020+extern GhosttyString goXtversionTrampoline(GhosttyTerminal, void*);
2121+extern bool goSizeTrampoline(GhosttyTerminal, void*, GhosttySizeReportSize*);
2222+extern bool goColorSchemeTrampoline(GhosttyTerminal, void*, GhosttyColorScheme*);
2323+extern bool goDeviceAttributesTrampoline(GhosttyTerminal, void*, GhosttyDeviceAttributes*);
2424+2525+// Helpers to set each effect via ghostty_terminal_set.
2626+// We need these because cgo cannot take the address of a Go-exported
2727+// function directly as a C function pointer.
2828+static inline GhosttyResult set_write_pty(GhosttyTerminal t) {
2929+ return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_WRITE_PTY, (const void*)goWritePtyTrampoline);
3030+}
3131+static inline GhosttyResult set_bell(GhosttyTerminal t) {
3232+ return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_BELL, (const void*)goBellTrampoline);
3333+}
3434+static inline GhosttyResult set_title_changed(GhosttyTerminal t) {
3535+ return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, (const void*)goTitleChangedTrampoline);
3636+}
3737+static inline GhosttyResult set_enquiry(GhosttyTerminal t) {
3838+ return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_ENQUIRY, (const void*)goEnquiryTrampoline);
3939+}
4040+static inline GhosttyResult set_xtversion(GhosttyTerminal t) {
4141+ return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_XTVERSION, (const void*)goXtversionTrampoline);
4242+}
4343+static inline GhosttyResult set_size(GhosttyTerminal t) {
4444+ return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_SIZE, (const void*)goSizeTrampoline);
4545+}
4646+static inline GhosttyResult set_color_scheme(GhosttyTerminal t) {
4747+ return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_COLOR_SCHEME, (const void*)goColorSchemeTrampoline);
4848+}
4949+static inline GhosttyResult set_device_attributes(GhosttyTerminal t) {
5050+ return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES, (const void*)goDeviceAttributesTrampoline);
5151+}
5252+5353+// Helper to clear an effect by setting it to NULL.
5454+static inline GhosttyResult clear_effect(GhosttyTerminal t, GhosttyTerminalOption opt) {
5555+ return ghostty_terminal_set(t, opt, NULL);
5656+}
5757+*/
5858+import "C"
5959+6060+import (
6161+ "runtime/cgo"
6262+ "unsafe"
6363+)
6464+6565+// syncEffects registers or clears each C effect based on whether
6666+// the corresponding Go effect handler is set.
6767+func (t *Terminal) syncEffects() {
6868+ if t.onWritePty != nil {
6969+ C.set_write_pty(t.ptr)
7070+ } else {
7171+ C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_WRITE_PTY)
7272+ }
7373+ if t.onBell != nil {
7474+ C.set_bell(t.ptr)
7575+ } else {
7676+ C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_BELL)
7777+ }
7878+ if t.onTitleChanged != nil {
7979+ C.set_title_changed(t.ptr)
8080+ } else {
8181+ C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_TITLE_CHANGED)
8282+ }
8383+ if t.onEnquiry != nil {
8484+ C.set_enquiry(t.ptr)
8585+ } else {
8686+ C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_ENQUIRY)
8787+ }
8888+ if t.onXtversion != nil {
8989+ C.set_xtversion(t.ptr)
9090+ } else {
9191+ C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_XTVERSION)
9292+ }
9393+ if t.onSize != nil {
9494+ C.set_size(t.ptr)
9595+ } else {
9696+ C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_SIZE)
9797+ }
9898+ if t.onColorScheme != nil {
9999+ C.set_color_scheme(t.ptr)
100100+ } else {
101101+ C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_COLOR_SCHEME)
102102+ }
103103+ if t.onDeviceAttributes != nil {
104104+ C.set_device_attributes(t.ptr)
105105+ } else {
106106+ C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES)
107107+ }
108108+}
109109+110110+// terminalFromUserdata recovers a *Terminal from the C userdata pointer.
111111+func terminalFromUserdata(userdata unsafe.Pointer) *Terminal {
112112+ return cgo.Handle(userdata).Value().(*Terminal)
113113+}
114114+115115+//export goWritePtyTrampoline
116116+func goWritePtyTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, data *C.uint8_t, length C.size_t) {
117117+ t := terminalFromUserdata(userdata)
118118+ if t.onWritePty != nil {
119119+ t.onWritePty(C.GoBytes(unsafe.Pointer(data), C.int(length)))
120120+ }
121121+}
122122+123123+//export goBellTrampoline
124124+func goBellTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) {
125125+ t := terminalFromUserdata(userdata)
126126+ if t.onBell != nil {
127127+ t.onBell()
128128+ }
129129+}
130130+131131+//export goTitleChangedTrampoline
132132+func goTitleChangedTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) {
133133+ t := terminalFromUserdata(userdata)
134134+ if t.onTitleChanged != nil {
135135+ t.onTitleChanged()
136136+ }
137137+}
138138+139139+//export goEnquiryTrampoline
140140+func goEnquiryTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) C.GhosttyString {
141141+ t := terminalFromUserdata(userdata)
142142+ if t.onEnquiry == nil {
143143+ return C.GhosttyString{}
144144+ }
145145+ return t.effectString(t.onEnquiry())
146146+}
147147+148148+//export goXtversionTrampoline
149149+func goXtversionTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) C.GhosttyString {
150150+ t := terminalFromUserdata(userdata)
151151+ if t.onXtversion == nil {
152152+ return C.GhosttyString{}
153153+ }
154154+ return t.effectString([]byte(t.onXtversion()))
155155+}
156156+157157+//export goSizeTrampoline
158158+func goSizeTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, outSize *C.GhosttySizeReportSize) C.bool {
159159+ t := terminalFromUserdata(userdata)
160160+ if t.onSize == nil {
161161+ return C.bool(false)
162162+ }
163163+ size, ok := t.onSize()
164164+ if !ok {
165165+ return C.bool(false)
166166+ }
167167+ outSize.rows = C.uint16_t(size.Rows)
168168+ outSize.columns = C.uint16_t(size.Columns)
169169+ outSize.cell_width = C.uint32_t(size.CellWidth)
170170+ outSize.cell_height = C.uint32_t(size.CellHeight)
171171+ return C.bool(true)
172172+}
173173+174174+//export goColorSchemeTrampoline
175175+func goColorSchemeTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, outScheme *C.GhosttyColorScheme) C.bool {
176176+ t := terminalFromUserdata(userdata)
177177+ if t.onColorScheme == nil {
178178+ return C.bool(false)
179179+ }
180180+ scheme, ok := t.onColorScheme()
181181+ if !ok {
182182+ return C.bool(false)
183183+ }
184184+ *outScheme = C.GhosttyColorScheme(scheme)
185185+ return C.bool(true)
186186+}
187187+188188+//export goDeviceAttributesTrampoline
189189+func goDeviceAttributesTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, outAttrs *C.GhosttyDeviceAttributes) C.bool {
190190+ t := terminalFromUserdata(userdata)
191191+ if t.onDeviceAttributes == nil {
192192+ return C.bool(false)
193193+ }
194194+ attrs, ok := t.onDeviceAttributes()
195195+ if !ok {
196196+ return C.bool(false)
197197+ }
198198+199199+ // Primary (DA1).
200200+ outAttrs.primary.conformance_level = C.uint16_t(attrs.Primary.ConformanceLevel)
201201+ outAttrs.primary.num_features = C.size_t(attrs.Primary.NumFeatures)
202202+ for i := 0; i < attrs.Primary.NumFeatures && i < 64; i++ {
203203+ outAttrs.primary.features[i] = C.uint16_t(attrs.Primary.Features[i])
204204+ }
205205+206206+ // Secondary (DA2).
207207+ outAttrs.secondary.device_type = C.uint16_t(attrs.Secondary.DeviceType)
208208+ outAttrs.secondary.firmware_version = C.uint16_t(attrs.Secondary.FirmwareVersion)
209209+ outAttrs.secondary.rom_cartridge = C.uint16_t(attrs.Secondary.ROMCartridge)
210210+211211+ // Tertiary (DA3).
212212+ outAttrs.tertiary.unit_id = C.uint32_t(attrs.Tertiary.UnitID)
213213+214214+ return C.bool(true)
215215+}
216216+217217+// effectString copies data into C memory, updates effectBuf, and
218218+// returns a GhosttyString pointing to it. The previous effectBuf
219219+// is freed. Returns a zero-length GhosttyString if data is empty.
220220+func (t *Terminal) effectString(data []byte) C.GhosttyString {
221221+ if t.effectBuf != nil {
222222+ C.free(t.effectBuf)
223223+ }
224224+225225+ if len(data) == 0 {
226226+ return C.GhosttyString{}
227227+ }
228228+229229+ cmem := C.CBytes(data)
230230+ t.effectBuf = cmem
231231+ return C.GhosttyString{
232232+ ptr: (*C.uint8_t)(cmem),
233233+ len: C.size_t(len(data)),
234234+ }
235235+}
+157
terminal_opt.go
···11+package libghostty
22+33+// Terminal option setters wrapping ghostty_terminal_set().
44+// Functions are ordered alphabetically.
55+66+/*
77+#include <ghostty/vt.h>
88+*/
99+import "C"
1010+1111+import "unsafe"
1212+1313+// SetEffectWritePty registers (or clears) the write-pty effect on a
1414+// live terminal. Pass nil to clear.
1515+func (t *Terminal) SetEffectWritePty(fn WritePtyFn) {
1616+ t.onWritePty = fn
1717+ t.syncEffects()
1818+}
1919+2020+// SetEffectBell registers (or clears) the bell effect on a live terminal.
2121+// Pass nil to clear.
2222+func (t *Terminal) SetEffectBell(fn BellFn) {
2323+ t.onBell = fn
2424+ t.syncEffects()
2525+}
2626+2727+// SetEffectTitleChanged registers (or clears) the title-changed effect
2828+// on a live terminal. Pass nil to clear.
2929+func (t *Terminal) SetEffectTitleChanged(fn TitleChangedFn) {
3030+ t.onTitleChanged = fn
3131+ t.syncEffects()
3232+}
3333+3434+// SetEffectEnquiry registers (or clears) the enquiry effect on a live
3535+// terminal. Pass nil to clear.
3636+func (t *Terminal) SetEffectEnquiry(fn EnquiryFn) {
3737+ t.onEnquiry = fn
3838+ t.syncEffects()
3939+}
4040+4141+// SetEffectXtversion registers (or clears) the xtversion effect on a
4242+// live terminal. Pass nil to clear.
4343+func (t *Terminal) SetEffectXtversion(fn XtversionFn) {
4444+ t.onXtversion = fn
4545+ t.syncEffects()
4646+}
4747+4848+// SetEffectSize registers (or clears) the size-report effect on a
4949+// live terminal. Pass nil to clear.
5050+func (t *Terminal) SetEffectSize(fn SizeFn) {
5151+ t.onSize = fn
5252+ t.syncEffects()
5353+}
5454+5555+// SetEffectColorScheme registers (or clears) the color-scheme effect
5656+// on a live terminal. Pass nil to clear.
5757+func (t *Terminal) SetEffectColorScheme(fn ColorSchemeFn) {
5858+ t.onColorScheme = fn
5959+ t.syncEffects()
6060+}
6161+6262+// SetEffectDeviceAttributes registers (or clears) the device-attributes
6363+// effect on a live terminal. Pass nil to clear.
6464+func (t *Terminal) SetEffectDeviceAttributes(fn DeviceAttributesFn) {
6565+ t.onDeviceAttributes = fn
6666+ t.syncEffects()
6767+}
6868+6969+// SetColorBackground sets the default background color. Pass nil to
7070+// clear (unset).
7171+func (t *Terminal) SetColorBackground(c *ColorRGB) error {
7272+ var val unsafe.Pointer
7373+ if c != nil {
7474+ cc := C.GhosttyColorRgb{r: C.uint8_t(c.R), g: C.uint8_t(c.G), b: C.uint8_t(c.B)}
7575+ val = unsafe.Pointer(&cc)
7676+ }
7777+ return resultError(C.ghostty_terminal_set(
7878+ t.ptr,
7979+ C.GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND,
8080+ val,
8181+ ))
8282+}
8383+8484+// SetColorCursor sets the default cursor color. Pass nil to clear (unset).
8585+func (t *Terminal) SetColorCursor(c *ColorRGB) error {
8686+ var val unsafe.Pointer
8787+ if c != nil {
8888+ cc := C.GhosttyColorRgb{r: C.uint8_t(c.R), g: C.uint8_t(c.G), b: C.uint8_t(c.B)}
8989+ val = unsafe.Pointer(&cc)
9090+ }
9191+ return resultError(C.ghostty_terminal_set(
9292+ t.ptr,
9393+ C.GHOSTTY_TERMINAL_OPT_COLOR_CURSOR,
9494+ val,
9595+ ))
9696+}
9797+9898+// SetColorForeground sets the default foreground color. Pass nil to
9999+// clear (unset).
100100+func (t *Terminal) SetColorForeground(c *ColorRGB) error {
101101+ var val unsafe.Pointer
102102+ if c != nil {
103103+ cc := C.GhosttyColorRgb{r: C.uint8_t(c.R), g: C.uint8_t(c.G), b: C.uint8_t(c.B)}
104104+ val = unsafe.Pointer(&cc)
105105+ }
106106+ return resultError(C.ghostty_terminal_set(
107107+ t.ptr,
108108+ C.GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND,
109109+ val,
110110+ ))
111111+}
112112+113113+// SetColorPalette sets the default 256-color palette. Pass nil to reset
114114+// to the built-in default palette.
115115+func (t *Terminal) SetColorPalette(palette *Palette) error {
116116+ var val unsafe.Pointer
117117+ if palette != nil {
118118+ // Convert Go palette to C palette.
119119+ var cp [PaletteSize]C.GhosttyColorRgb
120120+ for i, c := range palette {
121121+ cp[i] = C.GhosttyColorRgb{r: C.uint8_t(c.R), g: C.uint8_t(c.G), b: C.uint8_t(c.B)}
122122+ }
123123+ val = unsafe.Pointer(&cp[0])
124124+ }
125125+ return resultError(C.ghostty_terminal_set(
126126+ t.ptr,
127127+ C.GHOSTTY_TERMINAL_OPT_COLOR_PALETTE,
128128+ val,
129129+ ))
130130+}
131131+132132+// SetPwd sets the terminal working directory manually. An empty string
133133+// clears it.
134134+func (t *Terminal) SetPwd(pwd string) error {
135135+ s := C.GhosttyString{
136136+ ptr: (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(pwd))),
137137+ len: C.size_t(len(pwd)),
138138+ }
139139+ return resultError(C.ghostty_terminal_set(
140140+ t.ptr,
141141+ C.GHOSTTY_TERMINAL_OPT_PWD,
142142+ unsafe.Pointer(&s),
143143+ ))
144144+}
145145+146146+// SetTitle sets the terminal title manually. An empty string clears it.
147147+func (t *Terminal) SetTitle(title string) error {
148148+ s := C.GhosttyString{
149149+ ptr: (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(title))),
150150+ len: C.size_t(len(title)),
151151+ }
152152+ return resultError(C.ghostty_terminal_set(
153153+ t.ptr,
154154+ C.GHOSTTY_TERMINAL_OPT_TITLE,
155155+ unsafe.Pointer(&s),
156156+ ))
157157+}
+233
terminal_opt_test.go
···11+package libghostty
22+33+import "testing"
44+55+func TestTerminalSetTitle(t *testing.T) {
66+ term, err := NewTerminal(WithSize(80, 24))
77+ if err != nil {
88+ t.Fatal(err)
99+ }
1010+ defer term.Close()
1111+1212+ if err := term.SetTitle("my terminal"); err != nil {
1313+ t.Fatal(err)
1414+ }
1515+1616+ // Clear the title.
1717+ if err := term.SetTitle(""); err != nil {
1818+ t.Fatal(err)
1919+ }
2020+}
2121+2222+func TestTerminalSetPwd(t *testing.T) {
2323+ term, err := NewTerminal(WithSize(80, 24))
2424+ if err != nil {
2525+ t.Fatal(err)
2626+ }
2727+ defer term.Close()
2828+2929+ if err := term.SetPwd("/tmp"); err != nil {
3030+ t.Fatal(err)
3131+ }
3232+3333+ if err := term.SetPwd(""); err != nil {
3434+ t.Fatal(err)
3535+ }
3636+}
3737+3838+func TestTerminalSetColors(t *testing.T) {
3939+ term, err := NewTerminal(WithSize(80, 24))
4040+ if err != nil {
4141+ t.Fatal(err)
4242+ }
4343+ defer term.Close()
4444+4545+ white := &ColorRGB{R: 255, G: 255, B: 255}
4646+ black := &ColorRGB{R: 0, G: 0, B: 0}
4747+4848+ if err := term.SetColorForeground(white); err != nil {
4949+ t.Fatal(err)
5050+ }
5151+ if err := term.SetColorBackground(black); err != nil {
5252+ t.Fatal(err)
5353+ }
5454+ if err := term.SetColorCursor(white); err != nil {
5555+ t.Fatal(err)
5656+ }
5757+5858+ // Clear colors.
5959+ if err := term.SetColorForeground(nil); err != nil {
6060+ t.Fatal(err)
6161+ }
6262+ if err := term.SetColorBackground(nil); err != nil {
6363+ t.Fatal(err)
6464+ }
6565+ if err := term.SetColorCursor(nil); err != nil {
6666+ t.Fatal(err)
6767+ }
6868+}
6969+7070+func TestTerminalWithBell(t *testing.T) {
7171+ var bellCount int
7272+ term, err := NewTerminal(WithSize(80, 24), WithBell(func() {
7373+ bellCount++
7474+ }))
7575+ if err != nil {
7676+ t.Fatal(err)
7777+ }
7878+ defer term.Close()
7979+8080+ // BEL character should trigger the callback.
8181+ term.VTWrite([]byte("\x07"))
8282+ if bellCount != 1 {
8383+ t.Fatalf("expected 1 bell, got %d", bellCount)
8484+ }
8585+8686+ // Multiple BELs.
8787+ term.VTWrite([]byte("\x07\x07"))
8888+ if bellCount != 3 {
8989+ t.Fatalf("expected 3 bells, got %d", bellCount)
9090+ }
9191+}
9292+9393+func TestTerminalSetEffectBell(t *testing.T) {
9494+ term, err := NewTerminal(WithSize(80, 24))
9595+ if err != nil {
9696+ t.Fatal(err)
9797+ }
9898+ defer term.Close()
9999+100100+ var bellCount int
101101+ term.SetEffectBell(func() {
102102+ bellCount++
103103+ })
104104+105105+ term.VTWrite([]byte("\x07"))
106106+ if bellCount != 1 {
107107+ t.Fatalf("expected 1 bell, got %d", bellCount)
108108+ }
109109+110110+ // Clear the callback; bell should no longer fire.
111111+ term.SetEffectBell(nil)
112112+ term.VTWrite([]byte("\x07"))
113113+ if bellCount != 1 {
114114+ t.Fatalf("expected still 1 bell after clearing, got %d", bellCount)
115115+ }
116116+}
117117+118118+func TestTerminalWithWritePty(t *testing.T) {
119119+ var received []byte
120120+ term, err := NewTerminal(WithSize(80, 24), WithWritePty(func(data []byte) {
121121+ received = append(received, data...)
122122+ }))
123123+ if err != nil {
124124+ t.Fatal(err)
125125+ }
126126+ defer term.Close()
127127+128128+ // DA1 query should produce a response via write_pty.
129129+ term.VTWrite([]byte("\x1b[c"))
130130+ if len(received) == 0 {
131131+ t.Fatal("expected write_pty data from DA1 query")
132132+ }
133133+}
134134+135135+func TestTerminalWithTitleChanged(t *testing.T) {
136136+ var titleChanged int
137137+ term, err := NewTerminal(WithSize(80, 24), WithTitleChanged(func() {
138138+ titleChanged++
139139+ }))
140140+ if err != nil {
141141+ t.Fatal(err)
142142+ }
143143+ defer term.Close()
144144+145145+ // OSC 2 sets the title.
146146+ term.VTWrite([]byte("\x1b]2;hello\x07"))
147147+ if titleChanged != 1 {
148148+ t.Fatalf("expected 1 title change, got %d", titleChanged)
149149+ }
150150+}
151151+152152+func TestTerminalWithEnquiry(t *testing.T) {
153153+ var received []byte
154154+ term, err := NewTerminal(
155155+ WithSize(80, 24),
156156+ WithWritePty(func(data []byte) {
157157+ received = append(received, data...)
158158+ }),
159159+ WithEnquiry(func() []byte {
160160+ return []byte("hello")
161161+ }),
162162+ )
163163+ if err != nil {
164164+ t.Fatal(err)
165165+ }
166166+ defer term.Close()
167167+168168+ // ENQ character should trigger enquiry and write response via pty.
169169+ term.VTWrite([]byte("\x05"))
170170+ if string(received) != "hello" {
171171+ t.Fatalf("expected enquiry response %q, got %q", "hello", string(received))
172172+ }
173173+}
174174+175175+func TestTerminalWithXtversion(t *testing.T) {
176176+ var received []byte
177177+ term, err := NewTerminal(
178178+ WithSize(80, 24),
179179+ WithWritePty(func(data []byte) {
180180+ received = append(received, data...)
181181+ }),
182182+ WithXtversion(func() string {
183183+ return "myterm 1.0"
184184+ }),
185185+ )
186186+ if err != nil {
187187+ t.Fatal(err)
188188+ }
189189+ defer term.Close()
190190+191191+ // XTVERSION query: CSI > q
192192+ term.VTWrite([]byte("\x1b[>q"))
193193+ // Response should contain our version string in a DCS sequence.
194194+ if len(received) == 0 {
195195+ t.Fatal("expected xtversion response")
196196+ }
197197+ resp := string(received)
198198+ if !contains(resp, "myterm 1.0") {
199199+ t.Fatalf("expected response to contain %q, got %q", "myterm 1.0", resp)
200200+ }
201201+}
202202+203203+// contains reports whether s contains substr.
204204+func contains(s, substr string) bool {
205205+ for i := 0; i+len(substr) <= len(s); i++ {
206206+ if s[i:i+len(substr)] == substr {
207207+ return true
208208+ }
209209+ }
210210+ return false
211211+}
212212+213213+func TestTerminalSetColorPalette(t *testing.T) {
214214+ term, err := NewTerminal(WithSize(80, 24))
215215+ if err != nil {
216216+ t.Fatal(err)
217217+ }
218218+ defer term.Close()
219219+220220+ // Set a custom palette.
221221+ var palette Palette
222222+ for i := range palette {
223223+ palette[i] = ColorRGB{R: uint8(i), G: uint8(i), B: uint8(i)}
224224+ }
225225+ if err := term.SetColorPalette(&palette); err != nil {
226226+ t.Fatal(err)
227227+ }
228228+229229+ // Reset to default.
230230+ if err := term.SetColorPalette(nil); err != nil {
231231+ t.Fatal(err)
232232+ }
233233+}
+233
terminal_test.go
···11+package libghostty
22+33+import (
44+ "fmt"
55+ "io"
66+ "testing"
77+)
88+99+func TestNewTerminalClose(t *testing.T) {
1010+ term, err := NewTerminal(WithSize(80, 24))
1111+ if err != nil {
1212+ t.Fatal(err)
1313+ }
1414+ term.Close()
1515+}
1616+1717+func TestNewTerminalZeroDimensions(t *testing.T) {
1818+ _, err := NewTerminal(WithSize(0, 24))
1919+ if err == nil {
2020+ t.Fatal("expected error for zero cols")
2121+ }
2222+2323+ _, err = NewTerminal(WithSize(80, 0))
2424+ if err == nil {
2525+ t.Fatal("expected error for zero rows")
2626+ }
2727+}
2828+2929+func TestNewTerminalNoOptions(t *testing.T) {
3030+ _, err := NewTerminal()
3131+ if err == nil {
3232+ t.Fatal("expected error when no size is specified")
3333+ }
3434+}
3535+3636+func TestNewTerminalWithScrollback(t *testing.T) {
3737+ term, err := NewTerminal(WithSize(80, 24), WithMaxScrollback(1000))
3838+ if err != nil {
3939+ t.Fatal(err)
4040+ }
4141+ term.Close()
4242+}
4343+4444+func TestTerminalReset(t *testing.T) {
4545+ term, err := NewTerminal(WithSize(80, 24))
4646+ if err != nil {
4747+ t.Fatal(err)
4848+ }
4949+ defer term.Close()
5050+5151+ // Write some data then reset; should not panic or error.
5252+ term.VTWrite([]byte("hello"))
5353+ term.Reset()
5454+}
5555+5656+func TestTerminalResize(t *testing.T) {
5757+ term, err := NewTerminal(WithSize(80, 24))
5858+ if err != nil {
5959+ t.Fatal(err)
6060+ }
6161+ defer term.Close()
6262+6363+ if err := term.Resize(120, 40, 8, 16); err != nil {
6464+ t.Fatal(err)
6565+ }
6666+}
6767+6868+func TestTerminalResizeZero(t *testing.T) {
6969+ term, err := NewTerminal(WithSize(80, 24))
7070+ if err != nil {
7171+ t.Fatal(err)
7272+ }
7373+ defer term.Close()
7474+7575+ if err := term.Resize(0, 24, 8, 16); err == nil {
7676+ t.Fatal("expected error for zero cols")
7777+ }
7878+}
7979+8080+func TestTerminalVTWrite(t *testing.T) {
8181+ term, err := NewTerminal(WithSize(80, 24))
8282+ if err != nil {
8383+ t.Fatal(err)
8484+ }
8585+ defer term.Close()
8686+8787+ // Write plain text and escape sequences; should not panic.
8888+ term.VTWrite([]byte("hello world"))
8989+ term.VTWrite([]byte("\x1b[2J")) // clear screen
9090+ term.VTWrite(nil) // empty write
9191+}
9292+9393+func TestTerminalIOWriter(t *testing.T) {
9494+ term, err := NewTerminal(WithSize(80, 24))
9595+ if err != nil {
9696+ t.Fatal(err)
9797+ }
9898+ defer term.Close()
9999+100100+ // Terminal satisfies io.Writer.
101101+ var w io.Writer = term
102102+ n, err := fmt.Fprintf(w, "hello %s", "world")
103103+ if err != nil {
104104+ t.Fatal(err)
105105+ }
106106+ if n != len("hello world") {
107107+ t.Fatalf("expected %d bytes written, got %d", len("hello world"), n)
108108+ }
109109+}
110110+111111+func TestTerminalModeGetSet(t *testing.T) {
112112+ term, err := NewTerminal(WithSize(80, 24))
113113+ if err != nil {
114114+ t.Fatal(err)
115115+ }
116116+ defer term.Close()
117117+118118+ // Test several modes: set via ModeSet, read back via ModeGet.
119119+ tests := []struct {
120120+ name string
121121+ mode Mode
122122+ defaultVal bool
123123+ }{
124124+ {"CursorVisible", ModeCursorVisible, true},
125125+ {"Wraparound", ModeWraparound, true},
126126+ {"BracketedPaste", ModeBracketedPaste, false},
127127+ {"FocusEvent", ModeFocusEvent, false},
128128+ {"AltScreen", ModeAltScreen, false},
129129+ {"Origin", ModeOrigin, false},
130130+ }
131131+132132+ for _, tt := range tests {
133133+ t.Run(tt.name, func(t *testing.T) {
134134+ // Verify default value.
135135+ val, err := term.ModeGet(tt.mode)
136136+ if err != nil {
137137+ t.Fatal(err)
138138+ }
139139+ if val != tt.defaultVal {
140140+ t.Fatalf("expected default %v, got %v", tt.defaultVal, val)
141141+ }
142142+143143+ // Toggle the mode.
144144+ if err := term.ModeSet(tt.mode, !tt.defaultVal); err != nil {
145145+ t.Fatal(err)
146146+ }
147147+ val, err = term.ModeGet(tt.mode)
148148+ if err != nil {
149149+ t.Fatal(err)
150150+ }
151151+ if val != !tt.defaultVal {
152152+ t.Fatalf("expected %v after set, got %v", !tt.defaultVal, val)
153153+ }
154154+155155+ // Toggle back.
156156+ if err := term.ModeSet(tt.mode, tt.defaultVal); err != nil {
157157+ t.Fatal(err)
158158+ }
159159+ val, err = term.ModeGet(tt.mode)
160160+ if err != nil {
161161+ t.Fatal(err)
162162+ }
163163+ if val != tt.defaultVal {
164164+ t.Fatalf("expected %v after restore, got %v", tt.defaultVal, val)
165165+ }
166166+ })
167167+ }
168168+}
169169+170170+func TestTerminalModeVTWrite(t *testing.T) {
171171+ term, err := NewTerminal(WithSize(80, 24))
172172+ if err != nil {
173173+ t.Fatal(err)
174174+ }
175175+ defer term.Close()
176176+177177+ // Test that VT escape sequences correctly set and reset modes
178178+ // and that ModeGet reads them back.
179179+ // DEC private modes use CSI ? <n> h (set) / CSI ? <n> l (reset).
180180+ // ANSI modes use CSI <n> h (set) / CSI <n> l (reset).
181181+ tests := []struct {
182182+ name string
183183+ mode Mode
184184+ setSeq string
185185+ resetSeq string
186186+ defaultVal bool
187187+ }{
188188+ {"BracketedPaste", ModeBracketedPaste, "\x1b[?2004h", "\x1b[?2004l", false},
189189+ {"CursorVisible", ModeCursorVisible, "\x1b[?25h", "\x1b[?25l", true},
190190+ {"FocusEvent", ModeFocusEvent, "\x1b[?1004h", "\x1b[?1004l", false},
191191+ {"NormalMouse", ModeNormalMouse, "\x1b[?1000h", "\x1b[?1000l", false},
192192+ {"SGRMouse", ModeSGRMouse, "\x1b[?1006h", "\x1b[?1006l", false},
193193+ {"Insert", ModeInsert, "\x1b[4h", "\x1b[4l", false},
194194+ }
195195+196196+ for _, tt := range tests {
197197+ t.Run(tt.name, func(t *testing.T) {
198198+ // Verify default.
199199+ val, err := term.ModeGet(tt.mode)
200200+ if err != nil {
201201+ t.Fatal(err)
202202+ }
203203+ if val != tt.defaultVal {
204204+ t.Fatalf("expected default %v, got %v", tt.defaultVal, val)
205205+ }
206206+207207+ // Set via VT escape sequence.
208208+ term.VTWrite([]byte(tt.setSeq))
209209+ val, err = term.ModeGet(tt.mode)
210210+ if err != nil {
211211+ t.Fatal(err)
212212+ }
213213+ if !val {
214214+ t.Fatal("expected mode set after VT write set sequence")
215215+ }
216216+217217+ // Reset via VT escape sequence.
218218+ term.VTWrite([]byte(tt.resetSeq))
219219+ val, err = term.ModeGet(tt.mode)
220220+ if err != nil {
221221+ t.Fatal(err)
222222+ }
223223+ if val {
224224+ t.Fatal("expected mode reset after VT write reset sequence")
225225+ }
226226+227227+ // Restore to default for next subtest.
228228+ if tt.defaultVal {
229229+ term.VTWrite([]byte(tt.setSeq))
230230+ }
231231+ })
232232+ }
233233+}