ghostty-vt Zig Module (#8840)

This makes a `ghostty-vt` Zig module available from our `build.zig` that
contains a reusable Zig API version of our core terminal emulation layer
including escape sequence parsing, terminal state, and screen state.
This is the groundwork for phase one of my "libghostty" vision.

With SIMD disabled, `ghostty-vt` has no dependencies -- not even on libc
-- and can produce fully static standalone binaries. With SIMD enabled,
`ghostty-vt` only depends on libc.

The point of this PR is primarily to get the bug fixes I found in and to
get this running in CI on every commit so that we don't regress it. In
the future we'll do more (see the future section below).

> [!WARNING]
> **The API is extremely not stable and will definitely change in the
future.** The _functionality/logic_ is very stable, because it's the
same core logic used by Ghostty, but the API itself is not at all. For
this PR, we mostly just expose everything and we'll reshape the API
later.

## What is `libghostty-vt`? 

I've stated my vision for a `libghostty` for some time. You can find
background on that. Recently, I've realized that the _scope_ of
`libghostty` is way too large to ship as a single unit. To that end,
`libghostty` will be split into smaller scoped sub-libraries (that may
depend on each other for higher level functionality). The exact mapping
is being worked out.

**The first library I'm extracting is `libghostty-vt` (both Zig and C,
this PR starts with Zig).** This will be a library focused only on core
terminal emulation, terminal state, and screen state. It lacks rendering
support and input handling.

**But why?** The core terminal emulation is the primary source of both
missing functionality and bugs within terminal emulators. Look at this
[simple bug in jediterm](https://github.com/JetBrains/jediterm/pull/311)
that fails to parse a trivially common sequence resulting in horrendous
misrenders. Jediterm is used by every JetBrains IDE! Literally the core
terminal in a many-millions-of-dollars business!

`libghostty-vt` is a _zero dependency_ terminal emulation layer that
exposes a C API which will let any popular language build bindings so
that we can stop reinventing the terminal emulation layer and get best
in class (or near it) terminal emulation capabilities everywhere.

## In This PR

- `ghostty-vt` Zig module
- Example usage of it in `example/zig-vt`
- CI to run Zig module tests, test that our examples build, and test
SIMD on/off
- New feature build flag `-Dsimd` (default on) that turns SIMD on or off
- Unexposed feature flag that allows building the core terminal logic
without regex support (default on right now jus for the ghostty-vt
module as I figure out what our future regex story is in a post-oni
world).
- Fixes for non-SIMD builds

## Future

There's a lot to do in the future outside of this PR:

- Define a more stable Zig API
- Define a C API at all
- Figure out our regex engine story
- Documentation improvements
pull/8848/head
Mitchell Hashimoto 2025-09-22 10:06:36 -07:00 committed by GitHub
commit 8cb52323e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1104 additions and 4913 deletions

View File

@ -12,6 +12,7 @@ jobs:
needs:
- build-bench
- build-dist
- build-examples
- build-flatpak
- build-freebsd
- build-linux
@ -22,6 +23,7 @@ jobs:
- build-snap
- build-windows
- test
- test-simd
- test-gtk
- test-sentry-linux
- test-macos
@ -87,6 +89,42 @@ jobs:
- name: Build Benchmarks
run: nix develop -c zig build -Demit-bench
build-examples:
strategy:
fail-fast: false
matrix:
dir: [zig-vt]
name: Example ${{ matrix.dir }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Build Example
run: |
cd example/${{ matrix.dir }}
nix develop -c zig build
build-flatpak:
strategy:
fail-fast: false
@ -564,6 +602,41 @@ jobs:
-Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }}
test-simd:
strategy:
fail-fast: false
matrix:
simd: ["true", "false"]
name: Build -Dsimd=${{ matrix.simd }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Test
run: |
nix develop -c zig build test -Dsimd=${{ matrix.simd }}
test-sentry-linux:
strategy:
fail-fast: false

View File

@ -8,12 +8,25 @@ comptime {
}
pub fn build(b: *std.Build) !void {
// This defines all the available build options (e.g. `-D`).
// This defines all the available build options (e.g. `-D`). If you
// want to know what options are available, you can run `--help` or
// you can read `src/build/Config.zig`.
const config = try buildpkg.Config.init(b);
const test_filter = b.option(
[]const u8,
const test_filters = b.option(
[][]const u8,
"test-filter",
"Filter for test. Only applies to Zig tests.",
) orelse &[0][]const u8{};
// Ghostty dependencies used by many artifacts.
const deps = try buildpkg.SharedDeps.init(b, &config);
// The modules exported for Zig consumers of libghostty. If you're
// writing a Zig program that uses libghostty, read this file.
const mod = try buildpkg.GhosttyZig.init(
b,
&config,
&deps,
);
// All our steps which we'll hook up later. The steps are shown
@ -24,6 +37,10 @@ pub fn build(b: *std.Build) !void {
"Run the app under valgrind",
);
const test_step = b.step("test", "Run tests");
const test_lib_vt_step = b.step(
"test-lib-vt",
"Run libghostty-vt tests",
);
const test_valgrind_step = b.step(
"test-valgrind",
"Run tests under valgrind",
@ -37,10 +54,6 @@ pub fn build(b: *std.Build) !void {
const resources = try buildpkg.GhosttyResources.init(b, &config);
const i18n = if (config.i18n) try buildpkg.GhosttyI18n.init(b, &config) else null;
// Ghostty dependencies used by many artifacts.
const deps = try buildpkg.SharedDeps.init(b, &config);
if (config.emit_helpgen) deps.help_strings.install();
// Ghostty executable, the actual runnable Ghostty program.
const exe = try buildpkg.GhosttyExe.init(b, &config, &deps);
@ -83,6 +96,9 @@ pub fn build(b: *std.Build) !void {
&deps,
);
// Helpgen
if (config.emit_helpgen) deps.help_strings.install();
// Runtime "none" is libghostty, anything else is an executable.
if (config.app_runtime != .none) {
if (config.emit_exe) {
@ -185,7 +201,7 @@ pub fn build(b: *std.Build) !void {
run_step.dependOn(&macos_app_native_only.open.step);
// If we have no test filters, install the tests too
if (test_filter == null) {
if (test_filters.len == 0) {
macos_app_native_only.addTestStepDependencies(test_step);
}
}
@ -216,11 +232,24 @@ pub fn build(b: *std.Build) !void {
run_valgrind_step.dependOn(&run_cmd.step);
}
// Zig module tests
{
const mod_vt_test = b.addTest(.{
.root_module = mod.vt,
.target = config.target,
.optimize = config.optimize,
.filters = test_filters,
});
const mod_vt_test_run = b.addRunArtifact(mod_vt_test);
test_lib_vt_step.dependOn(&mod_vt_test_run.step);
}
// Tests
{
// Full unit tests
const test_exe = b.addTest(.{
.name = "ghostty-test",
.filters = if (test_filter) |v| &.{v} else &.{},
.filters = test_filters,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = config.baselineTarget(),
@ -230,7 +259,6 @@ pub fn build(b: *std.Build) !void {
.unwind_tables = .sync,
}),
});
if (config.emit_test_exe) b.installArtifact(test_exe);
_ = try deps.add(test_exe);
@ -238,6 +266,9 @@ pub fn build(b: *std.Build) !void {
const test_run = b.addRunArtifact(test_exe);
test_step.dependOn(&test_run.step);
// Normal tests always test our libghostty modules
test_step.dependOn(test_lib_vt_step);
// Valgrind test running
const valgrind_run = b.addSystemCommand(&.{
"valgrind",

View File

@ -1,189 +0,0 @@
import { ZigJS } from "zig-js";
const zjs = new ZigJS();
const importObject = {
module: {},
env: {
memory: new WebAssembly.Memory({
initial: 25,
maximum: 65536,
shared: true,
}),
log: (ptr: number, len: number) => {
const arr = new Uint8ClampedArray(zjs.memory.buffer, ptr, len);
const data = arr.slice();
const str = new TextDecoder("utf-8").decode(data);
console.log(str);
},
},
...zjs.importObject(),
};
const url = new URL("ghostty-wasm.wasm", import.meta.url);
fetch(url.href)
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject))
.then((results) => {
const memory = importObject.env.memory;
const {
malloc,
free,
config_new,
config_free,
config_load_string,
config_finalize,
face_new,
face_free,
face_render_glyph,
face_debug_canvas,
deferred_face_new,
deferred_face_free,
deferred_face_load,
deferred_face_face,
group_new,
group_free,
group_add_face,
group_init_sprite_face,
group_index_for_codepoint,
group_render_glyph,
group_cache_new,
group_cache_free,
group_cache_index_for_codepoint,
group_cache_render_glyph,
group_cache_atlas_grayscale,
group_cache_atlas_color,
atlas_new,
atlas_free,
atlas_debug_canvas,
shaper_new,
shaper_free,
shaper_test,
} = results.instance.exports;
// Give us access to the zjs value for debugging.
globalThis.zjs = zjs;
console.log(zjs);
// Initialize our zig-js memory
zjs.memory = memory;
// Helpers
const makeStr = (str) => {
const utf8 = new TextEncoder().encode(str);
const ptr = malloc(utf8.byteLength);
new Uint8Array(memory.buffer, ptr).set(utf8);
return { ptr: ptr, len: utf8.byteLength };
};
// Create our config
const config = config_new();
const config_str = makeStr("font-family = monospace");
config_load_string(config, config_str.ptr, config_str.len);
config_finalize(config);
free(config_str.ptr);
// Create our atlas
// const atlas = atlas_new(512, 0 /* grayscale */);
// Create some memory for our string
const font_name = makeStr("monospace");
// Initialize our deferred face
// const df = deferred_face_new(font_ptr, font.byteLength, 0 /* text */);
//deferred_face_load(df, 72 /* size */);
//const face = deferred_face_face(df);
// Initialize our font face
//const face = face_new(font_ptr, font.byteLength, 72 /* size in px */);
//free(font_ptr);
// Create our group
const group = group_new(32 /* size */);
group_add_face(
group,
0 /* regular */,
deferred_face_new(font_name.ptr, font_name.len, 0 /* text */),
);
group_add_face(
group,
0 /* regular */,
deferred_face_new(font_name.ptr, font_name.len, 1 /* emoji */),
);
// Initialize our sprite font, without this we just use the browser.
group_init_sprite_face(group);
// Create our group cache
const group_cache = group_cache_new(group);
// Render a glyph
// for (let i = 33; i <= 126; i++) {
// const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
// group_cache_render_glyph(group_cache, font_idx, i, 0);
// //face_render_glyph(face, atlas, i);
// }
//
// const emoji = ["🐏","🌞","🌚","🍱","💿","🐈","📃","📀","🕡","🙃"];
// for (let i = 0; i < emoji.length; i++) {
// const cp = emoji[i].codePointAt(0);
// const font_idx = group_cache_index_for_codepoint(group_cache, cp, 0, -1 /* best choice */);
// group_cache_render_glyph(group_cache, font_idx, cp, 0);
// }
for (let i = 0x2500; i <= 0x257f; i++) {
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
group_cache_render_glyph(group_cache, font_idx, i, 0);
}
for (let i = 0x2580; i <= 0x259f; i++) {
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
group_cache_render_glyph(group_cache, font_idx, i, 0);
}
for (let i = 0x2800; i <= 0x28ff; i++) {
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
group_cache_render_glyph(group_cache, font_idx, i, 0);
}
for (let i = 0x1fb00; i <= 0x1fb3b; i++) {
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
group_cache_render_glyph(group_cache, font_idx, i, 0);
}
for (let i = 0x1fb3c; i <= 0x1fb6b; i++) {
const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
group_cache_render_glyph(group_cache, font_idx, i, 0);
}
//face_render_glyph(face, atlas, "橋".codePointAt(0));
//face_render_glyph(face, atlas, "p".codePointAt(0));
// Debug our canvas
//face_debug_canvas(face);
// Let's try shaping
const shaper = shaper_new(120);
//const input = makeStr("hello🐏");
const input = makeStr("hello🐏👍🏽");
shaper_test(shaper, group_cache, input.ptr, input.len);
const cp = 1114112;
const font_idx = group_cache_index_for_codepoint(
group_cache,
cp,
0,
-1 /* best choice */,
);
group_cache_render_glyph(group_cache, font_idx, cp, -1);
// Debug our atlas canvas
{
const atlas = group_cache_atlas_grayscale(group_cache);
const id = atlas_debug_canvas(atlas);
document.getElementById("atlas-canvas").append(zjs.deleteValue(id));
}
{
const atlas = group_cache_atlas_color(group_cache);
const id = atlas_debug_canvas(atlas);
document.getElementById("atlas-color-canvas").append(zjs.deleteValue(id));
}
//face_free(face);
});

View File

@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Ghostty Example</title>
<script type="module" src="app.ts"></script>
</head>
<body>
<p>Open your console, we are just debugging here.</p>
<p>The current <b>grayscale</b> font atlas is rendered below.</p>
<div><div id="atlas-canvas" style="display: inline-block; border: 1px solid green;"></div></div>
<p>The current <b>color</b> font atlas is rendered below.</p>
<div><div id="atlas-color-canvas" style="display: inline-block; border: 1px solid blue;"></div></div>
</body>
</html>

4436
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
{
"name": "ghostty example",
"version": "0.1.0",
"description": "Example showing ghostty and wasm.",
"source": "index.html",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"start": "parcel",
"build": "parcel build",
"check": "tsc --noEmit"
},
"author": "Mitchell Hashimoto",
"license": "MIT",
"devDependencies": {
"@parcel/transformer-inline-string": "^2.8.0",
"parcel": "^2.8.0",
"typescript": "^4.9.3"
},
"dependencies": {
"zig-js": "file:../vendor/zig-js/js"
}
}

14
example/zig-vt/README.md Normal file
View File

@ -0,0 +1,14 @@
# Example: `ghostty-vt` Zig Module
This contains a simple example of how to use the `ghostty-vt` Zig module
exported by Ghostty to have access to a production grade terminal emulator.
Requires the Zig version stated in the `build.zig.zon` file.
## Usage
Run the program:
```shell-session
zig build run
```

44
example/zig-vt/build.zig Normal file
View File

@ -0,0 +1,44 @@
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 test_step = b.step("test", "Run unit tests");
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
if (b.lazyDependency("ghostty", .{})) |dep| {
exe_mod.addImport(
"ghostty-vt",
dep.module("ghostty-vt"),
);
}
// Exe
const exe = b.addExecutable(.{
.name = "zig_vt",
.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);
// Test
const exe_unit_tests = b.addTest(.{
.root_module = exe_mod,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
test_step.dependOn(&run_exe_unit_tests.step);
}

View File

@ -0,0 +1,24 @@
.{
.name = .zig_vt,
.version = "0.0.0",
.fingerprint = 0x6045575a7a8387e6,
.minimum_zig_version = "0.14.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,26 @@
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
pub fn main() !void {
// Use a debug allocator so we get leak checking. You probably want
// to replace this for release builds.
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const alloc = gpa.allocator();
// Initialize a terminal.
var t: ghostty_vt.Terminal = try .init(alloc, .{
.cols = 6,
.rows = 40,
});
defer t.deinit(alloc);
// Write some text. It'll wrap because this is too long for our
// columns size above (6).
try t.printString("Hello, World!");
// Get the plain string view of the terminal screen.
const str = try t.plainString(alloc);
defer alloc.free(str);
std.debug.print("{s}\n", .{str});
}

View File

@ -8,6 +8,7 @@ const builtin = @import("builtin");
const ApprtRuntime = @import("../apprt/runtime.zig").Runtime;
const FontBackend = @import("../font/backend.zig").Backend;
const RendererBackend = @import("../renderer/backend.zig").Backend;
const TerminalBuildOptions = @import("../terminal/build_options.zig").Options;
const XCFramework = @import("GhosttyXCFramework.zig");
const WasmTarget = @import("../os/wasm/target.zig").Target;
const expandPath = @import("../os/path.zig").expand;
@ -37,6 +38,7 @@ font_backend: FontBackend = .freetype,
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
simd: bool = true,
i18n: bool = true,
wasm_shared: bool = true,
@ -173,6 +175,12 @@ pub fn init(b: *std.Build) !Config {
}
};
config.simd = b.option(
bool,
"simd",
"Build with SIMD-accelerated code paths. Results in significant performance improvements.",
) orelse true;
config.wayland = b.option(
bool,
"gtk-wayland",
@ -453,6 +461,7 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry);
step.addOption(bool, "simd", self.simd);
step.addOption(bool, "i18n", self.i18n);
step.addOption(ApprtRuntime, "app_runtime", self.app_runtime);
step.addOption(FontBackend, "font_backend", self.font_backend);
@ -482,6 +491,23 @@ 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 {
return .{
.artifact = .ghostty,
.simd = self.simd,
.oniguruma = true,
.slow_runtime_safety = switch (self.optimize) {
.Debug => true,
.ReleaseSafe,
.ReleaseSmall,
.ReleaseFast,
=> false,
},
};
}
/// Returns a baseline CPU target retaining all the other CPU configs.
pub fn baselineTarget(self: *const Config) std.Build.ResolvedTarget {
// Set our cpu model as baseline. There may need to be other modifications

49
src/build/GhosttyZig.zig Normal file
View File

@ -0,0 +1,49 @@
//! GhosttyZig generates the Zig modules that Ghostty exports
//! for downstream usage.
const GhosttyZig = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
vt: *std.Build.Module,
pub fn init(
b: *std.Build,
cfg: *const Config,
deps: *const SharedDeps,
) !GhosttyZig {
// General build options
const general_options = b.addOptions();
try cfg.addOptions(general_options);
// Terminal module build options
var vt_options = cfg.terminalOptions();
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
// conditionally do this.
vt_options.oniguruma = false;
const vt = b.addModule("ghostty-vt", .{
.root_source_file = b.path("src/lib_vt.zig"),
.target = cfg.target,
.optimize = cfg.optimize,
// SIMD require libc/libcpp (both) but otherwise we don't care.
.link_libc = if (cfg.simd) true else null,
.link_libcpp = if (cfg.simd) true else null,
});
vt.addOptions("build_options", general_options);
vt_options.add(b, vt);
// We always need unicode tables
deps.unicode_tables.addModuleImport(vt);
// If SIMD is enabled, add all our SIMD dependencies.
if (cfg.simd) {
try SharedDeps.addSimd(b, vt, null);
}
return .{ .vt = vt };
}

View File

@ -107,6 +107,9 @@ pub fn add(
// Every exe gets build options populated
step.root_module.addOptions("build_options", self.options);
// Every exe needs the terminal options
self.config.terminalOptions().add(b, step.root_module);
// Freetype
_ = b.systemIntegrationOption("freetype", .{}); // Shows it in help
if (self.config.font_backend.hasFreetype()) {
@ -266,21 +269,6 @@ pub fn add(
}
}
// Simdutf
if (b.systemIntegrationOption("simdutf", .{})) {
step.linkSystemLibrary2("simdutf", dynamic_link_opts);
} else {
if (b.lazyDependency("simdutf", .{
.target = target,
.optimize = optimize,
})) |simdutf_dep| {
step.linkLibrary(simdutf_dep.artifact("simdutf"));
try static_libs.append(
simdutf_dep.artifact("simdutf").getEmittedBin(),
);
}
}
// Sentry
if (self.config.sentry) {
if (b.lazyDependency("sentry", .{
@ -309,6 +297,13 @@ pub fn add(
}
}
// Simd
if (self.config.simd) try addSimd(
b,
step.root_module,
&static_libs,
);
// Wasm we do manually since it is such a different build.
if (step.rootModuleTarget().cpu.arch == .wasm32) {
if (b.lazyDependency("zig_js", .{
@ -343,35 +338,8 @@ pub fn add(
step.addIncludePath(b.path("src/apprt/gtk"));
}
// C++ files
// libcpp is required for various dependencies
step.linkLibCpp();
step.addIncludePath(b.path("src"));
{
// From hwy/detect_targets.h
const HWY_AVX3_SPR: c_int = 1 << 4;
const HWY_AVX3_ZEN4: c_int = 1 << 6;
const HWY_AVX3_DL: c_int = 1 << 7;
const HWY_AVX3: c_int = 1 << 8;
// Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414
// To workaround this we just disable AVX512 support completely.
// The performance difference between AVX2 and AVX512 is not
// significant for our use case and AVX512 is very rare on consumer
// hardware anyways.
const HWY_DISABLED_TARGETS: c_int = HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3;
step.addCSourceFiles(.{
.files = &.{
"src/simd/base64.cpp",
"src/simd/codepoint_width.cpp",
"src/simd/index_of.cpp",
"src/simd/vt.cpp",
},
.flags = if (step.rootModuleTarget().cpu.arch == .x86_64) &.{
b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}),
} else &.{},
});
}
// We always require the system SDK so that our system headers are available.
// This makes things like `os/log.h` available for cross-compiling.
@ -481,24 +449,6 @@ pub fn add(
try static_libs.append(cimgui_dep.artifact("cimgui").getEmittedBin());
}
// Highway
if (b.lazyDependency("highway", .{
.target = target,
.optimize = optimize,
})) |highway_dep| {
step.linkLibrary(highway_dep.artifact("highway"));
try static_libs.append(highway_dep.artifact("highway").getEmittedBin());
}
// utfcpp - This is used as a dependency on our hand-written C++ code
if (b.lazyDependency("utfcpp", .{
.target = target,
.optimize = optimize,
})) |utfcpp_dep| {
step.linkLibrary(utfcpp_dep.artifact("utfcpp"));
try static_libs.append(utfcpp_dep.artifact("utfcpp").getEmittedBin());
}
// Fonts
{
// JetBrains Mono
@ -700,6 +650,79 @@ fn addGtkNg(
}
}
/// Add only the dependencies required for `Config.simd` enbled. This also
/// adds all the simd source files for compilation.
pub fn addSimd(
b: *std.Build,
m: *std.Build.Module,
static_libs: ?*LazyPathList,
) !void {
const target = m.resolved_target.?;
const optimize = m.optimize.?;
// Simdutf
if (b.systemIntegrationOption("simdutf", .{})) {
m.linkSystemLibrary("simdutf", dynamic_link_opts);
} else {
if (b.lazyDependency("simdutf", .{
.target = target,
.optimize = optimize,
})) |simdutf_dep| {
m.linkLibrary(simdutf_dep.artifact("simdutf"));
if (static_libs) |v| try v.append(
simdutf_dep.artifact("simdutf").getEmittedBin(),
);
}
}
// Highway
if (b.lazyDependency("highway", .{
.target = target,
.optimize = optimize,
})) |highway_dep| {
m.linkLibrary(highway_dep.artifact("highway"));
if (static_libs) |v| try v.append(highway_dep.artifact("highway").getEmittedBin());
}
// utfcpp - This is used as a dependency on our hand-written C++ code
if (b.lazyDependency("utfcpp", .{
.target = target,
.optimize = optimize,
})) |utfcpp_dep| {
m.linkLibrary(utfcpp_dep.artifact("utfcpp"));
if (static_libs) |v| try v.append(utfcpp_dep.artifact("utfcpp").getEmittedBin());
}
// SIMD C++ files
m.addIncludePath(b.path("src"));
{
// From hwy/detect_targets.h
const HWY_AVX3_SPR: c_int = 1 << 4;
const HWY_AVX3_ZEN4: c_int = 1 << 6;
const HWY_AVX3_DL: c_int = 1 << 7;
const HWY_AVX3: c_int = 1 << 8;
// Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414
// To workaround this we just disable AVX512 support completely.
// The performance difference between AVX2 and AVX512 is not
// significant for our use case and AVX512 is very rare on consumer
// hardware anyways.
const HWY_DISABLED_TARGETS: c_int = HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3;
m.addCSourceFiles(.{
.files = &.{
"src/simd/base64.cpp",
"src/simd/codepoint_width.cpp",
"src/simd/index_of.cpp",
"src/simd/vt.cpp",
},
.flags = if (target.result.cpu.arch == .x86_64) &.{
b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}),
} else &.{},
});
}
}
/// Creates the resources that can be prebuilt for our dist build.
pub fn gtkNgDistResources(
b: *std.Build,

View File

@ -64,11 +64,19 @@ pub fn init(b: *std.Build) !UnicodeTables {
/// Add the "unicode_tables" import.
pub fn addImport(self: *const UnicodeTables, step: *std.Build.Step.Compile) void {
self.props_output.addStepDependencies(&step.step);
step.root_module.addAnonymousImport("unicode_tables", .{
self.symbols_output.addStepDependencies(&step.step);
self.addModuleImport(step.root_module);
}
/// Add the "unicode_tables" import to a module.
pub fn addModuleImport(
self: *const UnicodeTables,
module: *std.Build.Module,
) void {
module.addAnonymousImport("unicode_tables", .{
.root_source_file = self.props_output,
});
self.symbols_output.addStepDependencies(&step.step);
step.root_module.addAnonymousImport("symbols_tables", .{
module.addAnonymousImport("symbols_tables", .{
.root_source_file = self.symbols_output,
});
}

View File

@ -18,6 +18,7 @@ pub const GhosttyI18n = @import("GhosttyI18n.zig");
pub const GhosttyXcodebuild = @import("GhosttyXcodebuild.zig");
pub const GhosttyXCFramework = @import("GhosttyXCFramework.zig");
pub const GhosttyWebdata = @import("GhosttyWebdata.zig");
pub const GhosttyZig = @import("GhosttyZig.zig");
pub const HelpStrings = @import("HelpStrings.zig");
pub const SharedDeps = @import("SharedDeps.zig");
pub const UnicodeTables = @import("UnicodeTables.zig");

View File

@ -19,7 +19,6 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("../global.zig").state;
const fontpkg = @import("../font/main.zig");
const inputpkg = @import("../input.zig");
const terminal = @import("../terminal/main.zig");
const internal_os = @import("../os/main.zig");
const cli = @import("../cli.zig");
@ -39,6 +38,16 @@ const RepeatableStringMap = @import("RepeatableStringMap.zig");
pub const Path = @import("path.zig").Path;
pub const RepeatablePath = @import("path.zig").RepeatablePath;
// We do this instead of importing all of terminal/main.zig to
// limit the dependency graph. This is important because some things
// like the `ghostty-build-data` binary depend on the Config but don't
// want to include all the other stuff.
const terminal = struct {
const CursorStyle = @import("../terminal/cursor.zig").Style;
const color = @import("../terminal/color.zig");
const x11_color = @import("../terminal/x11_color.zig");
};
const log = std.log.scoped(.config);
/// Used on Unixes for some defaults.

View File

@ -2,13 +2,20 @@ const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
/// Same as std.mem.copyForwards but prefers libc memmove if it is available
/// because it is generally much faster.
/// Same as std.mem.copyForwards/Backwards but prefers libc memmove if it is
/// available because it is generally much faster.
pub inline fn move(comptime T: type, dest: []T, source: []const T) void {
if (builtin.link_libc) {
_ = memmove(dest.ptr, source.ptr, source.len * @sizeOf(T));
} else {
std.mem.copyForwards(T, dest, source);
// Depending on the ordering of the copy, we need to use the
// proper call here. Unfortunately this function call is
// too generic to know this at comptime.
if (@intFromPtr(dest.ptr) <= @intFromPtr(source.ptr)) {
std.mem.copyForwards(T, dest, source);
} else {
std.mem.copyBackwards(T, dest, source);
}
}
}

70
src/lib_vt.zig Normal file
View File

@ -0,0 +1,70 @@
//! This is the public API of the ghostty-vt Zig module.
//!
//! WARNING: The API is not guaranteed to be stable.
//!
//! The functionality is extremely stable, since it is extracted
//! directly from Ghostty which has been used in real world scenarios
//! by thousands of users for years. However, the API itself (functions,
//! types, etc.) may change without warning. We're working on stabilizing
//! this in the future.
// The public API below reproduces a lot of terminal/main.zig but
// is separate because (1) we need our root file to be in `src/`
// so we can access other directories and (2) we may want to withhold
// parts of `terminal` that are not ready for public consumption
// or are too Ghostty-internal.
const terminal = @import("terminal/main.zig");
pub const apc = terminal.apc;
pub const dcs = terminal.dcs;
pub const osc = terminal.osc;
pub const point = terminal.point;
pub const color = terminal.color;
pub const device_status = terminal.device_status;
pub const kitty = terminal.kitty;
pub const modes = terminal.modes;
pub const page = terminal.page;
pub const parse_table = terminal.parse_table;
pub const search = terminal.search;
pub const size = terminal.size;
pub const x11_color = terminal.x11_color;
pub const Charset = terminal.Charset;
pub const CharsetSlot = terminal.Slots;
pub const CharsetActiveSlot = terminal.ActiveSlot;
pub const Cell = page.Cell;
pub const Coordinate = point.Coordinate;
pub const CSI = Parser.Action.CSI;
pub const DCS = Parser.Action.DCS;
pub const MouseShape = terminal.MouseShape;
pub const Page = page.Page;
pub const PageList = terminal.PageList;
pub const Parser = terminal.Parser;
pub const Pin = PageList.Pin;
pub const Point = point.Point;
pub const Screen = terminal.Screen;
pub const ScreenType = Terminal.ScreenType;
pub const Selection = terminal.Selection;
pub const SizeReportStyle = terminal.SizeReportStyle;
pub const StringMap = terminal.StringMap;
pub const Style = terminal.Style;
pub const Terminal = terminal.Terminal;
pub const Stream = terminal.Stream;
pub const Cursor = Screen.Cursor;
pub const CursorStyle = Screen.CursorStyle;
pub const CursorStyleReq = terminal.CursorStyle;
pub const DeviceAttributeReq = terminal.DeviceAttributeReq;
pub const Mode = modes.Mode;
pub const ModePacked = modes.ModePacked;
pub const ModifyKeyFormat = terminal.ModifyKeyFormat;
pub const ProtectedMode = terminal.ProtectedMode;
pub const StatusLineType = terminal.StatusLineType;
pub const StatusDisplay = terminal.StatusDisplay;
pub const EraseDisplay = terminal.EraseDisplay;
pub const EraseLine = terminal.EraseLine;
pub const TabClear = terminal.TabClear;
pub const Attribute = terminal.Attribute;
test {
_ = terminal;
}

View File

@ -6,7 +6,8 @@ const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
const Dir = std.fs.Dir;
const internal_os = @import("main.zig");
const allocTmpDir = @import("file.zig").allocTmpDir;
const freeTmpDir = @import("file.zig").freeTmpDir;
const log = std.log.scoped(.tempdir);
@ -31,8 +32,8 @@ pub fn init() !TempDir {
const dir = dir: {
const cwd = std.fs.cwd();
const tmp_dir = internal_os.allocTmpDir(std.heap.page_allocator) orelse break :dir cwd;
defer internal_os.freeTmpDir(std.heap.page_allocator, tmp_dir);
const tmp_dir = allocTmpDir(std.heap.page_allocator) orelse break :dir cwd;
defer freeTmpDir(std.heap.page_allocator, tmp_dir);
break :dir try cwd.openDir(tmp_dir, .{});
};

View File

@ -1,4 +1,62 @@
const std = @import("std");
const options = @import("build_options");
const assert = std.debug.assert;
const scalar_decoder = @import("base64_scalar.zig").scalar_decoder;
const log = std.log.scoped(.simd_base64);
pub fn maxLen(input: []const u8) usize {
if (comptime options.simd) return ghostty_simd_base64_max_length(
input.ptr,
input.len,
);
return maxLenScalar(input);
}
fn maxLenScalar(input: []const u8) usize {
return scalar_decoder.calcSizeForSlice(scalarInput(input)) catch |err| {
log.warn("failed to calculate base64 size for payload: {}", .{err});
return 0;
};
}
pub fn decode(input: []const u8, output: []u8) error{Base64Invalid}![]const u8 {
if (comptime options.simd) {
const res = ghostty_simd_base64_decode(
input.ptr,
input.len,
output.ptr,
);
if (res < 0) return error.Base64Invalid;
return output[0..@intCast(res)];
}
return decodeScalar(input, output);
}
fn decodeScalar(
input_raw: []const u8,
output: []u8,
) error{Base64Invalid}![]const u8 {
const input = scalarInput(input_raw);
const size = maxLenScalar(input);
if (size == 0) return "";
assert(output.len >= size);
scalar_decoder.decode(
output,
scalarInput(input),
) catch return error.Base64Invalid;
return output[0..size];
}
/// For non-SIMD enabled builds, we trim the padding from the end of the
/// base64 input in order to get identical output with the SIMD version.
fn scalarInput(input: []const u8) []const u8 {
var i: usize = 0;
while (input[input.len - i - 1] == '=') i += 1;
return input[0 .. input.len - i];
}
// base64.cpp
extern "c" fn ghostty_simd_base64_max_length(
@ -11,16 +69,6 @@ extern "c" fn ghostty_simd_base64_decode(
output: [*]u8,
) isize;
pub fn maxLen(input: []const u8) usize {
return ghostty_simd_base64_max_length(input.ptr, input.len);
}
pub fn decode(input: []const u8, output: []u8) error{Base64Invalid}![]const u8 {
const res = ghostty_simd_base64_decode(input.ptr, input.len, output.ptr);
if (res < 0) return error.Base64Invalid;
return output[0..@intCast(res)];
}
test "base64 maxLen" {
const testing = std.testing;
const len = maxLen("aGVsbG8gd29ybGQ=");

147
src/simd/base64_scalar.zig Normal file
View File

@ -0,0 +1,147 @@
const std = @import("std");
const assert = std.debug.assert;
pub const scalar_decoder: Base64Decoder = .init(
std.base64.standard_alphabet_chars,
null,
);
/// Copied from Zig 0.14.1 stdlib and commented out the invalid padding
/// scenarios, because Kitty Graphics requires a decoder that doesn't care
/// about invalid padding scenarios.
const Base64Decoder = struct {
const invalid_char: u8 = 0xff;
const invalid_char_tst: u32 = 0xff000000;
const Error = error{
InvalidCharacter,
InvalidPadding,
NoSpaceLeft,
};
/// e.g. 'A' => 0.
/// `invalid_char` for any value not in the 64 alphabet chars.
char_to_index: [256]u8,
fast_char_to_index: [4][256]u32,
pad_char: ?u8,
pub fn init(alphabet_chars: [64]u8, pad_char: ?u8) Base64Decoder {
var result = Base64Decoder{
.char_to_index = [_]u8{invalid_char} ** 256,
.fast_char_to_index = .{[_]u32{invalid_char_tst} ** 256} ** 4,
.pad_char = pad_char,
};
var char_in_alphabet = [_]bool{false} ** 256;
for (alphabet_chars, 0..) |c, i| {
assert(!char_in_alphabet[c]);
assert(pad_char == null or c != pad_char.?);
const ci = @as(u32, @intCast(i));
result.fast_char_to_index[0][c] = ci << 2;
result.fast_char_to_index[1][c] = (ci >> 4) | ((ci & 0x0f) << 12);
result.fast_char_to_index[2][c] = ((ci & 0x3) << 22) | ((ci & 0x3c) << 6);
result.fast_char_to_index[3][c] = ci << 16;
result.char_to_index[c] = @as(u8, @intCast(i));
char_in_alphabet[c] = true;
}
return result;
}
/// Return the maximum possible decoded size for a given input length - The actual length may be less if the input includes padding.
/// `InvalidPadding` is returned if the input length is not valid.
pub fn calcSizeUpperBound(decoder: *const Base64Decoder, source_len: usize) Error!usize {
var result = source_len / 4 * 3;
const leftover = source_len % 4;
if (decoder.pad_char != null) {
if (leftover % 4 != 0) return error.InvalidPadding;
} else {
if (leftover % 4 == 1) return error.InvalidPadding;
result += leftover * 3 / 4;
}
return result;
}
/// Return the exact decoded size for a slice.
/// `InvalidPadding` is returned if the input length is not valid.
pub fn calcSizeForSlice(decoder: *const Base64Decoder, source: []const u8) Error!usize {
const source_len = source.len;
var result = try decoder.calcSizeUpperBound(source_len);
if (decoder.pad_char) |pad_char| {
if (source_len >= 1 and source[source_len - 1] == pad_char) result -= 1;
if (source_len >= 2 and source[source_len - 2] == pad_char) result -= 1;
}
return result;
}
/// dest.len must be what you get from ::calcSize.
/// Invalid characters result in `error.InvalidCharacter`.
/// Invalid padding results in `error.InvalidPadding`.
pub fn decode(decoder: *const Base64Decoder, dest: []u8, source: []const u8) Error!void {
if (decoder.pad_char != null and source.len % 4 != 0) return error.InvalidPadding;
var dest_idx: usize = 0;
var fast_src_idx: usize = 0;
var acc: u12 = 0;
var acc_len: u4 = 0;
var leftover_idx: ?usize = null;
while (fast_src_idx + 16 < source.len and dest_idx + 15 < dest.len) : ({
fast_src_idx += 16;
dest_idx += 12;
}) {
var bits: u128 = 0;
inline for (0..4) |i| {
var new_bits: u128 = decoder.fast_char_to_index[0][source[fast_src_idx + i * 4]];
new_bits |= decoder.fast_char_to_index[1][source[fast_src_idx + 1 + i * 4]];
new_bits |= decoder.fast_char_to_index[2][source[fast_src_idx + 2 + i * 4]];
new_bits |= decoder.fast_char_to_index[3][source[fast_src_idx + 3 + i * 4]];
if ((new_bits & invalid_char_tst) != 0) return error.InvalidCharacter;
bits |= (new_bits << (24 * i));
}
std.mem.writeInt(u128, dest[dest_idx..][0..16], bits, .little);
}
while (fast_src_idx + 4 < source.len and dest_idx + 3 < dest.len) : ({
fast_src_idx += 4;
dest_idx += 3;
}) {
var bits = decoder.fast_char_to_index[0][source[fast_src_idx]];
bits |= decoder.fast_char_to_index[1][source[fast_src_idx + 1]];
bits |= decoder.fast_char_to_index[2][source[fast_src_idx + 2]];
bits |= decoder.fast_char_to_index[3][source[fast_src_idx + 3]];
if ((bits & invalid_char_tst) != 0) return error.InvalidCharacter;
std.mem.writeInt(u32, dest[dest_idx..][0..4], bits, .little);
}
const remaining = source[fast_src_idx..];
for (remaining, fast_src_idx..) |c, src_idx| {
const d = decoder.char_to_index[c];
if (d == invalid_char) {
if (decoder.pad_char == null or c != decoder.pad_char.?) return error.InvalidCharacter;
leftover_idx = src_idx;
break;
}
acc = (acc << 6) + d;
acc_len += 6;
if (acc_len >= 8) {
acc_len -= 8;
dest[dest_idx] = @as(u8, @truncate(acc >> acc_len));
dest_idx += 1;
}
}
// if (acc_len > 4 or (acc & (@as(u12, 1) << acc_len) - 1) != 0) {
// return error.InvalidPadding;
// }
if (leftover_idx == null) return;
const leftover = source[leftover_idx.?..];
if (decoder.pad_char) |pad_char| {
const padding_len = acc_len / 2;
var padding_chars: usize = 0;
for (leftover) |c| {
if (c != pad_char) {
return if (c == Base64Decoder.invalid_char) error.InvalidCharacter else error.InvalidPadding;
}
padding_chars += 1;
}
if (padding_chars != padding_len) return error.InvalidPadding;
}
}
};

View File

@ -1,11 +1,12 @@
const std = @import("std");
const options = @import("build_options");
// vt.cpp
extern "c" fn ghostty_simd_codepoint_width(u32) i8;
pub fn codepointWidth(cp: u32) i8 {
//return @import("ziglyph").display_width.codePointWidth(@intCast(cp), .half);
return ghostty_simd_codepoint_width(cp);
if (comptime options.simd) return ghostty_simd_codepoint_width(cp);
return @import("ziglyph").display_width.codePointWidth(@intCast(cp), .half);
}
test "codepointWidth basic" {

View File

@ -1,5 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const options = @import("build_options");
extern "c" fn ghostty_simd_index_of(
needle: u8,
@ -8,8 +9,16 @@ extern "c" fn ghostty_simd_index_of(
) usize;
pub fn indexOf(input: []const u8, needle: u8) ?usize {
const result = ghostty_simd_index_of(needle, input.ptr, input.len);
return if (result == input.len) null else result;
if (comptime options.simd) {
const result = ghostty_simd_index_of(needle, input.ptr, input.len);
return if (result == input.len) null else result;
}
return indexOfScalar(input, needle);
}
fn indexOfScalar(input: []const u8, needle: u8) ?usize {
return std.mem.indexOfScalar(u8, input, needle);
}
test "indexOf" {

View File

@ -1,3 +1,6 @@
//! SIMD-optimized routines. If `build_options.simd` is false, then the API
//! still works but we fall back to pure Zig scalar implementations.
const std = @import("std");
const codepoint_width = @import("codepoint_width.zig");

View File

@ -1,4 +1,7 @@
const std = @import("std");
const options = @import("build_options");
const assert = std.debug.assert;
const indexOf = @import("index_of.zig").indexOf;
// vt.cpp
extern "c" fn ghostty_simd_decode_utf8_until_control_seq(
@ -17,15 +20,68 @@ pub fn utf8DecodeUntilControlSeq(
input: []const u8,
output: []u32,
) DecodeResult {
var decoded: usize = 0;
const consumed = ghostty_simd_decode_utf8_until_control_seq(
input.ptr,
input.len,
output.ptr,
&decoded,
);
assert(output.len >= input.len);
return .{ .consumed = consumed, .decoded = decoded };
if (comptime options.simd) {
var decoded: usize = 0;
const consumed = ghostty_simd_decode_utf8_until_control_seq(
input.ptr,
input.len,
output.ptr,
&decoded,
);
return .{ .consumed = consumed, .decoded = decoded };
}
return utf8DecodeUntilControlSeqScalar(input, output);
}
fn utf8DecodeUntilControlSeqScalar(
input: []const u8,
output: []u32,
) DecodeResult {
// Find our escape
const idx = indexOf(input, 0x1B) orelse input.len;
const decode = input[0..idx];
// Go through and decode one item at a time.
var decode_offset: usize = 0;
var decode_count: usize = 0;
while (decode_offset < decode.len) {
const decode_rem = decode[decode_offset..];
const cp_len = std.unicode.utf8ByteSequenceLength(decode_rem[0]) catch {
// Note, this is matching our SIMD behavior, but it is admittedly
// a bit weird. See our "decode invalid leading byte" test too.
// SIMD should be our source of truth then we copy behavior here.
break;
};
// If we don't have that number of bytes available. we finish. We
// assume this is a partial input and we defer to the future.
if (decode_rem.len < cp_len) break;
// We have the bytes available, so move forward
const cp_bytes = decode_rem[0..cp_len];
decode_offset += cp_len;
if (std.unicode.utf8Decode(cp_bytes)) |cp| {
output[decode_count] = @intCast(cp);
decode_count += 1;
} else |_| {
// If decoding failed, we replace the leading byte with the
// replacement char and then continue decoding after that
// byte. This matches the SIMD behavior and is tested by the
// "invalid UTF-8" tests.
output[decode_count] = 0xFFFD;
decode_count += 1;
decode_offset -= cp_len - 1;
}
}
return .{
.consumed = decode_offset,
.decoded = decode_count,
};
}
test "decode no escape" {
@ -108,16 +164,18 @@ test "decode invalid UTF-8" {
var output: [64]u32 = undefined;
// Invalid leading 1s
// Invalid leading 2-byte sequence
{
const str = "hello\xc2\x00";
const str = "hello\xc2\x01";
try testing.expectEqual(DecodeResult{
.consumed = 7,
.decoded = 7,
}, utf8DecodeUntilControlSeq(str, &output));
}
// Replacement will only replace the invalid leading byte.
try testing.expectEqual(@as(u32, 0xFFFD), output[5]);
try testing.expectEqual(@as(u32, 0x01), output[6]);
}
// This is testing our current behavior so that we know we have to handle

View File

@ -4,7 +4,7 @@
const PageList = @This();
const std = @import("std");
const build_config = @import("../build_config.zig");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const fastmem = @import("../fastmem.zig");
@ -1153,9 +1153,11 @@ const ReflowCursor = struct {
self.page_cell.style_id = id;
}
// Copy Kitty virtual placeholder status
if (cell.codepoint() == kitty.graphics.unicode.placeholder) {
self.page_row.kitty_virtual_placeholder = true;
if (comptime build_options.kitty_graphics) {
// Copy Kitty virtual placeholder status
if (cell.codepoint() == kitty.graphics.unicode.placeholder) {
self.page_row.kitty_virtual_placeholder = true;
}
}
self.cursorForward();
@ -1492,7 +1494,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
},
}
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
assert(self.totalRows() >= self.rows);
}
}
@ -2524,7 +2526,7 @@ pub fn pin(self: *const PageList, pt: point.Point) ?Pin {
/// pin points to is removed completely, the tracked pin will be updated
/// to the top-left of the screen.
pub fn trackPin(self: *PageList, p: Pin) Allocator.Error!*Pin {
if (build_config.slow_runtime_safety) assert(self.pinIsValid(p));
if (build_options.slow_runtime_safety) assert(self.pinIsValid(p));
// Create our tracked pin
const tracked = try self.pool.pins.create();
@ -2556,7 +2558,7 @@ pub fn countTrackedPins(self: *const PageList) usize {
pub fn pinIsValid(self: *const PageList, p: Pin) bool {
// This is very slow so we want to ensure we only ever
// call this during slow runtime safety builds.
comptime assert(build_config.slow_runtime_safety);
comptime assert(build_options.slow_runtime_safety);
var it = self.pages.first;
while (it) |node| : (it = node.next) {
@ -3234,7 +3236,7 @@ pub fn pageIterator(
else
self.getBottomRight(tl_pt) orelse return .{ .row = null };
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
assert(tl_pin.eql(bl_pin) or tl_pin.before(bl_pin));
}
@ -3510,7 +3512,7 @@ pub const Pin = struct {
direction: Direction,
limit: ?Pin,
) PageIterator {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
if (limit) |l| {
// Check the order according to the iteration direction.
switch (direction) {
@ -3560,7 +3562,7 @@ pub const Pin = struct {
// Note: this is primarily unit tested as part of the Kitty
// graphics deletion code.
pub fn isBetween(self: Pin, top: Pin, bottom: Pin) bool {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
if (top.node == bottom.node) {
// If top is bottom, must be ordered.
assert(top.y <= bottom.y);
@ -8917,6 +8919,8 @@ test "PageList resize reflow less cols to wrap a multi-codepoint grapheme with a
}
test "PageList resize reflow less cols copy kitty placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@ -8956,6 +8960,8 @@ test "PageList resize reflow less cols copy kitty placeholder" {
}
test "PageList resize reflow more cols clears kitty placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@ -8997,6 +9003,8 @@ test "PageList resize reflow more cols clears kitty placeholder" {
}
test "PageList resize reflow wrap moves kitty placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -1,7 +1,7 @@
const Screen = @This();
const std = @import("std");
const build_config = @import("../build_config.zig");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const ansi = @import("ansi.zig");
@ -24,6 +24,8 @@ const Row = pagepkg.Row;
const Cell = pagepkg.Cell;
const Pin = PageList.Pin;
pub const CursorStyle = @import("cursor.zig").Style;
const log = std.log.scoped(.screen);
/// The general purpose allocator to use for all memory allocations.
@ -64,7 +66,10 @@ protected_mode: ansi.ProtectedMode = .off,
kitty_keyboard: kitty.KeyFlagStack = .{},
/// Kitty graphics protocol state.
kitty_images: kitty.graphics.ImageStorage = .{},
kitty_images: if (build_options.kitty_graphics)
kitty.graphics.ImageStorage
else
struct {} = .{},
/// Dirty flags for the renderer.
dirty: Dirty = .{},
@ -141,22 +146,6 @@ pub const Cursor = struct {
}
};
/// The visual style of the cursor. Whether or not it blinks
/// is determined by mode 12 (modes.zig). This mode is synchronized
/// with CSI q, the same as xterm.
pub const CursorStyle = enum {
bar, // DECSCUSR 5, 6
block, // DECSCUSR 1, 2
underline, // DECSCUSR 3, 4
/// The cursor styles below aren't known by DESCUSR and are custom
/// implemented in Ghostty. They are reported as some standard style
/// if requested, though.
/// Hollow block cursor. This is a block cursor with the center empty.
/// Reported as DECSCUSR 1 or 2 (block).
block_hollow,
};
/// Saved cursor state.
pub const SavedCursor = struct {
x: size.CellCountInt,
@ -222,7 +211,9 @@ pub fn init(
}
pub fn deinit(self: *Screen) void {
self.kitty_images.deinit(self.alloc, self);
if (comptime build_options.kitty_graphics) {
self.kitty_images.deinit(self.alloc, self);
}
self.cursor.deinit(self.alloc);
self.pages.deinit();
}
@ -232,7 +223,7 @@ pub fn deinit(self: *Screen) void {
/// tests. This only asserts the screen specific data so callers should
/// ensure they're also calling page integrity checks if necessary.
pub fn assertIntegrity(self: *const Screen) void {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
// We don't run integrity checks on Valgrind because its soooooo slow,
// Valgrind is our integrity checker, and we run these during unit
// tests (non-Valgrind) anyways so we're verifying anyways.
@ -283,9 +274,11 @@ pub fn reset(self: *Screen) void {
.page_cell = cursor_rac.cell,
};
// Reset kitty graphics storage
self.kitty_images.deinit(self.alloc, self);
self.kitty_images = .{ .dirty = true };
if (comptime build_options.kitty_graphics) {
// Reset kitty graphics storage
self.kitty_images.deinit(self.alloc, self);
self.kitty_images = .{ .dirty = true };
}
// Reset our basic state
self.saved_cursor = null;
@ -704,8 +697,10 @@ pub fn cursorDownScroll(self: *Screen) !void {
assert(self.cursor.y == self.pages.rows - 1);
defer self.assertIntegrity();
// Scrolling dirties the images because it updates their placements pins.
self.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Scrolling dirties the images because it updates their placements pins.
self.kitty_images.dirty = true;
}
// If we have no scrollback, then we shift all our rows instead.
if (self.no_scrollback) {
@ -772,7 +767,7 @@ pub fn cursorDownScroll(self: *Screen) !void {
// These assertions help catch some pagelist math errors. Our
// x/y should be unchanged after the grow.
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
const active = self.pages.pointFromPin(
.active,
page_pin,
@ -1168,10 +1163,12 @@ pub const Scroll = union(enum) {
pub fn scroll(self: *Screen, behavior: Scroll) void {
defer self.assertIntegrity();
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
}
switch (behavior) {
.active => self.pages.scroll(.{ .active = {} }),
@ -1190,10 +1187,12 @@ pub fn scrollClear(self: *Screen) !void {
try self.pages.scrollClear();
self.cursorReload();
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// No matter what, scrolling marks our image state as dirty since
// it could move placements. If there are no placements or no images
// this is still a very cheap operation.
self.kitty_images.dirty = true;
}
}
/// Returns true if the viewport is scrolled to the bottom of the screen.
@ -1313,14 +1312,16 @@ pub fn clearCells(
if (cells.len == self.pages.cols) row.styled = false;
}
if (row.kitty_virtual_placeholder and
cells.len == self.pages.cols)
{
for (cells) |c| {
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
break;
}
} else row.kitty_virtual_placeholder = false;
if (comptime build_options.kitty_graphics) {
if (row.kitty_virtual_placeholder and
cells.len == self.pages.cols)
{
for (cells) |c| {
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
break;
}
} else row.kitty_virtual_placeholder = false;
}
}
@memset(cells, self.blankCell());
@ -1584,8 +1585,10 @@ fn resizeInternal(
) !void {
defer self.assertIntegrity();
// No matter what we mark our image state as dirty
self.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// No matter what we mark our image state as dirty
self.kitty_images.dirty = true;
}
// Release the cursor style while resizing just
// in case the cursor ends up on a different page.

View File

@ -3,6 +3,7 @@
const StringMap = @This();
const std = @import("std");
const build_options = @import("terminal_options");
const oni = @import("oniguruma");
const point = @import("point.zig");
const Selection = @import("Selection.zig");
@ -19,7 +20,13 @@ pub fn deinit(self: StringMap, alloc: Allocator) void {
}
/// Returns an iterator that yields the next match of the given regex.
pub fn searchIterator(
/// Requires Ghostty to be compiled with regex support.
pub const searchIterator = if (build_options.oniguruma)
searchIteratorOni
else
void;
fn searchIteratorOni(
self: StringMap,
regex: oni.Regex,
) SearchIterator {
@ -85,6 +92,8 @@ pub const Match = struct {
};
test "StringMap searchIterator" {
if (comptime !build_options.oniguruma) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -4,6 +4,7 @@
const Terminal = @This();
const std = @import("std");
const build_options = @import("terminal_options");
const builtin = @import("builtin");
const assert = std.debug.assert;
const testing = std.testing;
@ -679,8 +680,10 @@ fn printCell(
// If this is a Kitty unicode placeholder then we need to mark the
// row so that the renderer can lookup rows with these much faster.
if (c == kitty.graphics.unicode.placeholder) {
self.screen.cursor.page_row.kitty_virtual_placeholder = true;
if (comptime build_options.kitty_graphics) {
if (c == kitty.graphics.unicode.placeholder) {
self.screen.cursor.page_row.kitty_virtual_placeholder = true;
}
}
// We check for an active hyperlink first because setHyperlink
@ -1143,8 +1146,10 @@ pub fn index(self: *Terminal) !void {
self.screen.cursor.x >= self.scrolling_region.left and
self.screen.cursor.x <= self.scrolling_region.right)
{
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
}
// If our scrolling region is at the top, we create scrollback.
if (self.scrolling_region.top == 0 and
@ -1472,8 +1477,10 @@ pub fn insertLines(self: *Terminal, count: usize) void {
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
}
// At the end we need to return the cursor to the row it started on.
const start_y = self.screen.cursor.y;
@ -1676,8 +1683,10 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Scrolling dirties the images because it updates their placements pins.
self.screen.kitty_images.dirty = true;
}
// At the end we need to return the cursor to the row it started on.
const start_y = self.screen.cursor.y;
@ -2136,12 +2145,14 @@ pub fn eraseDisplay(
// Unsets pending wrap state
self.screen.cursor.pending_wrap = false;
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
if (comptime build_options.kitty_graphics) {
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
}
},
.complete => {
@ -2195,12 +2206,14 @@ pub fn eraseDisplay(
// Unsets pending wrap state
self.screen.cursor.pending_wrap = false;
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
if (comptime build_options.kitty_graphics) {
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
}
// Cleared screen dirty bit
self.flags.dirty.clear = true;
@ -2574,10 +2587,12 @@ pub fn switchScreen(self: *Terminal, t: ScreenType) ?*Screen {
// Clear our selection
self.screen.clearSelection();
// Mark kitty images as dirty so they redraw. Without this set
// the images will remain where they were (the dirty bit on
// the screen only tracks the terminal grid, not the images).
self.screen.kitty_images.dirty = true;
if (comptime build_options.kitty_graphics) {
// Mark kitty images as dirty so they redraw. Without this set
// the images will remain where they were (the dirty bit on
// the screen only tracks the terminal grid, not the images).
self.screen.kitty_images.dirty = true;
}
// Mark our terminal as dirty to redraw the grid.
self.flags.dirty.clear = true;
@ -3862,6 +3877,8 @@ test "Terminal: print invoke charset single" {
}
test "Terminal: print kitty unicode placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);

View File

@ -1,4 +1,5 @@
const std = @import("std");
const build_options = @import("terminal_options");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
@ -33,17 +34,22 @@ pub const Handler = struct {
.identify => {
switch (byte) {
// Kitty graphics protocol
'G' => self.state = .{ .kitty = kitty_gfx.CommandParser.init(alloc) },
'G' => self.state = if (comptime build_options.kitty_graphics)
.{ .kitty = kitty_gfx.CommandParser.init(alloc) }
else
.{ .ignore = {} },
// Unknown
else => self.state = .{ .ignore = {} },
}
},
.kitty => |*p| p.feed(byte) catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
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 = {} };
};
} else unreachable,
}
}
@ -57,6 +63,8 @@ pub const Handler = struct {
.inactive => unreachable,
.ignore, .identify => null,
.kitty => |*p| kitty: {
if (comptime !build_options.kitty_graphics) unreachable;
const command = p.complete() catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
break :kitty null;
@ -81,23 +89,35 @@ pub const State = union(enum) {
identify: void,
/// Kitty graphics protocol
kitty: kitty_gfx.CommandParser,
kitty: if (build_options.kitty_graphics)
kitty_gfx.CommandParser
else
void,
pub fn deinit(self: *State) void {
switch (self.*) {
.inactive, .ignore, .identify => {},
.kitty => |*v| v.deinit(),
.kitty => |*v| if (comptime build_options.kitty_graphics)
v.deinit()
else
unreachable,
}
}
};
/// Possible APC commands.
pub const Command = union(enum) {
kitty: kitty_gfx.Command,
kitty: if (build_options.kitty_graphics)
kitty_gfx.Command
else
void,
pub fn deinit(self: *Command, alloc: Allocator) void {
switch (self.*) {
.kitty => |*v| v.deinit(alloc),
.kitty => |*v| if (comptime build_options.kitty_graphics)
v.deinit(alloc)
else
unreachable,
}
}
};
@ -113,6 +133,8 @@ test "unknown APC command" {
}
test "garbage Kitty command" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@ -123,6 +145,8 @@ test "garbage Kitty command" {
}
test "Kitty command with overflow u32" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@ -133,6 +157,8 @@ test "Kitty command with overflow u32" {
}
test "Kitty command with overflow i32" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@ -143,6 +169,8 @@ test "Kitty command with overflow i32" {
}
test "valid Kitty command" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -0,0 +1,52 @@
const std = @import("std");
pub const Options = struct {
/// The target artifact to build. This will gate some functionality.
artifact: Artifact,
/// Whether Oniguruma regex support is available. If this isn't
/// available, some features will be disabled. This may be outdated,
/// but the specific disabled features are:
///
/// - Kitty graphics protocol
/// - Tmux control mode
///
oniguruma: bool,
/// Whether to build SIMD-accelerated code paths. This pulls in more
/// build-time dependencies and adds libc as a runtime dependency,
/// but results in significant performance improvements.
simd: bool,
/// True if we should enable the "slow" runtime safety checks. These
/// are runtime safety checks that are slower than typical and should
/// generally be disabled in production builds.
slow_runtime_safety: bool,
/// Add the required build options for the terminal module.
pub fn add(
self: Options,
b: *std.Build,
m: *std.Build.Module,
) void {
const opts = b.addOptions();
opts.addOption(Artifact, "artifact", self.artifact);
opts.addOption(bool, "oniguruma", self.oniguruma);
opts.addOption(bool, "simd", self.simd);
opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety);
// These are synthesized based on other options.
opts.addOption(bool, "kitty_graphics", self.oniguruma);
opts.addOption(bool, "tmux_control_mode", self.oniguruma);
m.addOptions("terminal_options", opts);
}
};
pub const Artifact = enum {
/// Ghostty application
ghostty,
/// libghostty-vt, Zig module
lib,
};

15
src/terminal/cursor.zig Normal file
View File

@ -0,0 +1,15 @@
/// The visual style of the cursor. Whether or not it blinks
/// is determined by mode 12 (modes.zig). This mode is synchronized
/// with CSI q, the same as xterm.
pub const Style = enum {
bar, // DECSCUSR 5, 6
block, // DECSCUSR 1, 2
underline, // DECSCUSR 3, 4
/// The cursor styles below aren't known by DESCUSR and are custom
/// implemented in Ghostty. They are reported as some standard style
/// if requested, though.
/// Hollow block cursor. This is a block cursor with the center empty.
/// Reported as DECSCUSR 1 or 2 (block).
block_hollow,
};

View File

@ -1,4 +1,5 @@
const std = @import("std");
const build_options = @import("terminal_options");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const terminal = @import("main.zig");
@ -51,6 +52,11 @@ pub const Handler = struct {
0 => switch (dcs.final) {
// Tmux control mode
'p' => tmux: {
if (comptime !build_options.tmux_control_mode) {
log.debug("tmux control mode not enabled in build, ignoring", .{});
break :tmux null;
}
// Tmux control mode must start with ESC P 1000 p
if (dcs.params.len != 1 or dcs.params[0] != 1000) break :tmux null;
@ -121,9 +127,11 @@ pub const Handler = struct {
.ignore,
=> {},
.tmux => |*tmux| return .{
.tmux = (try tmux.put(byte)) orelse return null,
},
.tmux => |*tmux| if (comptime build_options.tmux_control_mode) {
return .{
.tmux = (try tmux.put(byte)) orelse return null,
};
} else unreachable,
.xtgettcap => |*list| {
if (list.items.len >= self.max_bytes) {
@ -157,10 +165,10 @@ pub const Handler = struct {
.ignore,
=> null,
.tmux => tmux: {
.tmux => if (comptime build_options.tmux_control_mode) tmux: {
self.state.deinit();
break :tmux .{ .tmux = .{ .exit = {} } };
},
} else unreachable,
.xtgettcap => |list| xtgettcap: {
for (list.items, 0..) |b, i| {
@ -203,7 +211,10 @@ pub const Command = union(enum) {
decrqss: DECRQSS,
/// Tmux control mode
tmux: terminal.tmux.Notification,
tmux: if (build_options.tmux_control_mode)
terminal.tmux.Notification
else
void,
pub fn deinit(self: Command) void {
switch (self) {
@ -269,7 +280,10 @@ const State = union(enum) {
},
/// Tmux control mode: https://github.com/tmux/tmux/wiki/Control-Mode
tmux: terminal.tmux.Client,
tmux: if (build_options.tmux_control_mode)
terminal.tmux.Client
else
void,
pub fn deinit(self: *State) void {
switch (self.*) {
@ -279,7 +293,9 @@ const State = union(enum) {
.xtgettcap => |*v| v.deinit(),
.decrqss => {},
.tmux => |*v| v.deinit(),
.tmux => |*v| if (comptime build_options.tmux_control_mode) {
v.deinit();
} else unreachable,
}
}
};
@ -395,6 +411,8 @@ test "DECRQSS invalid command" {
}
test "tmux enter and implicit exit" {
if (comptime !build_options.tmux_control_mode) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -1,8 +1,10 @@
//! Types and functions related to Kitty protocols.
const build_options = @import("terminal_options");
const key = @import("kitty/key.zig");
pub const color = @import("kitty/color.zig");
pub const graphics = @import("kitty/graphics.zig");
pub const graphics = if (build_options.kitty_graphics) @import("kitty/graphics.zig") else struct {};
pub const KeyFlags = key.Flags;
pub const KeyFlagStack = key.FlagStack;

View File

@ -9,9 +9,14 @@ const fastmem = @import("../../fastmem.zig");
const command = @import("graphics_command.zig");
const point = @import("../point.zig");
const PageList = @import("../PageList.zig");
const internal_os = @import("../../os/main.zig");
const wuffs = @import("wuffs");
const temp_dir = struct {
const TempDir = @import("../../os/TempDir.zig");
const allocTmpDir = @import("../../os/file.zig").allocTmpDir;
const freeTmpDir = @import("../../os/file.zig").freeTmpDir;
};
const log = std.log.scoped(.kitty_gfx);
/// Maximum width or height of an image. Taken directly from Kitty.
@ -276,8 +281,8 @@ pub const LoadingImage = struct {
fn isPathInTempDir(path: []const u8) bool {
if (std.mem.startsWith(u8, path, "/tmp")) return true;
if (std.mem.startsWith(u8, path, "/dev/shm")) return true;
if (internal_os.allocTmpDir(std.heap.page_allocator)) |dir| {
defer internal_os.freeTmpDir(std.heap.page_allocator, dir);
if (temp_dir.allocTmpDir(std.heap.page_allocator)) |dir| {
defer temp_dir.freeTmpDir(std.heap.page_allocator, dir);
if (std.mem.startsWith(u8, path, dir)) return true;
// The temporary dir is sometimes a symlink. On macOS for
@ -690,7 +695,7 @@ test "image load: temporary file without correct path" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try internal_os.TempDir.init();
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(.{
@ -723,7 +728,7 @@ test "image load: rgb, not compressed, temporary file" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try internal_os.TempDir.init();
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(.{
@ -760,7 +765,7 @@ test "image load: rgb, not compressed, regular file" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try internal_os.TempDir.init();
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(.{
@ -795,7 +800,7 @@ test "image load: png, not compressed, regular file" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try internal_os.TempDir.init();
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-png-none-50x76-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{

View File

@ -1,4 +1,5 @@
const builtin = @import("builtin");
const build_options = @import("terminal_options");
const charsets = @import("charsets.zig");
const sanitize = @import("sanitize.zig");
@ -20,7 +21,7 @@ pub const page = @import("page.zig");
pub const parse_table = @import("parse_table.zig");
pub const search = @import("search.zig");
pub const size = @import("size.zig");
pub const tmux = @import("tmux.zig");
pub const tmux = if (build_options.tmux_control_mode) @import("tmux.zig") else struct {};
pub const x11_color = @import("x11_color.zig");
pub const Charset = charsets.Charset;

View File

@ -1,5 +1,5 @@
const std = @import("std");
const build_config = @import("../build_config.zig");
const build_options = @import("terminal_options");
/// The possible cursor shapes. Not all app runtimes support these shapes.
/// The shapes are always based on the W3C supported cursor styles so we
@ -48,13 +48,20 @@ pub const MouseShape = enum(c_int) {
}
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk => @import("gobject").ext.defineEnum(
MouseShape,
.{ .name = "GhosttyMouseShape" },
),
pub const getGObjectType = gtk: {
switch (build_options.artifact) {
.ghostty => {},
.lib => break :gtk void,
}
.none => void,
break :gtk switch (@import("../build_config.zig").app_runtime) {
.gtk => @import("gobject").ext.defineEnum(
MouseShape,
.{ .name = "GhosttyMouseShape" },
),
.none => void,
};
};
};

View File

@ -11,7 +11,7 @@ const mem = std.mem;
const assert = std.debug.assert;
const Allocator = mem.Allocator;
const RGB = @import("color.zig").RGB;
const kitty = @import("kitty.zig");
const kitty_color = @import("kitty/color.zig");
const osc_color = @import("osc/color.zig");
pub const color = osc_color;
@ -132,7 +132,7 @@ pub const Command = union(enum) {
/// Kitty color protocol, OSC 21
/// https://sw.kovidgoyal.net/kitty/color-stack/#id1
kitty_color_protocol: kitty.color.OSC,
kitty_color_protocol: kitty_color.OSC,
/// Show a desktop notification (OSC 9 or OSC 777)
show_desktop_notification: struct {
@ -796,7 +796,7 @@ pub const Parser = struct {
self.command = .{
.kitty_color_protocol = .{
.list = std.ArrayList(kitty.color.OSC.Request).init(alloc),
.list = std.ArrayList(kitty_color.OSC.Request).init(alloc),
},
};
@ -1490,7 +1490,7 @@ pub const Parser = struct {
return;
}
const key = kitty.color.Kind.parse(self.temp_state.key) orelse {
const key = kitty_color.Kind.parse(self.temp_state.key) orelse {
log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key});
return;
};
@ -1504,7 +1504,7 @@ pub const Parser = struct {
switch (self.command) {
.kitty_color_protocol => |*v| {
// Cap our allocation amount for our list.
if (v.list.items.len >= @as(usize, kitty.color.Kind.max) * 2) {
if (v.list.items.len >= @as(usize, kitty_color.Kind.max) * 2) {
self.state = .invalid;
log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{});
return;
@ -2600,7 +2600,7 @@ test "OSC: hyperlink end" {
test "OSC: kitty color protocol" {
const testing = std.testing;
const Kind = kitty.color.Kind;
const Kind = kitty_color.Kind;
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();

View File

@ -1,6 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const assert = std.debug.assert;
@ -182,8 +182,8 @@ pub const Page = struct {
/// If this is true then verifyIntegrity will do nothing. This is
/// only present with runtime safety enabled.
pause_integrity_checks: if (build_config.slow_runtime_safety) usize else void =
if (build_config.slow_runtime_safety) 0 else {},
pause_integrity_checks: if (build_options.slow_runtime_safety) usize else void =
if (build_options.slow_runtime_safety) 0 else {},
/// Initialize a new page, allocating the required backing memory.
/// The size of the initialized page defaults to the full capacity.
@ -307,7 +307,7 @@ pub const Page = struct {
/// doing a lot of operations that would trigger integrity check
/// violations but you know the page will end up in a consistent state.
pub fn pauseIntegrityChecks(self: *Page, v: bool) void {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
if (v) {
self.pause_integrity_checks += 1;
} else {
@ -320,8 +320,11 @@ pub const Page = struct {
/// when runtime safety is enabled. This is a no-op when runtime
/// safety is disabled. This uses the libc allocator.
pub fn assertIntegrity(self: *const Page) void {
if (comptime build_config.slow_runtime_safety) {
self.verifyIntegrity(std.heap.c_allocator) catch |err| {
if (comptime build_options.slow_runtime_safety) {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
self.verifyIntegrity(alloc) catch |err| {
log.err("page integrity violation, crashing. err={}", .{err});
@panic("page integrity violation");
};
@ -351,7 +354,7 @@ pub const Page = struct {
// tests (non-Valgrind) anyways so we're verifying anyways.
if (std.valgrind.runningOnValgrind() > 0) return;
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
if (self.pause_integrity_checks > 0) return;
}
@ -760,7 +763,7 @@ pub const Page = struct {
// This is an integrity check: if the row claims it doesn't
// have managed memory then all cells must also not have
// managed memory.
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
for (other_cells) |cell| {
assert(!cell.hasGrapheme());
assert(!cell.hyperlink);
@ -787,7 +790,7 @@ pub const Page = struct {
if (src_cell.hasGrapheme()) {
// To prevent integrity checks flipping. This will
// get fixed up when we check the style id below.
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
dst_cell.style_id = style.default_id;
}
@ -890,8 +893,10 @@ pub const Page = struct {
error.NeedsRehash => return error.StyleSetNeedsRehash,
} orelse src_cell.style_id;
}
if (src_cell.codepoint() == kitty.graphics.unicode.placeholder) {
dst_row.kitty_virtual_placeholder = true;
if (comptime build_options.kitty_graphics) {
if (src_cell.codepoint() == kitty.graphics.unicode.placeholder) {
dst_row.kitty_virtual_placeholder = true;
}
}
}
}
@ -914,7 +919,7 @@ pub const Page = struct {
/// Get the cells for a row.
pub fn getCells(self: *const Page, row: *Row) []Cell {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
const rows = self.rows.ptr(self.memory);
const cells = self.cells.ptr(self.memory);
assert(@intFromPtr(row) >= @intFromPtr(rows));
@ -980,8 +985,10 @@ pub const Page = struct {
dst.hyperlink = true;
dst_row.hyperlink = true;
}
if (src.codepoint() == kitty.graphics.unicode.placeholder) {
dst_row.kitty_virtual_placeholder = true;
if (comptime build_options.kitty_graphics) {
if (src.codepoint() == kitty.graphics.unicode.placeholder) {
dst_row.kitty_virtual_placeholder = true;
}
}
}
}
@ -1002,7 +1009,9 @@ pub const Page = struct {
src_row.grapheme = false;
src_row.hyperlink = false;
src_row.styled = false;
src_row.kitty_virtual_placeholder = false;
if (comptime build_options.kitty_graphics) {
src_row.kitty_virtual_placeholder = false;
}
}
}
@ -1100,14 +1109,16 @@ pub const Page = struct {
if (cells.len == self.size.cols) row.styled = false;
}
if (row.kitty_virtual_placeholder and
cells.len == self.size.cols)
{
for (cells) |c| {
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
break;
}
} else row.kitty_virtual_placeholder = false;
if (comptime build_options.kitty_graphics) {
if (row.kitty_virtual_placeholder and
cells.len == self.size.cols)
{
for (cells) |c| {
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
break;
}
} else row.kitty_virtual_placeholder = false;
}
}
// Zero the cells as u64s since empirically this seems
@ -1363,7 +1374,7 @@ pub const Page = struct {
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
defer self.assertIntegrity();
if (build_config.slow_runtime_safety) assert(cell.codepoint() != 0);
if (build_options.slow_runtime_safety) assert(cell.codepoint() != 0);
const cell_offset = getOffset(Cell, self.memory, cell);
var map = self.grapheme_map.map(self.memory);
@ -1436,7 +1447,7 @@ pub const Page = struct {
/// there are scenarios where we want to move graphemes without changing
/// the content tag. Callers beware but assertIntegrity should catch this.
fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
assert(src.hasGrapheme());
assert(!dst.hasGrapheme());
}
@ -1453,7 +1464,7 @@ pub const Page = struct {
/// Clear the graphemes for a given cell.
pub fn clearGrapheme(self: *Page, row: *Row, cell: *Cell) void {
defer self.assertIntegrity();
if (build_config.slow_runtime_safety) assert(cell.hasGrapheme());
if (build_options.slow_runtime_safety) assert(cell.hasGrapheme());
// Get our entry in the map, which must exist
const cell_offset = getOffset(Cell, self.memory, cell);
@ -1929,6 +1940,9 @@ pub const Row = packed struct(u64) {
/// True if this row contains a virtual placeholder for the Kitty
/// graphics protocol. (U+10EEEE)
// Note: We keep this as memory-using even if the kitty graphics
// feature is disabled because we want to keep our padding and
// everything throughout the same.
kitty_virtual_placeholder: bool = false,
_padding: u23 = 0,

View File

@ -1,4 +1,5 @@
const std = @import("std");
const build_options = @import("terminal_options");
const assert = std.debug.assert;
const testing = std.testing;
const simd = @import("../simd/main.zig");
@ -64,8 +65,9 @@ pub fn Stream(comptime Handler: type) type {
/// Process a string of characters.
pub fn nextSlice(self: *Self, input: []const u8) !void {
// Debug mode disables the SIMD optimizations
if (comptime debug) {
// Disable SIMD optimizations if build requests it or if our
// manual debug mode is on.
if (comptime debug or !build_options.simd) {
for (input) |c| try self.next(c);
return;
}