Merge branch 'main' into fix/keybind

This commit is contained in:
Austin Drummond
2026-04-08 19:48:57 -04:00
77 changed files with 4836 additions and 307 deletions

7
.github/VOUCHED.td vendored
View File

@@ -82,6 +82,7 @@ faukah
filip7
flou
francescarpi
fru1tworld
gagbo
ghokun
gmile
@@ -102,6 +103,7 @@ icodesign
j0hnm4r5
jacobsandlund
jake-stewart
jamylak
jarred-sumner
jcollie
jesusvazquez
@@ -115,8 +117,10 @@ juniqlim
justonia
karesansui-u
kawarimidoll
kayleung
kenvandine
khipp
kierancanter
kirwiisp
kjvdven
kloneets
@@ -149,11 +153,13 @@ mischief
mitchellh
miupa
molechowski
moonmao42
mrconnorkenway
mrmage
mtak
natesmyth
neo773
neurosnap
nicholas-ochoa
nicosuave
nmggithub
@@ -189,6 +195,7 @@ seruman
silveirapf
slsrepo
sunshine-syz
tbrundige
tdgroot
tdslot
ticclick

View File

@@ -41,7 +41,7 @@ jobs:
mkdir dist
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6
- uses: flatpak/flatpak-github-actions/flatpak-builder@401fe28a8384095fc1531b9d320b292f0ee45adb # v6.7
with:
bundle: com.mitchellh.ghostty
manifest-path: dist/flatpak/com.mitchellh.ghostty.yml

View File

@@ -219,6 +219,7 @@ jobs:
)
runs-on: namespace-profile-ghostty-sm
env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
@@ -260,6 +261,110 @@ jobs:
libghostty-vt-source.tar.gz.minisig
token: ${{ secrets.GH_RELEASE_TOKEN }}
- name: Prep R2 Storage
run: |
mkdir -p blob/${GHOSTTY_COMMIT_LONG}
cp libghostty-vt-source.tar.gz blob/${GHOSTTY_COMMIT_LONG}/libghostty-vt-source.tar.gz
- name: Upload to R2
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }}
r2-bucket: ghostty-tip
source-dir: blob
destination-dir: ./
- name: Echo Release URLs
run: |
echo "Release URLs:"
echo " Source Tarball: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/libghostty-vt-source.tar.gz"
build-lib-vt-xcframework:
needs: [setup]
if: |
needs.setup.outputs.should_skip != 'true' &&
(
github.event_name == 'workflow_dispatch' ||
(
github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main'
)
)
runs-on: namespace-profile-ghostty-macos-tahoe
env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
with:
cache: |
xcode
path: |
/Users/runner/zig
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
with:
determinate: true
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.3.app
- name: Build XCFramework
run: nix develop -c zig build -Demit-lib-vt -Doptimize=ReleaseFast
- name: Zip XCFramework
run: |
cd zig-out/lib
zip -9 -r ../../ghostty-vt.xcframework.zip ghostty-vt.xcframework
- name: Sign XCFramework
run: |
echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key
echo -n "${{ secrets.MINISIGN_PASSWORD }}" > minisign.password
nix develop -c minisign -S -m ghostty-vt.xcframework.zip -s minisign.key < minisign.password
- name: Update Release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true
tag_name: tip
target_commitish: ${{ github.sha }}
files: |
ghostty-vt.xcframework.zip
ghostty-vt.xcframework.zip.minisig
token: ${{ secrets.GH_RELEASE_TOKEN }}
- name: Prep R2 Storage
run: |
mkdir -p blob/${GHOSTTY_COMMIT_LONG}
cp ghostty-vt.xcframework.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-vt.xcframework.zip
- name: Upload to R2
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }}
r2-bucket: ghostty-tip
source-dir: blob
destination-dir: ./
- name: Echo Release URLs
run: |
echo "Release URLs:"
echo " XCFramework: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-vt.xcframework.zip"
build-macos:
needs: [setup]
if: |

View File

@@ -89,6 +89,7 @@ jobs:
- build-examples-zig
- build-examples-cmake
- build-examples-cmake-windows
- build-examples-swift
- build-cmake
- build-flatpak
- build-libghostty-vt
@@ -98,6 +99,7 @@ jobs:
- build-linux
- build-linux-libghostty
- build-nix
# - build-nix-macos
- build-macos
- build-macos-freetype
- build-snap
@@ -181,6 +183,7 @@ jobs:
outputs:
zig: ${{ steps.list.outputs.zig }}
cmake: ${{ steps.list.outputs.cmake }}
swift: ${{ steps.list.outputs.swift }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -193,6 +196,9 @@ jobs:
cmake=$(ls example/*/CMakeLists.txt 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))')
echo "$cmake" | jq .
echo "cmake=$cmake" >> "$GITHUB_OUTPUT"
swift=$(ls example/*/Package.swift 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))')
echo "$swift" | jq .
echo "swift=$swift" >> "$GITHUB_OUTPUT"
build-examples-zig:
strategy:
@@ -290,6 +296,49 @@ jobs:
cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=${{ github.workspace }}
cmake --build build
build-examples-swift:
strategy:
fail-fast: false
matrix:
dir: ${{ fromJSON(needs.list-examples.outputs.swift) }}
name: Example ${{ matrix.dir }}
runs-on: namespace-profile-ghostty-macos-tahoe
needs: [test, list-examples]
env:
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
with:
cache: |
xcode
path: |
/Users/runner/zig
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
with:
determinate: true
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.3.app
- name: Build XCFramework
run: nix develop -c zig build -Demit-lib-vt
- name: Build and Run Example
run: |
cd example/${{ matrix.dir }}
swift run
build-cmake:
runs-on: namespace-profile-ghostty-sm
needs: test
@@ -765,16 +814,40 @@ jobs:
run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'main_ghostty.main'
- name: Test ReleaseFast build of libghostty-vt
run: nix build .#libghostty-vt-releasefast
run: |
nix build .#libghostty-vt-releasefast
nix build .#libghostty-vt-releasefast.tests.sanity-check
nix build .#libghostty-vt-releasefast.tests.pkg-config
nix build .#libghostty-vt-releasefast.tests.build-with-shared
nix build .#libghostty-vt-releasefast.tests.build-with-static
nix build .#libghostty-vt-releasefast.tests.build-example-c-vt-build-info
- name: Check to see if the library looks sane
run: nm result/lib/libghostty-vt.so.0.1.0 2>&1 | grep -q 'ghostty_terminal_new'
- name: Test ReleaseFast (no SIMD) build of libghostty-vt
run: |
nix build .#libghostty-vt-releasefast-no-simd
nix build .#libghostty-vt-releasefast-no-simd.tests.sanity-check
nix build .#libghostty-vt-releasefast-no-simd.tests.pkg-config
nix build .#libghostty-vt-releasefast-no-simd.tests.build-with-shared
nix build .#libghostty-vt-releasefast-no-simd.tests.build-with-static
nix build .#libghostty-vt-releasefast-no-simd.tests.build-example-c-vt-build-info
- name: Test Debug build of libghostty-vt
run: nix build .#libghostty-vt-debug
run: |
nix build .#libghostty-vt-debug
nix build .#libghostty-vt-debug.tests.sanity-check
nix build .#libghostty-vt-debug.tests.pkg-config
nix build .#libghostty-vt-debug.tests.build-with-shared
nix build .#libghostty-vt-debug.tests.build-with-static
nix build .#libghostty-vt-debug.tests.build-example-c-vt-build-info
- name: Check to see if the library looks sane
run: nm result/lib/libghostty-vt.so.0.1.0 2>&1 | grep -q 'ghostty_terminal_new'
- name: Test Debug (no SIMD) build of libghostty-vt
run: |
nix build .#libghostty-vt-debug-no-simd
nix build .#libghostty-vt-debug-no-simd.tests.sanity-check
nix build .#libghostty-vt-debug-no-simd.tests.pkg-config
nix build .#libghostty-vt-debug-no-simd.tests.build-with-shared
nix build .#libghostty-vt-debug-no-simd.tests.build-with-static
nix build .#libghostty-vt-debug-no-simd.tests.build-example-c-vt-build-info
build-dist:
runs-on: namespace-profile-ghostty-sm
@@ -896,6 +969,58 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# build-nix-macos:
# runs-on: namespace-profile-ghostty-macos-tahoe
# needs: test
# steps:
# - name: Checkout code
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# # Install Nix and use that to run our tests so our environment matches exactly.
# - uses: cachix/install-nix-action@96951a368ba55167b55f1c916f7d416bac6505fe # v31.10.3
# with:
# nix_path: nixpkgs=channel:nixos-unstable
# - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
# with:
# name: ghostty
# authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
# - name: Test ReleaseFast build of libghostty-vt
# run: |
# nix build .#libghostty-vt-releasefast
# nix build .#libghostty-vt-releasefast.tests.sanity-check
# nix build .#libghostty-vt-releasefast.tests.pkg-config
# nix build .#libghostty-vt-releasefast.tests.build-with-shared
# nix build .#libghostty-vt-releasefast.tests.build-with-static
# nix build .#libghostty-vt-releasefast.tests.build-example-c-vt-build-info
# - name: Test ReleaseFast (no SIMD) build of libghostty-vt
# run: |
# nix build .#libghostty-vt-releasefast-no-simd
# nix build .#libghostty-vt-releasefast-no-simd.tests.sanity-check
# nix build .#libghostty-vt-releasefast-no-simd.tests.pkg-config
# nix build .#libghostty-vt-releasefast-no-simd.tests.build-with-shared
# nix build .#libghostty-vt-releasefast-no-simd.tests.build-with-static
# nix build .#libghostty-vt-releasefast-no-simd.tests.build-example-c-vt-build-info
# - name: Test Debug build of libghostty-vt
# run: |
# nix build .#libghostty-vt-debug
# nix build .#libghostty-vt-debug.tests.sanity-check
# nix build .#libghostty-vt-debug.tests.pkg-config
# nix build .#libghostty-vt-debug.tests.build-with-shared
# nix build .#libghostty-vt-debug.tests.build-with-static
# nix build .#libghostty-vt-debug.tests.build-example-c-vt-build-info
# - name: Test Debug (no SIMD) build of libghostty-vt
# run: |
# nix build .#libghostty-vt-debug-no-simd
# nix build .#libghostty-vt-debug-no-simd.tests.sanity-check
# nix build .#libghostty-vt-debug-no-simd.tests.pkg-config
# nix build .#libghostty-vt-debug-no-simd.tests.build-with-shared
# nix build .#libghostty-vt-debug-no-simd.tests.build-with-static
# nix build .#libghostty-vt-debug-no-simd.tests.build-example-c-vt-build-info
build-macos:
runs-on: namespace-profile-ghostty-macos-tahoe
needs: test

View File

@@ -26,3 +26,6 @@ website/.next
# fuzz corpus files
test/fuzz-libghostty/corpus/
test/fuzz-libghostty/afl-out/
# Swift example build outputs
example/swift-vt-xcframework/.build/

View File

@@ -20,6 +20,10 @@ A file for [guiding coding agents](https://agents.md/).
- Build: `zig build -Demit-lib-vt`
- Build WASM: `zig build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall`
- Test: `zig build test-lib-vt -Dtest-filter=<filter>`
- Prefer this when the change is in a libghostty-vt file
- All C enums in `include/ghostty/vt/` must have a `_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE`
sentinel as the last entry to force int enum sizing (pre-C23 portability).
## Directory Structure

View File

@@ -149,7 +149,7 @@ C-compatible library for embedding a fast, feature-rich terminal emulator
in any 3rd party project. This library is called `libghostty`.
Due to the scope of this project, we're breaking libghostty down into
separate actually libraries, starting with `libghostty-vt`. The goal of
separate libraries, starting with `libghostty-vt`. The goal of
this project is to focus on parsing terminal sequences and maintaining
terminal state. This is covered in more detail in this
[blog post](https://mitchellh.com/writing/libghostty-is-coming).

View File

@@ -7,7 +7,7 @@ const buildpkg = @import("src/build/main.zig");
const app_zon_version = @import("build.zig.zon").version;
/// Libghostty version. We use a separate version from the app.
const lib_version = "0.1.0";
const lib_version = "0.1.0-dev";
/// Minimum required zig version.
const minimum_zig_version = @import("build.zig.zon").minimum_zig_version;
@@ -37,6 +37,7 @@ pub fn build(b: *std.Build) !void {
const config = try buildpkg.Config.init(
b,
file_version orelse app_zon_version,
lib_version,
);
const test_filters = b.option(
[][]const u8,
@@ -151,6 +152,20 @@ pub fn build(b: *std.Build) !void {
).step);
}
// libghostty-vt xcframework (Apple only, universal binary).
// Only when building on macOS (not cross-compiling) since
// xcodebuild is required.
if (builtin.os.tag.isDarwin() and config.target.result.os.tag.isDarwin()) {
const apple_libs = try buildpkg.GhosttyLibVt.initStaticAppleUniversal(
b,
&config,
&deps,
&mod,
);
const xcframework = buildpkg.GhosttyLibVt.xcframework(&apple_libs, b);
b.getInstallStep().dependOn(xcframework.step);
}
// Helpgen
if (config.emit_helpgen) deps.help_strings.install();

1
example/.gitignore vendored
View File

@@ -3,3 +3,4 @@ dist/
node_modules/
example.wasm*
build/
.build/

View File

@@ -19,18 +19,25 @@ void query_build_info() {
size_t version_major = 0;
size_t version_minor = 0;
size_t version_patch = 0;
GhosttyString version_pre = {0};
GhosttyString version_build = {0};
ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_STRING, &version_string);
ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_MAJOR, &version_major);
ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_MINOR, &version_minor);
ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_PATCH, &version_patch);
ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_PRE, &version_pre);
ghostty_build_info(GHOSTTY_BUILD_INFO_VERSION_BUILD, &version_build);
printf("Version: %.*s\n", (int)version_string.len, version_string.ptr);
printf("Version major: %zu\n", version_major);
printf("Version minor: %zu\n", version_minor);
printf("Version patch: %zu\n", version_patch);
if (version_pre.len > 0) {
printf("Version pre : %.*s\n", (int)version_pre.len, version_pre.ptr);
} else {
printf("Version pre : (none)\n");
}
if (version_build.len > 0) {
printf("Version build: %.*s\n", (int)version_build.len, version_build.ptr);
} else {

View File

@@ -0,0 +1,18 @@
# Example: `ghostty-vt` Kitty Graphics Protocol
This contains a simple example of how to use the system interface
(`ghostty_sys_set`) to install a PNG decoder callback, then send
a Kitty Graphics Protocol image via `ghostty_terminal_vt_write`.
This uses a `build.zig` and `Zig` to build the C program so that we
can reuse a lot of our build logic and depend directly on our source
tree, but Ghostty emits a standard C library that can be used with any
C tooling.
## Usage
Run the program:
```shell-session
zig build run
```

View File

@@ -0,0 +1,42 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const exe_mod = b.createModule(.{
.target = target,
.optimize = optimize,
});
exe_mod.addCSourceFiles(.{
.root = b.path("src"),
.files = &.{"main.c"},
});
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
if (b.lazyDependency("ghostty", .{
// Setting simd to false will force a pure static build that
// doesn't even require libc, but it has a significant performance
// penalty. If your embedding app requires libc anyway, you should
// always keep simd enabled.
// .simd = false,
})) |dep| {
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}
// Exe
const exe = b.addExecutable(.{
.name = "c_vt_kitty_graphics",
.root_module = exe_mod,
});
b.installArtifact(exe);
// Run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}

View File

@@ -0,0 +1,24 @@
.{
.name = .c_vt_kitty_graphics,
.version = "0.0.0",
.fingerprint = 0x432d40ecc8f15589,
.minimum_zig_version = "0.15.1",
.dependencies = .{
// Ghostty dependency. In reality, you'd probably use a URL-based
// dependency like the one showed (and commented out) below this one.
// We use a path dependency here for simplicity and to ensure our
// examples always test against the source they're bundled with.
.ghostty = .{ .path = "../../" },
// Example of what a URL-based dependency looks like:
// .ghostty = .{
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
// },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@@ -0,0 +1,205 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
//! [kitty-graphics-decode-png]
/**
* Minimal PNG decoder callback for the sys interface.
*
* A real implementation would use a PNG library (libpng, stb_image, etc.)
* to decode the PNG data. This example uses a hardcoded 1x1 red pixel
* since we know exactly what image we're sending.
*
* WARNING: This is only an example for providing a callback, it DOES NOT
* actually decode the PNG it is passed. It hardcodes a response.
*/
bool decode_png(void* userdata,
const GhosttyAllocator* allocator,
const uint8_t* data,
size_t data_len,
GhosttySysImage* out) {
int* count = (int*)userdata;
(*count)++;
printf(" decode_png called (size=%zu, call #%d)\n", data_len, *count);
/* Allocate RGBA pixel data through the provided allocator. */
const size_t pixel_len = 4; /* 1x1 RGBA */
uint8_t* pixels = ghostty_alloc(allocator, pixel_len);
if (!pixels) return false;
/* Fill with red (R=255, G=0, B=0, A=255). */
pixels[0] = 255;
pixels[1] = 0;
pixels[2] = 0;
pixels[3] = 255;
out->width = 1;
out->height = 1;
out->data = pixels;
out->data_len = pixel_len;
return true;
}
//! [kitty-graphics-decode-png]
//! [kitty-graphics-write-pty]
/**
* write_pty callback to capture terminal responses.
*
* The Kitty graphics protocol sends an APC response back to the pty
* when an image is loaded (unless suppressed with q=2).
*/
void on_write_pty(GhosttyTerminal terminal,
void* userdata,
const uint8_t* data,
size_t len) {
(void)terminal;
(void)userdata;
printf(" response (%zu bytes): ", len);
fwrite(data, 1, len, stdout);
printf("\n");
}
//! [kitty-graphics-write-pty]
//! [kitty-graphics-main]
int main() {
/* Install the PNG decoder via the sys interface. */
int decode_count = 0;
ghostty_sys_set(GHOSTTY_SYS_OPT_USERDATA, &decode_count);
ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, (const void*)decode_png);
/* Create a terminal with Kitty graphics enabled. */
GhosttyTerminal terminal = NULL;
GhosttyTerminalOptions opts = {
.cols = 80,
.rows = 24,
.max_scrollback = 0,
};
if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) {
fprintf(stderr, "Failed to create terminal\n");
return 1;
}
/* Set cell pixel dimensions so kitty graphics can compute grid sizes. */
ghostty_terminal_resize(terminal, 80, 24, 8, 16);
/* Set a storage limit to enable Kitty graphics. */
uint64_t storage_limit = 64 * 1024 * 1024; /* 64 MiB */
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT,
&storage_limit);
/* Install write_pty to see the protocol response. */
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY,
(const void*)on_write_pty);
/*
* Send a Kitty graphics command with an inline 1x1 PNG image.
*
* The escape sequence is:
* ESC _G a=T,f=100,q=1; <base64 PNG data> ESC \
*
* Where:
* a=T — transmit and display
* f=100 — PNG format
* q=1 — request a response (q=0 would suppress it)
*/
printf("Sending Kitty graphics PNG image:\n");
const char* kitty_cmd =
"\x1b_Ga=T,f=100,q=1;"
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA"
"DUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="
"\x1b\\";
ghostty_terminal_vt_write(terminal, (const uint8_t*)kitty_cmd,
strlen(kitty_cmd));
printf("PNG decode calls: %d\n", decode_count);
/* Query the kitty graphics storage to verify the image was stored. */
GhosttyKittyGraphics graphics = NULL;
if (ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS,
&graphics) != GHOSTTY_SUCCESS || !graphics) {
fprintf(stderr, "Failed to get kitty graphics storage\n");
return 1;
}
printf("\nKitty graphics storage is available.\n");
/* Iterate placements to find the image ID. */
GhosttyKittyGraphicsPlacementIterator iter = NULL;
if (ghostty_kitty_graphics_placement_iterator_new(NULL, &iter) != GHOSTTY_SUCCESS) {
fprintf(stderr, "Failed to create placement iterator\n");
return 1;
}
if (ghostty_kitty_graphics_get(graphics,
GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, &iter) != GHOSTTY_SUCCESS) {
fprintf(stderr, "Failed to get placement iterator\n");
return 1;
}
int placement_count = 0;
while (ghostty_kitty_graphics_placement_next(iter)) {
placement_count++;
uint32_t image_id = 0;
uint32_t placement_id = 0;
bool is_virtual = false;
int32_t z = 0;
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id);
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID, &placement_id);
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL, &is_virtual);
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z, &z);
printf(" placement #%d: image_id=%u placement_id=%u virtual=%s z=%d\n",
placement_count, image_id, placement_id,
is_virtual ? "true" : "false", z);
/* Look up the image and print its properties. */
GhosttyKittyGraphicsImage image =
ghostty_kitty_graphics_image(graphics, image_id);
if (!image) {
fprintf(stderr, "Failed to look up image %u\n", image_id);
return 1;
}
uint32_t width = 0, height = 0, number = 0;
GhosttyKittyImageFormat format = 0;
size_t data_len = 0;
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_NUMBER, &number);
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &width);
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &height);
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &format);
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, &data_len);
printf(" image: number=%u size=%ux%u format=%d data_len=%zu\n",
number, width, height, format, data_len);
/* Compute the rendered pixel size and grid size. */
uint32_t px_w = 0, px_h = 0, cols = 0, rows = 0;
if (ghostty_kitty_graphics_placement_pixel_size(iter, image, terminal,
&px_w, &px_h) == GHOSTTY_SUCCESS) {
printf(" rendered pixel size: %ux%u\n", px_w, px_h);
}
if (ghostty_kitty_graphics_placement_grid_size(iter, image, terminal,
&cols, &rows) == GHOSTTY_SUCCESS) {
printf(" grid size: %u cols x %u rows\n", cols, rows);
}
}
printf("Total placements: %d\n", placement_count);
ghostty_kitty_graphics_placement_iterator_free(iter);
/* Clean up. */
ghostty_terminal_free(terminal);
/* Clear the sys callbacks. */
ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, NULL);
ghostty_sys_set(GHOSTTY_SYS_OPT_USERDATA, NULL);
return 0;
}
//! [kitty-graphics-main]

View File

@@ -0,0 +1,21 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "swift-vt-xcframework",
platforms: [.macOS(.v13)],
targets: [
.executableTarget(
name: "swift-vt-xcframework",
dependencies: ["GhosttyVt"],
path: "Sources",
linkerSettings: [
.linkedLibrary("c++"),
]
),
.binaryTarget(
name: "GhosttyVt",
path: "../../zig-out/lib/ghostty-vt.xcframework"
),
]
)

View File

@@ -0,0 +1,23 @@
# swift-vt-xcframework
Demonstrates consuming libghostty-vt from a Swift Package using the
pre-built XCFramework. Creates a terminal, writes VT sequences into it,
and formats the screen contents as plain text.
This example requires the XCFramework to be built first.
## Building
First, build the XCFramework from the repository root:
```shell-session
zig build -Demit-lib-vt
```
Then build and run the Swift package:
```shell-session
cd example/swift-vt-xcframework
swift build
swift run
```

View File

@@ -0,0 +1,47 @@
import Foundation
import GhosttyVt
// Create a terminal with a small grid
var terminal: GhosttyTerminal?
var opts = GhosttyTerminalOptions(
cols: 80,
rows: 24,
max_scrollback: 0
)
let result = ghostty_terminal_new(nil, &terminal, opts)
guard result == GHOSTTY_SUCCESS, let terminal else {
fatalError("Failed to create terminal")
}
// Write some VT-encoded content
let text = "Hello from \u{1b}[1mSwift\u{1b}[0m via xcframework!\r\n"
text.withCString { ptr in
ghostty_terminal_vt_write(terminal, ptr, strlen(ptr))
}
// Format the terminal contents as plain text
var fmtOpts = GhosttyFormatterTerminalOptions()
fmtOpts.size = MemoryLayout<GhosttyFormatterTerminalOptions>.size
fmtOpts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN
fmtOpts.trim = true
var formatter: GhosttyFormatter?
let fmtResult = ghostty_formatter_terminal_new(nil, &formatter, terminal, fmtOpts)
guard fmtResult == GHOSTTY_SUCCESS, let formatter else {
fatalError("Failed to create formatter")
}
var buf: UnsafeMutablePointer<UInt8>?
var len: Int = 0
let allocResult = ghostty_formatter_format_alloc(formatter, nil, &buf, &len)
guard allocResult == GHOSTTY_SUCCESS, let buf else {
fatalError("Failed to format")
}
print("Plain text (\(len) bytes):")
let data = Data(bytes: buf, count: len)
print(String(data: data, encoding: .utf8) ?? "<invalid UTF-8>")
ghostty_free(nil, buf, len)
ghostty_formatter_free(formatter)
ghostty_terminal_free(terminal)

View File

@@ -91,24 +91,36 @@
});
packages =
forAllPlatforms (pkgs: {
# Deps are needed for environmental setup on macOS
deps = pkgs.callPackage ./build.zig.zon.nix {};
})
// forBuildablePlatforms (pkgs: rec {
ghostty-debug = pkgs.callPackage ./nix/package.nix (mkPkgArgs "Debug");
ghostty-releasesafe = pkgs.callPackage ./nix/package.nix (mkPkgArgs "ReleaseSafe");
ghostty-releasefast = pkgs.callPackage ./nix/package.nix (mkPkgArgs "ReleaseFast");
builtins.foldl'
lib.recursiveUpdate
{}
[
(
forAllPlatforms (pkgs: rec {
# Deps are needed for environmental setup on macOS
deps = pkgs.callPackage ./build.zig.zon.nix {};
ghostty = ghostty-releasefast;
default = ghostty;
libghostty-vt-debug = pkgs.callPackage ./nix/libghostty-vt.nix (mkPkgArgs "Debug");
libghostty-vt-releasesafe = pkgs.callPackage ./nix/libghostty-vt.nix (mkPkgArgs "ReleaseSafe");
libghostty-vt-releasefast = pkgs.callPackage ./nix/libghostty-vt.nix (mkPkgArgs "ReleaseFast");
libghostty-vt-debug-no-simd = pkgs.callPackage ./nix/libghostty-vt.nix ((mkPkgArgs "Debug") // {simd = false;});
libghostty-vt-releasesafe-no-simd = pkgs.callPackage ./nix/libghostty-vt.nix ((mkPkgArgs "ReleaseSafe") // {simd = false;});
libghostty-vt-releasefast-no-simd = pkgs.callPackage ./nix/libghostty-vt.nix ((mkPkgArgs "ReleaseFast") // {simd = false;});
libghostty-vt-debug = pkgs.callPackage ./nix/libghostty-vt.nix (mkPkgArgs "Debug");
libghostty-vt-releasesafe = pkgs.callPackage ./nix/libghostty-vt.nix (mkPkgArgs "ReleaseSafe");
libghostty-vt-releasefast = pkgs.callPackage ./nix/libghostty-vt.nix (mkPkgArgs "ReleaseFast");
libghostty-vt = libghostty-vt-releasefast;
})
)
(
forBuildablePlatforms (pkgs: rec {
ghostty-debug = pkgs.callPackage ./nix/package.nix (mkPkgArgs "Debug");
ghostty-releasesafe = pkgs.callPackage ./nix/package.nix (mkPkgArgs "ReleaseSafe");
ghostty-releasefast = pkgs.callPackage ./nix/package.nix (mkPkgArgs "ReleaseFast");
libghostty-vt = libghostty-vt-releasefast;
});
ghostty = ghostty-releasefast;
default = ghostty;
})
)
];
formatter = forAllPlatforms (pkgs: pkgs.alejandra);

View File

@@ -98,6 +98,11 @@
* grid refs to inspect cell codepoints, row wrap state, and cell styles.
*/
/** @example c-vt-kitty-graphics/src/main.c
* This example demonstrates how to use the system interface to install a
* PNG decoder callback and send a Kitty Graphics Protocol image.
*/
#ifndef GHOSTTY_VT_H
#define GHOSTTY_VT_H
@@ -118,11 +123,14 @@ extern "C" {
#include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/style.h>
#include <ghostty/vt/sys.h>
#include <ghostty/vt/key.h>
#include <ghostty/vt/kitty_graphics.h>
#include <ghostty/vt/modes.h>
#include <ghostty/vt/mouse.h>
#include <ghostty/vt/paste.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/selection.h>
#include <ghostty/vt/size_report.h>
#include <ghostty/vt/wasm.h>

View File

@@ -35,11 +35,12 @@ extern "C" {
/**
* Build optimization mode.
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_OPTIMIZE_DEBUG = 0,
GHOSTTY_OPTIMIZE_RELEASE_SAFE = 1,
GHOSTTY_OPTIMIZE_RELEASE_SMALL = 2,
GHOSTTY_OPTIMIZE_RELEASE_FAST = 3,
GHOSTTY_OPTIMIZE_MODE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyOptimizeMode;
/**
@@ -47,7 +48,7 @@ typedef enum {
*
* Each variant documents the expected output pointer type.
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_BUILD_INFO_INVALID = 0,
@@ -107,13 +108,22 @@ typedef enum {
*/
GHOSTTY_BUILD_INFO_VERSION_PATCH = 8,
/**
* The pre metadata string (e.g. "alpha", "beta", "dev"). Has zero length if
* no pre metadata is present.
*
* Output type: GhosttyString *
*/
GHOSTTY_BUILD_INFO_VERSION_PRE = 9,
/**
* The build metadata string (e.g. commit hash). Has zero length if
* no build metadata is present.
*
* Output type: GhosttyString *
*/
GHOSTTY_BUILD_INFO_VERSION_BUILD = 9,
GHOSTTY_BUILD_INFO_VERSION_BUILD = 10,
GHOSTTY_BUILD_INFO_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyBuildInfo;
/**

View File

@@ -71,9 +71,10 @@ extern "C" {
*
* @ingroup terminal
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_COLOR_SCHEME_LIGHT = 0,
GHOSTTY_COLOR_SCHEME_DARK = 1,
GHOSTTY_COLOR_SCHEME_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyColorScheme;
/**

View File

@@ -35,11 +35,12 @@ extern "C" {
/**
* Focus event types for focus reporting mode (mode 1004).
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Terminal window gained focus */
GHOSTTY_FOCUS_GAINED = 0,
/** Terminal window lost focus */
GHOSTTY_FOCUS_LOST = 1,
GHOSTTY_FOCUS_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyFocusEvent;
/**

View File

@@ -11,6 +11,7 @@
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/selection.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/terminal.h>
@@ -36,7 +37,7 @@ extern "C" {
*
* @ingroup formatter
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Plain text (no escape sequences). */
GHOSTTY_FORMATTER_FORMAT_PLAIN,
@@ -45,6 +46,7 @@ typedef enum {
/** HTML with inline styles. */
GHOSTTY_FORMATTER_FORMAT_HTML,
GHOSTTY_FORMATTER_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyFormatterFormat;
/**
@@ -106,13 +108,6 @@ typedef struct {
GhosttyFormatterScreenExtra screen;
} GhosttyFormatterTerminalExtra;
/**
* Opaque handle to a formatter instance.
*
* @ingroup formatter
*/
typedef struct GhosttyFormatterImpl* GhosttyFormatter;
/**
* Options for creating a terminal formatter.
*
@@ -133,6 +128,10 @@ typedef struct {
/** Extra terminal state to include in styled output. */
GhosttyFormatterTerminalExtra extra;
/** Optional selection to restrict output to a range.
* If NULL, the entire screen is formatted. */
const GhosttySelection *selection;
} GhosttyFormatterTerminalOptions;
/**

View File

@@ -109,6 +109,32 @@ GHOSTTY_API GhosttyResult ghostty_grid_ref_graphemes(const GhosttyGridRef *ref,
size_t buf_len,
size_t *out_len);
/**
* Get the hyperlink URI for the cell at the grid reference's position.
*
* Writes the URI bytes into the provided buffer. If the cell has no
* hyperlink, out_len is set to 0 and GHOSTTY_SUCCESS is returned.
*
* If the buffer is too small (or NULL), the function returns
* GHOSTTY_OUT_OF_SPACE and writes the required number of bytes to
* out_len. The caller can then retry with a sufficiently sized buffer.
*
* @param ref Pointer to the grid reference
* @param buf Output buffer for the URI bytes (may be NULL)
* @param buf_len Size of the output buffer in bytes
* @param[out] out_len On success, the number of bytes written. On
* GHOSTTY_OUT_OF_SPACE, the required buffer size in bytes.
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
* node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small
*
* @ingroup grid_ref
*/
GHOSTTY_API GhosttyResult ghostty_grid_ref_hyperlink_uri(
const GhosttyGridRef *ref,
uint8_t *buf,
size_t buf_len,
size_t *out_len);
/**
* Get the style of the cell at the grid reference's position.
*

View File

@@ -64,7 +64,7 @@ typedef uint8_t GhosttyKittyKeyFlags;
*
* @ingroup key
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Option key is not treated as alt */
GHOSTTY_OPTION_AS_ALT_FALSE = 0,
/** Option key is treated as alt */
@@ -73,6 +73,7 @@ typedef enum {
GHOSTTY_OPTION_AS_ALT_LEFT = 2,
/** Only right option key is treated as alt */
GHOSTTY_OPTION_AS_ALT_RIGHT = 3,
GHOSTTY_OPTION_AS_ALT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyOptionAsAlt;
/**
@@ -83,7 +84,7 @@ typedef enum {
*
* @ingroup key
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Terminal DEC mode 1: cursor key application mode (value: bool) */
GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0,
@@ -104,6 +105,7 @@ typedef enum {
/** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */
GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6,
GHOSTTY_KEY_ENCODER_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKeyEncoderOption;
/**

View File

@@ -28,13 +28,14 @@ typedef struct GhosttyKeyEventImpl *GhosttyKeyEvent;
*
* @ingroup key
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Key was released */
GHOSTTY_KEY_ACTION_RELEASE = 0,
/** Key was pressed */
GHOSTTY_KEY_ACTION_PRESS = 1,
/** Key is being repeated (held down) */
GHOSTTY_KEY_ACTION_REPEAT = 2,
GHOSTTY_KEY_ACTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKeyAction;
/**
@@ -103,7 +104,7 @@ typedef uint16_t GhosttyMods;
*
* @ingroup key
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_KEY_UNIDENTIFIED = 0,
// Writing System Keys (W3C § 3.1.1)
@@ -296,6 +297,7 @@ typedef enum {
GHOSTTY_KEY_COPY,
GHOSTTY_KEY_CUT,
GHOSTTY_KEY_PASTE,
GHOSTTY_KEY_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKey;
/**

View File

@@ -0,0 +1,636 @@
/**
* @file kitty_graphics.h
*
* Kitty graphics protocol
*
* See @ref kitty_graphics for a full usage guide.
*/
#ifndef GHOSTTY_VT_KITTY_GRAPHICS_H
#define GHOSTTY_VT_KITTY_GRAPHICS_H
#include <stdbool.h>
#include <stdint.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/selection.h>
#include <ghostty/vt/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup kitty_graphics Kitty Graphics
*
* API for inspecting images and placements stored via the
* [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/).
*
* The central object is @ref GhosttyKittyGraphics, an opaque handle to
* the image storage associated with a terminal's active screen. From it
* you can iterate over placements and look up individual images.
*
* ## Obtaining a KittyGraphics Handle
*
* A @ref GhosttyKittyGraphics handle is obtained from a terminal via
* ghostty_terminal_get() with @ref GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS.
* The handle is borrowed from the terminal and remains valid until the
* next mutating terminal call (e.g. ghostty_terminal_vt_write() or
* ghostty_terminal_reset()).
*
* Before images can be stored, Kitty graphics must be enabled on the
* terminal by setting a non-zero storage limit with
* @ref GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT, and a PNG
* decoder callback must be installed via ghostty_sys_set() with
* @ref GHOSTTY_SYS_OPT_DECODE_PNG.
*
* @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-decode-png
*
* ## Iterating Placements
*
* Placements are inspected through a @ref GhosttyKittyGraphicsPlacementIterator.
* The typical workflow is:
*
* 1. Create an iterator with ghostty_kitty_graphics_placement_iterator_new().
* 2. Populate it from the storage with ghostty_kitty_graphics_get() using
* @ref GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR.
* 3. Optionally filter by z-layer with
* ghostty_kitty_graphics_placement_iterator_set().
* 4. Advance with ghostty_kitty_graphics_placement_next() and read
* per-placement data with ghostty_kitty_graphics_placement_get().
* 5. For each placement, look up its image with
* ghostty_kitty_graphics_image() to access pixel data and dimensions.
* 6. Free the iterator with ghostty_kitty_graphics_placement_iterator_free().
*
* ## Looking Up Images
*
* Given an image ID (obtained from a placement via
* @ref GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID), call
* ghostty_kitty_graphics_image() to get a @ref GhosttyKittyGraphicsImage
* handle. From this handle, ghostty_kitty_graphics_image_get() provides
* the image dimensions, pixel format, compression, and a borrowed pointer
* to the raw pixel data.
*
* ## Rendering Helpers
*
* Several functions assist with rendering a placement:
*
* - ghostty_kitty_graphics_placement_pixel_size() — rendered pixel
* dimensions accounting for source rect and aspect ratio.
* - ghostty_kitty_graphics_placement_grid_size() — number of grid
* columns and rows the placement occupies.
* - ghostty_kitty_graphics_placement_viewport_pos() — viewport-relative
* grid position (may be negative for partially scrolled placements).
* - ghostty_kitty_graphics_placement_source_rect() — resolved source
* rectangle in pixels, clamped to image bounds.
* - ghostty_kitty_graphics_placement_rect() — bounding rectangle as a
* @ref GhosttySelection.
*
* ## Lifetime and Thread Safety
*
* All handles borrowed from the terminal (GhosttyKittyGraphics,
* GhosttyKittyGraphicsImage) are invalidated by any mutating terminal
* call. The placement iterator is independently owned and must be freed
* by the caller, but the data it yields is only valid while the
* underlying terminal is not mutated.
*
* ## Example
*
* The following example creates a terminal, sends a Kitty graphics
* image, then iterates placements and prints image metadata:
*
* @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-main
*
* @{
*/
/**
* Queryable data kinds for ghostty_kitty_graphics_get().
*
* @ingroup kitty_graphics
*/
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid / sentinel value. */
GHOSTTY_KITTY_GRAPHICS_DATA_INVALID = 0,
/**
* Populate a pre-allocated placement iterator with placement data from
* the storage. Iterator data is only valid as long as the underlying
* terminal is not mutated.
*
* Output type: GhosttyKittyGraphicsPlacementIterator *
*/
GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR = 1,
GHOSTTY_KITTY_GRAPHICS_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKittyGraphicsData;
/**
* Queryable data kinds for ghostty_kitty_graphics_placement_get().
*
* @ingroup kitty_graphics
*/
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid / sentinel value. */
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INVALID = 0,
/**
* The image ID this placement belongs to.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID = 1,
/**
* The placement ID.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID = 2,
/**
* Whether this is a virtual placement (unicode placeholder).
*
* Output type: bool *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL = 3,
/**
* Pixel offset from the left edge of the cell.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET = 4,
/**
* Pixel offset from the top edge of the cell.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET = 5,
/**
* Source rectangle x origin in pixels.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X = 6,
/**
* Source rectangle y origin in pixels.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y = 7,
/**
* Source rectangle width in pixels (0 = full image width).
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH = 8,
/**
* Source rectangle height in pixels (0 = full image height).
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT = 9,
/**
* Number of columns this placement occupies.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS = 10,
/**
* Number of rows this placement occupies.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS = 11,
/**
* Z-index for this placement.
*
* Output type: int32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKittyGraphicsPlacementData;
/**
* Z-layer classification for kitty graphics placements.
*
* Based on the kitty protocol z-index conventions:
* - BELOW_BG: z < INT32_MIN/2 (drawn below cell background)
* - BELOW_TEXT: INT32_MIN/2 <= z < 0 (above background, below text)
* - ABOVE_TEXT: z >= 0 (above text)
* - ALL: no filtering (current behavior)
*
* @ingroup kitty_graphics
*/
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_KITTY_PLACEMENT_LAYER_ALL = 0,
GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_BG = 1,
GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_TEXT = 2,
GHOSTTY_KITTY_PLACEMENT_LAYER_ABOVE_TEXT = 3,
GHOSTTY_KITTY_PLACEMENT_LAYER_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKittyPlacementLayer;
/**
* Settable options for ghostty_kitty_graphics_placement_iterator_set().
*
* @ingroup kitty_graphics
*/
typedef enum GHOSTTY_ENUM_TYPED {
/**
* Set the z-layer filter for the iterator.
*
* Input type: GhosttyKittyPlacementLayer *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER = 0,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKittyGraphicsPlacementIteratorOption;
/**
* Pixel format of a Kitty graphics image.
*
* @ingroup kitty_graphics
*/
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_KITTY_IMAGE_FORMAT_RGB = 0,
GHOSTTY_KITTY_IMAGE_FORMAT_RGBA = 1,
GHOSTTY_KITTY_IMAGE_FORMAT_PNG = 2,
GHOSTTY_KITTY_IMAGE_FORMAT_GRAY_ALPHA = 3,
GHOSTTY_KITTY_IMAGE_FORMAT_GRAY = 4,
GHOSTTY_KITTY_IMAGE_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKittyImageFormat;
/**
* Compression of a Kitty graphics image.
*
* @ingroup kitty_graphics
*/
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE = 0,
GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE = 1,
GHOSTTY_KITTY_IMAGE_COMPRESSION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKittyImageCompression;
/**
* Queryable data kinds for ghostty_kitty_graphics_image_get().
*
* @ingroup kitty_graphics
*/
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid / sentinel value. */
GHOSTTY_KITTY_IMAGE_DATA_INVALID = 0,
/**
* The image ID.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_ID = 1,
/**
* The image number.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_NUMBER = 2,
/**
* Image width in pixels.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_WIDTH = 3,
/**
* Image height in pixels.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_HEIGHT = 4,
/**
* Pixel format of the image.
*
* Output type: GhosttyKittyImageFormat *
*/
GHOSTTY_KITTY_IMAGE_DATA_FORMAT = 5,
/**
* Compression of the image.
*
* Output type: GhosttyKittyImageCompression *
*/
GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION = 6,
/**
* Borrowed pointer to the raw pixel data. Valid as long as the
* underlying terminal is not mutated.
*
* Output type: const uint8_t **
*/
GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR = 7,
/**
* Length of the raw pixel data in bytes.
*
* Output type: size_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN = 8,
GHOSTTY_KITTY_IMAGE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyKittyGraphicsImageData;
/**
* Get data from a kitty graphics storage instance.
*
* The output pointer must be of the appropriate type for the requested
* data kind.
*
* Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
*
* @param graphics The kitty graphics handle
* @param data The type of data to extract
* @param[out] out Pointer to store the extracted data
* @return GHOSTTY_SUCCESS on success
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_get(
GhosttyKittyGraphics graphics,
GhosttyKittyGraphicsData data,
void* out);
/**
* Look up a Kitty graphics image by its image ID.
*
* Returns NULL if no image with the given ID exists or if Kitty graphics
* are disabled at build time.
*
* @param graphics The kitty graphics handle
* @param image_id The image ID to look up
* @return An opaque image handle, or NULL if not found
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyKittyGraphicsImage ghostty_kitty_graphics_image(
GhosttyKittyGraphics graphics,
uint32_t image_id);
/**
* Get data from a Kitty graphics image.
*
* The output pointer must be of the appropriate type for the requested
* data kind.
*
* @param image The image handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param data The data kind to query
* @param[out] out Pointer to receive the queried value
* @return GHOSTTY_SUCCESS on success
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_image_get(
GhosttyKittyGraphicsImage image,
GhosttyKittyGraphicsImageData data,
void* out);
/**
* Create a new placement iterator instance.
*
* All fields except the allocator are left undefined until populated
* via ghostty_kitty_graphics_get() with
* GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param[out] out_iterator On success, receives the created iterator handle
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
* failure
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_new(
const GhosttyAllocator* allocator,
GhosttyKittyGraphicsPlacementIterator* out_iterator);
/**
* Free a placement iterator.
*
* @param iterator The iterator handle to free (may be NULL)
*
* @ingroup kitty_graphics
*/
GHOSTTY_API void ghostty_kitty_graphics_placement_iterator_free(
GhosttyKittyGraphicsPlacementIterator iterator);
/**
* Set an option on a placement iterator.
*
* Use GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER with a
* GhosttyKittyPlacementLayer value to filter placements by z-layer.
* The filter is applied during iteration: ghostty_kitty_graphics_placement_next()
* will skip placements that do not match the configured layer.
*
* The default layer is GHOSTTY_KITTY_PLACEMENT_LAYER_ALL (no filtering).
*
* @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param option The option to set
* @param value Pointer to the value (type depends on option; NULL returns
* GHOSTTY_INVALID_VALUE)
* @return GHOSTTY_SUCCESS on success
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_set(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsPlacementIteratorOption option,
const void* value);
/**
* Advance the placement iterator to the next placement.
*
* If a layer filter has been set via
* ghostty_kitty_graphics_placement_iterator_set(), only placements
* matching that layer are returned.
*
* @param iterator The iterator handle (may be NULL)
* @return true if advanced to the next placement, false if at the end
*
* @ingroup kitty_graphics
*/
GHOSTTY_API bool ghostty_kitty_graphics_placement_next(
GhosttyKittyGraphicsPlacementIterator iterator);
/**
* Get data from the current placement in a placement iterator.
*
* Call ghostty_kitty_graphics_placement_next() at least once before
* calling this function.
*
* @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param data The data kind to query
* @param[out] out Pointer to receive the queried value
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
* iterator is NULL or not positioned on a placement
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_get(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsPlacementData data,
void* out);
/**
* Compute the grid rectangle occupied by the current placement.
*
* Uses the placement's pin, the image dimensions, and the terminal's
* cell/pixel geometry to calculate the bounding rectangle. Virtual
* placements (unicode placeholders) return GHOSTTY_NO_VALUE.
*
* @param terminal The terminal handle
* @param image The image handle for this placement's image
* @param iterator The placement iterator positioned on a placement
* @param[out] out_selection On success, receives the bounding rectangle
* as a selection with rectangle=true
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE for
* virtual placements or when Kitty graphics are disabled
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_rect(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
GhosttySelection* out_selection);
/**
* Compute the rendered pixel size of the current placement.
*
* Takes into account the placement's source rectangle, specified
* columns/rows, and aspect ratio to calculate the final rendered
* pixel dimensions.
*
* @param iterator The placement iterator positioned on a placement
* @param image The image handle for this placement's image
* @param terminal The terminal handle
* @param[out] out_width On success, receives the width in pixels
* @param[out] out_height On success, receives the height in pixels
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when
* Kitty graphics are disabled
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_pixel_size(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
uint32_t* out_width,
uint32_t* out_height);
/**
* Compute the grid cell size of the current placement.
*
* Returns the number of columns and rows that the placement occupies
* in the terminal grid. If the placement specifies explicit columns
* and rows, those are returned directly; otherwise they are calculated
* from the pixel size and cell dimensions.
*
* @param iterator The placement iterator positioned on a placement
* @param image The image handle for this placement's image
* @param terminal The terminal handle
* @param[out] out_cols On success, receives the number of columns
* @param[out] out_rows On success, receives the number of rows
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when
* Kitty graphics are disabled
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_grid_size(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
uint32_t* out_cols,
uint32_t* out_rows);
/**
* Get the viewport-relative grid position of the current placement.
*
* Converts the placement's internal pin to viewport-relative column and
* row coordinates. The returned coordinates represent the top-left
* corner of the placement in the viewport's grid coordinate space.
*
* The row value can be negative when the placement's origin has
* scrolled above the top of the viewport. For example, a 4-row
* image that has scrolled up by 2 rows returns row=-2, meaning
* its top 2 rows are above the visible area but its bottom 2 rows
* are still on screen. Embedders should use these coordinates
* directly when computing the destination rectangle for rendering;
* the embedder is responsible for clipping the portion of the image
* that falls outside the viewport.
*
* Returns GHOSTTY_SUCCESS for any placement that is at least
* partially visible in the viewport. Returns GHOSTTY_NO_VALUE when
* the placement is completely outside the viewport (its bottom edge
* is above the viewport or its top edge is at or below the last
* viewport row), or when the placement is a virtual (unicode
* placeholder) placement.
*
* @param iterator The placement iterator positioned on a placement
* @param image The image handle for this placement's image
* @param terminal The terminal handle
* @param[out] out_col On success, receives the viewport-relative column
* @param[out] out_row On success, receives the viewport-relative row
* (may be negative for partially visible placements)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if fully
* off-screen or virtual, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_viewport_pos(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
int32_t* out_col,
int32_t* out_row);
/**
* Get the resolved source rectangle for the current placement.
*
* Applies kitty protocol semantics: a width or height of 0 in the
* placement means "use the full image dimension", and the resulting
* rectangle is clamped to the actual image bounds. The returned
* values are in pixels and are ready to use for texture sampling.
*
* @param iterator The placement iterator positioned on a placement
* @param image The image handle for this placement's image
* @param[out] out_x Source rect x origin in pixels
* @param[out] out_y Source rect y origin in pixels
* @param[out] out_width Source rect width in pixels
* @param[out] out_height Source rect height in pixels
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any
* handle is NULL or the iterator is not positioned
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_source_rect(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
uint32_t* out_x,
uint32_t* out_y,
uint32_t* out_width,
uint32_t* out_height);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_KITTY_GRAPHICS_H */

View File

@@ -146,7 +146,7 @@ static inline bool ghostty_mode_ansi(GhosttyMode mode) {
* These correspond to the Ps2 parameter in a DECRPM response
* sequence (CSI ? Ps1 ; Ps2 $ y).
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Mode is not recognized */
GHOSTTY_MODE_REPORT_NOT_RECOGNIZED = 0,
/** Mode is set (enabled) */
@@ -157,6 +157,7 @@ typedef enum {
GHOSTTY_MODE_REPORT_PERMANENTLY_SET = 3,
/** Mode is permanently reset */
GHOSTTY_MODE_REPORT_PERMANENTLY_RESET = 4,
GHOSTTY_MODE_REPORT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyModeReportState;
/**

View File

@@ -30,7 +30,7 @@ typedef struct GhosttyMouseEncoderImpl *GhosttyMouseEncoder;
*
* @ingroup mouse
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Mouse reporting disabled. */
GHOSTTY_MOUSE_TRACKING_NONE = 0,
@@ -45,6 +45,7 @@ typedef enum {
/** Any-event tracking mode. */
GHOSTTY_MOUSE_TRACKING_ANY = 4,
GHOSTTY_MOUSE_TRACKING_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyMouseTrackingMode;
/**
@@ -52,12 +53,13 @@ typedef enum {
*
* @ingroup mouse
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_MOUSE_FORMAT_X10 = 0,
GHOSTTY_MOUSE_FORMAT_UTF8 = 1,
GHOSTTY_MOUSE_FORMAT_SGR = 2,
GHOSTTY_MOUSE_FORMAT_URXVT = 3,
GHOSTTY_MOUSE_FORMAT_SGR_PIXELS = 4,
GHOSTTY_MOUSE_FORMAT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyMouseFormat;
/**
@@ -105,7 +107,7 @@ typedef struct {
*
* @ingroup mouse
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Mouse tracking mode (value: GhosttyMouseTrackingMode). */
GHOSTTY_MOUSE_ENCODER_OPT_EVENT = 0,
@@ -120,6 +122,7 @@ typedef enum {
/** Whether to enable motion deduplication by last cell (value: bool). */
GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL = 4,
GHOSTTY_MOUSE_ENCODER_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyMouseEncoderOption;
/**

View File

@@ -27,7 +27,7 @@ typedef struct GhosttyMouseEventImpl *GhosttyMouseEvent;
*
* @ingroup mouse
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Mouse button was pressed. */
GHOSTTY_MOUSE_ACTION_PRESS = 0,
@@ -36,6 +36,7 @@ typedef enum {
/** Mouse moved. */
GHOSTTY_MOUSE_ACTION_MOTION = 2,
GHOSTTY_MOUSE_ACTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyMouseAction;
/**
@@ -43,7 +44,7 @@ typedef enum {
*
* @ingroup mouse
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_MOUSE_BUTTON_UNKNOWN = 0,
GHOSTTY_MOUSE_BUTTON_LEFT = 1,
GHOSTTY_MOUSE_BUTTON_RIGHT = 2,
@@ -56,6 +57,7 @@ typedef enum {
GHOSTTY_MOUSE_BUTTON_NINE = 9,
GHOSTTY_MOUSE_BUTTON_TEN = 10,
GHOSTTY_MOUSE_BUTTON_ELEVEN = 11,
GHOSTTY_MOUSE_BUTTON_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyMouseButton;
/**

View File

@@ -13,26 +13,6 @@
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
/**
* Opaque handle to an OSC parser instance.
*
* This handle represents an OSC (Operating System Command) parser that can
* be used to parse the contents of OSC sequences.
*
* @ingroup osc
*/
typedef struct GhosttyOscParserImpl *GhosttyOscParser;
/**
* Opaque handle to a single OSC command.
*
* This handle represents a parsed OSC (Operating System Command) command.
* The command can be queried for its type and associated data.
*
* @ingroup osc
*/
typedef struct GhosttyOscCommandImpl *GhosttyOscCommand;
/** @defgroup osc OSC Parser
*
* OSC (Operating System Command) sequence parser and command handling.
@@ -59,7 +39,7 @@ typedef struct GhosttyOscCommandImpl *GhosttyOscCommand;
*
* @ingroup osc
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_OSC_COMMAND_INVALID = 0,
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
@@ -83,6 +63,7 @@ typedef enum {
GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20,
GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21,
GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22,
GHOSTTY_OSC_COMMAND_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyOscCommandType;
/**
@@ -93,7 +74,7 @@ typedef enum {
*
* @ingroup osc
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_OSC_DATA_INVALID = 0,
@@ -108,6 +89,7 @@ typedef enum {
* the same parser instance. Memory is owned by the parser.
*/
GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1,
GHOSTTY_OSC_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyOscCommandData;
/**

View File

@@ -42,7 +42,7 @@ typedef struct {
*
* @ingroup point
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Active area where the cursor can move. */
GHOSTTY_POINT_TAG_ACTIVE = 0,
@@ -54,7 +54,8 @@ typedef enum {
/** Scrollback history only (before active area). */
GHOSTTY_POINT_TAG_HISTORY = 3,
} GhosttyPointTag;
GHOSTTY_POINT_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyPointTag;
/**
* Point value union.

View File

@@ -81,33 +81,12 @@ extern "C" {
* @{
*/
/**
* Opaque handle to a render state instance.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateImpl* GhosttyRenderState;
/**
* Opaque handle to a render-state row iterator.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator;
/**
* Opaque handle to render-state row cells.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells;
/**
* Dirty state of a render state after update.
*
* @ingroup render
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Not dirty at all; rendering can be skipped. */
GHOSTTY_RENDER_STATE_DIRTY_FALSE = 0,
@@ -116,6 +95,7 @@ typedef enum {
/** Global state changed; renderer should redraw everything. */
GHOSTTY_RENDER_STATE_DIRTY_FULL = 2,
GHOSTTY_RENDER_STATE_DIRTY_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRenderStateDirty;
/**
@@ -123,7 +103,7 @@ typedef enum {
*
* @ingroup render
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Bar cursor (DECSCUSR 5, 6). */
GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR = 0,
@@ -135,6 +115,7 @@ typedef enum {
/** Hollow block cursor. */
GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW = 3,
GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRenderStateCursorVisualStyle;
/**
@@ -142,7 +123,7 @@ typedef enum {
*
* @ingroup render
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid / sentinel value. */
GHOSTTY_RENDER_STATE_DATA_INVALID = 0,
@@ -206,6 +187,7 @@ typedef enum {
/** Whether the cursor is on the tail of a wide character (bool).
* Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL = 17,
GHOSTTY_RENDER_STATE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRenderStateData;
/**
@@ -213,9 +195,10 @@ typedef enum {
*
* @ingroup render
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Set dirty state (GhosttyRenderStateDirty). */
GHOSTTY_RENDER_STATE_OPTION_DIRTY = 0,
GHOSTTY_RENDER_STATE_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRenderStateOption;
/**
@@ -223,7 +206,7 @@ typedef enum {
*
* @ingroup render
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid / sentinel value. */
GHOSTTY_RENDER_STATE_ROW_DATA_INVALID = 0,
@@ -238,6 +221,7 @@ typedef enum {
* valid as long as the underlying render state is not updated.
* It is unsafe to use cell data after updating the render state. */
GHOSTTY_RENDER_STATE_ROW_DATA_CELLS = 3,
GHOSTTY_RENDER_STATE_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRenderStateRowData;
/**
@@ -245,9 +229,10 @@ typedef enum {
*
* @ingroup render
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Set dirty state for the current row (bool). */
GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY = 0,
GHOSTTY_RENDER_STATE_ROW_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRenderStateRowOption;
/**
@@ -496,7 +481,7 @@ GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_new(
*
* @ingroup render
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid / sentinel value. */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_INVALID = 0,
@@ -530,6 +515,7 @@ typedef enum {
* color, in which case the caller should use whatever default foreground
* color it wants (e.g. the terminal foreground). */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR = 6,
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRenderStateRowCellsData;
/**

View File

@@ -57,7 +57,7 @@ typedef uint64_t GhosttyRow;
*
* @ingroup screen
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** A single codepoint (may be zero for empty). */
GHOSTTY_CELL_CONTENT_CODEPOINT = 0,
@@ -69,6 +69,7 @@ typedef enum {
/** No text; background color as RGB. */
GHOSTTY_CELL_CONTENT_BG_COLOR_RGB = 3,
GHOSTTY_CELL_CONTENT_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyCellContentTag;
/**
@@ -78,7 +79,7 @@ typedef enum {
*
* @ingroup screen
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Not a wide character, cell width 1. */
GHOSTTY_CELL_WIDE_NARROW = 0,
@@ -90,6 +91,7 @@ typedef enum {
/** Spacer at end of soft-wrapped line for a wide character. */
GHOSTTY_CELL_WIDE_SPACER_HEAD = 3,
GHOSTTY_CELL_WIDE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyCellWide;
/**
@@ -100,7 +102,7 @@ typedef enum {
*
* @ingroup screen
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Regular output content, such as command output. */
GHOSTTY_CELL_SEMANTIC_OUTPUT = 0,
@@ -109,6 +111,7 @@ typedef enum {
/** Content that is part of a shell prompt. */
GHOSTTY_CELL_SEMANTIC_PROMPT = 2,
GHOSTTY_CELL_SEMANTIC_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyCellSemanticContent;
/**
@@ -119,7 +122,7 @@ typedef enum {
*
* @ingroup screen
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_CELL_DATA_INVALID = 0,
@@ -201,6 +204,7 @@ typedef enum {
* Output type: GhosttyColorRgb *
*/
GHOSTTY_CELL_DATA_COLOR_RGB = 11,
GHOSTTY_CELL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyCellData;
/**
@@ -211,7 +215,7 @@ typedef enum {
*
* @ingroup screen
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** No prompt cells in this row. */
GHOSTTY_ROW_SEMANTIC_NONE = 0,
@@ -220,6 +224,7 @@ typedef enum {
/** Prompt cells exist and this is a continuation line. */
GHOSTTY_ROW_SEMANTIC_PROMPT_CONTINUATION = 2,
GHOSTTY_ROW_SEMANTIC_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRowSemanticPrompt;
/**
@@ -230,7 +235,7 @@ typedef enum {
*
* @ingroup screen
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_ROW_DATA_INVALID = 0,
@@ -289,6 +294,7 @@ typedef enum {
* Output type: bool *
*/
GHOSTTY_ROW_DATA_DIRTY = 8,
GHOSTTY_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyRowData;
/**

View File

@@ -0,0 +1,53 @@
/**
* @file selection.h
*
* Selection range type for specifying a region of terminal content.
*/
#ifndef GHOSTTY_VT_SELECTION_H
#define GHOSTTY_VT_SELECTION_H
#include <stdbool.h>
#include <stddef.h>
#include <ghostty/vt/grid_ref.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup selection Selection
*
* A selection range defined by two grid references that identifies a
* contiguous or rectangular region of terminal content.
*
* @{
*/
/**
* A selection range defined by two grid references.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
*
* @ingroup selection
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttySelection). */
size_t size;
/** Start of the selection range (inclusive). */
GhosttyGridRef start;
/** End of the selection range (inclusive). */
GhosttyGridRef end;
/** Whether the selection is rectangular (block) rather than linear. */
bool rectangle;
} GhosttySelection;
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_SELECTION_H */

View File

@@ -47,16 +47,6 @@
extern "C" {
#endif
/**
* Opaque handle to an SGR parser instance.
*
* This handle represents an SGR (Select Graphic Rendition) parser that can
* be used to parse SGR sequences and extract individual text attributes.
*
* @ingroup sgr
*/
typedef struct GhosttySgrParserImpl* GhosttySgrParser;
/**
* SGR attribute tags.
*
@@ -65,7 +55,7 @@ typedef struct GhosttySgrParserImpl* GhosttySgrParser;
*
* @ingroup sgr
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_SGR_ATTR_UNSET = 0,
GHOSTTY_SGR_ATTR_UNKNOWN = 1,
GHOSTTY_SGR_ATTR_BOLD = 2,
@@ -97,6 +87,7 @@ typedef enum {
GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 28,
GHOSTTY_SGR_ATTR_BG_256 = 29,
GHOSTTY_SGR_ATTR_FG_256 = 30,
GHOSTTY_SGR_ATTR_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySgrAttributeTag;
/**
@@ -104,13 +95,14 @@ typedef enum {
*
* @ingroup sgr
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_SGR_UNDERLINE_NONE = 0,
GHOSTTY_SGR_UNDERLINE_SINGLE = 1,
GHOSTTY_SGR_UNDERLINE_DOUBLE = 2,
GHOSTTY_SGR_UNDERLINE_CURLY = 3,
GHOSTTY_SGR_UNDERLINE_DOTTED = 4,
GHOSTTY_SGR_UNDERLINE_DASHED = 5,
GHOSTTY_SGR_UNDERLINE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySgrUnderline;
/**

View File

@@ -40,7 +40,7 @@ extern "C" {
*
* Determines the output format for the terminal size report.
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** In-band size report (mode 2048): ESC [ 48 ; rows ; cols ; height ; width t */
GHOSTTY_SIZE_REPORT_MODE_2048 = 0,
/** XTWINOPS text area size in pixels: ESC [ 4 ; height ; width t */
@@ -49,6 +49,7 @@ typedef enum {
GHOSTTY_SIZE_REPORT_CSI_16_T = 2,
/** XTWINOPS text area size in characters: ESC [ 8 ; rows ; cols t */
GHOSTTY_SIZE_REPORT_CSI_18_T = 3,
GHOSTTY_SIZE_REPORT_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySizeReportStyle;
/**

View File

@@ -46,11 +46,12 @@ typedef uint16_t GhosttyStyleId;
*
* @ingroup style
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_STYLE_COLOR_NONE = 0,
GHOSTTY_STYLE_COLOR_PALETTE = 1,
GHOSTTY_STYLE_COLOR_RGB = 2,
} GhosttyStyleColorTag;
GHOSTTY_STYLE_COLOR_TAG_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyStyleColorTag;
/**
* Style color value union.

134
include/ghostty/vt/sys.h Normal file
View File

@@ -0,0 +1,134 @@
/**
* @file sys.h
*
* System interface - runtime-swappable implementations for external dependencies.
*/
#ifndef GHOSTTY_VT_SYS_H
#define GHOSTTY_VT_SYS_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
/** @defgroup sys System Interface
*
* Runtime-swappable function pointers for operations that depend on
* external implementations (e.g. image decoding).
*
* These are process-global settings that must be configured at startup
* before any terminal functionality that depends on them is used.
* Setting these enables various optional features of the terminal. For
* example, setting a PNG decoder enables PNG image support in the Kitty
* Graphics Protocol.
*
* Use ghostty_sys_set() with a `GhosttySysOption` to install or clear
* an implementation. Passing NULL as the value clears the implementation
* and disables the corresponding feature.
*
* ## Example
*
* ### Defining a PNG decode callback
* @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-decode-png
*
* ### Installing the callback and sending a PNG image
* @snippet c-vt-kitty-graphics/src/main.c kitty-graphics-main
*
* @{
*/
#ifdef __cplusplus
extern "C" {
#endif
/**
* Result of decoding an image.
*
* The `data` buffer must be allocated through the allocator provided to
* the decode callback. The library takes ownership and will free it
* with the same allocator.
*/
typedef struct {
/** Image width in pixels. */
uint32_t width;
/** Image height in pixels. */
uint32_t height;
/** Pointer to the decoded RGBA pixel data. */
uint8_t* data;
/** Length of the pixel data in bytes. */
size_t data_len;
} GhosttySysImage;
/**
* Callback type for PNG decoding.
*
* Decodes raw PNG data into RGBA pixels. The output pixel data must be
* allocated through the provided allocator. The library takes ownership
* of the buffer and will free it with the same allocator.
*
* @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA
* @param allocator The allocator to use for the output pixel buffer
* @param data Pointer to the raw PNG data
* @param data_len Length of the raw PNG data in bytes
* @param[out] out On success, filled with the decoded image
* @return true on success, false on failure
*/
typedef bool (*GhosttySysDecodePngFn)(
void* userdata,
const GhosttyAllocator* allocator,
const uint8_t* data,
size_t data_len,
GhosttySysImage* out);
/**
* System option identifiers for ghostty_sys_set().
*/
typedef enum GHOSTTY_ENUM_TYPED {
/**
* Set the userdata pointer passed to all sys callbacks.
*
* Input type: void* (or NULL)
*/
GHOSTTY_SYS_OPT_USERDATA = 0,
/**
* Set the PNG decode function.
*
* When set, the terminal can accept PNG images via the Kitty
* Graphics Protocol. When cleared (NULL value), PNG decoding is
* unsupported and PNG image data will be rejected.
*
* Input type: GhosttySysDecodePngFn (function pointer, or NULL)
*/
GHOSTTY_SYS_OPT_DECODE_PNG = 1,
GHOSTTY_SYS_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySysOption;
/**
* Set a system-level option.
*
* Configures a process-global implementation function. These should be
* set once at startup before using any terminal functionality that
* depends on them.
*
* @param option The option to set
* @param value Pointer to the value (type depends on the option),
* or NULL to clear it
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
* option is not recognized
*/
GHOSTTY_API GhosttyResult ghostty_sys_set(GhosttySysOption option,
const void* value);
#ifdef __cplusplus
}
#endif
/** @} */
#endif /* GHOSTTY_VT_SYS_H */

View File

@@ -16,6 +16,7 @@
#include <ghostty/vt/modes.h>
#include <ghostty/vt/size_report.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/kitty_graphics.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/point.h>
#include <ghostty/vt/style.h>
@@ -154,13 +155,6 @@ extern "C" {
* @{
*/
/**
* Opaque handle to a terminal instance.
*
* @ingroup terminal
*/
typedef struct GhosttyTerminalImpl* GhosttyTerminal;
/**
* Terminal initialization options.
*
@@ -186,7 +180,7 @@ typedef struct {
*
* @ingroup terminal
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Scroll to the top of the scrollback. */
GHOSTTY_SCROLL_VIEWPORT_TOP,
@@ -195,6 +189,7 @@ typedef enum {
/** Scroll by a delta amount (up is negative). */
GHOSTTY_SCROLL_VIEWPORT_DELTA,
GHOSTTY_SCROLL_VIEWPORT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyTerminalScrollViewportTag;
/**
@@ -227,12 +222,13 @@ typedef struct {
*
* @ingroup terminal
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** The primary (normal) screen. */
GHOSTTY_TERMINAL_SCREEN_PRIMARY = 0,
/** The alternate screen. */
GHOSTTY_TERMINAL_SCREEN_ALTERNATE = 1,
GHOSTTY_TERMINAL_SCREEN_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyTerminalScreen;
/**
@@ -400,7 +396,7 @@ typedef GhosttyString (*GhosttyTerminalXtversionFn)(GhosttyTerminal terminal,
*
* @ingroup terminal
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/**
* Opaque userdata pointer passed to all callbacks.
*
@@ -534,6 +530,50 @@ typedef enum {
* Input type: GhosttyColorRgb[256]*
*/
GHOSTTY_TERMINAL_OPT_COLOR_PALETTE = 14,
/**
* Set the Kitty image storage limit in bytes.
*
* Applied to all initialized screens (primary and alternate).
* A value of zero disables the Kitty graphics protocol entirely,
* deleting all stored images and placements. A NULL value pointer
* is equivalent to zero (disables). Has no effect when Kitty graphics
* are disabled at build time.
*
* Input type: uint64_t*
*/
GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT = 15,
/**
* Enable or disable Kitty image loading via the file medium.
*
* A NULL value pointer is a no-op. Has no effect when Kitty graphics
* are disabled at build time.
*
* Input type: bool*
*/
GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16,
/**
* Enable or disable Kitty image loading via the temporary file medium.
*
* A NULL value pointer is a no-op. Has no effect when Kitty graphics
* are disabled at build time.
*
* Input type: bool*
*/
GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE = 17,
/**
* Enable or disable Kitty image loading via the shared memory medium.
*
* A NULL value pointer is a no-op. Has no effect when Kitty graphics
* are disabled at build time.
*
* Input type: bool*
*/
GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM = 18,
GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyTerminalOption;
/**
@@ -544,7 +584,7 @@ typedef enum {
*
* @ingroup terminal
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_TERMINAL_DATA_INVALID = 0,
@@ -756,6 +796,60 @@ typedef enum {
* Output type: GhosttyColorRgb[256] *
*/
GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT = 25,
/**
* The Kitty image storage limit in bytes for the active screen.
*
* A value of zero means the Kitty graphics protocol is disabled.
* Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
*
* Output type: uint64_t *
*/
GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_STORAGE_LIMIT = 26,
/**
* Whether the file medium is enabled for Kitty image loading on the
* active screen.
*
* Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
*
* Output type: bool *
*/
GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE = 27,
/**
* Whether the temporary file medium is enabled for Kitty image loading
* on the active screen.
*
* Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
*
* Output type: bool *
*/
GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE = 28,
/**
* Whether the shared memory medium is enabled for Kitty image loading
* on the active screen.
*
* Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
*
* Output type: bool *
*/
GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM = 29,
/**
* The Kitty graphics image storage for the active screen.
*
* Returns a borrowed pointer to the image storage. The pointer is valid
* until the next mutating terminal call (e.g. ghostty_terminal_vt_write()
* or ghostty_terminal_reset()).
*
* Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
*
* Output type: GhosttyKittyGraphics *
*/
GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS = 30,
GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyTerminalData;
/**
@@ -974,6 +1068,39 @@ GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal,
GhosttyPoint point,
GhosttyGridRef *out_ref);
/**
* Convert a grid reference back to a point in the given coordinate system.
*
* This is the inverse of ghostty_terminal_grid_ref(): given a grid reference,
* it returns the x/y coordinates in the requested coordinate system (active,
* viewport, screen, or history).
*
* The grid reference must have been obtained from the same terminal instance.
* Like all grid references, it is only valid until the next mutating terminal
* call.
*
* Not every grid reference is representable in every coordinate system. For
* example, a cell in scrollback history cannot be expressed in active
* coordinates, and a cell that has scrolled off the visible area cannot be
* expressed in viewport coordinates. In these cases, the function returns
* GHOSTTY_NO_VALUE.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param ref Pointer to the grid reference to convert
* @param tag The target coordinate system
* @param[out] out On success, set to the coordinate in the requested system (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
* or ref is NULL/invalid, GHOSTTY_NO_VALUE if the ref falls outside
* the requested coordinate system
*
* @ingroup terminal
*/
GHOSTTY_API GhosttyResult ghostty_terminal_point_from_grid_ref(
GhosttyTerminal terminal,
const GhosttyGridRef *ref,
GhosttyPointTag tag,
GhosttyPointCoordinate *out);
/** @} */
#ifdef __cplusplus

View File

@@ -7,6 +7,7 @@
#ifndef GHOSTTY_VT_TYPES_H
#define GHOSTTY_VT_TYPES_H
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
@@ -32,10 +33,45 @@
#endif
#endif
/**
* Enum int-sizing helpers.
*
* The Zig side backs all C enums with c_int, so the C declarations
* must use int as their underlying type to maintain ABI compatibility.
*
* C23 (detected via __STDC_VERSION__ >= 202311L) supports explicit
* enum underlying types with `enum : int { ... }`. For pre-C23
* compilers, which are free to choose any type that can represent
* all values (C11 §6.7.2.2), we add an INT_MAX sentinel as the last
* entry to force the compiler to use int.
*
* INT_MAX is used rather than a fixed constant like 0xFFFFFFFF
* because enum constants must have type int (which is signed).
* Values above INT_MAX overflow signed int and are a constraint
* violation in standard C; compilers that accept them interpret them
* as negative values via two's complement, which can collide with
* legitimate negative enum values.
*
* Usage:
* @code
* typedef enum GHOSTTY_ENUM_TYPED {
* FOO_A = 0,
* FOO_B = 1,
* FOO_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
* } Foo;
* @endcode
*/
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
#define GHOSTTY_ENUM_TYPED : int
#else
#define GHOSTTY_ENUM_TYPED
#endif
#define GHOSTTY_ENUM_MAX_VALUE INT_MAX
/**
* Result codes for libghostty-vt operations.
*/
typedef enum {
typedef enum GHOSTTY_ENUM_TYPED {
/** Operation completed successfully */
GHOSTTY_SUCCESS = 0,
/** Operation failed due to failed allocation */
@@ -46,8 +82,108 @@ typedef enum {
GHOSTTY_OUT_OF_SPACE = -3,
/** The requested value has no value */
GHOSTTY_NO_VALUE = -4,
GHOSTTY_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyResult;
/* ---- Opaque handles ---- */
/**
* Opaque handle to a terminal instance.
*
* @ingroup terminal
*/
typedef struct GhosttyTerminalImpl* GhosttyTerminal;
/**
* Opaque handle to a Kitty graphics image storage.
*
* Obtained via ghostty_terminal_get() with
* GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS. The pointer is borrowed from
* the terminal and remains valid until the next mutating terminal call
* (e.g. ghostty_terminal_vt_write() or ghostty_terminal_reset()).
*
* @ingroup kitty_graphics
*/
typedef struct GhosttyKittyGraphicsImpl* GhosttyKittyGraphics;
/**
* Opaque handle to a Kitty graphics image.
*
* Obtained via ghostty_kitty_graphics_image() with an image ID. The
* pointer is borrowed from the storage and remains valid until the next
* mutating terminal call.
*
* @ingroup kitty_graphics
*/
typedef const struct GhosttyKittyGraphicsImageImpl* GhosttyKittyGraphicsImage;
/**
* Opaque handle to a Kitty graphics placement iterator.
*
* @ingroup kitty_graphics
*/
typedef struct GhosttyKittyGraphicsPlacementIteratorImpl* GhosttyKittyGraphicsPlacementIterator;
/**
* Opaque handle to a render state instance.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateImpl* GhosttyRenderState;
/**
* Opaque handle to a render-state row iterator.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator;
/**
* Opaque handle to render-state row cells.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells;
/**
* Opaque handle to an SGR parser instance.
*
* This handle represents an SGR (Select Graphic Rendition) parser that can
* be used to parse SGR sequences and extract individual text attributes.
*
* @ingroup sgr
*/
typedef struct GhosttySgrParserImpl* GhosttySgrParser;
/**
* Opaque handle to a formatter instance.
*
* @ingroup formatter
*/
typedef struct GhosttyFormatterImpl* GhosttyFormatter;
/**
* Opaque handle to an OSC parser instance.
*
* This handle represents an OSC (Operating System Command) parser that can
* be used to parse the contents of OSC sequences.
*
* @ingroup osc
*/
typedef struct GhosttyOscParserImpl* GhosttyOscParser;
/**
* Opaque handle to a single OSC command.
*
* This handle represents a parsed OSC (Operating System Command) command.
* The command can be queried for its type and associated data.
*
* @ingroup osc
*/
typedef struct GhosttyOscCommandImpl* GhosttyOscCommand;
/* ---- Common value types ---- */
/**
* A borrowed byte string (pointer + length).
*

View File

@@ -673,6 +673,22 @@ class AppDelegate: NSObject,
syncDockBadge()
}
private func requestBadgeAuthorizationAndSet(_ center: UNUserNotificationCenter) {
center.requestAuthorization(options: [.badge]) { granted, error in
if let error = error {
Self.logger.warning("Error requesting badge authorization: \(error)")
return
}
// Permission granted, set the badge
if granted {
DispatchQueue.main.async {
self.setDockBadge()
}
}
}
}
private func syncDockBadge() {
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
@@ -683,23 +699,16 @@ class AppDelegate: NSObject,
DispatchQueue.main.async {
self.setDockBadge()
}
} else if settings.badgeSetting == .notSupported {
// If badge setting is not supported, we may be in a sandbox that doesn't allow it.
// We can still attempt to set the badge and hope for the best, but we should also
// request authorization just in case it is a permissions issue.
self.requestBadgeAuthorizationAndSet(center)
}
case .notDetermined:
// Not determined yet, request authorization for badge
center.requestAuthorization(options: [.badge]) { granted, error in
if let error = error {
Self.logger.warning("Error requesting badge authorization: \(error)")
return
}
if granted {
// Permission granted, set the badge
DispatchQueue.main.async {
self.setDockBadge()
}
}
}
self.requestBadgeAuthorizationAndSet(center)
case .denied, .provisional, .ephemeral:
// In these known non-authorized states, do not attempt to set the badge.

View File

@@ -82,7 +82,7 @@ class DockTilePlugin: NSObject, NSDockTilePlugIn {
/// Reset the application icon and dock tile icon to the default.
private func resetIcon(dockTile: NSDockTile) {
let appBundlePath = self.ghosttyAppURL?.path
let appIcon: NSImage
let appIcon: NSImage?
if #available(macOS 26.0, *) {
// Reset to the default (glassy) icon.
if let appBundlePath {
@@ -93,21 +93,8 @@ class DockTilePlugin: NSObject, NSDockTilePlugIn {
// Use the `Blueprint` icon to distinguish Debug from Release builds.
appIcon = pluginBundle.image(forResource: "BlueprintImage")!
#else
// Get the composed icon from the app bundle.
if let appBundlePath,
let iconRep = NSWorkspace.shared.icon(forFile: appBundlePath)
.bestRepresentation(
for: CGRect(origin: .zero, size: dockTile.size),
context: nil,
hints: nil
) {
appIcon = NSImage(size: dockTile.size)
appIcon.addRepresentation(iconRep)
} else {
// If something unexpected happens on macOS 26,
// fall back to a bundled icon.
appIcon = pluginBundle.image(forResource: "AppIconImage")!
}
// Reset to Ghostty.icon
appIcon = nil
#endif
} else {
// Use the bundled icon to keep the corner radius consistent with pre-Tahoe apps.
@@ -126,9 +113,14 @@ class DockTilePlugin: NSObject, NSDockTilePlugIn {
}
private extension NSDockTile {
func setIcon(_ newIcon: NSImage) {
func setIcon(_ newIcon: NSImage?) {
// Update the Dock tile on the main thread.
DispatchQueue.main.async {
guard let newIcon else {
self.contentView = nil
self.display()
return
}
let iconView = NSImageView(frame: CGRect(origin: .zero, size: self.size))
iconView.wantsLayer = true
iconView.image = newIcon

View File

@@ -46,6 +46,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
/// changes in the list.
private var tabWindowsHash: Int = 0
/// The initial window presentation is deferred by one runloop turn in a few places so
/// AppKit can settle tab/window state first. Close actions must cancel it to avoid
/// re-showing a tab that was already closed.
private var pendingInitialPresentation: DispatchWorkItem?
/// This is set to false by init if the window managed by this controller should not be restorable.
/// For example, terminals executing custom scripts are not restorable.
private var restorable: Bool = true
@@ -140,6 +145,27 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
center.removeObserver(self)
}
private func cancelPendingInitialPresentation() {
pendingInitialPresentation?.cancel()
pendingInitialPresentation = nil
}
private func scheduleInitialPresentation(_ block: @escaping () -> Void) {
cancelPendingInitialPresentation()
var scheduledWorkItem: DispatchWorkItem?
scheduledWorkItem = DispatchWorkItem { [weak self] in
guard let self else { return }
defer { self.pendingInitialPresentation = nil }
guard scheduledWorkItem?.isCancelled == false else { return }
block()
}
let workItem = scheduledWorkItem!
pendingInitialPresentation = workItem
DispatchQueue.main.async(execute: workItem)
}
// MARK: Base Controller Overrides
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
@@ -257,7 +283,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// We're dispatching this async because otherwise the lastCascadePoint doesn't
// take effect. Our best theory is there is some next-event-loop-tick logic
// that Cocoa is doing that we need to be after.
DispatchQueue.main.async {
c.scheduleInitialPresentation {
c.showWindow(self)
// Only cascade if we aren't fullscreen.
@@ -319,7 +345,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// Calculate the target frame based on the tree's view bounds
let treeSize: CGSize? = tree.root?.viewBounds()
DispatchQueue.main.async {
c.scheduleInitialPresentation {
c.showWindow(self)
if let window = c.window {
// If we have a tree size, resize the window's content to match
@@ -434,7 +460,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// We're dispatching this async because otherwise the lastCascadePoint doesn't
// take effect. Our best theory is there is some next-event-loop-tick logic
// that Cocoa is doing that we need to be after.
DispatchQueue.main.async {
controller.scheduleInitialPresentation {
// Only cascade if we aren't fullscreen and are alone in the tab group.
if !window.styleMask.contains(.fullScreen) &&
window.tabGroup?.windows.count ?? 1 == 1 {
@@ -650,6 +676,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
return
}
cancelPendingInitialPresentation()
// Undo
if let undoManager, let undoState {
// Register undo action to restore the tab
@@ -768,6 +796,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
func closeWindowImmediately() {
guard let window = window else { return }
cancelPendingInitialPresentation()
registerUndoForCloseWindow()
if let tabGroup = window.tabGroup, tabGroup.windows.count > 1 {
@@ -776,6 +806,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// This prevents unnecessary undos registered since AppKit may
// process them on later ticks so we can't just disable undo registration.
if let controller = window.windowController as? TerminalController {
controller.cancelPendingInitialPresentation()
controller.surfaceTree = .init()
}
@@ -1142,6 +1173,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
override func windowWillClose(_ notification: Notification) {
super.windowWillClose(notification)
cancelPendingInitialPresentation()
self.relabelTabs()
// If we remove a window, we reset the cascade point to the key window so that

View File

@@ -621,8 +621,13 @@ extension Ghostty {
}
func updateOSView(_ scrollView: SurfaceScrollView, context: Context) {
// Nothing to do: SwiftUI automatically updates the frame size, and
// SurfaceScrollView handles the rest in response to that
// SwiftUI may defer frame updates under system load (e.g., memory
// pressure, heavy I/O) or when external window managers trigger rapid
// layout changes. When that happens, the scroll view's bounds can
// fall out of sync with the size reported by GeometryReader, causing
// the surface to render at stale dimensions.
guard scrollView.bounds.size != size else { return }
scrollView.needsLayout = true
}
#else
func makeOSView(context: Context) -> SurfaceView {

View File

@@ -1,16 +1,21 @@
{
lib,
stdenv,
callPackage,
git,
lib,
llvmPackages,
pkg-config,
runCommand,
stdenv,
testers,
versionCheckHook,
zig_0_15,
revision ? "dirty",
optimize ? "Debug",
simd ? true,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
version = "0.1.0-dev";
pname = "libghostty-vt";
version = "0.1.0-dev+${revision}-nix";
# We limit source like this to try and reduce the amount of rebuilds as possible
# thus we only provide the source that is needed for the build
@@ -21,10 +26,7 @@ stdenv.mkDerivation (finalAttrs: {
root = ../.;
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
lib.fileset.unions [
../dist/linux
../images
../include
../po
../pkg
../src
../vendor
@@ -35,7 +37,7 @@ stdenv.mkDerivation (finalAttrs: {
);
};
deps = callPackage ../build.zig.zon.nix {name = "ghostty-cache-${finalAttrs.version}";};
deps = callPackage ../build.zig.zon.nix {name = "${finalAttrs.pname}-cache-${finalAttrs.version}";};
nativeBuildInputs = [
git
@@ -45,17 +47,20 @@ stdenv.mkDerivation (finalAttrs: {
buildInputs = [];
doCheck = false;
dontSetZigDefaultFlags = true;
zigBuildFlags = [
"--system"
"${finalAttrs.deps}"
"-Dversion-string=${finalAttrs.version}-${revision}-nix"
"-Dlib-version-string=${finalAttrs.version}"
"-Dcpu=baseline"
"-Doptimize=${optimize}"
"-Dapp-runtime=none"
"-Demit-lib-vt=true"
"-Dsimd=${lib.boolToString simd}"
];
zigCheckFlags = finalAttrs.zigBuildFlags ++ ["test-lib-vt"];
outputs = [
"out"
@@ -69,17 +74,159 @@ stdenv.mkDerivation (finalAttrs: {
mv "$out/include" "$dev"
mv "$out/share" "$dev"
ln -sf "$out/lib/libghostty-vt.so.0" "$dev/lib/libghostty-vt.so"
ln -sf "$out/lib/libghostty-vt.so.${lib.versions.major finalAttrs.version}" "$dev/lib/libghostty-vt.so"
'';
postFixup = ''
substituteInPlace "$dev/share/pkgconfig/libghostty-vt.pc" \
--replace "$out" "$dev"
--replace-fail "$out" "$dev"
'';
passthru.tests = {
sanity-check = let
version = "${lib.versions.major finalAttrs.version}.${lib.versions.minor finalAttrs.version}.${lib.versions.patch finalAttrs.version}";
in
runCommand "sanity-check" {} (builtins.concatStringsSep "\n" [
''
${lib.getExe' stdenv.cc "nm"} "${finalAttrs.finalPackage}/lib/libghostty-vt.so.${version}" | grep -q 'T ghostty_terminal_new'
${lib.getExe' stdenv.cc "nm"} "${finalAttrs.finalPackage.dev}/lib/libghostty-vt.a" | grep -q 'T ghostty_terminal_new'
''
(
lib.optionalString simd
''
${lib.getExe' stdenv.cc "nm"} "${finalAttrs.finalPackage.dev}/lib/libghostty-vt.a" | grep -q 'T .*simdutf'
${lib.getExe' stdenv.cc "nm"} "${finalAttrs.finalPackage.dev}/lib/libghostty-vt.a" | grep -q 'T .*3hwy'
''
)
''
touch "$out"
''
]);
pkg-config = testers.hasPkgConfigModules {
package = finalAttrs.finalPackage.dev;
};
build-with-shared = stdenv.mkDerivation {
name = "build-with-shared";
src = ./test-src;
doInstallCheck = true;
nativeBuildInputs = [pkg-config];
buildInputs = [finalAttrs.finalPackage];
buildPhase = ''
runHook preBuildHooks
cc -o test test_libghostty_vt.c \
''$(pkg-config --cflags --libs libghostty-vt) \
-Wl,-rpath,"${finalAttrs.finalPackage}/lib"
runHook postBuildHooks
'';
installPhase = ''
runHook preInstallHooks
mkdir -p "$out/bin";
cp -a test "$out/bin/test";
runHook postInstallHooks
'';
installCheckPhase = ''
runHook preInstallCheckHooks
"$out/bin/test" | grep -q "SIMD: ${
if simd
then "yes"
else "no"
}"
ldd "$out/bin/test" 2>/dev/null | grep -q libghostty-vt
runHook postInstallCheckHooks
'';
meta = {
mainProgram = "test";
};
};
build-with-static = stdenv.mkDerivation {
name = "build-with-static";
src = ./test-src;
doInstallCheck = true;
nativeBuildInputs = [pkg-config];
buildInputs = [finalAttrs.finalPackage llvmPackages.libcxxClang];
buildPhase = ''
runHook preBuildHooks
cc -o test test_libghostty_vt.c \
''$(pkg-config --cflags libghostty-vt) \
${finalAttrs.finalPackage.dev}/lib/libghostty-vt.a \
''$(pkg-config --libs-only-l --static libghostty-vt | sed 's/-lghostty-vt//') \
-Wl,-rpath,"${finalAttrs.finalPackage}/lib"
runHook postBuildHooks
'';
installPhase = ''
runHook preInstallHooks
mkdir -p "$out/bin";
cp -a test "$out/bin/test";
runHook postInstallHooks
'';
installCheckPhase = ''
runHook preInstallCheckHooks
"$out/bin/test" | grep -q "SIMD: ${
if simd
then "yes"
else "no"
}"
! ldd "$out/bin/test" 2>/dev/null | grep -q libghostty-vt
runHook postInstallCheckHooks
'';
meta = {
mainProgram = "test";
};
};
build-example-c-vt-build-info = stdenv.mkDerivation {
name = "build-example-c-vt-build-info";
version = finalAttrs.version;
src = ../example/c-vt-build-info/src;
doInstallCheck = true;
nativeBuildInputs = [pkg-config];
nativeInstallCheckInputs = [versionCheckHook];
buildInputs = [finalAttrs.finalPackage];
buildPhase = ''
runHook preBuildHooks
cc -o test main.c \
''$(pkg-config --cflags --libs libghostty-vt) \
-Wl,-rpath,"${finalAttrs.finalPackage}/lib"
runHook postBuildHooks
'';
installPhase = ''
runHook preInstallHooks
mkdir -p "$out/bin";
cp -a test "$out/bin/test";
runHook postInstallHooks
'';
installCheckPhase = ''
runHook preInstallCheckHooks
ldd "$out/bin/test" 2>/dev/null | grep -q libghostty-vt
runHook postInstallCheckHooks
'';
meta = {
mainProgram = "test";
};
};
};
meta = {
homepage = "https://ghostty.org";
license = lib.licenses.mit;
platforms = zig_0_15.meta.platforms;
pkgConfigModules = ["libghostty-vt"];
};
})

View File

@@ -30,7 +30,7 @@
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
version = "1.3.2-dev";
version = "1.3.2-dev+${revision}-nix";
# We limit source like this to try and reduce the amount of rebuilds as possible
# thus we only provide the source that is needed for the build
@@ -86,7 +86,7 @@ in
zigBuildFlags = [
"--system"
"${finalAttrs.deps}"
"-Dversion-string=${finalAttrs.version}-${revision}-nix"
"-Dversion-string=${finalAttrs.version}"
"-Dgtk-x11=${lib.boolToString enableX11}"
"-Dgtk-wayland=${lib.boolToString enableWayland}"
"-Dcpu=baseline"

View File

@@ -0,0 +1,9 @@
#include <ghostty/vt.h>
#include <stdio.h>
int main(void) {
bool simd = false;
GhosttyResult r = ghostty_build_info(GHOSTTY_BUILD_INFO_SIMD, &simd);
if (r != GHOSTTY_SUCCESS) return 1;
printf("SIMD: %s\n", simd ? "yes" : "no");
return 0;
}

View File

@@ -38,6 +38,7 @@ wasm_shared: bool = true,
/// Ghostty exe properties
exe_entrypoint: ExeEntrypoint = .ghostty,
version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
lib_version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
/// Binary properties
pie: bool = false,
@@ -68,7 +69,7 @@ is_dep: bool = false,
/// Environmental properties
env: std.process.EnvMap,
pub fn init(b: *std.Build, appVersion: []const u8) !Config {
pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Config {
// Setup our standard Zig target and optimize options, i.e.
// `-Doptimize` and `-Dtarget`.
const optimize = b.standardOptimizeOption(.{});
@@ -294,6 +295,20 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
};
};
// libghostty-vt properties
const lib_version_string = b.option(
[]const u8,
"lib-version-string",
"A specific version string to use for the build of libghostty-vt. " ++
"If not specified, git will be used. This must be a semantic version.",
);
config.lib_version = if (lib_version_string) |v|
try std.SemanticVersion.parse(v)
else
try std.SemanticVersion.parse(libVersion);
//---------------------------------------------------------------
// Binary Properties
@@ -519,13 +534,20 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
// Our version. We also add the string version so we don't need
// to do any allocations at runtime. This has to be long enough to
// accommodate realistic large branch names for dev versions.
var buf: [1024]u8 = undefined;
var app_version_buf: [1024]u8 = undefined;
step.addOption(std.SemanticVersion, "app_version", self.version);
step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ(
&buf,
&app_version_buf,
"{f}",
.{self.version},
));
var lib_version_buf: [1024]u8 = undefined;
step.addOption(std.SemanticVersion, "lib_version", self.lib_version);
step.addOption([:0]const u8, "lib_version_string", try std.fmt.bufPrintZ(
&lib_version_buf,
"{f}",
.{self.lib_version},
));
step.addOption(
ReleaseChannel,
"release_channel",
@@ -539,13 +561,16 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
/// Returns the build options for the terminal module. This assumes a
/// Ghostty executable being built. Callers should modify this as needed.
pub fn terminalOptions(self: *const Config) TerminalBuildOptions {
pub fn terminalOptions(self: *const Config, artifact: TerminalBuildOptions.Artifact) TerminalBuildOptions {
return .{
.artifact = .ghostty,
.artifact = artifact,
.simd = self.simd,
.oniguruma = true,
.c_abi = false,
.version = self.version,
.version = switch (artifact) {
.ghostty => self.version,
.lib => self.lib_version,
},
.slow_runtime_safety = switch (self.optimize) {
.Debug => true,
.ReleaseSafe,

View File

@@ -4,9 +4,12 @@ const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const RunStep = std.Build.Step.Run;
const Config = @import("Config.zig");
const GhosttyZig = @import("GhosttyZig.zig");
const LibtoolStep = @import("LibtoolStep.zig");
const LipoStep = @import("LipoStep.zig");
const SharedDeps = @import("SharedDeps.zig");
const XCFrameworkStep = @import("XCFrameworkStep.zig");
/// The step that generates the file.
step: *std.Build.Step,
@@ -40,7 +43,7 @@ pub fn initWasm(
const exe = b.addExecutable(.{
.name = "ghostty-vt",
.root_module = zig.vt_c,
.version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 },
.version = zig.version,
});
// Allow exported symbols to actually be exported.
@@ -99,6 +102,93 @@ pub fn initShared(
return initLib(b, zig, .dynamic);
}
/// Apple platform targets for xcframework slices.
pub const ApplePlatform = enum {
macos_universal,
ios,
ios_simulator,
// tvOS, watchOS, and visionOS are not yet supported by Zig's
// standard library (missing PATH_MAX, mcontext fields, etc.).
/// Platforms that have device + simulator pairs, gated on SDK detection.
const sdk_platforms = [_]struct {
os_tag: std.Target.Os.Tag,
device: ApplePlatform,
simulator: ApplePlatform,
}{
.{ .os_tag = .ios, .device = .ios, .simulator = .ios_simulator },
};
};
/// Static libraries for each Apple platform, keyed by `ApplePlatform`.
pub const AppleLibs = std.EnumMap(ApplePlatform, GhosttyLibVt);
/// Build static libraries for all available Apple platforms.
/// Always builds a macOS universal (arm64 + x86_64) fat binary.
/// Additional platforms are included if their SDK is detected.
pub fn initStaticAppleUniversal(
b: *std.Build,
cfg: *const Config,
deps: *const SharedDeps,
zig: *const GhosttyZig,
) !AppleLibs {
var result: AppleLibs = .{};
// macOS universal (arm64 + x86_64)
const aarch64_zig = try zig.retarget(
b,
cfg,
deps,
Config.genericMacOSTarget(b, .aarch64),
);
const x86_64_zig = try zig.retarget(
b,
cfg,
deps,
Config.genericMacOSTarget(b, .x86_64),
);
const aarch64 = try initStatic(b, &aarch64_zig);
const x86_64 = try initStatic(b, &x86_64_zig);
const universal = LipoStep.create(b, .{
.name = "ghostty-vt",
.out_name = "libghostty-vt.a",
.input_a = aarch64.output,
.input_b = x86_64.output,
});
result.put(.macos_universal, .{
.step = universal.step,
.artifact = universal.step,
.kind = .static,
.output = universal.output,
.dsym = null,
.pkg_config = null,
});
// Additional Apple platforms, each gated on SDK availability.
for (ApplePlatform.sdk_platforms) |p| {
const target_query: std.Target.Query = .{
.cpu_arch = .aarch64,
.os_tag = p.os_tag,
.os_version_min = Config.osVersionMin(p.os_tag),
};
if (detectAppleSDK(b.resolveTargetQuery(target_query).result)) {
const dev_zig = try zig.retarget(b, cfg, deps, b.resolveTargetQuery(target_query));
result.put(p.device, try initStatic(b, &dev_zig));
const sim_zig = try zig.retarget(b, cfg, deps, b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = p.os_tag,
.os_version_min = Config.osVersionMin(p.os_tag),
.abi = .simulator,
.cpu_model = .{ .explicit = &std.Target.aarch64.cpu.apple_a17 },
}));
result.put(p.simulator, try initStatic(b, &sim_zig));
}
}
return result;
}
fn initLib(
b: *std.Build,
zig: *const GhosttyZig,
@@ -113,7 +203,7 @@ fn initLib(
.name = if (kind == .static) "ghostty-vt-static" else "ghostty-vt",
.linkage = linkage,
.root_module = zig.vt_c,
.version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 },
.version = zig.version,
});
lib.installHeadersDirectory(
b.path("include/ghostty"),
@@ -184,12 +274,12 @@ fn initLib(
\\Name: libghostty-vt
\\URL: https://github.com/ghostty-org/ghostty
\\Description: Ghostty VT library
\\Version: 0.1.0
\\Version: {f}
\\Cflags: -I${{includedir}}
\\Libs: -L${{libdir}} -lghostty-vt
\\Libs.private: {s}
\\Requires.private: {s}
, .{ b.install_prefix, libsPrivate(zig), requiresPrivate(b) }));
, .{ b.install_prefix, zig.version, libsPrivate(zig), requiresPrivate(b) }));
};
// For static libraries with vendored SIMD dependencies, combine
@@ -294,6 +384,59 @@ fn requiresPrivate(b: *std.Build) []const u8 {
return "";
}
/// Create an XCFramework bundle from Apple platform static libraries.
pub fn xcframework(
apple_libs: *const AppleLibs,
b: *std.Build,
) *XCFrameworkStep {
// Generate a headers directory with a module map for Swift PM.
// We can't use include/ directly because it contains a module map
// for GhosttyKit (the macOS app library).
const wf = b.addWriteFiles();
_ = wf.addCopyDirectory(
b.path("include/ghostty"),
"ghostty",
.{ .include_extensions = &.{".h"} },
);
_ = wf.add("module.modulemap",
\\module GhosttyVt {
\\ umbrella header "ghostty/vt.h"
\\ export *
\\}
\\
);
const headers = wf.getDirectory();
var libraries: [AppleLibs.len]XCFrameworkStep.Library = undefined;
var lib_count: usize = 0;
for (std.enums.values(ApplePlatform)) |platform| {
if (apple_libs.get(platform)) |lib| {
libraries[lib_count] = .{
.library = lib.output,
.headers = headers,
.dsym = null,
};
lib_count += 1;
}
}
return XCFrameworkStep.create(b, .{
.name = "ghostty-vt",
.out_path = b.pathJoin(&.{ b.install_prefix, "lib/ghostty-vt.xcframework" }),
.libraries = libraries[0..lib_count],
});
}
/// Returns true if the Apple SDK for the given target is installed.
fn detectAppleSDK(target: std.Target) bool {
_ = std.zig.LibCInstallation.findNative(.{
.allocator = std.heap.page_allocator,
.target = &target,
.verbose = false,
}) catch return false;
return true;
}
pub fn install(
self: *const GhosttyLibVt,
step: *std.Build.Step,

View File

@@ -11,6 +11,9 @@ const TerminalBuildOptions = @import("../terminal/build_options.zig").Options;
vt: *std.Build.Module,
vt_c: *std.Build.Module,
/// The libghostty-vt version
version: std.SemanticVersion,
/// Static library paths for vendored SIMD dependencies. Populated
/// only when the dependencies are built from source (not provided
/// by the system via -Dsystem-integration). Used to produce a
@@ -21,9 +24,47 @@ pub fn init(
b: *std.Build,
cfg: *const Config,
deps: *const SharedDeps,
) !GhosttyZig {
return initInner(b, cfg, deps, "ghostty-vt", "ghostty-vt-c");
}
/// Create a new GhosttyZig with modules retargeted to a different
/// architecture. Used to produce universal (fat) binaries on macOS.
pub fn retarget(
self: *const GhosttyZig,
b: *std.Build,
cfg: *const Config,
deps: *const SharedDeps,
target: std.Build.ResolvedTarget,
) !GhosttyZig {
_ = self;
const retargeted_config = try b.allocator.create(Config);
retargeted_config.* = cfg.*;
retargeted_config.target = target;
const retargeted_deps = try b.allocator.create(SharedDeps);
retargeted_deps.* = try deps.retarget(b, target);
// Use unique module names to avoid collisions with the original target.
const arch_name = @tagName(target.result.cpu.arch);
return initInner(
b,
retargeted_config,
retargeted_deps,
b.fmt("ghostty-vt-{s}", .{arch_name}),
b.fmt("ghostty-vt-c-{s}", .{arch_name}),
);
}
fn initInner(
b: *std.Build,
cfg: *const Config,
deps: *const SharedDeps,
vt_name: []const u8,
vt_c_name: []const u8,
) !GhosttyZig {
// Terminal module build options
var vt_options = cfg.terminalOptions();
var vt_options = cfg.terminalOptions(.lib);
vt_options.artifact = .lib;
// We presently don't allow Oniguruma in our Zig module at all.
// We should expose this as a build option in the future so we can
@@ -34,7 +75,7 @@ pub fn init(
return .{
.vt = try initVt(
"ghostty-vt",
vt_name,
b,
cfg,
deps,
@@ -43,7 +84,7 @@ pub fn init(
),
.vt_c = try initVt(
"ghostty-vt-c",
vt_c_name,
b,
cfg,
deps,
@@ -55,6 +96,8 @@ pub fn init(
&simd_libs,
),
.version = cfg.lib_version,
.simd_libs = simd_libs,
};
}

View File

@@ -133,7 +133,7 @@ pub fn add(
step.root_module.addOptions("build_options", self.options);
// Every exe needs the terminal options
self.config.terminalOptions().add(b, step.root_module);
self.config.terminalOptions(.ghostty).add(b, step.root_module);
// C imports for locale constants and functions
{

View File

@@ -19,6 +19,24 @@ const builtin = @import("builtin");
// or are too Ghostty-internal.
const terminal = @import("terminal/main.zig");
/// System interface for the terminal package.
///
/// This module provides runtime-swappable function pointers for operations
/// that depend on external implementations. Embedders can use this to
/// provide or override default behaviors. These must be set at startup
/// before any terminal functionality is used.
///
/// This lets libghostty-vt have no runtime dependencies on external
/// libraries, while still allowing rich functionality that may require
/// external libraries (e.g. image decoding or regular expresssions).
///
/// Setting these will enable various features of the terminal package.
/// For example, setting a PNG decoder will enable support for PNG images in
/// the Kitty Graphics Protocol.
///
/// Additional functionality will be added here over time as needed.
pub const sys = terminal.sys;
pub const apc = terminal.apc;
pub const dcs = terminal.dcs;
pub const osc = terminal.osc;
@@ -171,6 +189,7 @@ comptime {
@export(&c.size_report_encode, .{ .name = "ghostty_size_report_encode" });
@export(&c.style_default, .{ .name = "ghostty_style_default" });
@export(&c.style_is_default, .{ .name = "ghostty_style_is_default" });
@export(&c.sys_set, .{ .name = "ghostty_sys_set" });
@export(&c.cell_get, .{ .name = "ghostty_cell_get" });
@export(&c.row_get, .{ .name = "ghostty_row_get" });
@export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" });
@@ -214,9 +233,24 @@ comptime {
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
@export(&c.kitty_graphics_get, .{ .name = "ghostty_kitty_graphics_get" });
@export(&c.kitty_graphics_image, .{ .name = "ghostty_kitty_graphics_image" });
@export(&c.kitty_graphics_image_get, .{ .name = "ghostty_kitty_graphics_image_get" });
@export(&c.kitty_graphics_placement_iterator_new, .{ .name = "ghostty_kitty_graphics_placement_iterator_new" });
@export(&c.kitty_graphics_placement_iterator_free, .{ .name = "ghostty_kitty_graphics_placement_iterator_free" });
@export(&c.kitty_graphics_placement_iterator_set, .{ .name = "ghostty_kitty_graphics_placement_iterator_set" });
@export(&c.kitty_graphics_placement_next, .{ .name = "ghostty_kitty_graphics_placement_next" });
@export(&c.kitty_graphics_placement_get, .{ .name = "ghostty_kitty_graphics_placement_get" });
@export(&c.kitty_graphics_placement_rect, .{ .name = "ghostty_kitty_graphics_placement_rect" });
@export(&c.kitty_graphics_placement_pixel_size, .{ .name = "ghostty_kitty_graphics_placement_pixel_size" });
@export(&c.kitty_graphics_placement_grid_size, .{ .name = "ghostty_kitty_graphics_placement_grid_size" });
@export(&c.kitty_graphics_placement_viewport_pos, .{ .name = "ghostty_kitty_graphics_placement_viewport_pos" });
@export(&c.kitty_graphics_placement_source_rect, .{ .name = "ghostty_kitty_graphics_placement_source_rect" });
@export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" });
@export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" });
@export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" });
@export(&c.grid_ref_hyperlink_uri, .{ .name = "ghostty_grid_ref_hyperlink_uri" });
@export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" });
@export(&c.build_info, .{ .name = "ghostty_build_info" });
@export(&c.type_json, .{ .name = "ghostty_type_json" });

View File

@@ -426,7 +426,7 @@ pub const State = struct {
// Calculate the dimensions of our image, taking in to
// account the rows / columns specified by the placement.
const dest_size = p.calculatedSize(image.*, t);
const dest_size = p.pixelSize(image.*, t);
// Calculate the source rectangle
const source_x = @min(image.width, p.source_x);

View File

@@ -255,7 +255,16 @@ pub const Options = struct {
/// The total storage limit for Kitty images in bytes for this
/// screen. Kitty image storage is per-screen.
kitty_image_storage_limit: usize = 320 * 1000 * 1000, // 320MB
kitty_image_storage_limit: usize = switch (build_options.artifact) {
.ghostty => 320 * 1000 * 1000, // 320MB
.lib => 10 * 1000 * 1000, // 10MB
},
/// The limits for what medium types are allowed for Kitty image loading.
kitty_image_loading_limits: if (build_options.kitty_graphics)
kitty.graphics.LoadingImage.Limits
else
void = if (build_options.kitty_graphics) .direct else {},
/// A simple, default terminal. If you rely on specific dimensions or
/// scrollback (or lack of) then do not use this directly. This is just
@@ -313,6 +322,7 @@ pub fn init(
&result,
opts.kitty_image_storage_limit,
) catch unreachable;
result.kitty_images.image_limits = opts.kitty_image_loading_limits;
}
return result;

View File

@@ -191,6 +191,26 @@ pub const Options = struct {
/// The default mode state. When the terminal gets a reset, it
/// will revert back to this state.
default_modes: modespkg.ModePacked = .{},
/// The total storage limit for Kitty images in bytes. Has no effect
/// if kitty images are disabled at build-time.
kitty_image_storage_limit: usize = switch (build_options.artifact) {
.ghostty => 320 * 1000 * 1000, // 320MB
// libghostty we start with a much lower limit since this is an
// embedded library and we want to be more conservative with memory
// usage by default.
.lib => 10 * 1000 * 1000, // 10MB
},
/// The limits for what medium types are allowed for Kitty image loading.
/// Has no effect if kitty images are disabled otherwise. For example,
// if no `sys.decode_png` hook is specified, png formats are disabled
// no matter what.
kitty_image_loading_limits: if (build_options.kitty_graphics)
kitty.graphics.LoadingImage.Limits
else
void = if (build_options.kitty_graphics) .direct else {},
};
/// Initialize a new terminal.
@@ -205,6 +225,8 @@ pub fn init(
.cols = cols,
.rows = rows,
.max_scrollback = opts.max_scrollback,
.kitty_image_storage_limit = opts.kitty_image_storage_limit,
.kitty_image_loading_limits = opts.kitty_image_loading_limits,
});
errdefer screen_set.deinit(alloc);
@@ -2693,6 +2715,34 @@ pub fn kittyGraphics(
return kitty.graphics.execute(alloc, self, cmd);
}
/// Set the storage size limit for Kitty graphics across all screens.
pub fn setKittyGraphicsSizeLimit(
self: *Terminal,
alloc: Allocator,
limit: usize,
) !void {
if (comptime !build_options.kitty_graphics) return;
var it = self.screens.all.iterator();
while (it.next()) |entry| {
const screen: *Screen = entry.value.*;
try screen.kitty_images.setLimit(alloc, screen, limit);
}
}
/// Set the allowed medium types for Kitty graphics image loading
/// across all screens.
pub fn setKittyGraphicsLoadingLimits(
self: *Terminal,
limits: kitty.graphics.LoadingImage.Limits,
) void {
if (comptime !build_options.kitty_graphics) return;
var it = self.screens.all.iterator();
while (it.next()) |entry| {
const screen: *Screen = entry.value.*;
screen.kitty_images.image_limits = limits;
}
}
/// Set a style attribute.
pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
try self.screens.active.setAttribute(attr);
@@ -2941,12 +2991,15 @@ pub fn switchScreen(self: *Terminal, key: ScreenSet.Key) !?*Screen {
.alternate => 0,
},
// Inherit our Kitty image storage limit from the primary
// Inherit our Kitty image settings from the primary
// screen if we have to initialize.
.kitty_image_storage_limit = if (comptime build_options.kitty_graphics)
primary.kitty_images.total_limit
else
0,
.kitty_image_loading_limits = if (comptime build_options.kitty_graphics)
primary.kitty_images.image_limits
else {},
},
);
};

View File

@@ -10,7 +10,7 @@ const log = std.log.scoped(.terminal_apc);
/// The start/feed/end functions are meant to be called from the terminal.Stream
/// apcStart, apcPut, and apcEnd functions, respectively.
pub const Handler = struct {
state: State = .{ .inactive = {} },
state: State = .inactive,
pub fn deinit(self: *Handler) void {
self.state.deinit();
@@ -36,17 +36,17 @@ pub const Handler = struct {
'G' => self.state = if (comptime build_options.kitty_graphics)
.{ .kitty = kitty_gfx.CommandParser.init(alloc) }
else
.{ .ignore = {} },
.ignore,
// Unknown
else => self.state = .{ .ignore = {} },
else => self.state = .ignore,
}
},
.kitty => |*p| if (comptime build_options.kitty_graphics) {
p.feed(byte) catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
self.state = .{ .ignore = {} };
self.state = .ignore;
};
} else unreachable,
}
@@ -55,7 +55,7 @@ pub const Handler = struct {
pub fn end(self: *Handler) ?Command {
defer {
self.state.deinit();
self.state = .{ .inactive = {} };
self.state = .inactive;
}
return switch (self.state) {

View File

@@ -2,6 +2,14 @@ const std = @import("std");
/// Options set by Zig build.zig and exposed via `terminal_options`.
pub const Options = struct {
pub const Artifact = enum {
/// Ghostty application
ghostty,
/// libghostty-vt, Zig module
lib,
};
/// The target artifact to build. This will gate some functionality.
artifact: Artifact,
@@ -47,34 +55,40 @@ pub const Options = struct {
opts.addOption(bool, "simd", self.simd);
opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety);
// Kitty graphics is almost always true. This used to be conditional on
// some other factors but we've since generalized the implementation
// to support optional PNG decoding, OS capabilities like filesystems,
// etc. So its safe to always enable it and just have the
// implementation deal with unsupported features as needed.
//
// We disable it on wasm32-freestanding because we at the least
// require the ability to get timestamps and there is no way to
// do that with freestanding targets.
const target = m.resolved_target.?.result;
opts.addOption(
bool,
"kitty_graphics",
!(target.cpu.arch == .wasm32 and target.os.tag == .freestanding),
);
// These are synthesized based on other options.
opts.addOption(bool, "kitty_graphics", self.oniguruma);
opts.addOption(bool, "tmux_control_mode", self.oniguruma);
// Version information.
var buf: [1024]u8 = undefined;
opts.addOption(
[]const u8,
"version_string",
std.fmt.bufPrint(
&buf,
b.fmt(
"{f}",
.{self.version},
) catch @panic("version string too long"),
),
);
opts.addOption(usize, "version_major", self.version.major);
opts.addOption(usize, "version_minor", self.version.minor);
opts.addOption(usize, "version_patch", self.version.patch);
opts.addOption(?[]const u8, "version_pre", self.version.pre);
opts.addOption(?[]const u8, "version_build", self.version.build);
m.addOptions("terminal_options", opts);
}
};
pub const Artifact = enum {
/// Ghostty application
ghostty,
/// libghostty-vt, Zig module
lib,
};

View File

@@ -5,7 +5,12 @@
via `lib.TaggedUnion`.
- Any functions must be updated all the way through from here to
`src/terminal/c/main.zig` to `src/lib_vt.zig` and the headers
in `include/ghostty/vt.h`.
in `include/ghostty/vt.h`. Specifically:
1. Define the function in `src/terminal/c/<module>.zig`.
2. Re-export it via a `pub const` in `src/terminal/c/main.zig`.
3. Add an `@export` call in `src/lib_vt.zig` with the
`ghostty_` prefixed symbol name.
4. Declare it in the corresponding header under `include/ghostty/vt/`.
- In `include/ghostty/vt.h`, always sort the header contents by:
(1) macros, (2) forward declarations, (3) types, (4) functions

View File

@@ -25,7 +25,8 @@ pub const BuildInfo = enum(c_int) {
version_major = 6,
version_minor = 7,
version_patch = 8,
version_build = 9,
version_pre = 9,
version_build = 10,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: BuildInfo) type {
@@ -33,7 +34,7 @@ pub const BuildInfo = enum(c_int) {
.invalid => void,
.simd, .kitty_graphics, .tmux_control_mode => bool,
.optimize => OptimizeMode,
.version_string, .version_build => lib.String,
.version_string, .version_pre, .version_build => lib.String,
.version_major, .version_minor, .version_patch => usize,
};
}
@@ -78,6 +79,13 @@ fn getTyped(
.version_major => out.* = build_options.version_major,
.version_minor => out.* = build_options.version_minor,
.version_patch => out.* = build_options.version_patch,
.version_pre => {
if (build_options.version_pre) |b| {
out.* = .{ .ptr = b.ptr, .len = b.len };
} else {
out.* = .{ .ptr = "", .len = 0 };
}
},
.version_build => {
if (build_options.version_build) |b| {
out.* = .{ .ptr = b.ptr, .len = b.len };
@@ -151,6 +159,12 @@ test "get version_patch" {
try testing.expectEqual(build_options.version_patch, value);
}
test "get version_pre" {
const testing = std.testing;
var value: lib.String = undefined;
try testing.expectEqual(Result.success, get(.version_pre, @ptrCast(&value)));
}
test "get version_build" {
const testing = std.testing;
var value: lib.String = undefined;

View File

@@ -3,6 +3,8 @@ const testing = std.testing;
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const terminal_c = @import("terminal.zig");
const grid_ref = @import("grid_ref.zig");
const selection_c = @import("selection.zig");
const ZigTerminal = @import("../Terminal.zig");
const formatterpkg = @import("../formatter.zig");
const Result = @import("result.zig").Result;
@@ -23,6 +25,8 @@ pub const Formatter = ?*FormatterWrapper;
/// C: GhosttyFormatterFormat
pub const Format = formatterpkg.Format;
const CSelection = selection_c.CSelection;
/// C: GhosttyFormatterScreenOptions
pub const ScreenOptions = extern struct {
/// C: GhosttyFormatterScreenExtra
@@ -63,6 +67,10 @@ pub const TerminalOptions = extern struct {
trim: bool,
extra: Extra,
/// Optional selection to restrict output to a range.
/// If null, the entire screen is formatted.
selection: ?*const CSelection = null,
/// C: GhosttyFormatterTerminalExtra
pub const Extra = extern struct {
size: usize = @sizeOf(Extra),
@@ -138,6 +146,12 @@ fn terminal_new_(
});
formatter.extra = opts.extra.toZig();
// Setup the content that we're formatting
if (opts.selection) |sel| formatter.content = .{
.selection = sel.toZig() orelse
return error.InvalidValue,
};
ptr.* = .{
.kind = .{ .terminal = formatter },
.alloc = alloc,
@@ -389,6 +403,50 @@ test "format vt" {
try testing.expect(std.mem.indexOf(u8, buf[0..written], "Test") != null);
}
test "format plain with selection" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello World", 11);
// Get grid refs for "World" (columns 6..10 on row 0)
var start_ref: grid_ref.CGridRef = .{};
try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 6, .y = 0 } },
}, &start_ref));
var end_ref: grid_ref.CGridRef = .{};
try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 10, .y = 0 } },
}, &end_ref));
const sel: selection_c.CSelection = .{
.start = start_ref,
.end = end_ref,
};
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib.alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .selection = &sel, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [1024]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqualStrings("World", buf[0..written]);
}
test "format html" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(

View File

@@ -3,11 +3,13 @@ const testing = std.testing;
const lib = @import("../lib.zig");
const page = @import("../page.zig");
const PageList = @import("../PageList.zig");
const point = @import("../point.zig");
const size = @import("../size.zig");
const stylepkg = @import("../style.zig");
const cell_c = @import("cell.zig");
const row_c = @import("row.zig");
const style_c = @import("style.zig");
const terminal_c = @import("terminal.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyGridRef
@@ -29,7 +31,7 @@ pub const CGridRef = extern struct {
};
}
fn toPin(self: CGridRef) ?PageList.Pin {
pub fn toPin(self: CGridRef) ?PageList.Pin {
return .{
.node = self.node orelse return null,
.x = self.x,
@@ -89,6 +91,38 @@ pub fn grid_ref_graphemes(
return .success;
}
pub fn grid_ref_hyperlink_uri(
ref: *const CGridRef,
out_buf: ?[*]u8,
buf_len: usize,
out_len: *usize,
) callconv(lib.calling_conv) Result {
const p = ref.toPin() orelse return .invalid_value;
const rac = p.node.data.getRowAndCell(p.x, p.y);
const cell = rac.cell;
if (!cell.hyperlink) {
out_len.* = 0;
return .success;
}
const link_id = p.node.data.lookupHyperlink(cell) orelse {
out_len.* = 0;
return .success;
};
const entry = p.node.data.hyperlink_set.get(p.node.data.memory, link_id);
const uri = entry.uri.slice(p.node.data.memory);
if (out_buf == null or buf_len < uri.len) {
out_len.* = uri.len;
return .out_of_space;
}
@memcpy(out_buf.?[0..uri.len], uri);
out_len.* = uri.len;
return .success;
}
pub fn grid_ref_style(
ref: *const CGridRef,
out: ?*style_c.Style,
@@ -154,3 +188,63 @@ test "grid_ref_style null out" {
const ref = CGridRef{};
try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, null));
}
test "grid_ref_hyperlink_uri null node" {
const ref = CGridRef{};
var len: usize = undefined;
try testing.expectEqual(Result.invalid_value, grid_ref_hyperlink_uri(&ref, null, 0, &len));
}
test "grid_ref_hyperlink_uri no hyperlink" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&terminal,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(terminal);
terminal_c.vt_write(terminal, "hello", 5);
var ref: CGridRef = undefined;
try testing.expectEqual(Result.success, terminal_c.grid_ref(
terminal,
point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
&ref,
));
var len: usize = undefined;
try testing.expectEqual(Result.success, grid_ref_hyperlink_uri(&ref, null, 0, &len));
try testing.expectEqual(@as(usize, 0), len);
}
test "grid_ref_hyperlink_uri with hyperlink" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&terminal,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(terminal);
// Write OSC 8 hyperlink: \e]8;;uri\e\\text\e]8;;\e\\
const seq = "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\";
terminal_c.vt_write(terminal, seq, seq.len);
var ref: CGridRef = undefined;
try testing.expectEqual(Result.success, terminal_c.grid_ref(
terminal,
point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
&ref,
));
// First query length with null buf
var len: usize = undefined;
try testing.expectEqual(Result.out_of_space, grid_ref_hyperlink_uri(&ref, null, 0, &len));
try testing.expectEqual(@as(usize, 19), len); // "https://example.com"
// Now read with a properly sized buffer
var buf: [256]u8 = undefined;
try testing.expectEqual(Result.success, grid_ref_hyperlink_uri(&ref, &buf, buf.len, &len));
try testing.expectEqualStrings("https://example.com", buf[0..len]);
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,10 +8,25 @@ pub const color = @import("color.zig");
pub const focus = @import("focus.zig");
pub const formatter = @import("formatter.zig");
pub const grid_ref = @import("grid_ref.zig");
pub const kitty_graphics = @import("kitty_graphics.zig");
pub const kitty_graphics_get = kitty_graphics.get;
pub const kitty_graphics_image = kitty_graphics.image_get_handle;
pub const kitty_graphics_image_get = kitty_graphics.image_get;
pub const kitty_graphics_placement_iterator_new = kitty_graphics.placement_iterator_new;
pub const kitty_graphics_placement_iterator_free = kitty_graphics.placement_iterator_free;
pub const kitty_graphics_placement_iterator_set = kitty_graphics.placement_iterator_set;
pub const kitty_graphics_placement_next = kitty_graphics.placement_iterator_next;
pub const kitty_graphics_placement_get = kitty_graphics.placement_get;
pub const kitty_graphics_placement_rect = kitty_graphics.placement_rect;
pub const kitty_graphics_placement_pixel_size = kitty_graphics.placement_pixel_size;
pub const kitty_graphics_placement_grid_size = kitty_graphics.placement_grid_size;
pub const kitty_graphics_placement_viewport_pos = kitty_graphics.placement_viewport_pos;
pub const kitty_graphics_placement_source_rect = kitty_graphics.placement_source_rect;
pub const types = @import("types.zig");
pub const modes = @import("modes.zig");
pub const osc = @import("osc.zig");
pub const render = @import("render.zig");
pub const selection = @import("selection.zig");
pub const key_event = @import("key_event.zig");
pub const key_encode = @import("key_encode.zig");
pub const mouse_event = @import("mouse_event.zig");
@@ -21,6 +36,7 @@ pub const row = @import("row.zig");
pub const sgr = @import("sgr.zig");
pub const size_report = @import("size_report.zig");
pub const style = @import("style.zig");
pub const sys = @import("sys.zig");
pub const terminal = @import("terminal.zig");
// The full C API, unexported.
@@ -131,6 +147,8 @@ pub const row_get = row.get;
pub const style_default = style.default_style;
pub const style_is_default = style.style_is_default;
pub const sys_set = sys.set;
pub const terminal_new = terminal.new;
pub const terminal_free = terminal.free;
pub const terminal_reset = terminal.reset;
@@ -142,12 +160,14 @@ pub const terminal_mode_get = terminal.mode_get;
pub const terminal_mode_set = terminal.mode_set;
pub const terminal_get = terminal.get;
pub const terminal_grid_ref = terminal.grid_ref;
pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref;
pub const type_json = types.get_json;
pub const grid_ref_cell = grid_ref.grid_ref_cell;
pub const grid_ref_row = grid_ref.grid_ref_row;
pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes;
pub const grid_ref_hyperlink_uri = grid_ref.grid_ref_hyperlink_uri;
pub const grid_ref_style = grid_ref.grid_ref_style;
test {
@@ -156,12 +176,14 @@ test {
_ = cell;
_ = color;
_ = grid_ref;
_ = kitty_graphics;
_ = row;
_ = focus;
_ = formatter;
_ = modes;
_ = osc;
_ = render;
_ = selection;
_ = key_event;
_ = key_encode;
_ = mouse_event;
@@ -170,6 +192,7 @@ test {
_ = sgr;
_ = size_report;
_ = style;
_ = sys;
_ = terminal;
_ = types;

View File

@@ -0,0 +1,16 @@
const grid_ref = @import("grid_ref.zig");
const Selection = @import("../Selection.zig");
/// C: GhosttySelection
pub const CSelection = extern struct {
size: usize = @sizeOf(CSelection),
start: grid_ref.CGridRef,
end: grid_ref.CGridRef,
rectangle: bool = false,
pub fn toZig(self: CSelection) ?Selection {
const start_pin = self.start.toPin() orelse return null;
const end_pin = self.end.toPin() orelse return null;
return Selection.init(start_pin, end_pin, self.rectangle);
}
};

137
src/terminal/c/sys.zig Normal file
View File

@@ -0,0 +1,137 @@
const std = @import("std");
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const terminal_sys = @import("../sys.zig");
const Result = @import("result.zig").Result;
/// C: GhosttySysImage
pub const Image = extern struct {
width: u32,
height: u32,
data: ?[*]u8,
data_len: usize,
};
/// C: GhosttySysDecodePngFn
pub const DecodePngFn = *const fn (
?*anyopaque,
*const CAllocator,
[*]const u8,
usize,
*Image,
) callconv(lib.calling_conv) bool;
/// C: GhosttySysOption
pub const Option = enum(c_int) {
userdata = 0,
decode_png = 1,
pub fn InType(comptime self: Option) type {
return switch (self) {
.userdata => ?*const anyopaque,
.decode_png => ?DecodePngFn,
};
}
};
/// Global state for the sys interface so we can call through to the C
/// callbacks from Zig.
const Global = struct {
userdata: ?*anyopaque = null,
decode_png: ?DecodePngFn = null,
};
/// Global state for the C sys interface.
var global: Global = .{};
/// Zig-compatible wrapper that calls through to the stored C callback.
/// The C callback allocates the pixel data through the provided allocator,
/// so we can take ownership directly.
fn decodePngWrapper(
alloc: std.mem.Allocator,
data: []const u8,
) terminal_sys.DecodeError!terminal_sys.Image {
const func = global.decode_png orelse return error.InvalidData;
const c_alloc = CAllocator.fromZig(&alloc);
var out: Image = undefined;
if (!func(global.userdata, &c_alloc, data.ptr, data.len, &out)) return error.InvalidData;
const result_data = out.data orelse return error.InvalidData;
return .{
.width = out.width,
.height = out.height,
.data = result_data[0..out.data_len],
};
}
pub fn set(
option: Option,
value: ?*const anyopaque,
) callconv(lib.calling_conv) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
return .invalid_value;
};
}
return switch (option) {
inline else => |comptime_option| setTyped(
comptime_option,
@ptrCast(@alignCast(value)),
),
};
}
fn setTyped(
comptime option: Option,
value: option.InType(),
) Result {
switch (option) {
.userdata => global.userdata = @constCast(value),
.decode_png => {
global.decode_png = value;
terminal_sys.decode_png = if (value != null) &decodePngWrapper else null;
},
}
return .success;
}
test "set decode_png with null clears" {
// Start from a known state.
global.decode_png = null;
terminal_sys.decode_png = null;
try std.testing.expectEqual(Result.success, set(.decode_png, null));
try std.testing.expect(terminal_sys.decode_png == null);
}
test "set decode_png installs wrapper" {
const S = struct {
fn decode(_: ?*anyopaque, _: *const CAllocator, _: [*]const u8, _: usize, out: *Image) callconv(lib.calling_conv) bool {
out.* = .{ .width = 1, .height = 1, .data = null, .data_len = 0 };
return true;
}
};
try std.testing.expectEqual(Result.success, set(
.decode_png,
@ptrCast(&S.decode),
));
try std.testing.expect(terminal_sys.decode_png != null);
// Clear it again.
try std.testing.expectEqual(Result.success, set(.decode_png, null));
try std.testing.expect(terminal_sys.decode_png == null);
}
test "set userdata" {
var data: u32 = 42;
try std.testing.expectEqual(Result.success, set(.userdata, @ptrCast(&data)));
try std.testing.expect(global.userdata == @as(?*anyopaque, @ptrCast(&data)));
// Clear it.
try std.testing.expectEqual(Result.success, set(.userdata, null));
try std.testing.expect(global.userdata == null);
}

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const testing = std.testing;
const build_options = @import("terminal_options");
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const ZigTerminal = @import("../Terminal.zig");
@@ -7,6 +8,7 @@ const Stream = @import("../stream_terminal.zig").Stream;
const ScreenSet = @import("../ScreenSet.zig");
const PageList = @import("../PageList.zig");
const kitty = @import("../kitty/key.zig");
const kitty_gfx_c = @import("kitty_graphics.zig");
const modes = @import("../modes.zig");
const point = @import("../point.zig");
const size = @import("../size.zig");
@@ -304,6 +306,10 @@ pub const Option = enum(c_int) {
color_background = 12,
color_cursor = 13,
color_palette = 14,
kitty_image_storage_limit = 15,
kitty_image_medium_file = 16,
kitty_image_medium_temp_file = 17,
kitty_image_medium_shared_mem = 18,
/// Input type expected for setting the option.
pub fn InType(comptime self: Option) type {
@@ -320,6 +326,11 @@ pub const Option = enum(c_int) {
.title, .pwd => ?*const lib.String,
.color_foreground, .color_background, .color_cursor => ?*const color.RGB.C,
.color_palette => ?*const color.PaletteC,
.kitty_image_storage_limit => ?*const u64,
.kitty_image_medium_file,
.kitty_image_medium_temp_file,
.kitty_image_medium_shared_mem,
=> ?*const bool,
};
}
};
@@ -388,6 +399,32 @@ fn setTyped(
);
wrapper.terminal.flags.dirty.palette = true;
},
.kitty_image_storage_limit => {
if (comptime !build_options.kitty_graphics) return .success;
const limit: usize = if (value) |v| @intCast(v.*) else 0;
var it = wrapper.terminal.screens.all.iterator();
while (it.next()) |entry| {
const screen = entry.value.*;
screen.kitty_images.setLimit(screen.alloc, screen, limit) catch return .out_of_memory;
}
},
.kitty_image_medium_file,
.kitty_image_medium_temp_file,
.kitty_image_medium_shared_mem,
=> {
if (comptime !build_options.kitty_graphics) return .success;
const val = (value orelse return .success).*;
var it = wrapper.terminal.screens.all.iterator();
while (it.next()) |entry| {
const screen = entry.value.*;
switch (option) {
.kitty_image_medium_file => screen.kitty_images.image_limits.file = val,
.kitty_image_medium_temp_file => screen.kitty_images.image_limits.temporary_file = val,
.kitty_image_medium_shared_mem => screen.kitty_images.image_limits.shared_memory = val,
else => unreachable,
}
}
},
}
return .success;
}
@@ -479,6 +516,9 @@ pub fn mode_set(
return .success;
}
/// C: GhosttyKittyGraphics
pub const KittyGraphics = kitty_gfx_c.KittyGraphics;
/// C: GhosttyTerminalScreen
pub const TerminalScreen = ScreenSet.Key;
@@ -513,6 +553,11 @@ pub const TerminalData = enum(c_int) {
color_background_default = 23,
color_cursor_default = 24,
color_palette_default = 25,
kitty_image_storage_limit = 26,
kitty_image_medium_file = 27,
kitty_image_medium_temp_file = 28,
kitty_image_medium_shared_mem = 29,
kitty_graphics = 30,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: TerminalData) type {
@@ -535,6 +580,12 @@ pub const TerminalData = enum(c_int) {
.color_cursor_default,
=> color.RGB.C,
.color_palette, .color_palette_default => color.PaletteC,
.kitty_image_storage_limit => u64,
.kitty_image_medium_file,
.kitty_image_medium_temp_file,
.kitty_image_medium_shared_mem,
=> bool,
.kitty_graphics => KittyGraphics,
};
}
};
@@ -603,6 +654,26 @@ fn getTyped(
.color_cursor_default => out.* = (t.colors.cursor.default orelse return .no_value).cval(),
.color_palette => out.* = color.paletteCval(&t.colors.palette.current),
.color_palette_default => out.* = color.paletteCval(&t.colors.palette.original),
.kitty_image_storage_limit => {
if (comptime !build_options.kitty_graphics) return .no_value;
out.* = @intCast(t.screens.active.kitty_images.total_limit);
},
.kitty_image_medium_file => {
if (comptime !build_options.kitty_graphics) return .no_value;
out.* = t.screens.active.kitty_images.image_limits.file;
},
.kitty_image_medium_temp_file => {
if (comptime !build_options.kitty_graphics) return .no_value;
out.* = t.screens.active.kitty_images.image_limits.temporary_file;
},
.kitty_image_medium_shared_mem => {
if (comptime !build_options.kitty_graphics) return .no_value;
out.* = t.screens.active.kitty_images.image_limits.shared_memory;
},
.kitty_graphics => {
if (comptime !build_options.kitty_graphics) return .no_value;
out.* = &t.screens.active.kitty_images;
},
}
return .success;
@@ -626,6 +697,20 @@ pub fn grid_ref(
return .success;
}
pub fn point_from_grid_ref(
terminal_: Terminal,
ref: *const grid_ref_c.CGridRef,
tag: point.Tag,
out: ?*point.Coordinate,
) callconv(lib.calling_conv) Result {
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
const p = ref.toPin() orelse return .invalid_value;
const pt = t.screens.active.pages.pointFromPin(tag, p) orelse
return .no_value;
if (out) |o| o.* = pt.coord();
return .success;
}
pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void {
const wrapper = terminal_ orelse return;
const t = wrapper.terminal;
@@ -1190,6 +1275,102 @@ test "grid_ref null terminal" {
}, &out_ref));
}
test "point_from_grid_ref roundtrip active" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer free(t);
vt_write(t, "Hello", 5);
// Get a grid ref at (2, 0) in active coords
var ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 2, .y = 0 } },
}, &ref));
// Convert back to active coords
var coord: point.Coordinate = undefined;
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .active, &coord));
try testing.expectEqual(@as(size.CellCountInt, 2), coord.x);
try testing.expectEqual(@as(u32, 0), coord.y);
}
test "point_from_grid_ref roundtrip viewport" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer free(t);
vt_write(t, "Hello", 5);
var ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .viewport,
.value = .{ .viewport = .{ .x = 0, .y = 0 } },
}, &ref));
var coord: point.Coordinate = undefined;
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .viewport, &coord));
try testing.expectEqual(@as(size.CellCountInt, 0), coord.x);
try testing.expectEqual(@as(u32, 0), coord.y);
}
test "point_from_grid_ref history ref to active returns no_value" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 4, .max_scrollback = 10_000 },
));
defer free(t);
// Write enough lines to push content into scrollback
for (0..10) |_| {
vt_write(t, "line\n", 5);
}
// Get a ref to the first line (now in scrollback)
var ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .screen,
.value = .{ .screen = .{ .x = 0, .y = 0 } },
}, &ref));
// Should succeed for screen coords
var coord: point.Coordinate = undefined;
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .screen, &coord));
try testing.expectEqual(@as(u32, 0), coord.y);
// Should fail for active coords (it's in scrollback)
try testing.expectEqual(Result.no_value, point_from_grid_ref(t, &ref, .active, &coord));
}
test "point_from_grid_ref null terminal" {
var ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.invalid_value, point_from_grid_ref(null, &ref, .active, null));
}
test "point_from_grid_ref null node" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer free(t);
const ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.invalid_value, point_from_grid_ref(t, &ref, .active, null));
}
test "set write_pty callback" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(

View File

@@ -13,6 +13,7 @@ const size_report = @import("size_report.zig");
const terminal = @import("terminal.zig");
const formatter = @import("formatter.zig");
const selection = @import("selection.zig");
const render = @import("render.zig");
const style_c = @import("style.zig");
const mouse_encode = @import("mouse_encode.zig");
@@ -26,6 +27,7 @@ pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
.{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
.{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
.{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
.{ "GhosttySelection", StructInfo.init(selection.CSelection) },
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
.{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },

View File

@@ -25,6 +25,7 @@ pub const unicode = @import("graphics_unicode.zig");
pub const Command = command.Command;
pub const CommandParser = command.Parser;
pub const Image = image.Image;
pub const LoadingImage = image.LoadingImage;
pub const ImageStorage = storage.ImageStorage;
pub const RenderPlacement = render.Placement;
pub const Response = command.Response;

View File

@@ -3,6 +3,7 @@ const assert = @import("../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const simd = @import("../../simd/main.zig");
const lib = @import("../lib.zig");
const log = std.log.scoped(.kitty_gfx);
@@ -394,39 +395,38 @@ pub const Transmission = struct {
compression: Compression = .none, // o
more_chunks: bool = false, // m
pub const Format = enum {
rgb, // 24
rgba, // 32
png, // 100
pub const Format = lib.Enum(lib.target, &.{
"rgb", // 24
"rgba", // 32
"png", // 100
// The following are not supported directly via the protocol
// but they are formats that a png may decode to that we
// support.
gray_alpha,
gray,
"gray_alpha",
"gray",
});
pub fn bpp(self: Format) u8 {
return switch (self) {
.gray => 1,
.gray_alpha => 2,
.rgb => 3,
.rgba => 4,
.png => unreachable, // Must be validated before
};
}
};
pub const Medium = lib.Enum(lib.target, &.{
"direct", // d
"file", // f
"temporary_file", // t
"shared_memory", // s
});
pub const Medium = enum {
direct, // d
file, // f
temporary_file, // t
shared_memory, // s
};
pub const Compression = lib.Enum(lib.target, &.{
"none",
"zlib_deflate", // z
});
pub const Compression = enum {
none,
zlib_deflate, // z
};
pub fn formatBpp(format: Format) u8 {
return switch (format) {
.gray => 1,
.gray_alpha => 2,
.rgb => 3,
.rgba => 4,
.png => unreachable, // Must be validated before
};
}
fn parse(kv: KV) !Transmission {
var result: Transmission = .{};

View File

@@ -44,7 +44,7 @@ pub fn execute(
var quiet = cmd.quiet;
const resp_: ?Response = switch (cmd.control) {
.query => query(alloc, cmd),
.query => query(alloc, terminal, cmd),
.display => display(alloc, terminal, cmd),
.delete => delete(alloc, terminal, cmd),
@@ -94,7 +94,11 @@ pub fn execute(
/// This command is used to attempt to load an image and respond with
/// success/error but does not persist any of the command to the terminal
/// state.
fn query(alloc: Allocator, cmd: *const Command) Response {
fn query(
alloc: Allocator,
terminal: *const Terminal,
cmd: *const Command,
) Response {
const t = cmd.control.query;
// Query requires image ID. We can't actually send a response without
@@ -112,7 +116,8 @@ fn query(alloc: Allocator, cmd: *const Command) Response {
};
// Attempt to load the image. If we cannot, then set an appropriate error.
var loading = LoadingImage.init(alloc, cmd) catch |err| {
const storage = &terminal.screens.active.kitty_images;
var loading = LoadingImage.init(alloc, cmd, storage.image_limits) catch |err| {
encodeError(&result, err);
return result;
};
@@ -322,7 +327,7 @@ fn loadAndAddImage(
}
break :loading loading.*;
} else try .init(alloc, cmd);
} else try .init(alloc, cmd, storage.image_limits);
// We only want to deinit on error. If we're chunking, then we don't
// want to deinit at all. If we're not chunking, then we'll deinit

View File

@@ -8,7 +8,7 @@ const posix = std.posix;
const fastmem = @import("../../fastmem.zig");
const command = @import("graphics_command.zig");
const PageList = @import("../PageList.zig");
const wuffs = @import("wuffs");
const sys = @import("../sys.zig");
const temp_dir = struct {
const TempDir = @import("../../os/TempDir.zig");
@@ -44,10 +44,38 @@ pub const LoadingImage = struct {
/// used if q isn't set on subsequent chunks.
quiet: command.Command.Quiet,
/// The limits of the Kitty Graphics protocol we should allow.
///
/// This can be used to restrict the type of images and other
/// parameters for resource or security reasons. Note that depending
/// on how libghostty is compiled, some of these may be fully unsupported
/// and ignored (e.g. "file" on wasm32-freestanding).
pub const Limits = packed struct {
file: bool,
temporary_file: bool,
shared_memory: bool,
pub const all: Limits = .{
.file = true,
.temporary_file = true,
.shared_memory = true,
};
pub const direct: Limits = .{
.file = false,
.temporary_file = false,
.shared_memory = false,
};
};
/// Initialize a chunked immage from the first image transmission.
/// If this is a multi-chunk image, this should only be the FIRST
/// chunk.
pub fn init(alloc: Allocator, cmd: *const command.Command) !LoadingImage {
pub fn init(
alloc: Allocator,
cmd: *const command.Command,
limits: Limits,
) !LoadingImage {
// Build our initial image from the properties sent via the control.
// These can be overwritten by the data loading process. For example,
// PNG loading sets the width/height from the data.
@@ -72,6 +100,26 @@ pub const LoadingImage = struct {
return result;
}
// Verify our capabilities and limits allow this.
{
// Special case if we don't support decoding PNGs and the format
// is a PNG we can save a lot of memory/effort buffering the
// data but failing up front.
if (t.format == .png and
sys.decode_png == null)
{
return error.UnsupportedMedium;
}
// Verify the medium is allowed
switch (t.medium) {
.direct => unreachable,
.file => if (!limits.file) return error.UnsupportedMedium,
.temporary_file => if (!limits.temporary_file) return error.UnsupportedMedium,
.shared_memory => if (!limits.shared_memory) return error.UnsupportedMedium,
}
}
// Otherwise, the payload data is guaranteed to be a path.
if (comptime builtin.os.tag != .windows) {
@@ -154,8 +202,8 @@ pub const LoadingImage = struct {
.png => stat_size,
// For these formats we have a size we must have.
.gray, .gray_alpha, .rgb, .rgba => |f| size: {
const bpp = f.bpp();
.gray, .gray_alpha, .rgb, .rgba => size: {
const bpp = command.Transmission.formatBpp(self.image.format);
break :size self.image.width * self.image.height * bpp;
},
};
@@ -342,7 +390,7 @@ pub const LoadingImage = struct {
if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge;
// Data length must be what we expect
const bpp = img.format.bpp();
const bpp = command.Transmission.formatBpp(img.format);
const expected_len = img.width * img.height * bpp;
const actual_len = self.data.items.len;
if (actual_len != expected_len) {
@@ -426,13 +474,14 @@ pub const LoadingImage = struct {
fn decodePng(self: *LoadingImage, alloc: Allocator) !void {
assert(self.image.format == .png);
const result = wuffs.png.decode(
const decode_png_fn = sys.decode_png orelse
return error.UnsupportedFormat;
const result = decode_png_fn(
alloc,
self.data.items,
) catch |err| switch (err) {
error.WuffsError => return error.InvalidData,
error.InvalidData => return error.InvalidData,
error.OutOfMemory => return error.OutOfMemory,
error.Overflow => return error.InvalidData,
};
defer alloc.free(result.data);
@@ -522,7 +571,7 @@ test "image load with invalid RGB data" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
}
@@ -540,7 +589,7 @@ test "image load with image too wide" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc));
}
@@ -559,7 +608,7 @@ test "image load with image too tall" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc));
}
@@ -583,7 +632,7 @@ test "image load: rgb, zlib compressed, direct" {
),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -611,7 +660,7 @@ test "image load: rgb, not compressed, direct" {
),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -640,7 +689,7 @@ test "image load: rgb, zlib compressed, direct, chunked" {
.data = try alloc.dupe(u8, data[0..1024]),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
// Read our remaining chunks
@@ -676,7 +725,7 @@ test "image load: rgb, zlib compressed, direct, chunked with zero initial chunk"
} },
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
// Read our remaining chunks
@@ -720,7 +769,7 @@ test "image load: temporary file without correct path" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd));
try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd, .all));
// Temporary file should still be there
try tmp_dir.dir.access(path, .{});
@@ -753,7 +802,7 @@ test "image load: rgb, not compressed, temporary file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -790,7 +839,7 @@ test "image load: rgb, not compressed, regular file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -799,6 +848,8 @@ test "image load: rgb, not compressed, regular file" {
}
test "image load: png, not compressed, regular file" {
if (sys.decode_png == null) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@@ -825,7 +876,7 @@ test "image load: png, not compressed, regular file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -833,3 +884,161 @@ test "image load: png, not compressed, regular file" {
try testing.expect(img.format == .rgba);
try tmp_dir.dir.access(path, .{});
}
test "limits: direct medium always allowed" {
const testing = std.testing;
const alloc = testing.allocator;
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .direct,
.width = 1,
.height = 1,
.image_id = 31,
} },
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
// Direct medium should work even with the most restrictive limits
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
}
test "limits: file medium blocked by limits" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
.sub_path = "image.data",
.data = data,
});
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath("image.data", &buf);
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .file,
.compression = .none,
.width = 20,
.height = 15,
.image_id = 31,
} },
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
try testing.expectError(error.UnsupportedMedium, LoadingImage.init(alloc, &cmd, .direct));
}
test "limits: file medium allowed by limits" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
.sub_path = "image.data",
.data = data,
});
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath("image.data", &buf);
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .file,
.compression = .none,
.width = 20,
.height = 15,
.image_id = 31,
} },
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd, .{
.file = true,
.temporary_file = false,
.shared_memory = false,
});
defer loading.deinit(alloc);
}
test "limits: temporary file medium blocked by limits" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
.sub_path = "tty-graphics-protocol-image.data",
.data = data,
});
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf);
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .temporary_file,
.compression = .none,
.width = 20,
.height = 15,
.image_id = 31,
} },
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
try testing.expectError(error.UnsupportedMedium, LoadingImage.init(alloc, &cmd, .{
.file = true,
.temporary_file = false,
.shared_memory = true,
}));
// File should still exist since we blocked before reading
try tmp_dir.dir.access("tty-graphics-protocol-image.data", .{});
}
test "limits: temporary file medium allowed by limits" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
.sub_path = "tty-graphics-protocol-image.data",
.data = data,
});
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf);
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .temporary_file,
.compression = .none,
.width = 20,
.height = 15,
.image_id = 31,
} },
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd, .{
.file = false,
.temporary_file = true,
.shared_memory = false,
});
defer loading.deinit(alloc);
}

View File

@@ -51,6 +51,9 @@ pub const ImageStorage = struct {
/// Non-null if there is an in-progress loading image.
loading: ?*LoadingImage = null,
/// The limits of what medium types are allowed for image loading.
image_limits: LoadingImage.Limits = .direct,
/// The total bytes of image data that have been loaded and the limit.
/// If the limit is reached, the oldest images will be evicted to make
/// space. Unused images take priority.
@@ -89,8 +92,9 @@ pub const ImageStorage = struct {
) !void {
// Special case disabling by quickly deleting all
if (limit == 0) {
const image_limits = self.image_limits;
self.deinit(alloc, s);
self.* = .{};
self.* = .{ .image_limits = image_limits };
}
// If we re lowering our limit, check if we need to evict.
@@ -658,9 +662,10 @@ pub const ImageStorage = struct {
}
}
/// Calculates the size of this placement's image in pixels,
/// taking in to account the specified rows and columns.
pub fn calculatedSize(
/// Returns the size of this placement's image in pixels,
/// taking into account the source rectangle, specified
/// rows/columns, and aspect ratio.
pub fn pixelSize(
self: Placement,
image: Image,
t: *const terminal.Terminal,
@@ -755,7 +760,7 @@ pub const ImageStorage = struct {
// Otherwise we calculate the pixel size, divide by
// cell size, and round up to the nearest integer.
const calc_size = self.calculatedSize(image, t);
const calc_size = self.pixelSize(image, t);
return .{
.cols = std.math.divCeil(
u32,
@@ -1334,7 +1339,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" {
// that's 100px width. 100px * (9 / 16) = 56.25, which should round
// to a height of 56px.
const calc_size = placement.calculatedSize(image, &t);
const calc_size = placement.pixelSize(image, &t);
try testing.expectEqual(@as(u32, 100), calc_size.width);
try testing.expectEqual(@as(u32, 56), calc_size.height);
}
@@ -1352,7 +1357,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" {
// 100px height. 100px * (16 / 9) = 177.77..., which should round to
// a width of 178px.
const calc_size = placement.calculatedSize(image, &t);
const calc_size = placement.pixelSize(image, &t);
try testing.expectEqual(@as(u32, 178), calc_size.width);
try testing.expectEqual(@as(u32, 100), calc_size.height);
}

View File

@@ -23,6 +23,7 @@ pub const search = @import("search.zig");
pub const sgr = @import("sgr.zig");
pub const size = @import("size.zig");
pub const size_report = @import("size_report.zig");
pub const sys = @import("sys.zig");
pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {};
pub const x11_color = @import("x11_color.zig");

View File

@@ -1,5 +1,7 @@
const std = @import("std");
const build_options = @import("terminal_options");
const testing = std.testing;
const apc = @import("apc.zig");
const csi = @import("csi.zig");
const device_attributes = @import("device_attributes.zig");
const device_status = @import("device_status.zig");
@@ -35,6 +37,12 @@ pub const Handler = struct {
/// effects.
effects: Effects = .readonly,
/// The APC command handler maintains the APC state. APC is like
/// CSI or OSC, but it is a private escape sequence that is used
/// to send commands to the terminal emulator. This is used by
/// the kitty graphics protocol.
apc_handler: apc.Handler = .{},
pub const Effects = struct {
/// Called when the terminal needs to write data back to the pty,
/// e.g. in response to a DECRQM query. The data is only valid
@@ -98,9 +106,7 @@ pub const Handler = struct {
}
pub fn deinit(self: *Handler) void {
// Currently does nothing but may in the future so callers should
// call this.
_ = self;
self.apc_handler.deinit();
}
pub fn vt(
@@ -230,6 +236,11 @@ pub const Handler = struct {
.color_operation => try self.colorOperation(value.op, &value.requests),
.kitty_color_report => try self.kittyColorOperation(value),
// APC
.apc_start => self.apc_handler.start(),
.apc_put => self.apc_handler.feed(self.terminal.gpa(), value),
.apc_end => self.apcEnd(),
// Effect-based handlers
.bell => self.bell(),
.device_attributes => self.reportDeviceAttributes(value),
@@ -249,13 +260,6 @@ pub const Handler = struct {
.dcs_unhook,
=> {},
// APC can modify terminal state (Kitty graphics) but we don't
// currently support it in the readonly stream.
.apc_start,
.apc_end,
.apc_put,
=> {},
// Have no terminal-modifying effect
.report_pwd,
.show_desktop_notification,
@@ -650,6 +654,33 @@ pub const Handler = struct {
}
}
}
fn apcEnd(self: *Handler) void {
const alloc = self.terminal.gpa();
var cmd = self.apc_handler.end() orelse return;
defer cmd.deinit(alloc);
switch (cmd) {
.kitty => |*kitty_cmd| if (comptime build_options.kitty_graphics) {
if (self.terminal.kittyGraphics(
alloc,
kitty_cmd,
)) |resp| resp: {
// Don't waste time encoding if we can't write responses
// anyways.
if (self.effects.write_pty == null) break :resp;
// Encode and write the response if we have one.
var buf: [1024]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
resp.encode(&writer) catch return;
writer.writeByte(0) catch return;
const final = writer.buffered();
if (final.len > 3) self.writePty(final[0 .. final.len - 1 :0]);
}
},
}
}
};
test "basic print" {
@@ -2069,3 +2100,52 @@ test "device attributes: custom response" {
s.nextSlice("\x1B[>c");
try testing.expectEqualStrings("\x1b[>41;100;0c", S.written.?);
}
test "kitty graphics APC response" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
const S = struct {
var written: ?[]const u8 = null;
fn writePty(_: *Handler, data: [:0]const u8) void {
if (written) |old| testing.allocator.free(old);
written = testing.allocator.dupe(u8, data) catch @panic("OOM");
}
};
S.written = null;
defer if (S.written) |old| testing.allocator.free(old);
var handler: Handler = .init(&t);
handler.effects.write_pty = &S.writePty;
var s: Stream = .initAlloc(testing.allocator, handler);
defer s.deinit();
// Send a kitty graphics transmit command with image id 1
s.nextSlice("\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2,c=10,r=1;////////\x1b\\");
// Should have written a response back
try testing.expectEqualStrings("\x1b_Gi=1;OK\x1b\\", S.written.?);
}
test "kitty graphics via APC" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
const handler: Handler = .init(&t);
var s: Stream = .initAlloc(testing.allocator, handler);
defer s.deinit();
// Send a kitty graphics transmit command via APC:
// ESC _ G <payload> ESC \
// a=t,t=d,f=24,i=1,s=1,v=2,c=10,r=1;//////// (1x2 RGB direct)
s.nextSlice("\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2,c=10,r=1;////////\x1b\\");
const storage = &t.screens.active.kitty_images;
const img = storage.imageById(1).?;
try testing.expectEqual(.rgb, img.format);
}

54
src/terminal/sys.zig Normal file
View File

@@ -0,0 +1,54 @@
//! System interface for the terminal package.
//!
//! This provides runtime-swappable function pointers for operations that
//! depend on external implementations (e.g. image decoding). Each function
//! pointer is initialized with a default implementation if available.
//!
//! This exists so that the terminal package doesn't have hard dependencies
//! on specific libraries and enables embedders of the terminal package to
//! swap out implementations as needed at startup to provide their own
//! implementations.
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_options = @import("terminal_options");
/// Decode PNG data into RGBA pixels. If null, PNG decoding is unsupported
/// and the exact semantics are up to callers. For example, the Kitty Graphics
/// Protocol will work but cannot accept PNG images.
pub var decode_png: ?DecodePngFn = png: {
if (build_options.artifact == .lib) break :png null;
break :png &decodePngWuffs;
};
pub const DecodeError = Allocator.Error || error{InvalidData};
pub const DecodePngFn = *const fn (Allocator, []const u8) DecodeError!Image;
/// The result of decoding an image. The caller owns the returned data
/// and must free it with the same allocator that was passed to the
/// decode function.
pub const Image = struct {
width: u32,
height: u32,
data: []u8,
};
fn decodePngWuffs(
alloc: Allocator,
data: []const u8,
) DecodeError!Image {
const wuffs = @import("wuffs");
const result = wuffs.png.decode(
alloc,
data,
) catch |err| switch (err) {
error.WuffsError => return error.InvalidData,
error.OutOfMemory => return error.OutOfMemory,
error.Overflow => return error.InvalidData,
};
return .{
.width = result.width,
.height = result.height,
.data = result.data,
};
}

View File

@@ -255,21 +255,12 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
},
.palette = .init(opts.config.palette),
},
.kitty_image_storage_limit = opts.config.image_storage_limit,
.kitty_image_loading_limits = .all,
};
});
errdefer term.deinit(alloc);
// Set the image size limits
var it = term.screens.all.iterator();
while (it.next()) |entry| {
const screen: *terminalpkg.Screen = entry.value.*;
try screen.kitty_images.setLimit(
alloc,
screen,
opts.config.image_storage_limit,
);
}
// Set our default cursor style
term.screens.active.cursor.cursor_style = opts.config.cursor_style;
@@ -463,16 +454,9 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi
break :cursor color.toTerminalRGB() orelse break :cursor null;
};
// Set the image size limits
var it = self.terminal.screens.all.iterator();
while (it.next()) |entry| {
const screen: *terminalpkg.Screen = entry.value.*;
try screen.kitty_images.setLimit(
self.alloc,
screen,
config.image_storage_limit,
);
}
// Set the image limits
try self.terminal.setKittyGraphicsSizeLimit(self.alloc, config.image_storage_limit);
self.terminal.setKittyGraphicsLoadingLimits(.all);
}
/// Resize the terminal.