From 631c58a302168356e15861f5d4ef6d95cac65299 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Nov 2025 14:54:05 -0800 Subject: [PATCH] unicode: update uucode, force emoji modifiers width 2 as standalone This updates uucode. As part of this, the wcwidth implementation was updated (in uucode) to make emoji modifiers width ZERO. But if they're standalone, we want them as width 2. So this also contains a change to force them as width 2 for our width calculation. This only matters for standalone emoji modifiers, because when they form a valid grapheme we don't use this width calculation. --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- src/build/uucode_config.zig | 17 ++++++++++++++++- src/terminal/Terminal.zig | 29 +++++++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 13 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 7c686cecc..f24871fd6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -39,8 +39,8 @@ }, .uucode = .{ // TODO: currently the use-llvm branch because its broken on self-hosted - .url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", - .hash = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", + .url = "https://github.com/jacobsandlund/uucode/archive/ca307fdeb7eca5c2812b288cdd5650e66b3115eb.tar.gz", + .hash = "uucode-0.1.0-ZZjBPkxTQwCphlTjg-UtY_TzztJy_TZqDL-S2nymmBXY", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland diff --git a/build.zig.zon.json b/build.zig.zon.json index ad0edaa50..526109532 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -114,10 +114,10 @@ "url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732", "hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8=" }, - "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT": { + "uucode-0.1.0-ZZjBPkxTQwCphlTjg-UtY_TzztJy_TZqDL-S2nymmBXY": { "name": "uucode", - "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", - "hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=" + "url": "https://github.com/jacobsandlund/uucode/archive/ca307fdeb7eca5c2812b288cdd5650e66b3115eb.tar.gz", + "hash": "sha256-AY1JqW7qrWy1+WrlGzL8rHW7mZA6CmYhJVr1sSg19oI=" }, "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": { "name": "vaxis", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index ccd6285d9..ab51e34dc 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -267,11 +267,11 @@ in }; } { - name = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT"; + name = "uucode-0.1.0-ZZjBPkxTQwCphlTjg-UtY_TzztJy_TZqDL-S2nymmBXY"; path = fetchZigArtifact { name = "uucode"; - url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz"; - hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="; + url = "https://github.com/jacobsandlund/uucode/archive/ca307fdeb7eca5c2812b288cdd5650e66b3115eb.tar.gz"; + hash = "sha256-AY1JqW7qrWy1+WrlGzL8rHW7mZA6CmYhJVr1sSg19oI="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 8abc4fb7d..f206259cf 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -20,7 +20,6 @@ https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz -https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz @@ -29,6 +28,7 @@ https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz +https://github.com/jacobsandlund/uucode/archive/ca307fdeb7eca5c2812b288cdd5650e66b3115eb.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251027-150540-8f50c1d/ghostty-themes.tgz https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 2a1e38986..8a39a0ccc 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -139,9 +139,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", - "dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", - "sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07" + "url": "https://github.com/jacobsandlund/uucode/archive/ca307fdeb7eca5c2812b288cdd5650e66b3115eb.tar.gz", + "dest": "vendor/p/uucode-0.1.0-ZZjBPkxTQwCphlTjg-UtY_TzztJy_TZqDL-S2nymmBXY", + "sha256": "018d49a96eeaad6cb5f96ae51b32fcac75bb99903a0a6621255af5b12835f682" }, { "type": "archive", diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 9a3b4bec7..d9e4cb4a3 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const assert = std.debug.assert; const config = @import("config.zig"); const config_x = @import("config.x.zig"); const d = config.default; @@ -17,11 +18,25 @@ fn computeWidth( _ = cp; _ = backing; _ = tracking; + + // Emoji modifiers are technically width 0 because they're joining + // points. But we handle joining via grapheme break and don't use width + // there. If a emoji modifier is standalone, we want it to take up + // two columns. + if (data.is_emoji_modifier) { + assert(data.wcwidth == 0); + data.wcwidth = 2; + return; + } + data.width = @intCast(@min(2, @max(0, data.wcwidth))); } const width = config.Extension{ - .inputs = &.{"wcwidth"}, + .inputs = &.{ + "is_emoji_modifier", + "wcwidth", + }, .compute = &computeWidth, .fields = &.{ .{ .name = "width", .type = u2 }, diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 64cda5ee3..fb3f458f7 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -3330,6 +3330,35 @@ test "Terminal: print multicodepoint grapheme, mode 2027" { } } +test "Terminal: Fitzpatrick skin tone next valid base" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + // This is: "👋🏿" (waving hand with dark skin tone) + try t.print(0x1F44B); // 👋 Waving hand (valid base) + try t.print(0x1F3FF); // 🏿 Dark skin tone modifier + + // The skin tone should combine with the base emoji into a single grapheme cluster, + // taking 2 cells (wide character). + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); + + // Row should be dirty + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + + // The base emoji should be in cell 0 with the skin tone as a grapheme + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x1F44B), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + } +} + test "Terminal: Fitzpatrick skin tone next to non-base" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);