Go bindings for libghostty-vt.
0
fork

Configure Feed

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.

+5782
+1
.envrc
··· 1 + use flake
+135
.github/workflows/test.yml
··· 1 + on: [push, pull_request] 2 + name: CI 3 + 4 + concurrency: 5 + group: ${{ github.workflow }}-${{ github.ref_name != 'main' && github.ref || github.run_id }} 6 + cancel-in-progress: true 7 + 8 + jobs: 9 + fmt: 10 + runs-on: namespace-profile-linux-sm 11 + steps: 12 + - name: Checkout code 13 + uses: actions/checkout@v4 14 + - name: Setup Cache 15 + uses: namespacelabs/nscloud-cache-action@v1 16 + with: 17 + path: /nix 18 + - name: Setup Nix 19 + uses: cachix/install-nix-action@v31 20 + with: 21 + nix_path: nixpkgs=channel:nixos-unstable 22 + - name: Check formatting 23 + run: nix develop -c sh -c 'test -z "$(gofmt -l .)"' 24 + 25 + test: 26 + runs-on: namespace-profile-linux-sm 27 + env: 28 + ZIG_LOCAL_CACHE_DIR: /zig/local-cache 29 + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache 30 + steps: 31 + - name: Checkout code 32 + uses: actions/checkout@v4 33 + - name: Setup Cache 34 + uses: namespacelabs/nscloud-cache-action@v1 35 + with: 36 + path: | 37 + /nix 38 + /zig 39 + - name: Setup Nix 40 + uses: cachix/install-nix-action@v31 41 + with: 42 + nix_path: nixpkgs=channel:nixos-unstable 43 + - name: Build libghostty 44 + run: nix develop -c make build 45 + - name: Test 46 + run: nix develop -c make test 47 + 48 + list-examples: 49 + runs-on: namespace-profile-linux-sm 50 + outputs: 51 + examples: ${{ steps.list.outputs.examples }} 52 + steps: 53 + - name: Checkout code 54 + uses: actions/checkout@v4 55 + - id: list 56 + name: List example directories 57 + run: | 58 + examples=$(ls -d examples/*/main.go 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))') 59 + echo "$examples" | jq . 60 + echo "examples=$examples" >> "$GITHUB_OUTPUT" 61 + 62 + examples: 63 + strategy: 64 + fail-fast: false 65 + matrix: 66 + example: ${{ fromJSON(needs.list-examples.outputs.examples) }} 67 + name: Example ${{ matrix.example }} 68 + runs-on: namespace-profile-linux-sm 69 + needs: [list-examples] 70 + env: 71 + ZIG_LOCAL_CACHE_DIR: /zig/local-cache 72 + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache 73 + steps: 74 + - name: Checkout code 75 + uses: actions/checkout@v4 76 + - name: Setup Cache 77 + uses: namespacelabs/nscloud-cache-action@v1 78 + with: 79 + path: | 80 + /nix 81 + /zig 82 + - name: Setup Nix 83 + uses: cachix/install-nix-action@v31 84 + with: 85 + nix_path: nixpkgs=channel:nixos-unstable 86 + - name: Build libghostty 87 + run: nix develop -c make build 88 + - name: Run example 89 + run: nix develop -c go run ./examples/${{ matrix.example }}/ 90 + 91 + build-shared: 92 + runs-on: namespace-profile-linux-sm 93 + env: 94 + ZIG_LOCAL_CACHE_DIR: /zig/local-cache 95 + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache 96 + steps: 97 + - name: Checkout code 98 + uses: actions/checkout@v4 99 + - name: Setup Cache 100 + uses: namespacelabs/nscloud-cache-action@v1 101 + with: 102 + path: | 103 + /nix 104 + /zig 105 + - name: Setup Nix 106 + uses: cachix/install-nix-action@v31 107 + with: 108 + nix_path: nixpkgs=channel:nixos-unstable 109 + - name: Build libghostty 110 + run: nix develop -c make build 111 + - name: Build (shared) 112 + run: nix develop -c go build ./... 113 + 114 + build-static: 115 + runs-on: namespace-profile-linux-sm 116 + env: 117 + ZIG_LOCAL_CACHE_DIR: /zig/local-cache 118 + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache 119 + steps: 120 + - name: Checkout code 121 + uses: actions/checkout@v4 122 + - name: Setup Cache 123 + uses: namespacelabs/nscloud-cache-action@v1 124 + with: 125 + path: | 126 + /nix 127 + /zig 128 + - name: Setup Nix 129 + uses: cachix/install-nix-action@v31 130 + with: 131 + nix_path: nixpkgs=channel:nixos-unstable 132 + - name: Build libghostty 133 + run: nix develop -c make build 134 + - name: Build (static) 135 + run: nix develop -c go build -tags static ./...
+3
.gitignore
··· 1 + .direnv/ 2 + build/ 3 + result
+29
AGENTS.md
··· 1 + # Libghosty Go Bindings 2 + 3 + ## Commands 4 + 5 + - **Build:** `make build` (runs CMake + `go build`) 6 + - **Test:** `make test` 7 + - **Clean:** `make clean` (removes `build/`, re-triggers CMake fetch on next build) 8 + 9 + ## Build 10 + 11 + - CMake fetches ghostty source via `FetchContent`; pinned commit is in `CMakeLists.txt` 12 + - Never modify anything under `build/` 13 + - cgo links static library by default; `-tags dynamic` links shared 14 + 15 + ## Code Guidelines 16 + 17 + - Ghostty headers are at `build/_deps/ghostty-src/zig-out/include/ghostty/` 18 + - Use a heavy commenting style that explains what/why clearly. 19 + - Comment every exported function, type, field, etc. 20 + - Types should be in the same filename as the C header, by default. 21 + If they're already moved, keep them where they are. For example, 22 + `color.h` types should go in `color.go`. 23 + - If a type is a thin layer over a C type, comment with what 24 + C type it corresponds to, e.g. `C: GhosttyResult` 25 + - When a C API uses `GhosttyString` with `len=0`, libghostty treats 26 + the pointer as garbage, so no nil special-casing is needed. 27 + - Define constants by their C reference, e.g. 28 + `DAConformanceVT100 = C.GHOSTTY_DA_CONFORMANCE_VT100`. Don't 29 + hardcode values.
+9
CMakeLists.txt
··· 1 + cmake_minimum_required(VERSION 3.19) 2 + project(go-libghostty LANGUAGES C) 3 + 4 + include(FetchContent) 5 + FetchContent_Declare(ghostty 6 + GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git 7 + GIT_TAG 48a01b8bd51b0cf4ba3ed281a6662ae131ee8239 8 + ) 9 + FetchContent_MakeAvailable(ghostty)
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2026 Mitchell Hashimoto 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+26
Makefile
··· 1 + BUILD_DIR := build 2 + 3 + # FetchContent places the ghostty source here. 4 + GHOSTTY_ZIG_OUT := $(CURDIR)/$(BUILD_DIR)/_deps/ghostty-src/zig-out 5 + PKG_CONFIG_PATH := $(GHOSTTY_ZIG_OUT)/share/pkgconfig 6 + DYLD_LIBRARY_PATH := $(GHOSTTY_ZIG_OUT)/lib 7 + LD_LIBRARY_PATH := $(GHOSTTY_ZIG_OUT)/lib 8 + 9 + # Stamp file to track whether the cmake build has run. 10 + STAMP := $(BUILD_DIR)/.ghostty-built 11 + 12 + .PHONY: build test clean 13 + 14 + $(STAMP): 15 + cmake -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release 16 + cmake --build $(BUILD_DIR) 17 + @touch $(STAMP) 18 + 19 + build: $(STAMP) 20 + PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) go build ./... 21 + 22 + test: $(STAMP) 23 + PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) DYLD_LIBRARY_PATH=$(DYLD_LIBRARY_PATH) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH) go test ./... 24 + 25 + clean: 26 + rm -rf $(BUILD_DIR)
+101
README.md
··· 1 + # Go Libghostty Bindings 2 + 3 + Go bindings for `libghostty-vt`. 4 + 5 + This project uses [cgo](https://pkg.go.dev/cmd/cgo) but `libghostty-vt` 6 + only depends on libc/libc++, so it is very easy to static link and very 7 + easy to cross-compile. The bindings default to static linking for this 8 + reason. 9 + 10 + > [!WARNING] 11 + > 12 + > **I'm not promising any API stability yet.** This is a new project and the 13 + > API may change as necessary. The underlying functionality is very stable, 14 + > but the Go API is still being designed. 15 + 16 + ## Example 17 + 18 + ```go 19 + package main 20 + 21 + import ( 22 + "fmt" 23 + "log" 24 + 25 + "github.com/mitchellh/go-libghostty" 26 + ) 27 + 28 + func main() { 29 + term, err := libghostty.NewTerminal(libghostty.WithSize(80, 24)) 30 + if err != nil { 31 + log.Fatal(err) 32 + } 33 + defer term.Close() 34 + 35 + // Feed VT data — bold green "world", then plain text. 36 + fmt.Fprintf(term, "Hello, \033[1;32mworld\033[0m!\r\n") 37 + 38 + // Format the terminal contents as plain text. 39 + f, err := libghostty.NewFormatter(term, 40 + libghostty.WithFormatterFormat(libghostty.FormatterFormatPlain), 41 + libghostty.WithFormatterTrim(true), 42 + ) 43 + if err != nil { 44 + log.Fatal(err) 45 + } 46 + defer f.Close() 47 + 48 + output, _ := f.FormatString() 49 + fmt.Println(output) // Hello, world! 50 + } 51 + ``` 52 + 53 + More examples are in the [`examples/`](examples/) directory. 54 + 55 + ## Usage 56 + 57 + Add the module to your Go project: 58 + 59 + ```shell 60 + go get github.com/mitchellh/go-libghostty 61 + ``` 62 + 63 + This is a cgo package that links `libghostty-vt` via `pkg-config`. By 64 + default it links statically. Before building your project, you need the 65 + library installed. Either install it system-wide or set `PKG_CONFIG_PATH` 66 + to point to a local checkout: 67 + 68 + ```shell 69 + export PKG_CONFIG_PATH=/path/to/libghostty-vt/share/pkgconfig 70 + ``` 71 + 72 + To link dynamically instead (requires the shared library at runtime, 73 + so you'll also need to set the library path): 74 + 75 + ```shell 76 + go build -tags dynamic 77 + ``` 78 + 79 + See the [Ghostty docs](https://ghostty.org/docs/install/build) for 80 + building `libghostty-vt` from source. 81 + 82 + ## Development 83 + 84 + CMake fetches and builds `libghostty-vt` automatically. CMake is only 85 + required and used for development of this module. For actual downstream 86 + usage, you can get `libghostty-vt` available however you like (e.g. system 87 + package, local checkout, etc.). 88 + 89 + You need [Zig](https://ghostty.org/docs/install/build) and CMake on your PATH. 90 + 91 + ```shell 92 + make build 93 + make test 94 + 95 + # If in a Nix dev shell: 96 + go build 97 + go test 98 + ``` 99 + 100 + If you use the Nix dev shell (`nix develop`), `go build` and `go test` 101 + work directly — the shell configures all paths automatically.
+18
TODO.md
··· 1 + # Missing APIs 2 + 3 + ## Not Bound 4 + 5 + - [ ] Key encoding (`key.h`, `key/encoder.h`, `key/event.h`) 6 + - [ ] Mouse encoding (`mouse.h`, `mouse/encoder.h`, `mouse/event.h`) 7 + - [ ] Render state (`render.h`) 8 + - [ ] OSC parser (`osc.h`) 9 + - [ ] SGR parser (`sgr.h`) 10 + - [ ] Paste utilities (`paste.h`) 11 + - [ ] Focus encoding (`focus.h`) 12 + 13 + ## Partially Bound 14 + 15 + - [ ] `ghostty_mode_report_encode()` 16 + - [ ] `ghostty_size_report_encode()` 17 + - [ ] `ghostty_type_json()` 18 + - [ ] `ghostty_style_default()`
+125
build_info.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + #include <ghostty/vt/build_info.h> 6 + */ 7 + import "C" 8 + 9 + import "unsafe" 10 + 11 + // OptimizeMode identifies the optimization mode the library was built with. 12 + // C: GhosttyOptimizeMode 13 + type OptimizeMode int 14 + 15 + const ( 16 + // OptimizeDebug is the debug optimization mode. 17 + OptimizeDebug OptimizeMode = C.GHOSTTY_OPTIMIZE_DEBUG 18 + 19 + // OptimizeReleaseSafe is the release-safe optimization mode. 20 + OptimizeReleaseSafe OptimizeMode = C.GHOSTTY_OPTIMIZE_RELEASE_SAFE 21 + 22 + // OptimizeReleaseSmall is the release-small optimization mode. 23 + OptimizeReleaseSmall OptimizeMode = C.GHOSTTY_OPTIMIZE_RELEASE_SMALL 24 + 25 + // OptimizeReleaseFast is the release-fast optimization mode. 26 + OptimizeReleaseFast OptimizeMode = C.GHOSTTY_OPTIMIZE_RELEASE_FAST 27 + ) 28 + 29 + // BuildInfo holds compile-time build configuration of libghostty-vt. 30 + // All values are constant for the lifetime of the process. 31 + // C: GhosttyBuildInfo (query enum) 32 + type BuildInfo struct { 33 + // SIMD reports whether SIMD-accelerated code paths are enabled. 34 + SIMD bool 35 + 36 + // KittyGraphics reports whether Kitty graphics protocol support 37 + // is available. 38 + KittyGraphics bool 39 + 40 + // TmuxControlMode reports whether tmux control mode support 41 + // is available. 42 + TmuxControlMode bool 43 + 44 + // Optimize is the optimization mode the library was built with. 45 + Optimize OptimizeMode 46 + 47 + // VersionString is the full version string 48 + // (e.g. "1.2.3" or "1.2.3-dev+abcdef"). 49 + VersionString string 50 + 51 + // VersionMajor is the major version number. 52 + VersionMajor uint 53 + 54 + // VersionMinor is the minor version number. 55 + VersionMinor uint 56 + 57 + // VersionPatch is the patch version number. 58 + VersionPatch uint 59 + 60 + // VersionBuild is the build metadata string (e.g. commit hash). 61 + // Empty if no build metadata is present. 62 + VersionBuild string 63 + } 64 + 65 + // GetBuildInfo queries all compile-time build configuration values 66 + // and returns them in a single BuildInfo struct. 67 + func GetBuildInfo() (BuildInfo, error) { 68 + var info BuildInfo 69 + 70 + var simd C.bool 71 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_SIMD, unsafe.Pointer(&simd))); err != nil { 72 + return info, err 73 + } 74 + info.SIMD = bool(simd) 75 + 76 + var kitty C.bool 77 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_KITTY_GRAPHICS, unsafe.Pointer(&kitty))); err != nil { 78 + return info, err 79 + } 80 + info.KittyGraphics = bool(kitty) 81 + 82 + var tmux C.bool 83 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_TMUX_CONTROL_MODE, unsafe.Pointer(&tmux))); err != nil { 84 + return info, err 85 + } 86 + info.TmuxControlMode = bool(tmux) 87 + 88 + var opt C.GhosttyOptimizeMode 89 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_OPTIMIZE, unsafe.Pointer(&opt))); err != nil { 90 + return info, err 91 + } 92 + info.Optimize = OptimizeMode(opt) 93 + 94 + var verStr C.GhosttyString 95 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_STRING, unsafe.Pointer(&verStr))); err != nil { 96 + return info, err 97 + } 98 + info.VersionString = C.GoStringN((*C.char)(unsafe.Pointer(verStr.ptr)), C.int(verStr.len)) 99 + 100 + var major C.size_t 101 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_MAJOR, unsafe.Pointer(&major))); err != nil { 102 + return info, err 103 + } 104 + info.VersionMajor = uint(major) 105 + 106 + var minor C.size_t 107 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_MINOR, unsafe.Pointer(&minor))); err != nil { 108 + return info, err 109 + } 110 + info.VersionMinor = uint(minor) 111 + 112 + var patch C.size_t 113 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_PATCH, unsafe.Pointer(&patch))); err != nil { 114 + return info, err 115 + } 116 + info.VersionPatch = uint(patch) 117 + 118 + var verBuild C.GhosttyString 119 + if err := resultError(C.ghostty_build_info(C.GHOSTTY_BUILD_INFO_VERSION_BUILD, unsafe.Pointer(&verBuild))); err != nil { 120 + return info, err 121 + } 122 + info.VersionBuild = C.GoStringN((*C.char)(unsafe.Pointer(verBuild.ptr)), C.int(verBuild.len)) 123 + 124 + return info, nil 125 + }
+27
build_info_test.go
··· 1 + package libghostty 2 + 3 + import "testing" 4 + 5 + func TestGetBuildInfo(t *testing.T) { 6 + info, err := GetBuildInfo() 7 + if err != nil { 8 + t.Fatal(err) 9 + } 10 + 11 + // Version string should be non-empty. 12 + if info.VersionString == "" { 13 + t.Fatal("expected non-empty version string") 14 + } 15 + 16 + // At least one version component should be non-zero. 17 + if info.VersionMajor == 0 && info.VersionMinor == 0 && info.VersionPatch == 0 { 18 + t.Fatal("expected at least one non-zero version component") 19 + } 20 + 21 + // Optimize mode should be a known value. 22 + switch info.Optimize { 23 + case OptimizeDebug, OptimizeReleaseSafe, OptimizeReleaseSmall, OptimizeReleaseFast: 24 + default: 25 + t.Fatalf("unexpected optimize mode: %d", info.Optimize) 26 + } 27 + }
+6
cgo_shared.go
··· 1 + //go:build dynamic 2 + 3 + package libghostty 4 + 5 + // #cgo pkg-config: libghostty-vt 6 + import "C"
+7
cgo_static.go
··· 1 + //go:build !dynamic 2 + 3 + package libghostty 4 + 5 + // #cgo pkg-config: --static libghostty-vt-static 6 + // #cgo CFLAGS: -DGHOSTTY_STATIC 7 + import "C"
+41
color.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + */ 6 + import "C" 7 + 8 + // ColorRGB represents an RGB color value. 9 + // C: GhosttyColorRgb 10 + type ColorRGB struct { 11 + R uint8 12 + G uint8 13 + B uint8 14 + } 15 + 16 + // PaletteSize is the number of entries in a terminal color palette. 17 + const PaletteSize = 256 18 + 19 + // Palette is a 256-color palette. 20 + type Palette [PaletteSize]ColorRGB 21 + 22 + // Named color palette indices. 23 + // C: GHOSTTY_COLOR_NAMED_* 24 + const ( 25 + ColorNamedBlack = C.GHOSTTY_COLOR_NAMED_BLACK 26 + ColorNamedRed = C.GHOSTTY_COLOR_NAMED_RED 27 + ColorNamedGreen = C.GHOSTTY_COLOR_NAMED_GREEN 28 + ColorNamedYellow = C.GHOSTTY_COLOR_NAMED_YELLOW 29 + ColorNamedBlue = C.GHOSTTY_COLOR_NAMED_BLUE 30 + ColorNamedMagenta = C.GHOSTTY_COLOR_NAMED_MAGENTA 31 + ColorNamedCyan = C.GHOSTTY_COLOR_NAMED_CYAN 32 + ColorNamedWhite = C.GHOSTTY_COLOR_NAMED_WHITE 33 + ColorNamedBrightBlack = C.GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 34 + ColorNamedBrightRed = C.GHOSTTY_COLOR_NAMED_BRIGHT_RED 35 + ColorNamedBrightGreen = C.GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 36 + ColorNamedBrightYellow = C.GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 37 + ColorNamedBrightBlue = C.GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 38 + ColorNamedBrightMagenta = C.GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 39 + ColorNamedBrightCyan = C.GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 40 + ColorNamedBrightWhite = C.GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 41 + )
+127
device.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + */ 6 + import "C" 7 + 8 + // ColorScheme identifies the terminal color scheme (light or dark). 9 + // C: GhosttyColorScheme 10 + type ColorScheme int 11 + 12 + const ( 13 + // ColorSchemeLight indicates a light color scheme. 14 + ColorSchemeLight ColorScheme = C.GHOSTTY_COLOR_SCHEME_LIGHT 15 + 16 + // ColorSchemeDark indicates a dark color scheme. 17 + ColorSchemeDark ColorScheme = C.GHOSTTY_COLOR_SCHEME_DARK 18 + ) 19 + 20 + // DA1 conformance levels (Pp parameter). 21 + // C: GHOSTTY_DA_CONFORMANCE_* 22 + const ( 23 + DAConformanceVT100 = C.GHOSTTY_DA_CONFORMANCE_VT100 24 + DAConformanceVT101 = C.GHOSTTY_DA_CONFORMANCE_VT101 25 + DAConformanceVT102 = C.GHOSTTY_DA_CONFORMANCE_VT102 26 + DAConformanceVT125 = C.GHOSTTY_DA_CONFORMANCE_VT125 27 + DAConformanceVT131 = C.GHOSTTY_DA_CONFORMANCE_VT131 28 + DAConformanceVT132 = C.GHOSTTY_DA_CONFORMANCE_VT132 29 + DAConformanceVT220 = C.GHOSTTY_DA_CONFORMANCE_VT220 30 + DAConformanceVT240 = C.GHOSTTY_DA_CONFORMANCE_VT240 31 + DAConformanceVT320 = C.GHOSTTY_DA_CONFORMANCE_VT320 32 + DAConformanceVT340 = C.GHOSTTY_DA_CONFORMANCE_VT340 33 + DAConformanceVT420 = C.GHOSTTY_DA_CONFORMANCE_VT420 34 + DAConformanceVT510 = C.GHOSTTY_DA_CONFORMANCE_VT510 35 + DAConformanceVT520 = C.GHOSTTY_DA_CONFORMANCE_VT520 36 + DAConformanceVT525 = C.GHOSTTY_DA_CONFORMANCE_VT525 37 + DAConformanceLevel2 = C.GHOSTTY_DA_CONFORMANCE_LEVEL_2 38 + DAConformanceLevel3 = C.GHOSTTY_DA_CONFORMANCE_LEVEL_3 39 + DAConformanceLevel4 = C.GHOSTTY_DA_CONFORMANCE_LEVEL_4 40 + DAConformanceLevel5 = C.GHOSTTY_DA_CONFORMANCE_LEVEL_5 41 + ) 42 + 43 + // DA1 feature codes (Ps parameters). 44 + // C: GHOSTTY_DA_FEATURE_* 45 + const ( 46 + DAFeatureColumns132 = C.GHOSTTY_DA_FEATURE_COLUMNS_132 47 + DAFeaturePrinter = C.GHOSTTY_DA_FEATURE_PRINTER 48 + DAFeatureReGIS = C.GHOSTTY_DA_FEATURE_REGIS 49 + DAFeatureSixel = C.GHOSTTY_DA_FEATURE_SIXEL 50 + DAFeatureSelectiveErase = C.GHOSTTY_DA_FEATURE_SELECTIVE_ERASE 51 + DAFeatureUserDefinedKeys = C.GHOSTTY_DA_FEATURE_USER_DEFINED_KEYS 52 + DAFeatureNationalReplacement = C.GHOSTTY_DA_FEATURE_NATIONAL_REPLACEMENT 53 + DAFeatureTechnicalCharacters = C.GHOSTTY_DA_FEATURE_TECHNICAL_CHARACTERS 54 + DAFeatureLocator = C.GHOSTTY_DA_FEATURE_LOCATOR 55 + DAFeatureTerminalState = C.GHOSTTY_DA_FEATURE_TERMINAL_STATE 56 + DAFeatureWindowing = C.GHOSTTY_DA_FEATURE_WINDOWING 57 + DAFeatureHorizontalScrolling = C.GHOSTTY_DA_FEATURE_HORIZONTAL_SCROLLING 58 + DAFeatureANSIColor = C.GHOSTTY_DA_FEATURE_ANSI_COLOR 59 + DAFeatureRectangularEditing = C.GHOSTTY_DA_FEATURE_RECTANGULAR_EDITING 60 + DAFeatureANSITextLocator = C.GHOSTTY_DA_FEATURE_ANSI_TEXT_LOCATOR 61 + DAFeatureClipboard = C.GHOSTTY_DA_FEATURE_CLIPBOARD 62 + ) 63 + 64 + // DA2 device type identifiers (Pp parameter). 65 + // C: GHOSTTY_DA_DEVICE_TYPE_* 66 + const ( 67 + DADeviceTypeVT100 = C.GHOSTTY_DA_DEVICE_TYPE_VT100 68 + DADeviceTypeVT220 = C.GHOSTTY_DA_DEVICE_TYPE_VT220 69 + DADeviceTypeVT240 = C.GHOSTTY_DA_DEVICE_TYPE_VT240 70 + DADeviceTypeVT330 = C.GHOSTTY_DA_DEVICE_TYPE_VT330 71 + DADeviceTypeVT340 = C.GHOSTTY_DA_DEVICE_TYPE_VT340 72 + DADeviceTypeVT320 = C.GHOSTTY_DA_DEVICE_TYPE_VT320 73 + DADeviceTypeVT382 = C.GHOSTTY_DA_DEVICE_TYPE_VT382 74 + DADeviceTypeVT420 = C.GHOSTTY_DA_DEVICE_TYPE_VT420 75 + DADeviceTypeVT510 = C.GHOSTTY_DA_DEVICE_TYPE_VT510 76 + DADeviceTypeVT520 = C.GHOSTTY_DA_DEVICE_TYPE_VT520 77 + DADeviceTypeVT525 = C.GHOSTTY_DA_DEVICE_TYPE_VT525 78 + ) 79 + 80 + // DeviceAttributes holds the response data for all three DA levels. 81 + // The terminal fills whichever sub-struct matches the request type. 82 + // C: GhosttyDeviceAttributes 83 + type DeviceAttributes struct { 84 + // Primary is the DA1 response data (CSI c). 85 + Primary DeviceAttributesPrimary 86 + 87 + // Secondary is the DA2 response data (CSI > c). 88 + Secondary DeviceAttributesSecondary 89 + 90 + // Tertiary is the DA3 response data (CSI = c). 91 + Tertiary DeviceAttributesTertiary 92 + } 93 + 94 + // DeviceAttributesPrimary holds primary device attributes (DA1). 95 + // C: GhosttyDeviceAttributesPrimary 96 + type DeviceAttributesPrimary struct { 97 + // ConformanceLevel is the Pp parameter. E.g. 62 for VT220. 98 + ConformanceLevel uint16 99 + 100 + // Features contains the DA1 feature codes (Ps parameters). 101 + // Only the first NumFeatures entries are valid. 102 + Features [64]uint16 103 + 104 + // NumFeatures is the number of valid entries in Features. 105 + NumFeatures int 106 + } 107 + 108 + // DeviceAttributesSecondary holds secondary device attributes (DA2). 109 + // C: GhosttyDeviceAttributesSecondary 110 + type DeviceAttributesSecondary struct { 111 + // DeviceType is the terminal type identifier (Pp). E.g. 1 for VT220. 112 + DeviceType uint16 113 + 114 + // FirmwareVersion is the firmware/patch version number (Pv). 115 + FirmwareVersion uint16 116 + 117 + // ROMCartridge is the ROM cartridge registration number (Pc). 118 + // Always 0 for emulators. 119 + ROMCartridge uint16 120 + } 121 + 122 + // DeviceAttributesTertiary holds tertiary device attributes (DA3). 123 + // C: GhosttyDeviceAttributesTertiary 124 + type DeviceAttributesTertiary struct { 125 + // UnitID is encoded as 8 uppercase hex digits in the response. 126 + UnitID uint32 127 + }
+51
doc.go
··· 1 + // Package libghostty provides Go bindings for libghostty-vt, a 2 + // virtual terminal emulator library from the Ghostty project. 3 + // 4 + // # Getting Started 5 + // 6 + // Create a terminal with [NewTerminal], feed it input with 7 + // [Terminal.VTWrite] (or [Terminal.Write] for an [io.Writer]), and 8 + // inspect state through data getters such as [Terminal.CursorX], 9 + // [Terminal.Title], and [Terminal.ActiveScreen]. When finished, call 10 + // [Terminal.Close] to release resources. 11 + // 12 + // term, err := libghostty.NewTerminal( 13 + // libghostty.WithSize(80, 24), 14 + // libghostty.WithMaxScrollback(1000), 15 + // ) 16 + // if err != nil { 17 + // log.Fatal(err) 18 + // } 19 + // defer term.Close() 20 + // 21 + // term.VTWrite([]byte("Hello, world!\r\n")) 22 + // 23 + // # Effects 24 + // 25 + // The terminal communicates side-effects back to the host through 26 + // effect callbacks. Register them at creation time with functional 27 + // options like [WithWritePty], [WithBell], and [WithEnquiry], or 28 + // on a live terminal with [Terminal.SetEffectWritePty] and friends. 29 + // 30 + // [WithWritePty] is the most common effect — it delivers data that 31 + // the terminal wants to send back to the pty (e.g. query responses): 32 + // 33 + // term, _ := libghostty.NewTerminal( 34 + // libghostty.WithSize(80, 24), 35 + // libghostty.WithWritePty(func(data []byte) { 36 + // os.Stdout.Write(data) 37 + // }), 38 + // ) 39 + // 40 + // # Terminal Options 41 + // 42 + // Terminal properties can be changed after creation with setter 43 + // methods such as [Terminal.SetColorForeground], 44 + // [Terminal.SetColorBackground], [Terminal.SetColorPalette], 45 + // [Terminal.SetTitle], and [Terminal.SetPwd]. 46 + // 47 + // # Linking 48 + // 49 + // This is a cgo package. By default it links the shared library via 50 + // pkg-config. Build with "-tags static" to link statically instead. 51 + package libghostty
+38
examples/build-info/main.go
··· 1 + // Example build-info demonstrates querying libghostty's compile-time 2 + // build configuration using the GetBuildInfo API. 3 + package main 4 + 5 + import ( 6 + "fmt" 7 + "log" 8 + 9 + "github.com/mitchellh/go-libghostty" 10 + ) 11 + 12 + func boolStr(b bool) string { 13 + if b { 14 + return "enabled" 15 + } 16 + return "disabled" 17 + } 18 + 19 + func main() { 20 + info, err := libghostty.GetBuildInfo() 21 + if err != nil { 22 + log.Fatal(err) 23 + } 24 + 25 + fmt.Printf("SIMD: %s\n", boolStr(info.SIMD)) 26 + fmt.Printf("Kitty graphics: %s\n", boolStr(info.KittyGraphics)) 27 + fmt.Printf("Tmux control mode: %s\n", boolStr(info.TmuxControlMode)) 28 + 29 + fmt.Printf("Version: %s\n", info.VersionString) 30 + fmt.Printf("Version major: %d\n", info.VersionMajor) 31 + fmt.Printf("Version minor: %d\n", info.VersionMinor) 32 + fmt.Printf("Version patch: %d\n", info.VersionPatch) 33 + if info.VersionBuild != "" { 34 + fmt.Printf("Version build: %s\n", info.VersionBuild) 35 + } else { 36 + fmt.Printf("Version build: (none)\n") 37 + } 38 + }
+126
examples/colors/main.go
··· 1 + // Command colors demonstrates the libghostty color APIs: setting and 2 + // querying foreground, background, cursor, and palette colors, as well 3 + // as the distinction between "effective" (OSC-overridden) and "default" 4 + // values. 5 + package main 6 + 7 + import ( 8 + "fmt" 9 + "log" 10 + 11 + ghostty "github.com/mitchellh/go-libghostty" 12 + ) 13 + 14 + func main() { 15 + // Step 1: Create an 80×24 terminal with no scrollback. 16 + t, err := ghostty.NewTerminal( 17 + ghostty.WithSize(80, 24), 18 + ghostty.WithMaxScrollback(0), 19 + ) 20 + if err != nil { 21 + log.Fatal(err) 22 + } 23 + defer t.Close() 24 + 25 + // Step 2: Print colors before any configuration — everything is unset. 26 + fmt.Println("=== Before setting colors ===") 27 + printColors(t) 28 + 29 + // Step 3: Apply a Catppuccin-inspired dark theme via the config API. 30 + if err := t.SetColorForeground(&ghostty.ColorRGB{R: 205, G: 214, B: 244}); err != nil { 31 + log.Fatal(err) 32 + } 33 + if err := t.SetColorBackground(&ghostty.ColorRGB{R: 30, G: 30, B: 46}); err != nil { 34 + log.Fatal(err) 35 + } 36 + if err := t.SetColorCursor(&ghostty.ColorRGB{R: 245, G: 224, B: 220}); err != nil { 37 + log.Fatal(err) 38 + } 39 + 40 + // Override the first 8 palette entries with Catppuccin colors. 41 + palette, err := t.ColorPalette() 42 + if err != nil { 43 + log.Fatal(err) 44 + } 45 + palette[ghostty.ColorNamedBlack] = ghostty.ColorRGB{R: 69, G: 71, B: 90} 46 + palette[ghostty.ColorNamedRed] = ghostty.ColorRGB{R: 243, G: 139, B: 168} 47 + palette[ghostty.ColorNamedGreen] = ghostty.ColorRGB{R: 166, G: 227, B: 161} 48 + palette[ghostty.ColorNamedYellow] = ghostty.ColorRGB{R: 249, G: 226, B: 175} 49 + palette[ghostty.ColorNamedBlue] = ghostty.ColorRGB{R: 137, G: 180, B: 250} 50 + palette[ghostty.ColorNamedMagenta] = ghostty.ColorRGB{R: 245, G: 194, B: 231} 51 + palette[ghostty.ColorNamedCyan] = ghostty.ColorRGB{R: 148, G: 226, B: 213} 52 + palette[ghostty.ColorNamedWhite] = ghostty.ColorRGB{R: 186, G: 194, B: 222} 53 + if err := t.SetColorPalette(palette); err != nil { 54 + log.Fatal(err) 55 + } 56 + 57 + // Step 4: Print colors after applying the theme. 58 + fmt.Println("\n=== After setting Catppuccin theme ===") 59 + printColors(t) 60 + 61 + // Step 5: Use OSC 10 to override the foreground color to red via VT 62 + // input. This changes the "effective" color but leaves the "default" 63 + // unchanged. 64 + t.VTWrite([]byte("\x1b]10;rgb:ff/00/00\x1b\\")) 65 + 66 + fmt.Println("\n=== After OSC 10 override (fg → red) ===") 67 + printColors(t) 68 + 69 + // Step 7: Clear the default foreground by passing nil. 70 + if err := t.SetColorForeground(nil); err != nil { 71 + log.Fatal(err) 72 + } 73 + 74 + fmt.Println("\n=== After clearing default foreground ===") 75 + printColors(t) 76 + } 77 + 78 + // printColors prints the effective and default values for foreground, 79 + // background, cursor, and palette[0]. 80 + func printColors(t *ghostty.Terminal) { 81 + type colorPair struct { 82 + label string 83 + eff, def func() (*ghostty.ColorRGB, error) 84 + } 85 + 86 + pairs := []colorPair{ 87 + {"Foreground", t.ColorForeground, t.ColorForegroundDefault}, 88 + {"Background", t.ColorBackground, t.ColorBackgroundDefault}, 89 + {"Cursor", t.ColorCursor, t.ColorCursorDefault}, 90 + } 91 + 92 + for _, p := range pairs { 93 + eff, err := p.eff() 94 + if err != nil { 95 + log.Fatal(err) 96 + } 97 + def, err := p.def() 98 + if err != nil { 99 + log.Fatal(err) 100 + } 101 + fmt.Printf(" %-12s effective=%-12s default=%s\n", 102 + p.label, formatColor(eff), formatColor(def)) 103 + } 104 + 105 + // Print palette entry 0 (black). 106 + palette, err := t.ColorPalette() 107 + if err != nil { 108 + log.Fatal(err) 109 + } 110 + paletteDefault, err := t.ColorPaletteDefault() 111 + if err != nil { 112 + log.Fatal(err) 113 + } 114 + fmt.Printf(" %-12s effective=%-12s default=%s\n", 115 + "Palette[0]", 116 + formatColor(&palette[0]), 117 + formatColor(&paletteDefault[0])) 118 + } 119 + 120 + // formatColor formats a *ColorRGB as "#RRGGBB" or "(not set)" if nil. 121 + func formatColor(c *ghostty.ColorRGB) string { 122 + if c == nil { 123 + return "(not set)" 124 + } 125 + return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B) 126 + }
+64
examples/effects/main.go
··· 1 + // Example program demonstrating terminal effect callbacks. 2 + // 3 + // It registers write_pty, bell, and title_changed effect handlers, then 4 + // feeds VT sequences that trigger each one. Output shows how the 5 + // callbacks fire and how terminal state can be queried from within them. 6 + package main 7 + 8 + import ( 9 + "fmt" 10 + "log" 11 + 12 + ghostty "github.com/mitchellh/go-libghostty" 13 + ) 14 + 15 + func main() { 16 + // Bell counter, captured by the bell handler closure. 17 + bellCount := 0 18 + 19 + // We declare term here so the title_changed closure can capture it. 20 + var term *ghostty.Terminal 21 + 22 + var err error 23 + term, err = ghostty.NewTerminal( 24 + ghostty.WithSize(80, 24), 25 + 26 + // write_pty: called when the terminal writes data back (e.g. query responses). 27 + ghostty.WithWritePty(func(data []byte) { 28 + fmt.Printf("write_pty: %d bytes: %q\n", len(data), data) 29 + }), 30 + 31 + // bell: called on BEL (0x07). 32 + ghostty.WithBell(func() { 33 + bellCount++ 34 + fmt.Printf("bell: count=%d\n", bellCount) 35 + }), 36 + 37 + // title_changed: called when the terminal title changes via OSC 0/2. 38 + ghostty.WithTitleChanged(func() { 39 + x, err := term.CursorX() 40 + if err != nil { 41 + log.Fatal(err) 42 + } 43 + fmt.Printf("title_changed: cursor_x=%d\n", x) 44 + }), 45 + ) 46 + if err != nil { 47 + log.Fatal(err) 48 + } 49 + defer term.Close() 50 + 51 + // BEL → triggers bell handler. 52 + term.VTWrite([]byte{0x07}) 53 + 54 + // OSC 2 (set title) → triggers title_changed handler. 55 + term.VTWrite([]byte("\x1b]2;hello\x1b\\")) 56 + 57 + // DECRQM query → triggers write_pty with the response. 58 + term.VTWrite([]byte("\x1b[?7$p")) 59 + 60 + // Another BEL → triggers bell handler again. 61 + term.VTWrite([]byte{0x07}) 62 + 63 + fmt.Printf("total bell count: %d\n", bellCount) 64 + }
+55
examples/formatter/main.go
··· 1 + // Example program demonstrating the Formatter API from libghostty. 2 + // It creates a terminal, writes various VT sequences to it, then 3 + // formats the terminal contents as plain text with trimming enabled. 4 + package main 5 + 6 + import ( 7 + "fmt" 8 + "log" 9 + 10 + lg "github.com/mitchellh/go-libghostty" 11 + ) 12 + 13 + func main() { 14 + // Create an 80x24 terminal. 15 + term, err := lg.NewTerminal(lg.WithSize(80, 24)) 16 + if err != nil { 17 + log.Fatal(err) 18 + } 19 + defer term.Close() 20 + 21 + // Write some content with VT formatting. 22 + fmt.Fprintf(term, "Line 1: Hello World!\r\n") 23 + fmt.Fprintf(term, "Line 2: \033[1mBold\033[0m and \033[4mUnderline\033[0m\r\n") 24 + fmt.Fprintf(term, "Line 3: placeholder\r\n") 25 + 26 + // Move to row 3, col 1 and overwrite line 3. 27 + fmt.Fprintf(term, "\033[3;1H") // CUP row 3 col 1 28 + fmt.Fprintf(term, "\033[2K") // Erase entire line 29 + fmt.Fprintf(term, "Line 3: Overwritten!\r\n") 30 + 31 + // Place text at specific positions. 32 + fmt.Fprintf(term, "\033[5;10H") // CUP row 5 col 10 33 + fmt.Fprintf(term, "Placed at (5,10)") 34 + fmt.Fprintf(term, "\033[1;72H") // CUP row 1 col 72 35 + fmt.Fprintf(term, "RIGHT->") 36 + 37 + // Create a plain-text formatter with trimming enabled. 38 + f, err := lg.NewFormatter(term, 39 + lg.WithFormatterFormat(lg.FormatterFormatPlain), 40 + lg.WithFormatterTrim(true), 41 + ) 42 + if err != nil { 43 + log.Fatal(err) 44 + } 45 + defer f.Close() 46 + 47 + // Format and print the output. 48 + output, err := f.FormatString() 49 + if err != nil { 50 + log.Fatal(err) 51 + } 52 + 53 + fmt.Printf("%s\n", output) 54 + fmt.Printf("(%d bytes)\n", len(output)) 55 + }
+93
examples/grid-traverse/main.go
··· 1 + // Example grid-traverse demonstrates walking the terminal grid 2 + // cell-by-cell using the GridRef API to inspect content and style. 3 + package main 4 + 5 + import ( 6 + "fmt" 7 + "log" 8 + 9 + "github.com/mitchellh/go-libghostty" 10 + ) 11 + 12 + func main() { 13 + term, err := libghostty.NewTerminal(libghostty.WithSize(10, 3)) 14 + if err != nil { 15 + log.Fatal(err) 16 + } 17 + defer term.Close() 18 + 19 + // Write some content: two plain lines and one bold line. 20 + term.VTWrite([]byte("Hello!\r\n")) 21 + term.VTWrite([]byte("World\r\n")) 22 + term.VTWrite([]byte("\033[1mBold")) 23 + 24 + cols, err := term.Cols() 25 + if err != nil { 26 + log.Fatal(err) 27 + } 28 + rows, err := term.Rows() 29 + if err != nil { 30 + log.Fatal(err) 31 + } 32 + 33 + for row := range rows { 34 + fmt.Printf("Row %d: ", row) 35 + 36 + for col := range cols { 37 + ref, err := term.GridRef(libghostty.Point{ 38 + Tag: libghostty.PointTagActive, 39 + X: col, 40 + Y: uint32(row), 41 + }) 42 + if err != nil { 43 + log.Fatal(err) 44 + } 45 + 46 + cell, err := ref.Cell() 47 + if err != nil { 48 + log.Fatal(err) 49 + } 50 + 51 + hasText, err := cell.HasText() 52 + if err != nil { 53 + log.Fatal(err) 54 + } 55 + 56 + if hasText { 57 + cp, err := cell.Codepoint() 58 + if err != nil { 59 + log.Fatal(err) 60 + } 61 + fmt.Printf("%c", rune(cp)) 62 + } else { 63 + fmt.Print(".") 64 + } 65 + } 66 + 67 + // Print wrap and bold state for the first cell in the row. 68 + ref, err := term.GridRef(libghostty.Point{ 69 + Tag: libghostty.PointTagActive, 70 + X: 0, 71 + Y: uint32(row), 72 + }) 73 + if err != nil { 74 + log.Fatal(err) 75 + } 76 + 77 + rowData, err := ref.Row() 78 + if err != nil { 79 + log.Fatal(err) 80 + } 81 + wrap, err := rowData.Wrap() 82 + if err != nil { 83 + log.Fatal(err) 84 + } 85 + 86 + style, err := ref.Style() 87 + if err != nil { 88 + log.Fatal(err) 89 + } 90 + 91 + fmt.Printf(" (wrap=%t, bold=%t)\n", wrap, style.Bold()) 92 + } 93 + }
+19
examples/modes/main.go
··· 1 + // Example: modes demonstrates the Mode API from libghostty. 2 + // It prints the value, ANSI flag, and packed hex for a couple of modes. 3 + package main 4 + 5 + import ( 6 + "fmt" 7 + 8 + ghostty "github.com/mitchellh/go-libghostty" 9 + ) 10 + 11 + func main() { 12 + // DEC mode 25: cursor visible (DECTCEM) 13 + m := ghostty.ModeCursorVisible 14 + fmt.Printf("value=%d ansi=%v packed=0x%04x\n", m.Value(), m.ANSI(), uint16(m)) 15 + 16 + // ANSI mode 4: insert mode 17 + m = ghostty.ModeInsert 18 + fmt.Printf("value=%d ansi=%v packed=0x%04x\n", m.Value(), m.ANSI(), uint16(m)) 19 + }
+191
examples/render/main.go
··· 1 + // Example render demonstrates the RenderState API by creating a terminal, 2 + // writing styled VT content, and iterating over the resulting rows and 3 + // cells to produce ANSI-colored output. 4 + package main 5 + 6 + import ( 7 + "fmt" 8 + "log" 9 + 10 + "github.com/mitchellh/go-libghostty" 11 + ) 12 + 13 + // resolveColor converts a StyleColor to a concrete ColorRGB using the 14 + // render state's palette and a fallback for unset colors. 15 + func resolveColor(sc libghostty.StyleColor, colors *libghostty.RenderStateColors, fallback libghostty.ColorRGB) libghostty.ColorRGB { 16 + switch sc.Tag { 17 + case libghostty.StyleColorRGB: 18 + return sc.RGB 19 + case libghostty.StyleColorPalette: 20 + return colors.Palette[sc.Palette] 21 + default: 22 + return fallback 23 + } 24 + } 25 + 26 + // cursorStyleName returns a human-readable name for a cursor visual style. 27 + func cursorStyleName(s libghostty.CursorVisualStyle) string { 28 + switch s { 29 + case libghostty.CursorVisualStyleBar: 30 + return "bar" 31 + case libghostty.CursorVisualStyleBlock: 32 + return "block" 33 + case libghostty.CursorVisualStyleUnderline: 34 + return "underline" 35 + case libghostty.CursorVisualStyleBlockHollow: 36 + return "block_hollow" 37 + default: 38 + return "unknown" 39 + } 40 + } 41 + 42 + func main() { 43 + // 1. Create terminal 40x5 with scrollback 10000. 44 + term, err := libghostty.NewTerminal( 45 + libghostty.WithSize(40, 5), 46 + libghostty.WithMaxScrollback(10000), 47 + ) 48 + if err != nil { 49 + log.Fatal(err) 50 + } 51 + defer term.Close() 52 + 53 + // 2. Create render state. 54 + rs, err := libghostty.NewRenderState() 55 + if err != nil { 56 + log.Fatal(err) 57 + } 58 + defer rs.Close() 59 + 60 + // 3. Write styled VT content. 61 + term.VTWrite([]byte("Hello, \033[1;32mworld\033[0m!\r\n")) 62 + term.VTWrite([]byte("\033[4munderlined\033[0m text\r\n")) 63 + term.VTWrite([]byte("\033[38;2;255;128;0morange\033[0m\r\n")) 64 + 65 + // 4. Update render state from terminal. 66 + if err := rs.Update(term); err != nil { 67 + log.Fatal(err) 68 + } 69 + 70 + // 5. Check and print dirty state. 71 + dirty, err := rs.Dirty() 72 + if err != nil { 73 + log.Fatal(err) 74 + } 75 + fmt.Printf("dirty: %d\n", dirty) 76 + 77 + // 6. Get and print colors. 78 + colors, err := rs.Colors() 79 + if err != nil { 80 + log.Fatal(err) 81 + } 82 + fmt.Printf("bg: #%02x%02x%02x\n", colors.Background.R, colors.Background.G, colors.Background.B) 83 + fmt.Printf("fg: #%02x%02x%02x\n", colors.Foreground.R, colors.Foreground.G, colors.Foreground.B) 84 + 85 + // 7. Cursor information. 86 + cursorVisible, err := rs.CursorVisible() 87 + if err != nil { 88 + log.Fatal(err) 89 + } 90 + cursorHasValue, err := rs.CursorViewportHasValue() 91 + if err != nil { 92 + log.Fatal(err) 93 + } 94 + if cursorVisible && cursorHasValue { 95 + cx, err := rs.CursorViewportX() 96 + if err != nil { 97 + log.Fatal(err) 98 + } 99 + cy, err := rs.CursorViewportY() 100 + if err != nil { 101 + log.Fatal(err) 102 + } 103 + style, err := rs.CursorVisualStyle() 104 + if err != nil { 105 + log.Fatal(err) 106 + } 107 + fmt.Printf("cursor: x=%d y=%d style=%s\n", cx, cy, cursorStyleName(style)) 108 + } else { 109 + fmt.Printf("cursor: not visible\n") 110 + } 111 + 112 + // 8. Iterate rows and cells. 113 + ri, err := libghostty.NewRenderStateRowIterator() 114 + if err != nil { 115 + log.Fatal(err) 116 + } 117 + defer ri.Close() 118 + 119 + rc, err := libghostty.NewRenderStateRowCells() 120 + if err != nil { 121 + log.Fatal(err) 122 + } 123 + defer rc.Close() 124 + 125 + if err := rs.RowIterator(ri); err != nil { 126 + log.Fatal(err) 127 + } 128 + 129 + for ri.Next() { 130 + rowDirty, err := ri.Dirty() 131 + if err != nil { 132 + log.Fatal(err) 133 + } 134 + _ = rowDirty 135 + 136 + if err := ri.Cells(rc); err != nil { 137 + log.Fatal(err) 138 + } 139 + 140 + for rc.Next() { 141 + graphemes, err := rc.Graphemes() 142 + if err != nil { 143 + log.Fatal(err) 144 + } 145 + if len(graphemes) == 0 { 146 + continue 147 + } 148 + 149 + style, err := rc.Style() 150 + if err != nil { 151 + log.Fatal(err) 152 + } 153 + 154 + // Resolve foreground color. 155 + fg := resolveColor(style.FgColor(), colors, colors.Foreground) 156 + 157 + // Emit ANSI true-color escape for foreground. 158 + fmt.Printf("\033[38;2;%d;%d;%dm", fg.R, fg.G, fg.B) 159 + 160 + // Bold marker. 161 + if style.Bold() { 162 + fmt.Printf("\033[1m") 163 + } 164 + 165 + // Underline marker. 166 + if style.Underline() != libghostty.UnderlineNone { 167 + fmt.Printf("\033[4m") 168 + } 169 + 170 + // Print codepoints. 171 + for _, cp := range graphemes { 172 + fmt.Printf("%c", rune(cp)) 173 + } 174 + 175 + // Reset style after each cell. 176 + fmt.Printf("\033[0m") 177 + } 178 + 179 + // Clear row dirty flag. 180 + if err := ri.SetDirty(false); err != nil { 181 + log.Fatal(err) 182 + } 183 + 184 + fmt.Println() 185 + } 186 + 187 + // 9. Reset global dirty state. 188 + if err := rs.SetDirty(libghostty.RenderStateDirtyFalse); err != nil { 189 + log.Fatal(err) 190 + } 191 + }
+113
flake.lock
··· 1 + { 2 + "nodes": { 3 + "flake-compat": { 4 + "flake": false, 5 + "locked": { 6 + "lastModified": 1696426674, 7 + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 + "owner": "edolstra", 9 + "repo": "flake-compat", 10 + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 + "type": "github" 12 + }, 13 + "original": { 14 + "owner": "edolstra", 15 + "repo": "flake-compat", 16 + "type": "github" 17 + } 18 + }, 19 + "flake-utils": { 20 + "inputs": { 21 + "systems": "systems" 22 + }, 23 + "locked": { 24 + "lastModified": 1731533236, 25 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 26 + "owner": "numtide", 27 + "repo": "flake-utils", 28 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 29 + "type": "github" 30 + }, 31 + "original": { 32 + "owner": "numtide", 33 + "repo": "flake-utils", 34 + "type": "github" 35 + } 36 + }, 37 + "nixpkgs": { 38 + "locked": { 39 + "lastModified": 1775126147, 40 + "narHash": "sha256-6d2gqeTZREHUbEgsb6KM8/oIHQH2dtx6Z/WvcJbMz7s=", 41 + "rev": "8d8c1fa5b412c223ffa47410867813290cdedfef", 42 + "type": "tarball", 43 + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre973403.8d8c1fa5b412/nixexprs.tar.xz" 44 + }, 45 + "original": { 46 + "type": "tarball", 47 + "url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz" 48 + } 49 + }, 50 + "root": { 51 + "inputs": { 52 + "flake-utils": "flake-utils", 53 + "nixpkgs": "nixpkgs", 54 + "zig": "zig" 55 + } 56 + }, 57 + "systems": { 58 + "locked": { 59 + "lastModified": 1681028828, 60 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 61 + "owner": "nix-systems", 62 + "repo": "default", 63 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 64 + "type": "github" 65 + }, 66 + "original": { 67 + "owner": "nix-systems", 68 + "repo": "default", 69 + "type": "github" 70 + } 71 + }, 72 + "systems_2": { 73 + "flake": false, 74 + "locked": { 75 + "lastModified": 1681028828, 76 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 77 + "owner": "nix-systems", 78 + "repo": "default", 79 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 80 + "type": "github" 81 + }, 82 + "original": { 83 + "owner": "nix-systems", 84 + "repo": "default", 85 + "type": "github" 86 + } 87 + }, 88 + "zig": { 89 + "inputs": { 90 + "flake-compat": "flake-compat", 91 + "nixpkgs": [ 92 + "nixpkgs" 93 + ], 94 + "systems": "systems_2" 95 + }, 96 + "locked": { 97 + "lastModified": 1775305215, 98 + "narHash": "sha256-g4lTL10wdDvvj7PfJjyyyGhj9Xwj7/FWHxS7cVHUAPc=", 99 + "owner": "mitchellh", 100 + "repo": "zig-overlay", 101 + "rev": "06f352deecc7a7607d6828a8acb7f9a2ac41b7bc", 102 + "type": "github" 103 + }, 104 + "original": { 105 + "owner": "mitchellh", 106 + "repo": "zig-overlay", 107 + "type": "github" 108 + } 109 + } 110 + }, 111 + "root": "root", 112 + "version": 7 113 + }
+40
flake.nix
··· 1 + { 2 + description = "go-libghostty"; 3 + 4 + inputs = { 5 + nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; 6 + flake-utils.url = "github:numtide/flake-utils"; 7 + zig = { 8 + url = "github:mitchellh/zig-overlay"; 9 + inputs.nixpkgs.follows = "nixpkgs"; 10 + }; 11 + }; 12 + 13 + outputs = { 14 + nixpkgs, 15 + flake-utils, 16 + zig, 17 + ... 18 + }: 19 + flake-utils.lib.eachDefaultSystem ( 20 + system: let 21 + pkgs = nixpkgs.legacyPackages.${system}; 22 + in { 23 + devShells.default = pkgs.mkShell { 24 + packages = [ 25 + pkgs.cmake 26 + pkgs.go 27 + zig.packages.${system}."0.15.2" 28 + ] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ 29 + pkgs.libcxx 30 + ]; 31 + 32 + shellHook = '' 33 + export PKG_CONFIG_PATH="$PWD/build/_deps/ghostty-src/zig-out/share/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" 34 + export DYLD_LIBRARY_PATH="$PWD/build/_deps/ghostty-src/zig-out/lib''${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" 35 + export LD_LIBRARY_PATH="$PWD/build/_deps/ghostty-src/zig-out/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" 36 + ''; 37 + }; 38 + } 39 + ); 40 + }
+232
formatter.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + 6 + // Helper to create a properly initialized GhosttyFormatterTerminalOptions (sized struct). 7 + static inline GhosttyFormatterTerminalOptions init_formatter_terminal_options() { 8 + GhosttyFormatterTerminalOptions opts = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions); 9 + opts.extra.size = sizeof(GhosttyFormatterTerminalExtra); 10 + opts.extra.screen.size = sizeof(GhosttyFormatterScreenExtra); 11 + return opts; 12 + } 13 + */ 14 + import "C" 15 + 16 + import ( 17 + "io" 18 + "unsafe" 19 + ) 20 + 21 + // FormatterFormat selects the output format for a Formatter. 22 + // C: GhosttyFormatterFormat 23 + type FormatterFormat int 24 + 25 + const ( 26 + // FormatterFormatPlain emits plain text (no escape sequences). 27 + FormatterFormatPlain FormatterFormat = C.GHOSTTY_FORMATTER_FORMAT_PLAIN 28 + 29 + // FormatterFormatVT emits VT sequences preserving colors, styles, URLs, etc. 30 + FormatterFormatVT FormatterFormat = C.GHOSTTY_FORMATTER_FORMAT_VT 31 + 32 + // FormatterFormatHTML emits HTML with inline styles. 33 + FormatterFormatHTML FormatterFormat = C.GHOSTTY_FORMATTER_FORMAT_HTML 34 + ) 35 + 36 + // formatterOpts wraps the C options struct so that functional options 37 + // can mutate it directly. Only fields explicitly set by an option are 38 + // modified; everything else retains the GHOSTTY_INIT_SIZED defaults. 39 + type formatterOpts struct { 40 + c C.GhosttyFormatterTerminalOptions 41 + } 42 + 43 + // FormatterOption is a functional option for configuring a Formatter. 44 + type FormatterOption func(*formatterOpts) 45 + 46 + // WithFormatterFormat sets the output format (plain, VT, or HTML). 47 + // Defaults to FormatterFormatPlain if not specified. 48 + func WithFormatterFormat(f FormatterFormat) FormatterOption { 49 + return func(o *formatterOpts) { 50 + o.c.emit = C.GhosttyFormatterFormat(f) 51 + } 52 + } 53 + 54 + // WithFormatterUnwrap enables unwrapping of soft-wrapped lines. 55 + func WithFormatterUnwrap(unwrap bool) FormatterOption { 56 + return func(o *formatterOpts) { 57 + o.c.unwrap = C.bool(unwrap) 58 + } 59 + } 60 + 61 + // WithFormatterTrim enables trimming of trailing whitespace on 62 + // non-blank lines. 63 + func WithFormatterTrim(trim bool) FormatterOption { 64 + return func(o *formatterOpts) { 65 + o.c.trim = C.bool(trim) 66 + } 67 + } 68 + 69 + // WithFormatterExtraPalette emits the palette using OSC 4 sequences. 70 + func WithFormatterExtraPalette(v bool) FormatterOption { 71 + return func(o *formatterOpts) { 72 + o.c.extra.palette = C.bool(v) 73 + } 74 + } 75 + 76 + // WithFormatterExtraModes emits terminal modes that differ from their 77 + // defaults using CSI h/l. 78 + func WithFormatterExtraModes(v bool) FormatterOption { 79 + return func(o *formatterOpts) { 80 + o.c.extra.modes = C.bool(v) 81 + } 82 + } 83 + 84 + // WithFormatterExtraScrollingRegion emits scrolling region state using 85 + // DECSTBM and DECSLRM sequences. 86 + func WithFormatterExtraScrollingRegion(v bool) FormatterOption { 87 + return func(o *formatterOpts) { 88 + o.c.extra.scrolling_region = C.bool(v) 89 + } 90 + } 91 + 92 + // WithFormatterExtraTabstops emits tabstop positions by clearing all 93 + // tabs and setting each one. 94 + func WithFormatterExtraTabstops(v bool) FormatterOption { 95 + return func(o *formatterOpts) { 96 + o.c.extra.tabstops = C.bool(v) 97 + } 98 + } 99 + 100 + // WithFormatterExtraPwd emits the present working directory using OSC 7. 101 + func WithFormatterExtraPwd(v bool) FormatterOption { 102 + return func(o *formatterOpts) { 103 + o.c.extra.pwd = C.bool(v) 104 + } 105 + } 106 + 107 + // WithFormatterExtraKeyboard emits keyboard modes such as 108 + // ModifyOtherKeys. 109 + func WithFormatterExtraKeyboard(v bool) FormatterOption { 110 + return func(o *formatterOpts) { 111 + o.c.extra.keyboard = C.bool(v) 112 + } 113 + } 114 + 115 + // WithFormatterExtraCursor emits cursor position using CUP (CSI H). 116 + func WithFormatterExtraCursor(v bool) FormatterOption { 117 + return func(o *formatterOpts) { 118 + o.c.extra.screen.cursor = C.bool(v) 119 + } 120 + } 121 + 122 + // WithFormatterExtraStyle emits current SGR style state based on the 123 + // cursor's active style_id. 124 + func WithFormatterExtraStyle(v bool) FormatterOption { 125 + return func(o *formatterOpts) { 126 + o.c.extra.screen.style = C.bool(v) 127 + } 128 + } 129 + 130 + // WithFormatterExtraHyperlink emits current hyperlink state using 131 + // OSC 8 sequences. 132 + func WithFormatterExtraHyperlink(v bool) FormatterOption { 133 + return func(o *formatterOpts) { 134 + o.c.extra.screen.hyperlink = C.bool(v) 135 + } 136 + } 137 + 138 + // WithFormatterExtraProtection emits character protection mode using 139 + // DECSCA. 140 + func WithFormatterExtraProtection(v bool) FormatterOption { 141 + return func(o *formatterOpts) { 142 + o.c.extra.screen.protection = C.bool(v) 143 + } 144 + } 145 + 146 + // WithFormatterExtraKittyKeyboard emits Kitty keyboard protocol state 147 + // using CSI > u and CSI = sequences. 148 + func WithFormatterExtraKittyKeyboard(v bool) FormatterOption { 149 + return func(o *formatterOpts) { 150 + o.c.extra.screen.kitty_keyboard = C.bool(v) 151 + } 152 + } 153 + 154 + // WithFormatterExtraCharsets emits character set designations and 155 + // invocations. 156 + func WithFormatterExtraCharsets(v bool) FormatterOption { 157 + return func(o *formatterOpts) { 158 + o.c.extra.screen.charsets = C.bool(v) 159 + } 160 + } 161 + 162 + // Formatter wraps a Ghostty formatter handle that can produce 163 + // plain text, VT sequences, or HTML from a terminal's current state. 164 + // The terminal must outlive the formatter. 165 + // 166 + // Formatter implements io.WriterTo so formatted output can be written 167 + // directly to any io.Writer. 168 + // C: GhosttyFormatter 169 + type Formatter struct { 170 + ptr C.GhosttyFormatter 171 + } 172 + 173 + // NewFormatter creates a formatter for the given terminal's active screen. 174 + // The terminal must outlive the formatter. The formatter captures a 175 + // borrowed reference to the terminal and reads its current state on 176 + // each Format call. 177 + func NewFormatter(t *Terminal, opts ...FormatterOption) (*Formatter, error) { 178 + // Start with GHOSTTY_INIT_SIZED defaults; options only touch 179 + // fields the caller explicitly sets. 180 + fo := formatterOpts{c: C.init_formatter_terminal_options()} 181 + for _, opt := range opts { 182 + opt(&fo) 183 + } 184 + 185 + var ptr C.GhosttyFormatter 186 + if err := resultError(C.ghostty_formatter_terminal_new(nil, &ptr, t.ptr, fo.c)); err != nil { 187 + return nil, err 188 + } 189 + 190 + return &Formatter{ptr: ptr}, nil 191 + } 192 + 193 + // Close frees the formatter handle. After this call, the formatter 194 + // must not be used. 195 + func (f *Formatter) Close() { 196 + C.ghostty_formatter_free(f.ptr) 197 + } 198 + 199 + // Format runs the formatter and returns the output as a byte slice. 200 + // Each call reflects the terminal's current state at the time of the call. 201 + // The returned buffer is allocated by libghostty and copied into Go memory. 202 + func (f *Formatter) Format() ([]byte, error) { 203 + var outPtr *C.uint8_t 204 + var outLen C.size_t 205 + if err := resultError(C.ghostty_formatter_format_alloc(f.ptr, nil, &outPtr, &outLen)); err != nil { 206 + return nil, err 207 + } 208 + defer C.ghostty_free(nil, outPtr, outLen) 209 + 210 + return C.GoBytes(unsafe.Pointer(outPtr), C.int(outLen)), nil 211 + } 212 + 213 + // FormatString runs the formatter and returns the output as a string. 214 + // This is a convenience wrapper around Format. 215 + func (f *Formatter) FormatString() (string, error) { 216 + b, err := f.Format() 217 + if err != nil { 218 + return "", err 219 + } 220 + return string(b), nil 221 + } 222 + 223 + // WriteTo implements io.WriterTo. It formats the current terminal 224 + // state and writes the entire output to w. 225 + func (f *Formatter) WriteTo(w io.Writer) (int64, error) { 226 + b, err := f.Format() 227 + if err != nil { 228 + return 0, err 229 + } 230 + n, err := w.Write(b) 231 + return int64(n), err 232 + }
+176
formatter_test.go
··· 1 + package libghostty 2 + 3 + import ( 4 + "bytes" 5 + "io" 6 + "strings" 7 + "testing" 8 + ) 9 + 10 + // Verify interface satisfaction at compile time. 11 + var _ io.WriterTo = (*Formatter)(nil) 12 + 13 + func TestFormatterPlainText(t *testing.T) { 14 + term, err := NewTerminal(WithSize(80, 24)) 15 + if err != nil { 16 + t.Fatal(err) 17 + } 18 + defer term.Close() 19 + 20 + term.VTWrite([]byte("hello world")) 21 + 22 + f, err := NewFormatter(term, WithFormatterFormat(FormatterFormatPlain)) 23 + if err != nil { 24 + t.Fatal(err) 25 + } 26 + defer f.Close() 27 + 28 + out, err := f.FormatString() 29 + if err != nil { 30 + t.Fatal(err) 31 + } 32 + if !strings.Contains(out, "hello world") { 33 + t.Fatalf("expected output to contain 'hello world', got %q", out) 34 + } 35 + } 36 + 37 + func TestFormatterVT(t *testing.T) { 38 + term, err := NewTerminal(WithSize(80, 24)) 39 + if err != nil { 40 + t.Fatal(err) 41 + } 42 + defer term.Close() 43 + 44 + // Write red text. 45 + term.VTWrite([]byte("\x1b[31mred text\x1b[0m")) 46 + 47 + f, err := NewFormatter(term, WithFormatterFormat(FormatterFormatVT)) 48 + if err != nil { 49 + t.Fatal(err) 50 + } 51 + defer f.Close() 52 + 53 + out, err := f.FormatString() 54 + if err != nil { 55 + t.Fatal(err) 56 + } 57 + if !strings.Contains(out, "red text") { 58 + t.Fatalf("expected output to contain 'red text', got %q", out) 59 + } 60 + // VT format should contain escape sequences. 61 + if !strings.Contains(out, "\x1b[") { 62 + t.Fatalf("expected VT output to contain escape sequences, got %q", out) 63 + } 64 + } 65 + 66 + func TestFormatterHTML(t *testing.T) { 67 + term, err := NewTerminal(WithSize(80, 24)) 68 + if err != nil { 69 + t.Fatal(err) 70 + } 71 + defer term.Close() 72 + 73 + term.VTWrite([]byte("\x1b[1mbold text\x1b[0m")) 74 + 75 + f, err := NewFormatter(term, WithFormatterFormat(FormatterFormatHTML)) 76 + if err != nil { 77 + t.Fatal(err) 78 + } 79 + defer f.Close() 80 + 81 + out, err := f.FormatString() 82 + if err != nil { 83 + t.Fatal(err) 84 + } 85 + if !strings.Contains(out, "bold text") { 86 + t.Fatalf("expected output to contain 'bold text', got %q", out) 87 + } 88 + } 89 + 90 + func TestFormatterFormat(t *testing.T) { 91 + term, err := NewTerminal(WithSize(80, 24)) 92 + if err != nil { 93 + t.Fatal(err) 94 + } 95 + defer term.Close() 96 + 97 + term.VTWrite([]byte("bytes test")) 98 + 99 + f, err := NewFormatter(term, WithFormatterFormat(FormatterFormatPlain)) 100 + if err != nil { 101 + t.Fatal(err) 102 + } 103 + defer f.Close() 104 + 105 + out, err := f.Format() 106 + if err != nil { 107 + t.Fatal(err) 108 + } 109 + if !strings.Contains(string(out), "bytes test") { 110 + t.Fatalf("expected output to contain 'bytes test', got %q", string(out)) 111 + } 112 + } 113 + 114 + func TestFormatterReflectsCurrentState(t *testing.T) { 115 + term, err := NewTerminal(WithSize(80, 24)) 116 + if err != nil { 117 + t.Fatal(err) 118 + } 119 + defer term.Close() 120 + 121 + f, err := NewFormatter(term, WithFormatterFormat(FormatterFormatPlain)) 122 + if err != nil { 123 + t.Fatal(err) 124 + } 125 + defer f.Close() 126 + 127 + // Format before writing anything. 128 + out1, err := f.FormatString() 129 + if err != nil { 130 + t.Fatal(err) 131 + } 132 + 133 + // Write some text and format again. 134 + term.VTWrite([]byte("after write")) 135 + out2, err := f.FormatString() 136 + if err != nil { 137 + t.Fatal(err) 138 + } 139 + 140 + if !strings.Contains(out2, "after write") { 141 + t.Fatalf("expected second format to contain 'after write', got %q", out2) 142 + } 143 + if strings.Contains(out1, "after write") { 144 + t.Fatal("first format should not contain text written afterward") 145 + } 146 + } 147 + 148 + func TestFormatterWriteTo(t *testing.T) { 149 + term, err := NewTerminal(WithSize(80, 24)) 150 + if err != nil { 151 + t.Fatal(err) 152 + } 153 + defer term.Close() 154 + 155 + term.VTWrite([]byte("writeto test")) 156 + 157 + f, err := NewFormatter(term, WithFormatterFormat(FormatterFormatPlain)) 158 + if err != nil { 159 + t.Fatal(err) 160 + } 161 + defer f.Close() 162 + 163 + // Formatter satisfies io.WriterTo. 164 + var wt io.WriterTo = f 165 + var buf bytes.Buffer 166 + n, err := wt.WriteTo(&buf) 167 + if err != nil { 168 + t.Fatal(err) 169 + } 170 + if n != int64(buf.Len()) { 171 + t.Fatalf("WriteTo returned %d but buffer has %d bytes", n, buf.Len()) 172 + } 173 + if !strings.Contains(buf.String(), "writeto test") { 174 + t.Fatalf("expected output to contain 'writeto test', got %q", buf.String()) 175 + } 176 + }
+3
go.mod
··· 1 + module github.com/mitchellh/go-libghostty 2 + 3 + go 1.26.0
+90
grid_ref.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + 6 + // Helper to create a properly initialized GhosttyGridRef (sized struct). 7 + static inline GhosttyGridRef init_grid_ref() { 8 + GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef); 9 + return ref; 10 + } 11 + */ 12 + import "C" 13 + 14 + import "unsafe" 15 + 16 + // initCGridRef returns a zero-initialized C GhosttyGridRef with its 17 + // size field set (GHOSTTY_INIT_SIZED). Used by terminal.go to pass 18 + // a grid ref to C APIs. 19 + func initCGridRef() C.GhosttyGridRef { 20 + return C.init_grid_ref() 21 + } 22 + 23 + // GridRef is a resolved reference to a specific cell position in the 24 + // terminal's internal page structure. Obtain a GridRef from 25 + // Terminal.GridRef, then extract cell or row data from it. 26 + // 27 + // A GridRef is only valid until the next update to the terminal 28 + // instance. Read and cache any needed information immediately. 29 + // C: GhosttyGridRef 30 + type GridRef struct { 31 + ref C.GhosttyGridRef 32 + } 33 + 34 + // Cell returns the cell at the grid reference's position. 35 + func (g *GridRef) Cell() (*Cell, error) { 36 + var cell C.GhosttyCell 37 + if err := resultError(C.ghostty_grid_ref_cell(&g.ref, &cell)); err != nil { 38 + return nil, err 39 + } 40 + return &Cell{c: cell}, nil 41 + } 42 + 43 + // Row returns the row at the grid reference's position. 44 + func (g *GridRef) Row() (*Row, error) { 45 + var row C.GhosttyRow 46 + if err := resultError(C.ghostty_grid_ref_row(&g.ref, &row)); err != nil { 47 + return nil, err 48 + } 49 + return &Row{c: row}, nil 50 + } 51 + 52 + // Graphemes returns the full grapheme cluster codepoints for the cell 53 + // at the grid reference's position. Returns nil if the cell has no text. 54 + func (g *GridRef) Graphemes() ([]uint32, error) { 55 + // First call to get the required length. 56 + var outLen C.size_t 57 + err := resultError(C.ghostty_grid_ref_graphemes(&g.ref, nil, 0, &outLen)) 58 + if err != nil { 59 + // OUT_OF_SPACE means we need a bigger buffer; outLen has the size. 60 + ge, ok := err.(*Error) 61 + if !ok || ge.Result != ResultOutOfSpace { 62 + return nil, err 63 + } 64 + } 65 + 66 + if outLen == 0 { 67 + return nil, nil 68 + } 69 + 70 + buf := make([]uint32, uint(outLen)) 71 + if err := resultError(C.ghostty_grid_ref_graphemes( 72 + &g.ref, 73 + (*C.uint32_t)(unsafe.Pointer(&buf[0])), 74 + C.size_t(len(buf)), 75 + &outLen, 76 + )); err != nil { 77 + return nil, err 78 + } 79 + 80 + return buf[:uint(outLen)], nil 81 + } 82 + 83 + // Style returns the style of the cell at the grid reference's position. 84 + func (g *GridRef) Style() (*Style, error) { 85 + cs := initCStyle() 86 + if err := resultError(C.ghostty_grid_ref_style(&g.ref, &cs)); err != nil { 87 + return nil, err 88 + } 89 + return &Style{c: cs}, nil 90 + }
+170
mode.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + */ 6 + import "C" 7 + 8 + // Mode is a packed 16-bit terminal mode identifier. It encodes a mode 9 + // value (bits 0–14) and an ANSI flag (bit 15). DEC private modes have 10 + // the ANSI bit clear; standard ANSI modes have it set. 11 + // C: GhosttyMode 12 + type Mode uint16 13 + 14 + // Value returns the numeric mode value (0–32767). 15 + func (m Mode) Value() uint16 { 16 + return uint16(m) & 0x7FFF 17 + } 18 + 19 + // ANSI reports whether this is a standard ANSI mode. If false, it is 20 + // a DEC private mode (?-prefixed). 21 + func (m Mode) ANSI() bool { 22 + return (m >> 15) != 0 23 + } 24 + 25 + // ANSI modes. 26 + var ( 27 + // ModeKAM is keyboard action mode (disable keyboard). 28 + ModeKAM = Mode(C.GHOSTTY_MODE_KAM) 29 + 30 + // ModeInsert is insert mode. 31 + ModeInsert = Mode(C.GHOSTTY_MODE_INSERT) 32 + 33 + // ModeSRM is send/receive mode. 34 + ModeSRM = Mode(C.GHOSTTY_MODE_SRM) 35 + 36 + // ModeLinefeed is linefeed/new line mode. 37 + ModeLinefeed = Mode(C.GHOSTTY_MODE_LINEFEED) 38 + ) 39 + 40 + // DEC private modes. 41 + var ( 42 + // ModeDECCKM is cursor keys mode. 43 + ModeDECCKM = Mode(C.GHOSTTY_MODE_DECCKM) 44 + 45 + // Mode132Column is 132/80 column mode. 46 + Mode132Column = Mode(C.GHOSTTY_MODE_132_COLUMN) 47 + 48 + // ModeSlowScroll is slow scroll mode. 49 + ModeSlowScroll = Mode(C.GHOSTTY_MODE_SLOW_SCROLL) 50 + 51 + // ModeReverseColors is reverse video mode. 52 + ModeReverseColors = Mode(C.GHOSTTY_MODE_REVERSE_COLORS) 53 + 54 + // ModeOrigin is origin mode. 55 + ModeOrigin = Mode(C.GHOSTTY_MODE_ORIGIN) 56 + 57 + // ModeWraparound is auto-wrap mode. 58 + ModeWraparound = Mode(C.GHOSTTY_MODE_WRAPAROUND) 59 + 60 + // ModeAutorepeat is auto-repeat keys mode. 61 + ModeAutorepeat = Mode(C.GHOSTTY_MODE_AUTOREPEAT) 62 + 63 + // ModeX10Mouse is X10 mouse reporting mode. 64 + ModeX10Mouse = Mode(C.GHOSTTY_MODE_X10_MOUSE) 65 + 66 + // ModeCursorBlinking is cursor blink mode. 67 + ModeCursorBlinking = Mode(C.GHOSTTY_MODE_CURSOR_BLINKING) 68 + 69 + // ModeCursorVisible is cursor visible mode (DECTCEM). 70 + ModeCursorVisible = Mode(C.GHOSTTY_MODE_CURSOR_VISIBLE) 71 + 72 + // ModeEnableMode3 allows 132 column mode. 73 + ModeEnableMode3 = Mode(C.GHOSTTY_MODE_ENABLE_MODE_3) 74 + 75 + // ModeReverseWrap is reverse wrap mode. 76 + ModeReverseWrap = Mode(C.GHOSTTY_MODE_REVERSE_WRAP) 77 + 78 + // ModeAltScreenLegacy is alternate screen (legacy, mode 47). 79 + ModeAltScreenLegacy = Mode(C.GHOSTTY_MODE_ALT_SCREEN_LEGACY) 80 + 81 + // ModeKeypadKeys is application keypad mode. 82 + ModeKeypadKeys = Mode(C.GHOSTTY_MODE_KEYPAD_KEYS) 83 + 84 + // ModeLeftRightMargin is left/right margin mode. 85 + ModeLeftRightMargin = Mode(C.GHOSTTY_MODE_LEFT_RIGHT_MARGIN) 86 + 87 + // ModeNormalMouse is normal mouse tracking mode. 88 + ModeNormalMouse = Mode(C.GHOSTTY_MODE_NORMAL_MOUSE) 89 + 90 + // ModeButtonMouse is button-event mouse tracking mode. 91 + ModeButtonMouse = Mode(C.GHOSTTY_MODE_BUTTON_MOUSE) 92 + 93 + // ModeAnyMouse is any-event mouse tracking mode. 94 + ModeAnyMouse = Mode(C.GHOSTTY_MODE_ANY_MOUSE) 95 + 96 + // ModeFocusEvent enables focus in/out events. 97 + ModeFocusEvent = Mode(C.GHOSTTY_MODE_FOCUS_EVENT) 98 + 99 + // ModeUTF8Mouse is UTF-8 mouse format mode. 100 + ModeUTF8Mouse = Mode(C.GHOSTTY_MODE_UTF8_MOUSE) 101 + 102 + // ModeSGRMouse is SGR mouse format mode. 103 + ModeSGRMouse = Mode(C.GHOSTTY_MODE_SGR_MOUSE) 104 + 105 + // ModeAltScroll is alternate scroll mode. 106 + ModeAltScroll = Mode(C.GHOSTTY_MODE_ALT_SCROLL) 107 + 108 + // ModeURxvtMouse is URxvt mouse format mode. 109 + ModeURxvtMouse = Mode(C.GHOSTTY_MODE_URXVT_MOUSE) 110 + 111 + // ModeSGRPixelsMouse is SGR-Pixels mouse format mode. 112 + ModeSGRPixelsMouse = Mode(C.GHOSTTY_MODE_SGR_PIXELS_MOUSE) 113 + 114 + // ModeNumlockKeypad ignores keypad with NumLock. 115 + ModeNumlockKeypad = Mode(C.GHOSTTY_MODE_NUMLOCK_KEYPAD) 116 + 117 + // ModeAltEscPrefix makes Alt key send ESC prefix. 118 + ModeAltEscPrefix = Mode(C.GHOSTTY_MODE_ALT_ESC_PREFIX) 119 + 120 + // ModeAltSendsEsc makes Alt send escape. 121 + ModeAltSendsEsc = Mode(C.GHOSTTY_MODE_ALT_SENDS_ESC) 122 + 123 + // ModeReverseWrapExt is extended reverse wrap mode. 124 + ModeReverseWrapExt = Mode(C.GHOSTTY_MODE_REVERSE_WRAP_EXT) 125 + 126 + // ModeAltScreen is alternate screen mode (mode 1047). 127 + ModeAltScreen = Mode(C.GHOSTTY_MODE_ALT_SCREEN) 128 + 129 + // ModeSaveCursor saves cursor position (DECSC, mode 1048). 130 + ModeSaveCursor = Mode(C.GHOSTTY_MODE_SAVE_CURSOR) 131 + 132 + // ModeAltScreenSave is alt screen + save cursor + clear (mode 1049). 133 + ModeAltScreenSave = Mode(C.GHOSTTY_MODE_ALT_SCREEN_SAVE) 134 + 135 + // ModeBracketedPaste is bracketed paste mode. 136 + ModeBracketedPaste = Mode(C.GHOSTTY_MODE_BRACKETED_PASTE) 137 + 138 + // ModeSyncOutput is synchronized output mode. 139 + ModeSyncOutput = Mode(C.GHOSTTY_MODE_SYNC_OUTPUT) 140 + 141 + // ModeGraphemeCluster is grapheme cluster mode. 142 + ModeGraphemeCluster = Mode(C.GHOSTTY_MODE_GRAPHEME_CLUSTER) 143 + 144 + // ModeColorSchemeReport enables color scheme reporting. 145 + ModeColorSchemeReport = Mode(C.GHOSTTY_MODE_COLOR_SCHEME_REPORT) 146 + 147 + // ModeInBandResize enables in-band size reports. 148 + ModeInBandResize = Mode(C.GHOSTTY_MODE_IN_BAND_RESIZE) 149 + ) 150 + 151 + // ModeReportState represents DECRPM report state values (Ps2 parameter). 152 + // C: GhosttyModeReportState 153 + type ModeReportState int 154 + 155 + const ( 156 + // ModeReportNotRecognized means the mode is not recognized. 157 + ModeReportNotRecognized ModeReportState = C.GHOSTTY_MODE_REPORT_NOT_RECOGNIZED 158 + 159 + // ModeReportSet means the mode is set (enabled). 160 + ModeReportSet ModeReportState = C.GHOSTTY_MODE_REPORT_SET 161 + 162 + // ModeReportReset means the mode is reset (disabled). 163 + ModeReportReset ModeReportState = C.GHOSTTY_MODE_REPORT_RESET 164 + 165 + // ModeReportPermanentlySet means the mode is permanently set. 166 + ModeReportPermanentlySet ModeReportState = C.GHOSTTY_MODE_REPORT_PERMANENTLY_SET 167 + 168 + // ModeReportPermanentlyReset means the mode is permanently reset. 169 + ModeReportPermanentlyReset ModeReportState = C.GHOSTTY_MODE_REPORT_PERMANENTLY_RESET 170 + )
+23
mode_test.go
··· 1 + package libghostty 2 + 3 + import "testing" 4 + 5 + func TestModeValueANSI(t *testing.T) { 6 + m := ModeInsert 7 + if m.Value() != 4 { 8 + t.Fatalf("expected value 4, got %d", m.Value()) 9 + } 10 + if !m.ANSI() { 11 + t.Fatal("expected ANSI mode") 12 + } 13 + } 14 + 15 + func TestModeValueDEC(t *testing.T) { 16 + m := ModeCursorVisible 17 + if m.Value() != 25 { 18 + t.Fatalf("expected value 25, got %d", m.Value()) 19 + } 20 + if m.ANSI() { 21 + t.Fatal("expected DEC private mode") 22 + } 23 + }
+51
point.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + */ 6 + import "C" 7 + 8 + import "unsafe" 9 + 10 + // PointTag determines which coordinate system a point uses. 11 + // C: GhosttyPointTag 12 + type PointTag int 13 + 14 + const ( 15 + // PointTagActive references the active area where the cursor can move. 16 + PointTagActive PointTag = C.GHOSTTY_POINT_TAG_ACTIVE 17 + 18 + // PointTagViewport references the visible viewport (changes when scrolled). 19 + PointTagViewport PointTag = C.GHOSTTY_POINT_TAG_VIEWPORT 20 + 21 + // PointTagScreen references the full screen including scrollback. 22 + PointTagScreen PointTag = C.GHOSTTY_POINT_TAG_SCREEN 23 + 24 + // PointTagHistory references scrollback history only (before active area). 25 + PointTagHistory PointTag = C.GHOSTTY_POINT_TAG_HISTORY 26 + ) 27 + 28 + // Point is a tagged position in the terminal grid. The Tag determines 29 + // which coordinate system X and Y refer to. 30 + // C: GhosttyPoint 31 + type Point struct { 32 + // Tag determines the coordinate system. 33 + Tag PointTag 34 + 35 + // X is the column (0-indexed). 36 + X uint16 37 + 38 + // Y is the row (0-indexed). May exceed page size for screen/history tags. 39 + Y uint32 40 + } 41 + 42 + // toC converts a Go Point to a C GhosttyPoint. 43 + func (p Point) toC() C.GhosttyPoint { 44 + var cp C.GhosttyPoint 45 + cp.tag = C.GhosttyPointTag(p.Tag) 46 + // Set the coordinate in the value union. 47 + coord := (*C.GhosttyPointCoordinate)(unsafe.Pointer(&cp.value[0])) 48 + coord.x = C.uint16_t(p.X) 49 + coord.y = C.uint32_t(p.Y) 50 + return cp 51 + }
+103
render_state.go
··· 1 + package libghostty 2 + 3 + // Render state for creating high-performance renderers. 4 + // Wraps the GhosttyRenderState C APIs (excluding row/cell iterators). 5 + 6 + /* 7 + #include <ghostty/vt.h> 8 + */ 9 + import "C" 10 + 11 + // RenderState holds the state required to render a visible screen 12 + // (viewport) of a terminal instance. It is stateful and optimized 13 + // for repeated updates from a single terminal, only updating dirty 14 + // regions of the screen. 15 + // 16 + // Basic usage: 17 + // 1. Create an empty render state with NewRenderState. 18 + // 2. Update it from a terminal via Update whenever needed. 19 + // 3. Read from the render state to get data for drawing. 20 + // 21 + // C: GhosttyRenderState 22 + type RenderState struct { 23 + ptr C.GhosttyRenderState 24 + } 25 + 26 + // RenderStateDirty describes the dirty state after an update. 27 + // C: GhosttyRenderStateDirty 28 + type RenderStateDirty int 29 + 30 + const ( 31 + // RenderStateDirtyFalse means not dirty; rendering can be skipped. 32 + RenderStateDirtyFalse RenderStateDirty = C.GHOSTTY_RENDER_STATE_DIRTY_FALSE 33 + 34 + // RenderStateDirtyPartial means some rows changed; renderer can 35 + // redraw incrementally. 36 + RenderStateDirtyPartial RenderStateDirty = C.GHOSTTY_RENDER_STATE_DIRTY_PARTIAL 37 + 38 + // RenderStateDirtyFull means global state changed; renderer should 39 + // redraw everything. 40 + RenderStateDirtyFull RenderStateDirty = C.GHOSTTY_RENDER_STATE_DIRTY_FULL 41 + ) 42 + 43 + // CursorVisualStyle describes the visual style of the cursor. 44 + // C: GhosttyRenderStateCursorVisualStyle 45 + type CursorVisualStyle int 46 + 47 + const ( 48 + // CursorVisualStyleBar is a bar cursor (DECSCUSR 5, 6). 49 + CursorVisualStyleBar CursorVisualStyle = C.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR 50 + 51 + // CursorVisualStyleBlock is a block cursor (DECSCUSR 1, 2). 52 + CursorVisualStyleBlock CursorVisualStyle = C.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK 53 + 54 + // CursorVisualStyleUnderline is an underline cursor (DECSCUSR 3, 4). 55 + CursorVisualStyleUnderline CursorVisualStyle = C.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE 56 + 57 + // CursorVisualStyleBlockHollow is a hollow block cursor. 58 + CursorVisualStyleBlockHollow CursorVisualStyle = C.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW 59 + ) 60 + 61 + // RenderStateColors holds all color information from a render state, 62 + // retrieved in a single call via the sized-struct API. 63 + // C: GhosttyRenderStateColors 64 + type RenderStateColors struct { 65 + // Background is the default/current background color. 66 + Background ColorRGB 67 + 68 + // Foreground is the default/current foreground color. 69 + Foreground ColorRGB 70 + 71 + // Cursor is the cursor color when explicitly set by terminal state. 72 + // Only valid when CursorHasValue is true. 73 + Cursor ColorRGB 74 + 75 + // CursorHasValue is true when Cursor contains a valid explicit 76 + // cursor color value. 77 + CursorHasValue bool 78 + 79 + // Palette is the active 256-color palette. 80 + Palette Palette 81 + } 82 + 83 + // NewRenderState creates a new empty render state. 84 + func NewRenderState() (*RenderState, error) { 85 + var ptr C.GhosttyRenderState 86 + if err := resultError(C.ghostty_render_state_new(nil, &ptr)); err != nil { 87 + return nil, err 88 + } 89 + return &RenderState{ptr: ptr}, nil 90 + } 91 + 92 + // Close frees the underlying render state handle. After this call, 93 + // the render state must not be used. 94 + func (rs *RenderState) Close() { 95 + C.ghostty_render_state_free(rs.ptr) 96 + } 97 + 98 + // Update updates the render state from a terminal instance. This 99 + // consumes terminal/screen dirty state. The terminal must not be 100 + // used concurrently during this call. 101 + func (rs *RenderState) Update(t *Terminal) error { 102 + return resultError(C.ghostty_render_state_update(rs.ptr, t.ptr)) 103 + }
+133
render_state_cell.go
··· 1 + package libghostty 2 + 3 + // Render-state row cell iterator wrapping the 4 + // GhosttyRenderStateRowCells C APIs. 5 + 6 + /* 7 + #include <ghostty/vt.h> 8 + */ 9 + import "C" 10 + 11 + import ( 12 + "errors" 13 + "unsafe" 14 + ) 15 + 16 + // RenderStateRowCells iterates over cells in a render-state row. 17 + // Create one with NewRenderStateRowCells, populate it via 18 + // RenderStateRowIterator.Cells, then advance with Next (or jump 19 + // with Select) and read data with getter methods. 20 + // 21 + // A single instance can be reused across rows to avoid repeated 22 + // allocation. Cell data is only valid until the next call to 23 + // RenderState.Update. 24 + // 25 + // C: GhosttyRenderStateRowCells 26 + type RenderStateRowCells struct { 27 + ptr C.GhosttyRenderStateRowCells 28 + } 29 + 30 + // NewRenderStateRowCells creates a new row cells instance. The 31 + // instance is empty until populated via RenderStateRowIterator.Cells. 32 + func NewRenderStateRowCells() (*RenderStateRowCells, error) { 33 + var ptr C.GhosttyRenderStateRowCells 34 + if err := resultError(C.ghostty_render_state_row_cells_new(nil, &ptr)); err != nil { 35 + return nil, err 36 + } 37 + return &RenderStateRowCells{ptr: ptr}, nil 38 + } 39 + 40 + // Close frees the underlying row cells handle. After this call, 41 + // the instance must not be used. 42 + func (rc *RenderStateRowCells) Close() { 43 + C.ghostty_render_state_row_cells_free(rc.ptr) 44 + } 45 + 46 + // Next advances the iterator to the next cell. Returns true if the 47 + // iterator moved successfully and cell data is available. Returns 48 + // false when there are no more cells. 49 + func (rc *RenderStateRowCells) Next() bool { 50 + return bool(C.ghostty_render_state_row_cells_next(rc.ptr)) 51 + } 52 + 53 + // Select positions the iterator at the given column index (0-based) 54 + // so that subsequent reads return data for that cell. 55 + func (rc *RenderStateRowCells) Select(x uint16) error { 56 + return resultError(C.ghostty_render_state_row_cells_select(rc.ptr, C.uint16_t(x))) 57 + } 58 + 59 + // Raw returns the raw Cell value for the current iterator position. 60 + // The returned Cell can be used with the same getter methods as cells 61 + // obtained from GridRef. 62 + func (rc *RenderStateRowCells) Raw() (*Cell, error) { 63 + var v C.GhosttyCell 64 + 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 { 65 + return nil, err 66 + } 67 + return &Cell{c: v}, nil 68 + } 69 + 70 + // Style returns the style for the current cell. 71 + func (rc *RenderStateRowCells) Style() (*Style, error) { 72 + cs := initCStyle() 73 + 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 { 74 + return nil, err 75 + } 76 + return &Style{c: cs}, nil 77 + } 78 + 79 + // Graphemes returns the full grapheme cluster codepoints for the 80 + // current cell. The base codepoint is first, followed by any extra 81 + // codepoints. Returns nil if the cell has no text. 82 + func (rc *RenderStateRowCells) Graphemes() ([]uint32, error) { 83 + // Get the number of codepoints. 84 + var n C.uint32_t 85 + 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 { 86 + return nil, err 87 + } 88 + if n == 0 { 89 + return nil, nil 90 + } 91 + 92 + // Read codepoints into a buffer. 93 + buf := make([]uint32, uint32(n)) 94 + 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 { 95 + return nil, err 96 + } 97 + return buf, nil 98 + } 99 + 100 + // BgColor returns the resolved background color for the current cell. 101 + // Returns nil (without error) when the cell has no background color, 102 + // in which case the caller should use the terminal default background. 103 + func (rc *RenderStateRowCells) BgColor() (*ColorRGB, error) { 104 + var v C.GhosttyColorRgb 105 + err := resultError(C.ghostty_render_state_row_cells_get(rc.ptr, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, unsafe.Pointer(&v))) 106 + if err != nil { 107 + var ge *Error 108 + if errors.As(err, &ge) && ge.Result == ResultInvalidValue { 109 + return nil, nil 110 + } 111 + return nil, err 112 + } 113 + c := ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)} 114 + return &c, nil 115 + } 116 + 117 + // FgColor returns the resolved foreground color for the current cell. 118 + // Returns nil (without error) when the cell has no explicit foreground 119 + // color, in which case the caller should use the terminal default 120 + // foreground. Bold color handling is not applied. 121 + func (rc *RenderStateRowCells) FgColor() (*ColorRGB, error) { 122 + var v C.GhosttyColorRgb 123 + err := resultError(C.ghostty_render_state_row_cells_get(rc.ptr, C.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, unsafe.Pointer(&v))) 124 + if err != nil { 125 + var ge *Error 126 + if errors.As(err, &ge) && ge.Result == ResultInvalidValue { 127 + return nil, nil 128 + } 129 + return nil, err 130 + } 131 + c := ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)} 132 + return &c, nil 133 + }
+334
render_state_cell_test.go
··· 1 + package libghostty 2 + 3 + import "testing" 4 + 5 + func TestRenderStateRowCells(t *testing.T) { 6 + term, err := NewTerminal(WithSize(80, 24)) 7 + if err != nil { 8 + t.Fatal(err) 9 + } 10 + defer term.Close() 11 + 12 + term.VTWrite([]byte("hello")) 13 + 14 + rs, err := NewRenderState() 15 + if err != nil { 16 + t.Fatal(err) 17 + } 18 + defer rs.Close() 19 + 20 + if err := rs.Update(term); err != nil { 21 + t.Fatal(err) 22 + } 23 + 24 + ri, err := NewRenderStateRowIterator() 25 + if err != nil { 26 + t.Fatal(err) 27 + } 28 + defer ri.Close() 29 + 30 + if err := rs.RowIterator(ri); err != nil { 31 + t.Fatal(err) 32 + } 33 + 34 + // Advance to first row. 35 + if !ri.Next() { 36 + t.Fatal("expected at least one row") 37 + } 38 + 39 + rc, err := NewRenderStateRowCells() 40 + if err != nil { 41 + t.Fatal(err) 42 + } 43 + defer rc.Close() 44 + 45 + if err := ri.Cells(rc); err != nil { 46 + t.Fatal(err) 47 + } 48 + 49 + // Iterate cells and count them. 50 + count := 0 51 + for rc.Next() { 52 + count++ 53 + } 54 + if count != 80 { 55 + t.Fatalf("expected 80 cells, got %d", count) 56 + } 57 + } 58 + 59 + func TestRenderStateRowCellsSelect(t *testing.T) { 60 + term, err := NewTerminal(WithSize(80, 24)) 61 + if err != nil { 62 + t.Fatal(err) 63 + } 64 + defer term.Close() 65 + 66 + term.VTWrite([]byte("ABCDE")) 67 + 68 + rs, err := NewRenderState() 69 + if err != nil { 70 + t.Fatal(err) 71 + } 72 + defer rs.Close() 73 + 74 + if err := rs.Update(term); err != nil { 75 + t.Fatal(err) 76 + } 77 + 78 + ri, err := NewRenderStateRowIterator() 79 + if err != nil { 80 + t.Fatal(err) 81 + } 82 + defer ri.Close() 83 + 84 + if err := rs.RowIterator(ri); err != nil { 85 + t.Fatal(err) 86 + } 87 + 88 + if !ri.Next() { 89 + t.Fatal("expected at least one row") 90 + } 91 + 92 + rc, err := NewRenderStateRowCells() 93 + if err != nil { 94 + t.Fatal(err) 95 + } 96 + defer rc.Close() 97 + 98 + if err := ri.Cells(rc); err != nil { 99 + t.Fatal(err) 100 + } 101 + 102 + // Select column 2 (should be 'C'). 103 + if err := rc.Select(2); err != nil { 104 + t.Fatal(err) 105 + } 106 + 107 + graphemes, err := rc.Graphemes() 108 + if err != nil { 109 + t.Fatal(err) 110 + } 111 + if len(graphemes) != 1 || graphemes[0] != 'C' { 112 + t.Fatalf("expected ['C'], got %v", graphemes) 113 + } 114 + } 115 + 116 + func TestRenderStateRowCellsGraphemes(t *testing.T) { 117 + term, err := NewTerminal(WithSize(80, 24)) 118 + if err != nil { 119 + t.Fatal(err) 120 + } 121 + defer term.Close() 122 + 123 + term.VTWrite([]byte("AB")) 124 + 125 + rs, err := NewRenderState() 126 + if err != nil { 127 + t.Fatal(err) 128 + } 129 + defer rs.Close() 130 + 131 + if err := rs.Update(term); err != nil { 132 + t.Fatal(err) 133 + } 134 + 135 + ri, err := NewRenderStateRowIterator() 136 + if err != nil { 137 + t.Fatal(err) 138 + } 139 + defer ri.Close() 140 + 141 + if err := rs.RowIterator(ri); err != nil { 142 + t.Fatal(err) 143 + } 144 + 145 + if !ri.Next() { 146 + t.Fatal("expected at least one row") 147 + } 148 + 149 + rc, err := NewRenderStateRowCells() 150 + if err != nil { 151 + t.Fatal(err) 152 + } 153 + defer rc.Close() 154 + 155 + if err := ri.Cells(rc); err != nil { 156 + t.Fatal(err) 157 + } 158 + 159 + // First cell should be 'A'. 160 + if !rc.Next() { 161 + t.Fatal("expected at least one cell") 162 + } 163 + graphemes, err := rc.Graphemes() 164 + if err != nil { 165 + t.Fatal(err) 166 + } 167 + if len(graphemes) != 1 || graphemes[0] != 'A' { 168 + t.Fatalf("expected ['A'], got %v", graphemes) 169 + } 170 + 171 + // Second cell should be 'B'. 172 + if !rc.Next() { 173 + t.Fatal("expected second cell") 174 + } 175 + graphemes, err = rc.Graphemes() 176 + if err != nil { 177 + t.Fatal(err) 178 + } 179 + if len(graphemes) != 1 || graphemes[0] != 'B' { 180 + t.Fatalf("expected ['B'], got %v", graphemes) 181 + } 182 + 183 + // Empty cell should return nil. 184 + if !rc.Next() { 185 + t.Fatal("expected third cell") 186 + } 187 + graphemes, err = rc.Graphemes() 188 + if err != nil { 189 + t.Fatal(err) 190 + } 191 + if graphemes != nil { 192 + t.Fatalf("expected nil graphemes for empty cell, got %v", graphemes) 193 + } 194 + } 195 + 196 + func TestRenderStateRowCellsStyle(t *testing.T) { 197 + term, err := NewTerminal(WithSize(80, 24)) 198 + if err != nil { 199 + t.Fatal(err) 200 + } 201 + defer term.Close() 202 + 203 + // Write bold text. 204 + term.VTWrite([]byte("\x1b[1mX")) 205 + 206 + rs, err := NewRenderState() 207 + if err != nil { 208 + t.Fatal(err) 209 + } 210 + defer rs.Close() 211 + 212 + if err := rs.Update(term); err != nil { 213 + t.Fatal(err) 214 + } 215 + 216 + ri, err := NewRenderStateRowIterator() 217 + if err != nil { 218 + t.Fatal(err) 219 + } 220 + defer ri.Close() 221 + 222 + if err := rs.RowIterator(ri); err != nil { 223 + t.Fatal(err) 224 + } 225 + 226 + if !ri.Next() { 227 + t.Fatal("expected at least one row") 228 + } 229 + 230 + rc, err := NewRenderStateRowCells() 231 + if err != nil { 232 + t.Fatal(err) 233 + } 234 + defer rc.Close() 235 + 236 + if err := ri.Cells(rc); err != nil { 237 + t.Fatal(err) 238 + } 239 + 240 + if !rc.Next() { 241 + t.Fatal("expected at least one cell") 242 + } 243 + 244 + style, err := rc.Style() 245 + if err != nil { 246 + t.Fatal(err) 247 + } 248 + if !style.Bold() { 249 + t.Fatal("expected bold style") 250 + } 251 + } 252 + 253 + func TestRenderStateRowCellsColors(t *testing.T) { 254 + term, err := NewTerminal(WithSize(80, 24)) 255 + if err != nil { 256 + t.Fatal(err) 257 + } 258 + defer term.Close() 259 + 260 + // Write text with explicit fg/bg colors (SGR 38;2;R;G;B and 48;2;R;G;B). 261 + term.VTWrite([]byte("\x1b[38;2;255;0;0;48;2;0;0;255mX")) 262 + 263 + rs, err := NewRenderState() 264 + if err != nil { 265 + t.Fatal(err) 266 + } 267 + defer rs.Close() 268 + 269 + if err := rs.Update(term); err != nil { 270 + t.Fatal(err) 271 + } 272 + 273 + ri, err := NewRenderStateRowIterator() 274 + if err != nil { 275 + t.Fatal(err) 276 + } 277 + defer ri.Close() 278 + 279 + if err := rs.RowIterator(ri); err != nil { 280 + t.Fatal(err) 281 + } 282 + 283 + if !ri.Next() { 284 + t.Fatal("expected at least one row") 285 + } 286 + 287 + rc, err := NewRenderStateRowCells() 288 + if err != nil { 289 + t.Fatal(err) 290 + } 291 + defer rc.Close() 292 + 293 + if err := ri.Cells(rc); err != nil { 294 + t.Fatal(err) 295 + } 296 + 297 + if !rc.Next() { 298 + t.Fatal("expected at least one cell") 299 + } 300 + 301 + fg, err := rc.FgColor() 302 + if err != nil { 303 + t.Fatal(err) 304 + } 305 + if fg == nil { 306 + t.Fatal("expected non-nil fg color") 307 + } 308 + if *fg != (ColorRGB{R: 255, G: 0, B: 0}) { 309 + t.Fatalf("expected red fg, got %+v", *fg) 310 + } 311 + 312 + bg, err := rc.BgColor() 313 + if err != nil { 314 + t.Fatal(err) 315 + } 316 + if bg == nil { 317 + t.Fatal("expected non-nil bg color") 318 + } 319 + if *bg != (ColorRGB{R: 0, G: 0, B: 255}) { 320 + t.Fatalf("expected blue bg, got %+v", *bg) 321 + } 322 + 323 + // Empty cell should have nil colors (use default). 324 + if !rc.Next() { 325 + t.Fatal("expected second cell") 326 + } 327 + fg, err = rc.FgColor() 328 + if err != nil { 329 + t.Fatal(err) 330 + } 331 + if fg != nil { 332 + t.Fatalf("expected nil fg for unstyled cell, got %+v", *fg) 333 + } 334 + }
+226
render_state_data.go
··· 1 + package libghostty 2 + 3 + // Render state data getters and setters wrapping 4 + // ghostty_render_state_get() and ghostty_render_state_set(). 5 + // Functions are ordered alphabetically. 6 + 7 + /* 8 + #include <ghostty/vt.h> 9 + 10 + // Helper to create a properly initialized GhosttyRenderStateColors (sized struct). 11 + static inline GhosttyRenderStateColors init_render_state_colors() { 12 + GhosttyRenderStateColors c = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors); 13 + return c; 14 + } 15 + */ 16 + import "C" 17 + 18 + import ( 19 + "errors" 20 + "unsafe" 21 + ) 22 + 23 + // Cols returns the viewport width in cells. 24 + func (rs *RenderState) Cols() (uint16, error) { 25 + var v C.uint16_t 26 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLS, unsafe.Pointer(&v))); err != nil { 27 + return 0, err 28 + } 29 + return uint16(v), nil 30 + } 31 + 32 + // ColorBackground returns the default/current background color. 33 + func (rs *RenderState) ColorBackground() (ColorRGB, error) { 34 + var v C.GhosttyColorRgb 35 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_BACKGROUND, unsafe.Pointer(&v))); err != nil { 36 + return ColorRGB{}, err 37 + } 38 + return ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}, nil 39 + } 40 + 41 + // ColorCursor returns the cursor color when explicitly set by terminal 42 + // state. Returns nil (without error) when no explicit cursor color is set. 43 + func (rs *RenderState) ColorCursor() (*ColorRGB, error) { 44 + // Check whether a cursor color is set first. 45 + var has C.bool 46 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR_HAS_VALUE, unsafe.Pointer(&has))); err != nil { 47 + return nil, err 48 + } 49 + if !bool(has) { 50 + return nil, nil 51 + } 52 + 53 + var v C.GhosttyColorRgb 54 + err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_CURSOR, unsafe.Pointer(&v))) 55 + if err != nil { 56 + var ge *Error 57 + if errors.As(err, &ge) && ge.Result == ResultInvalidValue { 58 + return nil, nil 59 + } 60 + return nil, err 61 + } 62 + c := ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)} 63 + return &c, nil 64 + } 65 + 66 + // ColorForeground returns the default/current foreground color. 67 + func (rs *RenderState) ColorForeground() (ColorRGB, error) { 68 + var v C.GhosttyColorRgb 69 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_FOREGROUND, unsafe.Pointer(&v))); err != nil { 70 + return ColorRGB{}, err 71 + } 72 + return ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}, nil 73 + } 74 + 75 + // ColorPalette returns the active 256-color palette. 76 + func (rs *RenderState) ColorPalette() (*Palette, error) { 77 + var cp [PaletteSize]C.GhosttyColorRgb 78 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_COLOR_PALETTE, unsafe.Pointer(&cp[0]))); err != nil { 79 + return nil, err 80 + } 81 + var p Palette 82 + for i, c := range cp { 83 + p[i] = ColorRGB{R: uint8(c.r), G: uint8(c.g), B: uint8(c.b)} 84 + } 85 + return &p, nil 86 + } 87 + 88 + // Colors returns all color information from the render state in a 89 + // single call using the sized-struct API. 90 + func (rs *RenderState) Colors() (*RenderStateColors, error) { 91 + cc := C.init_render_state_colors() 92 + if err := resultError(C.ghostty_render_state_colors_get(rs.ptr, &cc)); err != nil { 93 + return nil, err 94 + } 95 + 96 + result := &RenderStateColors{ 97 + Background: ColorRGB{R: uint8(cc.background.r), G: uint8(cc.background.g), B: uint8(cc.background.b)}, 98 + Foreground: ColorRGB{R: uint8(cc.foreground.r), G: uint8(cc.foreground.g), B: uint8(cc.foreground.b)}, 99 + Cursor: ColorRGB{R: uint8(cc.cursor.r), G: uint8(cc.cursor.g), B: uint8(cc.cursor.b)}, 100 + CursorHasValue: bool(cc.cursor_has_value), 101 + } 102 + 103 + for i, c := range cc.palette { 104 + result.Palette[i] = ColorRGB{R: uint8(c.r), G: uint8(c.g), B: uint8(c.b)} 105 + } 106 + return result, nil 107 + } 108 + 109 + // CursorBlinking reports whether the cursor should blink based on 110 + // terminal modes. 111 + func (rs *RenderState) CursorBlinking() (bool, error) { 112 + var v C.bool 113 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING, unsafe.Pointer(&v))); err != nil { 114 + return false, err 115 + } 116 + return bool(v), nil 117 + } 118 + 119 + // CursorPasswordInput reports whether the cursor is at a password 120 + // input field. 121 + func (rs *RenderState) CursorPasswordInput() (bool, error) { 122 + var v C.bool 123 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT, unsafe.Pointer(&v))); err != nil { 124 + return false, err 125 + } 126 + return bool(v), nil 127 + } 128 + 129 + // CursorVisible reports whether the cursor is visible based on 130 + // terminal modes. 131 + func (rs *RenderState) CursorVisible() (bool, error) { 132 + var v C.bool 133 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE, unsafe.Pointer(&v))); err != nil { 134 + return false, err 135 + } 136 + return bool(v), nil 137 + } 138 + 139 + // CursorVisualStyle returns the visual style of the cursor. 140 + func (rs *RenderState) CursorVisualStyle() (CursorVisualStyle, error) { 141 + var v C.GhosttyRenderStateCursorVisualStyle 142 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, unsafe.Pointer(&v))); err != nil { 143 + return 0, err 144 + } 145 + return CursorVisualStyle(v), nil 146 + } 147 + 148 + // CursorViewportHasValue reports whether the cursor is visible within 149 + // the viewport. If false, the cursor viewport position values are 150 + // undefined. 151 + func (rs *RenderState) CursorViewportHasValue() (bool, error) { 152 + var v C.bool 153 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, unsafe.Pointer(&v))); err != nil { 154 + return false, err 155 + } 156 + return bool(v), nil 157 + } 158 + 159 + // CursorViewportWideTail reports whether the cursor is on the tail 160 + // of a wide character. Only valid when CursorViewportHasValue 161 + // returns true. 162 + func (rs *RenderState) CursorViewportWideTail() (bool, error) { 163 + var v C.bool 164 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL, unsafe.Pointer(&v))); err != nil { 165 + return false, err 166 + } 167 + return bool(v), nil 168 + } 169 + 170 + // CursorViewportX returns the cursor viewport x position in cells. 171 + // Only valid when CursorViewportHasValue returns true. 172 + func (rs *RenderState) CursorViewportX() (uint16, error) { 173 + var v C.uint16_t 174 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, unsafe.Pointer(&v))); err != nil { 175 + return 0, err 176 + } 177 + return uint16(v), nil 178 + } 179 + 180 + // CursorViewportY returns the cursor viewport y position in cells. 181 + // Only valid when CursorViewportHasValue returns true. 182 + func (rs *RenderState) CursorViewportY() (uint16, error) { 183 + var v C.uint16_t 184 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, unsafe.Pointer(&v))); err != nil { 185 + return 0, err 186 + } 187 + return uint16(v), nil 188 + } 189 + 190 + // Dirty returns the current dirty state. 191 + func (rs *RenderState) Dirty() (RenderStateDirty, error) { 192 + var v C.GhosttyRenderStateDirty 193 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_DIRTY, unsafe.Pointer(&v))); err != nil { 194 + return 0, err 195 + } 196 + return RenderStateDirty(v), nil 197 + } 198 + 199 + // RowIterator populates a pre-allocated row iterator with row data 200 + // from the render state. The iterator can then be advanced with Next 201 + // and queried with getter methods. 202 + // 203 + // The iterator can be reused across multiple calls. Row data is only 204 + // valid until the next call to Update. 205 + func (rs *RenderState) RowIterator(ri *RenderStateRowIterator) error { 206 + return resultError(C.ghostty_render_state_get( 207 + rs.ptr, 208 + C.GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, 209 + unsafe.Pointer(&ri.ptr), 210 + )) 211 + } 212 + 213 + // Rows returns the viewport height in cells. 214 + func (rs *RenderState) Rows() (uint16, error) { 215 + var v C.uint16_t 216 + if err := resultError(C.ghostty_render_state_get(rs.ptr, C.GHOSTTY_RENDER_STATE_DATA_ROWS, unsafe.Pointer(&v))); err != nil { 217 + return 0, err 218 + } 219 + return uint16(v), nil 220 + } 221 + 222 + // SetDirty sets the dirty state. 223 + func (rs *RenderState) SetDirty(dirty RenderStateDirty) error { 224 + v := C.GhosttyRenderStateDirty(dirty) 225 + return resultError(C.ghostty_render_state_set(rs.ptr, C.GHOSTTY_RENDER_STATE_OPTION_DIRTY, unsafe.Pointer(&v))) 226 + }
+89
render_state_row.go
··· 1 + package libghostty 2 + 3 + // Render-state row iterator wrapping the 4 + // GhosttyRenderStateRowIterator C APIs. 5 + 6 + /* 7 + #include <ghostty/vt.h> 8 + */ 9 + import "C" 10 + 11 + import "unsafe" 12 + 13 + // RenderStateRowIterator iterates over rows in a render state. 14 + // Create one with NewRenderStateRowIterator, populate it via 15 + // RenderState.RowIterator, then advance with Next and read data 16 + // with getter methods. 17 + // 18 + // Row data is only valid as long as the underlying render state 19 + // is not updated. It is unsafe to use row data after calling 20 + // RenderState.Update. 21 + // 22 + // C: GhosttyRenderStateRowIterator 23 + type RenderStateRowIterator struct { 24 + ptr C.GhosttyRenderStateRowIterator 25 + } 26 + 27 + // NewRenderStateRowIterator creates a new row iterator instance. 28 + // The iterator is empty until populated via RenderState.RowIterator. 29 + func NewRenderStateRowIterator() (*RenderStateRowIterator, error) { 30 + var ptr C.GhosttyRenderStateRowIterator 31 + if err := resultError(C.ghostty_render_state_row_iterator_new(nil, &ptr)); err != nil { 32 + return nil, err 33 + } 34 + return &RenderStateRowIterator{ptr: ptr}, nil 35 + } 36 + 37 + // Close frees the underlying row iterator handle. After this call, 38 + // the iterator must not be used. 39 + func (ri *RenderStateRowIterator) Close() { 40 + C.ghostty_render_state_row_iterator_free(ri.ptr) 41 + } 42 + 43 + // Next advances the iterator to the next row. Returns true if the 44 + // iterator moved successfully and row data is available. Returns 45 + // false when there are no more rows. 46 + func (ri *RenderStateRowIterator) Next() bool { 47 + return bool(C.ghostty_render_state_row_iterator_next(ri.ptr)) 48 + } 49 + 50 + // Dirty reports whether the current row is dirty and requires a 51 + // redraw. 52 + func (ri *RenderStateRowIterator) Dirty() (bool, error) { 53 + var v C.bool 54 + if err := resultError(C.ghostty_render_state_row_get(ri.ptr, C.GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY, unsafe.Pointer(&v))); err != nil { 55 + return false, err 56 + } 57 + return bool(v), nil 58 + } 59 + 60 + // SetDirty sets the dirty state for the current row. 61 + func (ri *RenderStateRowIterator) SetDirty(dirty bool) error { 62 + v := C.bool(dirty) 63 + return resultError(C.ghostty_render_state_row_set(ri.ptr, C.GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY, unsafe.Pointer(&v))) 64 + } 65 + 66 + // Raw returns the raw Row value for the current iterator position. 67 + // The returned Row can be used with the same getter methods as rows 68 + // obtained from GridRef. 69 + func (ri *RenderStateRowIterator) Raw() (*Row, error) { 70 + var v C.GhosttyRow 71 + if err := resultError(C.ghostty_render_state_row_get(ri.ptr, C.GHOSTTY_RENDER_STATE_ROW_DATA_RAW, unsafe.Pointer(&v))); err != nil { 72 + return nil, err 73 + } 74 + return &Row{c: v}, nil 75 + } 76 + 77 + // Cells populates a pre-allocated row cells instance with cell data 78 + // for the current row. The cells instance can then be advanced with 79 + // Next or positioned with Select. 80 + // 81 + // The cells instance can be reused across rows. Cell data is only 82 + // valid until the next call to RenderState.Update. 83 + func (ri *RenderStateRowIterator) Cells(rc *RenderStateRowCells) error { 84 + return resultError(C.ghostty_render_state_row_get( 85 + ri.ptr, 86 + C.GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, 87 + unsafe.Pointer(&rc.ptr), 88 + )) 89 + }
+138
render_state_row_test.go
··· 1 + package libghostty 2 + 3 + import "testing" 4 + 5 + func TestRenderStateRowIterator(t *testing.T) { 6 + term, err := NewTerminal(WithSize(80, 24)) 7 + if err != nil { 8 + t.Fatal(err) 9 + } 10 + defer term.Close() 11 + 12 + rs, err := NewRenderState() 13 + if err != nil { 14 + t.Fatal(err) 15 + } 16 + defer rs.Close() 17 + 18 + if err := rs.Update(term); err != nil { 19 + t.Fatal(err) 20 + } 21 + 22 + ri, err := NewRenderStateRowIterator() 23 + if err != nil { 24 + t.Fatal(err) 25 + } 26 + defer ri.Close() 27 + 28 + if err := rs.RowIterator(ri); err != nil { 29 + t.Fatal(err) 30 + } 31 + 32 + // Iterate all rows and count them. 33 + count := 0 34 + for ri.Next() { 35 + count++ 36 + } 37 + if count != 24 { 38 + t.Fatalf("expected 24 rows, got %d", count) 39 + } 40 + } 41 + 42 + func TestRenderStateRowIteratorDirty(t *testing.T) { 43 + term, err := NewTerminal(WithSize(80, 24)) 44 + if err != nil { 45 + t.Fatal(err) 46 + } 47 + defer term.Close() 48 + 49 + rs, err := NewRenderState() 50 + if err != nil { 51 + t.Fatal(err) 52 + } 53 + defer rs.Close() 54 + 55 + if err := rs.Update(term); err != nil { 56 + t.Fatal(err) 57 + } 58 + 59 + ri, err := NewRenderStateRowIterator() 60 + if err != nil { 61 + t.Fatal(err) 62 + } 63 + defer ri.Close() 64 + 65 + if err := rs.RowIterator(ri); err != nil { 66 + t.Fatal(err) 67 + } 68 + 69 + // First row after initial update should be dirty. 70 + if !ri.Next() { 71 + t.Fatal("expected at least one row") 72 + } 73 + dirty, err := ri.Dirty() 74 + if err != nil { 75 + t.Fatal(err) 76 + } 77 + if !dirty { 78 + t.Fatal("expected first row to be dirty after initial update") 79 + } 80 + 81 + // Clear dirty and verify. 82 + if err := ri.SetDirty(false); err != nil { 83 + t.Fatal(err) 84 + } 85 + dirty, _ = ri.Dirty() 86 + if dirty { 87 + t.Fatal("expected row not dirty after clearing") 88 + } 89 + } 90 + 91 + func TestRenderStateRowIteratorRaw(t *testing.T) { 92 + term, err := NewTerminal(WithSize(80, 24)) 93 + if err != nil { 94 + t.Fatal(err) 95 + } 96 + defer term.Close() 97 + 98 + // Write some text so the first row has content. 99 + term.VTWrite([]byte("hello")) 100 + 101 + rs, err := NewRenderState() 102 + if err != nil { 103 + t.Fatal(err) 104 + } 105 + defer rs.Close() 106 + 107 + if err := rs.Update(term); err != nil { 108 + t.Fatal(err) 109 + } 110 + 111 + ri, err := NewRenderStateRowIterator() 112 + if err != nil { 113 + t.Fatal(err) 114 + } 115 + defer ri.Close() 116 + 117 + if err := rs.RowIterator(ri); err != nil { 118 + t.Fatal(err) 119 + } 120 + 121 + if !ri.Next() { 122 + t.Fatal("expected at least one row") 123 + } 124 + 125 + row, err := ri.Raw() 126 + if err != nil { 127 + t.Fatal(err) 128 + } 129 + 130 + // The first row should not be wrapped. 131 + wrap, err := row.Wrap() 132 + if err != nil { 133 + t.Fatal(err) 134 + } 135 + if wrap { 136 + t.Fatal("expected first row not to be wrapped") 137 + } 138 + }
+182
render_state_test.go
··· 1 + package libghostty 2 + 3 + import "testing" 4 + 5 + func TestRenderStateNewClose(t *testing.T) { 6 + rs, err := NewRenderState() 7 + if err != nil { 8 + t.Fatal(err) 9 + } 10 + defer rs.Close() 11 + } 12 + 13 + func TestRenderStateUpdate(t *testing.T) { 14 + term, err := NewTerminal(WithSize(80, 24)) 15 + if err != nil { 16 + t.Fatal(err) 17 + } 18 + defer term.Close() 19 + 20 + rs, err := NewRenderState() 21 + if err != nil { 22 + t.Fatal(err) 23 + } 24 + defer rs.Close() 25 + 26 + if err := rs.Update(term); err != nil { 27 + t.Fatal(err) 28 + } 29 + 30 + cols, err := rs.Cols() 31 + if err != nil { 32 + t.Fatal(err) 33 + } 34 + if cols != 80 { 35 + t.Fatalf("expected 80 cols, got %d", cols) 36 + } 37 + 38 + rows, err := rs.Rows() 39 + if err != nil { 40 + t.Fatal(err) 41 + } 42 + if rows != 24 { 43 + t.Fatalf("expected 24 rows, got %d", rows) 44 + } 45 + } 46 + 47 + func TestRenderStateDirty(t *testing.T) { 48 + term, err := NewTerminal(WithSize(80, 24)) 49 + if err != nil { 50 + t.Fatal(err) 51 + } 52 + defer term.Close() 53 + 54 + rs, err := NewRenderState() 55 + if err != nil { 56 + t.Fatal(err) 57 + } 58 + defer rs.Close() 59 + 60 + // First update should be dirty (full). 61 + if err := rs.Update(term); err != nil { 62 + t.Fatal(err) 63 + } 64 + dirty, err := rs.Dirty() 65 + if err != nil { 66 + t.Fatal(err) 67 + } 68 + if dirty != RenderStateDirtyFull { 69 + t.Fatalf("expected full dirty after first update, got %d", dirty) 70 + } 71 + 72 + // Clear dirty and verify. 73 + if err := rs.SetDirty(RenderStateDirtyFalse); err != nil { 74 + t.Fatal(err) 75 + } 76 + dirty, _ = rs.Dirty() 77 + if dirty != RenderStateDirtyFalse { 78 + t.Fatalf("expected not dirty after clearing, got %d", dirty) 79 + } 80 + } 81 + 82 + func TestRenderStateColors(t *testing.T) { 83 + term, err := NewTerminal(WithSize(80, 24)) 84 + if err != nil { 85 + t.Fatal(err) 86 + } 87 + defer term.Close() 88 + 89 + // Set colors so we have known values. 90 + red := &ColorRGB{R: 255, G: 0, B: 0} 91 + blue := &ColorRGB{R: 0, G: 0, B: 255} 92 + if err := term.SetColorForeground(red); err != nil { 93 + t.Fatal(err) 94 + } 95 + if err := term.SetColorBackground(blue); err != nil { 96 + t.Fatal(err) 97 + } 98 + 99 + rs, err := NewRenderState() 100 + if err != nil { 101 + t.Fatal(err) 102 + } 103 + defer rs.Close() 104 + 105 + if err := rs.Update(term); err != nil { 106 + t.Fatal(err) 107 + } 108 + 109 + fg, err := rs.ColorForeground() 110 + if err != nil { 111 + t.Fatal(err) 112 + } 113 + if fg != *red { 114 + t.Fatalf("expected fg %+v, got %+v", *red, fg) 115 + } 116 + 117 + bg, err := rs.ColorBackground() 118 + if err != nil { 119 + t.Fatal(err) 120 + } 121 + if bg != *blue { 122 + t.Fatalf("expected bg %+v, got %+v", *blue, bg) 123 + } 124 + 125 + // Bulk colors API. 126 + colors, err := rs.Colors() 127 + if err != nil { 128 + t.Fatal(err) 129 + } 130 + if colors.Foreground != *red { 131 + t.Fatalf("expected colors.Foreground %+v, got %+v", *red, colors.Foreground) 132 + } 133 + if colors.Background != *blue { 134 + t.Fatalf("expected colors.Background %+v, got %+v", *blue, colors.Background) 135 + } 136 + } 137 + 138 + func TestRenderStateCursor(t *testing.T) { 139 + term, err := NewTerminal(WithSize(80, 24)) 140 + if err != nil { 141 + t.Fatal(err) 142 + } 143 + defer term.Close() 144 + 145 + rs, err := NewRenderState() 146 + if err != nil { 147 + t.Fatal(err) 148 + } 149 + defer rs.Close() 150 + 151 + if err := rs.Update(term); err != nil { 152 + t.Fatal(err) 153 + } 154 + 155 + visible, err := rs.CursorVisible() 156 + if err != nil { 157 + t.Fatal(err) 158 + } 159 + if !visible { 160 + t.Fatal("expected cursor visible by default") 161 + } 162 + 163 + hasViewport, err := rs.CursorViewportHasValue() 164 + if err != nil { 165 + t.Fatal(err) 166 + } 167 + if !hasViewport { 168 + t.Fatal("expected cursor in viewport by default") 169 + } 170 + 171 + x, err := rs.CursorViewportX() 172 + if err != nil { 173 + t.Fatal(err) 174 + } 175 + y, err := rs.CursorViewportY() 176 + if err != nil { 177 + t.Fatal(err) 178 + } 179 + if x != 0 || y != 0 { 180 + t.Fatalf("expected cursor at 0,0, got %d,%d", x, y) 181 + } 182 + }
+50
result.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + */ 6 + import "C" 7 + 8 + import "fmt" 9 + 10 + // Result represents a Ghostty result code. 11 + // 12 + // C: GhosttyResult 13 + type Result int 14 + 15 + const ( 16 + ResultSuccess Result = C.GHOSTTY_SUCCESS 17 + ResultOutOfMemory Result = C.GHOSTTY_OUT_OF_MEMORY 18 + ResultInvalidValue Result = C.GHOSTTY_INVALID_VALUE 19 + ResultOutOfSpace Result = C.GHOSTTY_OUT_OF_SPACE 20 + ResultNoValue Result = C.GHOSTTY_NO_VALUE 21 + ) 22 + 23 + // Error holds a non-success Ghostty result. 24 + type Error struct { 25 + Result Result 26 + } 27 + 28 + func (e *Error) Error() string { 29 + switch e.Result { 30 + case ResultOutOfMemory: 31 + return "ghostty: out of memory" 32 + case ResultInvalidValue: 33 + return "ghostty: invalid value" 34 + case ResultOutOfSpace: 35 + return "ghostty: out of space" 36 + case ResultNoValue: 37 + return "ghostty: no value" 38 + default: 39 + return fmt.Sprintf("ghostty: result=%d", int(e.Result)) 40 + } 41 + } 42 + 43 + // Convert a result code to an error, returning nil on success. 44 + func resultError(result C.GhosttyResult) error { 45 + if result == C.GHOSTTY_SUCCESS { 46 + return nil 47 + } 48 + 49 + return &Error{Result: Result(result)} 50 + }
+50
result_test.go
··· 1 + package libghostty 2 + 3 + import ( 4 + "errors" 5 + "testing" 6 + ) 7 + 8 + func TestErrorMessage(t *testing.T) { 9 + tests := []struct { 10 + result Result 11 + want string 12 + }{ 13 + {ResultOutOfMemory, "ghostty: out of memory"}, 14 + {ResultInvalidValue, "ghostty: invalid value"}, 15 + {ResultOutOfSpace, "ghostty: out of space"}, 16 + {ResultNoValue, "ghostty: no value"}, 17 + {Result(9999), "ghostty: result=9999"}, 18 + } 19 + 20 + for _, tt := range tests { 21 + e := &Error{Result: tt.result} 22 + if got := e.Error(); got != tt.want { 23 + t.Errorf("Error{%d}.Error() = %q, want %q", tt.result, got, tt.want) 24 + } 25 + } 26 + } 27 + 28 + func TestErrorAs(t *testing.T) { 29 + var err error = &Error{Result: ResultOutOfMemory} 30 + 31 + var target *Error 32 + if !errors.As(err, &target) { 33 + t.Fatal("expected errors.As to succeed") 34 + } 35 + if target.Result != ResultOutOfMemory { 36 + t.Fatalf("expected ResultOutOfMemory, got %d", target.Result) 37 + } 38 + } 39 + 40 + func TestErrorAsType(t *testing.T) { 41 + var err error = &Error{Result: ResultInvalidValue} 42 + 43 + target, ok := errors.AsType[*Error](err) 44 + if !ok { 45 + t.Fatal("expected errors.AsType to succeed") 46 + } 47 + if target.Result != ResultInvalidValue { 48 + t.Fatalf("expected ResultInvalidValue, got %d", target.Result) 49 + } 50 + }
+272
screen.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + */ 6 + import "C" 7 + 8 + import "unsafe" 9 + 10 + // Cell is a wrapper around an opaque terminal grid cell value. 11 + // Use getter methods to extract data from it. 12 + // C: GhosttyCell 13 + type Cell struct { 14 + c C.GhosttyCell 15 + } 16 + 17 + // Row is a wrapper around an opaque terminal grid row value. 18 + // Use getter methods to extract data from it. 19 + // C: GhosttyRow 20 + type Row struct { 21 + c C.GhosttyRow 22 + } 23 + 24 + // CellContentTag describes what kind of content a cell holds. 25 + // C: GhosttyCellContentTag 26 + type CellContentTag int 27 + 28 + const ( 29 + // CellContentCodepoint means a single codepoint (may be zero for empty). 30 + CellContentCodepoint CellContentTag = C.GHOSTTY_CELL_CONTENT_CODEPOINT 31 + 32 + // CellContentCodepointGrapheme means a codepoint that is part of a 33 + // multi-codepoint grapheme cluster. 34 + CellContentCodepointGrapheme CellContentTag = C.GHOSTTY_CELL_CONTENT_CODEPOINT_GRAPHEME 35 + 36 + // CellContentBgColorPalette means no text; background color from palette. 37 + CellContentBgColorPalette CellContentTag = C.GHOSTTY_CELL_CONTENT_BG_COLOR_PALETTE 38 + 39 + // CellContentBgColorRGB means no text; background color as RGB. 40 + CellContentBgColorRGB CellContentTag = C.GHOSTTY_CELL_CONTENT_BG_COLOR_RGB 41 + ) 42 + 43 + // CellWide describes the width behavior of a cell. 44 + // C: GhosttyCellWide 45 + type CellWide int 46 + 47 + const ( 48 + // CellWideNarrow means not a wide character, cell width 1. 49 + CellWideNarrow CellWide = C.GHOSTTY_CELL_WIDE_NARROW 50 + 51 + // CellWideWide means wide character, cell width 2. 52 + CellWideWide CellWide = C.GHOSTTY_CELL_WIDE_WIDE 53 + 54 + // CellWideSpacerTail means spacer after wide character (do not render). 55 + CellWideSpacerTail CellWide = C.GHOSTTY_CELL_WIDE_SPACER_TAIL 56 + 57 + // CellWideSpacerHead means spacer at end of soft-wrapped line for a 58 + // wide character. 59 + CellWideSpacerHead CellWide = C.GHOSTTY_CELL_WIDE_SPACER_HEAD 60 + ) 61 + 62 + // CellSemanticContent is the semantic content type of a cell, 63 + // as set by OSC 133 sequences. 64 + // C: GhosttyCellSemanticContent 65 + type CellSemanticContent int 66 + 67 + const ( 68 + // CellSemanticOutput means regular output content (e.g. command output). 69 + CellSemanticOutput CellSemanticContent = C.GHOSTTY_CELL_SEMANTIC_OUTPUT 70 + 71 + // CellSemanticInput means content that is part of user input. 72 + CellSemanticInput CellSemanticContent = C.GHOSTTY_CELL_SEMANTIC_INPUT 73 + 74 + // CellSemanticPrompt means content that is part of a shell prompt. 75 + CellSemanticPrompt CellSemanticContent = C.GHOSTTY_CELL_SEMANTIC_PROMPT 76 + ) 77 + 78 + // RowSemanticPrompt indicates whether any cells in a row are part of 79 + // a shell prompt, as reported by OSC 133 sequences. 80 + // C: GhosttyRowSemanticPrompt 81 + type RowSemanticPrompt int 82 + 83 + const ( 84 + // RowSemanticNone means no prompt cells in this row. 85 + RowSemanticNone RowSemanticPrompt = C.GHOSTTY_ROW_SEMANTIC_NONE 86 + 87 + // RowSemanticPromptPrimary means prompt cells exist and this is a 88 + // primary prompt line. 89 + RowSemanticPromptPrimary RowSemanticPrompt = C.GHOSTTY_ROW_SEMANTIC_PROMPT 90 + 91 + // RowSemanticPromptContinuation means prompt cells exist and this is 92 + // a continuation line. 93 + RowSemanticPromptContinuation RowSemanticPrompt = C.GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION 94 + ) 95 + 96 + // Codepoint returns the codepoint of the cell (0 if empty). 97 + func (c *Cell) Codepoint() (uint32, error) { 98 + var v C.uint32_t 99 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_CODEPOINT, unsafe.Pointer(&v))); err != nil { 100 + return 0, err 101 + } 102 + return uint32(v), nil 103 + } 104 + 105 + // ContentTag returns the content tag describing what kind of content 106 + // the cell holds. 107 + func (c *Cell) ContentTag() (CellContentTag, error) { 108 + var v C.GhosttyCellContentTag 109 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_CONTENT_TAG, unsafe.Pointer(&v))); err != nil { 110 + return 0, err 111 + } 112 + return CellContentTag(v), nil 113 + } 114 + 115 + // Wide returns the wide property of the cell. 116 + func (c *Cell) Wide() (CellWide, error) { 117 + var v C.GhosttyCellWide 118 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_WIDE, unsafe.Pointer(&v))); err != nil { 119 + return 0, err 120 + } 121 + return CellWide(v), nil 122 + } 123 + 124 + // HasText reports whether the cell has text to render. 125 + func (c *Cell) HasText() (bool, error) { 126 + var v C.bool 127 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_HAS_TEXT, unsafe.Pointer(&v))); err != nil { 128 + return false, err 129 + } 130 + return bool(v), nil 131 + } 132 + 133 + // HasStyling reports whether the cell has non-default styling. 134 + func (c *Cell) HasStyling() (bool, error) { 135 + var v C.bool 136 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_HAS_STYLING, unsafe.Pointer(&v))); err != nil { 137 + return false, err 138 + } 139 + return bool(v), nil 140 + } 141 + 142 + // StyleID returns the style ID for the cell. 143 + func (c *Cell) StyleID() (uint16, error) { 144 + var v C.uint16_t 145 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_STYLE_ID, unsafe.Pointer(&v))); err != nil { 146 + return 0, err 147 + } 148 + return uint16(v), nil 149 + } 150 + 151 + // HasHyperlink reports whether the cell has a hyperlink. 152 + func (c *Cell) HasHyperlink() (bool, error) { 153 + var v C.bool 154 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_HAS_HYPERLINK, unsafe.Pointer(&v))); err != nil { 155 + return false, err 156 + } 157 + return bool(v), nil 158 + } 159 + 160 + // Protected reports whether the cell is protected. 161 + func (c *Cell) Protected() (bool, error) { 162 + var v C.bool 163 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_PROTECTED, unsafe.Pointer(&v))); err != nil { 164 + return false, err 165 + } 166 + return bool(v), nil 167 + } 168 + 169 + // Semantic returns the semantic content type of the cell. 170 + func (c *Cell) Semantic() (CellSemanticContent, error) { 171 + var v C.GhosttyCellSemanticContent 172 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_SEMANTIC_CONTENT, unsafe.Pointer(&v))); err != nil { 173 + return 0, err 174 + } 175 + return CellSemanticContent(v), nil 176 + } 177 + 178 + // ColorPalette returns the palette index for the cell's background 179 + // color. Only valid when the cell's content tag is CellContentBgColorPalette. 180 + func (c *Cell) ColorPalette() (uint8, error) { 181 + var v C.GhosttyColorPaletteIndex 182 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_COLOR_PALETTE, unsafe.Pointer(&v))); err != nil { 183 + return 0, err 184 + } 185 + return uint8(v), nil 186 + } 187 + 188 + // ColorRGB returns the RGB color for the cell's background color. 189 + // Only valid when the cell's content tag is CellContentBgColorRGB. 190 + func (c *Cell) ColorRGB() (ColorRGB, error) { 191 + var v C.GhosttyColorRgb 192 + if err := resultError(C.ghostty_cell_get(c.c, C.GHOSTTY_CELL_DATA_COLOR_RGB, unsafe.Pointer(&v))); err != nil { 193 + return ColorRGB{}, err 194 + } 195 + return ColorRGB{R: uint8(v.r), G: uint8(v.g), B: uint8(v.b)}, nil 196 + } 197 + 198 + // Wrap reports whether the row is soft-wrapped. 199 + func (r *Row) Wrap() (bool, error) { 200 + var v C.bool 201 + if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_WRAP, unsafe.Pointer(&v))); err != nil { 202 + return false, err 203 + } 204 + return bool(v), nil 205 + } 206 + 207 + // WrapContinuation reports whether the row is a continuation of 208 + // a soft-wrapped row. 209 + func (r *Row) WrapContinuation() (bool, error) { 210 + var v C.bool 211 + if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_WRAP_CONTINUATION, unsafe.Pointer(&v))); err != nil { 212 + return false, err 213 + } 214 + return bool(v), nil 215 + } 216 + 217 + // Grapheme reports whether any cells in the row have grapheme clusters. 218 + func (r *Row) Grapheme() (bool, error) { 219 + var v C.bool 220 + if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_GRAPHEME, unsafe.Pointer(&v))); err != nil { 221 + return false, err 222 + } 223 + return bool(v), nil 224 + } 225 + 226 + // Styled reports whether any cells in the row have styling 227 + // (may have false positives). 228 + func (r *Row) Styled() (bool, error) { 229 + var v C.bool 230 + if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_STYLED, unsafe.Pointer(&v))); err != nil { 231 + return false, err 232 + } 233 + return bool(v), nil 234 + } 235 + 236 + // Hyperlink reports whether any cells in the row have hyperlinks 237 + // (may have false positives). 238 + func (r *Row) Hyperlink() (bool, error) { 239 + var v C.bool 240 + if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_HYPERLINK, unsafe.Pointer(&v))); err != nil { 241 + return false, err 242 + } 243 + return bool(v), nil 244 + } 245 + 246 + // Semantic returns the semantic prompt state of the row. 247 + func (r *Row) Semantic() (RowSemanticPrompt, error) { 248 + var v C.GhosttyRowSemanticPrompt 249 + if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_SEMANTIC_PROMPT, unsafe.Pointer(&v))); err != nil { 250 + return 0, err 251 + } 252 + return RowSemanticPrompt(v), nil 253 + } 254 + 255 + // KittyVirtualPlaceholder reports whether the row contains a 256 + // Kitty virtual placeholder. 257 + func (r *Row) KittyVirtualPlaceholder() (bool, error) { 258 + var v C.bool 259 + if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_KITTY_VIRTUAL_PLACEHOLDER, unsafe.Pointer(&v))); err != nil { 260 + return false, err 261 + } 262 + return bool(v), nil 263 + } 264 + 265 + // Dirty reports whether the row is dirty and requires a redraw. 266 + func (r *Row) Dirty() (bool, error) { 267 + var v C.bool 268 + if err := resultError(C.ghostty_row_get(r.c, C.GHOSTTY_ROW_DATA_DIRTY, unsafe.Pointer(&v))); err != nil { 269 + return false, err 270 + } 271 + return bool(v), nil 272 + }
+40
size_report.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + */ 6 + import "C" 7 + 8 + // SizeReportStyle determines the output format for a terminal size report. 9 + // C: GhosttySizeReportStyle 10 + type SizeReportStyle int 11 + 12 + const ( 13 + // SizeReportMode2048 is the in-band size report (mode 2048). 14 + SizeReportMode2048 SizeReportStyle = C.GHOSTTY_SIZE_REPORT_MODE_2048 15 + 16 + // SizeReportCSI14T is the XTWINOPS text area size in pixels. 17 + SizeReportCSI14T SizeReportStyle = C.GHOSTTY_SIZE_REPORT_CSI_14_T 18 + 19 + // SizeReportCSI16T is the XTWINOPS cell size in pixels. 20 + SizeReportCSI16T SizeReportStyle = C.GHOSTTY_SIZE_REPORT_CSI_16_T 21 + 22 + // SizeReportCSI18T is the XTWINOPS text area size in characters. 23 + SizeReportCSI18T SizeReportStyle = C.GHOSTTY_SIZE_REPORT_CSI_18_T 24 + ) 25 + 26 + // SizeReportSize holds terminal geometry for XTWINOPS size queries. 27 + // C: GhosttySizeReportSize 28 + type SizeReportSize struct { 29 + // Rows is the terminal row count in cells. 30 + Rows uint16 31 + 32 + // Columns is the terminal column count in cells. 33 + Columns uint16 34 + 35 + // CellWidth is the width of a single terminal cell in pixels. 36 + CellWidth uint32 37 + 38 + // CellHeight is the height of a single terminal cell in pixels. 39 + CellHeight uint32 40 + }
+148
style.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <ghostty/vt.h> 5 + 6 + // Helper to create a properly initialized GhosttyStyle (sized struct). 7 + static inline GhosttyStyle init_style() { 8 + GhosttyStyle style = GHOSTTY_INIT_SIZED(GhosttyStyle); 9 + return style; 10 + } 11 + */ 12 + import "C" 13 + 14 + import "unsafe" 15 + 16 + // initCStyle returns a zero-initialized C GhosttyStyle with its size 17 + // field set (GHOSTTY_INIT_SIZED). Used by other files that need to 18 + // pass a style to C APIs. 19 + func initCStyle() C.GhosttyStyle { 20 + return C.init_style() 21 + } 22 + 23 + // StyleColorTag identifies the type of color in a style color. 24 + // C: GhosttyStyleColorTag 25 + type StyleColorTag int 26 + 27 + const ( 28 + // StyleColorNone means no color is set. 29 + StyleColorNone StyleColorTag = C.GHOSTTY_STYLE_COLOR_NONE 30 + 31 + // StyleColorPalette means the color is a palette index. 32 + StyleColorPalette StyleColorTag = C.GHOSTTY_STYLE_COLOR_PALETTE 33 + 34 + // StyleColorRGB means the color is a direct RGB value. 35 + StyleColorRGB StyleColorTag = C.GHOSTTY_STYLE_COLOR_RGB 36 + ) 37 + 38 + // StyleColor is a tagged union representing a color in a style attribute. 39 + // Check Tag to determine which field is valid. 40 + // C: GhosttyStyleColor 41 + type StyleColor struct { 42 + // Tag identifies the type of color. 43 + Tag StyleColorTag 44 + 45 + // Palette is the palette index (valid when Tag is StyleColorPalette). 46 + Palette uint8 47 + 48 + // RGB is the direct RGB color (valid when Tag is StyleColorRGB). 49 + RGB ColorRGB 50 + } 51 + 52 + // Underline style constants. 53 + // C: GhosttySgrUnderline 54 + const ( 55 + UnderlineNone = C.GHOSTTY_SGR_UNDERLINE_NONE 56 + UnderlineSingle = C.GHOSTTY_SGR_UNDERLINE_SINGLE 57 + UnderlineDouble = C.GHOSTTY_SGR_UNDERLINE_DOUBLE 58 + UnderlineCurly = C.GHOSTTY_SGR_UNDERLINE_CURLY 59 + UnderlineDotted = C.GHOSTTY_SGR_UNDERLINE_DOTTED 60 + UnderlineDashed = C.GHOSTTY_SGR_UNDERLINE_DASHED 61 + ) 62 + 63 + // Style is a thin wrapper around the C GhosttyStyle. It provides 64 + // getter methods to access individual style attributes without 65 + // copying the entire struct upfront. 66 + // C: GhosttyStyle 67 + type Style struct { 68 + c C.GhosttyStyle 69 + } 70 + 71 + // IsDefault reports whether the style is the default style 72 + // (no colors, no flags). 73 + func (s *Style) IsDefault() bool { 74 + return bool(C.ghostty_style_is_default(&s.c)) 75 + } 76 + 77 + // FgColor returns the foreground color. 78 + func (s *Style) FgColor() StyleColor { 79 + return styleColorFromC(s.c.fg_color) 80 + } 81 + 82 + // BgColor returns the background color. 83 + func (s *Style) BgColor() StyleColor { 84 + return styleColorFromC(s.c.bg_color) 85 + } 86 + 87 + // UnderlineColor returns the underline color. 88 + func (s *Style) UnderlineColor() StyleColor { 89 + return styleColorFromC(s.c.underline_color) 90 + } 91 + 92 + // Bold reports whether bold is set. 93 + func (s *Style) Bold() bool { 94 + return bool(s.c.bold) 95 + } 96 + 97 + // Italic reports whether italic is set. 98 + func (s *Style) Italic() bool { 99 + return bool(s.c.italic) 100 + } 101 + 102 + // Faint reports whether faint (dim) is set. 103 + func (s *Style) Faint() bool { 104 + return bool(s.c.faint) 105 + } 106 + 107 + // Blink reports whether blink is set. 108 + func (s *Style) Blink() bool { 109 + return bool(s.c.blink) 110 + } 111 + 112 + // Inverse reports whether inverse video is set. 113 + func (s *Style) Inverse() bool { 114 + return bool(s.c.inverse) 115 + } 116 + 117 + // Invisible reports whether invisible is set. 118 + func (s *Style) Invisible() bool { 119 + return bool(s.c.invisible) 120 + } 121 + 122 + // Strikethrough reports whether strikethrough is set. 123 + func (s *Style) Strikethrough() bool { 124 + return bool(s.c.strikethrough) 125 + } 126 + 127 + // Overline reports whether overline is set. 128 + func (s *Style) Overline() bool { 129 + return bool(s.c.overline) 130 + } 131 + 132 + // Underline returns the underline style (one of the Underline* constants). 133 + func (s *Style) Underline() int { 134 + return int(s.c.underline) 135 + } 136 + 137 + // styleColorFromC converts a C GhosttyStyleColor to a Go StyleColor. 138 + func styleColorFromC(c C.GhosttyStyleColor) StyleColor { 139 + sc := StyleColor{Tag: StyleColorTag(c.tag)} 140 + switch sc.Tag { 141 + case StyleColorPalette: 142 + sc.Palette = uint8(*(*C.GhosttyColorPaletteIndex)(unsafe.Pointer(&c.value[0]))) 143 + case StyleColorRGB: 144 + rgb := *(*C.GhosttyColorRgb)(unsafe.Pointer(&c.value[0])) 145 + sc.RGB = ColorRGB{R: uint8(rgb.r), G: uint8(rgb.g), B: uint8(rgb.b)} 146 + } 147 + return sc 148 + }
+417
terminal.go
··· 1 + package libghostty 2 + 3 + /* 4 + #include <stdlib.h> 5 + #include <ghostty/vt.h> 6 + */ 7 + import "C" 8 + 9 + import ( 10 + "runtime/cgo" 11 + "unsafe" 12 + ) 13 + 14 + // Terminal wraps a Ghostty VT terminal handle. 15 + // C: GhosttyTerminal 16 + type Terminal struct { 17 + ptr C.GhosttyTerminal 18 + 19 + // handle is a cgo.Handle pointing back to this Terminal. It is 20 + // stored as the C-side userdata (GHOSTTY_TERMINAL_OPT_USERDATA) 21 + // so that C effect trampolines can recover the *Terminal and 22 + // dispatch to the appropriate Go effect handler. 23 + handle cgo.Handle 24 + 25 + onWritePty WritePtyFn 26 + onBell BellFn 27 + onTitleChanged TitleChangedFn 28 + onEnquiry EnquiryFn 29 + onXtversion XtversionFn 30 + onSize SizeFn 31 + onColorScheme ColorSchemeFn 32 + onDeviceAttributes DeviceAttributesFn 33 + 34 + // effectBuf holds C-allocated memory for the most recent response 35 + // returned by an effect trampoline (e.g. enquiry, xtversion). 36 + // libghostty copies the data immediately, so a single buffer 37 + // shared across effects is sufficient. 38 + effectBuf unsafe.Pointer 39 + } 40 + 41 + // TerminalOption is a functional option for configuring a Terminal. 42 + type TerminalOption func(*TerminalConfig) 43 + 44 + // TerminalConfig holds the configuration for creating a Terminal. 45 + // It can be passed directly to NewTerminal or built up using 46 + // functional options like WithSize and WithMaxScrollback. 47 + // C: GhosttyTerminalOptions 48 + type TerminalConfig struct { 49 + // Cols is the terminal width in cells. Must be greater than zero. 50 + Cols uint16 51 + 52 + // Rows is the terminal height in cells. Must be greater than zero. 53 + Rows uint16 54 + 55 + // MaxScrollback is the maximum number of lines to keep in scrollback 56 + // history. Defaults to 0 (no scrollback). 57 + MaxScrollback uint 58 + 59 + // Effect handlers applied after terminal creation. 60 + onWritePty WritePtyFn 61 + onBell BellFn 62 + onTitleChanged TitleChangedFn 63 + onEnquiry EnquiryFn 64 + onXtversion XtversionFn 65 + onSize SizeFn 66 + onColorScheme ColorSchemeFn 67 + onDeviceAttributes DeviceAttributesFn 68 + } 69 + 70 + // WritePtyFn is called when the terminal writes data back to the pty 71 + // (e.g. query responses). The data is only valid for the call duration. 72 + // C: GhosttyTerminalWritePtyFn 73 + type WritePtyFn func(data []byte) 74 + 75 + // BellFn is called when the terminal receives a BEL character (0x07). 76 + // C: GhosttyTerminalBellFn 77 + type BellFn func() 78 + 79 + // TitleChangedFn is called when the terminal title changes via OSC 0/2. 80 + // C: GhosttyTerminalTitleChangedFn 81 + type TitleChangedFn func() 82 + 83 + // EnquiryFn is called when the terminal receives ENQ (0x05). 84 + // Return the response bytes; nil or empty means no response. 85 + // C: GhosttyTerminalEnquiryFn 86 + type EnquiryFn func() []byte 87 + 88 + // XtversionFn is called for XTVERSION queries (CSI > q). 89 + // Return the version string; empty uses the default "libghostty". 90 + // C: GhosttyTerminalXtversionFn 91 + type XtversionFn func() string 92 + 93 + // SizeFn is called for XTWINOPS size queries (CSI 14/16/18 t). 94 + // Return the size and true, or zero value and false to ignore the query. 95 + // C: GhosttyTerminalSizeFn 96 + type SizeFn func() (SizeReportSize, bool) 97 + 98 + // ColorSchemeFn is called for color scheme queries (CSI ? 996 n). 99 + // Return the scheme and true, or zero value and false to ignore the query. 100 + // C: GhosttyTerminalColorSchemeFn 101 + type ColorSchemeFn func() (ColorScheme, bool) 102 + 103 + // DeviceAttributesFn is called for device attributes queries 104 + // (CSI c / CSI > c / CSI = c). Return the attributes and true, 105 + // or zero value and false to ignore the query. 106 + // C: GhosttyTerminalDeviceAttributesFn 107 + type DeviceAttributesFn func() (DeviceAttributes, bool) 108 + 109 + // WithSize sets the terminal dimensions in cells. 110 + // Both cols and rows must be greater than zero. 111 + func WithSize(cols, rows uint16) TerminalOption { 112 + return func(c *TerminalConfig) { 113 + c.Cols = cols 114 + c.Rows = rows 115 + } 116 + } 117 + 118 + // WithMaxScrollback sets the maximum number of lines to keep in 119 + // scrollback history. Defaults to 0 (no scrollback). 120 + func WithMaxScrollback(lines uint) TerminalOption { 121 + return func(c *TerminalConfig) { 122 + c.MaxScrollback = lines 123 + } 124 + } 125 + 126 + // WithWritePty registers an effect handler invoked when the terminal 127 + // writes data back to the pty (e.g. query responses). The data slice 128 + // is only valid for the duration of the call. 129 + func WithWritePty(fn WritePtyFn) TerminalOption { 130 + return func(c *TerminalConfig) { 131 + c.onWritePty = fn 132 + } 133 + } 134 + 135 + // WithBell registers an effect handler invoked when the terminal 136 + // receives a BEL character (0x07). 137 + func WithBell(fn BellFn) TerminalOption { 138 + return func(c *TerminalConfig) { 139 + c.onBell = fn 140 + } 141 + } 142 + 143 + // WithTitleChanged registers an effect handler invoked when the 144 + // terminal title changes via OSC 0 or OSC 2. 145 + func WithTitleChanged(fn TitleChangedFn) TerminalOption { 146 + return func(c *TerminalConfig) { 147 + c.onTitleChanged = fn 148 + } 149 + } 150 + 151 + // WithEnquiry registers an effect handler invoked when the terminal 152 + // receives an ENQ character (0x05). Return the response bytes; nil 153 + // or empty means no response. 154 + func WithEnquiry(fn EnquiryFn) TerminalOption { 155 + return func(c *TerminalConfig) { 156 + c.onEnquiry = fn 157 + } 158 + } 159 + 160 + // WithXtversion registers an effect handler invoked for XTVERSION 161 + // queries (CSI > q). Return the version string; empty uses the 162 + // default "libghostty" version. 163 + func WithXtversion(fn XtversionFn) TerminalOption { 164 + return func(c *TerminalConfig) { 165 + c.onXtversion = fn 166 + } 167 + } 168 + 169 + // WithSizeReport registers an effect handler invoked for XTWINOPS 170 + // size queries (CSI 14/16/18 t). Return the size and true, or 171 + // zero value and false to silently ignore the query. 172 + func WithSizeReport(fn SizeFn) TerminalOption { 173 + return func(c *TerminalConfig) { 174 + c.onSize = fn 175 + } 176 + } 177 + 178 + // WithColorScheme registers an effect handler invoked for color 179 + // scheme queries (CSI ? 996 n). Return the scheme and true, or 180 + // zero value and false to silently ignore the query. 181 + func WithColorScheme(fn ColorSchemeFn) TerminalOption { 182 + return func(c *TerminalConfig) { 183 + c.onColorScheme = fn 184 + } 185 + } 186 + 187 + // WithDeviceAttributes registers an effect handler invoked for 188 + // device attributes queries (CSI c / CSI > c / CSI = c). Return 189 + // the attributes and true, or zero value and false to silently 190 + // ignore the query. 191 + func WithDeviceAttributes(fn DeviceAttributesFn) TerminalOption { 192 + return func(c *TerminalConfig) { 193 + c.onDeviceAttributes = fn 194 + } 195 + } 196 + 197 + // NewTerminal creates a new terminal with the given options. 198 + // WithSize is required; cols and rows must both be greater than zero. 199 + func NewTerminal(opts ...TerminalOption) (*Terminal, error) { 200 + // Apply defaults and user options. 201 + cfg := TerminalConfig{} 202 + for _, opt := range opts { 203 + opt(&cfg) 204 + } 205 + 206 + options := C.GhosttyTerminalOptions{ 207 + cols: C.uint16_t(cfg.Cols), 208 + rows: C.uint16_t(cfg.Rows), 209 + max_scrollback: C.size_t(cfg.MaxScrollback), 210 + } 211 + 212 + var cterm C.GhosttyTerminal 213 + if err := resultError(C.ghostty_terminal_new(nil, &cterm, options)); err != nil { 214 + return nil, err 215 + } 216 + 217 + t := &Terminal{ 218 + ptr: cterm, 219 + onWritePty: cfg.onWritePty, 220 + onBell: cfg.onBell, 221 + onTitleChanged: cfg.onTitleChanged, 222 + onEnquiry: cfg.onEnquiry, 223 + onXtversion: cfg.onXtversion, 224 + onSize: cfg.onSize, 225 + onColorScheme: cfg.onColorScheme, 226 + onDeviceAttributes: cfg.onDeviceAttributes, 227 + } 228 + 229 + // Always set userdata to our handle so trampolines can find us. 230 + t.handle = cgo.NewHandle(t) 231 + C.ghostty_terminal_set( 232 + t.ptr, 233 + C.GHOSTTY_TERMINAL_OPT_USERDATA, 234 + unsafe.Pointer(t.handle), 235 + ) 236 + 237 + // Register any effects that were provided via options. 238 + t.syncEffects() 239 + 240 + return t, nil 241 + } 242 + 243 + // Close frees the underlying terminal handle and releases the cgo.Handle. 244 + // After this call, the terminal must not be used. 245 + func (t *Terminal) Close() { 246 + t.handle.Delete() 247 + C.ghostty_terminal_free(t.ptr) 248 + if t.effectBuf != nil { 249 + C.free(t.effectBuf) 250 + } 251 + } 252 + 253 + // Reset performs a full terminal reset (RIS). 254 + // All state is reset to initial configuration (modes, scrollback, 255 + // scrolling region, screen contents). Dimensions are preserved. 256 + func (t *Terminal) Reset() { 257 + C.ghostty_terminal_reset(t.ptr) 258 + } 259 + 260 + // Resize changes the terminal dimensions. 261 + // Both cols and rows must be greater than zero. cellWidthPx and 262 + // cellHeightPx specify the pixel dimensions of a single cell, used 263 + // for image protocols and size reports. 264 + func (t *Terminal) Resize(cols, rows uint16, cellWidthPx, cellHeightPx uint32) error { 265 + return resultError(C.ghostty_terminal_resize( 266 + t.ptr, 267 + C.uint16_t(cols), 268 + C.uint16_t(rows), 269 + C.uint32_t(cellWidthPx), 270 + C.uint32_t(cellHeightPx), 271 + )) 272 + } 273 + 274 + // VTWrite feeds raw VT-encoded bytes through the terminal's parser, 275 + // updating terminal state. Malformed input is handled gracefully and 276 + // will not cause an error. 277 + func (t *Terminal) VTWrite(data []byte) { 278 + if len(data) == 0 { 279 + return 280 + } 281 + C.ghostty_terminal_vt_write(t.ptr, (*C.uint8_t)(&data[0]), C.size_t(len(data))) 282 + } 283 + 284 + // Write implements io.Writer by feeding data through the terminal's 285 + // VT parser. It always consumes all bytes and never returns an error. 286 + func (t *Terminal) Write(p []byte) (int, error) { 287 + t.VTWrite(p) 288 + return len(p), nil 289 + } 290 + 291 + // ModeGet returns the current value of a terminal mode. 292 + func (t *Terminal) ModeGet(mode Mode) (bool, error) { 293 + var val C.bool 294 + if err := resultError(C.ghostty_terminal_mode_get(t.ptr, C.GhosttyMode(mode), &val)); err != nil { 295 + return false, err 296 + } 297 + return bool(val), nil 298 + } 299 + 300 + // ModeSet sets a terminal mode to the given value. 301 + func (t *Terminal) ModeSet(mode Mode, value bool) error { 302 + return resultError(C.ghostty_terminal_mode_set(t.ptr, C.GhosttyMode(mode), C.bool(value))) 303 + } 304 + 305 + // ScrollViewportTag describes the scroll behavior. 306 + // C: GhosttyTerminalScrollViewportTag 307 + type ScrollViewportTag int 308 + 309 + const ( 310 + // ScrollViewportTop scrolls to the top of scrollback. 311 + ScrollViewportTop ScrollViewportTag = C.GHOSTTY_SCROLL_VIEWPORT_TOP 312 + 313 + // ScrollViewportBottom scrolls to the bottom (active area). 314 + ScrollViewportBottom ScrollViewportTag = C.GHOSTTY_SCROLL_VIEWPORT_BOTTOM 315 + 316 + // ScrollViewportDelta scrolls by a delta amount (up is negative). 317 + ScrollViewportDelta ScrollViewportTag = C.GHOSTTY_SCROLL_VIEWPORT_DELTA 318 + ) 319 + 320 + // ScrollViewport scrolls the terminal viewport to the top of scrollback. 321 + func (t *Terminal) ScrollViewportTop() { 322 + var sv C.GhosttyTerminalScrollViewport 323 + sv.tag = C.GHOSTTY_SCROLL_VIEWPORT_TOP 324 + C.ghostty_terminal_scroll_viewport(t.ptr, sv) 325 + } 326 + 327 + // ScrollViewportBottom scrolls the terminal viewport to the bottom 328 + // (active area). 329 + func (t *Terminal) ScrollViewportBottom() { 330 + var sv C.GhosttyTerminalScrollViewport 331 + sv.tag = C.GHOSTTY_SCROLL_VIEWPORT_BOTTOM 332 + C.ghostty_terminal_scroll_viewport(t.ptr, sv) 333 + } 334 + 335 + // ScrollViewportDelta scrolls the terminal viewport by the given delta 336 + // (negative for up, positive for down). 337 + func (t *Terminal) ScrollViewportDelta(delta int) { 338 + var sv C.GhosttyTerminalScrollViewport 339 + sv.tag = C.GHOSTTY_SCROLL_VIEWPORT_DELTA 340 + // Set the delta in the value union. The delta field is at offset 0. 341 + *(*C.intptr_t)(unsafe.Pointer(&sv.value[0])) = C.intptr_t(delta) 342 + C.ghostty_terminal_scroll_viewport(t.ptr, sv) 343 + } 344 + 345 + // TerminalScreen identifies which screen buffer is active. 346 + // C: GhosttyTerminalScreen 347 + type TerminalScreen int 348 + 349 + const ( 350 + // ScreenPrimary is the primary (normal) screen. 351 + ScreenPrimary TerminalScreen = C.GHOSTTY_TERMINAL_SCREEN_PRIMARY 352 + 353 + // ScreenAlternate is the alternate screen. 354 + ScreenAlternate TerminalScreen = C.GHOSTTY_TERMINAL_SCREEN_ALTERNATE 355 + ) 356 + 357 + // Scrollbar holds the scrollbar state for the terminal viewport. 358 + // C: GhosttyTerminalScrollbar 359 + type Scrollbar struct { 360 + // Total is the total size of the scrollable area in rows. 361 + Total uint64 362 + 363 + // Offset is the offset into the total area that the viewport is at. 364 + Offset uint64 365 + 366 + // Len is the length of the visible area in rows. 367 + Len uint64 368 + } 369 + 370 + // KittyKeyFlags holds the current Kitty keyboard protocol flags. 371 + // These can be combined using bitwise OR. 372 + // C: GhosttyKittyKeyFlags (uint8_t) 373 + type KittyKeyFlags uint8 374 + 375 + // Kitty keyboard protocol flag constants. 376 + const ( 377 + // KittyKeyDisabled means the Kitty keyboard protocol is disabled. 378 + KittyKeyDisabled KittyKeyFlags = C.GHOSTTY_KITTY_KEY_DISABLED 379 + 380 + // KittyKeyDisambiguate enables disambiguating escape codes. 381 + KittyKeyDisambiguate KittyKeyFlags = C.GHOSTTY_KITTY_KEY_DISAMBIGUATE 382 + 383 + // KittyKeyReportEvents enables reporting key press and release events. 384 + KittyKeyReportEvents KittyKeyFlags = C.GHOSTTY_KITTY_KEY_REPORT_EVENTS 385 + 386 + // KittyKeyReportAlternates enables reporting alternate key codes. 387 + KittyKeyReportAlternates KittyKeyFlags = C.GHOSTTY_KITTY_KEY_REPORT_ALTERNATES 388 + 389 + // KittyKeyReportAll enables reporting all key events including those 390 + // normally handled by the terminal. 391 + KittyKeyReportAll KittyKeyFlags = C.GHOSTTY_KITTY_KEY_REPORT_ALL 392 + 393 + // KittyKeyReportAssociated enables reporting associated text with 394 + // key events. 395 + KittyKeyReportAssociated KittyKeyFlags = C.GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED 396 + 397 + // KittyKeyAll has all Kitty keyboard protocol flags enabled. 398 + KittyKeyAll KittyKeyFlags = C.GHOSTTY_KITTY_KEY_ALL 399 + ) 400 + 401 + // GridRef resolves a point in the terminal grid to a grid reference. 402 + // The returned GridRef is only valid until the next terminal update. 403 + // 404 + // Lookups using PointTagActive and PointTagViewport are fast. 405 + // PointTagScreen and PointTagHistory may be expensive for large 406 + // scrollback buffers. 407 + func (t *Terminal) GridRef(point Point) (*GridRef, error) { 408 + ref := initCGridRef() 409 + if err := resultError(C.ghostty_terminal_grid_ref( 410 + t.ptr, 411 + point.toC(), 412 + &ref, 413 + )); err != nil { 414 + return nil, err 415 + } 416 + return &GridRef{ref: ref}, nil 417 + }
+263
terminal_data.go
··· 1 + package libghostty 2 + 3 + // Terminal data getters wrapping ghostty_terminal_get(). 4 + // Functions are ordered alphabetically. 5 + 6 + /* 7 + #include <ghostty/vt.h> 8 + */ 9 + import "C" 10 + 11 + import ( 12 + "errors" 13 + "unsafe" 14 + ) 15 + 16 + // ActiveScreen returns which screen buffer is currently active. 17 + func (t *Terminal) ActiveScreen() (TerminalScreen, error) { 18 + var v C.GhosttyTerminalScreen 19 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_ACTIVE_SCREEN, unsafe.Pointer(&v))); err != nil { 20 + return 0, err 21 + } 22 + return TerminalScreen(v), nil 23 + } 24 + 25 + // Cols returns the terminal width in cells. 26 + func (t *Terminal) Cols() (uint16, error) { 27 + var v C.uint16_t 28 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_COLS, unsafe.Pointer(&v))); err != nil { 29 + return 0, err 30 + } 31 + return uint16(v), nil 32 + } 33 + 34 + // ColorBackground returns the effective background color (OSC override 35 + // or default). Returns nil if no background color is set. 36 + func (t *Terminal) ColorBackground() (*ColorRGB, error) { 37 + return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND) 38 + } 39 + 40 + // ColorBackgroundDefault returns the default background color, ignoring 41 + // any OSC override. Returns nil if no default is set. 42 + func (t *Terminal) ColorBackgroundDefault() (*ColorRGB, error) { 43 + return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT) 44 + } 45 + 46 + // ColorCursor returns the effective cursor color (OSC override or 47 + // default). Returns nil if no cursor color is set. 48 + func (t *Terminal) ColorCursor() (*ColorRGB, error) { 49 + return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_CURSOR) 50 + } 51 + 52 + // ColorCursorDefault returns the default cursor color, ignoring any 53 + // OSC override. Returns nil if no default is set. 54 + func (t *Terminal) ColorCursorDefault() (*ColorRGB, error) { 55 + return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT) 56 + } 57 + 58 + // ColorForeground returns the effective foreground color (OSC override 59 + // or default). Returns nil if no foreground color is set. 60 + func (t *Terminal) ColorForeground() (*ColorRGB, error) { 61 + return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND) 62 + } 63 + 64 + // ColorForegroundDefault returns the default foreground color, ignoring 65 + // any OSC override. Returns nil if no default is set. 66 + func (t *Terminal) ColorForegroundDefault() (*ColorRGB, error) { 67 + return t.getColorRGB(C.GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT) 68 + } 69 + 70 + // ColorPalette returns the current 256-color palette (with any OSC 71 + // overrides applied). 72 + func (t *Terminal) ColorPalette() (*Palette, error) { 73 + return t.getPalette(C.GHOSTTY_TERMINAL_DATA_COLOR_PALETTE) 74 + } 75 + 76 + // ColorPaletteDefault returns the default 256-color palette, ignoring 77 + // any OSC overrides. 78 + func (t *Terminal) ColorPaletteDefault() (*Palette, error) { 79 + return t.getPalette(C.GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT) 80 + } 81 + 82 + // CursorPendingWrap reports whether the cursor has a pending wrap 83 + // (the next printed character will soft-wrap to the next line). 84 + func (t *Terminal) CursorPendingWrap() (bool, error) { 85 + var v C.bool 86 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_PENDING_WRAP, unsafe.Pointer(&v))); err != nil { 87 + return false, err 88 + } 89 + return bool(v), nil 90 + } 91 + 92 + // CursorStyle returns the current SGR style of the cursor. This is 93 + // the style that will be applied to newly printed characters. 94 + func (t *Terminal) CursorStyle() (*Style, error) { 95 + cs := initCStyle() 96 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_STYLE, unsafe.Pointer(&cs))); err != nil { 97 + return nil, err 98 + } 99 + return &Style{c: cs}, nil 100 + } 101 + 102 + // CursorVisible reports whether the cursor is visible (DEC mode 25). 103 + func (t *Terminal) CursorVisible() (bool, error) { 104 + var v C.bool 105 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE, unsafe.Pointer(&v))); err != nil { 106 + return false, err 107 + } 108 + return bool(v), nil 109 + } 110 + 111 + // CursorX returns the cursor column position (0-indexed). 112 + func (t *Terminal) CursorX() (uint16, error) { 113 + var v C.uint16_t 114 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_X, unsafe.Pointer(&v))); err != nil { 115 + return 0, err 116 + } 117 + return uint16(v), nil 118 + } 119 + 120 + // CursorY returns the cursor row position within the active area 121 + // (0-indexed). 122 + func (t *Terminal) CursorY() (uint16, error) { 123 + var v C.uint16_t 124 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_CURSOR_Y, unsafe.Pointer(&v))); err != nil { 125 + return 0, err 126 + } 127 + return uint16(v), nil 128 + } 129 + 130 + // HeightPx returns the total terminal height in pixels 131 + // (rows * cell_height_px as set by Resize). 132 + func (t *Terminal) HeightPx() (uint32, error) { 133 + var v C.uint32_t 134 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_HEIGHT_PX, unsafe.Pointer(&v))); err != nil { 135 + return 0, err 136 + } 137 + return uint32(v), nil 138 + } 139 + 140 + // KittyKeyboardFlags returns the current Kitty keyboard protocol flags. 141 + func (t *Terminal) KittyKeyboardFlags() (KittyKeyFlags, error) { 142 + var v C.uint8_t 143 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_KITTY_KEYBOARD_FLAGS, unsafe.Pointer(&v))); err != nil { 144 + return 0, err 145 + } 146 + return KittyKeyFlags(v), nil 147 + } 148 + 149 + // MouseTracking reports whether any mouse tracking mode is active. 150 + func (t *Terminal) MouseTracking() (bool, error) { 151 + var v C.bool 152 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_MOUSE_TRACKING, unsafe.Pointer(&v))); err != nil { 153 + return false, err 154 + } 155 + return bool(v), nil 156 + } 157 + 158 + // Pwd returns the terminal's current working directory as set by 159 + // escape sequences (e.g. OSC 7). Returns an empty string if unset. 160 + // The returned string is copied; it remains valid after subsequent 161 + // calls to VTWrite or Reset. 162 + func (t *Terminal) Pwd() (string, error) { 163 + var s C.GhosttyString 164 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_PWD, unsafe.Pointer(&s))); err != nil { 165 + return "", err 166 + } 167 + return C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len)), nil 168 + } 169 + 170 + // Rows returns the terminal height in cells. 171 + func (t *Terminal) Rows() (uint16, error) { 172 + var v C.uint16_t 173 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_ROWS, unsafe.Pointer(&v))); err != nil { 174 + return 0, err 175 + } 176 + return uint16(v), nil 177 + } 178 + 179 + // Scrollbar returns the scrollbar state for the terminal viewport. 180 + // This may be expensive to calculate depending on the viewport position; 181 + // call only as needed. 182 + func (t *Terminal) Scrollbar() (Scrollbar, error) { 183 + var v C.GhosttyTerminalScrollbar 184 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_SCROLLBAR, unsafe.Pointer(&v))); err != nil { 185 + return Scrollbar{}, err 186 + } 187 + return Scrollbar{ 188 + Total: uint64(v.total), 189 + Offset: uint64(v.offset), 190 + Len: uint64(v.len), 191 + }, nil 192 + } 193 + 194 + // ScrollbackRows returns the number of scrollback rows (total rows 195 + // minus viewport rows). 196 + func (t *Terminal) ScrollbackRows() (uint, error) { 197 + var v C.size_t 198 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS, unsafe.Pointer(&v))); err != nil { 199 + return 0, err 200 + } 201 + return uint(v), nil 202 + } 203 + 204 + // Title returns the terminal title as set by escape sequences 205 + // (e.g. OSC 0/2). Returns an empty string if unset. The returned 206 + // string is copied; it remains valid after subsequent calls to 207 + // VTWrite or Reset. 208 + func (t *Terminal) Title() (string, error) { 209 + var s C.GhosttyString 210 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_TITLE, unsafe.Pointer(&s))); err != nil { 211 + return "", err 212 + } 213 + return C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len)), nil 214 + } 215 + 216 + // TotalRows returns the total number of rows in the active screen 217 + // including scrollback. 218 + func (t *Terminal) TotalRows() (uint, error) { 219 + var v C.size_t 220 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_TOTAL_ROWS, unsafe.Pointer(&v))); err != nil { 221 + return 0, err 222 + } 223 + return uint(v), nil 224 + } 225 + 226 + // WidthPx returns the total terminal width in pixels 227 + // (cols * cell_width_px as set by Resize). 228 + func (t *Terminal) WidthPx() (uint32, error) { 229 + var v C.uint32_t 230 + if err := resultError(C.ghostty_terminal_get(t.ptr, C.GHOSTTY_TERMINAL_DATA_WIDTH_PX, unsafe.Pointer(&v))); err != nil { 231 + return 0, err 232 + } 233 + return uint32(v), nil 234 + } 235 + 236 + // getColorRGB is a helper that reads a single ColorRGB value from the 237 + // terminal. Returns nil (without error) when the result is NO_VALUE. 238 + func (t *Terminal) getColorRGB(data C.GhosttyTerminalData) (*ColorRGB, error) { 239 + var c C.GhosttyColorRgb 240 + err := resultError(C.ghostty_terminal_get(t.ptr, data, unsafe.Pointer(&c))) 241 + if err != nil { 242 + var ge *Error 243 + if errors.As(err, &ge) && ge.Result == ResultNoValue { 244 + return nil, nil 245 + } 246 + return nil, err 247 + } 248 + return &ColorRGB{R: uint8(c.r), G: uint8(c.g), B: uint8(c.b)}, nil 249 + } 250 + 251 + // getPalette is a helper that reads a full 256-color palette from the 252 + // terminal. 253 + func (t *Terminal) getPalette(data C.GhosttyTerminalData) (*Palette, error) { 254 + var cp [PaletteSize]C.GhosttyColorRgb 255 + if err := resultError(C.ghostty_terminal_get(t.ptr, data, unsafe.Pointer(&cp[0]))); err != nil { 256 + return nil, err 257 + } 258 + var p Palette 259 + for i, c := range cp { 260 + p[i] = ColorRGB{R: uint8(c.r), G: uint8(c.g), B: uint8(c.b)} 261 + } 262 + return &p, nil 263 + }
+268
terminal_data_test.go
··· 1 + package libghostty 2 + 3 + import "testing" 4 + 5 + func TestTerminalColsRows(t *testing.T) { 6 + term, err := NewTerminal(WithSize(80, 24)) 7 + if err != nil { 8 + t.Fatal(err) 9 + } 10 + defer term.Close() 11 + 12 + cols, err := term.Cols() 13 + if err != nil { 14 + t.Fatal(err) 15 + } 16 + if cols != 80 { 17 + t.Fatalf("expected 80 cols, got %d", cols) 18 + } 19 + 20 + rows, err := term.Rows() 21 + if err != nil { 22 + t.Fatal(err) 23 + } 24 + if rows != 24 { 25 + t.Fatalf("expected 24 rows, got %d", rows) 26 + } 27 + 28 + // Resize and verify. 29 + if err := term.Resize(120, 40, 8, 16); err != nil { 30 + t.Fatal(err) 31 + } 32 + cols, _ = term.Cols() 33 + rows, _ = term.Rows() 34 + if cols != 120 || rows != 40 { 35 + t.Fatalf("expected 120x40 after resize, got %dx%d", cols, rows) 36 + } 37 + } 38 + 39 + func TestTerminalCursorPosition(t *testing.T) { 40 + term, err := NewTerminal(WithSize(80, 24)) 41 + if err != nil { 42 + t.Fatal(err) 43 + } 44 + defer term.Close() 45 + 46 + // Cursor starts at 0,0. 47 + x, err := term.CursorX() 48 + if err != nil { 49 + t.Fatal(err) 50 + } 51 + y, err := term.CursorY() 52 + if err != nil { 53 + t.Fatal(err) 54 + } 55 + if x != 0 || y != 0 { 56 + t.Fatalf("expected cursor at 0,0, got %d,%d", x, y) 57 + } 58 + 59 + // Move cursor to col 5, row 3 (1-indexed in VT: CSI 4;6H). 60 + term.VTWrite([]byte("\x1b[4;6H")) 61 + x, _ = term.CursorX() 62 + y, _ = term.CursorY() 63 + if x != 5 || y != 3 { 64 + t.Fatalf("expected cursor at 5,3, got %d,%d", x, y) 65 + } 66 + } 67 + 68 + func TestTerminalTitle(t *testing.T) { 69 + term, err := NewTerminal(WithSize(80, 24)) 70 + if err != nil { 71 + t.Fatal(err) 72 + } 73 + defer term.Close() 74 + 75 + // Set title via SetTitle and read it back. 76 + if err := term.SetTitle("hello"); err != nil { 77 + t.Fatal(err) 78 + } 79 + title, err := term.Title() 80 + if err != nil { 81 + t.Fatal(err) 82 + } 83 + if title != "hello" { 84 + t.Fatalf("expected title %q, got %q", "hello", title) 85 + } 86 + 87 + // Set title via VT escape and read back. 88 + term.VTWrite([]byte("\x1b]2;world\x07")) 89 + title, _ = term.Title() 90 + if title != "world" { 91 + t.Fatalf("expected title %q, got %q", "world", title) 92 + } 93 + } 94 + 95 + func TestTerminalPwd(t *testing.T) { 96 + term, err := NewTerminal(WithSize(80, 24)) 97 + if err != nil { 98 + t.Fatal(err) 99 + } 100 + defer term.Close() 101 + 102 + if err := term.SetPwd("/home/user"); err != nil { 103 + t.Fatal(err) 104 + } 105 + pwd, err := term.Pwd() 106 + if err != nil { 107 + t.Fatal(err) 108 + } 109 + if pwd != "/home/user" { 110 + t.Fatalf("expected pwd %q, got %q", "/home/user", pwd) 111 + } 112 + } 113 + 114 + func TestTerminalTotalScrollbackRows(t *testing.T) { 115 + term, err := NewTerminal(WithSize(80, 24), WithMaxScrollback(100)) 116 + if err != nil { 117 + t.Fatal(err) 118 + } 119 + defer term.Close() 120 + 121 + total, err := term.TotalRows() 122 + if err != nil { 123 + t.Fatal(err) 124 + } 125 + if total < 24 { 126 + t.Fatalf("expected at least 24 total rows, got %d", total) 127 + } 128 + 129 + sb, err := term.ScrollbackRows() 130 + if err != nil { 131 + t.Fatal(err) 132 + } 133 + // Fresh terminal has no scrollback content. 134 + if sb != 0 { 135 + t.Fatalf("expected 0 scrollback rows, got %d", sb) 136 + } 137 + } 138 + 139 + func TestTerminalPixelDimensions(t *testing.T) { 140 + term, err := NewTerminal(WithSize(80, 24)) 141 + if err != nil { 142 + t.Fatal(err) 143 + } 144 + defer term.Close() 145 + 146 + // Before resize with pixel sizes, dimensions are 0. 147 + if err := term.Resize(80, 24, 10, 20); err != nil { 148 + t.Fatal(err) 149 + } 150 + w, err := term.WidthPx() 151 + if err != nil { 152 + t.Fatal(err) 153 + } 154 + h, err := term.HeightPx() 155 + if err != nil { 156 + t.Fatal(err) 157 + } 158 + if w != 800 { 159 + t.Fatalf("expected width 800px, got %d", w) 160 + } 161 + if h != 480 { 162 + t.Fatalf("expected height 480px, got %d", h) 163 + } 164 + } 165 + 166 + func TestTerminalMouseTracking(t *testing.T) { 167 + term, err := NewTerminal(WithSize(80, 24)) 168 + if err != nil { 169 + t.Fatal(err) 170 + } 171 + defer term.Close() 172 + 173 + tracking, err := term.MouseTracking() 174 + if err != nil { 175 + t.Fatal(err) 176 + } 177 + if tracking { 178 + t.Fatal("expected no mouse tracking by default") 179 + } 180 + 181 + // Enable normal mouse tracking. 182 + term.VTWrite([]byte("\x1b[?1000h")) 183 + tracking, _ = term.MouseTracking() 184 + if !tracking { 185 + t.Fatal("expected mouse tracking after enabling") 186 + } 187 + } 188 + 189 + func TestTerminalColorRoundTrip(t *testing.T) { 190 + term, err := NewTerminal(WithSize(80, 24)) 191 + if err != nil { 192 + t.Fatal(err) 193 + } 194 + defer term.Close() 195 + 196 + // No color set initially. 197 + fg, err := term.ColorForeground() 198 + if err != nil { 199 + t.Fatal(err) 200 + } 201 + if fg != nil { 202 + t.Fatal("expected nil foreground before setting") 203 + } 204 + 205 + // Set and read back. 206 + red := &ColorRGB{R: 255, G: 0, B: 0} 207 + if err := term.SetColorForeground(red); err != nil { 208 + t.Fatal(err) 209 + } 210 + fg, err = term.ColorForeground() 211 + if err != nil { 212 + t.Fatal(err) 213 + } 214 + if fg == nil || *fg != *red { 215 + t.Fatalf("expected %+v, got %+v", red, fg) 216 + } 217 + 218 + // Default getter should also return the value. 219 + fgDef, err := term.ColorForegroundDefault() 220 + if err != nil { 221 + t.Fatal(err) 222 + } 223 + if fgDef == nil || *fgDef != *red { 224 + t.Fatalf("expected default %+v, got %+v", red, fgDef) 225 + } 226 + 227 + // Clear and verify nil again. 228 + if err := term.SetColorForeground(nil); err != nil { 229 + t.Fatal(err) 230 + } 231 + fg, _ = term.ColorForeground() 232 + if fg != nil { 233 + t.Fatal("expected nil foreground after clearing") 234 + } 235 + } 236 + 237 + func TestTerminalPaletteRoundTrip(t *testing.T) { 238 + term, err := NewTerminal(WithSize(80, 24)) 239 + if err != nil { 240 + t.Fatal(err) 241 + } 242 + defer term.Close() 243 + 244 + // Read default palette. 245 + p, err := term.ColorPalette() 246 + if err != nil { 247 + t.Fatal(err) 248 + } 249 + if p == nil { 250 + t.Fatal("expected non-nil palette") 251 + } 252 + 253 + // Set a custom palette and read it back. 254 + var custom Palette 255 + for i := range custom { 256 + custom[i] = ColorRGB{R: uint8(i), G: 0, B: 0} 257 + } 258 + if err := term.SetColorPalette(&custom); err != nil { 259 + t.Fatal(err) 260 + } 261 + p, _ = term.ColorPalette() 262 + if p[0] != (ColorRGB{R: 0, G: 0, B: 0}) { 263 + t.Fatalf("expected palette[0] = {0,0,0}, got %+v", p[0]) 264 + } 265 + if p[128] != (ColorRGB{R: 128, G: 0, B: 0}) { 266 + t.Fatalf("expected palette[128] = {128,0,0}, got %+v", p[128]) 267 + } 268 + }
+235
terminal_effect.go
··· 1 + package libghostty 2 + 3 + // C trampolines for terminal effects. 4 + // 5 + // Each exported Go function here is passed to the C side as a function 6 + // pointer. The C library calls it with a userdata void*, which is a 7 + // cgo.Handle pointing back to the owning *Terminal. The trampoline 8 + // recovers the Terminal and dispatches to the user-supplied Go effect handler. 9 + 10 + /* 11 + #include <stdlib.h> 12 + #include <ghostty/vt.h> 13 + 14 + // Forward declarations for the Go trampolines so we can take their 15 + // addresses on the C side. 16 + extern void goWritePtyTrampoline(GhosttyTerminal, void*, uint8_t*, size_t); 17 + extern void goBellTrampoline(GhosttyTerminal, void*); 18 + extern void goTitleChangedTrampoline(GhosttyTerminal, void*); 19 + extern GhosttyString goEnquiryTrampoline(GhosttyTerminal, void*); 20 + extern GhosttyString goXtversionTrampoline(GhosttyTerminal, void*); 21 + extern bool goSizeTrampoline(GhosttyTerminal, void*, GhosttySizeReportSize*); 22 + extern bool goColorSchemeTrampoline(GhosttyTerminal, void*, GhosttyColorScheme*); 23 + extern bool goDeviceAttributesTrampoline(GhosttyTerminal, void*, GhosttyDeviceAttributes*); 24 + 25 + // Helpers to set each effect via ghostty_terminal_set. 26 + // We need these because cgo cannot take the address of a Go-exported 27 + // function directly as a C function pointer. 28 + static inline GhosttyResult set_write_pty(GhosttyTerminal t) { 29 + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_WRITE_PTY, (const void*)goWritePtyTrampoline); 30 + } 31 + static inline GhosttyResult set_bell(GhosttyTerminal t) { 32 + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_BELL, (const void*)goBellTrampoline); 33 + } 34 + static inline GhosttyResult set_title_changed(GhosttyTerminal t) { 35 + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, (const void*)goTitleChangedTrampoline); 36 + } 37 + static inline GhosttyResult set_enquiry(GhosttyTerminal t) { 38 + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_ENQUIRY, (const void*)goEnquiryTrampoline); 39 + } 40 + static inline GhosttyResult set_xtversion(GhosttyTerminal t) { 41 + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_XTVERSION, (const void*)goXtversionTrampoline); 42 + } 43 + static inline GhosttyResult set_size(GhosttyTerminal t) { 44 + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_SIZE, (const void*)goSizeTrampoline); 45 + } 46 + static inline GhosttyResult set_color_scheme(GhosttyTerminal t) { 47 + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_COLOR_SCHEME, (const void*)goColorSchemeTrampoline); 48 + } 49 + static inline GhosttyResult set_device_attributes(GhosttyTerminal t) { 50 + return ghostty_terminal_set(t, GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES, (const void*)goDeviceAttributesTrampoline); 51 + } 52 + 53 + // Helper to clear an effect by setting it to NULL. 54 + static inline GhosttyResult clear_effect(GhosttyTerminal t, GhosttyTerminalOption opt) { 55 + return ghostty_terminal_set(t, opt, NULL); 56 + } 57 + */ 58 + import "C" 59 + 60 + import ( 61 + "runtime/cgo" 62 + "unsafe" 63 + ) 64 + 65 + // syncEffects registers or clears each C effect based on whether 66 + // the corresponding Go effect handler is set. 67 + func (t *Terminal) syncEffects() { 68 + if t.onWritePty != nil { 69 + C.set_write_pty(t.ptr) 70 + } else { 71 + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_WRITE_PTY) 72 + } 73 + if t.onBell != nil { 74 + C.set_bell(t.ptr) 75 + } else { 76 + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_BELL) 77 + } 78 + if t.onTitleChanged != nil { 79 + C.set_title_changed(t.ptr) 80 + } else { 81 + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_TITLE_CHANGED) 82 + } 83 + if t.onEnquiry != nil { 84 + C.set_enquiry(t.ptr) 85 + } else { 86 + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_ENQUIRY) 87 + } 88 + if t.onXtversion != nil { 89 + C.set_xtversion(t.ptr) 90 + } else { 91 + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_XTVERSION) 92 + } 93 + if t.onSize != nil { 94 + C.set_size(t.ptr) 95 + } else { 96 + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_SIZE) 97 + } 98 + if t.onColorScheme != nil { 99 + C.set_color_scheme(t.ptr) 100 + } else { 101 + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_COLOR_SCHEME) 102 + } 103 + if t.onDeviceAttributes != nil { 104 + C.set_device_attributes(t.ptr) 105 + } else { 106 + C.clear_effect(t.ptr, C.GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES) 107 + } 108 + } 109 + 110 + // terminalFromUserdata recovers a *Terminal from the C userdata pointer. 111 + func terminalFromUserdata(userdata unsafe.Pointer) *Terminal { 112 + return cgo.Handle(userdata).Value().(*Terminal) 113 + } 114 + 115 + //export goWritePtyTrampoline 116 + func goWritePtyTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, data *C.uint8_t, length C.size_t) { 117 + t := terminalFromUserdata(userdata) 118 + if t.onWritePty != nil { 119 + t.onWritePty(C.GoBytes(unsafe.Pointer(data), C.int(length))) 120 + } 121 + } 122 + 123 + //export goBellTrampoline 124 + func goBellTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) { 125 + t := terminalFromUserdata(userdata) 126 + if t.onBell != nil { 127 + t.onBell() 128 + } 129 + } 130 + 131 + //export goTitleChangedTrampoline 132 + func goTitleChangedTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) { 133 + t := terminalFromUserdata(userdata) 134 + if t.onTitleChanged != nil { 135 + t.onTitleChanged() 136 + } 137 + } 138 + 139 + //export goEnquiryTrampoline 140 + func goEnquiryTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) C.GhosttyString { 141 + t := terminalFromUserdata(userdata) 142 + if t.onEnquiry == nil { 143 + return C.GhosttyString{} 144 + } 145 + return t.effectString(t.onEnquiry()) 146 + } 147 + 148 + //export goXtversionTrampoline 149 + func goXtversionTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer) C.GhosttyString { 150 + t := terminalFromUserdata(userdata) 151 + if t.onXtversion == nil { 152 + return C.GhosttyString{} 153 + } 154 + return t.effectString([]byte(t.onXtversion())) 155 + } 156 + 157 + //export goSizeTrampoline 158 + func goSizeTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, outSize *C.GhosttySizeReportSize) C.bool { 159 + t := terminalFromUserdata(userdata) 160 + if t.onSize == nil { 161 + return C.bool(false) 162 + } 163 + size, ok := t.onSize() 164 + if !ok { 165 + return C.bool(false) 166 + } 167 + outSize.rows = C.uint16_t(size.Rows) 168 + outSize.columns = C.uint16_t(size.Columns) 169 + outSize.cell_width = C.uint32_t(size.CellWidth) 170 + outSize.cell_height = C.uint32_t(size.CellHeight) 171 + return C.bool(true) 172 + } 173 + 174 + //export goColorSchemeTrampoline 175 + func goColorSchemeTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, outScheme *C.GhosttyColorScheme) C.bool { 176 + t := terminalFromUserdata(userdata) 177 + if t.onColorScheme == nil { 178 + return C.bool(false) 179 + } 180 + scheme, ok := t.onColorScheme() 181 + if !ok { 182 + return C.bool(false) 183 + } 184 + *outScheme = C.GhosttyColorScheme(scheme) 185 + return C.bool(true) 186 + } 187 + 188 + //export goDeviceAttributesTrampoline 189 + func goDeviceAttributesTrampoline(_ C.GhosttyTerminal, userdata unsafe.Pointer, outAttrs *C.GhosttyDeviceAttributes) C.bool { 190 + t := terminalFromUserdata(userdata) 191 + if t.onDeviceAttributes == nil { 192 + return C.bool(false) 193 + } 194 + attrs, ok := t.onDeviceAttributes() 195 + if !ok { 196 + return C.bool(false) 197 + } 198 + 199 + // Primary (DA1). 200 + outAttrs.primary.conformance_level = C.uint16_t(attrs.Primary.ConformanceLevel) 201 + outAttrs.primary.num_features = C.size_t(attrs.Primary.NumFeatures) 202 + for i := 0; i < attrs.Primary.NumFeatures && i < 64; i++ { 203 + outAttrs.primary.features[i] = C.uint16_t(attrs.Primary.Features[i]) 204 + } 205 + 206 + // Secondary (DA2). 207 + outAttrs.secondary.device_type = C.uint16_t(attrs.Secondary.DeviceType) 208 + outAttrs.secondary.firmware_version = C.uint16_t(attrs.Secondary.FirmwareVersion) 209 + outAttrs.secondary.rom_cartridge = C.uint16_t(attrs.Secondary.ROMCartridge) 210 + 211 + // Tertiary (DA3). 212 + outAttrs.tertiary.unit_id = C.uint32_t(attrs.Tertiary.UnitID) 213 + 214 + return C.bool(true) 215 + } 216 + 217 + // effectString copies data into C memory, updates effectBuf, and 218 + // returns a GhosttyString pointing to it. The previous effectBuf 219 + // is freed. Returns a zero-length GhosttyString if data is empty. 220 + func (t *Terminal) effectString(data []byte) C.GhosttyString { 221 + if t.effectBuf != nil { 222 + C.free(t.effectBuf) 223 + } 224 + 225 + if len(data) == 0 { 226 + return C.GhosttyString{} 227 + } 228 + 229 + cmem := C.CBytes(data) 230 + t.effectBuf = cmem 231 + return C.GhosttyString{ 232 + ptr: (*C.uint8_t)(cmem), 233 + len: C.size_t(len(data)), 234 + } 235 + }
+157
terminal_opt.go
··· 1 + package libghostty 2 + 3 + // Terminal option setters wrapping ghostty_terminal_set(). 4 + // Functions are ordered alphabetically. 5 + 6 + /* 7 + #include <ghostty/vt.h> 8 + */ 9 + import "C" 10 + 11 + import "unsafe" 12 + 13 + // SetEffectWritePty registers (or clears) the write-pty effect on a 14 + // live terminal. Pass nil to clear. 15 + func (t *Terminal) SetEffectWritePty(fn WritePtyFn) { 16 + t.onWritePty = fn 17 + t.syncEffects() 18 + } 19 + 20 + // SetEffectBell registers (or clears) the bell effect on a live terminal. 21 + // Pass nil to clear. 22 + func (t *Terminal) SetEffectBell(fn BellFn) { 23 + t.onBell = fn 24 + t.syncEffects() 25 + } 26 + 27 + // SetEffectTitleChanged registers (or clears) the title-changed effect 28 + // on a live terminal. Pass nil to clear. 29 + func (t *Terminal) SetEffectTitleChanged(fn TitleChangedFn) { 30 + t.onTitleChanged = fn 31 + t.syncEffects() 32 + } 33 + 34 + // SetEffectEnquiry registers (or clears) the enquiry effect on a live 35 + // terminal. Pass nil to clear. 36 + func (t *Terminal) SetEffectEnquiry(fn EnquiryFn) { 37 + t.onEnquiry = fn 38 + t.syncEffects() 39 + } 40 + 41 + // SetEffectXtversion registers (or clears) the xtversion effect on a 42 + // live terminal. Pass nil to clear. 43 + func (t *Terminal) SetEffectXtversion(fn XtversionFn) { 44 + t.onXtversion = fn 45 + t.syncEffects() 46 + } 47 + 48 + // SetEffectSize registers (or clears) the size-report effect on a 49 + // live terminal. Pass nil to clear. 50 + func (t *Terminal) SetEffectSize(fn SizeFn) { 51 + t.onSize = fn 52 + t.syncEffects() 53 + } 54 + 55 + // SetEffectColorScheme registers (or clears) the color-scheme effect 56 + // on a live terminal. Pass nil to clear. 57 + func (t *Terminal) SetEffectColorScheme(fn ColorSchemeFn) { 58 + t.onColorScheme = fn 59 + t.syncEffects() 60 + } 61 + 62 + // SetEffectDeviceAttributes registers (or clears) the device-attributes 63 + // effect on a live terminal. Pass nil to clear. 64 + func (t *Terminal) SetEffectDeviceAttributes(fn DeviceAttributesFn) { 65 + t.onDeviceAttributes = fn 66 + t.syncEffects() 67 + } 68 + 69 + // SetColorBackground sets the default background color. Pass nil to 70 + // clear (unset). 71 + func (t *Terminal) SetColorBackground(c *ColorRGB) error { 72 + var val unsafe.Pointer 73 + if c != nil { 74 + cc := C.GhosttyColorRgb{r: C.uint8_t(c.R), g: C.uint8_t(c.G), b: C.uint8_t(c.B)} 75 + val = unsafe.Pointer(&cc) 76 + } 77 + return resultError(C.ghostty_terminal_set( 78 + t.ptr, 79 + C.GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND, 80 + val, 81 + )) 82 + } 83 + 84 + // SetColorCursor sets the default cursor color. Pass nil to clear (unset). 85 + func (t *Terminal) SetColorCursor(c *ColorRGB) error { 86 + var val unsafe.Pointer 87 + if c != nil { 88 + cc := C.GhosttyColorRgb{r: C.uint8_t(c.R), g: C.uint8_t(c.G), b: C.uint8_t(c.B)} 89 + val = unsafe.Pointer(&cc) 90 + } 91 + return resultError(C.ghostty_terminal_set( 92 + t.ptr, 93 + C.GHOSTTY_TERMINAL_OPT_COLOR_CURSOR, 94 + val, 95 + )) 96 + } 97 + 98 + // SetColorForeground sets the default foreground color. Pass nil to 99 + // clear (unset). 100 + func (t *Terminal) SetColorForeground(c *ColorRGB) error { 101 + var val unsafe.Pointer 102 + if c != nil { 103 + cc := C.GhosttyColorRgb{r: C.uint8_t(c.R), g: C.uint8_t(c.G), b: C.uint8_t(c.B)} 104 + val = unsafe.Pointer(&cc) 105 + } 106 + return resultError(C.ghostty_terminal_set( 107 + t.ptr, 108 + C.GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, 109 + val, 110 + )) 111 + } 112 + 113 + // SetColorPalette sets the default 256-color palette. Pass nil to reset 114 + // to the built-in default palette. 115 + func (t *Terminal) SetColorPalette(palette *Palette) error { 116 + var val unsafe.Pointer 117 + if palette != nil { 118 + // Convert Go palette to C palette. 119 + var cp [PaletteSize]C.GhosttyColorRgb 120 + for i, c := range palette { 121 + cp[i] = C.GhosttyColorRgb{r: C.uint8_t(c.R), g: C.uint8_t(c.G), b: C.uint8_t(c.B)} 122 + } 123 + val = unsafe.Pointer(&cp[0]) 124 + } 125 + return resultError(C.ghostty_terminal_set( 126 + t.ptr, 127 + C.GHOSTTY_TERMINAL_OPT_COLOR_PALETTE, 128 + val, 129 + )) 130 + } 131 + 132 + // SetPwd sets the terminal working directory manually. An empty string 133 + // clears it. 134 + func (t *Terminal) SetPwd(pwd string) error { 135 + s := C.GhosttyString{ 136 + ptr: (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(pwd))), 137 + len: C.size_t(len(pwd)), 138 + } 139 + return resultError(C.ghostty_terminal_set( 140 + t.ptr, 141 + C.GHOSTTY_TERMINAL_OPT_PWD, 142 + unsafe.Pointer(&s), 143 + )) 144 + } 145 + 146 + // SetTitle sets the terminal title manually. An empty string clears it. 147 + func (t *Terminal) SetTitle(title string) error { 148 + s := C.GhosttyString{ 149 + ptr: (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(title))), 150 + len: C.size_t(len(title)), 151 + } 152 + return resultError(C.ghostty_terminal_set( 153 + t.ptr, 154 + C.GHOSTTY_TERMINAL_OPT_TITLE, 155 + unsafe.Pointer(&s), 156 + )) 157 + }
+233
terminal_opt_test.go
··· 1 + package libghostty 2 + 3 + import "testing" 4 + 5 + func TestTerminalSetTitle(t *testing.T) { 6 + term, err := NewTerminal(WithSize(80, 24)) 7 + if err != nil { 8 + t.Fatal(err) 9 + } 10 + defer term.Close() 11 + 12 + if err := term.SetTitle("my terminal"); err != nil { 13 + t.Fatal(err) 14 + } 15 + 16 + // Clear the title. 17 + if err := term.SetTitle(""); err != nil { 18 + t.Fatal(err) 19 + } 20 + } 21 + 22 + func TestTerminalSetPwd(t *testing.T) { 23 + term, err := NewTerminal(WithSize(80, 24)) 24 + if err != nil { 25 + t.Fatal(err) 26 + } 27 + defer term.Close() 28 + 29 + if err := term.SetPwd("/tmp"); err != nil { 30 + t.Fatal(err) 31 + } 32 + 33 + if err := term.SetPwd(""); err != nil { 34 + t.Fatal(err) 35 + } 36 + } 37 + 38 + func TestTerminalSetColors(t *testing.T) { 39 + term, err := NewTerminal(WithSize(80, 24)) 40 + if err != nil { 41 + t.Fatal(err) 42 + } 43 + defer term.Close() 44 + 45 + white := &ColorRGB{R: 255, G: 255, B: 255} 46 + black := &ColorRGB{R: 0, G: 0, B: 0} 47 + 48 + if err := term.SetColorForeground(white); err != nil { 49 + t.Fatal(err) 50 + } 51 + if err := term.SetColorBackground(black); err != nil { 52 + t.Fatal(err) 53 + } 54 + if err := term.SetColorCursor(white); err != nil { 55 + t.Fatal(err) 56 + } 57 + 58 + // Clear colors. 59 + if err := term.SetColorForeground(nil); err != nil { 60 + t.Fatal(err) 61 + } 62 + if err := term.SetColorBackground(nil); err != nil { 63 + t.Fatal(err) 64 + } 65 + if err := term.SetColorCursor(nil); err != nil { 66 + t.Fatal(err) 67 + } 68 + } 69 + 70 + func TestTerminalWithBell(t *testing.T) { 71 + var bellCount int 72 + term, err := NewTerminal(WithSize(80, 24), WithBell(func() { 73 + bellCount++ 74 + })) 75 + if err != nil { 76 + t.Fatal(err) 77 + } 78 + defer term.Close() 79 + 80 + // BEL character should trigger the callback. 81 + term.VTWrite([]byte("\x07")) 82 + if bellCount != 1 { 83 + t.Fatalf("expected 1 bell, got %d", bellCount) 84 + } 85 + 86 + // Multiple BELs. 87 + term.VTWrite([]byte("\x07\x07")) 88 + if bellCount != 3 { 89 + t.Fatalf("expected 3 bells, got %d", bellCount) 90 + } 91 + } 92 + 93 + func TestTerminalSetEffectBell(t *testing.T) { 94 + term, err := NewTerminal(WithSize(80, 24)) 95 + if err != nil { 96 + t.Fatal(err) 97 + } 98 + defer term.Close() 99 + 100 + var bellCount int 101 + term.SetEffectBell(func() { 102 + bellCount++ 103 + }) 104 + 105 + term.VTWrite([]byte("\x07")) 106 + if bellCount != 1 { 107 + t.Fatalf("expected 1 bell, got %d", bellCount) 108 + } 109 + 110 + // Clear the callback; bell should no longer fire. 111 + term.SetEffectBell(nil) 112 + term.VTWrite([]byte("\x07")) 113 + if bellCount != 1 { 114 + t.Fatalf("expected still 1 bell after clearing, got %d", bellCount) 115 + } 116 + } 117 + 118 + func TestTerminalWithWritePty(t *testing.T) { 119 + var received []byte 120 + term, err := NewTerminal(WithSize(80, 24), WithWritePty(func(data []byte) { 121 + received = append(received, data...) 122 + })) 123 + if err != nil { 124 + t.Fatal(err) 125 + } 126 + defer term.Close() 127 + 128 + // DA1 query should produce a response via write_pty. 129 + term.VTWrite([]byte("\x1b[c")) 130 + if len(received) == 0 { 131 + t.Fatal("expected write_pty data from DA1 query") 132 + } 133 + } 134 + 135 + func TestTerminalWithTitleChanged(t *testing.T) { 136 + var titleChanged int 137 + term, err := NewTerminal(WithSize(80, 24), WithTitleChanged(func() { 138 + titleChanged++ 139 + })) 140 + if err != nil { 141 + t.Fatal(err) 142 + } 143 + defer term.Close() 144 + 145 + // OSC 2 sets the title. 146 + term.VTWrite([]byte("\x1b]2;hello\x07")) 147 + if titleChanged != 1 { 148 + t.Fatalf("expected 1 title change, got %d", titleChanged) 149 + } 150 + } 151 + 152 + func TestTerminalWithEnquiry(t *testing.T) { 153 + var received []byte 154 + term, err := NewTerminal( 155 + WithSize(80, 24), 156 + WithWritePty(func(data []byte) { 157 + received = append(received, data...) 158 + }), 159 + WithEnquiry(func() []byte { 160 + return []byte("hello") 161 + }), 162 + ) 163 + if err != nil { 164 + t.Fatal(err) 165 + } 166 + defer term.Close() 167 + 168 + // ENQ character should trigger enquiry and write response via pty. 169 + term.VTWrite([]byte("\x05")) 170 + if string(received) != "hello" { 171 + t.Fatalf("expected enquiry response %q, got %q", "hello", string(received)) 172 + } 173 + } 174 + 175 + func TestTerminalWithXtversion(t *testing.T) { 176 + var received []byte 177 + term, err := NewTerminal( 178 + WithSize(80, 24), 179 + WithWritePty(func(data []byte) { 180 + received = append(received, data...) 181 + }), 182 + WithXtversion(func() string { 183 + return "myterm 1.0" 184 + }), 185 + ) 186 + if err != nil { 187 + t.Fatal(err) 188 + } 189 + defer term.Close() 190 + 191 + // XTVERSION query: CSI > q 192 + term.VTWrite([]byte("\x1b[>q")) 193 + // Response should contain our version string in a DCS sequence. 194 + if len(received) == 0 { 195 + t.Fatal("expected xtversion response") 196 + } 197 + resp := string(received) 198 + if !contains(resp, "myterm 1.0") { 199 + t.Fatalf("expected response to contain %q, got %q", "myterm 1.0", resp) 200 + } 201 + } 202 + 203 + // contains reports whether s contains substr. 204 + func contains(s, substr string) bool { 205 + for i := 0; i+len(substr) <= len(s); i++ { 206 + if s[i:i+len(substr)] == substr { 207 + return true 208 + } 209 + } 210 + return false 211 + } 212 + 213 + func TestTerminalSetColorPalette(t *testing.T) { 214 + term, err := NewTerminal(WithSize(80, 24)) 215 + if err != nil { 216 + t.Fatal(err) 217 + } 218 + defer term.Close() 219 + 220 + // Set a custom palette. 221 + var palette Palette 222 + for i := range palette { 223 + palette[i] = ColorRGB{R: uint8(i), G: uint8(i), B: uint8(i)} 224 + } 225 + if err := term.SetColorPalette(&palette); err != nil { 226 + t.Fatal(err) 227 + } 228 + 229 + // Reset to default. 230 + if err := term.SetColorPalette(nil); err != nil { 231 + t.Fatal(err) 232 + } 233 + }
+233
terminal_test.go
··· 1 + package libghostty 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "testing" 7 + ) 8 + 9 + func TestNewTerminalClose(t *testing.T) { 10 + term, err := NewTerminal(WithSize(80, 24)) 11 + if err != nil { 12 + t.Fatal(err) 13 + } 14 + term.Close() 15 + } 16 + 17 + func TestNewTerminalZeroDimensions(t *testing.T) { 18 + _, err := NewTerminal(WithSize(0, 24)) 19 + if err == nil { 20 + t.Fatal("expected error for zero cols") 21 + } 22 + 23 + _, err = NewTerminal(WithSize(80, 0)) 24 + if err == nil { 25 + t.Fatal("expected error for zero rows") 26 + } 27 + } 28 + 29 + func TestNewTerminalNoOptions(t *testing.T) { 30 + _, err := NewTerminal() 31 + if err == nil { 32 + t.Fatal("expected error when no size is specified") 33 + } 34 + } 35 + 36 + func TestNewTerminalWithScrollback(t *testing.T) { 37 + term, err := NewTerminal(WithSize(80, 24), WithMaxScrollback(1000)) 38 + if err != nil { 39 + t.Fatal(err) 40 + } 41 + term.Close() 42 + } 43 + 44 + func TestTerminalReset(t *testing.T) { 45 + term, err := NewTerminal(WithSize(80, 24)) 46 + if err != nil { 47 + t.Fatal(err) 48 + } 49 + defer term.Close() 50 + 51 + // Write some data then reset; should not panic or error. 52 + term.VTWrite([]byte("hello")) 53 + term.Reset() 54 + } 55 + 56 + func TestTerminalResize(t *testing.T) { 57 + term, err := NewTerminal(WithSize(80, 24)) 58 + if err != nil { 59 + t.Fatal(err) 60 + } 61 + defer term.Close() 62 + 63 + if err := term.Resize(120, 40, 8, 16); err != nil { 64 + t.Fatal(err) 65 + } 66 + } 67 + 68 + func TestTerminalResizeZero(t *testing.T) { 69 + term, err := NewTerminal(WithSize(80, 24)) 70 + if err != nil { 71 + t.Fatal(err) 72 + } 73 + defer term.Close() 74 + 75 + if err := term.Resize(0, 24, 8, 16); err == nil { 76 + t.Fatal("expected error for zero cols") 77 + } 78 + } 79 + 80 + func TestTerminalVTWrite(t *testing.T) { 81 + term, err := NewTerminal(WithSize(80, 24)) 82 + if err != nil { 83 + t.Fatal(err) 84 + } 85 + defer term.Close() 86 + 87 + // Write plain text and escape sequences; should not panic. 88 + term.VTWrite([]byte("hello world")) 89 + term.VTWrite([]byte("\x1b[2J")) // clear screen 90 + term.VTWrite(nil) // empty write 91 + } 92 + 93 + func TestTerminalIOWriter(t *testing.T) { 94 + term, err := NewTerminal(WithSize(80, 24)) 95 + if err != nil { 96 + t.Fatal(err) 97 + } 98 + defer term.Close() 99 + 100 + // Terminal satisfies io.Writer. 101 + var w io.Writer = term 102 + n, err := fmt.Fprintf(w, "hello %s", "world") 103 + if err != nil { 104 + t.Fatal(err) 105 + } 106 + if n != len("hello world") { 107 + t.Fatalf("expected %d bytes written, got %d", len("hello world"), n) 108 + } 109 + } 110 + 111 + func TestTerminalModeGetSet(t *testing.T) { 112 + term, err := NewTerminal(WithSize(80, 24)) 113 + if err != nil { 114 + t.Fatal(err) 115 + } 116 + defer term.Close() 117 + 118 + // Test several modes: set via ModeSet, read back via ModeGet. 119 + tests := []struct { 120 + name string 121 + mode Mode 122 + defaultVal bool 123 + }{ 124 + {"CursorVisible", ModeCursorVisible, true}, 125 + {"Wraparound", ModeWraparound, true}, 126 + {"BracketedPaste", ModeBracketedPaste, false}, 127 + {"FocusEvent", ModeFocusEvent, false}, 128 + {"AltScreen", ModeAltScreen, false}, 129 + {"Origin", ModeOrigin, false}, 130 + } 131 + 132 + for _, tt := range tests { 133 + t.Run(tt.name, func(t *testing.T) { 134 + // Verify default value. 135 + val, err := term.ModeGet(tt.mode) 136 + if err != nil { 137 + t.Fatal(err) 138 + } 139 + if val != tt.defaultVal { 140 + t.Fatalf("expected default %v, got %v", tt.defaultVal, val) 141 + } 142 + 143 + // Toggle the mode. 144 + if err := term.ModeSet(tt.mode, !tt.defaultVal); err != nil { 145 + t.Fatal(err) 146 + } 147 + val, err = term.ModeGet(tt.mode) 148 + if err != nil { 149 + t.Fatal(err) 150 + } 151 + if val != !tt.defaultVal { 152 + t.Fatalf("expected %v after set, got %v", !tt.defaultVal, val) 153 + } 154 + 155 + // Toggle back. 156 + if err := term.ModeSet(tt.mode, tt.defaultVal); err != nil { 157 + t.Fatal(err) 158 + } 159 + val, err = term.ModeGet(tt.mode) 160 + if err != nil { 161 + t.Fatal(err) 162 + } 163 + if val != tt.defaultVal { 164 + t.Fatalf("expected %v after restore, got %v", tt.defaultVal, val) 165 + } 166 + }) 167 + } 168 + } 169 + 170 + func TestTerminalModeVTWrite(t *testing.T) { 171 + term, err := NewTerminal(WithSize(80, 24)) 172 + if err != nil { 173 + t.Fatal(err) 174 + } 175 + defer term.Close() 176 + 177 + // Test that VT escape sequences correctly set and reset modes 178 + // and that ModeGet reads them back. 179 + // DEC private modes use CSI ? <n> h (set) / CSI ? <n> l (reset). 180 + // ANSI modes use CSI <n> h (set) / CSI <n> l (reset). 181 + tests := []struct { 182 + name string 183 + mode Mode 184 + setSeq string 185 + resetSeq string 186 + defaultVal bool 187 + }{ 188 + {"BracketedPaste", ModeBracketedPaste, "\x1b[?2004h", "\x1b[?2004l", false}, 189 + {"CursorVisible", ModeCursorVisible, "\x1b[?25h", "\x1b[?25l", true}, 190 + {"FocusEvent", ModeFocusEvent, "\x1b[?1004h", "\x1b[?1004l", false}, 191 + {"NormalMouse", ModeNormalMouse, "\x1b[?1000h", "\x1b[?1000l", false}, 192 + {"SGRMouse", ModeSGRMouse, "\x1b[?1006h", "\x1b[?1006l", false}, 193 + {"Insert", ModeInsert, "\x1b[4h", "\x1b[4l", false}, 194 + } 195 + 196 + for _, tt := range tests { 197 + t.Run(tt.name, func(t *testing.T) { 198 + // Verify default. 199 + val, err := term.ModeGet(tt.mode) 200 + if err != nil { 201 + t.Fatal(err) 202 + } 203 + if val != tt.defaultVal { 204 + t.Fatalf("expected default %v, got %v", tt.defaultVal, val) 205 + } 206 + 207 + // Set via VT escape sequence. 208 + term.VTWrite([]byte(tt.setSeq)) 209 + val, err = term.ModeGet(tt.mode) 210 + if err != nil { 211 + t.Fatal(err) 212 + } 213 + if !val { 214 + t.Fatal("expected mode set after VT write set sequence") 215 + } 216 + 217 + // Reset via VT escape sequence. 218 + term.VTWrite([]byte(tt.resetSeq)) 219 + val, err = term.ModeGet(tt.mode) 220 + if err != nil { 221 + t.Fatal(err) 222 + } 223 + if val { 224 + t.Fatal("expected mode reset after VT write reset sequence") 225 + } 226 + 227 + // Restore to default for next subtest. 228 + if tt.defaultVal { 229 + term.VTWrite([]byte(tt.setSeq)) 230 + } 231 + }) 232 + } 233 + }