mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-01 15:38:35 +00:00
Merge branch 'main' into jacob/uucode
This commit is contained in:
2
.github/workflows/nix.yml
vendored
2
.github/workflows/nix.yml
vendored
@@ -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
|
||||
|
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@@ -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
|
||||
|
2
.github/workflows/release-tip.yml
vendored
2
.github/workflows/release-tip.yml
vendored
@@ -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
|
||||
|
2
.github/workflows/snap.yml
vendored
2
.github/workflows/snap.yml
vendored
@@ -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
|
||||
|
48
.github/workflows/test.yml
vendored
48
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
2
.github/workflows/update-colorschemes.yml
vendored
2
.github/workflows/update-colorschemes.yml
vendored
@@ -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
|
||||
|
23
build.zig
23
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);
|
||||
}
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
|
6
build.zig.zon.json
generated
6
build.zig.zon.json
generated
@@ -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",
|
||||
|
6
build.zig.zon.nix
generated
6
build.zig.zon.nix
generated
@@ -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=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
2
build.zig.zon.txt
generated
2
build.zig.zon.txt
generated
@@ -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
|
||||
|
@@ -1,4 +1,6 @@
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
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;
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -353,6 +353,7 @@ typedef struct {
|
||||
typedef struct {
|
||||
const char* ptr;
|
||||
uintptr_t len;
|
||||
bool sentinel;
|
||||
} ghostty_string_s;
|
||||
|
||||
typedef struct {
|
||||
|
@@ -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
|
||||
|
@@ -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<TerminalEntity?> {
|
||||
@@ -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(
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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(.{});
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
};
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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"),
|
||||
|
@@ -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;
|
||||
}
|
||||
|
33
src/build/docker/lib-c-docs/Dockerfile
Normal file
33
src/build/docker/lib-c-docs/Dockerfile
Normal file
@@ -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"]
|
16
src/build/docker/lib-c-docs/entrypoint.sh
Executable file
16
src/build/docker/lib-c-docs/entrypoint.sh
Executable file
@@ -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;"
|
@@ -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"),
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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 =
|
||||
|
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@@ -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.
|
||||
|
@@ -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 "<Error getting font name>";
|
||||
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);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
""")
|
||||
|
@@ -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);
|
||||
|
97
src/lib/enum.zig
Normal file
97
src/lib/enum.zig
Normal file
@@ -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));
|
||||
}
|
||||
}
|
10
src/lib/main.zig
Normal file
10
src/lib/main.zig
Normal file
@@ -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());
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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),
|
||||
},
|
||||
|
@@ -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 = {} },
|
||||
|
@@ -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);
|
||||
|
17
src/terminal/c/main.zig
Normal file
17
src/terminal/c/main.zig
Normal file
@@ -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");
|
||||
}
|
132
src/terminal/c/osc.zig
Normal file
132
src/terminal/c/osc.zig
Normal file
@@ -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));
|
||||
}
|
5
src/terminal/c/result.zig
Normal file
5
src/terminal/c/result.zig
Normal file
@@ -0,0 +1,5 @@
|
||||
/// C: GhosttyResult
|
||||
pub const Result = enum(c_int) {
|
||||
success = 0,
|
||||
out_of_memory = -1,
|
||||
};
|
@@ -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);
|
||||
}
|
@@ -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());
|
||||
|
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user