From a584163306dec700ff143a9a5e20f5d9ce2a0f79 Mon Sep 17 00:00:00 2001 From: rhodes-b <59537185+rhodes-b@users.noreply.github.com> Date: Mon, 22 Sep 2025 23:14:05 -0500 Subject: [PATCH 01/27] load runtime css before user options --- src/apprt/gtk/class/application.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 4202115e5..cb6bb00e8 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -809,6 +809,9 @@ pub const Application = extern struct { const writer = buf.writer(alloc); + try loadRuntimeCss414(config, &writer); + try loadRuntimeCss416(config, &writer); + const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background; try writer.print( @@ -847,9 +850,6 @@ pub const Application = extern struct { , .{ .font_family = font_family }); } - try loadRuntimeCss414(config, &writer); - try loadRuntimeCss416(config, &writer); - // ensure that we have a sentinel try writer.writeByte(0); From b006101ddd140002f87e09ae727ea23064ddc0d7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Sep 2025 12:33:22 -0700 Subject: [PATCH 02/27] lib-vt: boilerplate to build a shared object --- build.zig | 12 ++++++- include/ghostty-vt.h | 14 ++++++++ src/build/GhosttyLibVt.zig | 72 ++++++++++++++++++++++++++++++++++++++ src/build/main.zig | 1 + src/lib_vt.zig | 6 ++++ src/terminal/c_api.zig | 5 +++ src/terminal/main.zig | 7 ++++ 7 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 include/ghostty-vt.h create mode 100644 src/build/GhosttyLibVt.zig create mode 100644 src/terminal/c_api.zig diff --git a/build.zig b/build.zig index 8838572b7..5ec69d40e 100644 --- a/build.zig +++ b/build.zig @@ -31,6 +31,7 @@ pub fn build(b: *std.Build) !void { // All our steps which we'll hook up later. The steps are shown // up here just so that they are more self-documenting. + const libvt_step = b.step("lib-vt", "Build libghostty-vt"); const run_step = b.step("run", "Run the app"); const run_valgrind_step = b.step( "run-valgrind", @@ -86,7 +87,7 @@ pub fn build(b: *std.Build) !void { check_step.dependOn(dist.install_step); } - // libghostty + // libghostty (internal, big) const libghostty_shared = try buildpkg.GhosttyLib.initShared( b, &deps, @@ -96,6 +97,15 @@ pub fn build(b: *std.Build) !void { &deps, ); + // libghostty-vt + const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared( + b, + &mod, + &deps, + ); + libghostty_vt_shared.install(libvt_step, "libghostty-vt.so"); + libghostty_vt_shared.installHeader(libvt_step); + // Helpgen if (config.emit_helpgen) deps.help_strings.install(); diff --git a/include/ghostty-vt.h b/include/ghostty-vt.h new file mode 100644 index 000000000..591b095a2 --- /dev/null +++ b/include/ghostty-vt.h @@ -0,0 +1,14 @@ +// libghostty-vt + +#ifndef GHOSTTY_VT_H +#define GHOSTTY_VT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_H */ diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig new file mode 100644 index 000000000..9f0646624 --- /dev/null +++ b/src/build/GhosttyLibVt.zig @@ -0,0 +1,72 @@ +const GhosttyLibVt = @This(); + +const std = @import("std"); +const RunStep = std.Build.Step.Run; +const Config = @import("Config.zig"); +const GhosttyZig = @import("GhosttyZig.zig"); +const SharedDeps = @import("SharedDeps.zig"); +const LibtoolStep = @import("LibtoolStep.zig"); +const LipoStep = @import("LipoStep.zig"); + +/// The step that generates the file. +step: *std.Build.Step, + +/// The final library file +output: std.Build.LazyPath, +dsym: ?std.Build.LazyPath, + +pub fn initShared( + b: *std.Build, + zig: *const GhosttyZig, + deps: *const SharedDeps, +) !GhosttyLibVt { + const lib = b.addSharedLibrary(.{ + .name = "ghostty-vt", + .root_module = zig.vt, + }); + + // Get our debug symbols + const dsymutil: ?std.Build.LazyPath = dsymutil: { + if (!deps.config.target.result.os.tag.isDarwin()) { + break :dsymutil null; + } + + const dsymutil = RunStep.create(b, "dsymutil"); + dsymutil.addArgs(&.{"dsymutil"}); + dsymutil.addFileArg(lib.getEmittedBin()); + dsymutil.addArgs(&.{"-o"}); + const output = dsymutil.addOutputFileArg("libghostty-vt.dSYM"); + break :dsymutil output; + }; + + return .{ + .step = &lib.step, + .output = lib.getEmittedBin(), + .dsym = dsymutil, + }; +} + +pub fn install( + self: *const GhosttyLibVt, + step: *std.Build.Step, + name: []const u8, +) void { + const b = self.step.owner; + const lib_install = b.addInstallLibFile( + self.output, + name, + ); + step.dependOn(&lib_install.step); +} + +pub fn installHeader( + self: *const GhosttyLibVt, + step: *std.Build.Step, +) void { + const b = self.step.owner; + const header_install = b.addInstallHeaderFile( + b.path("include/ghostty-vt.h"), + "ghostty-vt.h", + ); + step.dependOn(&header_install.step); +} diff --git a/src/build/main.zig b/src/build/main.zig index 0ee41352b..1f36d375c 100644 --- a/src/build/main.zig +++ b/src/build/main.zig @@ -13,6 +13,7 @@ pub const GhosttyDocs = @import("GhosttyDocs.zig"); pub const GhosttyExe = @import("GhosttyExe.zig"); pub const GhosttyFrameData = @import("GhosttyFrameData.zig"); pub const GhosttyLib = @import("GhosttyLib.zig"); +pub const GhosttyLibVt = @import("GhosttyLibVt.zig"); pub const GhosttyResources = @import("GhosttyResources.zig"); pub const GhosttyI18n = @import("GhosttyI18n.zig"); pub const GhosttyXcodebuild = @import("GhosttyXcodebuild.zig"); diff --git a/src/lib_vt.zig b/src/lib_vt.zig index d375a89d2..444f08d3c 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -65,6 +65,12 @@ pub const EraseLine = terminal.EraseLine; pub const TabClear = terminal.TabClear; pub const Attribute = terminal.Attribute; +comptime { + if (terminal.is_c_lib) { + _ = terminal.c_api; + } +} + test { _ = terminal; } diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig new file mode 100644 index 000000000..287711fec --- /dev/null +++ b/src/terminal/c_api.zig @@ -0,0 +1,5 @@ +pub export fn ghostty_hi() void { + // Does nothing, but you can see this symbol exists: + // nm -D --defined-only zig-out/lib/libghostty-vt.so | rg ' T ' + // This is temporary as we figure out the API. +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 1fea9934e..4106786e1 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -62,6 +62,13 @@ pub const Attribute = sgr.Attribute; pub const isSafePaste = sanitize.isSafePaste; +/// This is set to true when we're building the C library. +pub const is_c_lib = @import("root") == @import("../lib_vt.zig"); + +/// This is the C API for this package. Do NOT reference this unless +/// you want a bunch of symbols exported into your final artifact. +pub const c_api = @import("c_api.zig"); + test { @import("std").testing.refAllDecls(@This()); From 35095fddda330339a44da164deac3e905ecb8a7a Mon Sep 17 00:00:00 2001 From: Brice <59537185+rhodes-b@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:15:04 -0500 Subject: [PATCH 03/27] comment to load standard css options before some of the user configured styles --- src/apprt/gtk/class/application.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index cb6bb00e8..6ab3ad282 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -809,6 +809,7 @@ pub const Application = extern struct { const writer = buf.writer(alloc); + // Load standard css first as it can override some of the user configured styling. try loadRuntimeCss414(config, &writer); try loadRuntimeCss416(config, &writer); From 85345c31cf10fa6ff237afafede5190810d1fa93 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Sep 2025 15:01:48 -0700 Subject: [PATCH 04/27] build: don't add deps when cross compiling to darwin --- src/build/GhosttyExe.zig | 1 + src/build/SharedDeps.zig | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/build/GhosttyExe.zig b/src/build/GhosttyExe.zig index 083aecdb5..1dfc4b4b7 100644 --- a/src/build/GhosttyExe.zig +++ b/src/build/GhosttyExe.zig @@ -1,6 +1,7 @@ const Ghostty = @This(); const std = @import("std"); +const builtin = @import("builtin"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 20b5df862..b3fe860d1 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -1,6 +1,8 @@ const SharedDeps = @This(); const std = @import("std"); +const builtin = @import("builtin"); + const Config = @import("Config.zig"); const HelpStrings = @import("HelpStrings.zig"); const MetallibStep = @import("MetallibStep.zig"); @@ -104,6 +106,19 @@ pub fn add( var static_libs = LazyPathList.init(b.allocator); errdefer static_libs.deinit(); + // WARNING: This is a hack! + // If we're cross-compiling to Darwin then we don't add any deps. + // We don't support cross-compiling to Darwin but due to the way + // lazy dependencies work with Zig, we call this function. So we just + // bail. The build will fail but the build would've failed anyways. + // And this lets other non-platform-specific targets like `lib-vt` + // cross-compile properly. + if (!builtin.target.os.tag.isDarwin() and + self.config.target.result.os.tag.isDarwin()) + { + return static_libs; + } + // Every exe gets build options populated step.root_module.addOptions("build_options", self.options); From 3d04fbb45164482c41a60615b1496f9879887781 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Sep 2025 14:48:09 -0700 Subject: [PATCH 05/27] ci: build lib-vt for a variety of targets We don't run our tests on all these targets so its not sure they all will function correctly, but at least we can be sure they build. --- .github/workflows/test.yml | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d12d6e67f..55f609b61 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,7 @@ jobs: - build-examples - build-flatpak - build-freebsd + - build-libghostty-vt - build-linux - build-linux-libghostty - build-nix @@ -194,6 +195,48 @@ jobs: zig build \ -Dsnap + build-libghostty-vt: + strategy: + matrix: + target: + [ + aarch64-macos, + x86_64-macos, + aarch64-linux, + x86_64-linux, + x86_64-windows, + ] + 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 + run: | + nix develop -c zig build lib-vt \ + -Dtarget=${{ matrix.target }} \ + -Dsimd=false + build-linux: strategy: fail-fast: false From def4969aff9500833559d6073522cc8f90a7db02 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Sep 2025 15:15:15 -0700 Subject: [PATCH 06/27] build: shared object is a dylib on macOS --- build.zig | 3 +-- src/build/Config.zig | 10 ++++++++++ src/build/GhosttyLibVt.zig | 10 +++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/build.zig b/build.zig index 5ec69d40e..5cef2df02 100644 --- a/build.zig +++ b/build.zig @@ -101,9 +101,8 @@ pub fn build(b: *std.Build) !void { const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared( b, &mod, - &deps, ); - libghostty_vt_shared.install(libvt_step, "libghostty-vt.so"); + libghostty_vt_shared.install(libvt_step, "libghostty-vt"); libghostty_vt_shared.installHeader(libvt_step); // Helpgen diff --git a/src/build/Config.zig b/src/build/Config.zig index 474674d3a..7dfdb913d 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -589,6 +589,16 @@ pub fn genericMacOSTarget( }); } +/// The extension to use for a shared library on the given target. +pub fn sharedLibExt(target: std.Build.ResolvedTarget) []const u8 { + return if (target.result.os.tag.isDarwin()) + "dylib" + else if (target.result.os.tag == .windows) + return "dll" + else + return "so"; +} + /// The possible entrypoints for the exe artifact. This has no effect on /// other artifact types (i.e. lib, wasm_module). /// diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 9f0646624..85d71ae0f 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -15,11 +15,14 @@ step: *std.Build.Step, output: std.Build.LazyPath, dsym: ?std.Build.LazyPath, +/// Library extension, e.g. "dylib", "so", "dll" +ext: []const u8, + pub fn initShared( b: *std.Build, zig: *const GhosttyZig, - deps: *const SharedDeps, ) !GhosttyLibVt { + const target = zig.vt.resolved_target.?; const lib = b.addSharedLibrary(.{ .name = "ghostty-vt", .root_module = zig.vt, @@ -27,7 +30,7 @@ pub fn initShared( // Get our debug symbols const dsymutil: ?std.Build.LazyPath = dsymutil: { - if (!deps.config.target.result.os.tag.isDarwin()) { + if (!target.result.os.tag.isDarwin()) { break :dsymutil null; } @@ -43,6 +46,7 @@ pub fn initShared( .step = &lib.step, .output = lib.getEmittedBin(), .dsym = dsymutil, + .ext = Config.sharedLibExt(target), }; } @@ -54,7 +58,7 @@ pub fn install( const b = self.step.owner; const lib_install = b.addInstallLibFile( self.output, - name, + b.fmt("{s}.{s}", .{ name, self.ext }), ); step.dependOn(&lib_install.step); } From 32bf37e5e42bf6097cfea0f445ae30fe997535a5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Sep 2025 15:26:06 -0700 Subject: [PATCH 07/27] setup basic doxygen for docs We may not stick with Doxygen, but it gives us something to start with. --- Doxyfile | 28 ++++++++++++++++++++++++++++ nix/devShell.nix | 2 ++ src/lib_vt.zig | 6 +++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 Doxyfile diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 000000000..d6bf92a9a --- /dev/null +++ b/Doxyfile @@ -0,0 +1,28 @@ +# Doxyfile 1.13.2 + +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = "libghostty" +INPUT = include/ghostty-vt.h +INPUT_ENCODING = UTF-8 +RECURSIVE = NO + +#--------------------------------------------------------------------------- +# HTML Output +#--------------------------------------------------------------------------- + +GENERATE_HTML = YES +HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty + +#--------------------------------------------------------------------------- +# Man Output +#--------------------------------------------------------------------------- + +GENERATE_MAN = YES +MAN_OUTPUT = zig-out/share/man +MAN_EXTENSION = .3 + +#--------------------------------------------------------------------------- +# Other Output +#--------------------------------------------------------------------------- + +GENERATE_LATEX = NO diff --git a/nix/devShell.nix b/nix/devShell.nix index 783d6018d..0c97ec0da 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -3,6 +3,7 @@ lib, stdenv, bashInteractive, + doxygen, nushell, appstream, flatpak-builder, @@ -89,6 +90,7 @@ in packages = [ # For builds + doxygen jq llvmPackages_latest.llvm minisign diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 444f08d3c..98242ce78 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -66,9 +66,9 @@ pub const TabClear = terminal.TabClear; pub const Attribute = terminal.Attribute; comptime { - if (terminal.is_c_lib) { - _ = terminal.c_api; - } + // If we're building the C library (vs. the Zig module) then + // we want to reference the C API so that it gets exported. + if (terminal.is_c_lib) _ = terminal.c_api; } test { From 5265414a36d9b5ad942ca997adf7348b8d0bd5d4 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 22 Sep 2025 19:36:07 -0500 Subject: [PATCH 08/27] config: smarter parsing in autoParseStruct Fixes #8849 Previously, the `parseAutoStruct` function that was used to parse generic structs for the config simply split the input value on commas without taking into account quoting or escapes. This led to problems because it was impossible to include a comma in the value of config entries that were parsed by `parseAutoStruct`. This is particularly problematic because `ghostty +show-config --default` would produce output like the following: ``` command-palette-entry = title:Focus Split: Next,description:Focus the next split, if any.,action:goto_split:next ``` Because the `description` contains a comma, Ghostty is unable to parse this correctly. The value would be split into four parts: ``` title:Focus Split: Next description:Focus the next split if any. action:goto_split:next ``` Instead of three parts: ``` title:Focus Split: Next description:Focus the next split, if any. action:goto_split:next ``` Because `parseAutoStruct` simply looked for commas to split on, no amount of quoting or escaping would allow that to be parsed correctly. This is fixed by (1) introducing a parser that will split the input to `parseAutoStruct` into fields while taking into account quotes and escaping. And (2) changing the `ghostty +show-config` output to put the values in `command-palette-entry` into quotes so that Ghostty can parse it's own output. `parseAutoStruct` will also now parse double quoted values as a Zig string literal. This makes it easier to embed control codes, whitespace, and commas in values. --- src/cli/Splitter.zig | 424 ++++++++++++++++++++++++++++++++++++++++++ src/cli/args.zig | 22 ++- src/config/Config.zig | 77 ++++++-- src/input/Binding.zig | 17 +- src/input/command.zig | 7 +- 5 files changed, 525 insertions(+), 22 deletions(-) create mode 100644 src/cli/Splitter.zig diff --git a/src/cli/Splitter.zig b/src/cli/Splitter.zig new file mode 100644 index 000000000..ce8847083 --- /dev/null +++ b/src/cli/Splitter.zig @@ -0,0 +1,424 @@ +//! Iterator to split a string into fields by commas, taking into account +//! quotes and escapes. +//! +//! Supports the same escapes as in Zig literal strings. +//! +//! Quotes must begin and end with a double quote (`"`). It is an error to not +//! end a quote that was begun. To include a double quote inside a quote (or to +//! not have a double quote start a quoted section) escape it with a backslash. +//! +//! Single quotes (`'`) are not special, they do not begin a quoted block. +//! +//! Zig multiline string literals are NOT supported. +//! +//! Quotes and escapes are not stripped or decoded, that must be handled as a +//! separate step! +const Splitter = @This(); + +pub const Error = error{ + UnclosedQuote, + UnfinishedEscape, + IllegalEscape, +}; + +/// the string that we are splitting +str: []const u8, +/// how much of the string has been consumed so far +index: usize, + +/// initialize a splitter with the given string +pub fn init(str: []const u8) Splitter { + return .{ + .str = str, + .index = 0, + }; +} + +/// return the next field, null if no more fields +pub fn next(self: *Splitter) Error!?[]const u8 { + if (self.index >= self.str.len) return null; + + // where the current field starts + const start = self.index; + // state of state machine + const State = enum { + normal, + quoted, + escape, + hexescape, + unicodeescape, + }; + // keep track of the state to return to when done processing an escape + // sequence. + var last: State = .normal; + // used to count number of digits seen in a hex escape + var hexescape_digits: usize = 0; + // sub-state of parsing hex escapes + var unicodeescape_state: enum { + start, + digits, + } = .start; + // number of digits in a unicode escape seen so far + var unicodeescape_digits: usize = 0; + // accumulator for value of unicode escape + var unicodeescape_value: usize = 0; + + loop: switch (State.normal) { + .normal => { + if (self.index >= self.str.len) return self.str[start..]; + switch (self.str[self.index]) { + ',' => { + self.index += 1; + return self.str[start .. self.index - 1]; + }, + '"' => { + self.index += 1; + continue :loop .quoted; + }, + '\\' => { + self.index += 1; + last = .normal; + continue :loop .escape; + }, + else => { + self.index += 1; + continue :loop .normal; + }, + } + }, + .quoted => { + if (self.index >= self.str.len) return error.UnclosedQuote; + switch (self.str[self.index]) { + '"' => { + self.index += 1; + continue :loop .normal; + }, + '\\' => { + self.index += 1; + last = .quoted; + continue :loop .escape; + }, + else => { + self.index += 1; + continue :loop .quoted; + }, + } + }, + .escape => { + if (self.index >= self.str.len) return error.UnfinishedEscape; + switch (self.str[self.index]) { + 'n', 'r', 't', '\\', '\'', '"' => { + self.index += 1; + continue :loop last; + }, + 'x' => { + self.index += 1; + hexescape_digits = 0; + continue :loop .hexescape; + }, + 'u' => { + self.index += 1; + unicodeescape_state = .start; + unicodeescape_digits = 0; + unicodeescape_value = 0; + continue :loop .unicodeescape; + }, + else => return error.IllegalEscape, + } + }, + .hexescape => { + if (self.index >= self.str.len) return error.UnfinishedEscape; + switch (self.str[self.index]) { + '0'...'9', 'a'...'f', 'A'...'F' => { + self.index += 1; + hexescape_digits += 1; + if (hexescape_digits == 2) continue :loop last; + continue :loop .hexescape; + }, + else => return error.IllegalEscape, + } + }, + .unicodeescape => { + if (self.index >= self.str.len) return error.UnfinishedEscape; + switch (unicodeescape_state) { + .start => { + switch (self.str[self.index]) { + '{' => { + self.index += 1; + unicodeescape_value = 0; + unicodeescape_state = .digits; + continue :loop .unicodeescape; + }, + else => return error.IllegalEscape, + } + }, + .digits => { + switch (self.str[self.index]) { + '}' => { + self.index += 1; + if (unicodeescape_digits == 0) return error.IllegalEscape; + continue :loop last; + }, + '0'...'9' => |d| { + self.index += 1; + unicodeescape_digits += 1; + unicodeescape_value <<= 4; + unicodeescape_value += d - '0'; + }, + 'a'...'f' => |d| { + self.index += 1; + unicodeescape_digits += 1; + unicodeescape_value <<= 4; + unicodeescape_value += d - 'a'; + }, + 'A'...'F' => |d| { + self.index += 1; + unicodeescape_digits += 1; + unicodeescape_value <<= 4; + unicodeescape_value += d - 'A'; + }, + else => return error.IllegalEscape, + } + if (unicodeescape_value > 0x10ffff) return error.IllegalEscape; + continue :loop .unicodeescape; + }, + } + }, + } +} + +/// Return any remaining string data, whether it has a comma or not. +pub fn rest(self: *Splitter) ?[]const u8 { + if (self.index >= self.str.len) return null; + defer self.index = self.str.len; + return self.str[self.index..]; +} + +test "splitter 1" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("a,b,c"); + try testing.expectEqualStrings("a", (try s.next()).?); + try testing.expectEqualStrings("b", (try s.next()).?); + try testing.expectEqualStrings("c", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 2" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init(""); + try testing.expect(null == try s.next()); +} + +test "splitter 3" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("a"); + try testing.expectEqualStrings("a", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 4" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\x5a"); + try testing.expectEqualStrings("\\x5a", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 5" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("'a',b"); + try testing.expectEqualStrings("'a'", (try s.next()).?); + try testing.expectEqualStrings("b", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 6" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("'a,b',c"); + try testing.expectEqualStrings("'a", (try s.next()).?); + try testing.expectEqualStrings("b'", (try s.next()).?); + try testing.expectEqualStrings("c", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 7" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\"a,b\",c"); + try testing.expectEqualStrings("\"a,b\"", (try s.next()).?); + try testing.expectEqualStrings("c", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 8" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init(" a , b "); + try testing.expectEqualStrings(" a ", (try s.next()).?); + try testing.expectEqualStrings(" b ", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 9" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\x"); + try testing.expectError(error.UnfinishedEscape, s.next()); +} + +test "splitter 10" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\x5"); + try testing.expectError(error.UnfinishedEscape, s.next()); +} + +test "splitter 11" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\u"); + try testing.expectError(error.UnfinishedEscape, s.next()); +} + +test "splitter 12" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\u{"); + try testing.expectError(error.UnfinishedEscape, s.next()); +} + +test "splitter 13" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\u{}"); + try testing.expectError(error.IllegalEscape, s.next()); +} + +test "splitter 14" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\u{h1}"); + try testing.expectError(error.IllegalEscape, s.next()); +} + +test "splitter 15" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\u{10ffff}"); + try testing.expectEqualStrings("\\u{10ffff}", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 16" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\u{110000}"); + try testing.expectError(error.IllegalEscape, s.next()); +} + +test "splitter 17" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\d"); + try testing.expectError(error.IllegalEscape, s.next()); +} + +test "splitter 18" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\\n\\r\\t\\\"\\'\\\\"); + try testing.expectEqualStrings("\\n\\r\\t\\\"\\'\\\\", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 19" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\"abc'def'ghi\""); + try testing.expectEqualStrings("\"abc'def'ghi\"", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 20" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("\",\",abc"); + try testing.expectEqualStrings("\",\"", (try s.next()).?); + try testing.expectEqualStrings("abc", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 21" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("'a','b', 'c'"); + try testing.expectEqualStrings("'a'", (try s.next()).?); + try testing.expectEqualStrings("'b'", (try s.next()).?); + try testing.expectEqualStrings(" 'c'", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 22" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("abc\"def"); + try testing.expectError(error.UnclosedQuote, s.next()); +} + +test "splitter 23" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("title:\"Focus Split: Up\",description:\"Focus the split above, if it exists.\",action:goto_split:up"); + try testing.expectEqualStrings("title:\"Focus Split: Up\"", (try s.next()).?); + try testing.expectEqualStrings("description:\"Focus the split above, if it exists.\"", (try s.next()).?); + try testing.expectEqualStrings("action:goto_split:up", (try s.next()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 24" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("a,b,c,def"); + try testing.expectEqualStrings("a", (try s.next()).?); + try testing.expectEqualStrings("b", (try s.next()).?); + try testing.expectEqualStrings("c,def", s.rest().?); + try testing.expect(null == try s.next()); +} + +test "splitter 25" { + const std = @import("std"); + const testing = std.testing; + + var s: Splitter = .init("a,\\u{10,df}"); + try testing.expectEqualStrings("a", (try s.next()).?); + try testing.expectError(error.IllegalEscape, s.next()); +} diff --git a/src/cli/args.zig b/src/cli/args.zig index 4db0a29a2..9edb11b2b 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -7,6 +7,7 @@ const diags = @import("diagnostics.zig"); const internal_os = @import("../os/main.zig"); const Diagnostic = diags.Diagnostic; const DiagnosticList = diags.DiagnosticList; +const Splitter = @import("Splitter.zig"); const log = std.log.scoped(.cli); @@ -527,24 +528,31 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { const FieldSet = std.StaticBitSet(info.fields.len); var fields_set: FieldSet = .initEmpty(); - // We split each value by "," - var iter = std.mem.splitSequence(u8, v, ","); - loop: while (iter.next()) |entry| { + // We split each value by "," allowing for quoting and escaping. + var iter: Splitter = .init(v); + loop: while (try iter.next()) |entry| { // Find the key/value, trimming whitespace. The value may be quoted // which we strip the quotes from. const idx = mem.indexOf(u8, entry, ":") orelse return error.InvalidValue; const key = std.mem.trim(u8, entry[0..idx], whitespace); + + // used if we need to decode a double-quoted string. + var buf: std.ArrayListUnmanaged(u8) = .empty; + defer buf.deinit(alloc); + const value = value: { - var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); + const value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); // Detect a quoted string. if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') { - // Trim quotes since our CLI args processor expects - // quotes to already be gone. - value = value[1 .. value.len - 1]; + // Decode a double-quoted string as a Zig string literal. + const writer = buf.writer(alloc); + const parsed = try std.zig.string_literal.parseWrite(writer, value); + if (parsed == .failure) return error.InvalidValue; + break :value buf.items; } break :value value; diff --git a/src/config/Config.zig b/src/config/Config.zig index 1ef9de947..e9aa0944e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2363,9 +2363,21 @@ keybind: Keybinds = .{}, /// (`:`), and then the specified value. The syntax for actions is identical /// to the one for keybind actions. Whitespace in between fields is ignored. /// +/// If you need to embed commas or any other special characters in the values, +/// enclose the value in double quotes and it will be interpreted as a Zig +/// string literal. This is also useful for including whitespace at the +/// beginning or the end of a value. See the +/// [Zig documentation](https://ziglang.org/documentation/master/#Escape-Sequences) +/// for more information on string literals. Note that multiline string literals +/// are not supported. +/// +/// Double quotes can not be used around the field names. +/// /// ```ini /// command-palette-entry = title:Reset Font Style, action:csi:0m /// command-palette-entry = title:Crash on Main Thread,description:Causes a crash on the main (UI) thread.,action:crash:main +/// command-palette-entry = title:Focus Split: Right,description:"Focus the split to the right, if it exists.",action:goto_split:right +/// command-palette-entry = title:"Ghostty",description:"Add a little Ghostty to your terminal.",action:"text:\xf0\x9f\x91\xbb" /// ``` /// /// By default, the command palette is preloaded with most actions that might @@ -7029,18 +7041,24 @@ pub const RepeatableCommand = struct { return; } - var buf: [4096]u8 = undefined; for (self.value.items) |item| { - const str = if (item.description.len > 0) std.fmt.bufPrint( - &buf, - "title:{s},description:{s},action:{}", - .{ item.title, item.description, item.action }, - ) else std.fmt.bufPrint( - &buf, - "title:{s},action:{}", - .{ item.title, item.action }, - ); - try formatter.formatEntry([]const u8, str catch return error.OutOfMemory); + var buf: [4096]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + var writer = fbs.writer(); + + writer.writeAll("title:\"") catch return error.OutOfMemory; + std.zig.stringEscape(item.title, "", .{}, writer) catch return error.OutOfMemory; + writer.writeAll("\"") catch return error.OutOfMemory; + + if (item.description.len > 0) { + writer.writeAll(",description:\"") catch return error.OutOfMemory; + std.zig.stringEscape(item.description, "", .{}, writer) catch return error.OutOfMemory; + writer.writeAll("\"") catch return error.OutOfMemory; + } + + writer.print(",action:\"{}\"", .{item.action}) catch return error.OutOfMemory; + + try formatter.formatEntry([]const u8, fbs.getWritten()); } } @@ -7106,7 +7124,7 @@ pub const RepeatableCommand = struct { var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Bobr, action:text:Bober"); try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:Bober\n", buf.items); + try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:Bober\"\n", buf.items); } test "RepeatableCommand formatConfig multiple items" { @@ -7122,7 +7140,40 @@ pub const RepeatableCommand = struct { try list.parseCLI(alloc, "title:Bobr, action:text:kurwa"); try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle"); try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:kurwa\na = title:Ja,description:pierdole,action:text:jakie bydle\n", buf.items); + try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:kurwa\"\na = title:\"Ja\",description:\"pierdole\",action:\"text:jakie bydle\"\n", buf.items); + } + + test "RepeatableCommand parseCLI commas" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + { + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:\"Bo,br\",action:\"text:kur,wa\""); + try testing.expectEqual(@as(usize, 1), list.value.items.len); + + const item = list.value.items[0]; + try testing.expectEqualStrings("Bo,br", item.title); + try testing.expectEqualStrings("", item.description); + try testing.expect(item.action == .text); + try testing.expectEqualStrings("kur,wa", item.action.text); + } + { + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:\"Bo,br\",description:\"abc,def\",action:text:kurwa"); + try testing.expectEqual(@as(usize, 1), list.value.items.len); + + const item = list.value.items[0]; + try testing.expectEqualStrings("Bo,br", item.title); + try testing.expectEqualStrings("abc,def", item.description); + try testing.expect(item.action == .text); + try testing.expectEqualStrings("kurwa", item.action.text); + } } }; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 54e7754f2..016f6a947 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1213,7 +1213,7 @@ pub const Action = union(enum) { const value_info = @typeInfo(Value); switch (Value) { void => {}, - []const u8 => try writer.print("{s}", .{value}), + []const u8 => try std.zig.stringEscape(value, "", .{}, writer), else => switch (value_info) { .@"enum" => try writer.print("{s}", .{@tagName(value)}), .float => try writer.print("{d}", .{value}), @@ -3223,3 +3223,18 @@ test "parse: set_font_size" { try testing.expectEqual(13.5, binding.action.set_font_size); } } + +test "action: format" { + const testing = std.testing; + const alloc = testing.allocator; + + const a: Action = .{ .text = "👻" }; + + var buf: std.ArrayListUnmanaged(u8) = .empty; + defer buf.deinit(alloc); + + const writer = buf.writer(alloc); + try a.format("", .{}, writer); + + try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.items); +} diff --git a/src/input/command.zig b/src/input/command.zig index 63feb2edf..bf5061c12 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -472,13 +472,18 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Quit the application.", }}, + .text => comptime &.{.{ + .action = .{ .text = "👻" }, + .title = "Ghostty", + .description = "Put a little Ghostty in your terminal.", + }}, + // No commands because they're parameterized and there // aren't obvious values users would use. It is possible that // these may have commands in the future if there are very // common values that users tend to use. .csi, .esc, - .text, .cursor_key, .set_font_size, .scroll_page_fractional, From 5f3fd9742fb5eb68de945a37069e8b813ea6d667 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 23 Sep 2025 21:53:52 -0500 Subject: [PATCH 09/27] rename Splitter-CommaSplitter --- src/cli/{Splitter.zig => CommaSplitter.zig} | 58 ++++++++++----------- src/cli/args.zig | 4 +- 2 files changed, 31 insertions(+), 31 deletions(-) rename src/cli/{Splitter.zig => CommaSplitter.zig} (89%) diff --git a/src/cli/Splitter.zig b/src/cli/CommaSplitter.zig similarity index 89% rename from src/cli/Splitter.zig rename to src/cli/CommaSplitter.zig index ce8847083..3168c1ffa 100644 --- a/src/cli/Splitter.zig +++ b/src/cli/CommaSplitter.zig @@ -13,7 +13,7 @@ //! //! Quotes and escapes are not stripped or decoded, that must be handled as a //! separate step! -const Splitter = @This(); +const CommaSplitter = @This(); pub const Error = error{ UnclosedQuote, @@ -27,7 +27,7 @@ str: []const u8, index: usize, /// initialize a splitter with the given string -pub fn init(str: []const u8) Splitter { +pub fn init(str: []const u8) CommaSplitter { return .{ .str = str, .index = 0, @@ -35,7 +35,7 @@ pub fn init(str: []const u8) Splitter { } /// return the next field, null if no more fields -pub fn next(self: *Splitter) Error!?[]const u8 { +pub fn next(self: *CommaSplitter) Error!?[]const u8 { if (self.index >= self.str.len) return null; // where the current field starts @@ -188,7 +188,7 @@ pub fn next(self: *Splitter) Error!?[]const u8 { } /// Return any remaining string data, whether it has a comma or not. -pub fn rest(self: *Splitter) ?[]const u8 { +pub fn rest(self: *CommaSplitter) ?[]const u8 { if (self.index >= self.str.len) return null; defer self.index = self.str.len; return self.str[self.index..]; @@ -198,7 +198,7 @@ test "splitter 1" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("a,b,c"); + var s: CommaSplitter = .init("a,b,c"); try testing.expectEqualStrings("a", (try s.next()).?); try testing.expectEqualStrings("b", (try s.next()).?); try testing.expectEqualStrings("c", (try s.next()).?); @@ -209,7 +209,7 @@ test "splitter 2" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init(""); + var s: CommaSplitter = .init(""); try testing.expect(null == try s.next()); } @@ -217,7 +217,7 @@ test "splitter 3" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("a"); + var s: CommaSplitter = .init("a"); try testing.expectEqualStrings("a", (try s.next()).?); try testing.expect(null == try s.next()); } @@ -226,7 +226,7 @@ test "splitter 4" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\x5a"); + var s: CommaSplitter = .init("\\x5a"); try testing.expectEqualStrings("\\x5a", (try s.next()).?); try testing.expect(null == try s.next()); } @@ -235,7 +235,7 @@ test "splitter 5" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("'a',b"); + var s: CommaSplitter = .init("'a',b"); try testing.expectEqualStrings("'a'", (try s.next()).?); try testing.expectEqualStrings("b", (try s.next()).?); try testing.expect(null == try s.next()); @@ -245,7 +245,7 @@ test "splitter 6" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("'a,b',c"); + var s: CommaSplitter = .init("'a,b',c"); try testing.expectEqualStrings("'a", (try s.next()).?); try testing.expectEqualStrings("b'", (try s.next()).?); try testing.expectEqualStrings("c", (try s.next()).?); @@ -256,7 +256,7 @@ test "splitter 7" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\"a,b\",c"); + var s: CommaSplitter = .init("\"a,b\",c"); try testing.expectEqualStrings("\"a,b\"", (try s.next()).?); try testing.expectEqualStrings("c", (try s.next()).?); try testing.expect(null == try s.next()); @@ -266,7 +266,7 @@ test "splitter 8" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init(" a , b "); + var s: CommaSplitter = .init(" a , b "); try testing.expectEqualStrings(" a ", (try s.next()).?); try testing.expectEqualStrings(" b ", (try s.next()).?); try testing.expect(null == try s.next()); @@ -276,7 +276,7 @@ test "splitter 9" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\x"); + var s: CommaSplitter = .init("\\x"); try testing.expectError(error.UnfinishedEscape, s.next()); } @@ -284,7 +284,7 @@ test "splitter 10" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\x5"); + var s: CommaSplitter = .init("\\x5"); try testing.expectError(error.UnfinishedEscape, s.next()); } @@ -292,7 +292,7 @@ test "splitter 11" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\u"); + var s: CommaSplitter = .init("\\u"); try testing.expectError(error.UnfinishedEscape, s.next()); } @@ -300,7 +300,7 @@ test "splitter 12" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\u{"); + var s: CommaSplitter = .init("\\u{"); try testing.expectError(error.UnfinishedEscape, s.next()); } @@ -308,7 +308,7 @@ test "splitter 13" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\u{}"); + var s: CommaSplitter = .init("\\u{}"); try testing.expectError(error.IllegalEscape, s.next()); } @@ -316,7 +316,7 @@ test "splitter 14" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\u{h1}"); + var s: CommaSplitter = .init("\\u{h1}"); try testing.expectError(error.IllegalEscape, s.next()); } @@ -324,7 +324,7 @@ test "splitter 15" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\u{10ffff}"); + var s: CommaSplitter = .init("\\u{10ffff}"); try testing.expectEqualStrings("\\u{10ffff}", (try s.next()).?); try testing.expect(null == try s.next()); } @@ -333,7 +333,7 @@ test "splitter 16" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\u{110000}"); + var s: CommaSplitter = .init("\\u{110000}"); try testing.expectError(error.IllegalEscape, s.next()); } @@ -341,7 +341,7 @@ test "splitter 17" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\d"); + var s: CommaSplitter = .init("\\d"); try testing.expectError(error.IllegalEscape, s.next()); } @@ -349,7 +349,7 @@ test "splitter 18" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\\n\\r\\t\\\"\\'\\\\"); + var s: CommaSplitter = .init("\\n\\r\\t\\\"\\'\\\\"); try testing.expectEqualStrings("\\n\\r\\t\\\"\\'\\\\", (try s.next()).?); try testing.expect(null == try s.next()); } @@ -358,7 +358,7 @@ test "splitter 19" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\"abc'def'ghi\""); + var s: CommaSplitter = .init("\"abc'def'ghi\""); try testing.expectEqualStrings("\"abc'def'ghi\"", (try s.next()).?); try testing.expect(null == try s.next()); } @@ -367,7 +367,7 @@ test "splitter 20" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("\",\",abc"); + var s: CommaSplitter = .init("\",\",abc"); try testing.expectEqualStrings("\",\"", (try s.next()).?); try testing.expectEqualStrings("abc", (try s.next()).?); try testing.expect(null == try s.next()); @@ -377,7 +377,7 @@ test "splitter 21" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("'a','b', 'c'"); + var s: CommaSplitter = .init("'a','b', 'c'"); try testing.expectEqualStrings("'a'", (try s.next()).?); try testing.expectEqualStrings("'b'", (try s.next()).?); try testing.expectEqualStrings(" 'c'", (try s.next()).?); @@ -388,7 +388,7 @@ test "splitter 22" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("abc\"def"); + var s: CommaSplitter = .init("abc\"def"); try testing.expectError(error.UnclosedQuote, s.next()); } @@ -396,7 +396,7 @@ test "splitter 23" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("title:\"Focus Split: Up\",description:\"Focus the split above, if it exists.\",action:goto_split:up"); + var s: CommaSplitter = .init("title:\"Focus Split: Up\",description:\"Focus the split above, if it exists.\",action:goto_split:up"); try testing.expectEqualStrings("title:\"Focus Split: Up\"", (try s.next()).?); try testing.expectEqualStrings("description:\"Focus the split above, if it exists.\"", (try s.next()).?); try testing.expectEqualStrings("action:goto_split:up", (try s.next()).?); @@ -407,7 +407,7 @@ test "splitter 24" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("a,b,c,def"); + var s: CommaSplitter = .init("a,b,c,def"); try testing.expectEqualStrings("a", (try s.next()).?); try testing.expectEqualStrings("b", (try s.next()).?); try testing.expectEqualStrings("c,def", s.rest().?); @@ -418,7 +418,7 @@ test "splitter 25" { const std = @import("std"); const testing = std.testing; - var s: Splitter = .init("a,\\u{10,df}"); + var s: CommaSplitter = .init("a,\\u{10,df}"); try testing.expectEqualStrings("a", (try s.next()).?); try testing.expectError(error.IllegalEscape, s.next()); } diff --git a/src/cli/args.zig b/src/cli/args.zig index 9edb11b2b..2d2d199be 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -7,7 +7,7 @@ const diags = @import("diagnostics.zig"); const internal_os = @import("../os/main.zig"); const Diagnostic = diags.Diagnostic; const DiagnosticList = diags.DiagnosticList; -const Splitter = @import("Splitter.zig"); +const CommaSplitter = @import("CommaSplitter.zig"); const log = std.log.scoped(.cli); @@ -529,7 +529,7 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { var fields_set: FieldSet = .initEmpty(); // We split each value by "," allowing for quoting and escaping. - var iter: Splitter = .init(v); + var iter: CommaSplitter = .init(v); loop: while (try iter.next()) |entry| { // Find the key/value, trimming whitespace. The value may be quoted // which we strip the quotes from. From 315b54822a5992978454785c024e37ec942d732d Mon Sep 17 00:00:00 2001 From: CoderJoshDK <74162303+CoderJoshDK@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:08:12 -0400 Subject: [PATCH 10/27] fix: file creation when directory already exists --- src/config/Config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 1ef9de947..a3a8388a5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3351,7 +3351,7 @@ pub fn loadOptionalFile( fn writeConfigTemplate(path: []const u8) !void { log.info("creating template config file: path={s}", .{path}); if (std.fs.path.dirname(path)) |dir_path| { - try std.fs.makeDirAbsolute(dir_path); + try std.fs.cwd().makePath(dir_path); } const file = try std.fs.createFileAbsolute(path, .{}); defer file.close(); From 969fcfaec32e5d508bedc4dc5c6aebcf407618e8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Sep 2025 19:57:29 -0700 Subject: [PATCH 11/27] lib: allocator interface based on Zig allocators --- include/ghostty-vt.h | 121 ++++++++++++++++ src/lib/allocator.zig | 317 +++++++++++++++++++++++++++++++++++++++++ src/lib_vt.zig | 9 +- src/terminal/c_api.zig | 47 +++++- 4 files changed, 489 insertions(+), 5 deletions(-) create mode 100644 src/lib/allocator.zig diff --git a/include/ghostty-vt.h b/include/ghostty-vt.h index 591b095a2..b61069058 100644 --- a/include/ghostty-vt.h +++ b/include/ghostty-vt.h @@ -7,6 +7,127 @@ extern "C" { #endif +#include +#include +#include + +//------------------------------------------------------------------- +// Types + +typedef struct GhosttyOscParser GhosttyOscParser; + +typedef enum { + GHOSTTY_VT_SUCCESS = 0, + GHOSTTY_VT_OUT_OF_MEMORY = -1, +} GhosttyVtResult; + +typedef struct { + /** + * Return a pointer to `len` bytes with specified `alignment`, or return + * `NULL` indicating the allocation failed. + * + * @param ctx The allocator context + * @param len Number of bytes to allocate + * @param alignment Required alignment for the allocation. Guaranteed to + * be a power of two between 1 and 16 inclusive. + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to allocated memory, or NULL if allocation failed + */ + void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory in place. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to resize + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return true if resize was successful in-place, false if relocation would be required + */ + bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory, allowing relocation. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * A non-`NULL` return value indicates the resize was successful. The + * allocation may have same address, or may have been relocated. In either + * case, the allocation now has size of `new_len`. A `NULL` return value + * indicates that the resize would be equivalent to allocating new memory, + * copying the bytes from the old memory, and then freeing the old memory. + * In such case, it is more efficient for the caller to perform the copy. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to remap + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed + */ + void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Free and invalidate a region of memory. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to free + * @param memory_len Size of the memory block + * @param alignment Alignment (must match original allocation) + * @param ret_addr First return address of the allocation call stack (0 if not provided) + */ + void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); +} GhosttyVtAllocatorVtable; + +/** + * Custom memory allocator. + * + * Usage example: + * @code + * GhosttyVtAllocator allocator = { + * .vtable = &my_allocator_vtable, + * .ctx = my_allocator_state + * }; + * @endcode + */ +typedef struct { + /** + * Opaque context pointer passed to all vtable functions. + * This allows the allocator implementation to maintain state + * or reference external resources needed for memory management. + */ + void *ctx; + + /** + * Pointer to the allocator's vtable containing function pointers + * for memory operations (alloc, resize, remap, free). + */ + const GhosttyVtAllocatorVtable *vtable; +} GhosttyVtAllocator; + +//------------------------------------------------------------------- +// Functions + #ifdef __cplusplus } #endif diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig new file mode 100644 index 000000000..ef296f23d --- /dev/null +++ b/src/lib/allocator.zig @@ -0,0 +1,317 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const testing = std.testing; + +/// Useful alias since they're required to create Zig allocators +pub const ZigVTable = std.mem.Allocator.VTable; + +/// The VTable required by the C interface. +pub const VTable = extern struct { + alloc: *const fn (*anyopaque, len: usize, alignment: u8, ret_addr: usize) callconv(.c) ?[*]u8, + resize: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) bool, + remap: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) ?[*]u8, + free: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, ret_addr: usize) callconv(.c) void, +}; + +/// The Allocator interface for custom memory allocation strategies +/// within C libghostty APIs. +/// +/// This -- purposely -- matches the Zig allocator interface. We do this +/// for two reasons: (1) Zig's allocator interface is well proven in +/// the real world to be flexible and useful, and (2) it allows us to +/// easily convert C allocators to Zig allocators and vice versa, since +/// we're written in Zig. +pub const Allocator = extern struct { + ctx: *anyopaque, + vtable: *const VTable, + + /// vtable for the Zig allocator interface to map our extern + /// allocator to Zig's allocator interface. + pub const zig_vtable: ZigVTable = .{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }; + + /// Create a C allocator from a Zig allocator. This requires that + /// the Zig allocator be pointer-stable for the lifetime of the + /// C allocator. + pub fn fromZig(zig_alloc: *const std.mem.Allocator) Allocator { + return .{ + .ctx = @ptrCast(@constCast(zig_alloc)), + .vtable = &ZigAllocator.vtable, + }; + } + + /// Create a Zig allocator from this C allocator. This requires + /// a pointer to a Zig allocator vtable that we can populate with + /// our callbacks. + pub fn zig(self: *const Allocator) std.mem.Allocator { + return .{ + .ptr = @ptrCast(@constCast(self)), + .vtable = &zig_vtable, + }; + } + + fn alloc( + ctx: *anyopaque, + len: usize, + alignment: std.mem.Alignment, + ra: usize, + ) ?[*]u8 { + const self: *Allocator = @ptrCast(@alignCast(ctx)); + return self.vtable.alloc( + self.ctx, + len, + @intFromEnum(alignment), + ra, + ); + } + + fn resize( + ctx: *anyopaque, + old_mem: []u8, + alignment: std.mem.Alignment, + new_len: usize, + ra: usize, + ) bool { + const self: *Allocator = @ptrCast(@alignCast(ctx)); + return self.vtable.resize( + self.ctx, + old_mem.ptr, + old_mem.len, + @intFromEnum(alignment), + new_len, + ra, + ); + } + + fn remap( + ctx: *anyopaque, + old_mem: []u8, + alignment: std.mem.Alignment, + new_len: usize, + ra: usize, + ) ?[*]u8 { + const self: *Allocator = @ptrCast(@alignCast(ctx)); + return self.vtable.remap( + self.ctx, + old_mem.ptr, + old_mem.len, + @intFromEnum(alignment), + new_len, + ra, + ); + } + + fn free( + ctx: *anyopaque, + old_mem: []u8, + alignment: std.mem.Alignment, + ra: usize, + ) void { + const self: *Allocator = @ptrCast(@alignCast(ctx)); + self.vtable.free( + self.ctx, + old_mem.ptr, + old_mem.len, + @intFromEnum(alignment), + ra, + ); + } +}; + +/// An allocator implementation that wraps a Zig allocator so that +/// it can be exposed to C. +const ZigAllocator = struct { + const vtable: VTable = .{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }; + + fn alloc( + ctx: *anyopaque, + len: usize, + alignment: u8, + ra: usize, + ) callconv(.c) ?[*]u8 { + const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx)); + return zig_alloc.vtable.alloc( + zig_alloc.ptr, + len, + @enumFromInt(alignment), + ra, + ); + } + + fn resize( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + new_len: usize, + ra: usize, + ) callconv(.c) bool { + const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx)); + return zig_alloc.vtable.resize( + zig_alloc.ptr, + memory[0..memory_len], + @enumFromInt(alignment), + new_len, + ra, + ); + } + + fn remap( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + new_len: usize, + ra: usize, + ) callconv(.c) ?[*]u8 { + const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx)); + return zig_alloc.vtable.remap( + zig_alloc.ptr, + memory[0..memory_len], + @enumFromInt(alignment), + new_len, + ra, + ); + } + + fn free( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + ra: usize, + ) callconv(.c) void { + const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx)); + return zig_alloc.vtable.free( + zig_alloc.ptr, + memory[0..memory_len], + @enumFromInt(alignment), + ra, + ); + } +}; + +/// C allocator (libc) +pub const CAllocator = struct { + comptime { + if (!builtin.link_libc) { + @compileError("C allocator is only available when linking against libc"); + } + } + + const vtable: VTable = .{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }; + + fn alloc( + ctx: *anyopaque, + len: usize, + alignment: u8, + ra: usize, + ) callconv(.c) ?[*]u8 { + return std.heap.c_allocator.vtable.alloc( + ctx, + len, + @enumFromInt(alignment), + ra, + ); + } + + fn resize( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + new_len: usize, + ra: usize, + ) callconv(.c) bool { + return std.heap.c_allocator.vtable.resize( + ctx, + memory[0..memory_len], + @enumFromInt(alignment), + new_len, + ra, + ); + } + + fn remap( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + new_len: usize, + ra: usize, + ) callconv(.c) ?[*]u8 { + return std.heap.c_allocator.vtable.remap( + ctx, + memory[0..memory_len], + @enumFromInt(alignment), + new_len, + ra, + ); + } + + fn free( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + ra: usize, + ) callconv(.c) void { + std.heap.c_allocator.vtable.free( + ctx, + memory[0..memory_len], + @enumFromInt(alignment), + ra, + ); + } +}; + +pub const c_allocator: Allocator = .{ + .ctx = undefined, + .vtable = &CAllocator.vtable, +}; + +/// Allocator that can be sent to the C API that does full +/// leak checking within Zig tests. This should only be used from +/// Zig tests. +pub const test_allocator: Allocator = b: { + if (!builtin.is_test) @compileError("test_allocator can only be used in tests"); + break :b .fromZig(&testing.allocator); +}; + +test "c allocator" { + if (!comptime builtin.link_libc) return error.SkipZigTest; + + const alloc = c_allocator.zig(); + const str = try alloc.alloc(u8, 10); + defer alloc.free(str); + try testing.expectEqual(10, str.len); +} + +test "fba allocator" { + var buf: [1024]u8 = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buf); + const zig_alloc = fba.allocator(); + + // Convert the Zig allocator to a C interface + const c_alloc: Allocator = .fromZig(&zig_alloc); + + // Convert back to Zig so we can test it. + const alloc = c_alloc.zig(); + const str = try alloc.alloc(u8, 10); + defer alloc.free(str); + try testing.expectEqual(10, str.len); +} diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 98242ce78..19849805c 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -68,9 +68,16 @@ pub const Attribute = terminal.Attribute; comptime { // If we're building the C library (vs. the Zig module) then // we want to reference the C API so that it gets exported. - if (terminal.is_c_lib) _ = terminal.c_api; + if (terminal.is_c_lib) { + const c = terminal.c_api; + @export(&c.ghostty_vt_osc_new, .{ .name = "ghostty_vt_osc_new" }); + @export(&c.ghostty_vt_osc_free, .{ .name = "ghostty_vt_osc_free" }); + } } test { _ = terminal; + + // Tests always test the C API + _ = terminal.c_api; } diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig index 287711fec..079a16fb3 100644 --- a/src/terminal/c_api.zig +++ b/src/terminal/c_api.zig @@ -1,5 +1,44 @@ -pub export fn ghostty_hi() void { - // Does nothing, but you can see this symbol exists: - // nm -D --defined-only zig-out/lib/libghostty-vt.so | rg ' T ' - // This is temporary as we figure out the API. +const std = @import("std"); +const lib_alloc = @import("../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const osc = @import("osc.zig"); + +pub const GhosttyOscParser = extern struct { + parser: *osc.Parser, +}; + +pub const Result = enum(c_int) { + success = 0, + out_of_memory = -1, +}; + +pub fn ghostty_vt_osc_new( + c_alloc: *const CAllocator, + result: *GhosttyOscParser, +) callconv(.c) Result { + const alloc = c_alloc.zig(); + const ptr = alloc.create(osc.Parser) catch return .out_of_memory; + ptr.* = .initAlloc(alloc); + result.* = .{ .parser = ptr }; + return .success; +} + +pub fn ghostty_vt_osc_free(parser: GhosttyOscParser) callconv(.c) void { + const alloc = parser.parser.alloc.?; + parser.parser.deinit(); + alloc.destroy(parser.parser); +} + +test { + _ = lib_alloc; +} + +test "osc" { + const testing = std.testing; + var p: GhosttyOscParser = undefined; + try testing.expectEqual(Result.success, ghostty_vt_osc_new( + &lib_alloc.test_allocator, + &p, + )); + ghostty_vt_osc_free(p); } From e1429dabae7046c8cda2cd6097bda2eba9bd8761 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 09:42:20 -0700 Subject: [PATCH 12/27] example/c-vt --- example/c-vt/build.zig | 42 ++++++++++++++++++++++++++++++++++++++ example/c-vt/build.zig.zon | 24 ++++++++++++++++++++++ example/c-vt/src/main.c | 3 +++ include/ghostty-vt.h | 3 +++ 4 files changed, 72 insertions(+) create mode 100644 example/c-vt/build.zig create mode 100644 example/c-vt/build.zig.zon create mode 100644 example/c-vt/src/main.c diff --git a/example/c-vt/build.zig b/example/c-vt/build.zig new file mode 100644 index 000000000..b1ec9f5b1 --- /dev/null +++ b/example/c-vt/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt", + .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); +} diff --git a/example/c-vt/build.zig.zon b/example/c-vt/build.zig.zon new file mode 100644 index 000000000..3230f440e --- /dev/null +++ b/example/c-vt/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt, + .version = "0.0.0", + .fingerprint = 0x413a8529b1255f9a, + .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", + }, +} diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c new file mode 100644 index 000000000..45269d1d4 --- /dev/null +++ b/example/c-vt/src/main.c @@ -0,0 +1,3 @@ +int main() { + return 42; +} diff --git a/include/ghostty-vt.h b/include/ghostty-vt.h index b61069058..7bbbdf3da 100644 --- a/include/ghostty-vt.h +++ b/include/ghostty-vt.h @@ -21,6 +21,9 @@ typedef enum { GHOSTTY_VT_OUT_OF_MEMORY = -1, } GhosttyVtResult; +//------------------------------------------------------------------- +// Allocator Interface + typedef struct { /** * Return a pointer to `len` bytes with specified `alignment`, or return From de013148d3c2c6b59c5dd035a3a72dd6132445c0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 09:59:57 -0700 Subject: [PATCH 13/27] build: install the ghostty-vt artifact --- build.zig | 4 ++-- src/build/GhosttyLibVt.zig | 32 +++++++++----------------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/build.zig b/build.zig index 5cef2df02..c6c461b4c 100644 --- a/build.zig +++ b/build.zig @@ -102,8 +102,8 @@ pub fn build(b: *std.Build) !void { b, &mod, ); - libghostty_vt_shared.install(libvt_step, "libghostty-vt"); - libghostty_vt_shared.installHeader(libvt_step); + libghostty_vt_shared.install(libvt_step); + libghostty_vt_shared.install(b.getInstallStep()); // Helpgen if (config.emit_helpgen) deps.help_strings.install(); diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 85d71ae0f..eda49a38d 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -11,13 +11,13 @@ const LipoStep = @import("LipoStep.zig"); /// The step that generates the file. step: *std.Build.Step, +/// The artifact result +artifact: *std.Build.Step.InstallArtifact, + /// The final library file output: std.Build.LazyPath, dsym: ?std.Build.LazyPath, -/// Library extension, e.g. "dylib", "so", "dll" -ext: []const u8, - pub fn initShared( b: *std.Build, zig: *const GhosttyZig, @@ -27,6 +27,10 @@ pub fn initShared( .name = "ghostty-vt", .root_module = zig.vt, }); + lib.installHeader( + b.path("include/ghostty-vt.h"), + "ghostty-vt.h", + ); // Get our debug symbols const dsymutil: ?std.Build.LazyPath = dsymutil: { @@ -44,33 +48,15 @@ pub fn initShared( return .{ .step = &lib.step, + .artifact = b.addInstallArtifact(lib, .{}), .output = lib.getEmittedBin(), .dsym = dsymutil, - .ext = Config.sharedLibExt(target), }; } pub fn install( self: *const GhosttyLibVt, step: *std.Build.Step, - name: []const u8, ) void { - const b = self.step.owner; - const lib_install = b.addInstallLibFile( - self.output, - b.fmt("{s}.{s}", .{ name, self.ext }), - ); - step.dependOn(&lib_install.step); -} - -pub fn installHeader( - self: *const GhosttyLibVt, - step: *std.Build.Step, -) void { - const b = self.step.owner; - const header_install = b.addInstallHeaderFile( - b.path("include/ghostty-vt.h"), - "ghostty-vt.h", - ); - step.dependOn(&header_install.step); + step.dependOn(&self.artifact.step); } From 2c78ad8889dee68e4fd06a975593d0c8fc476210 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 10:19:13 -0700 Subject: [PATCH 14/27] lib-vt: setup a default allocator if null --- example/c-vt/src/main.c | 8 +++++++- include/ghostty-vt.h | 5 ++++- src/lib/allocator.zig | 16 ++++++++++++++++ src/terminal/c_api.zig | 5 +++-- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index 45269d1d4..406e617b6 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -1,3 +1,9 @@ +#include +#include + int main() { - return 42; + GhosttyOscParser parser; + ghostty_vt_osc_new(NULL, &parser); + ghostty_vt_osc_free(parser); + return 0; } diff --git a/include/ghostty-vt.h b/include/ghostty-vt.h index 7bbbdf3da..3c4f6212d 100644 --- a/include/ghostty-vt.h +++ b/include/ghostty-vt.h @@ -14,7 +14,7 @@ extern "C" { //------------------------------------------------------------------- // Types -typedef struct GhosttyOscParser GhosttyOscParser; +typedef struct GhosttyOscParser *GhosttyOscParser; typedef enum { GHOSTTY_VT_SUCCESS = 0, @@ -131,6 +131,9 @@ typedef struct { //------------------------------------------------------------------- // Functions +GhosttyVtResult ghostty_vt_osc_new(const GhosttyVtAllocator*, GhosttyOscParser*); +void ghostty_vt_osc_free(GhosttyOscParser); + #ifdef __cplusplus } #endif diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig index ef296f23d..de1453dbe 100644 --- a/src/lib/allocator.zig +++ b/src/lib/allocator.zig @@ -13,6 +13,22 @@ pub const VTable = extern struct { free: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, ret_addr: usize) callconv(.c) void, }; +/// Returns an allocator to use for the given possibly-null C allocator, +/// ensuring some allocator is always returned. +pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator { + // If we're given an allocator, use it. + if (c_alloc_) |c_alloc| return c_alloc.zig(); + + // If we have libc, use that. We prefer libc if we have it because + // its generally fast but also lets the embedder easily override + // malloc/free with custom allocators like mimalloc or something. + if (comptime builtin.link_libc) return std.heap.c_allocator; + + // No libc, use the preferred allocator for releases which is the + // Zig SMP allocator. + return std.heap.smp_allocator; +} + /// The Allocator interface for custom memory allocation strategies /// within C libghostty APIs. /// diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig index 079a16fb3..debffdc59 100644 --- a/src/terminal/c_api.zig +++ b/src/terminal/c_api.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const lib_alloc = @import("../lib/allocator.zig"); const CAllocator = lib_alloc.Allocator; const osc = @import("osc.zig"); @@ -13,10 +14,10 @@ pub const Result = enum(c_int) { }; pub fn ghostty_vt_osc_new( - c_alloc: *const CAllocator, + c_alloc_: ?*const CAllocator, result: *GhosttyOscParser, ) callconv(.c) Result { - const alloc = c_alloc.zig(); + const alloc = lib_alloc.default(c_alloc_); const ptr = alloc.create(osc.Parser) catch return .out_of_memory; ptr.* = .initAlloc(alloc); result.* = .{ .parser = ptr }; From e7a0198103a4fc9f9640be3b1f20e9a12aab9130 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 10:36:46 -0700 Subject: [PATCH 15/27] lib-vt: docs --- example/c-vt/README.md | 17 ++++++++++++ include/ghostty-vt.h | 62 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 example/c-vt/README.md diff --git a/example/c-vt/README.md b/example/c-vt/README.md new file mode 100644 index 000000000..e8a409761 --- /dev/null +++ b/example/c-vt/README.md @@ -0,0 +1,17 @@ +# Example: `ghostty-vt` C Program + +This contains a simple example of how to use the `ghostty-vt` C library +with a C program. + +This uses a `build.zig` and `Zig` to build the C program so that we +can reuse a lot of our build logic and depend directly on our source +tree, but Ghostty emits a standard C library that can be used with any +C tooling. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/include/ghostty-vt.h b/include/ghostty-vt.h index 3c4f6212d..0eacf064e 100644 --- a/include/ghostty-vt.h +++ b/include/ghostty-vt.h @@ -1,4 +1,12 @@ -// libghostty-vt +/** + * @file ghostty-vt.h + * + * libghostty-vt - Virtual terminal sequence parsing library + * + * This library provides functionality for parsing and handling terminal + * escape sequences as well as maintaining terminal state such as styles, + * cursor position, screen, scrollback, and more. + */ #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H @@ -14,16 +22,39 @@ extern "C" { //------------------------------------------------------------------- // Types +/** + * Opaque handle to an OSC parser instance. + * + * This handle represents an OSC (Operating System Command) parser that can + * be used to parse the contents of OSC sequences. This isn't a full VT + * parser; it is only the OSC parser component. This is useful if you have + * a parser already and want to only extract and handle OSC sequences. + */ typedef struct GhosttyOscParser *GhosttyOscParser; +/** + * Result codes for libghostty-vt operations. + */ typedef enum { + /** Operation completed successfully */ GHOSTTY_VT_SUCCESS = 0, + /** Operation failed due to failed allocation */ GHOSTTY_VT_OUT_OF_MEMORY = -1, } GhosttyVtResult; //------------------------------------------------------------------- // Allocator Interface +/** + * Function table for custom memory allocator operations. + * + * This vtable defines the interface for a custom memory allocator. All + * function pointers must be valid and non-NULL. + * + * NOTE(mitchellh): In the future, I think we can have default + * implementations if resize/remap are null. alloc/free must always + * be supplied. + */ typedef struct { /** * Return a pointer to `len` bytes with specified `alignment`, or return @@ -105,6 +136,11 @@ typedef struct { /** * Custom memory allocator. * + * For functions that take an allocator pointer, a NULL pointer indicates + * that the default allocator should be used. The default allocator will + * be libc malloc/free if we're linking to libc. If libc isn't linked, + * a custom allocator is used (currently Zig's SMP allocator). + * * Usage example: * @code * GhosttyVtAllocator allocator = { @@ -131,8 +167,28 @@ typedef struct { //------------------------------------------------------------------- // Functions -GhosttyVtResult ghostty_vt_osc_new(const GhosttyVtAllocator*, GhosttyOscParser*); -void ghostty_vt_osc_free(GhosttyOscParser); +/** + * Create a new OSC parser instance. + * + * Creates a new OSC (Operating System Command) parser using the provided + * allocator. The parser must be freed using ghostty_vt_osc_free() when + * no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param parser Pointer to store the created parser handle + * @return GHOSTTY_VT_SUCCESS on success, or an error code on failure + */ +GhosttyVtResult ghostty_vt_osc_new(const GhosttyVtAllocator* allocator, GhosttyOscParser* parser); + +/** + * Free an OSC parser instance. + * + * Releases all resources associated with the OSC parser. After this call, + * the parser handle becomes invalid and must not be used. + * + * @param parser The parser handle to free (may be NULL) + */ +void ghostty_vt_osc_free(GhosttyOscParser parser); #ifdef __cplusplus } From fba953feebfb466d36589b7340115e3af293bd67 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 10:46:42 -0700 Subject: [PATCH 16/27] ci: run the c-vt example --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55f609b61..ebba20867 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -95,7 +95,7 @@ jobs: strategy: fail-fast: false matrix: - dir: [zig-vt] + dir: [c-vt, zig-vt] name: Example ${{ matrix.dir }} runs-on: namespace-profile-ghostty-sm needs: test From 43089a01f1b33cb8e0f8410065b2c0fd54b884c0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 11:00:44 -0700 Subject: [PATCH 17/27] lib: c allocator can use fromZig --- src/lib/allocator.zig | 85 +------------------------------------------ 1 file changed, 2 insertions(+), 83 deletions(-) diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig index de1453dbe..41e579f27 100644 --- a/src/lib/allocator.zig +++ b/src/lib/allocator.zig @@ -216,89 +216,8 @@ const ZigAllocator = struct { } }; -/// C allocator (libc) -pub const CAllocator = struct { - comptime { - if (!builtin.link_libc) { - @compileError("C allocator is only available when linking against libc"); - } - } - - const vtable: VTable = .{ - .alloc = alloc, - .resize = resize, - .remap = remap, - .free = free, - }; - - fn alloc( - ctx: *anyopaque, - len: usize, - alignment: u8, - ra: usize, - ) callconv(.c) ?[*]u8 { - return std.heap.c_allocator.vtable.alloc( - ctx, - len, - @enumFromInt(alignment), - ra, - ); - } - - fn resize( - ctx: *anyopaque, - memory: [*]u8, - memory_len: usize, - alignment: u8, - new_len: usize, - ra: usize, - ) callconv(.c) bool { - return std.heap.c_allocator.vtable.resize( - ctx, - memory[0..memory_len], - @enumFromInt(alignment), - new_len, - ra, - ); - } - - fn remap( - ctx: *anyopaque, - memory: [*]u8, - memory_len: usize, - alignment: u8, - new_len: usize, - ra: usize, - ) callconv(.c) ?[*]u8 { - return std.heap.c_allocator.vtable.remap( - ctx, - memory[0..memory_len], - @enumFromInt(alignment), - new_len, - ra, - ); - } - - fn free( - ctx: *anyopaque, - memory: [*]u8, - memory_len: usize, - alignment: u8, - ra: usize, - ) callconv(.c) void { - std.heap.c_allocator.vtable.free( - ctx, - memory[0..memory_len], - @enumFromInt(alignment), - ra, - ); - } -}; - -pub const c_allocator: Allocator = .{ - .ctx = undefined, - .vtable = &CAllocator.vtable, -}; +/// libc Allocator, requires linking libc +pub const c_allocator: Allocator = .fromZig(&std.heap.c_allocator); /// Allocator that can be sent to the C API that does full /// leak checking within Zig tests. This should only be used from From 4d165fbaaa9810ac536c8fcfaea09bc32cb6e085 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 11:06:21 -0700 Subject: [PATCH 18/27] remove unused items --- src/build/Config.zig | 10 ---------- src/build/GhosttyExe.zig | 1 - src/terminal/c_api.zig | 16 +++++++++++++--- src/terminal/main.zig | 3 --- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/build/Config.zig b/src/build/Config.zig index 7dfdb913d..474674d3a 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -589,16 +589,6 @@ pub fn genericMacOSTarget( }); } -/// The extension to use for a shared library on the given target. -pub fn sharedLibExt(target: std.Build.ResolvedTarget) []const u8 { - return if (target.result.os.tag.isDarwin()) - "dylib" - else if (target.result.os.tag == .windows) - return "dll" - else - return "so"; -} - /// The possible entrypoints for the exe artifact. This has no effect on /// other artifact types (i.e. lib, wasm_module). /// diff --git a/src/build/GhosttyExe.zig b/src/build/GhosttyExe.zig index 1dfc4b4b7..083aecdb5 100644 --- a/src/build/GhosttyExe.zig +++ b/src/build/GhosttyExe.zig @@ -1,7 +1,6 @@ const Ghostty = @This(); const std = @import("std"); -const builtin = @import("builtin"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig index debffdc59..2ea0820f2 100644 --- a/src/terminal/c_api.zig +++ b/src/terminal/c_api.zig @@ -1,30 +1,40 @@ const std = @import("std"); +const assert = std.debug.assert; const builtin = @import("builtin"); const lib_alloc = @import("../lib/allocator.zig"); const CAllocator = lib_alloc.Allocator; const osc = @import("osc.zig"); +/// C: GhosttyOscParser pub const GhosttyOscParser = extern struct { parser: *osc.Parser, + + comptime { + // C API is an opaque pointer so the sizes must match. + assert(@sizeOf(@This()) == @sizeOf(usize)); + } }; +/// C: GhosttyResult pub const Result = enum(c_int) { success = 0, out_of_memory = -1, }; pub fn ghostty_vt_osc_new( - c_alloc_: ?*const CAllocator, + alloc_: ?*const CAllocator, result: *GhosttyOscParser, ) callconv(.c) Result { - const alloc = lib_alloc.default(c_alloc_); - const ptr = alloc.create(osc.Parser) catch return .out_of_memory; + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(osc.Parser) catch + return .out_of_memory; ptr.* = .initAlloc(alloc); result.* = .{ .parser = ptr }; return .success; } pub fn ghostty_vt_osc_free(parser: GhosttyOscParser) callconv(.c) void { + // C-built parsers always have an associated allocator. const alloc = parser.parser.alloc.?; parser.parser.deinit(); alloc.destroy(parser.parser); diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 4106786e1..4064c0c9c 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -64,9 +64,6 @@ pub const isSafePaste = sanitize.isSafePaste; /// This is set to true when we're building the C library. pub const is_c_lib = @import("root") == @import("../lib_vt.zig"); - -/// This is the C API for this package. Do NOT reference this unless -/// you want a bunch of symbols exported into your final artifact. pub const c_api = @import("c_api.zig"); test { From 0944f051aaf96e0a640ae798f4194b3777d4f5ce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 11:09:17 -0700 Subject: [PATCH 19/27] terminal: simplify opaque type --- src/terminal/c_api.zig | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig index 2ea0820f2..86ea5fed8 100644 --- a/src/terminal/c_api.zig +++ b/src/terminal/c_api.zig @@ -6,14 +6,7 @@ const CAllocator = lib_alloc.Allocator; const osc = @import("osc.zig"); /// C: GhosttyOscParser -pub const GhosttyOscParser = extern struct { - parser: *osc.Parser, - - comptime { - // C API is an opaque pointer so the sizes must match. - assert(@sizeOf(@This()) == @sizeOf(usize)); - } -}; +pub const GhosttyOscParser = *osc.Parser; /// C: GhosttyResult pub const Result = enum(c_int) { @@ -29,15 +22,15 @@ pub fn ghostty_vt_osc_new( const ptr = alloc.create(osc.Parser) catch return .out_of_memory; ptr.* = .initAlloc(alloc); - result.* = .{ .parser = ptr }; + result.* = ptr; return .success; } pub fn ghostty_vt_osc_free(parser: GhosttyOscParser) callconv(.c) void { // C-built parsers always have an associated allocator. - const alloc = parser.parser.alloc.?; - parser.parser.deinit(); - alloc.destroy(parser.parser); + const alloc = parser.alloc.?; + parser.deinit(); + alloc.destroy(parser); } test { From 513cdf667bb2fdd94eb00a7b3dc57df80f1531e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 12:22:04 -0700 Subject: [PATCH 20/27] build: add pkg-config for libghostty-vt --- build.zig | 2 ++ src/build/GhosttyLibVt.zig | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/build.zig b/build.zig index c6c461b4c..b515ba251 100644 --- a/build.zig +++ b/build.zig @@ -104,6 +104,8 @@ pub fn build(b: *std.Build) !void { ); libghostty_vt_shared.install(libvt_step); libghostty_vt_shared.install(b.getInstallStep()); + libghostty_vt_shared.installPkgConfig(libvt_step); + libghostty_vt_shared.installPkgConfig(b.getInstallStep()); // Helpgen if (config.emit_helpgen) deps.help_strings.install(); diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index eda49a38d..9c995952a 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -17,6 +17,7 @@ artifact: *std.Build.Step.InstallArtifact, /// The final library file output: std.Build.LazyPath, dsym: ?std.Build.LazyPath, +pkg_config: std.Build.LazyPath, pub fn initShared( b: *std.Build, @@ -46,11 +47,29 @@ pub fn initShared( break :dsymutil output; }; + // pkg-config + const pc: std.Build.LazyPath = pc: { + const wf = b.addWriteFiles(); + break :pc wf.add("libghostty-vt.pc", b.fmt( + \\prefix={s} + \\includedir=${{prefix}}/include + \\libdir=${{prefix}}/lib + \\ + \\Name: libghostty-vt + \\URL: https://github.com/ghostty-org/ghostty + \\Description: Ghostty VT library + \\Version: 0.1.0 + \\Cflags: -I${{includedir}} + \\Libs: -L${{libdir}} -lghostty-vt + , .{b.install_prefix})); + }; + return .{ .step = &lib.step, .artifact = b.addInstallArtifact(lib, .{}), .output = lib.getEmittedBin(), .dsym = dsymutil, + .pkg_config = pc, }; } @@ -60,3 +79,15 @@ pub fn install( ) void { step.dependOn(&self.artifact.step); } + +pub fn installPkgConfig( + self: *const GhosttyLibVt, + step: *std.Build.Step, +) void { + const b = step.owner; + step.dependOn(&b.addInstallFileWithDir( + self.pkg_config, + .prefix, + "share/pkgconfig/libghostty-vt.pc", + ).step); +} From 48827b21d817dd167955be41d060eb3aea2239d7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 12:24:47 -0700 Subject: [PATCH 21/27] some PR feedback --- build.zig | 2 -- example/c-vt/src/main.c | 4 +++- include/ghostty-vt.h | 4 ++-- src/build/GhosttyLibVt.zig | 8 +------- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/build.zig b/build.zig index b515ba251..c6c461b4c 100644 --- a/build.zig +++ b/build.zig @@ -104,8 +104,6 @@ pub fn build(b: *std.Build) !void { ); libghostty_vt_shared.install(libvt_step); libghostty_vt_shared.install(b.getInstallStep()); - libghostty_vt_shared.installPkgConfig(libvt_step); - libghostty_vt_shared.installPkgConfig(b.getInstallStep()); // Helpgen if (config.emit_helpgen) deps.help_strings.install(); diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index 406e617b6..f8ea2a9e5 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -3,7 +3,9 @@ int main() { GhosttyOscParser parser; - ghostty_vt_osc_new(NULL, &parser); + if (ghostty_vt_osc_new(NULL, &parser) != GHOSTTY_VT_SUCCESS) { + return 1; + } ghostty_vt_osc_free(parser); return 0; } diff --git a/include/ghostty-vt.h b/include/ghostty-vt.h index 0eacf064e..81f7bd4ba 100644 --- a/include/ghostty-vt.h +++ b/include/ghostty-vt.h @@ -15,9 +15,9 @@ extern "C" { #endif +#include #include #include -#include //------------------------------------------------------------------- // Types @@ -178,7 +178,7 @@ typedef struct { * @param parser Pointer to store the created parser handle * @return GHOSTTY_VT_SUCCESS on success, or an error code on failure */ -GhosttyVtResult ghostty_vt_osc_new(const GhosttyVtAllocator* allocator, GhosttyOscParser* parser); +GhosttyVtResult ghostty_vt_osc_new(const GhosttyVtAllocator *allocator, GhosttyOscParser *parser); /** * Free an OSC parser instance. diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 9c995952a..05cffd212 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -76,15 +76,9 @@ pub fn initShared( pub fn install( self: *const GhosttyLibVt, step: *std.Build.Step, -) void { - step.dependOn(&self.artifact.step); -} - -pub fn installPkgConfig( - self: *const GhosttyLibVt, - step: *std.Build.Step, ) void { const b = step.owner; + step.dependOn(&self.artifact.step); step.dependOn(&b.addInstallFileWithDir( self.pkg_config, .prefix, From 96e9053862799193e14cc9bd4369a0febe221b08 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 12:27:04 -0700 Subject: [PATCH 22/27] move header into subdirectory --- Doxyfile | 2 +- example/c-vt/src/main.c | 2 +- include/{ghostty-vt.h => ghostty/vt.h} | 2 +- src/build/GhosttyLibVt.zig | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename include/{ghostty-vt.h => ghostty/vt.h} (99%) diff --git a/Doxyfile b/Doxyfile index d6bf92a9a..fccd4a493 100644 --- a/Doxyfile +++ b/Doxyfile @@ -2,7 +2,7 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "libghostty" -INPUT = include/ghostty-vt.h +INPUT = include/ghostty/vt.h INPUT_ENCODING = UTF-8 RECURSIVE = NO diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index f8ea2a9e5..ed491de2c 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -1,5 +1,5 @@ #include -#include +#include int main() { GhosttyOscParser parser; diff --git a/include/ghostty-vt.h b/include/ghostty/vt.h similarity index 99% rename from include/ghostty-vt.h rename to include/ghostty/vt.h index 81f7bd4ba..cf47a34dc 100644 --- a/include/ghostty-vt.h +++ b/include/ghostty/vt.h @@ -1,5 +1,5 @@ /** - * @file ghostty-vt.h + * @file vt.h * * libghostty-vt - Virtual terminal sequence parsing library * diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 05cffd212..0029d6756 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -29,8 +29,8 @@ pub fn initShared( .root_module = zig.vt, }); lib.installHeader( - b.path("include/ghostty-vt.h"), - "ghostty-vt.h", + b.path("include/ghostty/vt.h"), + "ghostty/vt.h", ); // Get our debug symbols From 37e238c2f6921b1bb5163b6b0c3c4800f336efe5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 12:36:45 -0700 Subject: [PATCH 23/27] remove vt prefixes --- example/c-vt/src/main.c | 4 ++-- include/ghostty/vt.h | 18 +++++++++--------- src/lib_vt.zig | 4 ++-- src/terminal/c_api.zig | 14 +++++++------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index ed491de2c..1eaa659d2 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -3,9 +3,9 @@ int main() { GhosttyOscParser parser; - if (ghostty_vt_osc_new(NULL, &parser) != GHOSTTY_VT_SUCCESS) { + if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) { return 1; } - ghostty_vt_osc_free(parser); + ghostty_osc_free(parser); return 0; } diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index cf47a34dc..90700e9c4 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -37,10 +37,10 @@ typedef struct GhosttyOscParser *GhosttyOscParser; */ typedef enum { /** Operation completed successfully */ - GHOSTTY_VT_SUCCESS = 0, + GHOSTTY_SUCCESS = 0, /** Operation failed due to failed allocation */ - GHOSTTY_VT_OUT_OF_MEMORY = -1, -} GhosttyVtResult; + GHOSTTY_OUT_OF_MEMORY = -1, +} GhosttyResult; //------------------------------------------------------------------- // Allocator Interface @@ -131,7 +131,7 @@ typedef struct { * @param ret_addr First return address of the allocation call stack (0 if not provided) */ void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); -} GhosttyVtAllocatorVtable; +} GhosttyAllocatorVtable; /** * Custom memory allocator. @@ -143,7 +143,7 @@ typedef struct { * * Usage example: * @code - * GhosttyVtAllocator allocator = { + * GhosttyAllocator allocator = { * .vtable = &my_allocator_vtable, * .ctx = my_allocator_state * }; @@ -161,8 +161,8 @@ typedef struct { * Pointer to the allocator's vtable containing function pointers * for memory operations (alloc, resize, remap, free). */ - const GhosttyVtAllocatorVtable *vtable; -} GhosttyVtAllocator; + const GhosttyAllocatorVtable *vtable; +} GhosttyAllocator; //------------------------------------------------------------------- // Functions @@ -178,7 +178,7 @@ typedef struct { * @param parser Pointer to store the created parser handle * @return GHOSTTY_VT_SUCCESS on success, or an error code on failure */ -GhosttyVtResult ghostty_vt_osc_new(const GhosttyVtAllocator *allocator, GhosttyOscParser *parser); +GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); /** * Free an OSC parser instance. @@ -188,7 +188,7 @@ GhosttyVtResult ghostty_vt_osc_new(const GhosttyVtAllocator *allocator, GhosttyO * * @param parser The parser handle to free (may be NULL) */ -void ghostty_vt_osc_free(GhosttyOscParser parser); +void ghostty_osc_free(GhosttyOscParser parser); #ifdef __cplusplus } diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 19849805c..656509cce 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -70,8 +70,8 @@ comptime { // we want to reference the C API so that it gets exported. if (terminal.is_c_lib) { const c = terminal.c_api; - @export(&c.ghostty_vt_osc_new, .{ .name = "ghostty_vt_osc_new" }); - @export(&c.ghostty_vt_osc_free, .{ .name = "ghostty_vt_osc_free" }); + @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); + @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); } } diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig index 86ea5fed8..d4fc335e4 100644 --- a/src/terminal/c_api.zig +++ b/src/terminal/c_api.zig @@ -6,7 +6,7 @@ const CAllocator = lib_alloc.Allocator; const osc = @import("osc.zig"); /// C: GhosttyOscParser -pub const GhosttyOscParser = *osc.Parser; +pub const OscParser = *osc.Parser; /// C: GhosttyResult pub const Result = enum(c_int) { @@ -14,9 +14,9 @@ pub const Result = enum(c_int) { out_of_memory = -1, }; -pub fn ghostty_vt_osc_new( +pub fn osc_new( alloc_: ?*const CAllocator, - result: *GhosttyOscParser, + result: *OscParser, ) callconv(.c) Result { const alloc = lib_alloc.default(alloc_); const ptr = alloc.create(osc.Parser) catch @@ -26,7 +26,7 @@ pub fn ghostty_vt_osc_new( return .success; } -pub fn ghostty_vt_osc_free(parser: GhosttyOscParser) callconv(.c) void { +pub fn osc_free(parser: OscParser) callconv(.c) void { // C-built parsers always have an associated allocator. const alloc = parser.alloc.?; parser.deinit(); @@ -39,10 +39,10 @@ test { test "osc" { const testing = std.testing; - var p: GhosttyOscParser = undefined; - try testing.expectEqual(Result.success, ghostty_vt_osc_new( + var p: OscParser = undefined; + try testing.expectEqual(Result.success, osc_new( &lib_alloc.test_allocator, &p, )); - ghostty_vt_osc_free(p); + osc_free(p); } From 37372fa50bd5cd48125c9ed3abe378a8c3b15d1f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 12:38:14 -0700 Subject: [PATCH 24/27] more docs --- include/ghostty/vt.h | 19 ++++++++++++++++--- src/lib/allocator.zig | 3 +++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 90700e9c4..a1b1f221a 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -51,9 +51,22 @@ typedef enum { * This vtable defines the interface for a custom memory allocator. All * function pointers must be valid and non-NULL. * - * NOTE(mitchellh): In the future, I think we can have default - * implementations if resize/remap are null. alloc/free must always - * be supplied. + * If you're not going to use a custom allocator, you can ignore all of + * this. All functions that take an allocator pointer allow NULL to use a + * default allocator. + * + * The interface is based on the Zig allocator interface. I'll say up front + * that it is easy to look at this interface and think "wow, this is really + * overcomplicated". The reason for this complexity is well thought out by + * the Zig folks, and it enables a diverse set of allocation strategies + * as shown by the Zig ecosystem. As a consolation, please note that many + * of the arguments are only needed for advanced use cases and can be + * safely ignored in simple implementations. For example, if you look at + * the Zig implementation of the libc allocator in `lib/std/heap.zig` + * (search for CAllocator), you'll see it is very simple. + * + * NOTE(mitchellh): In the future, we can have default implementations of + * resize/remap and allow those to be null. */ typedef struct { /** diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig index 41e579f27..bcd7f9dcc 100644 --- a/src/lib/allocator.zig +++ b/src/lib/allocator.zig @@ -6,6 +6,7 @@ const testing = std.testing; pub const ZigVTable = std.mem.Allocator.VTable; /// The VTable required by the C interface. +/// C: GhosttyAllocatorVtable pub const VTable = extern struct { alloc: *const fn (*anyopaque, len: usize, alignment: u8, ret_addr: usize) callconv(.c) ?[*]u8, resize: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) bool, @@ -37,6 +38,8 @@ pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator { /// the real world to be flexible and useful, and (2) it allows us to /// easily convert C allocators to Zig allocators and vice versa, since /// we're written in Zig. +/// +/// C: GhosttyAllocator pub const Allocator = extern struct { ctx: *anyopaque, vtable: *const VTable, From 232b1898fa544e40de2e0d4e72c8db45258d35d4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Sep 2025 12:48:41 -0700 Subject: [PATCH 25/27] lib-vt: update header comments --- include/ghostty/vt.h | 13 ++++++++++++- src/terminal/c_api.zig | 5 +++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index a1b1f221a..12ed2d015 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -6,6 +6,9 @@ * This library provides functionality for parsing and handling terminal * escape sequences as well as maintaining terminal state such as styles, * cursor position, screen, scrollback, and more. + * + * WARNING: This is an incomplete, work-in-progress API. It is not yet + * stable and is definitely going to change. */ #ifndef GHOSTTY_VT_H @@ -65,6 +68,14 @@ typedef enum { * the Zig implementation of the libc allocator in `lib/std/heap.zig` * (search for CAllocator), you'll see it is very simple. * + * We chose to align with the Zig allocator interface because: + * + * 1. It is a proven interface that serves a wide variety of use cases + * in the real world via the Zig ecosystem. It's shown to work. + * + * 2. Our core implementation itself is Zig, and this lets us very + * cheaply and easily convert between C and Zig allocators. + * * NOTE(mitchellh): In the future, we can have default implementations of * resize/remap and allow those to be null. */ @@ -189,7 +200,7 @@ typedef struct { * * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator * @param parser Pointer to store the created parser handle - * @return GHOSTTY_VT_SUCCESS on success, or an error code on failure + * @return GHOSTTY_SUCCESS on success, or an error code on failure */ GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig index d4fc335e4..194a91d6d 100644 --- a/src/terminal/c_api.zig +++ b/src/terminal/c_api.zig @@ -6,7 +6,7 @@ const CAllocator = lib_alloc.Allocator; const osc = @import("osc.zig"); /// C: GhosttyOscParser -pub const OscParser = *osc.Parser; +pub const OscParser = ?*osc.Parser; /// C: GhosttyResult pub const Result = enum(c_int) { @@ -26,8 +26,9 @@ pub fn osc_new( return .success; } -pub fn osc_free(parser: OscParser) callconv(.c) void { +pub fn osc_free(parser_: OscParser) callconv(.c) void { // C-built parsers always have an associated allocator. + const parser = parser_ orelse return; const alloc = parser.alloc.?; parser.deinit(); alloc.destroy(parser); From 74419b2fcf8beec1e1dec9bfbef2fbfc812e59a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:08:57 +0000 Subject: [PATCH 26/27] build(deps): bump cachix/install-nix-action from 31.6.2 to 31.7.0 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.6.2 to 31.7.0. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md) - [Commits](https://github.com/cachix/install-nix-action/compare/a809471b5c7c913aa67bec8f459a11a0decc3fce...9280e7aca88deada44c930f1e2c78e21c3ae3edd) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-version: 31.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 4 +- .github/workflows/test.yml | 48 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index d6e06170c..09ec4aeed 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -42,7 +42,7 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index e90e79261..66dfe5fc2 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -89,7 +89,7 @@ jobs: /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index ec4cf5a07..853378d43 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -34,7 +34,7 @@ jobs: with: # Important so that build number generation works fetch-depth: 0 - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -168,7 +168,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ebba20867..9efd257ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,7 +80,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -114,7 +114,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -147,7 +147,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -181,7 +181,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -223,7 +223,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -259,7 +259,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -288,7 +288,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -321,7 +321,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -367,7 +367,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -579,7 +579,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -621,7 +621,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -669,7 +669,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -704,7 +704,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -768,7 +768,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -795,7 +795,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -823,7 +823,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -850,7 +850,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -877,7 +877,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -904,7 +904,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -931,7 +931,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -965,7 +965,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -992,7 +992,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1029,7 +1029,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1117,7 +1117,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 5226b9ef5..3f0d1d1e2 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -29,7 +29,7 @@ jobs: /zig - name: Setup Nix - uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2 + uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 From f8fa81293217b234b009341ec450bac9c09a6965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Milkovi=C4=87?= Date: Thu, 25 Sep 2025 11:45:41 +0200 Subject: [PATCH 27/27] i18n: add Croatian hr_HR translation (#8668) --- CODEOWNERS | 1 + po/hr_HR.UTF-8.po | 321 ++++++++++++++++++++++++++++++++++++++++ src/os/i18n_locales.zig | 1 + 3 files changed, 323 insertions(+) create mode 100644 po/hr_HR.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index 630296980..9e854b06c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -185,6 +185,7 @@ /po/he_IL.UTF-8.po @ghostty-org/he_IL /po/it_IT.UTF-8.po @ghostty-org/it_IT /po/zh_TW.UTF-8.po @ghostty-org/zh_TW +/po/hr_HR.UTF-8.po @ghostty-org/hr_HR # Packaging - Snap /snap/ @ghostty-org/snap diff --git a/po/hr_HR.UTF-8.po b/po/hr_HR.UTF-8.po new file mode 100644 index 000000000..f95ca469e --- /dev/null +++ b/po/hr_HR.UTF-8.po @@ -0,0 +1,321 @@ +# Croatian translations for com.mitchellh.ghostty package +# Hrvatski prijevod za paket com.mitchellh.ghostty. +# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Filip , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-07-22 17:18+0000\n" +"PO-Revision-Date: 2025-09-16 17:47+0200\n" +"Last-Translator: Filip7 \n" +"Language-Team: Croatian \n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Promijeni naslov terminala" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Ostavi prazno za povratak zadanog naslova." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Otkaži" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Greške u postavkama" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Pronađene su jedna ili više grešaka u postavkama. Pregledaj niže navedene greške" +"te ponovno učitaj postavke ili zanemari ove greške." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Zanemari" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Reload Configuration" +msgstr "Ponovno učitaj postavke" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Podijeli gore" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Podijeli dolje" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Podijeli lijevo" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Podijeli desno" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Izvrši naredbu…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Kopiraj" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 +msgid "Paste" +msgstr "Zalijepi" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Očisti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Resetiraj" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Podijeli" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Promijeni naslov…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Kartica" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:265 +msgid "New Tab" +msgstr "Nova kartica" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Zatvori karticu" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Prozor" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Novi prozor" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Zatvori prozor" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Postavke" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Otvori postavke" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Paleta naredbi" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Inspektor terminala" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 +msgid "About Ghostty" +msgstr "O Ghosttyju" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Izađi" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Dopusti pristup međuspremniku" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Program pokušava pročitati vrijednost međuspremnika. Trenutna" +"vrijednost međuspremnika je prikazana niže." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Odbij" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Dopusti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "Zapamti izbor za ovu podjelu" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "Ponovno učitaj postavke za prikaz ovog upita" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Aplikacija pokušava pisati u međuspremnik. Trenutačna vrijednost " +"međuspremnika prikazana je niže." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Upozorenje: Potencijalno opasno lijepljenje" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Lijepljenje ovog teksta u terminal može biti opasno jer se čini da " +"neke naredbe mogu biti izvršene." + +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 +msgid "Close" +msgstr "Zatvori" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Zatvori Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Zatvori prozor?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Zatvori karticu?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Zatvori podjelu?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Sve sesije terminala će biti prekinute." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Sve sesije terminala u ovom prozoru će biti prekinute." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Sve sesije terminala u ovoj kartici će biti prekinute." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Pokrenuti procesi u ovom odjeljku će biti prekinuti." + +#: src/apprt/gtk/Surface.zig:1266 +msgid "Copied to clipboard" +msgstr "Kopirano u međuspremnik" + +#: src/apprt/gtk/Surface.zig:1268 +msgid "Cleared clipboard" +msgstr "Očišćen međuspremnik" + +#: src/apprt/gtk/Surface.zig:2525 +msgid "Command succeeded" +msgstr "Naredba je uspjela" + +#: src/apprt/gtk/Surface.zig:2527 +msgid "Command failed" +msgstr "Naredba nije uspjela" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Glavni izbornik" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Pregledaj otvorene kartice" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Nova podjela" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Pokrenuta je debug verzija Ghosttyja! Performanse će biti smanjene." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Ponovno učitane postavke" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Razvijatelji Ghosttyja" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: inspektor terminala" diff --git a/src/os/i18n_locales.zig b/src/os/i18n_locales.zig index c01b15106..0fca223b9 100644 --- a/src/os/i18n_locales.zig +++ b/src/os/i18n_locales.zig @@ -51,4 +51,5 @@ pub const locales = [_][:0]const u8{ "hu_HU.UTF-8", "he_IL.UTF-8", "zh_TW.UTF-8", + "hr_HR.UTF-8", };