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 improvementspull/8848/head
commit
8cb52323e5
|
|
@ -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
|
||||
|
|
|
|||
51
build.zig
51
build.zig
|
|
@ -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",
|
||||
|
|
|
|||
189
example/app.ts
189
example/app.ts
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
@ -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});
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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, .{});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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=");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(.{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue