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 d12d6e67f..9efd257ca 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 @@ -79,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 @@ -94,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 @@ -113,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 @@ -146,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 @@ -180,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 @@ -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@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + 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 @@ -216,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 @@ -245,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 @@ -278,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 @@ -324,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 @@ -536,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 @@ -578,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 @@ -626,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 @@ -661,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 @@ -725,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 @@ -752,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 @@ -780,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 @@ -807,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 @@ -834,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 @@ -861,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 @@ -888,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 @@ -922,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 @@ -949,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 @@ -986,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 @@ -1074,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 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/Doxyfile b/Doxyfile new file mode 100644 index 000000000..fccd4a493 --- /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/build.zig b/build.zig index f34692b03..764db3438 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,14 @@ pub fn build(b: *std.Build) !void { &deps, ); + // libghostty-vt + const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared( + b, + &mod, + ); + libghostty_vt_shared.install(libvt_step); + libghostty_vt_shared.install(b.getInstallStep()); + // Helpgen if (config.emit_helpgen) deps.help_strings.install(); 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/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..1eaa659d2 --- /dev/null +++ b/example/c-vt/src/main.c @@ -0,0 +1,11 @@ +#include +#include + +int main() { + GhosttyOscParser parser; + if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) { + return 1; + } + ghostty_osc_free(parser); + return 0; +} diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h new file mode 100644 index 000000000..12ed2d015 --- /dev/null +++ b/include/ghostty/vt.h @@ -0,0 +1,221 @@ +/** + * @file 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. + * + * WARNING: This is an incomplete, work-in-progress API. It is not yet + * stable and is definitely going to change. + */ + +#ifndef GHOSTTY_VT_H +#define GHOSTTY_VT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +//------------------------------------------------------------------- +// 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_SUCCESS = 0, + /** Operation failed due to failed allocation */ + GHOSTTY_OUT_OF_MEMORY = -1, +} GhosttyResult; + +//------------------------------------------------------------------- +// 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. + * + * 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. + * + * 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. + */ +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); +} GhosttyAllocatorVtable; + +/** + * 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 + * GhosttyAllocator 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 GhosttyAllocatorVtable *vtable; +} GhosttyAllocator; + +//------------------------------------------------------------------- +// Functions + +/** + * 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_SUCCESS on success, or an error code on failure + */ +GhosttyResult ghostty_osc_new(const GhosttyAllocator *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_osc_free(GhosttyOscParser parser); + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_H */ 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/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/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 4202115e5..6ab3ad282 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -809,6 +809,10 @@ 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); + const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background; try writer.print( @@ -847,9 +851,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); diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig new file mode 100644 index 000000000..0029d6756 --- /dev/null +++ b/src/build/GhosttyLibVt.zig @@ -0,0 +1,87 @@ +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 artifact result +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, + zig: *const GhosttyZig, +) !GhosttyLibVt { + const target = zig.vt.resolved_target.?; + const lib = b.addSharedLibrary(.{ + .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: { + if (!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; + }; + + // 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, + }; +} + +pub fn install( + 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, + "share/pkgconfig/libghostty-vt.pc", + ).step); +} diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 5faf3ba44..9461d48b7 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"); @@ -114,6 +116,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); 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/cli/CommaSplitter.zig b/src/cli/CommaSplitter.zig new file mode 100644 index 000000000..3168c1ffa --- /dev/null +++ b/src/cli/CommaSplitter.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 CommaSplitter = @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) CommaSplitter { + return .{ + .str = str, + .index = 0, + }; +} + +/// return the next field, null if no more fields +pub fn next(self: *CommaSplitter) 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: *CommaSplitter) ?[]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: 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()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 2" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init(""); + try testing.expect(null == try s.next()); +} + +test "splitter 3" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .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: CommaSplitter = .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: 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()); +} + +test "splitter 6" { + const std = @import("std"); + const testing = std.testing; + + 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()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 7" { + const std = @import("std"); + const testing = std.testing; + + 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()); +} + +test "splitter 8" { + const std = @import("std"); + const testing = std.testing; + + 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()); +} + +test "splitter 9" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("\\x"); + try testing.expectError(error.UnfinishedEscape, s.next()); +} + +test "splitter 10" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("\\x5"); + try testing.expectError(error.UnfinishedEscape, s.next()); +} + +test "splitter 11" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("\\u"); + try testing.expectError(error.UnfinishedEscape, s.next()); +} + +test "splitter 12" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("\\u{"); + try testing.expectError(error.UnfinishedEscape, s.next()); +} + +test "splitter 13" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("\\u{}"); + try testing.expectError(error.IllegalEscape, s.next()); +} + +test "splitter 14" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("\\u{h1}"); + try testing.expectError(error.IllegalEscape, s.next()); +} + +test "splitter 15" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .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: CommaSplitter = .init("\\u{110000}"); + try testing.expectError(error.IllegalEscape, s.next()); +} + +test "splitter 17" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("\\d"); + try testing.expectError(error.IllegalEscape, s.next()); +} + +test "splitter 18" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .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: CommaSplitter = .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: CommaSplitter = .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: 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()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 22" { + const std = @import("std"); + const testing = std.testing; + + var s: CommaSplitter = .init("abc\"def"); + try testing.expectError(error.UnclosedQuote, s.next()); +} + +test "splitter 23" { + const std = @import("std"); + const testing = std.testing; + + 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()).?); + try testing.expect(null == try s.next()); +} + +test "splitter 24" { + const std = @import("std"); + const testing = std.testing; + + 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().?); + try testing.expect(null == try s.next()); +} + +test "splitter 25" { + const std = @import("std"); + const testing = std.testing; + + 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 4db0a29a2..2d2d199be 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 CommaSplitter = @import("CommaSplitter.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: 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. 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..66e63fd3f 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 @@ -3351,7 +3363,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(); @@ -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 225ef0ae4..642044067 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}), @@ -3227,3 +3227,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, diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig new file mode 100644 index 000000000..bcd7f9dcc --- /dev/null +++ b/src/lib/allocator.zig @@ -0,0 +1,255 @@ +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. +/// 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, + 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, +}; + +/// 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. +/// +/// 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. +/// +/// C: GhosttyAllocator +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, + ); + } +}; + +/// 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 +/// 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 d375a89d2..656509cce 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -65,6 +65,19 @@ pub const EraseLine = terminal.EraseLine; pub const TabClear = terminal.TabClear; 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) { + const c = terminal.c_api; + @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); + @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); + } +} + test { _ = terminal; + + // Tests always test the C API + _ = terminal.c_api; } 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", }; diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig new file mode 100644 index 000000000..194a91d6d --- /dev/null +++ b/src/terminal/c_api.zig @@ -0,0 +1,49 @@ +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 OscParser = ?*osc.Parser; + +/// C: GhosttyResult +pub const Result = enum(c_int) { + success = 0, + out_of_memory = -1, +}; + +pub fn osc_new( + alloc_: ?*const CAllocator, + result: *OscParser, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(osc.Parser) catch + return .out_of_memory; + ptr.* = .initAlloc(alloc); + result.* = ptr; + return .success; +} + +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); +} + +test { + _ = lib_alloc; +} + +test "osc" { + const testing = std.testing; + var p: OscParser = undefined; + try testing.expectEqual(Result.success, osc_new( + &lib_alloc.test_allocator, + &p, + )); + osc_free(p); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 1fea9934e..4064c0c9c 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -62,6 +62,10 @@ 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"); +pub const c_api = @import("c_api.zig"); + test { @import("std").testing.refAllDecls(@This());