diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:28:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 23:29:28 +0300 |
| commit | 4a3302d19a2baa3a33e910e0088c24e780905b90 (patch) | |
| tree | dcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/unicode | |
| parent | 550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff) | |
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/unicode')
| -rw-r--r-- | vendor/libghostty-vt/src/unicode/grapheme.zig | 185 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/unicode/main.zig | 53 |
2 files changed, 238 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/unicode/grapheme.zig b/vendor/libghostty-vt/src/unicode/grapheme.zig index d233dec7..73782dd6 100644 --- a/vendor/libghostty-vt/src/unicode/grapheme.zig +++ b/vendor/libghostty-vt/src/unicode/grapheme.zig @@ -2,6 +2,31 @@ const std = @import("std"); const table = @import("props_table.zig").table; const uucode = @import("uucode"); +/// Width change requested by a codepoint that continues a grapheme cluster. +pub const GraphemeWidthEffect = enum { + /// Do not append the codepoint to the cluster and leave break state as it + /// was before seeing it. + ignore, + + /// Append the codepoint but leave the current cluster width unchanged. + no_change, + + /// Make the cluster occupy two terminal cells. + wide, + + /// Make the cluster occupy one terminal cell. + narrow, +}; + +/// Result of measuring the first grapheme cluster in a codepoint slice. +pub const GraphemeWidth = struct { + /// Number of codepoints consumed from the input slice. + len: usize, + + /// Display width in terminal cells. + width: u2, +}; + /// Determines if there is a grapheme break between two codepoints. This /// must be called sequentially maintaining the state between calls. /// @@ -21,6 +46,99 @@ pub fn graphemeBreak(cp1: u21, cp2: u21, state: *uucode.grapheme.BreakState) boo return value.result; } +/// Returns the width effect of appending cp after prev within a grapheme. +/// +/// This is the shared width-decision kernel for the streaming terminal +/// printer and for graphemeWidth. It assumes graphemeBreak has already said +/// there is no break between prev and cp; it does not perform segmentation. +/// +/// The .ignore result is important for invalid emoji variation selectors. The +/// terminal does not store those selectors in the cell, so callers must also +/// restore their grapheme break state and leave prev unchanged when they see +/// .ignore. +pub inline fn graphemeWidthEffect(prev: u21, cp: u21) GraphemeWidthEffect { + // Emoji variation selectors modify the width of a valid base: + // VS16 makes the grapheme wide and VS15 makes it narrow. Check that + // prev forms a valid variation sequence in emoji-variation-sequences.txt; + // if it does not, ignore the selector entirely. + if (cp == 0xFE0F or cp == 0xFE0E) { + const prev_props = table.get(prev); + if (!prev_props.emoji_vs_base) return .ignore; + + return switch (cp) { + 0xFE0F => .wide, + 0xFE0E => .narrow, + else => unreachable, + }; + } + + // If a code point contributes to the width of a grapheme, the whole + // grapheme is at least width 2 because the first code point must be at + // least width 1 to start. Prepend code points could effectively mean + // the first code point should be width 0, but we don't handle that yet. + if (!table.get(cp).width_zero_in_grapheme) return .wide; + + return .no_change; +} + +/// Measures the first grapheme cluster in cps using the same segmentation and +/// width rules as Terminal.print with mode 2027. +/// +/// This is not a streaming API: cps must contain a complete first grapheme +/// cluster or the logical end of the string. If bytes/codepoints arrive in +/// chunks, keep buffering when this consumes all available codepoints and more +/// input may still arrive. +/// +/// For codepoint types wider than u21, values greater than U+10FFFF are +/// accepted so FFI-facing callers can use u32 input without trapping. An +/// invalid value consumes one codepoint at width 1 when it starts the slice, +/// and terminates the current cluster when it appears later. For u21 callers +/// these checks are comptime-dead. +pub fn graphemeWidth(comptime T: type, cps: []const T) GraphemeWidth { + const check_invalid = comptime @bitSizeOf(T) > @bitSizeOf(u21); + + if (cps.len == 0) return .{ .len = 0, .width = 0 }; + + // The C API accepts u32 codepoints, so it can receive values outside + // Unicode's range. Guard before narrowing to u21; native u21 callers + // skip this path at comptime. + if (check_invalid and invalidCodepoint(cps[0])) return .{ .len = 1, .width = 1 }; + + var len: usize = 1; + var width = table.get(@as(u21, @intCast(cps[0]))).width; + var prev: u21 = @intCast(cps[0]); + var state: uucode.grapheme.BreakState = .default; + + while (len < cps.len) : (len += 1) { + // Treat invalid u32 input as a boundary so a valid prefix cluster can + // still be returned without attempting to narrow the invalid value. + if (check_invalid and invalidCodepoint(cps[len])) break; + + const cp: u21 = @intCast(cps[len]); + const state_before = state; + if (graphemeBreak(prev, cp, &state)) break; + + switch (graphemeWidthEffect(prev, cp)) { + .ignore => state = state_before, + .no_change => prev = cp, + .wide => { + width = 2; + prev = cp; + }, + .narrow => { + width = 1; + prev = cp; + }, + } + } + + return .{ .len = len, .width = width }; +} + +inline fn invalidCodepoint(cp: anytype) bool { + return cp > 0x10FFFF; +} + /// This is all the structures and data for the precomputed lookup table /// for all possible permutations of state and grapheme break properties. /// Precomputation requires 2^13 keys of 4 bit values so the whole table is @@ -178,3 +296,70 @@ test "long emoji zwj sequences" { try std.testing.expect(cp1 == 0x1F466); // 👦 try std.testing.expect(graphemeBreak(cp1, cp2, &state)); // break } + +test "grapheme width: variation selectors" { + const testing = std.testing; + + try testing.expectEqual(GraphemeWidthEffect.wide, graphemeWidthEffect(0x2764, 0xFE0F)); + try testing.expectEqual(GraphemeWidthEffect.narrow, graphemeWidthEffect(0x23, 0xFE0E)); + try testing.expectEqual(GraphemeWidthEffect.ignore, graphemeWidthEffect('x', 0xFE0F)); + + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 2 }, graphemeWidth(u21, &.{ 0x2764, 0xFE0F })); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 2 }, graphemeWidth(u21, &.{ 0x23, 0xFE0F })); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 1 }, graphemeWidth(u21, &.{ 'x', 0xFE0F })); + try testing.expectEqual(GraphemeWidth{ .len = 3, .width = 1 }, graphemeWidth(u21, &.{ 'x', 0xFE0F, 0xFE0F })); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 1 }, graphemeWidth(u21, &.{ 0x23, 0xFE0E })); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 1 }, graphemeWidth(u21, &.{ 0x231A, 0xFE0E })); + try testing.expectEqual(GraphemeWidth{ .len = 3, .width = 1 }, graphemeWidth(u21, &.{ 0x231A, 0xFE0E, 0xFE0F })); + try testing.expectEqual(GraphemeWidth{ .len = 4, .width = 2 }, graphemeWidth(u21, &.{ 0x1F3F4, 0x200D, 0x2620, 0xFE0F })); +} + +test "grapheme width: emoji sequences" { + const testing = std.testing; + + try testing.expectEqual(GraphemeWidth{ .len = 5, .width = 2 }, graphemeWidth(u21, &.{ 0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467 })); + try testing.expectEqual(GraphemeWidth{ .len = 3, .width = 2 }, graphemeWidth(u21, &.{ 0x23, 0xFE0F, 0x20E3 })); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 1 }, graphemeWidth(u21, &.{ '1', 0x20E3 })); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 2 }, graphemeWidth(u21, &.{ 0x1F44B, 0x1F3FF })); +} + +test "grapheme width: spacing marks can widen narrow clusters" { + const testing = std.testing; + + var mark: ?u21 = null; + for (0..0x110000) |cp_usize| { + const cp: u21 = @intCast(cp_usize); + const props = table.get(cp); + if (props.width != 1 or props.width_zero_in_grapheme) continue; + + var state: uucode.grapheme.BreakState = .default; + if (!graphemeBreak('a', cp, &state)) { + mark = cp; + break; + } + } + + try testing.expect(mark != null); + const cp = mark.?; + try testing.expectEqual(@as(u2, 1), table.get(cp).width); + try testing.expect(!table.get(cp).width_zero_in_grapheme); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 2 }, graphemeWidth(u21, &.{ 'a', cp })); +} + +test "grapheme width: segmentation" { + const testing = std.testing; + + try testing.expectEqual(GraphemeWidth{ .len = 1, .width = 1 }, graphemeWidth(u21, &.{'a'})); + try testing.expectEqual(GraphemeWidth{ .len = 1, .width = 1 }, graphemeWidth(u21, &.{ 'a', 'b' })); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 2 }, graphemeWidth(u21, &.{ 0x1F1E6, 0x1F1E7, 0x1F1E8 })); + try testing.expectEqual(GraphemeWidth{ .len = 1, .width = 2 }, graphemeWidth(u21, &.{0x1F1E8})); + try testing.expectEqual(GraphemeWidth{ .len = 0, .width = 0 }, graphemeWidth(u21, &.{})); + try testing.expectEqual(GraphemeWidth{ .len = 2, .width = 0 }, graphemeWidth(u21, &.{ 0x0301, 0x0302 })); +} + +test "grapheme width: u32 invalid codepoints stand alone" { + const testing = std.testing; + + try testing.expectEqual(GraphemeWidth{ .len = 1, .width = 1 }, graphemeWidth(u32, &.{ 0x110000, 0x0301 })); + try testing.expectEqual(GraphemeWidth{ .len = 1, .width = 1 }, graphemeWidth(u32, &.{ 'a', 0x110000 })); +} diff --git a/vendor/libghostty-vt/src/unicode/main.zig b/vendor/libghostty-vt/src/unicode/main.zig index 11ecbd90..fb18b7d1 100644 --- a/vendor/libghostty-vt/src/unicode/main.zig +++ b/vendor/libghostty-vt/src/unicode/main.zig @@ -3,7 +3,60 @@ pub const lut = @import("lut.zig"); const grapheme = @import("grapheme.zig"); pub const table = @import("props_table.zig").table; pub const Properties = @import("props.zig").Properties; +pub const GraphemeWidthEffect = grapheme.GraphemeWidthEffect; +pub const GraphemeWidth = grapheme.GraphemeWidth; pub const graphemeBreak = grapheme.graphemeBreak; +pub const graphemeWidth = grapheme.graphemeWidth; +pub const graphemeWidthEffect = grapheme.graphemeWidthEffect; + +/// Returns the terminal display width of a codepoint in terminal +/// grid cells: 0, 1, or 2. +/// +/// This is the same width table the terminal uses when laying out +/// printed text: 0 for zero-width codepoints (controls, combining +/// marks, default-ignorables, surrogates), 2 for wide codepoints +/// (East Asian Wide/Fullwidth, regional indicators, clamped at 2), +/// and 1 otherwise. +/// +/// This operates on a single codepoint and cannot account for +/// grapheme-cluster-level width rules (VS16, combining sequences); +/// callers needing cluster-accurate widths should use graphemeWidth(). +/// Summing per-codepoint widths is only correct when mode 2027 is +/// disabled. +pub fn codepointWidth(cp: u21) u2 { + return table.get(cp).width; +} + +test "codepointWidth" { + const testing = @import("std").testing; + + // Narrow (width 1) + try testing.expectEqual(1, codepointWidth('a')); + try testing.expectEqual(1, codepointWidth(' ')); + try testing.expectEqual(1, codepointWidth(0x10FFFF)); // max codepoint + + // C0/C1 control characters (width 0) + try testing.expectEqual(0, codepointWidth(0x00)); // NUL + try testing.expectEqual(0, codepointWidth(0x07)); // BEL + try testing.expectEqual(0, codepointWidth(0x1B)); // ESC + try testing.expectEqual(0, codepointWidth(0x7F)); // DEL + try testing.expectEqual(0, codepointWidth(0x80)); // C1 PAD + + // Zero-width codepoints + try testing.expectEqual(0, codepointWidth(0x0301)); // combining acute + try testing.expectEqual(0, codepointWidth(0x200B)); // zero width space + try testing.expectEqual(0, codepointWidth(0x200D)); // ZWJ + try testing.expectEqual(0, codepointWidth(0xFE0F)); // VS16 + try testing.expectEqual(0, codepointWidth(0xD800)); // surrogate + + // Wide (width 2) + try testing.expectEqual(2, codepointWidth(0x4E00)); // CJK ideograph + try testing.expectEqual(2, codepointWidth(0xFF21)); // fullwidth A + try testing.expectEqual(2, codepointWidth(0xAC00)); // Hangul syllable + try testing.expectEqual(2, codepointWidth(0x1F600)); // emoji + try testing.expectEqual(2, codepointWidth(0x1F1E6)); // regional indicator + try testing.expectEqual(2, codepointWidth(0x2E3B)); // three-em dash (clamped) +} test { @import("std").testing.refAllDecls(@This()); |
