libghostty: -Dvt-features to compile out unused features

This introduces a `-Dvt-features` build option for libghostty-vt that
compiles out optional feature areas, primarily so size-conscious
embedders (e.g. wasm) can significantly trim the binary.

The flag is similar to `-Dcpu`, `+feature` or `feature` to enable it,
`-feature` to disable, magic word `all` to turn all features on or off.
Example: `-Dvt-features=-all,+render-state` builds only the render
state API.

### Sizes

| Build | Bytes | Brotli |
|---|---|---|
| default (all features) | 876,500 | 218,309 |
| web interactive (`-all,+render-state,+input-encode,+selection,+color,+grid-introspection`) | 661,119 | 168,994 |
| read-only viewer (`-all,+render-state`) | 537,441 | 132,858 |
| bare VT core (`-all`) | 515,422 | 125,756 |
| xterm.js browser bundle (incl. renderers) | 488,663 | 99,311 |
| @xterm/headless | 182,672 | 39,651 |

Note: xterm versions are stable as of this commit.
This commit is contained in:
Mitchell Hashimoto
2026-08-14 21:02:46 -07:00
parent 0e0893adff
commit 1fdbb8c912
11 changed files with 570 additions and 177 deletions

View File

@@ -95,6 +95,7 @@ jobs:
- build-flatpak
- build-libghostty-vt
- build-libghostty-vt-wasm
- build-libghostty-vt-features
- build-libghostty-vt-android
- build-libghostty-vt-macos
- build-libghostty-vt-windows
@@ -726,6 +727,52 @@ jobs:
echo "Verified ${artifact} requires simd128"
done
# Compile-only checks for the -Dvt-features flags so that future changes
# don't regress any feature combination.
build-libghostty-vt-features:
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Compile feature flag matrix
run: |
features="snapshot formatter selection render_state input_encode color grid_introspection glyph_protocol kitty_graphics"
configs="-all"
for f in $features; do configs="$configs -all,+$f"; done
for cfg in $configs; do
echo "::group::-Dvt-features=$cfg"
# The wasm lib build compiles the C API export block; the
# test build compiles every module test without running any.
nix develop -c zig build -Demit-lib-vt \
"-Dvt-features=$cfg" \
-Dtarget=wasm32-freestanding
nix develop -c zig build test-lib-vt-build \
"-Dvt-features=$cfg"
echo "::endgroup::"
done
# lib-vt requires macOS runner for macOS/iOS builds because it requires the `apple_sdk` path
build-libghostty-vt-macos:
strategy:

View File

@@ -141,6 +141,27 @@ Some notes for packaging the wasm module:
a large performance win for VT parsing. If you target an unusual runtime
without SIMD support, opt out with `-Dcpu=generic`.
- Optional feature areas can be compiled out with `-Dvt-features` to
significantly reduce binary size. The flag takes comma-separated
modifications applied to the default all-enabled feature set,
`-Dcpu`-style: `+feature` (or bare `feature`) enables, `-feature`
disables, and the special name `all` refers to every feature. Hyphens
and underscores are interchangeable in feature names. For example, a
read-only terminal viewer only needs the render state API:
```sh
zig build -Demit-lib-vt -Dtarget=wasm32-freestanding \
-Doptimize=ReleaseSmall -Dvt-features=-all,+render-state
```
This roughly halves the compressed module size versus the default
build. An interactive terminal typically wants
`-Dvt-features=-all,+render-state,+input-encode,+selection,+color`.
Disabled features drop both their C API exports and any escape
sequence handling (the sequences are still consumed and safely
ignored). See the `Features` struct in `src/terminal/build_options.zig`
for the full list of features and what each one covers.
- `ReleaseSmall` is the recommended optimization mode for the web. Running
the result through [Binaryen's](https://github.com/WebAssembly/binaryen)
`wasm-opt -O3` shrinks it by roughly a further 10% without hurting

View File

@@ -69,6 +69,10 @@ pub fn build(b: *std.Build) !void {
"test-lib-vt",
"Run libghostty-vt tests",
);
const test_lib_vt_build_step = b.step(
"test-lib-vt-build",
"Build libghostty-vt tests without running them (compile check)",
);
const test_valgrind_step = b.step(
"test-valgrind",
"Run tests under valgrind",
@@ -329,6 +333,7 @@ pub fn build(b: *std.Build) !void {
});
const mod_vt_test_run = b.addRunArtifact(mod_vt_test);
test_lib_vt_step.dependOn(&mod_vt_test_run.step);
test_lib_vt_build_step.dependOn(&mod_vt_test.step);
const mod_vt_c_test = b.addTest(.{
.root_module = mod.vt_c,
@@ -336,6 +341,7 @@ pub fn build(b: *std.Build) !void {
});
const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test);
test_lib_vt_step.dependOn(&mod_vt_c_test_run.step);
test_lib_vt_build_step.dependOn(&mod_vt_c_test.step);
}
// Tests (skip when building libghostty-vt)

View File

@@ -62,6 +62,10 @@ emit_xcframework: bool = false,
emit_webdata: bool = false,
emit_unicode_table_gen: bool = false,
/// Feature gates for libghostty-vt artifacts (-Dvt-features). The full
/// Ghostty application ignores this and always enables everything.
vt_features: TerminalBuildOptions.Features = .{},
/// True when Ghostty is being built as a dependency of another project
/// rather than as the root project.
is_dep: bool = false,
@@ -421,6 +425,30 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
config.emit_lib_vt = emit_lib_vt;
config.vt_features = features: {
const list = b.option(
[]const u8,
"vt-features",
"Comma-separated libghostty-vt feature modifications applied " ++
"to the default all-enabled set, -Dcpu style: `+feature` " ++
"or `feature` enables, `-feature` disables, and `all` " ++
"means every feature (e.g. `-all,+render-state` for a " ++
"render-only build). Only applies to lib artifacts.",
) orelse break :features .{};
break :features TerminalBuildOptions.Features.parse(list) catch {
var valid: std.ArrayList(u8) = .empty;
inline for (@typeInfo(TerminalBuildOptions.Features).@"struct".fields) |field| {
if (valid.items.len > 0) try valid.appendSlice(b.allocator, ", ");
try valid.appendSlice(b.allocator, field.name);
}
std.log.err(
"-Dvt-features={s} contains an unknown feature. Valid features: all, {s}",
.{ list, valid.items },
);
return error.UnknownVtFeature;
};
};
config.emit_exe = b.option(
bool,
"emit-exe",
@@ -684,6 +712,12 @@ pub fn terminalOptions(
.simd = self.simd,
.oniguruma = true,
.c_abi = false,
// The application requires every feature; only lib artifacts
// may trim them.
.features = switch (artifact) {
.ghostty => .{},
.lib => self.vt_features,
},
.version = switch (artifact) {
.ghostty => self.version,
.lib => self.lib_version,

View File

@@ -180,44 +180,50 @@ comptime {
_ = @import("quirks_memset.zig");
const c = terminal.c_api;
@export(&c.key_event_new, .{ .name = "ghostty_key_event_new" });
@export(&c.key_event_free, .{ .name = "ghostty_key_event_free" });
@export(&c.key_event_set_action, .{ .name = "ghostty_key_event_set_action" });
@export(&c.key_event_get_action, .{ .name = "ghostty_key_event_get_action" });
@export(&c.key_event_set_key, .{ .name = "ghostty_key_event_set_key" });
@export(&c.key_event_get_key, .{ .name = "ghostty_key_event_get_key" });
@export(&c.key_event_set_mods, .{ .name = "ghostty_key_event_set_mods" });
@export(&c.key_event_get_mods, .{ .name = "ghostty_key_event_get_mods" });
@export(&c.key_event_set_consumed_mods, .{ .name = "ghostty_key_event_set_consumed_mods" });
@export(&c.key_event_get_consumed_mods, .{ .name = "ghostty_key_event_get_consumed_mods" });
@export(&c.key_event_set_composing, .{ .name = "ghostty_key_event_set_composing" });
@export(&c.key_event_get_composing, .{ .name = "ghostty_key_event_get_composing" });
@export(&c.key_event_set_utf8, .{ .name = "ghostty_key_event_set_utf8" });
@export(&c.key_event_get_utf8, .{ .name = "ghostty_key_event_get_utf8" });
@export(&c.key_event_set_unshifted_codepoint, .{ .name = "ghostty_key_event_set_unshifted_codepoint" });
@export(&c.key_event_get_unshifted_codepoint, .{ .name = "ghostty_key_event_get_unshifted_codepoint" });
@export(&c.key_encoder_new, .{ .name = "ghostty_key_encoder_new" });
@export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" });
@export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" });
@export(&c.key_encoder_setopt_from_terminal, .{ .name = "ghostty_key_encoder_setopt_from_terminal" });
@export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" });
@export(&c.mouse_event_new, .{ .name = "ghostty_mouse_event_new" });
@export(&c.mouse_event_free, .{ .name = "ghostty_mouse_event_free" });
@export(&c.mouse_event_set_action, .{ .name = "ghostty_mouse_event_set_action" });
@export(&c.mouse_event_get_action, .{ .name = "ghostty_mouse_event_get_action" });
@export(&c.mouse_event_set_button, .{ .name = "ghostty_mouse_event_set_button" });
@export(&c.mouse_event_clear_button, .{ .name = "ghostty_mouse_event_clear_button" });
@export(&c.mouse_event_get_button, .{ .name = "ghostty_mouse_event_get_button" });
@export(&c.mouse_event_set_mods, .{ .name = "ghostty_mouse_event_set_mods" });
@export(&c.mouse_event_get_mods, .{ .name = "ghostty_mouse_event_get_mods" });
@export(&c.mouse_event_set_position, .{ .name = "ghostty_mouse_event_set_position" });
@export(&c.mouse_event_get_position, .{ .name = "ghostty_mouse_event_get_position" });
@export(&c.mouse_encoder_new, .{ .name = "ghostty_mouse_encoder_new" });
@export(&c.mouse_encoder_free, .{ .name = "ghostty_mouse_encoder_free" });
@export(&c.mouse_encoder_setopt, .{ .name = "ghostty_mouse_encoder_setopt" });
@export(&c.mouse_encoder_setopt_from_terminal, .{ .name = "ghostty_mouse_encoder_setopt_from_terminal" });
@export(&c.mouse_encoder_reset, .{ .name = "ghostty_mouse_encoder_reset" });
@export(&c.mouse_encoder_encode, .{ .name = "ghostty_mouse_encoder_encode" });
const features = terminal.options;
if (features.input_encode) {
@export(&c.key_event_new, .{ .name = "ghostty_key_event_new" });
@export(&c.key_event_free, .{ .name = "ghostty_key_event_free" });
@export(&c.key_event_set_action, .{ .name = "ghostty_key_event_set_action" });
@export(&c.key_event_get_action, .{ .name = "ghostty_key_event_get_action" });
@export(&c.key_event_set_key, .{ .name = "ghostty_key_event_set_key" });
@export(&c.key_event_get_key, .{ .name = "ghostty_key_event_get_key" });
@export(&c.key_event_set_mods, .{ .name = "ghostty_key_event_set_mods" });
@export(&c.key_event_get_mods, .{ .name = "ghostty_key_event_get_mods" });
@export(&c.key_event_set_consumed_mods, .{ .name = "ghostty_key_event_set_consumed_mods" });
@export(&c.key_event_get_consumed_mods, .{ .name = "ghostty_key_event_get_consumed_mods" });
@export(&c.key_event_set_composing, .{ .name = "ghostty_key_event_set_composing" });
@export(&c.key_event_get_composing, .{ .name = "ghostty_key_event_get_composing" });
@export(&c.key_event_set_utf8, .{ .name = "ghostty_key_event_set_utf8" });
@export(&c.key_event_get_utf8, .{ .name = "ghostty_key_event_get_utf8" });
@export(&c.key_event_set_unshifted_codepoint, .{ .name = "ghostty_key_event_set_unshifted_codepoint" });
@export(&c.key_event_get_unshifted_codepoint, .{ .name = "ghostty_key_event_get_unshifted_codepoint" });
@export(&c.key_encoder_new, .{ .name = "ghostty_key_encoder_new" });
@export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" });
@export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" });
@export(&c.key_encoder_setopt_from_terminal, .{ .name = "ghostty_key_encoder_setopt_from_terminal" });
@export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" });
@export(&c.focus_encode, .{ .name = "ghostty_focus_encode" });
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
@export(&c.paste_encode, .{ .name = "ghostty_paste_encode" });
@export(&c.mouse_event_new, .{ .name = "ghostty_mouse_event_new" });
@export(&c.mouse_event_free, .{ .name = "ghostty_mouse_event_free" });
@export(&c.mouse_event_set_action, .{ .name = "ghostty_mouse_event_set_action" });
@export(&c.mouse_event_get_action, .{ .name = "ghostty_mouse_event_get_action" });
@export(&c.mouse_event_set_button, .{ .name = "ghostty_mouse_event_set_button" });
@export(&c.mouse_event_clear_button, .{ .name = "ghostty_mouse_event_clear_button" });
@export(&c.mouse_event_get_button, .{ .name = "ghostty_mouse_event_get_button" });
@export(&c.mouse_event_set_mods, .{ .name = "ghostty_mouse_event_set_mods" });
@export(&c.mouse_event_get_mods, .{ .name = "ghostty_mouse_event_get_mods" });
@export(&c.mouse_event_set_position, .{ .name = "ghostty_mouse_event_set_position" });
@export(&c.mouse_event_get_position, .{ .name = "ghostty_mouse_event_get_position" });
@export(&c.mouse_encoder_new, .{ .name = "ghostty_mouse_encoder_new" });
@export(&c.mouse_encoder_free, .{ .name = "ghostty_mouse_encoder_free" });
@export(&c.mouse_encoder_setopt, .{ .name = "ghostty_mouse_encoder_setopt" });
@export(&c.mouse_encoder_setopt_from_terminal, .{ .name = "ghostty_mouse_encoder_setopt_from_terminal" });
@export(&c.mouse_encoder_reset, .{ .name = "ghostty_mouse_encoder_reset" });
@export(&c.mouse_encoder_encode, .{ .name = "ghostty_mouse_encoder_encode" });
}
@export(&c.osc_new, .{ .name = "ghostty_osc_new" });
@export(&c.osc_free, .{ .name = "ghostty_osc_free" });
@export(&c.osc_next, .{ .name = "ghostty_osc_next" });
@@ -226,10 +232,7 @@ comptime {
@export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" });
@export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" });
@export(&c.color_scheme_report_encode, .{ .name = "ghostty_color_scheme_report_encode" });
@export(&c.focus_encode, .{ .name = "ghostty_focus_encode" });
@export(&c.mode_report_encode, .{ .name = "ghostty_mode_report_encode" });
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
@export(&c.paste_encode, .{ .name = "ghostty_paste_encode" });
@export(&c.unicode_codepoint_width, .{ .name = "ghostty_unicode_codepoint_width" });
@export(&c.unicode_grapheme_width, .{ .name = "ghostty_unicode_grapheme_width" });
@export(&c.size_report_encode, .{ .name = "ghostty_size_report_encode" });
@@ -237,21 +240,25 @@ comptime {
@export(&c.style_is_default, .{ .name = "ghostty_style_is_default" });
@export(&c.sys_log_stderr, .{ .name = "ghostty_sys_log_stderr" });
@export(&c.sys_set, .{ .name = "ghostty_sys_set" });
@export(&c.cell_get, .{ .name = "ghostty_cell_get" });
@export(&c.cell_get_multi, .{ .name = "ghostty_cell_get_multi" });
@export(&c.row_get, .{ .name = "ghostty_row_get" });
@export(&c.row_get_multi, .{ .name = "ghostty_row_get_multi" });
@export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" });
@export(&c.color_contrast, .{ .name = "ghostty_color_contrast" });
@export(&c.color_luminance, .{ .name = "ghostty_color_luminance" });
@export(&c.color_parse, .{ .name = "ghostty_color_parse" });
@export(&c.color_parse_palette_entry, .{ .name = "ghostty_color_parse_palette_entry" });
@export(&c.color_parse_x11, .{ .name = "ghostty_color_parse_x11" });
@export(&c.color_palette_default, .{ .name = "ghostty_color_palette_default" });
@export(&c.color_palette_generate, .{ .name = "ghostty_color_palette_generate" });
@export(&c.color_perceived_luminance, .{ .name = "ghostty_color_perceived_luminance" });
@export(&c.color_x11_name_count, .{ .name = "ghostty_color_x11_name_count" });
@export(&c.color_x11_names, .{ .name = "ghostty_color_x11_names" });
if (features.grid_introspection) {
@export(&c.cell_get, .{ .name = "ghostty_cell_get" });
@export(&c.cell_get_multi, .{ .name = "ghostty_cell_get_multi" });
@export(&c.row_get, .{ .name = "ghostty_row_get" });
@export(&c.row_get_multi, .{ .name = "ghostty_row_get_multi" });
}
if (features.color) {
@export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" });
@export(&c.color_contrast, .{ .name = "ghostty_color_contrast" });
@export(&c.color_luminance, .{ .name = "ghostty_color_luminance" });
@export(&c.color_parse, .{ .name = "ghostty_color_parse" });
@export(&c.color_parse_palette_entry, .{ .name = "ghostty_color_parse_palette_entry" });
@export(&c.color_parse_x11, .{ .name = "ghostty_color_parse_x11" });
@export(&c.color_palette_default, .{ .name = "ghostty_color_palette_default" });
@export(&c.color_palette_generate, .{ .name = "ghostty_color_palette_generate" });
@export(&c.color_perceived_luminance, .{ .name = "ghostty_color_perceived_luminance" });
@export(&c.color_x11_name_count, .{ .name = "ghostty_color_x11_name_count" });
@export(&c.color_x11_names, .{ .name = "ghostty_color_x11_names" });
}
@export(&c.sgr_new, .{ .name = "ghostty_sgr_new" });
@export(&c.sgr_free, .{ .name = "ghostty_sgr_free" });
@export(&c.sgr_reset, .{ .name = "ghostty_sgr_reset" });
@@ -261,33 +268,39 @@ comptime {
@export(&c.sgr_unknown_partial, .{ .name = "ghostty_sgr_unknown_partial" });
@export(&c.sgr_attribute_tag, .{ .name = "ghostty_sgr_attribute_tag" });
@export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" });
@export(&c.formatter_terminal_new, .{ .name = "ghostty_formatter_terminal_new" });
@export(&c.formatter_format_buf, .{ .name = "ghostty_formatter_format_buf" });
@export(&c.formatter_format_alloc, .{ .name = "ghostty_formatter_format_alloc" });
@export(&c.formatter_free, .{ .name = "ghostty_formatter_free" });
@export(&c.terminal_selection_format_buf, .{ .name = "ghostty_terminal_selection_format_buf" });
@export(&c.terminal_selection_format_alloc, .{ .name = "ghostty_terminal_selection_format_alloc" });
@export(&c.render_state_new, .{ .name = "ghostty_render_state_new" });
@export(&c.render_state_update, .{ .name = "ghostty_render_state_update" });
@export(&c.render_state_begin_update, .{ .name = "ghostty_render_state_begin_update" });
@export(&c.render_state_end_update, .{ .name = "ghostty_render_state_end_update" });
@export(&c.render_state_get, .{ .name = "ghostty_render_state_get" });
@export(&c.render_state_get_multi, .{ .name = "ghostty_render_state_get_multi" });
@export(&c.render_state_set, .{ .name = "ghostty_render_state_set" });
@export(&c.render_state_colors_get, .{ .name = "ghostty_render_state_colors_get" });
@export(&c.render_state_row_iterator_new, .{ .name = "ghostty_render_state_row_iterator_new" });
@export(&c.render_state_row_iterator_next, .{ .name = "ghostty_render_state_row_iterator_next" });
@export(&c.render_state_row_get, .{ .name = "ghostty_render_state_row_get" });
@export(&c.render_state_row_get_multi, .{ .name = "ghostty_render_state_row_get_multi" });
@export(&c.render_state_row_set, .{ .name = "ghostty_render_state_row_set" });
@export(&c.render_state_row_iterator_free, .{ .name = "ghostty_render_state_row_iterator_free" });
@export(&c.render_state_row_cells_new, .{ .name = "ghostty_render_state_row_cells_new" });
@export(&c.render_state_row_cells_next, .{ .name = "ghostty_render_state_row_cells_next" });
@export(&c.render_state_row_cells_select, .{ .name = "ghostty_render_state_row_cells_select" });
@export(&c.render_state_row_cells_get, .{ .name = "ghostty_render_state_row_cells_get" });
@export(&c.render_state_row_cells_get_multi, .{ .name = "ghostty_render_state_row_cells_get_multi" });
@export(&c.render_state_row_cells_free, .{ .name = "ghostty_render_state_row_cells_free" });
@export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
if (features.formatter) {
@export(&c.formatter_terminal_new, .{ .name = "ghostty_formatter_terminal_new" });
@export(&c.formatter_format_buf, .{ .name = "ghostty_formatter_format_buf" });
@export(&c.formatter_format_alloc, .{ .name = "ghostty_formatter_format_alloc" });
@export(&c.formatter_free, .{ .name = "ghostty_formatter_free" });
}
if (features.formatter and features.selection) {
@export(&c.terminal_selection_format_buf, .{ .name = "ghostty_terminal_selection_format_buf" });
@export(&c.terminal_selection_format_alloc, .{ .name = "ghostty_terminal_selection_format_alloc" });
}
if (features.render_state) {
@export(&c.render_state_new, .{ .name = "ghostty_render_state_new" });
@export(&c.render_state_update, .{ .name = "ghostty_render_state_update" });
@export(&c.render_state_begin_update, .{ .name = "ghostty_render_state_begin_update" });
@export(&c.render_state_end_update, .{ .name = "ghostty_render_state_end_update" });
@export(&c.render_state_get, .{ .name = "ghostty_render_state_get" });
@export(&c.render_state_get_multi, .{ .name = "ghostty_render_state_get_multi" });
@export(&c.render_state_set, .{ .name = "ghostty_render_state_set" });
@export(&c.render_state_colors_get, .{ .name = "ghostty_render_state_colors_get" });
@export(&c.render_state_row_iterator_new, .{ .name = "ghostty_render_state_row_iterator_new" });
@export(&c.render_state_row_iterator_next, .{ .name = "ghostty_render_state_row_iterator_next" });
@export(&c.render_state_row_get, .{ .name = "ghostty_render_state_row_get" });
@export(&c.render_state_row_get_multi, .{ .name = "ghostty_render_state_row_get_multi" });
@export(&c.render_state_row_set, .{ .name = "ghostty_render_state_row_set" });
@export(&c.render_state_row_iterator_free, .{ .name = "ghostty_render_state_row_iterator_free" });
@export(&c.render_state_row_cells_new, .{ .name = "ghostty_render_state_row_cells_new" });
@export(&c.render_state_row_cells_next, .{ .name = "ghostty_render_state_row_cells_next" });
@export(&c.render_state_row_cells_select, .{ .name = "ghostty_render_state_row_cells_select" });
@export(&c.render_state_row_cells_get, .{ .name = "ghostty_render_state_row_cells_get" });
@export(&c.render_state_row_cells_get_multi, .{ .name = "ghostty_render_state_row_cells_get_multi" });
@export(&c.render_state_row_cells_free, .{ .name = "ghostty_render_state_row_cells_free" });
@export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
}
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
@export(&c.terminal_free, .{ .name = "ghostty_terminal_free" });
@export(&c.terminal_reset, .{ .name = "ghostty_terminal_reset" });
@@ -303,66 +316,82 @@ comptime {
@export(&c.terminal_continuation_write, .{ .name = "ghostty_terminal_continuation_write" });
@export(&c.terminal_continuation_buf, .{ .name = "ghostty_terminal_continuation_buf" });
@export(&c.terminal_continuation_alloc, .{ .name = "ghostty_terminal_continuation_alloc" });
@export(&c.terminal_select_word, .{ .name = "ghostty_terminal_select_word" });
@export(&c.terminal_select_word_between, .{ .name = "ghostty_terminal_select_word_between" });
@export(&c.terminal_select_line, .{ .name = "ghostty_terminal_select_line" });
@export(&c.terminal_select_all, .{ .name = "ghostty_terminal_select_all" });
@export(&c.terminal_select_output, .{ .name = "ghostty_terminal_select_output" });
@export(&c.terminal_selection_adjust, .{ .name = "ghostty_terminal_selection_adjust" });
@export(&c.terminal_selection_order, .{ .name = "ghostty_terminal_selection_order" });
@export(&c.terminal_selection_ordered, .{ .name = "ghostty_terminal_selection_ordered" });
@export(&c.terminal_selection_contains, .{ .name = "ghostty_terminal_selection_contains" });
@export(&c.terminal_selection_equal, .{ .name = "ghostty_terminal_selection_equal" });
@export(&c.selection_gesture_new, .{ .name = "ghostty_selection_gesture_new" });
@export(&c.selection_gesture_free, .{ .name = "ghostty_selection_gesture_free" });
@export(&c.selection_gesture_reset, .{ .name = "ghostty_selection_gesture_reset" });
@export(&c.selection_gesture_event, .{ .name = "ghostty_selection_gesture_event" });
@export(&c.selection_gesture_get, .{ .name = "ghostty_selection_gesture_get" });
@export(&c.selection_gesture_get_multi, .{ .name = "ghostty_selection_gesture_get_multi" });
@export(&c.selection_gesture_event_new, .{ .name = "ghostty_selection_gesture_event_new" });
@export(&c.selection_gesture_event_free, .{ .name = "ghostty_selection_gesture_event_free" });
@export(&c.selection_gesture_event_set, .{ .name = "ghostty_selection_gesture_event_set" });
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
@export(&c.terminal_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" });
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
@export(&c.snapshot_encode, .{ .name = "ghostty_snapshot_encode" });
@export(&c.snapshot_encode_buf, .{ .name = "ghostty_snapshot_encode_buf" });
@export(&c.snapshot_encode_alloc, .{ .name = "ghostty_snapshot_encode_alloc" });
@export(&c.snapshot_decoder_new, .{ .name = "ghostty_snapshot_decoder_new" });
@export(&c.snapshot_decoder_new_buf, .{ .name = "ghostty_snapshot_decoder_new_buf" });
@export(&c.snapshot_decoder_free, .{ .name = "ghostty_snapshot_decoder_free" });
@export(&c.snapshot_decoder_set, .{ .name = "ghostty_snapshot_decoder_set" });
@export(&c.snapshot_decoder_get, .{ .name = "ghostty_snapshot_decoder_get" });
@export(&c.snapshot_decoder_get_multi, .{ .name = "ghostty_snapshot_decoder_get_multi" });
@export(&c.snapshot_decoder_ready, .{ .name = "ghostty_snapshot_decoder_ready" });
@export(&c.snapshot_decoder_next, .{ .name = "ghostty_snapshot_decoder_next" });
@export(&c.snapshot_decoder_decode, .{ .name = "ghostty_snapshot_decoder_decode" });
@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_image_get_multi, .{ .name = "ghostty_kitty_graphics_image_get_multi" });
@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_get_multi, .{ .name = "ghostty_kitty_graphics_placement_get_multi" });
@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.kitty_graphics_placement_render_info, .{ .name = "ghostty_kitty_graphics_placement_render_info" });
@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.tracked_grid_ref_free, .{ .name = "ghostty_tracked_grid_ref_free" });
@export(&c.tracked_grid_ref_has_value, .{ .name = "ghostty_tracked_grid_ref_has_value" });
@export(&c.tracked_grid_ref_point, .{ .name = "ghostty_tracked_grid_ref_point" });
@export(&c.tracked_grid_ref_set, .{ .name = "ghostty_tracked_grid_ref_set" });
@export(&c.tracked_grid_ref_snapshot, .{ .name = "ghostty_tracked_grid_ref_snapshot" });
if (features.selection) {
@export(&c.terminal_select_word, .{ .name = "ghostty_terminal_select_word" });
@export(&c.terminal_select_word_between, .{ .name = "ghostty_terminal_select_word_between" });
@export(&c.terminal_select_line, .{ .name = "ghostty_terminal_select_line" });
@export(&c.terminal_select_all, .{ .name = "ghostty_terminal_select_all" });
@export(&c.terminal_select_output, .{ .name = "ghostty_terminal_select_output" });
@export(&c.terminal_selection_adjust, .{ .name = "ghostty_terminal_selection_adjust" });
@export(&c.terminal_selection_order, .{ .name = "ghostty_terminal_selection_order" });
@export(&c.terminal_selection_ordered, .{ .name = "ghostty_terminal_selection_ordered" });
@export(&c.terminal_selection_contains, .{ .name = "ghostty_terminal_selection_contains" });
@export(&c.terminal_selection_equal, .{ .name = "ghostty_terminal_selection_equal" });
@export(&c.selection_gesture_new, .{ .name = "ghostty_selection_gesture_new" });
@export(&c.selection_gesture_free, .{ .name = "ghostty_selection_gesture_free" });
@export(&c.selection_gesture_reset, .{ .name = "ghostty_selection_gesture_reset" });
@export(&c.selection_gesture_event, .{ .name = "ghostty_selection_gesture_event" });
@export(&c.selection_gesture_get, .{ .name = "ghostty_selection_gesture_get" });
@export(&c.selection_gesture_get_multi, .{ .name = "ghostty_selection_gesture_get_multi" });
@export(&c.selection_gesture_event_new, .{ .name = "ghostty_selection_gesture_event_new" });
@export(&c.selection_gesture_event_free, .{ .name = "ghostty_selection_gesture_event_free" });
@export(&c.selection_gesture_event_set, .{ .name = "ghostty_selection_gesture_event_set" });
}
// Selections are expressed in grid references, so the untracked
// reference constructors are required by both features.
if (features.grid_introspection or features.selection) {
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
}
if (features.grid_introspection) {
@export(&c.terminal_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" });
}
if (features.snapshot) {
@export(&c.snapshot_encode, .{ .name = "ghostty_snapshot_encode" });
@export(&c.snapshot_encode_buf, .{ .name = "ghostty_snapshot_encode_buf" });
@export(&c.snapshot_encode_alloc, .{ .name = "ghostty_snapshot_encode_alloc" });
@export(&c.snapshot_decoder_new, .{ .name = "ghostty_snapshot_decoder_new" });
@export(&c.snapshot_decoder_new_buf, .{ .name = "ghostty_snapshot_decoder_new_buf" });
@export(&c.snapshot_decoder_free, .{ .name = "ghostty_snapshot_decoder_free" });
@export(&c.snapshot_decoder_set, .{ .name = "ghostty_snapshot_decoder_set" });
@export(&c.snapshot_decoder_get, .{ .name = "ghostty_snapshot_decoder_get" });
@export(&c.snapshot_decoder_get_multi, .{ .name = "ghostty_snapshot_decoder_get_multi" });
@export(&c.snapshot_decoder_ready, .{ .name = "ghostty_snapshot_decoder_ready" });
@export(&c.snapshot_decoder_next, .{ .name = "ghostty_snapshot_decoder_next" });
@export(&c.snapshot_decoder_decode, .{ .name = "ghostty_snapshot_decoder_decode" });
}
if (features.kitty_graphics) {
@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_image_get_multi, .{ .name = "ghostty_kitty_graphics_image_get_multi" });
@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_get_multi, .{ .name = "ghostty_kitty_graphics_placement_get_multi" });
@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.kitty_graphics_placement_render_info, .{ .name = "ghostty_kitty_graphics_placement_render_info" });
}
if (features.grid_introspection) {
@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.tracked_grid_ref_free, .{ .name = "ghostty_tracked_grid_ref_free" });
@export(&c.tracked_grid_ref_has_value, .{ .name = "ghostty_tracked_grid_ref_has_value" });
@export(&c.tracked_grid_ref_point, .{ .name = "ghostty_tracked_grid_ref_point" });
@export(&c.tracked_grid_ref_set, .{ .name = "ghostty_tracked_grid_ref_set" });
}
if (features.grid_introspection and features.snapshot) {
@export(&c.tracked_grid_ref_snapshot, .{ .name = "ghostty_tracked_grid_ref_snapshot" });
}
@export(&c.build_info, .{ .name = "ghostty_build_info" });
@export(&c.type_json, .{ .name = "ghostty_type_json" });
@export(&c.alloc_alloc, .{ .name = "ghostty_alloc" });

View File

@@ -15873,6 +15873,8 @@ test "Terminal: deleteLines wide char at right margin with full clear" {
}
test "Terminal: glyph APC stores session glossary entries" {
if (comptime !build_options.glyph_protocol) return error.SkipZigTest;
const alloc = testing.allocator;
const io_impl = testing.io;
var t = try init(io_impl, alloc, .{ .cols = 80, .rows = 24 });

View File

@@ -82,12 +82,16 @@ pub const Handler = struct {
if (byte == ';') {
const str = id.buf[0..id.len];
if (std.mem.eql(u8, str, glyph.identifier)) {
if (self.enabled.contains(.glyph)) {
self.state = .{ .glyph = .init(
alloc,
self.max_bytes.get(.glyph) orelse
Protocol.defaultMaxBytes(.glyph),
) };
if (comptime build_options.glyph_protocol) {
if (self.enabled.contains(.glyph)) {
self.state = .{ .glyph = .init(
alloc,
self.max_bytes.get(.glyph) orelse
Protocol.defaultMaxBytes(.glyph),
) };
} else {
self.state = .ignore;
}
} else {
self.state = .ignore;
}
@@ -127,11 +131,13 @@ pub const Handler = struct {
};
} else unreachable,
.glyph => |*p| p.feed(byte) catch |err| {
log.warn("glyph protocol error: {}", .{err});
p.deinit();
self.state = .ignore;
},
.glyph => |*p| if (comptime build_options.glyph_protocol) {
p.feed(byte) catch |err| {
log.warn("glyph protocol error: {}", .{err});
p.deinit();
self.state = .ignore;
};
} else unreachable,
}
}
@@ -191,14 +197,14 @@ pub const Handler = struct {
return;
} else unreachable,
.glyph => |*p| {
.glyph => |*p| if (comptime build_options.glyph_protocol) {
p.feedSlice(rem) catch |err| {
log.warn("glyph protocol error: {}", .{err});
p.deinit();
self.state = .ignore;
};
return;
},
} else unreachable,
}
}
}
@@ -229,6 +235,8 @@ pub const Handler = struct {
},
.glyph => |*p| glyph_cmd: {
if (comptime !build_options.glyph_protocol) unreachable;
const command = p.complete(p.alloc) catch |err| {
log.warn("glyph protocol error: {}", .{err});
break :glyph_cmd null;
@@ -269,7 +277,10 @@ pub const State = union(enum) {
void,
/// Glyph protocol
glyph: glyph.CommandParser,
glyph: if (build_options.glyph_protocol)
glyph.CommandParser
else
void,
/// An unsupported APC retained for the optional unknown callback.
/// Keep this after recognized protocol states so their tag values and
@@ -280,7 +291,10 @@ pub const State = union(enum) {
switch (self.*) {
.inactive, .ignore, .identify => {},
.unknown => |*v| v.deinit(),
.glyph => |*v| v.deinit(),
.glyph => |*v| if (comptime build_options.glyph_protocol)
v.deinit()
else
unreachable,
.kitty => |*v| if (comptime build_options.kitty_graphics)
v.deinit()
else
@@ -415,7 +429,11 @@ pub const Command = union(enum) {
else
void,
glyph: glyph.Request,
glyph: if (build_options.glyph_protocol)
glyph.Request
else
void,
unknown: Unknown,
pub fn deinit(self: *Command, alloc: Allocator) void {
@@ -425,7 +443,11 @@ pub const Command = union(enum) {
else
unreachable,
.glyph => |*v| v.deinit(alloc),
.glyph => |*v| if (comptime build_options.glyph_protocol)
v.deinit(alloc)
else
unreachable,
.unknown => |*v| v.deinit(alloc),
}
}
@@ -636,6 +658,8 @@ test "garbage glyph command" {
}
test "valid glyph command" {
if (comptime !build_options.glyph_protocol) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
@@ -700,6 +724,8 @@ test "feedSlice unknown APC command is ignored" {
}
test "feedSlice valid glyph command" {
if (comptime !build_options.glyph_protocol) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;

View File

@@ -36,9 +36,227 @@ pub const Options = struct {
/// Options.
c_abi: bool,
/// Optional feature gates, all enabled by default. See Features.
features: Features = .{},
/// The version of the application.
version: std.SemanticVersion,
/// Optional features for lib artifacts. Disabling a feature removes
/// its C API exports and any stream-integrated handling from the
/// build, primarily so size-conscious embedders (e.g. wasm) can trim
/// the binary. The Ghostty application itself always builds with
/// every feature enabled.
///
/// Field names are the accepted tokens for `-Dvt-features`, which
/// acts like `-Dcpu`: comma-separated modifications applied on top
/// of the default (all enabled) set. `+feature` and `feature`
/// enable, `-feature` disables. The special name `all` refers to
/// every feature, so `-Dvt-features=-all,+render-state` builds only
/// the render state API. Hyphens in names are interchangeable with
/// underscores. Fields are emitted verbatim as bool options on the
/// `terminal_options` module.
pub const Features = packed struct {
/// Terminal state serialization: encode a full terminal (screen
/// contents, scrollback, cursor, styles, modes, etc.) to a
/// compact binary format and decode it back into a live
/// terminal. Used for session persistence, window restoration,
/// and moving terminal state between processes.
///
/// C API: `ghostty_snapshot_*` and, together with
/// `grid_introspection`, `ghostty_tracked_grid_ref_snapshot`.
snapshot: bool = true,
/// Textual export of terminal contents as plain text, VT
/// (replayable escape sequences), or HTML, with optional extras
/// such as cursor position, palette, modes, and tabstops. Used
/// for copy-to-clipboard, dump-to-file, and debugging.
///
/// C API: `ghostty_formatter_*` and, together with `selection`,
/// `ghostty_terminal_selection_format_*`.
formatter: bool = true,
/// Text selection: select word/line/output/all, selection
/// ordering, adjustment, and containment math, and the
/// click/drag gesture state machine (single/double/triple
/// click behaviors). Selections are expressed in grid
/// references, so enabling this also keeps the untracked grid
/// reference constructors even if `grid_introspection` is
/// disabled.
///
/// C API: `ghostty_terminal_select_*`,
/// `ghostty_terminal_selection_*`, `ghostty_selection_gesture_*`.
selection: bool = true,
/// The render state API: a coherent, update-in-place view of
/// the visible screen (rows, cells, styles, cursor, colors,
/// palette) designed to drive a renderer at frame rates. This
/// is the primary read path for embedders that draw the
/// terminal.
///
/// C API: `ghostty_render_state_*`.
render_state: bool = true,
/// Encoding of host input events into the byte sequences a
/// terminal application expects: keyboard input (including the
/// Kitty keyboard protocol), mouse reporting, focus reporting,
/// and (bracketed) paste. Any interactive embedder needs this;
/// a read-only viewer does not.
///
/// C API: `ghostty_key_event_*`, `ghostty_key_encoder_*`,
/// `ghostty_mouse_event_*`, `ghostty_mouse_encoder_*`,
/// `ghostty_focus_encode`, `ghostty_paste_*`.
input_encode: bool = true,
/// Color utilities: luminance, perceived luminance, and
/// contrast math (e.g. for minimum-contrast rendering), color
/// string parsing (`#rrggbb`, X11 color names, XParseColor
/// `rgb:`/`rgbi:` device specs), the default 256-color palette
/// and palette generation from configurable bases, and the X11
/// color name table itself.
///
/// C API: `ghostty_color_*`.
color: bool = true,
/// Direct inspection of grid contents at a position via grid
/// references: cell and row data getters (codepoints,
/// graphemes, style, hyperlink URI, wrap state, ...) plus
/// tracked grid references that stay valid across terminal
/// updates. Used for hyperlink hover/open, accessibility, and
/// tests; deliberately not built for render loops (use
/// `render_state` for that).
///
/// C API: `ghostty_grid_ref_*`, `ghostty_tracked_grid_ref_*`,
/// `ghostty_cell_get*`, `ghostty_row_get*`, and (shared with
/// `selection`) `ghostty_terminal_grid_ref` /
/// `ghostty_terminal_point_from_grid_ref`.
grid_introspection: bool = true,
/// The APC glyph protocol: a Ghostty extension that lets
/// terminal applications register custom font glyphs (glyf
/// outlines) for private-use-area codepoints. Embedders that
/// don't render registered glyphs can disable this; the
/// sequences are still consumed and safely ignored.
///
/// No C API surface of its own; this gates the stream handling
/// and glossary storage.
glyph_protocol: bool = true,
/// The Kitty graphics protocol: APC command parsing, image
/// transmission and storage (PNG decoding via the sys
/// interface), and placement tracking, plus the read APIs a
/// renderer uses to draw placed images. Disabled sequences are
/// still consumed and safely ignored.
///
/// This requires the ability to get timestamps from the OS, so
/// it is always disabled on freestanding targets (e.g.
/// wasm32-freestanding) regardless of this setting.
///
/// C API: `ghostty_kitty_graphics_*`.
kitty_graphics: bool = true,
pub fn parse(list: []const u8) error{UnknownFeature}!Features {
// Modifications apply on top of the default set.
var result: Features = .{};
var it = std.mem.splitAny(u8, list, ", ");
while (it.next()) |raw| {
if (raw.len == 0) continue;
var name = raw;
var enable = true;
switch (name[0]) {
'+' => name = name[1..],
'-' => {
enable = false;
name = name[1..];
},
else => {},
}
// A dangling sign with no name is malformed.
if (name.len == 0) return error.UnknownFeature;
// The special name "all" refers to every feature.
if (std.mem.eql(u8, name, "all")) {
inline for (@typeInfo(Features).@"struct".fields) |field| {
@field(result, field.name) = enable;
}
continue;
}
var found = false;
inline for (@typeInfo(Features).@"struct".fields) |field| {
if (eqlName(name, field.name)) {
@field(result, field.name) = enable;
found = true;
}
}
if (!found) return error.UnknownFeature;
}
return result;
}
/// Feature name equality where '-' in the input matches '_' in
/// the field name.
fn eqlName(input: []const u8, name: []const u8) bool {
if (input.len != name.len) return false;
for (input, name) |a, b| {
const norm: u8 = if (a == '-') '_' else a;
if (norm != b) return false;
}
return true;
}
test "parse" {
const testing = std.testing;
const none: Features = @bitCast(
@as(@typeInfo(Features).@"struct".backing_integer.?, 0),
);
// Empty input keeps the defaults.
try testing.expectEqual(Features{}, try Features.parse(""));
// All three token forms, comma separated.
try testing.expectEqual(
Features{ .snapshot = false },
try Features.parse("-snapshot"),
);
try testing.expectEqual(
Features{ .snapshot = false, .formatter = false },
try Features.parse("-snapshot,+render_state,-formatter,color"),
);
// The special "all" name refers to every feature.
try testing.expectEqual(none, try Features.parse("-all"));
try testing.expectEqual(Features{}, try Features.parse("-all,+all"));
var render_only = none;
render_only.render_state = true;
try testing.expectEqual(
render_only,
try Features.parse("-all,+render-state"),
);
// Hyphens are interchangeable with underscores.
try testing.expectEqual(
Features{ .input_encode = false, .grid_introspection = false },
try Features.parse("-input-encode,-grid_introspection"),
);
// Later tokens win.
try testing.expectEqual(
Features{},
try Features.parse("-snapshot,+snapshot"),
);
// Unknown names and dangling signs are errors.
try testing.expectError(error.UnknownFeature, Features.parse("bogus"));
try testing.expectError(error.UnknownFeature, Features.parse("-snapshot,-"));
}
};
/// Add the required build options for the terminal module.
///
/// The memory referenced by self is expected to stick around (it isn't
@@ -55,25 +273,25 @@ 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, "tmux_control_mode", self.oniguruma);
// Feature gates, emitted as flat bools (e.g. `options.snapshot`).
const target = m.resolved_target.?.result;
inline for (@typeInfo(Features).@"struct".fields) |field| {
var value = @field(self.features, field.name);
// Kitty graphics requires the ability to get timestamps and
// there is no way to do that on freestanding targets, so it
// is always disabled there regardless of the feature setting.
if (comptime std.mem.eql(u8, field.name, "kitty_graphics")) {
if (target.cpu.arch == .wasm32 and target.os.tag == .freestanding)
value = false;
}
opts.addOption(bool, field.name, value);
}
// Version information.
opts.addOption(
[]const u8,
@@ -92,3 +310,7 @@ pub const Options = struct {
m.addOptions("terminal_options", opts);
}
};
test {
_ = Options.Features;
}

View File

@@ -639,6 +639,8 @@ fn computeViewportPos(
}
test "placement_iterator new/free" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,

View File

@@ -5405,6 +5405,8 @@ test "set color sets dirty flag" {
}
test "set glyph protocol disables APC handling and clears glossary" {
if (comptime !build_options.glyph_protocol) return error.SkipZigTest;
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,

View File

@@ -1054,7 +1054,7 @@ pub const Handler = struct {
}
},
.glyph => |*glyph_req| {
.glyph => |*glyph_req| if (comptime build_options.glyph_protocol) {
const resp = self.terminal.glyphProtocol(alloc, glyph_req);
if (resp) |r| resp_block: {
// Don't waste time encoding if we can't write responses
@@ -1824,6 +1824,8 @@ test "full reset" {
}
test "glyph protocol APC with write_pty callback" {
if (comptime !build_options.glyph_protocol) return error.SkipZigTest;
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);