diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 09ec4aeed..ef6f96555 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,7 +36,7 @@ jobs: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 66dfe5fc2..af912215c 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 853378d43..7f7b85e2f 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -163,7 +163,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 6feb39887..4e9aa168c 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -38,7 +38,7 @@ jobs: tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9efd257ca..1638b0fd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,7 +73,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -107,7 +107,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -140,7 +140,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -174,7 +174,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -216,7 +216,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -252,7 +252,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -281,7 +281,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -314,7 +314,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -360,7 +360,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -572,7 +572,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -614,7 +614,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -662,7 +662,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -697,7 +697,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -761,7 +761,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -790,7 +790,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -818,7 +818,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -845,7 +845,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -872,7 +872,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -899,7 +899,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -926,7 +926,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -960,7 +960,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -987,7 +987,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -1022,7 +1022,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix @@ -1110,7 +1110,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 3f0d1d1e2..4e9db4225 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 with: path: | /nix diff --git a/build.zig b/build.zig index c6c461b4c..62fa77511 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,10 @@ comptime { } pub fn build(b: *std.Build) !void { + // Works around a Zig but still present in 0.15.1. Remove when fixed. + // https://github.com/ghostty-org/ghostty/issues/8924 + try limitCoresForZigBug(); + // This defines all the available build options (e.g. `-D`). If you // want to know what options are available, you can run `--help` or // you can read `src/build/Config.zig`. @@ -251,6 +255,15 @@ pub fn build(b: *std.Build) !void { }); const mod_vt_test_run = b.addRunArtifact(mod_vt_test); test_lib_vt_step.dependOn(&mod_vt_test_run.step); + + const mod_vt_c_test = b.addTest(.{ + .root_module = mod.vt_c, + .target = config.target, + .optimize = config.optimize, + .filters = test_filters, + }); + const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test); + test_lib_vt_step.dependOn(&mod_vt_c_test_run.step); } // Tests @@ -298,3 +311,13 @@ pub fn build(b: *std.Build) !void { try translations_step.addError("cannot update translations when i18n is disabled", .{}); } } + +// WARNING: Remove this when https://github.com/ghostty-org/ghostty/issues/8924 is resolved! +// Limit ourselves to 32 cpus on Linux because of an upstream Zig bug. +fn limitCoresForZigBug() !void { + if (comptime builtin.os.tag != .linux) return; + const pid = std.os.linux.getpid(); + var set: std.bit_set.ArrayBitSet(usize, std.os.linux.CPU_SETSIZE * 8) = .initEmpty(); + for (0..32) |cpu| set.set(cpu); + try std.os.linux.sched_setaffinity(pid, &set.masks); +} diff --git a/build.zig.zon b/build.zig.zon index 2114dad3f..0e5fdfb1f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,6 +3,7 @@ .version = "1.2.1", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, + .minimum_zig_version = "0.14.1", .dependencies = .{ // Zig libs @@ -119,8 +120,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - .hash = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + .hash = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 357c1ce99..ac4098f96 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3": { + "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - "hash": "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + "hash": "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index e82bbed7a..24bb3b258 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3"; + name = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz"; - hash = "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz"; + hash = "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 903fcbf99..5b5e542dd 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -29,7 +29,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst https://github.com/jacobsandlund/uucode/archive/f748edb9639d9b3c8ae13d6190953f419a615343.tar.gz -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz +https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index 1eaa659d2..b1297d7a7 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -1,4 +1,6 @@ #include +#include +#include #include int main() { @@ -6,6 +8,29 @@ int main() { if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) { return 1; } + + // Setup change window title command to change the title to "hello" + ghostty_osc_next(parser, '0'); + ghostty_osc_next(parser, ';'); + const char *title = "hello"; + for (size_t i = 0; i < strlen(title); i++) { + ghostty_osc_next(parser, title[i]); + } + + // End parsing and get command + GhosttyOscCommand command = ghostty_osc_end(parser, 0); + + // Get and print command type + GhosttyOscCommandType type = ghostty_osc_command_type(command); + printf("Command type: %d\n", type); + + // Extract and print the title + if (ghostty_osc_command_data(command, GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR, &title)) { + printf("Extracted title: %s\n", title); + } else { + printf("Failed to extract title\n"); + } + ghostty_osc_free(parser); return 0; } diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 4bd18311b..824f76adc 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - "dest": "vendor/p/N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3", - "sha256": "24f63d339d1dfe7eab1b35add1a419214ec804c5abbb6200a9ef55bb5c7908cc" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250922-150534-d28055b/ghostty-themes.tgz", + "dest": "vendor/p/N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", + "sha256": "99d854c4002a2b14515f0103da569a6d4a3d659a996bf31902c3b4f98f4beee4" }, { "type": "archive", diff --git a/include/ghostty.h b/include/ghostty.h index 7888b380c..3f1e0c9d9 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -353,6 +353,7 @@ typedef struct { typedef struct { const char* ptr; uintptr_t len; + bool sentinel; } ghostty_string_s; typedef struct { diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 12ed2d015..4b930a96f 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -11,6 +11,27 @@ * stable and is definitely going to change. */ +/** + * @mainpage libghostty-vt - Virtual Terminal Sequence Parser + * + * libghostty-vt is a C library which implements a modern terminal emulator, + * extracted from the [Ghostty](https://ghostty.org) terminal emulator. + * + * libghostty-vt contains the logic for handling the core parts of a terminal + * emulator: parsing terminal escape sequences and maintaining terminal state. + * It can handle scrollback, line wrapping, reflow on resize, and more. + * + * @warning This library is currently in development and the API is not yet stable. + * Breaking changes are expected in future versions. Use with caution in production code. + * + * @section groups_sec API Reference + * + * The API is organized into the following groups: + * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * - @ref allocator "Memory Management" - Memory management and custom allocators + * + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H @@ -32,9 +53,22 @@ extern "C" { * 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. + * + * @ingroup osc */ typedef struct GhosttyOscParser *GhosttyOscParser; +/** + * Opaque handle to a single OSC command. + * + * This handle represents a parsed OSC (Operating System Command) command. + * The command can be queried for its type and associated data using + * `ghostty_osc_command_type` and `ghostty_osc_command_data`. + * + * @ingroup osc + */ +typedef struct GhosttyOscCommand *GhosttyOscCommand; + /** * Result codes for libghostty-vt operations. */ @@ -45,15 +79,107 @@ typedef enum { GHOSTTY_OUT_OF_MEMORY = -1, } GhosttyResult; +/** + * OSC command types. + * + * @ingroup osc + */ +typedef enum { + GHOSTTY_OSC_COMMAND_INVALID = 0, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, + GHOSTTY_OSC_COMMAND_PROMPT_START = 3, + GHOSTTY_OSC_COMMAND_PROMPT_END = 4, + GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5, + GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6, + GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7, + GHOSTTY_OSC_COMMAND_REPORT_PWD = 8, + GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9, + GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10, + GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11, + GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12, + GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13, + GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14, + GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15, + GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16, + GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17, + GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18, + GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19, + GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20, +} GhosttyOscCommandType; + +/** + * OSC command data types. + * + * These values specify what type of data to extract from an OSC command + * using `ghostty_osc_command_data`. + * + * @ingroup osc + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_OSC_DATA_INVALID = 0, + + /** + * Window title string data. + * + * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE + * + * Output type: const char ** (pointer to null-terminated string) + * + * Lifetime: Valid until the next call to any ghostty_osc_* function with + * the same parser instance. Memory is owned by the parser. + */ + GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, +} GhosttyOscCommandData; + //------------------------------------------------------------------- // Allocator Interface +/** @defgroup allocator Memory Management + * + * libghostty-vt does require memory allocation for various operations, + * but is resilient to allocation failures and will gracefully handle + * out-of-memory situations by returning error codes. + * + * The exact memory management semantics are documented in the relevant + * functions and data structures. + * + * libghostty-vt uses explicit memory allocation via an allocator + * interface provided by GhosttyAllocator. The interface is based on the + * [Zig](https://ziglang.org) allocator interface, since this has been + * shown to be a flexible and powerful interface in practice and enables + * a wide variety of allocation strategies. + * + * **For the common case, you can pass NULL as the allocator for any + * function that accepts one,** and libghostty will use a default allocator. + * The default allocator will be libc malloc/free if libc is linked. + * Otherwise, a custom allocator is used (currently Zig's SMP allocator) + * that doesn't require any external dependencies. + * + * ## Basic Usage + * + * For simple use cases, you can ignore this interface entirely by passing NULL + * as the allocator parameter to functions that accept one. This will use the + * default allocator (typically libc malloc/free, if libc is linked, but + * we provide our own default allocator if libc isn't linked). + * + * To use a custom allocator: + * 1. Implement the GhosttyAllocatorVtable function pointers + * 2. Create a GhosttyAllocator struct with your vtable and context + * 3. Pass the allocator to functions that accept one + * + * @{ + */ + /** * 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. * + * @ingroup allocator + * * 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. @@ -165,6 +291,8 @@ typedef struct { * 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). * + * @ingroup allocator + * * Usage example: * @code * GhosttyAllocator allocator = { @@ -188,9 +316,32 @@ typedef struct { const GhosttyAllocatorVtable *vtable; } GhosttyAllocator; +/** @} */ // end of allocator group + //------------------------------------------------------------------- // Functions +/** @defgroup osc OSC Parser + * + * OSC (Operating System Command) sequence parser and command handling. + * + * The parser operates in a streaming fashion, processing input byte-by-byte + * to handle OSC sequences that may arrive in fragments across multiple reads. + * This interface makes it easy to integrate into most environments and avoids + * over-allocating buffers. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_osc_new() + * 2. Feed bytes to the parser using ghostty_osc_next() + * 3. Finalize parsing with ghostty_osc_end() to get the command + * 4. Query command type and extract data using ghostty_osc_command_type() + * and ghostty_osc_command_data() + * 5. Free the parser with ghostty_osc_free() when done + * + * @{ + */ + /** * Create a new OSC parser instance. * @@ -214,6 +365,89 @@ GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParse */ void ghostty_osc_free(GhosttyOscParser parser); +/** + * Reset an OSC parser instance to its initial state. + * + * Resets the parser state, clearing any partially parsed OSC sequences + * and returning the parser to its initial state. This is useful for + * reusing a parser instance or recovering from parse errors. + * + * @param parser The parser handle to reset, must not be null. + */ +void ghostty_osc_reset(GhosttyOscParser parser); + +/** + * Parse the next byte in an OSC sequence. + * + * Processes a single byte as part of an OSC sequence. The parser maintains + * internal state to track the progress through the sequence. Call this + * function for each byte in the sequence data. + * + * When finished pumping the parser with bytes, call ghostty_osc_end + * to get the final result. + * + * @param parser The parser handle, must not be null. + * @param byte The next byte to parse + */ +void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); + +/** + * Finalize OSC parsing and retrieve the parsed command. + * + * Call this function after feeding all bytes of an OSC sequence to the parser + * using ghostty_osc_next() with the exception of the terminating character + * (ESC or ST). This function finalizes the parsing process and returns the + * parsed OSC command. + * + * The return value is never NULL. Invalid commands will return a command + * with type GHOSTTY_OSC_COMMAND_INVALID. + * + * The terminator parameter specifies the byte that terminated the OSC sequence + * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is + * preserved in the parsed command so that responses can use the same terminator + * format for better compatibility with the calling program. For commands that + * do not require a response, this parameter is ignored and the resulting + * command will not retain the terminator information. + * + * The returned command handle is valid until the next call to any + * `ghostty_osc_*` function with the same parser instance with the exception + * of command introspection functions such as `ghostty_osc_command_type`. + * + * @param parser The parser handle, must not be null. + * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) + * @return Handle to the parsed OSC command + */ +GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); + +/** + * Get the type of an OSC command. + * + * Returns the type identifier for the given OSC command. This can be used + * to determine what kind of command was parsed and what data might be + * available from it. + * + * @param command The OSC command handle to query (may be NULL) + * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL + */ +GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); + +/** + * Extract data from an OSC command. + * + * Extracts typed data from the given OSC command based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid command types, output types, and memory + * safety information are documented in the `GhosttyOscCommandData` enum. + * + * @param command The OSC command handle to query (may be NULL) + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return true if data extraction was successful, false otherwise + */ +bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); + +/** @} */ // end of osc group + #ifdef __cplusplus } #endif diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index f7242ee56..46a752198 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -43,11 +43,13 @@ struct NewTerminalIntent: AppIntent { ) var parent: TerminalEntity? + // Performing in the background can avoid opening multiple windows at the same time + // using `foreground` will cause `perform` and `AppDelegate.applicationDidBecomeActive(_:)`/`AppDelegate.applicationShouldHandleReopen(_:hasVisibleWindows:)` running at the 'same' time @available(macOS 26.0, *) - static var supportedModes: IntentModes = .foreground(.immediate) + static var supportedModes: IntentModes = .background @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes") - static var openAppWhenRun = true + static var openAppWhenRun = false @MainActor func perform() async throws -> some IntentResult & ReturnsValue { @@ -96,6 +98,11 @@ struct NewTerminalIntent: AppIntent { parent = nil } + defer { + if !NSApp.isActive { + NSApp.activate(ignoringOtherApps: true) + } + } switch location { case .window: let newController = TerminalController.newWindow( diff --git a/pkg/freetype/main.zig b/pkg/freetype/main.zig index b39650423..6ec818181 100644 --- a/pkg/freetype/main.zig +++ b/pkg/freetype/main.zig @@ -9,6 +9,7 @@ pub const Library = @import("Library.zig"); pub const Error = errors.Error; pub const Face = face.Face; +pub const LoadFlags = face.LoadFlags; pub const Tag = tag.Tag; pub const mulFix = computations.mulFix; diff --git a/src/Surface.zig b/src/Surface.zig index 8edeadf83..03974dfc6 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -260,7 +260,7 @@ const DerivedConfig = struct { font: font.SharedGridSet.DerivedConfig, mouse_interval: u64, mouse_hide_while_typing: bool, - mouse_scroll_multiplier: f64, + mouse_scroll_multiplier: configpkg.MouseScrollMultiplier, mouse_shift_capture: configpkg.MouseShiftCapture, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, macos_option_as_alt: ?configpkg.OptionAsAlt, @@ -2829,7 +2829,7 @@ pub fn scrollCallback( // scroll events to pixels by multiplying the wheel tick value and the cell size. This means // that a wheel tick of 1 results in single scroll event. const yoff_adjusted: f64 = if (scroll_mods.precision) - yoff + yoff * self.config.mouse_scroll_multiplier.precision else yoff_adjusted: { // Round out the yoff to an absolute minimum of 1. macos tries to // simulate precision scrolling with non precision events by @@ -2843,7 +2843,7 @@ pub fn scrollCallback( else @min(yoff, -1); - break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier; + break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier.discrete; }; // Add our previously saved pending amount to the offset to get the diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 6ab3ad282..f7ed0d38c 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -524,13 +524,19 @@ pub const Application = extern struct { if (!config.@"quit-after-last-window-closed") break :q false; // If the quit timer has expired, quit. - if (priv.quit_timer == .expired) break :q true; + if (priv.quit_timer == .expired) { + log.debug("must_quit due to quit timer expired", .{}); + break :q true; + } // If we have no windows attached to our app, also quit. if (priv.requested_window and @as( ?*glib.List, self.as(gtk.Application).getWindows(), - ) == null) break :q true; + ) == null) { + log.debug("must_quit due to no app windows", .{}); + break :q true; + } // No quit conditions met break :q false; diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index fb933073c..344bf8f21 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -51,6 +51,13 @@ pub const Surface = extern struct { pub const Tree = datastruct.SplitTree(Self); pub const properties = struct { + /// This property is set to true when the bell is ringing. Note that + /// this property will only emit a changed signal when there is a + /// full state change. If a bell is ringing and another bell event + /// comes through, the change notification will NOT be emitted. + /// + /// If you need to know every scenario the bell is triggered, + /// listen to the `bell` signal instead. pub const @"bell-ringing" = struct { pub const name = "bell-ringing"; const impl = gobject.ext.defineProperty( @@ -296,6 +303,19 @@ pub const Surface = extern struct { }; pub const signals = struct { + /// Emitted whenever the bell event is received. Unlike the + /// `bell-ringing` property, this is emitted every time the event + /// is received and not just on state changes. + pub const bell = struct { + pub const name = "bell"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; /// Emitted whenever the surface would like to be closed for any /// reason. /// @@ -1674,6 +1694,16 @@ pub const Surface = extern struct { } pub fn setBellRinging(self: *Self, ringing: bool) void { + // Prevent duplicate change notifications if the signals we emit + // in this function cause this state to change again. + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + + // Logic around bell reaction happens on every event even if we're + // already in the ringing state. + if (ringing) self.ringBell(); + + // Property change only happens on actual state change const priv = self.private(); if (priv.bell_ringing == ringing) return; priv.bell_ringing = ringing; @@ -1858,20 +1888,26 @@ pub const Surface = extern struct { self.as(gtk.Widget).setCursorFromName(name.ptr); } - fn propBellRinging( - self: *Self, - _: *gobject.ParamSpec, - _: ?*anyopaque, - ) callconv(.c) void { + /// Handle bell features that need to happen every time a BEL is received + /// Currently this is audio and system but this could change in the future. + fn ringBell(self: *Self) void { const priv = self.private(); - if (!priv.bell_ringing) return; + + // Emit the signal + signals.bell.impl.emit( + self, + null, + .{}, + null, + ); // Activate actions if they exist _ = self.as(gtk.Widget).activateAction("tab.ring-bell", null); _ = self.as(gtk.Widget).activateAction("win.ring-bell", null); - // Do our sound const config = if (priv.config) |c| c.get() else return; + + // Do our sound if (config.@"bell-features".audio) audio: { const config_path = config.@"bell-audio-path" orelse break :audio; const path, const required = switch (config_path) { @@ -2859,7 +2895,6 @@ pub const Surface = extern struct { class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); - class.bindTemplateCallback("notify_bell_ringing", &propBellRinging); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown); @@ -2884,6 +2919,7 @@ pub const Surface = extern struct { }); // Signals + signals.bell.impl.register(.{}); signals.@"close-request".impl.register(.{}); signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-write".impl.register(.{}); diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index df6ea647f..c0dd6ab1f 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -1489,6 +1489,13 @@ pub const Window = extern struct { const priv = self.private(); if (priv.tab_view.getNPages() == 0) { // If we have no pages left then we want to close window. + + // If the tab overview is open, then we don't close the window + // because its a rather abrupt experience. This also fixes an + // issue where dragging out the last tab in the tab overview + // won't cause Ghostty to exit. + if (priv.tab_overview.getOpen() != 0) return; + self.as(gtk.Window).close(); } } diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index ad971e991..7ed78ecb3 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -169,7 +169,6 @@ template $GhosttySurface: Adw.Bin { "surface", ] - notify::bell-ringing => $notify_bell_ringing(); notify::config => $notify_config(); notify::error => $notify_error(); notify::mouse-hover-url => $notify_mouse_hover_url(); diff --git a/src/apprt/gtk/ui/1.5/surface-title-dialog.blp b/src/apprt/gtk/ui/1.5/surface-title-dialog.blp index 24ae26f37..90d9f9c0b 100644 --- a/src/apprt/gtk/ui/1.5/surface-title-dialog.blp +++ b/src/apprt/gtk/ui/1.5/surface-title-dialog.blp @@ -6,11 +6,14 @@ template $GhosttySurfaceTitleDialog: Adw.AlertDialog { body: _("Leave blank to restore the default title."); responses [ - cancel: _("Cancel") suggested, - ok: _("OK") destructive, + cancel: _("Cancel"), + ok: _("OK") suggested, ] + default-response: "ok"; focus-widget: entry; - extra-child: Entry entry {}; + extra-child: Entry entry { + activates-default: true; + }; } diff --git a/src/build/Config.zig b/src/build/Config.zig index 474674d3a..0b7dae14d 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -498,6 +498,7 @@ pub fn terminalOptions(self: *const Config) TerminalBuildOptions { .artifact = .ghostty, .simd = self.simd, .oniguruma = true, + .c_abi = false, .slow_runtime_safety = switch (self.optimize) { .Debug => true, .ReleaseSafe, diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 0029d6756..80f2bf9ad 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -26,7 +26,7 @@ pub fn initShared( const target = zig.vt.resolved_target.?; const lib = b.addSharedLibrary(.{ .name = "ghostty-vt", - .root_module = zig.vt, + .root_module = zig.vt_c, }); lib.installHeader( b.path("include/ghostty/vt.h"), diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig index f175eb957..a8d2726bc 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig @@ -5,18 +5,17 @@ const GhosttyZig = @This(); const std = @import("std"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); +const TerminalBuildOptions = @import("../terminal/build_options.zig").Options; +/// The `_c`-suffixed modules are built with the C ABI enabled. vt: *std.Build.Module, +vt_c: *std.Build.Module, pub fn init( b: *std.Build, cfg: *const Config, deps: *const SharedDeps, ) !GhosttyZig { - // General build options - const general_options = b.addOptions(); - try cfg.addOptions(general_options); - // Terminal module build options var vt_options = cfg.terminalOptions(); vt_options.artifact = .lib; @@ -25,7 +24,41 @@ pub fn init( // conditionally do this. vt_options.oniguruma = false; - const vt = b.addModule("ghostty-vt", .{ + return .{ + .vt = try initVt( + "ghostty-vt", + b, + cfg, + deps, + vt_options, + ), + + .vt_c = try initVt( + "ghostty-vt-c", + b, + cfg, + deps, + options: { + var dup = vt_options; + dup.c_abi = true; + break :options dup; + }, + ), + }; +} + +fn initVt( + name: []const u8, + b: *std.Build, + cfg: *const Config, + deps: *const SharedDeps, + vt_options: TerminalBuildOptions, +) !*std.Build.Module { + // General build options + const general_options = b.addOptions(); + try cfg.addOptions(general_options); + + const vt = b.addModule(name, .{ .root_source_file = b.path("src/lib_vt.zig"), .target = cfg.target, .optimize = cfg.optimize, @@ -45,5 +78,5 @@ pub fn init( try SharedDeps.addSimd(b, vt, null); } - return .{ .vt = vt }; + return vt; } diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile new file mode 100644 index 000000000..f30dfba90 --- /dev/null +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -0,0 +1,33 @@ +#-------------------------------------------------------------------- +# Generate documentation with Doxygen +#-------------------------------------------------------------------- +FROM ubuntu:24.04 AS builder + +# Build argument for noindex header +ARG ADD_NOINDEX_HEADER=false +RUN apt-get update && apt-get install -y \ + doxygen \ + graphviz \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /ghostty +COPY include/ ./include/ +COPY Doxyfile ./ +RUN mkdir -p zig-out/share/ghostty/doc/libghostty +RUN doxygen + +#-------------------------------------------------------------------- +# Host the static HTML +#-------------------------------------------------------------------- +FROM nginx:alpine AS runtime + +# Pass build arg to runtime stage +ARG ADD_NOINDEX_HEADER=false +ENV ADD_NOINDEX_HEADER=$ADD_NOINDEX_HEADER + +# Copy documentation and entrypoint script +COPY --from=builder /ghostty/zig-out/share/ghostty/doc/libghostty /usr/share/nginx/html +COPY src/build/docker/lib-c-docs/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 80 +CMD ["/entrypoint.sh"] diff --git a/src/build/docker/lib-c-docs/entrypoint.sh b/src/build/docker/lib-c-docs/entrypoint.sh new file mode 100755 index 000000000..928d6e163 --- /dev/null +++ b/src/build/docker/lib-c-docs/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +if [ "$ADD_NOINDEX_HEADER" = "true" ]; then + cat > /etc/nginx/conf.d/noindex.conf << 'EOF' +server { + listen 80; + location / { + root /usr/share/nginx/html; + index index.html; + add_header X-Robots-Tag "noindex, nofollow" always; + } +} +EOF + # Remove default server config + rm -f /etc/nginx/conf.d/default.conf +fi +exec nginx -g "daemon off;" diff --git a/src/cli/args.zig b/src/cli/args.zig index 2d2d199be..b8f393864 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -507,13 +507,18 @@ pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T { fn parseStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { return switch (@typeInfo(T).@"struct".layout) { - .auto => parseAutoStruct(T, alloc, v), + .auto => parseAutoStruct(T, alloc, v, null), .@"packed" => parsePackedStruct(T, v), else => @compileError("unsupported struct layout"), }; } -pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { +pub fn parseAutoStruct( + comptime T: type, + alloc: Allocator, + v: []const u8, + default_: ?T, +) !T { const info = @typeInfo(T).@"struct"; comptime assert(info.layout == .auto); @@ -573,9 +578,18 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { // Ensure all required fields are set inline for (info.fields, 0..) |field, i| { if (!fields_set.isSet(i)) { - const default_ptr = field.default_value_ptr orelse return error.InvalidValue; - const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); - @field(result, field.name) = typed_ptr.*; + @field(result, field.name) = default: { + // If we're given a default value then we inherit those. + // Otherwise we use the default values as specified by the + // struct. + if (default_) |default| { + break :default @field(default, field.name); + } else { + const default_ptr = field.default_value_ptr orelse return error.InvalidValue; + const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); + break :default typed_ptr.*; + } + }; } } @@ -1194,7 +1208,18 @@ test "parseIntoField: struct with basic fields" { try testing.expectEqual(84, data.value.b); try testing.expectEqual(24, data.value.c); - // Missing require dfield + // Set with explicit default + data.value = try parseAutoStruct( + @TypeOf(data.value), + alloc, + "a:hello", + .{ .a = "oh no", .b = 42 }, + ); + try testing.expectEqualStrings("hello", data.value.a); + try testing.expectEqual(42, data.value.b); + try testing.expectEqual(12, data.value.c); + + // Missing required field try testing.expectError( error.InvalidValue, parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"), diff --git a/src/config.zig b/src/config.zig index e83dff530..569d4bec2 100644 --- a/src/config.zig +++ b/src/config.zig @@ -27,6 +27,7 @@ pub const FontStyle = Config.FontStyle; pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; +pub const MouseScrollMultiplier = Config.MouseScrollMultiplier; pub const NonNativeFullscreen = Config.NonNativeFullscreen; pub const OptionAsAlt = Config.OptionAsAlt; pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; diff --git a/src/config/Config.zig b/src/config/Config.zig index 66e63fd3f..46eb03fe2 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -416,9 +416,12 @@ pub const compatibility = std.StaticStringMap( /// necessarily force them to be. Decreasing this value will make nerd font /// icons smaller. /// -/// The default value for the icon height is 1.2 times the height of capital -/// letters in your primary font, so something like -16.6% would make icons -/// roughly the same height as capital letters. +/// This value only applies to icons that are constrained to a single cell by +/// neighboring characters. An icon that is free to spread across two cells +/// can always use up to the full line height of the primary font. +/// +/// The default value is 2/3 times the height of capital letters in your primary +/// font plus 1/3 times the font's line height. /// /// See the notes about adjustments in `adjust-cell-width`. /// @@ -833,14 +836,20 @@ palette: Palette = .{}, /// * `never` @"mouse-shift-capture": MouseShiftCapture = .false, -/// Multiplier for scrolling distance with the mouse wheel. Any value less -/// than 0.01 or greater than 10,000 will be clamped to the nearest valid -/// value. +/// Multiplier for scrolling distance with the mouse wheel. /// -/// A value of "3" (default) scrolls 3 lines per tick. +/// A prefix of `precision:` or `discrete:` can be used to set the multiplier +/// only for scrolling with the specific type of devices. These can be +/// comma-separated to set both types of multipliers at the same time, e.g. +/// `precision:0.1,discrete:3`. If no prefix is used, the multiplier applies +/// to all scrolling devices. Specifying a prefix was introduced in Ghostty +/// 1.2.1. /// -/// Available since: 1.2.0 -@"mouse-scroll-multiplier": f64 = 3.0, +/// The value will be clamped to [0.01, 10,000]. Both of these are extreme +/// and you're likely to have a bad experience if you set either extreme. +/// +/// The default value is "3" for discrete devices and "1" for precision devices. +@"mouse-scroll-multiplier": MouseScrollMultiplier = .default, /// The opacity level (opposite of transparency) of the background. A value of /// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 @@ -4077,7 +4086,8 @@ pub fn finalize(self: *Config) !void { } // Clamp our mouse scroll multiplier - self.@"mouse-scroll-multiplier" = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier")); + self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".precision)); + self.@"mouse-scroll-multiplier".discrete = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".discrete)); // Clamp our split opacity self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity")); @@ -6508,7 +6518,7 @@ pub const RepeatableCodepointMap = struct { return .{ .map = try self.map.clone(alloc) }; } - /// Compare if two of our value are requal. Required by Config. + /// Compare if two of our value are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.map.list.slice(); const itemsB = other.map.list.slice(); @@ -7010,6 +7020,7 @@ pub const RepeatableCommand = struct { inputpkg.Command, alloc, input, + null, ); try self.value.append(alloc, cmd); } @@ -7319,6 +7330,108 @@ pub const MouseShiftCapture = enum { never, }; +/// See mouse-scroll-multiplier +pub const MouseScrollMultiplier = struct { + const Self = @This(); + + precision: f64 = 1, + discrete: f64 = 3, + + pub const default: MouseScrollMultiplier = .{}; + + pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { + const input = input_ orelse return error.ValueRequired; + self.* = cli.args.parseAutoStruct( + MouseScrollMultiplier, + alloc, + input, + self.*, + ) catch |err| switch (err) { + error.InvalidValue => bare: { + const v = std.fmt.parseFloat( + f64, + input, + ) catch return error.InvalidValue; + break :bare .{ + .precision = v, + .discrete = v, + }; + }, + else => return err, + }; + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { + _ = alloc; + return self.*; + } + + /// Compare if two of our value are equal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + return self.precision == other.precision and self.discrete == other.discrete; + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + var buf: [32]u8 = undefined; + const formatted = std.fmt.bufPrint( + &buf, + "precision:{d},discrete:{d}", + .{ self.precision, self.discrete }, + ) catch return error.OutOfMemory; + try formatter.formatEntry([]const u8, formatted); + } + + test "parse" { + const testing = std.testing; + const alloc = testing.allocator; + const epsilon = 0.00001; + + var args: Self = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "3"); + try testing.expectApproxEqAbs(3, args.precision, epsilon); + try testing.expectApproxEqAbs(3, args.discrete, epsilon); + + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "precision:1"); + try testing.expectApproxEqAbs(1, args.precision, epsilon); + try testing.expectApproxEqAbs(3, args.discrete, epsilon); + + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "discrete:5"); + try testing.expectApproxEqAbs(0.1, args.precision, epsilon); + try testing.expectApproxEqAbs(5, args.discrete, epsilon); + + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "precision:3,discrete:7"); + try testing.expectApproxEqAbs(3, args.precision, epsilon); + try testing.expectApproxEqAbs(7, args.discrete, epsilon); + + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "discrete:8,precision:6"); + try testing.expectApproxEqAbs(6, args.precision, epsilon); + try testing.expectApproxEqAbs(8, args.discrete, epsilon); + + args = .default; + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "foo:1")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:bar")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,discrete:3,foo:5")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,,discrete:3")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, ",precision:1,discrete:3")); + } + + test "format entry MouseScrollMultiplier" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var args: Self = .{ .precision = 1.5, .discrete = 2.5 }; + try args.formatEntry(formatterpkg.entryFormatter("mouse-scroll-multiplier", buf.writer())); + try testing.expectEqualSlices(u8, "mouse-scroll-multiplier = precision:1.5,discrete:2.5\n", buf.items); + } +}; + /// How to treat requests to write to or read from the clipboard pub const ClipboardAccess = enum { allow, @@ -7933,6 +8046,7 @@ pub const Theme = struct { Theme, alloc, input, + null, ); return; } diff --git a/src/extra/vim.zig b/src/extra/vim.zig index e5261cd74..4443fd168 100644 --- a/src/extra/vim.zig +++ b/src/extra/vim.zig @@ -10,7 +10,7 @@ pub const ftdetect = \\" \\" THIS FILE IS AUTO-GENERATED \\ - \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* set ft=ghostty + \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* setf ghostty \\ ; pub const ftplugin = diff --git a/src/font/Collection.zig b/src/font/Collection.zig index ad9590d70..04b9882dc 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -1213,6 +1213,9 @@ test "metrics" { // and 1em should be the point size * dpi scale, so 12 * (96/72) // which is 16, and 16 * 1.049 = 16.784, which finally is rounded // to 17. + // + // The icon height is (2 * cap_height + face_height) / 3 + // = (2 * 623 + 1049) / 3 = 765, and 16 * 0.765 = 12.24. .cell_height = 17, .cell_baseline = 3, .underline_position = 17, @@ -1223,7 +1226,10 @@ test "metrics" { .overline_thickness = 1, .box_thickness = 1, .cursor_height = 17, - .icon_height = 11, + .icon_height = 12.24, + .face_width = 8.0, + .face_height = 16.784, + .face_y = @round(3.04) - @as(f64, 3.04), // use f64, not comptime float, for exact match with runtime value }, c.metrics); // Resize should change metrics @@ -1240,7 +1246,10 @@ test "metrics" { .overline_thickness = 2, .box_thickness = 2, .cursor_height = 34, - .icon_height = 23, + .icon_height = 24.48, + .face_width = 16.0, + .face_height = 33.568, + .face_y = @round(6.08) - @as(f64, 6.08), // use f64, not comptime float, for exact match with runtime value }, c.metrics); } @@ -1369,3 +1378,155 @@ test "adjusted sizes" { ); } } + +test "face metrics" { + // The web canvas backend doesn't calculate face metrics, only cell metrics + if (options.backend != .web_canvas) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const narrowFont = font.embedded.cozette; + const wideFont = font.embedded.geist_mono; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = init(); + defer c.deinit(alloc); + const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; + c.load_options = .{ .library = lib, .size = size }; + + const narrowIndex = try c.add(alloc, try .init( + lib, + narrowFont, + .{ .size = size }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + const wideIndex = try c.add(alloc, try .init( + lib, + wideFont, + .{ .size = size }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + + const narrowMetrics: font.Metrics.FaceMetrics = (try c.getFace(narrowIndex)).getMetrics(); + const wideMetrics: font.Metrics.FaceMetrics = (try c.getFace(wideIndex)).getMetrics(); + + // Verify provided/measured metrics. Measured + // values are backend-dependent due to hinting. + const narrowMetricsExpected = font.Metrics.FaceMetrics{ + .px_per_em = 16.0, + .cell_width = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 8.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 7.3828125, + .web_canvas => unreachable, + }, + .ascent = 12.3046875, + .descent = -3.6953125, + .line_gap = 0.0, + .underline_position = -1.2265625, + .underline_thickness = 1.2265625, + .strikethrough_position = 6.15625, + .strikethrough_thickness = 1.234375, + .cap_height = 9.84375, + .ex_height = 7.3828125, + .ascii_height = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 18.0625, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 16.0, + .web_canvas => unreachable, + }, + }; + const wideMetricsExpected = font.Metrics.FaceMetrics{ + .px_per_em = 16.0, + .cell_width = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 10.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 9.6, + .web_canvas => unreachable, + }, + .ascent = 14.72, + .descent = -3.52, + .line_gap = 1.6, + .underline_position = -1.6, + .underline_thickness = 0.8, + .strikethrough_position = 4.24, + .strikethrough_thickness = 0.8, + .cap_height = 11.36, + .ex_height = 8.48, + .ascii_height = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 16.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 15.472000000000001, + .web_canvas => unreachable, + }, + }; + + inline for ( + .{ narrowMetricsExpected, wideMetricsExpected }, + .{ narrowMetrics, wideMetrics }, + ) |metricsExpected, metricsActual| { + inline for (@typeInfo(font.Metrics.FaceMetrics).@"struct".fields) |field| { + const expected = @field(metricsExpected, field.name); + const actual = @field(metricsActual, field.name); + // Unwrap optional fields + const expectedValue, const actualValue = unwrap: switch (@typeInfo(field.type)) { + .optional => { + if (expected) |expectedValue| if (actual) |actualValue| { + break :unwrap .{ expectedValue, actualValue }; + }; + // Null values can be compared directly + try std.testing.expectEqual(expected, actual); + continue; + }, + else => break :unwrap .{ expected, actual }, + }; + // All non-null values are floats + const eps = std.math.floatEps(@TypeOf(actualValue - expectedValue)); + try std.testing.expectApproxEqRel( + expectedValue, + actualValue, + std.math.sqrt(eps), + ); + } + } + + // Verify estimated metrics. icWidth() should equal the smaller of + // 2 * cell_width and ascii_height. For a narrow (wide) font, the + // smaller quantity is the former (latter). + try std.testing.expectEqual( + 2 * narrowMetrics.cell_width, + narrowMetrics.icWidth(), + ); + try std.testing.expectEqual( + wideMetrics.ascii_height, + wideMetrics.icWidth(), + ); +} diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 9f6df9dc3..a0bc047c4 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -36,11 +36,17 @@ cursor_thickness: u32 = 1, cursor_height: u32, /// The constraint height for nerd fonts icons. -icon_height: u32, +icon_height: f64, -/// Original cell width in pixels. This is used to keep -/// glyphs centered if the cell width is adjusted wider. -original_cell_width: ?u32 = null, +/// The unrounded face width, used in scaling calculations. +face_width: f64, + +/// The unrounded face height, used in scaling calculations. +face_height: f64, + +/// The vertical bearing of face within the pixel-rounded +/// and possibly height-adjusted cell +face_y: f64, /// Minimum acceptable values for some fields to prevent modifiers /// from being able to, for example, cause 0-thickness underlines. @@ -53,7 +59,9 @@ const Minimums = struct { const box_thickness = 1; const cursor_thickness = 1; const cursor_height = 1; - const icon_height = 1; + const icon_height = 1.0; + const face_height = 1.0; + const face_width = 1.0; }; /// Metrics extracted from a font face, based on @@ -214,8 +222,10 @@ pub fn calc(face: FaceMetrics) Metrics { // We use the ceiling of the provided cell width and height to ensure // that the cell is large enough for the provided size, since we cast // it to an integer later. - const cell_width = @ceil(face.cell_width); - const cell_height = @ceil(face.lineHeight()); + const face_width = face.cell_width; + const face_height = face.lineHeight(); + const cell_width = @ceil(face_width); + const cell_height = @ceil(face_height); // We split our line gap in two parts, and put half of it on the top // of the cell and the other half on the bottom, so that our text never @@ -224,7 +234,11 @@ pub fn calc(face: FaceMetrics) Metrics { // Unlike all our other metrics, `cell_baseline` is relative to the // BOTTOM of the cell. - const cell_baseline = @round(half_line_gap - face.descent); + const face_baseline = half_line_gap - face.descent; + const cell_baseline = @round(face_baseline); + + // We keep track of the vertical bearing of the face in the cell + const face_y = cell_baseline - face_baseline; // We calculate a top_to_baseline to make following calculations simpler. const top_to_baseline = cell_height - cell_baseline; @@ -237,16 +251,8 @@ pub fn calc(face: FaceMetrics) Metrics { const underline_position = @round(top_to_baseline - face.underlinePosition()); const strikethrough_position = @round(top_to_baseline - face.strikethroughPosition()); - // The calculation for icon height in the nerd fonts patcher - // is two thirds cap height to one third line height, but we - // use an opinionated default of 1.2 * cap height instead. - // - // Doing this prevents fonts with very large line heights - // from having excessively oversized icons, and allows fonts - // with very small line heights to still have roomy icons. - // - // We do cap it at `cell_height` though for obvious reasons. - const icon_height = @min(cell_height, cap_height * 1.2); + // Same heuristic as the font_patcher script + const icon_height = (2 * cap_height + face_height) / 3; var result: Metrics = .{ .cell_width = @intFromFloat(cell_width), @@ -260,7 +266,10 @@ pub fn calc(face: FaceMetrics) Metrics { .overline_thickness = @intFromFloat(underline_thickness), .box_thickness = @intFromFloat(underline_thickness), .cursor_height = @intFromFloat(cell_height), - .icon_height = @intFromFloat(icon_height), + .icon_height = icon_height, + .face_width = face_width, + .face_height = face_height, + .face_y = face_y, }; // Ensure all metrics are within their allowable range. @@ -286,11 +295,6 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const new = @max(entry.value_ptr.apply(original), 1); if (new == original) continue; - // Preserve the original cell width if not set. - if (self.original_cell_width == null) { - self.original_cell_width = self.cell_width; - } - // Set the new value @field(self, @tagName(tag)) = new; @@ -307,6 +311,7 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const diff = new - original; const diff_bottom = diff / 2; const diff_top = diff - diff_bottom; + self.face_y += @floatFromInt(diff_bottom); self.cell_baseline +|= diff_bottom; self.underline_position +|= diff_top; self.strikethrough_position +|= diff_top; @@ -315,6 +320,7 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const diff = original - new; const diff_bottom = diff / 2; const diff_top = diff - diff_bottom; + self.face_y -= @floatFromInt(diff_bottom); self.cell_baseline -|= diff_bottom; self.underline_position -|= diff_top; self.strikethrough_position -|= diff_top; @@ -417,25 +423,35 @@ pub const Modifier = union(enum) { /// Apply a modifier to a numeric value. pub fn apply(self: Modifier, v: anytype) @TypeOf(v) { const T = @TypeOf(v); - const signed = @typeInfo(T).int.signedness == .signed; - return switch (self) { - .percent => |p| percent: { - const p_clamped: f64 = @max(0, p); - const v_f64: f64 = @floatFromInt(v); - const applied_f64: f64 = @round(v_f64 * p_clamped); - const applied_T: T = @intFromFloat(applied_f64); - break :percent applied_T; - }, + const Tinfo = @typeInfo(T); + return switch (comptime Tinfo) { + .int, .comptime_int => switch (self) { + .percent => |p| percent: { + const p_clamped: f64 = @max(0, p); + const v_f64: f64 = @floatFromInt(v); + const applied_f64: f64 = @round(v_f64 * p_clamped); + const applied_T: T = @intFromFloat(applied_f64); + break :percent applied_T; + }, - .absolute => |abs| absolute: { - const v_i64: i64 = @intCast(v); - const abs_i64: i64 = @intCast(abs); - const applied_i64: i64 = v_i64 +| abs_i64; - const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64); - const applied_T: T = std.math.cast(T, clamped_i64) orelse - std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64))); - break :absolute applied_T; + .absolute => |abs| absolute: { + const v_i64: i64 = @intCast(v); + const abs_i64: i64 = @intCast(abs); + const applied_i64: i64 = v_i64 +| abs_i64; + const clamped_i64: i64 = if (Tinfo.int.signedness == .signed) + applied_i64 + else + @max(0, applied_i64); + const applied_T: T = std.math.cast(T, clamped_i64) orelse + std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64))); + break :absolute applied_T; + }, }, + .float, .comptime_float => return switch (self) { + .percent => |p| v * @max(0, p), + .absolute => |abs| v + @as(T, @floatFromInt(abs)), + }, + else => {}, }; } @@ -481,7 +497,7 @@ pub const Key = key: { var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined; var count: usize = 0; for (field_infos, 0..) |field, i| { - if (field.type != u32 and field.type != i32) continue; + if (field.type != u32 and field.type != i32 and field.type != f64) continue; enumFields[i] = .{ .name = field.name, .value = i }; count += 1; } @@ -512,7 +528,10 @@ fn init() Metrics { .overline_thickness = 0, .box_thickness = 0, .cursor_height = 0, - .icon_height = 0, + .icon_height = 0.0, + .face_width = 0.0, + .face_height = 0.0, + .face_y = 0.0, }; } @@ -542,6 +561,7 @@ test "Metrics: adjust cell height smaller" { try set.put(alloc, .cell_height, .{ .percent = 0.75 }); var m: Metrics = init(); + m.face_y = 0.33; m.cell_baseline = 50; m.underline_position = 55; m.strikethrough_position = 30; @@ -549,6 +569,7 @@ test "Metrics: adjust cell height smaller" { m.cell_height = 100; m.cursor_height = 100; m.apply(set); + try testing.expectEqual(-11.67, m.face_y); try testing.expectEqual(@as(u32, 75), m.cell_height); try testing.expectEqual(@as(u32, 38), m.cell_baseline); try testing.expectEqual(@as(u32, 42), m.underline_position); @@ -570,6 +591,7 @@ test "Metrics: adjust cell height larger" { try set.put(alloc, .cell_height, .{ .percent = 1.75 }); var m: Metrics = init(); + m.face_y = 0.33; m.cell_baseline = 50; m.underline_position = 55; m.strikethrough_position = 30; @@ -577,6 +599,7 @@ test "Metrics: adjust cell height larger" { m.cell_height = 100; m.cursor_height = 100; m.apply(set); + try testing.expectEqual(37.33, m.face_y); try testing.expectEqual(@as(u32, 175), m.cell_height); try testing.expectEqual(@as(u32, 87), m.cell_baseline); try testing.expectEqual(@as(u32, 93), m.underline_position); diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index e79fd117f..3fd9cf204 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -270,11 +270,9 @@ pub fn renderGlyph( // Always use these constraints for emoji. if (p == .emoji) { render_opts.constraint = .{ - // Make the emoji as wide as possible, scaling proportionally, - // but then scale it down as necessary if its new size exceeds - // the cell height. - .size_horizontal = .cover, - .size_vertical = .fit, + // Scale emoji to be as large as possible + // while preserving their aspect ratio. + .size = .cover, // Center the emoji in its cells. .align_horizontal = .center, diff --git a/src/font/face.zig b/src/font/face.zig index 9da3c30f6..7216fea97 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -93,6 +93,14 @@ pub const Variation = struct { }; }; +/// The size and position of a glyph. +pub const GlyphSize = struct { + width: f64, + height: f64, + x: f64, + y: f64, +}; + /// Additional options for rendering glyphs. pub const RenderOptions = struct { /// The metrics that are defining the grid layout. These are usually @@ -136,10 +144,8 @@ pub const RenderOptions = struct { /// Don't constrain the glyph in any way. pub const none: Constraint = .{}; - /// Vertical sizing rule. - size_vertical: Size = .none, - /// Horizontal sizing rule. - size_horizontal: Size = .none, + /// Sizing rule. + size: Size = .none, /// Vertical alignment rule. align_vertical: Align = .none, @@ -155,42 +161,40 @@ pub const RenderOptions = struct { /// Bottom padding when resizing. pad_bottom: f64 = 0.0, - // This acts as a multiple of the provided width when applying - // constraints, so if this is 1.6 for example, then a width of - // 10 would be treated as though it were 16. - group_width: f64 = 1.0, - // This acts as a multiple of the provided height when applying - // constraints, so if this is 1.6 for example, then a height of - // 10 would be treated as though it were 16. - group_height: f64 = 1.0, - // This is an x offset for the actual width within the group width. - // If this is 0.5 then the glyph will be offset so that its left - // edge sits at the halfway point of the group width. - group_x: f64 = 0.0, - // This is a y offset for the actual height within the group height. - // If this is 0.5 then the glyph will be offset so that its bottom - // edge sits at the halfway point of the group height. - group_y: f64 = 0.0, + // Size and bearings of the glyph relative + // to the bounding box of its scale group. + relative_width: f64 = 1.0, + relative_height: f64 = 1.0, + relative_x: f64 = 0.0, + relative_y: f64 = 0.0, - /// Maximum ratio of width to height when resizing. + /// Maximum aspect ratio (width/height) to allow when stretching. max_xy_ratio: ?f64 = null, /// Maximum number of cells horizontally to use. max_constraint_width: u2 = 2, - /// What to use as the height metric when constraining the glyph. + /// What to use as the height metric when constraining the glyph and + /// the constraint width is 1, height: Height = .cell, pub const Size = enum { /// Don't change the size of this glyph. none, - /// Move the glyph and optionally scale it down - /// proportionally to fit within the given axis. + /// Scale the glyph down if needed to fit within the bounds, + /// preserving aspect ratio. fit, - /// Move and resize the glyph proportionally to - /// cover the given axis. + /// Scale the glyph up or down to exactly match the bounds, + /// preserving aspect ratio. cover, - /// Same as `cover` but not proportional. + /// Scale the glyph down if needed to fit within the bounds, + /// preserving aspect ratio. If the glyph doesn't cover a + /// single cell, scale up. If the glyph exceeds a single + /// cell but is within the bounds, do nothing. + /// (Nerd Font specific rule.) + fit_cover1, + /// Stretch the glyph to exactly fit the bounds in both + /// directions, disregarding aspect ratio. stretch, }; @@ -205,30 +209,27 @@ pub const RenderOptions = struct { end, /// Move the glyph so that it is centered on this axis. center, + /// Move the glyph so that it is centered on this axis, + /// but always with respect to the first cell even for + /// multi-cell constraints. (Nerd Font specific rule.) + center1, }; pub const Height = enum { - /// Use the full height of the cell for constraining this glyph. + /// Always use the full height of the cell for constraining this glyph. cell, - /// Use the "icon height" from the grid metrics as the height. + /// When the constraint width is 1, use the "icon height" from the grid + /// metrics as the height. (When the constraint width is >1, the + /// constraint height is always the full cell height.) icon, }; - /// The size and position of a glyph. - pub const GlyphSize = struct { - width: f64, - height: f64, - x: f64, - y: f64, - }; - /// Returns true if the constraint does anything. If it doesn't, /// because it neither sizes nor positions the glyph, then this /// returns false. pub inline fn doesAnything(self: Constraint) bool { - return self.size_horizontal != .none or + return self.size != .none or self.align_horizontal != .none or - self.size_vertical != .none or self.align_vertical != .none; } @@ -241,156 +242,202 @@ pub const RenderOptions = struct { /// Number of cells horizontally available for this glyph. constraint_width: u2, ) GlyphSize { - var g = glyph; + if (!self.doesAnything()) return glyph; - const available_width: f64 = @floatFromInt( - metrics.cell_width * @min( - self.max_constraint_width, - constraint_width, - ), - ); - const available_height: f64 = @floatFromInt(switch (self.height) { - .cell => metrics.cell_height, - .icon => metrics.icon_height, - }); + // For extra wide font faces, never stretch glyphs across two cells. + // This mirrors font_patcher. + const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height)) + 1 + else + @min(self.max_constraint_width, constraint_width); - const w = available_width - - self.pad_left * available_width - - self.pad_right * available_width; - const h = available_height - - self.pad_top * available_height - - self.pad_bottom * available_height; - - // Subtract padding from the bearings so that our - // alignment and sizing code works correctly. We - // re-add before returning. - g.x -= self.pad_left * available_width; - g.y -= self.pad_bottom * available_height; - - // Multiply by group width and height for better sizing. - g.width *= self.group_width; - g.height *= self.group_height; - - switch (self.size_horizontal) { - .none => {}, - .fit => if (g.width > w) { - const orig_height = g.height; - // Adjust our height and width to proportionally - // scale them to fit the glyph to the cell width. - g.height *= w / g.width; - g.width = w; - // Set our x to 0 since anything else would mean - // the glyph extends outside of the cell width. - g.x = 0; - // Compensate our y to keep things vertically - // centered as they're scaled down. - g.y += (orig_height - g.height) / 2; - } else if (g.width + g.x > w) { - // If the width of the glyph can fit in the cell but - // is currently outside due to the left bearing, then - // we reduce the left bearing just enough to fit it - // back in the cell. - g.x = w - g.width; - } else if (g.x < 0) { - g.x = 0; - }, - .cover => { - const orig_height = g.height; - - g.height *= w / g.width; - g.width = w; - - g.x = 0; - - g.y += (orig_height - g.height) / 2; - }, - .stretch => { - g.width = w; - g.x = 0; - }, - } - - switch (self.size_vertical) { - .none => {}, - .fit => if (g.height > h) { - const orig_width = g.width; - // Adjust our height and width to proportionally - // scale them to fit the glyph to the cell height. - g.width *= h / g.height; - g.height = h; - // Set our y to 0 since anything else would mean - // the glyph extends outside of the cell height. - g.y = 0; - // Compensate our x to keep things horizontally - // centered as they're scaled down. - g.x += (orig_width - g.width) / 2; - } else if (g.height + g.y > h) { - // If the height of the glyph can fit in the cell but - // is currently outside due to the bottom bearing, then - // we reduce the bottom bearing just enough to fit it - // back in the cell. - g.y = h - g.height; - } else if (g.y < 0) { - g.y = 0; - }, - .cover => { - const orig_width = g.width; - - g.width *= h / g.height; - g.height = h; - - g.y = 0; - - g.x += (orig_width - g.width) / 2; - }, - .stretch => { - g.height = h; - g.y = 0; - }, - } - - // Add group-relative position - g.x += self.group_x * g.width; - g.y += self.group_y * g.height; - - // Divide group width and height back out before we align. - g.width /= self.group_width; - g.height /= self.group_height; - - if (self.max_xy_ratio) |ratio| if (g.width > g.height * ratio) { - const orig_width = g.width; - g.width = g.height * ratio; - g.x += (orig_width - g.width) / 2; + // The bounding box for the glyph's scale group. + // Scaling and alignment rules are calculated for + // this box and then applied to the glyph. + var group: GlyphSize = group: { + const group_width = glyph.width / self.relative_width; + const group_height = glyph.height / self.relative_height; + break :group .{ + .width = group_width, + .height = group_height, + .x = glyph.x - (group_width * self.relative_x), + .y = glyph.y - (group_height * self.relative_y), + }; }; - switch (self.align_horizontal) { - .none => {}, - .start => g.x = 0, - .end => g.x = w - g.width, - .center => g.x = (w - g.width) / 2, + // The new, constrained glyph size + var constrained_glyph = glyph; + + // Apply prescribed scaling + const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width); + constrained_glyph.width *= width_factor; + constrained_glyph.x *= width_factor; + constrained_glyph.height *= height_factor; + constrained_glyph.y *= height_factor; + + // NOTE: font_patcher jumps through a lot of hoops at this + // point to ensure that the glyph remains within the target + // bounding box after rounding to font definition units. + // This is irrelevant here as we're not rounding, we're + // staying in f64 and heading straight to rendering. + + // Align vertically + if (self.align_vertical != .none) { + // Vertically scale group bounding box. + group.height *= height_factor; + group.y *= height_factor; + + // Calculate offset and shift the glyph + constrained_glyph.y += self.offset_vertical(group, metrics); } - switch (self.align_vertical) { - .none => {}, - .start => g.y = 0, - .end => g.y = h - g.height, - .center => g.y = (h - g.height) / 2, + // Align horizontally + if (self.align_horizontal != .none) { + // Horizontally scale group bounding box. + group.width *= width_factor; + group.x *= width_factor; + + // Calculate offset and shift the glyph + constrained_glyph.x += self.offset_horizontal(group, metrics, min_constraint_width); } - // Re-add our padding before returning. - g.x += self.pad_left * available_width; - g.y += self.pad_bottom * available_height; + return constrained_glyph; + } - // If the available height is less than the cell height, we - // add half of the difference to center it in the full height. - // - // If necessary, in the future, we can adjust this to account - // for alignment, but that isn't necessary with any of the nf - // icons afaict. - const cell_height: f64 = @floatFromInt(metrics.cell_height); - g.y += (cell_height - available_height) / 2; + /// Return width and height scaling factors for this scaling group. + fn scale_factors( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + min_constraint_width: u2, + ) struct { f64, f64 } { + if (self.size == .none) { + return .{ 1.0, 1.0 }; + } - return g; + const multi_cell = (min_constraint_width > 1); + + const pad_width_factor = @as(f64, @floatFromInt(min_constraint_width)) - (self.pad_left + self.pad_right); + const pad_height_factor = 1 - (self.pad_bottom + self.pad_top); + + const target_width = pad_width_factor * metrics.face_width; + const target_height = pad_height_factor * switch (self.height) { + .cell => metrics.face_height, + // icon_height only applies with single-cell constraints. + // This mirrors font_patcher. + .icon => if (multi_cell) + metrics.face_height + else + metrics.icon_height, + }; + + var width_factor = target_width / group.width; + var height_factor = target_height / group.height; + + switch (self.size) { + .none => unreachable, + .fit => { + // Scale down to fit if needed + height_factor = @min(1, width_factor, height_factor); + width_factor = height_factor; + }, + .cover => { + // Scale to cover + height_factor = @min(width_factor, height_factor); + width_factor = height_factor; + }, + .fit_cover1 => { + // Scale down to fit or up to cover at least one cell + // NOTE: This is similar to font_patcher's "pa" mode, + // however, font_patcher will only do the upscaling + // part if the constraint width is 1, resulting in + // some icons becoming smaller when the constraint + // width increases. You'd see icons shrinking when + // opening up a space after them. This makes no + // sense, so we've fixed the rule such that these + // icons are scaled to the same size for multi-cell + // constraints as they would be for single-cell. + height_factor = @min(width_factor, height_factor); + if (multi_cell and (height_factor > 1)) { + // Call back into this function with + // constraint width 1 to get single-cell scale + // factors. We use the height factor as width + // could have been modified by max_xy_ratio. + _, const single_height_factor = self.scale_factors(group, metrics, 1); + height_factor = @max(1, single_height_factor); + } + width_factor = height_factor; + }, + .stretch => {}, + } + + // Reduce aspect ratio if required + if (self.max_xy_ratio) |ratio| { + if (group.width * width_factor > group.height * height_factor * ratio) { + width_factor = group.height * height_factor * ratio / group.width; + } + } + + return .{ width_factor, height_factor }; + } + + /// Return vertical offset needed to align this group + fn offset_vertical( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + ) f64 { + // We use face_height and offset by face_y, rather than + // using cell_height directly, to account for the asymmetry + // of the pixel cell around the face (a consequence of + // aligning the baseline with a pixel boundary rather than + // vertically centering the face). + const new_group_y = metrics.face_y + switch (self.align_vertical) { + .none => return 0.0, + .start => self.pad_bottom * metrics.face_height, + .end => end: { + const pad_top_dy = self.pad_top * metrics.face_height; + break :end metrics.face_height - pad_top_dy - group.height; + }, + .center, .center1 => (metrics.face_height - group.height) / 2, + }; + return new_group_y - group.y; + } + + /// Return horizontal offset needed to align this group + fn offset_horizontal( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + min_constraint_width: u2, + ) f64 { + // For multi-cell constraints, we align relative to the span + // from the left edge of the first face cell to the right + // edge of the last face cell as they sit within the rounded + // and adjusted pixel cell (centered if narrower than the + // pixel cell, left-aligned if wider). + const face_x, const full_face_span = facecalcs: { + const cell_width: f64 = @floatFromInt(metrics.cell_width); + const full_width: f64 = @floatFromInt(min_constraint_width * metrics.cell_width); + const cell_margin = cell_width - metrics.face_width; + break :facecalcs .{ @max(0, cell_margin / 2), full_width - cell_margin }; + }; + const pad_left_x = self.pad_left * metrics.face_width; + const new_group_x = face_x + switch (self.align_horizontal) { + .none => return 0.0, + .start => pad_left_x, + .end => end: { + const pad_right_dx = self.pad_right * metrics.face_width; + break :end @max(pad_left_x, full_face_span - pad_right_dx - group.width); + }, + .center => @max(pad_left_x, (full_face_span - group.width) / 2), + // NOTE: .center1 implements the font_patcher rule of centering + // in the first cell even for multi-cell constraints. Since glyphs + // are not allowed to protrude to the left, this results in the + // left-alignment like .start when the glyph is wider than a cell. + .center1 => @max(pad_left_x, (metrics.face_width - group.width) / 2), + }; + return new_group_x - group.x; } }; }; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index cb9993cbf..8c9611c04 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -388,19 +388,16 @@ pub const Face = struct { y = @round(y); } - // If the cell width was adjusted wider, we re-center all glyphs - // in the new width, so that they aren't weirdly off to the left. - if (metrics.original_cell_width) |original| recenter: { - // We don't do this if the constraint has a horizontal alignment, - // since in that case the position was already calculated with the - // new cell width in mind. - if (opts.constraint.align_horizontal != .none) break :recenter; - - // If the original width was wider then we don't do anything. - if (original >= metrics.cell_width) break :recenter; - + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((opts.constraint.align_horizontal == .none) and (metrics.face_width < cell_width)) { // We add half the difference to re-center. - x += (cell_width - @as(f64, @floatFromInt(original))) / 2; + x += (cell_width - metrics.face_width) / 2; } // Our whole-pixel bearings for the final glyph. diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 82cf107c8..bdcd82ab3 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -170,7 +170,7 @@ pub const Face = struct { if (string.len > 1024) break :skip; var tmp: [512]u16 = undefined; const max = string.len / 2; - for (@as([]const u16, @alignCast(@ptrCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c); + for (@as([]const u16, @ptrCast(@alignCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c); const len = std.unicode.utf16LeToUtf8(buf, tmp[0..max]) catch return string; return buf[0..len]; } @@ -351,26 +351,16 @@ pub const Face = struct { return glyph.*.bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_BGRA; } - /// Render a glyph using the glyph index. The rendered glyph is stored in the - /// given texture atlas. - pub fn renderGlyph( - self: Face, - alloc: Allocator, - atlas: *font.Atlas, - glyph_index: u32, - opts: font.face.RenderOptions, - ) !Glyph { - self.ft_mutex.lock(); - defer self.ft_mutex.unlock(); - + /// Set the load flags to use when loading a glyph for measurement or + /// rendering. + fn glyphLoadFlags(self: Face, constrained: bool) freetype.LoadFlags { // Hinting should only be enabled if the configured load flags specify // it and the provided constraint doesn't actually do anything, since // if it does, then it'll mess up the hinting anyway when it moves or // resizes the glyph. - const do_hinting = self.load_flags.hinting and !opts.constraint.doesAnything(); + const do_hinting = self.load_flags.hinting and !constrained; - // Load the glyph. - try self.face.loadGlyph(glyph_index, .{ + return .{ // If our glyph has color, we want to render the color .color = self.face.hasColor(), @@ -392,42 +382,56 @@ pub const Face = struct { // SVG glyphs under FreeType, since that requires bundling another // dependency to handle rendering the SVG. .no_svg = true, - }); + }; + } + + /// Get a rect that represents the position and size of the loaded glyph. + fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.face.GlyphSize { + // If we're dealing with an outline glyph then we get the + // outline's bounding box instead of using the built-in + // metrics, since that's more precise and allows better + // cell-fitting. + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { + // Get the glyph's bounding box before we transform it at all. + // We use this rather than the metrics, since it's more precise. + var bbox: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); + + return .{ + .x = f26dot6ToF64(bbox.xMin), + .y = f26dot6ToF64(bbox.yMin), + .width = f26dot6ToF64(bbox.xMax - bbox.xMin), + .height = f26dot6ToF64(bbox.yMax - bbox.yMin), + }; + } + + return .{ + .x = f26dot6ToF64(glyph.*.metrics.horiBearingX), + .y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), + .width = f26dot6ToF64(glyph.*.metrics.width), + .height = f26dot6ToF64(glyph.*.metrics.height), + }; + } + + /// Render a glyph using the glyph index. The rendered glyph is stored in the + /// given texture atlas. + pub fn renderGlyph( + self: Face, + alloc: Allocator, + atlas: *font.Atlas, + glyph_index: u32, + opts: font.face.RenderOptions, + ) !Glyph { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + // Load the glyph. + try self.face.loadGlyph(glyph_index, self.glyphLoadFlags(opts.constraint.doesAnything())); const glyph = self.face.handle.*.glyph; // We get a rect that represents the position // and size of the glyph before any changes. - const rect: struct { - x: f64, - y: f64, - width: f64, - height: f64, - } = metrics: { - // If we're dealing with an outline glyph then we get the - // outline's bounding box instead of using the built-in - // metrics, since that's more precise and allows better - // cell-fitting. - if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { - // Get the glyph's bounding box before we transform it at all. - // We use this rather than the metrics, since it's more precise. - var bbox: freetype.c.FT_BBox = undefined; - _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); - - break :metrics .{ - .x = f26dot6ToF64(bbox.xMin), - .y = f26dot6ToF64(bbox.yMin), - .width = f26dot6ToF64(bbox.xMax - bbox.xMin), - .height = f26dot6ToF64(bbox.yMax - bbox.yMin), - }; - } - - break :metrics .{ - .x = f26dot6ToF64(glyph.*.metrics.horiBearingX), - .y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), - .width = f26dot6ToF64(glyph.*.metrics.width), - .height = f26dot6ToF64(glyph.*.metrics.height), - }; - }; + const rect = getGlyphSize(glyph); // If our glyph is smaller than a quarter pixel in either axis // then it has no outlines or they're too small to render. @@ -498,17 +502,14 @@ pub const Face = struct { y = @round(y); } - // If the cell width was adjusted wider, we re-center all glyphs - // in the new width, so that they aren't weirdly off to the left. - if (metrics.original_cell_width) |original| recenter: { - // We don't do this if the constraint has a horizontal alignment, - // since in that case the position was already calculated with the - // new cell width in mind. - if (opts.constraint.align_horizontal != .none) break :recenter; - - // If the original width was wider then we don't do anything. - if (original >= metrics.cell_width) break :recenter; - + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((opts.constraint.align_horizontal == .none) and (metrics.face_width < cell_width)) { // We add half the difference to re-center. // // NOTE: We round this to a whole-pixel amount because under @@ -516,7 +517,7 @@ pub const Face = struct { // the case under CoreText. If we move the outlines by // a non-whole-pixel amount, it completely ruins the // hinting. - x += @round((cell_width - @as(f64, @floatFromInt(original))) / 2); + x += @round((cell_width - metrics.face_width) / 2); } // Now we can render the glyph. @@ -976,23 +977,15 @@ pub const Face = struct { var c: u8 = ' '; while (c < 127) : (c += 1) { if (face.getCharIndex(c)) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { const glyph = face.handle.*.glyph; max = @max( f26dot6ToF64(glyph.*.advance.x), max, ); - top = @max( - f26dot6ToF64(glyph.*.metrics.horiBearingY), - top, - ); - bottom = @min( - f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), - bottom, - ); + const rect = getGlyphSize(glyph); + top = @max(rect.y + rect.height, top); + bottom = @min(rect.y, bottom); } else |_| {} } } @@ -1031,11 +1024,8 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { - break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height); + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { + break :cap getGlyphSize(face.handle.*.glyph).height; } else |_| {} } break :cap null; @@ -1044,11 +1034,8 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { - break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height); + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { + break :ex getGlyphSize(face.handle.*.glyph).height; } else |_| {} } break :ex null; @@ -1063,10 +1050,7 @@ pub const Face = struct { const glyph = face.getCharIndex('水') orelse break :ic_width null; - face.loadGlyph(glyph, .{ - .render = false, - .no_svg = true, - }) catch break :ic_width null; + face.loadGlyph(glyph, self.glyphLoadFlags(false)) catch break :ic_width null; const ft_glyph = face.handle.*.glyph; @@ -1078,21 +1062,19 @@ pub const Face = struct { // This can sometimes happen if there's a CJK font that has been // patched with the nerd fonts patcher and it butchers the advance // values so the advance ends up half the width of the actual glyph. - if (ft_glyph.*.metrics.width > ft_glyph.*.advance.x) { + const ft_glyph_width = getGlyphSize(ft_glyph).width; + const advance = f26dot6ToF64(ft_glyph.*.advance.x); + if (ft_glyph_width > advance) { var buf: [1024]u8 = undefined; const font_name = self.name(&buf) catch ""; log.warn( "(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.", - .{ - font_name, - f26dot6ToF64(ft_glyph.*.metrics.width), - f26dot6ToF64(ft_glyph.*.advance.x), - }, + .{ font_name, ft_glyph_width, advance }, ); break :ic_width null; } - break :ic_width f26dot6ToF64(ft_glyph.*.advance.x); + break :ic_width advance; }; return .{ @@ -1211,25 +1193,31 @@ test "color emoji" { alloc, &atlas, ft_font.glyphIndex('🥸').?, - .{ .grid_metrics = .{ - .cell_width = 13, - .cell_height = 24, - .cell_baseline = 0, - .underline_position = 0, - .underline_thickness = 0, - .strikethrough_position = 0, - .strikethrough_thickness = 0, - .overline_position = 0, - .overline_thickness = 0, - .box_thickness = 0, - .cursor_height = 0, - .icon_height = 0, - }, .constraint_width = 2, .constraint = .{ - .size_horizontal = .cover, - .size_vertical = .cover, - .align_horizontal = .center, - .align_vertical = .center, - } }, + .{ + .grid_metrics = .{ + .cell_width = 13, + .cell_height = 24, + .cell_baseline = 0, + .underline_position = 0, + .underline_thickness = 0, + .strikethrough_position = 0, + .strikethrough_thickness = 0, + .overline_position = 0, + .overline_thickness = 0, + .box_thickness = 0, + .cursor_height = 0, + .icon_height = 0, + .face_width = 13, + .face_height = 24, + .face_y = 0, + }, + .constraint_width = 2, + .constraint = .{ + .size = .fit, + .align_horizontal = .center, + .align_vertical = .center, + }, + }, ); try testing.expectEqual(@as(u32, 24), glyph.height); } diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 11902d310..04088b1aa 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -6,16 +6,15 @@ const Constraint = @import("face.zig").RenderOptions.Constraint; -/// Get the a constraints for the provided codepoint. +/// Get the constraints for the provided codepoint. pub fn getConstraint(cp: u21) ?Constraint { return switch (cp) { 0x2500...0x259f, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, .pad_left = -0.02, .pad_right = -0.02, .pad_top = -0.01, @@ -23,12 +22,11 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0x2630, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .height = .icon, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, .pad_left = 0.1, .pad_right = 0.1, .pad_top = 0.1, @@ -36,49 +34,45 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0x276c...0x276d, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3999999999999999, - .group_height = 1.1222570532915361, - .group_x = 0.1428571428571428, - .group_y = 0.0349162011173184, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7142857142857143, + .relative_height = 0.8910614525139665, + .relative_x = 0.1428571428571428, + .relative_y = 0.0349162011173184, .pad_top = 0.15, .pad_bottom = 0.15, }, 0x276e...0x276f, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0115606936416186, - .group_height = 1.1222570532915361, - .group_x = 0.0057142857142857, - .group_y = 0.0125698324022346, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9885714285714285, + .relative_height = 0.8910614525139665, + .relative_x = 0.0057142857142857, + .relative_y = 0.0125698324022346, .pad_top = 0.15, .pad_bottom = 0.15, }, 0x2770...0x2771, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, .pad_top = 0.15, .pad_bottom = 0.15, }, 0xe0b0, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.06, .pad_right = -0.06, .pad_top = -0.01, @@ -87,20 +81,18 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0b1, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.7, }, 0xe0b2, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.06, .pad_right = -0.06, .pad_top = -0.01, @@ -109,20 +101,18 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0b3, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.7, }, 0xe0b4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.06, .pad_right = -0.06, .pad_top = -0.01, @@ -131,20 +121,18 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0b5, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.5, }, 0xe0b6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.06, .pad_right = -0.06, .pad_top = -0.01, @@ -153,21 +141,19 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0b7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.5, }, 0xe0b8, 0xe0bc, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -176,20 +162,18 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xe0b9, 0xe0bd, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0ba, 0xe0be, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -198,19 +182,17 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xe0bb, 0xe0bf, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c0, 0xe0c8, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -218,18 +200,16 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c1, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c2, 0xe0ca, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -237,17 +217,15 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c3, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = 0.03, .pad_right = 0.03, .pad_top = 0.03, @@ -256,10 +234,9 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c5, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = 0.03, .pad_right = 0.03, .pad_top = 0.03, @@ -268,10 +245,9 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = 0.03, .pad_right = 0.03, .pad_top = 0.03, @@ -280,10 +256,9 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0c7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = 0.03, .pad_right = 0.03, .pad_top = 0.03, @@ -292,10 +267,9 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0cc, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.02, .pad_right = -0.02, .pad_top = -0.01, @@ -304,36 +278,32 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0cd, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.865, }, 0xe0ce, 0xe0d0...0xe0d1, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .fit_cover1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0cf, 0xe0d3, 0xe0d5, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, - .align_horizontal = .center, - .align_vertical = .center, + .size = .fit_cover1, + .align_horizontal = .center1, + .align_vertical = .center1, }, 0xe0d2, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.02, .pad_right = -0.02, .pad_top = -0.01, @@ -342,11 +312,10 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0d4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.02, .pad_right = -0.02, .pad_top = -0.01, @@ -355,11 +324,10 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0d6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -368,11 +336,10 @@ pub fn getConstraint(cp: u21) ?Constraint { }, 0xe0d7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .pad_left = -0.05, .pad_right = -0.05, .pad_top = -0.01, @@ -425,640 +392,583 @@ pub fn getConstraint(cp: u21) ?Constraint { 0xf307...0xf847, 0xf0001...0xf1af0, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, }, 0xea61, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3310225303292895, - .group_height = 1.0762439807383628, - .group_x = 0.0846354166666667, - .group_y = 0.0708426547352722, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7513020833333334, + .relative_height = 0.9291573452647278, + .relative_x = 0.0846354166666667, + .relative_y = 0.0708426547352722, }, 0xea7d, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1912058627581612, - .group_height = 1.1426759670259987, - .group_x = 0.0917225950782998, - .group_y = 0.0416204217536071, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8394854586129754, + .relative_height = 0.8751387347391787, + .relative_x = 0.0917225950782998, + .relative_y = 0.0416204217536071, }, 0xea99, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0642857142857143, - .group_height = 2.0929152148664345, - .group_x = 0.0302013422818792, - .group_y = 0.2269700332963374, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9395973154362416, + .relative_height = 0.4778024417314096, + .relative_x = 0.0302013422818792, + .relative_y = 0.2269700332963374, }, 0xea9a, 0xeaa1, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3032069970845481, - .group_height = 1.1731770833333333, - .group_x = 0.1526845637583893, - .group_y = 0.0754716981132075, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7673378076062640, + .relative_height = 0.8523862375138734, + .relative_x = 0.1526845637583893, + .relative_y = 0.0754716981132075, }, 0xea9b, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1640625000000000, - .group_height = 1.3134110787172011, - .group_x = 0.0721476510067114, - .group_y = 0.0871254162042175, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8590604026845637, + .relative_height = 0.7613762486126526, + .relative_x = 0.0721476510067114, + .relative_y = 0.0871254162042175, }, 0xea9c, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1640625000000000, - .group_height = 1.3201465201465201, - .group_x = 0.0721476510067114, - .group_y = 0.0832408435072142, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8590604026845637, + .relative_height = 0.7574916759156493, + .relative_x = 0.0721476510067114, + .relative_y = 0.0832408435072142, }, 0xea9d, 0xeaa0, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.4493150684931506, - .group_height = 1.9693989071038251, - .group_x = 0.2863534675615212, - .group_y = 0.2763596004439512, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4082774049217002, + .relative_height = 0.5077691453940066, + .relative_x = 0.2863534675615212, + .relative_y = 0.2763596004439512, }, 0xea9e...0xea9f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.9540983606557376, - .group_height = 2.4684931506849317, - .group_x = 0.2136465324384788, - .group_y = 0.3068812430632630, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5117449664429530, + .relative_height = 0.4051054384017758, + .relative_x = 0.2136465324384788, + .relative_y = 0.3068812430632630, }, 0xeaa2, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2405228758169935, - .group_height = 1.0595187680461982, - .group_x = 0.0679662802950474, - .group_y = 0.0147523709167545, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8061116965226555, + .relative_height = 0.9438247156716689, + .relative_x = 0.0679662802950474, + .relative_y = 0.0147523709167545, }, 0xeab4, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0054815974941269, - .group_height = 1.8994082840236686, - .group_y = 0.2024922118380062, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9945482866043613, + .relative_height = 0.5264797507788161, + .relative_y = 0.2024922118380062, }, 0xeab5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8994082840236686, - .group_height = 1.0054815974941269, - .group_x = 0.2024922118380062, - .group_y = 0.0054517133956386, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5264797507788161, + .relative_height = 0.9945482866043613, + .relative_x = 0.2024922118380062, + .relative_y = 0.0054517133956386, }, 0xeab6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8994082840236686, - .group_height = 1.0054815974941269, - .group_x = 0.2710280373831775, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5264797507788161, + .relative_height = 0.9945482866043613, + .relative_x = 0.2710280373831775, }, 0xeab7, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0054815974941269, - .group_height = 1.8994082840236686, - .group_x = 0.0054517133956386, - .group_y = 0.2710280373831775, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9945482866043613, + .relative_height = 0.5264797507788161, + .relative_x = 0.0054517133956386, + .relative_y = 0.2710280373831775, }, 0xead4...0xead5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.4144620811287478, - .group_x = 0.1483790523690773, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7069825436408977, + .relative_x = 0.1483790523690773, }, 0xead6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.1388535031847133, - .group_y = 0.0687919463087248, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8780760626398211, + .relative_y = 0.0687919463087248, }, 0xeb43, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3631840796019901, - .group_height = 1.0003813300793167, - .group_x = 0.1991657977059437, - .group_y = 0.0003811847221163, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7335766423357665, + .relative_height = 0.9996188152778837, + .relative_x = 0.1991657977059437, + .relative_y = 0.0003811847221163, }, 0xeb6e, 0xeb71, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 2.0183246073298431, - .group_y = 0.2522697795071336, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4954604409857328, + .relative_y = 0.2522697795071336, }, 0xeb6f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0104712041884816, - .group_x = 0.2493489583333333, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4973958333333333, + .relative_x = 0.2493489583333333, }, 0xeb70, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0104712041884816, - .group_height = 1.0039062500000000, - .group_x = 0.2493489583333333, - .group_y = 0.0038910505836576, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4973958333333333, + .relative_height = 0.9961089494163424, + .relative_x = 0.2493489583333333, + .relative_y = 0.0038910505836576, }, 0xeb8a, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.8828125000000000, - .group_height = 2.9818561935339356, - .group_x = 0.2642276422764228, - .group_y = 0.3313050881410256, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.3468834688346883, + .relative_height = 0.3353615785256410, + .relative_x = 0.2642276422764228, + .relative_y = 0.3313050881410256, }, 0xeb9a, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1440626883664857, - .group_height = 1.0595187680461982, - .group_x = 0.0679662802950474, - .group_y = 0.0147523709167545, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8740779768177028, + .relative_height = 0.9438247156716689, + .relative_x = 0.0679662802950474, + .relative_y = 0.0147523709167545, }, 0xebd5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0727069351230425, - .group_height = 1.0730882652023592, - .group_y = 0.0681102082395584, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9322210636079249, + .relative_height = 0.9318897917604415, + .relative_y = 0.0681102082395584, }, 0xebd6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.0003554839321263, - .group_y = 0.0003553576082064, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9996446423917936, + .relative_y = 0.0003553576082064, }, 0xec07, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.8604846818377689, - .group_height = 2.9804665603035656, - .group_x = 0.2615335565120357, - .group_y = 0.3311487268518519, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.3495911047345768, + .relative_height = 0.3355179398148149, + .relative_x = 0.2615335565120357, + .relative_y = 0.3311487268518519, }, 0xec0b, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0721073225265512, - .group_height = 1.0003813300793167, - .group_y = 0.0003811847221163, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9327424400417101, + .relative_height = 0.9996188152778837, + .relative_y = 0.0003811847221163, }, 0xec0c, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2486979166666667, - .group_x = 0.1991657977059437, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8008342022940563, + .relative_x = 0.1991657977059437, }, 0xf019, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, }, 0xf030, 0xf03e, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.1426844014510278, - .group_y = 0.0624338624338624, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.8751322751322751, + .relative_y = 0.0624338624338624, }, 0xf03d, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.3328631875881523, - .group_y = 0.1248677248677249, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7502645502645503, + .relative_y = 0.1248677248677249, }, 0xf03f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8003104407193382, - .group_x = 0.0005406676069582, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5554597570408116, + .relative_x = 0.0005406676069582, }, 0xf040, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1263939384681190, - .group_height = 1.0007255897868335, - .group_x = 0.0003164442515641, - .group_y = 0.0001959631589261, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8877888683953564, + .relative_height = 0.9992749363119733, + .relative_x = 0.0003164442515641, + .relative_y = 0.0001959631589261, }, 0xf044, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0087313432835820, - .group_height = 1.0077472527472529, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9913442331878375, + .relative_height = 0.9923123057630445, + .relative_y = 0.0002010014265405, }, 0xf04a, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.3321224771947897, - .group_y = 0.1247354497354497, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.7506817256817256, + .relative_y = 0.1247354497354497, }, 0xf051, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.7994923857868019, - .group_height = 1.3321224771947897, - .group_y = 0.1247354497354497, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5557122708039492, + .relative_height = 0.7506817256817256, + .relative_y = 0.1247354497354497, }, 0xf052, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1439802384724422, - .group_height = 1.1430071621244535, - .group_y = 0.0626172338785870, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8741409740917385, + .relative_height = 0.8748851565736010, + .relative_y = 0.0626172338785870, }, 0xf053, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0025185185185186, - .group_height = 1.1416267186919362, - .group_y = 0.0620882827561120, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4993711622401420, + .relative_height = 0.8759430588185509, + .relative_y = 0.0620882827561120, }, 0xf05a...0xf05b, 0xf0aa, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.0002824582824583, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987423244802840, + .relative_height = 0.9997176214776941, + .relative_y = 0.0002010014265405, }, 0xf071, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.1426844014510278, - .group_x = 0.0004701457451810, - .group_y = 0.0624338624338624, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.8751322751322751, + .relative_x = 0.0004701457451810, + .relative_y = 0.0624338624338624, }, 0xf078, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1434320241691844, - .group_height = 2.0026841590612778, - .group_y = 0.1879786499051550, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8745600777856455, + .relative_height = 0.4993298596163721, + .relative_y = 0.1879786499051550, }, 0xf07b, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.2285368802902055, - .group_y = 0.0930118110236220, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.8139763779527559, + .relative_y = 0.0930118110236220, }, 0xf081, 0xf092, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1441233373639663, - .group_height = 1.1430071621244535, - .group_y = 0.0626172338785870, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8740316426933279, + .relative_height = 0.8748851565736010, + .relative_y = 0.0626172338785870, }, 0xf08c, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2859733978234582, - .group_height = 1.1426844014510278, - .group_y = 0.0624338624338624, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7776210625293841, + .relative_height = 0.8751322751322751, + .relative_y = 0.0624338624338624, }, 0xf09f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.7489690176588770, - .group_x = 0.0006952841596131, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5717654171704958, + .relative_x = 0.0006952841596131, }, 0xf0a1, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.0749103295228757, - .group_y = 0.0349409448818898, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8885754583921015, + .relative_height = 0.9303101594008066, + .relative_y = 0.0349409448818898, }, 0xf0a2, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1429529187840552, - .group_height = 1.0002824582824583, - .group_x = 0.0001253913778381, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8749266777006549, + .relative_height = 0.9997176214776941, + .relative_x = 0.0001253913778381, + .relative_y = 0.0002010014265405, }, 0xf0a3, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0005921977940631, - .group_height = 1.0001448722153810, - .group_x = 0.0005918473033957, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9994081526966043, + .relative_height = 0.9998551487695376, + .relative_x = 0.0005918473033957, }, 0xf0a4, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.3332396658348704, - .group_y = 0.1250334663306335, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987423244802840, + .relative_height = 0.7500526916695081, + .relative_y = 0.1250334663306335, }, 0xf0ca, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0335226652102676, - .group_height = 1.2308163060897437, - .group_y = 0.0938253501046103, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9675646540335450, + .relative_height = 0.8124689241215546, + .relative_y = 0.0938253501046103, }, 0xf0d6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.4330042313117066, - .group_y = 0.1510826771653543, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6978346456692913, + .relative_y = 0.1510826771653543, }, 0xf0de, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3984670905653893, - .group_height = 2.6619718309859155, - .group_x = 0.0004030632809351, - .group_y = 0.5708994708994709, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7150686682199350, + .relative_height = 0.3756613756613756, + .relative_x = 0.0004030632809351, + .relative_y = 0.5708994708994709, }, 0xf0e7, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3348918927786344, - .group_height = 1.0001196386424678, - .group_x = 0.0006021702214782, - .group_y = 0.0001196243307751, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7491243338952770, + .relative_height = 0.9998803756692248, + .relative_x = 0.0006021702214782, + .relative_y = 0.0001196243307751, }, 0xf296, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0005202277820979, - .group_height = 1.0386597451628128, - .group_x = 0.0001795653226322, - .group_y = 0.0187142907131644, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9994800427141276, + .relative_height = 0.9627792014248586, + .relative_x = 0.0001795653226322, + .relative_y = 0.0187142907131644, }, 0xf2c4, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3292088488938882, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7523272214386461, }, 0xf2c5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0118264574212998, - .group_height = 1.1664315937940761, - .group_x = 0.0004377219006858, - .group_y = 0.0713422007255139, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9883117728988424, + .relative_height = 0.8573155985489722, + .relative_x = 0.0004377219006858, + .relative_y = 0.0713422007255139, }, 0xf2f0, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.0342088873926949, - .group_y = 0.0165984862232646, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987423244802840, + .relative_height = 0.9669226518842459, + .relative_y = 0.0165984862232646, }, 0xf306, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3001222493887530, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7691584391161260, }, else => null, }; diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index a103a30ac..4965dabe4 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -50,10 +50,10 @@ class PatchSetAttributeEntry(TypedDict): stretch: str params: dict[str, float | bool] - group_x: float - group_y: float - group_width: float - group_height: float + relative_x: float + relative_y: float + relative_width: float + relative_height: float class PatchSet(TypedDict): @@ -143,7 +143,7 @@ def parse_alignment(val: str) -> str | None: return { "l": ".start", "r": ".end", - "c": ".center", + "c": ".center1", # font-patcher specific centering rule, see face.zig "": None, }.get(val, ".none") @@ -158,10 +158,10 @@ def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash: float(params.get("overlap", 0.0)), float(params.get("xy-ratio", -1.0)), float(params.get("ypadding", 0.0)), - float(attr.get("group_x", 0.0)), - float(attr.get("group_y", 0.0)), - float(attr.get("group_width", 1.0)), - float(attr.get("group_height", 1.0)), + float(attr.get("relative_x", 0.0)), + float(attr.get("relative_y", 0.0)), + float(attr.get("relative_width", 1.0)), + float(attr.get("relative_height", 1.0)), ) @@ -187,10 +187,10 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) stretch = attr.get("stretch", "") params = attr.get("params", {}) - group_x = attr.get("group_x", 0.0) - group_y = attr.get("group_y", 0.0) - group_width = attr.get("group_width", 1.0) - group_height = attr.get("group_height", 1.0) + relative_x = attr.get("relative_x", 0.0) + relative_y = attr.get("relative_y", 0.0) + relative_width = attr.get("relative_width", 1.0) + relative_height = attr.get("relative_height", 1.0) overlap = params.get("overlap", 0.0) xy_ratio = params.get("xy-ratio", -1.0) @@ -204,28 +204,30 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) s = f"{keys}\n => .{{\n" - # These translations don't quite capture the way - # the actual patcher does scaling, but they're a - # good enough compromise. - if "xy" in stretch: - s += " .size_horizontal = .stretch,\n" - s += " .size_vertical = .stretch,\n" - elif "!" in stretch or "^" in stretch: - s += " .size_horizontal = .cover,\n" - s += " .size_vertical = .fit,\n" + # This maps the font_patcher stretch rules to a Constrain instance + # NOTE: some comments in font_patcher indicate that only x or y + # would also be a valid spec, but no icons use it, so we won't + # support it until we have to. + if "pa" in stretch: + if "!" in stretch or overlap: + s += " .size = .cover,\n" + else: + s += " .size = .fit_cover1,\n" + elif "xy" in stretch: + s += " .size = .stretch,\n" else: - s += " .size_horizontal = .fit,\n" - s += " .size_vertical = .fit,\n" + print(f"Warning: Unknown stretch rule {stretch}") - # `^` indicates that scaling should fill - # the whole cell, not just the icon height. + # `^` indicates that scaling should use the + # full cell height, not just the icon height, + # even when the constraint width is 1 if "^" not in stretch: s += " .height = .icon,\n" # There are two cases where we want to limit the constraint width to 1: # - If there's a `1` in the stretch mode string. - # - If the stretch mode is `xy` and there's not an explicit `2`. - if "1" in stretch or ("xy" in stretch and "2" not in stretch): + # - If the stretch mode is not `pa` and there's not an explicit `2`. + if "1" in stretch or ("pa" not in stretch and "2" not in stretch): s += " .max_constraint_width = 1,\n" if align is not None: @@ -233,14 +235,14 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) if valign is not None: s += f" .align_vertical = {valign},\n" - if group_width != 1.0: - s += f" .group_width = {group_width:.16f},\n" - if group_height != 1.0: - s += f" .group_height = {group_height:.16f},\n" - if group_x != 0.0: - s += f" .group_x = {group_x:.16f},\n" - if group_y != 0.0: - s += f" .group_y = {group_y:.16f},\n" + if relative_width != 1.0: + s += f" .relative_width = {relative_width:.16f},\n" + if relative_height != 1.0: + s += f" .relative_height = {relative_height:.16f},\n" + if relative_x != 0.0: + s += f" .relative_x = {relative_x:.16f},\n" + if relative_y != 0.0: + s += f" .relative_y = {relative_y:.16f},\n" # `overlap` and `ypadding` are mutually exclusive, # this is asserted in the nerd fonts patcher itself. @@ -286,7 +288,7 @@ def generate_zig_switch_arms( yMin = math.inf xMax = -math.inf yMax = -math.inf - individual_bounds: dict[int, tuple[int, int, int ,int]] = {} + individual_bounds: dict[int, tuple[int, int, int, int]] = {} for cp in group: if cp not in cmap: continue @@ -306,10 +308,10 @@ def generate_zig_switch_arms( this_bounds = individual_bounds[cp] this_width = this_bounds[2] - this_bounds[0] this_height = this_bounds[3] - this_bounds[1] - entries[cp]["group_width"] = group_width / this_width - entries[cp]["group_height"] = group_height / this_height - entries[cp]["group_x"] = (this_bounds[0] - xMin) / group_width - entries[cp]["group_y"] = (this_bounds[1] - yMin) / group_height + entries[cp]["relative_width"] = this_width / group_width + entries[cp]["relative_height"] = this_height / group_height + entries[cp]["relative_x"] = (this_bounds[0] - xMin) / group_width + entries[cp]["relative_y"] = (this_bounds[1] - yMin) / group_height del entries[0] @@ -350,7 +352,7 @@ if __name__ == "__main__": const Constraint = @import("face.zig").RenderOptions.Constraint; -/// Get the a constraints for the provided codepoint. +/// Get the constraints for the provided codepoint. pub fn getConstraint(cp: u21) ?Constraint { return switch (cp) { """) diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 5ab9d3cd4..49ab00ecd 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -197,7 +197,9 @@ pub const VTEvent = struct { ) !void { switch (@TypeOf(v)) { void => {}, - []const u8 => try md.put("data", try alloc.dupeZ(u8, v)), + []const u8, + [:0]const u8, + => try md.put("data", try alloc.dupeZ(u8, v)), else => |T| switch (@typeInfo(T)) { .@"struct" => |info| inline for (info.fields) |field| { try encodeMetadataSingle( @@ -284,7 +286,9 @@ pub const VTEvent = struct { try std.fmt.allocPrintZ(alloc, "{}", .{value}), ), - []const u8 => try md.put(key, try alloc.dupeZ(u8, value)), + []const u8, + [:0]const u8, + => try md.put(key, try alloc.dupeZ(u8, value)), else => |T| { @compileLog(T); diff --git a/src/lib/enum.zig b/src/lib/enum.zig new file mode 100644 index 000000000..c3971ebde --- /dev/null +++ b/src/lib/enum.zig @@ -0,0 +1,97 @@ +const std = @import("std"); + +/// Create an enum type with the given keys that is C ABI compatible +/// if we're targeting C, otherwise a Zig enum with smallest possible +/// backing type. +/// +/// In all cases, the enum keys will be created in the order given. +/// For C ABI, this means that the order MUST NOT be changed in order +/// to preserve ABI compatibility. You can set a key to null to +/// remove it from the Zig enum while keeping the "hole" in the C enum +/// to preserve ABI compatibility. +/// +/// C detection is up to the caller, since there are multiple ways +/// to do that. We rely on the `target` parameter to determine whether we +/// should create a C compatible enum or a Zig enum. +/// +/// For the Zig enum, the enum value is not guaranteed to be stable, so +/// it shouldn't be relied for things like serialization. +pub fn Enum( + target: Target, + keys: []const ?[:0]const u8, +) type { + var fields: [keys.len]std.builtin.Type.EnumField = undefined; + var fields_i: usize = 0; + var holes: usize = 0; + for (keys) |key_| { + const key: [:0]const u8 = key_ orelse { + switch (target) { + // For Zig we don't track holes because the enum value + // isn't guaranteed to be stable and we want to use the + // smallest possible backing type. + .zig => {}, + + // For C we must track holes to preserve ABI compatibility + // with subsequent values. + .c => holes += 1, + } + continue; + }; + + fields[fields_i] = .{ + .name = key, + .value = fields_i + holes, + }; + fields_i += 1; + } + + // Assigned to var so that the type name is nicer in stack traces. + const Result = @Type(.{ .@"enum" = .{ + .tag_type = switch (target) { + .c => c_int, + .zig => std.math.IntFittingRange(0, fields_i - 1), + }, + .fields = fields[0..fields_i], + .decls = &.{}, + .is_exhaustive = true, + } }); + return Result; +} + +pub const Target = union(enum) { + c, + zig, +}; + +test "zig" { + const testing = std.testing; + const T = Enum(.zig, &.{ "a", "b", "c", "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(u2, info.tag_type); +} + +test "c" { + const testing = std.testing; + const T = Enum(.c, &.{ "a", "b", "c", "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(c_int, info.tag_type); +} + +test "abi by removing a key" { + const testing = std.testing; + // C + { + const T = Enum(.c, &.{ "a", "b", null, "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(c_int, info.tag_type); + try testing.expectEqual(3, @intFromEnum(T.d)); + } + + // Zig + { + const T = Enum(.zig, &.{ "a", "b", null, "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(u2, info.tag_type); + try testing.expectEqual(2, @intFromEnum(T.d)); + } +} diff --git a/src/lib/main.zig b/src/lib/main.zig new file mode 100644 index 000000000..4ef8dcb2d --- /dev/null +++ b/src/lib/main.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +const enumpkg = @import("enum.zig"); + +pub const allocator = @import("allocator.zig"); +pub const Enum = enumpkg.Enum; +pub const EnumTarget = enumpkg.Target; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 656509cce..8c49b4900 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -7,6 +7,7 @@ //! by thousands of users for years. However, the API itself (functions, //! types, etc.) may change without warning. We're working on stabilizing //! this in the future. +const lib = @This(); // The public API below reproduces a lot of terminal/main.zig but // is separate because (1) we need our root file to be in `src/` @@ -30,8 +31,8 @@ pub const size = terminal.size; pub const x11_color = terminal.x11_color; pub const Charset = terminal.Charset; -pub const CharsetSlot = terminal.Slots; -pub const CharsetActiveSlot = terminal.ActiveSlot; +pub const CharsetSlot = terminal.CharsetSlot; +pub const CharsetActiveSlot = terminal.CharsetActiveSlot; pub const Cell = page.Cell; pub const Coordinate = point.Coordinate; pub const CSI = Parser.Action.CSI; @@ -68,16 +69,22 @@ 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) { + if (@import("root") == lib) { const c = terminal.c_api; @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); + @export(&c.osc_next, .{ .name = "ghostty_osc_next" }); + @export(&c.osc_reset, .{ .name = "ghostty_osc_reset" }); + @export(&c.osc_end, .{ .name = "ghostty_osc_end" }); + @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); + @export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" }); } } test { _ = terminal; - - // Tests always test the C API - _ = terminal.c_api; + _ = @import("lib/main.zig"); + if (comptime terminal.options.c_abi) { + _ = terminal.c_api; + } } diff --git a/src/main_c.zig b/src/main_c.zig index 9a9bcc6d2..d3fb753ef 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -63,18 +63,42 @@ const Info = extern struct { pub const String = extern struct { ptr: ?[*]const u8, len: usize, + sentinel: bool, pub const empty: String = .{ .ptr = null, .len = 0, + .sentinel = false, }; - pub fn fromSlice(slice: []const u8) String { + pub fn fromSlice(slice: anytype) String { return .{ .ptr = slice.ptr, .len = slice.len, + .sentinel = sentinel: { + const info = @typeInfo(@TypeOf(slice)); + switch (info) { + .pointer => |p| { + if (p.size != .slice) @compileError("only slices supported"); + if (p.child != u8) @compileError("only u8 slices supported"); + const sentinel_ = p.sentinel(); + if (sentinel_) |sentinel| if (sentinel != 0) @compileError("only 0 is supported for sentinels"); + break :sentinel sentinel_ != null; + }, + else => @compileError("only []const u8 and [:0]const u8"), + } + }, }; } + + pub fn deinit(self: *const String) void { + const ptr = self.ptr orelse return; + if (self.sentinel) { + state.alloc.free(ptr[0..self.len :0]); + } else { + state.alloc.free(ptr[0..self.len]); + } + } }; /// Initialize ghostty global state. @@ -129,5 +153,45 @@ pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { /// Free a string allocated by Ghostty. pub export fn ghostty_string_free(str: String) void { - state.alloc.free(str.ptr.?[0..str.len]); + str.deinit(); +} + +test "ghostty_string_s empty string" { + const testing = std.testing; + const empty_string = String.empty; + defer empty_string.deinit(); + + try testing.expect(empty_string.len == 0); + try testing.expect(empty_string.sentinel == false); +} + +test "ghostty_string_s c string" { + const testing = std.testing; + state.alloc = testing.allocator; + + const slice: [:0]const u8 = "hello"; + const allocated_slice = try testing.allocator.dupeZ(u8, slice); + const c_null_string = String.fromSlice(allocated_slice); + defer c_null_string.deinit(); + + try testing.expect(allocated_slice[5] == 0); + try testing.expect(@TypeOf(slice) == [:0]const u8); + try testing.expect(@TypeOf(allocated_slice) == [:0]u8); + try testing.expect(c_null_string.len == 5); + try testing.expect(c_null_string.sentinel == true); +} + +test "ghostty_string_s zig string" { + const testing = std.testing; + state.alloc = testing.allocator; + + const slice: []const u8 = "hello"; + const allocated_slice = try testing.allocator.dupe(u8, slice); + const zig_string = String.fromSlice(allocated_slice); + defer zig_string.deinit(); + + try testing.expect(@TypeOf(slice) == []const u8); + try testing.expect(@TypeOf(allocated_slice) == []u8); + try testing.expect(zig_string.len == 5); + try testing.expect(zig_string.sentinel == false); } diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 3cf306f91..46e660bfd 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -236,8 +236,8 @@ pub fn isCovering(cp: u21) bool { } /// Returns true of the codepoint is a "symbol-like" character, which -/// for now we define as anything in a private use area and anything -/// in several unicode blocks: +/// for now we define as anything in a private use area, except +/// the Powerline range, and anything in several unicode blocks: /// - Dingbats /// - Emoticons /// - Miscellaneous Symbols @@ -249,11 +249,13 @@ pub fn isCovering(cp: u21) bool { /// In the future it may be prudent to expand this to encompass more /// symbol-like characters, and/or exclude some PUA sections. pub fn isSymbol(cp: u21) bool { - return symbols.get(cp); + return symbols.get(cp) and !isPowerline(cp); } /// Returns the appropriate `constraint_width` for /// the provided cell when rendering its glyph(s). +/// +/// Tested as part of the Screen tests. pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const cell = cell_pin.rowAndCell().cell; const cp = cell.codepoint(); @@ -274,9 +276,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { // If we have a previous cell and it was a symbol then we need // to also constrain. This is so that multiple PUA glyphs align. - // As an exception, we ignore powerline glyphs since they are - // used for box drawing and we consider them whitespace. - if (cell_pin.x > 0) prev: { + if (cell_pin.x > 0) { const prev_cp = prev_cp: { var copy = cell_pin; copy.x -= 1; @@ -284,9 +284,6 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { break :prev_cp prev_cell.codepoint(); }; - // We consider powerline glyphs whitespace. - if (isPowerline(prev_cp)) break :prev; - if (isSymbol(prev_cp)) { return 1; } @@ -300,10 +297,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const next_cell = copy.rowAndCell().cell; break :next_cp next_cell.codepoint(); }; - if (next_cp == 0 or - isSpace(next_cp) or - isPowerline(next_cp)) - { + if (next_cp == 0 or isSpace(next_cp)) { return 2; } @@ -312,9 +306,10 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { } /// Whether min contrast should be disabled for a given glyph. +/// True for glyphs used for terminal graphics, such as box +/// drawing characters, block elements, and Powerline glyphs. pub fn noMinContrast(cp: u21) bool { - // TODO: We should disable for all box drawing type characters. - return isPowerline(cp); + return isBoxDrawing(cp) or isBlockElement(cp) or isLegacyComputing(cp) or isPowerline(cp); } // Some general spaces, others intentionally kept @@ -328,10 +323,36 @@ fn isSpace(char: u21) bool { }; } +// Returns true if the codepoint is a box drawing character. +fn isBoxDrawing(char: u21) bool { + return switch (char) { + 0x2500...0x257F => true, + else => false, + }; +} + +// Returns true if the codepoint is a block element. +fn isBlockElement(char: u21) bool { + return switch (char) { + 0x2580...0x259F => true, + else => false, + }; +} + +// Returns true if the codepoint is in a Symbols for Legacy +// Computing block, including supplements. +fn isLegacyComputing(char: u21) bool { + return switch (char) { + 0x1FB00...0x1FBFF => true, + 0x1CC00...0x1CEBF => true, // Supplement introduced in Unicode 16.0 + else => false, + }; +} + // Returns true if the codepoint is a part of the Powerline range. fn isPowerline(char: u21) bool { return switch (char) { - 0xE0B0...0xE0C8, 0xE0CA, 0xE0CC...0xE0D2, 0xE0D4 => true, + 0xE0B0...0xE0D7 => true, else => false, }; } @@ -492,3 +513,97 @@ test "Contents with zero-sized screen" { c.setCursor(null, null); try testing.expect(c.getCursorGlyph() == null); } + +test "Screen cell constraint widths" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try terminal.Screen.init(alloc, 4, 1, 0); + defer s.deinit(); + + // for each case, the numbers in the comment denote expected + // constraint widths for the symbol-containing cells + + // symbol->nothing: 2 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } + + // symbol->character: 1 + { + try s.testWriteString("z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // symbol->space: 2 + { + try s.testWriteString(" z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } + // symbol->no-break space: 1 + { + try s.testWriteString("\u{00a0}z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // symbol->end of row: 1 + { + try s.testWriteString(" "); + const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p3)); + s.reset(); + } + + // character->symbol: 2 + { + try s.testWriteString("z"); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p1)); + s.reset(); + } + + // symbol->symbol: 1,1 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + try testing.expectEqual(1, constraintWidth(p1)); + s.reset(); + } + + // symbol->space->symbol: 2,2 + { + try s.testWriteString(" "); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + try testing.expectEqual(2, constraintWidth(p2)); + s.reset(); + } + + // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p1)); + s.reset(); + } +} diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index fbc8cab99..802c769a6 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -3093,8 +3093,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // its cell(s), we don't modify the alignment at all. .constraint = getConstraint(cp) orelse if (cellpkg.isSymbol(cp)) .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit, } else .none, .constraint_width = constraintWidth(cell_pin), }, diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 1f2e814f6..6deb03da5 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -274,7 +274,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action { // Exit depends on current state if (self.state == next_state) null else switch (self.state) { .osc_string => if (self.osc_parser.end(c)) |cmd| - Action{ .osc_dispatch = cmd } + Action{ .osc_dispatch = cmd.* } else null, .dcs_passthrough => Action{ .dcs_unhook = {} }, diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig index 1b0449bbf..e209a56fa 100644 --- a/src/terminal/build_options.zig +++ b/src/terminal/build_options.zig @@ -1,5 +1,6 @@ const std = @import("std"); +/// Options set by Zig build.zig and exposed via `terminal_options`. pub const Options = struct { /// The target artifact to build. This will gate some functionality. artifact: Artifact, @@ -23,6 +24,10 @@ pub const Options = struct { /// generally be disabled in production builds. slow_runtime_safety: bool, + /// Force C ABI mode on or off. If not set, then it will be set based on + /// Options. + c_abi: bool, + /// Add the required build options for the terminal module. pub fn add( self: Options, @@ -31,6 +36,7 @@ pub const Options = struct { ) void { const opts = b.addOptions(); opts.addOption(Artifact, "artifact", self.artifact); + opts.addOption(bool, "c_abi", self.c_abi); opts.addOption(bool, "oniguruma", self.oniguruma); opts.addOption(bool, "simd", self.simd); opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig new file mode 100644 index 000000000..68fd77edd --- /dev/null +++ b/src/terminal/c/main.zig @@ -0,0 +1,17 @@ +pub const osc = @import("osc.zig"); + +// The full C API, unexported. +pub const osc_new = osc.new; +pub const osc_free = osc.free; +pub const osc_reset = osc.reset; +pub const osc_next = osc.next; +pub const osc_end = osc.end; +pub const osc_command_type = osc.commandType; +pub const osc_command_data = osc.commandData; + +test { + _ = osc; + + // We want to make sure we run the tests for the C allocator interface. + _ = @import("../../lib/allocator.zig"); +} diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig new file mode 100644 index 000000000..8b6a8409c --- /dev/null +++ b/src/terminal/c/osc.zig @@ -0,0 +1,132 @@ +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"); +const Result = @import("result.zig").Result; + +/// C: GhosttyOscParser +pub const Parser = ?*osc.Parser; + +/// C: GhosttyOscCommand +pub const Command = ?*osc.Command; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Parser, +) 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 free(parser_: Parser) 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); +} + +pub fn reset(parser_: Parser) callconv(.c) void { + parser_.?.reset(); +} + +pub fn next(parser_: Parser, byte: u8) callconv(.c) void { + parser_.?.next(byte); +} + +pub fn end(parser_: Parser, terminator: u8) callconv(.c) Command { + return parser_.?.end(terminator); +} + +pub fn commandType(command_: Command) callconv(.c) osc.Command.Key { + const command = command_ orelse return .invalid; + return command.*; +} + +/// C: GhosttyOscCommandData +pub const CommandData = enum(c_int) { + invalid = 0, + change_window_title_str = 1, + + /// Output type expected for querying the data of the given kind. + pub fn OutType(comptime self: CommandData) type { + return switch (self) { + .invalid => void, + .change_window_title_str => [*:0]const u8, + }; + } +}; + +pub fn commandData( + command_: Command, + data: CommandData, + out: ?*anyopaque, +) callconv(.c) bool { + return switch (data) { + inline else => |comptime_data| commandDataTyped( + command_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn commandDataTyped( + command_: Command, + comptime data: CommandData, + out: *data.OutType(), +) bool { + const command = command_.?; + switch (data) { + .invalid => return false, + .change_window_title_str => switch (command.*) { + .change_window_title => |v| out.* = v.ptr, + else => return false, + }, + } + + return true; +} + +test "alloc" { + const testing = std.testing; + var p: Parser = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &p, + )); + free(p); +} + +test "command type null" { + const testing = std.testing; + try testing.expectEqual(.invalid, commandType(null)); +} + +test "change window title" { + const testing = std.testing; + var p: Parser = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &p, + )); + defer free(p); + + // Parse it + next(p, '0'); + next(p, ';'); + next(p, 'a'); + const cmd = end(p, 0); + try testing.expectEqual(.change_window_title, commandType(cmd)); + + // Extract the title + var title: [*:0]const u8 = undefined; + try testing.expect(commandData(cmd, .change_window_title_str, @ptrCast(&title))); + try testing.expectEqualStrings("a", std.mem.span(title)); +} diff --git a/src/terminal/c/result.zig b/src/terminal/c/result.zig new file mode 100644 index 000000000..a2ebc9b69 --- /dev/null +++ b/src/terminal/c/result.zig @@ -0,0 +1,5 @@ +/// C: GhosttyResult +pub const Result = enum(c_int) { + success = 0, + out_of_memory = -1, +}; diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig deleted file mode 100644 index 194a91d6d..000000000 --- a/src/terminal/c_api.zig +++ /dev/null @@ -1,49 +0,0 @@ -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 4064c0c9c..6875ba89d 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -1,5 +1,4 @@ const builtin = @import("builtin"); -const build_options = @import("terminal_options"); const charsets = @import("charsets.zig"); const sanitize = @import("sanitize.zig"); @@ -21,7 +20,7 @@ pub const page = @import("page.zig"); pub const parse_table = @import("parse_table.zig"); pub const search = @import("search.zig"); pub const size = @import("size.zig"); -pub const tmux = if (build_options.tmux_control_mode) @import("tmux.zig") else struct {}; +pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {}; pub const x11_color = @import("x11_color.zig"); pub const Charset = charsets.Charset; @@ -62,9 +61,11 @@ pub const Attribute = sgr.Attribute; pub const isSafePaste = sanitize.isSafePaste; +pub const Options = @import("build_options.zig").Options; +pub const options = @import("terminal_options"); + /// 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"); +pub const c_api = if (options.c_abi) @import("c/main.zig") else void; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index bd7337b42..800257c3d 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -7,9 +7,11 @@ const osc = @This(); const std = @import("std"); const builtin = @import("builtin"); +const build_options = @import("terminal_options"); const mem = std.mem; const assert = std.debug.assert; const Allocator = mem.Allocator; +const LibEnum = @import("../lib/enum.zig").Enum; const RGB = @import("color.zig").RGB; const kitty_color = @import("kitty/color.zig"); const osc_color = @import("osc/color.zig"); @@ -17,26 +19,26 @@ pub const color = osc_color; const log = std.log.scoped(.osc); -pub const Command = union(enum) { +pub const Command = union(Key) { /// This generally shouldn't ever be set except as an initial zero value. /// Ignore it. invalid, /// Set the window title of the terminal /// - /// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8 + /// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8 /// with each code unit further encoded with two hex digits). /// /// If title mode 2 is set or the terminal is setup for unconditional /// utf-8 titles text is interpreted as utf-8. Else text is interpreted /// as latin1. - change_window_title: []const u8, + change_window_title: [:0]const u8, /// Set the icon of the terminal window. The name of the icon is not /// well defined, so this is currently ignored by Ghostty at the time /// of writing this. We just parse it so that we don't get parse errors /// in the log. - change_window_icon: []const u8, + change_window_icon: [:0]const u8, /// First do a fresh-line. Then start a new command, and enter prompt mode: /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a @@ -52,7 +54,7 @@ pub const Command = union(enum) { /// - secondary: a non-editable continuation line /// - right: a right-aligned prompt that may need adjustment during reflow prompt_start: struct { - aid: ?[]const u8 = null, + aid: ?[:0]const u8 = null, kind: enum { primary, continuation, secondary, right } = .primary, redraw: bool = true, }, @@ -94,7 +96,7 @@ pub const Command = union(enum) { /// contents is set on the clipboard. clipboard_contents: struct { kind: u8, - data: []const u8, + data: [:0]const u8, }, /// OSC 7. Reports the current working directory of the shell. This is @@ -104,7 +106,7 @@ pub const Command = union(enum) { report_pwd: struct { /// The reported pwd value. This is not checked for validity. It should /// be a file URL but it is up to the caller to utilize this value. - value: []const u8, + value: [:0]const u8, }, /// OSC 22. Set the mouse shape. There doesn't seem to be a standard @@ -112,7 +114,7 @@ pub const Command = union(enum) { /// are moving towards using the W3C CSS cursor names. For OSC parsing, /// we just parse whatever string is given. mouse_shape: struct { - value: []const u8, + value: [:0]const u8, }, /// OSC color operations to set, reset, or report color settings. Some OSCs @@ -136,14 +138,14 @@ pub const Command = union(enum) { /// Show a desktop notification (OSC 9 or OSC 777) show_desktop_notification: struct { - title: []const u8, - body: []const u8, + title: [:0]const u8, + body: [:0]const u8, }, /// Start a hyperlink (OSC 8) hyperlink_start: struct { - id: ?[]const u8 = null, - uri: []const u8, + id: ?[:0]const u8 = null, + uri: [:0]const u8, }, /// End a hyperlink (OSC 8) @@ -155,12 +157,12 @@ pub const Command = union(enum) { }, /// ConEmu show GUI message box (OSC 9;2) - conemu_show_message_box: []const u8, + conemu_show_message_box: [:0]const u8, /// ConEmu change tab title (OSC 9;3) conemu_change_tab_title: union(enum) { reset, - value: []const u8, + value: [:0]const u8, }, /// ConEmu progress report (OSC 9;4) @@ -170,7 +172,35 @@ pub const Command = union(enum) { conemu_wait_input, /// ConEmu GUI macro (OSC 9;6) - conemu_guimacro: []const u8, + conemu_guimacro: [:0]const u8, + + pub const Key = LibEnum( + if (build_options.c_abi) .c else .zig, + // NOTE: Order matters, see LibEnum documentation. + &.{ + "invalid", + "change_window_title", + "change_window_icon", + "prompt_start", + "prompt_end", + "end_of_input", + "end_of_command", + "clipboard_contents", + "report_pwd", + "mouse_shape", + "color_operation", + "kitty_color_protocol", + "show_desktop_notification", + "hyperlink_start", + "hyperlink_end", + "conemu_sleep", + "conemu_show_message_box", + "conemu_change_tab_title", + "conemu_progress_report", + "conemu_wait_input", + "conemu_guimacro", + }, + ); pub const ProgressReport = struct { pub const State = enum(c_int) { @@ -275,7 +305,7 @@ pub const Parser = struct { /// Temporary state that is dependent on the current state. temp_state: union { /// Current string parameter being populated - str: *[]const u8, + str: *[:0]const u8, /// Current numeric parameter being populated num: u16, @@ -431,7 +461,7 @@ pub const Parser = struct { self.reset(); } - /// Reset the parser start. + /// Reset the parser state. pub fn reset(self: *Parser) void { // If the state is already empty then we do nothing because // we may touch uninitialized memory. @@ -468,7 +498,10 @@ pub const Parser = struct { // If our buffer is full then we're invalid, so we set our state // accordingly and indicate the sequence is incomplete so that we // don't accidentally issue a command when ending. - if (self.buf_idx >= self.buf.len) { + // + // We always keep space for 1 byte at the end to null-terminate + // values. + if (self.buf_idx >= self.buf.len - 1) { if (self.state != .invalid) { log.warn( "OSC sequence too long (> {d}), ignoring. state={}", @@ -1007,7 +1040,8 @@ pub const Parser = struct { .notification_title => switch (c) { ';' => { - self.command.show_desktop_notification.title = self.buf[self.buf_start .. self.buf_idx - 1]; + self.buf[self.buf_idx - 1] = 0; + self.command.show_desktop_notification.title = self.buf[self.buf_start .. self.buf_idx - 1 :0]; self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; self.buf_start = self.buf_idx; self.state = .string; @@ -1376,7 +1410,8 @@ pub const Parser = struct { fn endHyperlink(self: *Parser) void { switch (self.command) { .hyperlink_start => |*v| { - const value = self.buf[self.buf_start..self.buf_idx]; + self.buf[self.buf_idx] = 0; + const value = self.buf[self.buf_start..self.buf_idx :0]; if (v.id == null and value.len == 0) { self.command = .{ .hyperlink_end = {} }; return; @@ -1390,10 +1425,12 @@ pub const Parser = struct { } fn endHyperlinkOptionValue(self: *Parser) void { - const value = if (self.buf_start == self.buf_idx) + const value: [:0]const u8 = if (self.buf_start == self.buf_idx) "" - else - self.buf[self.buf_start .. self.buf_idx - 1]; + else buf: { + self.buf[self.buf_idx - 1] = 0; + break :buf self.buf[self.buf_start .. self.buf_idx - 1 :0]; + }; if (mem.eql(u8, self.temp_state.key, "id")) { switch (self.command) { @@ -1408,7 +1445,11 @@ pub const Parser = struct { } fn endSemanticOptionValue(self: *Parser) void { - const value = self.buf[self.buf_start..self.buf_idx]; + const value = value: { + self.buf[self.buf_idx] = 0; + defer self.buf_idx += 1; + break :value self.buf[self.buf_start..self.buf_idx :0]; + }; if (mem.eql(u8, self.temp_state.key, "aid")) { switch (self.command) { @@ -1465,7 +1506,9 @@ pub const Parser = struct { } fn endString(self: *Parser) void { - self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx]; + self.buf[self.buf_idx] = 0; + defer self.buf_idx += 1; + self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx :0]; } fn endConEmuSleepValue(self: *Parser) void { @@ -1559,15 +1602,25 @@ pub const Parser = struct { } fn endAllocableString(self: *Parser) void { + const alloc = self.alloc.?; const list = self.buf_dynamic.?; - self.temp_state.str.* = list.items; + list.append(alloc, 0) catch { + log.warn("allocation failed on allocable string termination", .{}); + self.temp_state.str.* = ""; + return; + }; + + self.temp_state.str.* = list.items[0 .. list.items.len - 1 :0]; } /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. The optional terminator_ch /// is the final character in the OSC sequence. This is used to determine /// the response terminator. - pub fn end(self: *Parser, terminator_ch: ?u8) ?Command { + /// + /// The returned pointer is only valid until the next call to the parser. + /// Callers should copy out any data they wish to retain across calls. + pub fn end(self: *Parser, terminator_ch: ?u8) ?*Command { if (!self.complete) { if (comptime !builtin.is_test) log.warn( "invalid OSC command: {s}", @@ -1626,7 +1679,7 @@ pub const Parser = struct { else => {}, } - return self.command; + return &self.command; } }; @@ -1642,7 +1695,7 @@ test "OSC: change_window_title" { p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } @@ -1655,7 +1708,7 @@ test "OSC: change_window_title with 2" { p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } @@ -1677,7 +1730,7 @@ test "OSC: change_window_title with utf8" { p.next(0xE2); p.next(0x80); p.next(0x90); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("— ‐", cmd.change_window_title); } @@ -1688,7 +1741,7 @@ test "OSC: change_window_title empty" { var p: Parser = .init(); p.next('2'); p.next(';'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("", cmd.change_window_title); } @@ -1701,7 +1754,7 @@ test "OSC: change_window_icon" { p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_icon); try testing.expectEqualStrings("ab", cmd.change_window_icon); } @@ -1714,7 +1767,7 @@ test "OSC: prompt_start" { const input = "133;A"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.aid == null); try testing.expect(cmd.prompt_start.redraw); @@ -1728,7 +1781,7 @@ test "OSC: prompt_start with single option" { const input = "133;A;aid=14"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); } @@ -1741,7 +1794,7 @@ test "OSC: prompt_start with redraw disabled" { const input = "133;A;redraw=0"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(!cmd.prompt_start.redraw); } @@ -1754,7 +1807,7 @@ test "OSC: prompt_start with redraw invalid value" { const input = "133;A;redraw=42"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.redraw); try testing.expect(cmd.prompt_start.kind == .primary); @@ -1768,7 +1821,7 @@ test "OSC: prompt_start with continuation" { const input = "133;A;k=c"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.kind == .continuation); } @@ -1781,7 +1834,7 @@ test "OSC: prompt_start with secondary" { const input = "133;A;k=s"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.kind == .secondary); } @@ -1794,7 +1847,7 @@ test "OSC: end_of_command no exit code" { const input = "133;D"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .end_of_command); } @@ -1806,7 +1859,7 @@ test "OSC: end_of_command with exit code" { const input = "133;D;25"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .end_of_command); try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); } @@ -1819,7 +1872,7 @@ test "OSC: prompt_end" { const input = "133;B"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .prompt_end); } @@ -1831,7 +1884,7 @@ test "OSC: end_of_input" { const input = "133;C"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .end_of_input); } @@ -1843,7 +1896,7 @@ test "OSC: get/set clipboard" { const input = "52;s;?"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 's'); try testing.expectEqualStrings("?", cmd.clipboard_contents.data); @@ -1857,7 +1910,7 @@ test "OSC: get/set clipboard (optional parameter)" { const input = "52;;?"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 'c'); try testing.expectEqualStrings("?", cmd.clipboard_contents.data); @@ -1872,7 +1925,7 @@ test "OSC: get/set clipboard with allocator" { const input = "52;s;?"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 's'); try testing.expectEqualStrings("?", cmd.clipboard_contents.data); @@ -1887,7 +1940,7 @@ test "OSC: clear clipboard" { const input = "52;;"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 'c'); try testing.expectEqualStrings("", cmd.clipboard_contents.data); @@ -1901,7 +1954,7 @@ test "OSC: report pwd" { const input = "7;file:///tmp/example"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .report_pwd); try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value); } @@ -1913,7 +1966,7 @@ test "OSC: report pwd empty" { const input = "7;"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .report_pwd); try testing.expectEqualStrings("", cmd.report_pwd.value); } @@ -1926,7 +1979,7 @@ test "OSC: pointer cursor" { const input = "22;pointer"; for (input) |ch| p.next(ch); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .mouse_shape); try testing.expectEqualStrings("pointer", cmd.mouse_shape.value); } @@ -1943,6 +1996,36 @@ test "OSC: longer than buffer" { try testing.expect(p.complete == false); } +test "OSC: one shorter than buffer length" { + const testing = std.testing; + + var p: Parser = .init(); + + const prefix = "0;"; + const title = "a" ** (Parser.MAX_BUF - prefix.len - 1); + const input = prefix ++ title; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings(title, cmd.change_window_title); +} + +test "OSC: exactly at buffer length" { + const testing = std.testing; + + var p: Parser = .init(); + + const prefix = "0;"; + const title = "a" ** (Parser.MAX_BUF - prefix.len); + const input = prefix ++ title; + for (input) |ch| p.next(ch); + + // This should be null because we always reserve space for a null terminator. + try testing.expect(p.end(null) == null); + try testing.expect(p.complete == false); +} + test "OSC: OSC 9;1 ConEmu sleep" { const testing = std.testing; @@ -1951,7 +2034,7 @@ test "OSC: OSC 9;1 ConEmu sleep" { const input = "9;1;420"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_sleep); try testing.expectEqual(420, cmd.conemu_sleep.duration_ms); @@ -1965,7 +2048,7 @@ test "OSC: OSC 9;1 ConEmu sleep with no value default to 100ms" { const input = "9;1;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_sleep); try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); @@ -1979,7 +2062,7 @@ test "OSC: OSC 9;1 conemu sleep cannot exceed 10000ms" { const input = "9;1;12345"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_sleep); try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms); @@ -1993,7 +2076,7 @@ test "OSC: OSC 9;1 conemu sleep invalid input" { const input = "9;1;foo"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_sleep); try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); @@ -2007,7 +2090,7 @@ test "OSC: OSC 9;1 conemu sleep -> desktop notification 1" { const input = "9;1"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("1", cmd.show_desktop_notification.body); @@ -2021,7 +2104,7 @@ test "OSC: OSC 9;1 conemu sleep -> desktop notification 2" { const input = "9;1a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body); @@ -2035,7 +2118,7 @@ test "OSC: OSC 9 show desktop notification" { const input = "9;Hello world"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("", cmd.show_desktop_notification.title); try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); @@ -2049,7 +2132,7 @@ test "OSC: OSC 9 show single character desktop notification" { const input = "9;H"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("", cmd.show_desktop_notification.title); try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); @@ -2063,7 +2146,7 @@ test "OSC: OSC 777 show desktop notification with title" { const input = "777;notify;Title;Body"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title"); try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); @@ -2077,7 +2160,7 @@ test "OSC: OSC 9;2 ConEmu message box" { const input = "9;2;hello world"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_show_message_box); try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box); } @@ -2090,7 +2173,7 @@ test "OSC: 9;2 ConEmu message box invalid input" { const input = "9;2"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); } @@ -2103,7 +2186,7 @@ test "OSC: 9;2 ConEmu message box empty message" { const input = "9;2;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_show_message_box); try testing.expectEqualStrings("", cmd.conemu_show_message_box); } @@ -2116,7 +2199,7 @@ test "OSC: 9;2 ConEmu message box spaces only message" { const input = "9;2; "; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_show_message_box); try testing.expectEqualStrings(" ", cmd.conemu_show_message_box); } @@ -2129,7 +2212,7 @@ test "OSC: OSC 9;2 message box -> desktop notification 1" { const input = "9;2"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); @@ -2143,7 +2226,7 @@ test "OSC: OSC 9;2 message box -> desktop notification 2" { const input = "9;2a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body); @@ -2157,7 +2240,7 @@ test "OSC: 9;3 ConEmu change tab title" { const input = "9;3;foo bar"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_change_tab_title); try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value); } @@ -2170,7 +2253,7 @@ test "OSC: 9;3 ConEmu change tab title reset" { const input = "9;3;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; const expected_command: Command = .{ .conemu_change_tab_title = .reset }; try testing.expectEqual(expected_command, cmd); @@ -2184,7 +2267,7 @@ test "OSC: 9;3 ConEmu change tab title spaces only" { const input = "9;3; "; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_change_tab_title); try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value); @@ -2198,7 +2281,7 @@ test "OSC: OSC 9;3 change tab title -> desktop notification 1" { const input = "9;3"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("3", cmd.show_desktop_notification.body); @@ -2212,7 +2295,7 @@ test "OSC: OSC 9;3 message box -> desktop notification 2" { const input = "9;3a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body); @@ -2226,7 +2309,7 @@ test "OSC: OSC 9;4 ConEmu progress set" { const input = "9;4;1;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expect(cmd.conemu_progress_report.progress == 100); @@ -2240,7 +2323,7 @@ test "OSC: OSC 9;4 ConEmu progress set overflow" { const input = "9;4;1;900"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expectEqual(100, cmd.conemu_progress_report.progress); @@ -2254,7 +2337,7 @@ test "OSC: OSC 9;4 ConEmu progress set single digit" { const input = "9;4;1;9"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expect(cmd.conemu_progress_report.progress == 9); @@ -2268,7 +2351,7 @@ test "OSC: OSC 9;4 ConEmu progress set double digit" { const input = "9;4;1;94"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expectEqual(94, cmd.conemu_progress_report.progress); @@ -2282,7 +2365,7 @@ test "OSC: OSC 9;4 ConEmu progress set extra semicolon ignored" { const input = "9;4;1;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .set); try testing.expectEqual(100, cmd.conemu_progress_report.progress); @@ -2296,7 +2379,7 @@ test "OSC: OSC 9;4 ConEmu progress remove with no progress" { const input = "9;4;0;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .remove); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2310,7 +2393,7 @@ test "OSC: OSC 9;4 ConEmu progress remove with double semicolon" { const input = "9;4;0;;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .remove); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2324,7 +2407,7 @@ test "OSC: OSC 9;4 ConEmu progress remove ignores progress" { const input = "9;4;0;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .remove); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2338,7 +2421,7 @@ test "OSC: OSC 9;4 ConEmu progress remove extra semicolon" { const input = "9;4;0;100;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .remove); } @@ -2351,7 +2434,7 @@ test "OSC: OSC 9;4 ConEmu progress error" { const input = "9;4;2"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .@"error"); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2365,7 +2448,7 @@ test "OSC: OSC 9;4 ConEmu progress error with progress" { const input = "9;4;2;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .@"error"); try testing.expect(cmd.conemu_progress_report.progress == 100); @@ -2379,7 +2462,7 @@ test "OSC: OSC 9;4 progress pause" { const input = "9;4;4"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .pause); try testing.expect(cmd.conemu_progress_report.progress == null); @@ -2393,7 +2476,7 @@ test "OSC: OSC 9;4 ConEmu progress pause with progress" { const input = "9;4;4;100"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_progress_report); try testing.expect(cmd.conemu_progress_report.state == .pause); try testing.expect(cmd.conemu_progress_report.progress == 100); @@ -2407,7 +2490,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 1" { const input = "9;4"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("4", cmd.show_desktop_notification.body); @@ -2421,7 +2504,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 2" { const input = "9;4;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body); @@ -2435,7 +2518,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 3" { const input = "9;4;5"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body); @@ -2449,7 +2532,7 @@ test "OSC: OSC 9;4 progress -> desktop notification 4" { const input = "9;4;5a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body); @@ -2463,7 +2546,7 @@ test "OSC: OSC 9;5 ConEmu wait input" { const input = "9;5"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_wait_input); } @@ -2475,7 +2558,7 @@ test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" { const input = "9;5;foo"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_wait_input); } @@ -2499,7 +2582,7 @@ test "OSC: hyperlink" { const input = "8;;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } @@ -2512,7 +2595,7 @@ test "OSC: hyperlink with id set" { const input = "8;id=foo;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo"); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2526,7 +2609,7 @@ test "OSC: hyperlink with empty id" { const input = "8;id=;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqual(null, cmd.hyperlink_start.id); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2540,7 +2623,7 @@ test "OSC: hyperlink with incomplete key" { const input = "8;id;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqual(null, cmd.hyperlink_start.id); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2554,7 +2637,7 @@ test "OSC: hyperlink with empty key" { const input = "8;=value;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqual(null, cmd.hyperlink_start.id); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2568,7 +2651,7 @@ test "OSC: hyperlink with empty key and id" { const input = "8;=value:id=foo;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo"); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); @@ -2594,7 +2677,7 @@ test "OSC: hyperlink end" { const input = "8;;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_end); } @@ -2608,7 +2691,7 @@ test "OSC: kitty color protocol" { const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len); { @@ -2690,7 +2773,7 @@ test "OSC: kitty color protocol double reset" { const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); p.reset(); @@ -2706,7 +2789,7 @@ test "OSC: kitty color protocol reset after invalid" { const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); p.reset(); @@ -2727,7 +2810,7 @@ test "OSC: kitty color protocol no key" { const input = "21;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); } @@ -2741,7 +2824,7 @@ test "OSC: 9;6: ConEmu guimacro 1" { const input = "9;6;a"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_guimacro); try testing.expectEqualStrings("a", cmd.conemu_guimacro); } @@ -2755,7 +2838,7 @@ test "OSC: 9;6: ConEmu guimacro 2" { const input = "9;6;ab"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .conemu_guimacro); try testing.expectEqualStrings("ab", cmd.conemu_guimacro); } @@ -2769,7 +2852,7 @@ test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { const input = "9;6"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .show_desktop_notification); try testing.expectEqualStrings("6", cmd.show_desktop_notification.body); }