Something went wrong. Try again.
A charm-like tui library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144//! Terminal cell width of Unicode code points.//!//! Uses static East-Asian-Width / combining-mark range tables (≈Unicode 15).//! This is a per-code-point approximation: it is correct for CJK, fullwidth//! forms, and standalone emoji, but is **not** grapheme-cluster aware — ZWJ//! emoji sequences (e.g. 👨👩👧) are measured per component. For fully//! grapheme-correct width, zg's `DisplayWidth` (codeberg.org/atman/zg) is the//! upgrade path, at the cost of an allocator + runtime init.
const Range = struct { lo: u21, hi: u21 };
/// Code points rendered two cells wide: East Asian Wide + Fullwidth, CJK,/// Hangul syllables, and emoji blocks. Sorted, non-overlapping.const wide = [_]Range{ .{ .lo = 0x1100, .hi = 0x115F }, // Hangul Jamo .{ .lo = 0x2329, .hi = 0x232A }, // angle brackets .{ .lo = 0x2E80, .hi = 0x303E }, // CJK radicals … CJK symbols .{ .lo = 0x3041, .hi = 0x33FF }, // Hiragana … CJK compat .{ .lo = 0x3400, .hi = 0x4DBF }, // CJK Extension A .{ .lo = 0x4E00, .hi = 0x9FFF }, // CJK Unified Ideographs .{ .lo = 0xA000, .hi = 0xA4CF }, // Yi .{ .lo = 0xA960, .hi = 0xA97F }, // Hangul Jamo Extended-A .{ .lo = 0xAC00, .hi = 0xD7A3 }, // Hangul Syllables .{ .lo = 0xF900, .hi = 0xFAFF }, // CJK Compatibility Ideographs .{ .lo = 0xFE10, .hi = 0xFE19 }, // Vertical Forms .{ .lo = 0xFE30, .hi = 0xFE6F }, // CJK Compatibility / Small Forms .{ .lo = 0xFF00, .hi = 0xFF60 }, // Fullwidth Forms .{ .lo = 0xFFE0, .hi = 0xFFE6 }, // Fullwidth Signs .{ .lo = 0x16FE0, .hi = 0x16FE4 }, // Tangut/Khitan marks .{ .lo = 0x17000, .hi = 0x18CFF }, // Tangut, Khitan .{ .lo = 0x1AFF0, .hi = 0x1B2FF }, // Kana Extended/Supplement .{ .lo = 0x1F004, .hi = 0x1F004 }, // Mahjong red dragon .{ .lo = 0x1F0CF, .hi = 0x1F0CF }, // playing card black joker .{ .lo = 0x1F18E, .hi = 0x1F18E }, // negative squared AB .{ .lo = 0x1F191, .hi = 0x1F19A }, // squared CL … VS .{ .lo = 0x1F200, .hi = 0x1F2FF }, // Enclosed Ideographic Supplement .{ .lo = 0x1F300, .hi = 0x1F64F }, // Misc Symbols & Pictographs, Emoticons .{ .lo = 0x1F680, .hi = 0x1F6FF }, // Transport & Map Symbols .{ .lo = 0x1F900, .hi = 0x1F9FF }, // Supplemental Symbols & Pictographs .{ .lo = 0x1FA70, .hi = 0x1FAFF }, // Symbols & Pictographs Extended-A .{ .lo = 0x20000, .hi = 0x3FFFD }, // CJK Extension B … G};
/// Code points rendered zero cells wide: combining marks, variation/// selectors, and zero-width formatting characters. Partial coverage — the/// common blocks plus the larger Arabic/Indic/Thai mark ranges. Sorted.const zero_width = [_]Range{ .{ .lo = 0x0300, .hi = 0x036F }, // Combining Diacritical Marks .{ .lo = 0x0483, .hi = 0x0489 }, .{ .lo = 0x0591, .hi = 0x05BD }, .{ .lo = 0x05BF, .hi = 0x05BF }, .{ .lo = 0x05C1, .hi = 0x05C2 }, .{ .lo = 0x05C4, .hi = 0x05C5 }, .{ .lo = 0x05C7, .hi = 0x05C7 }, .{ .lo = 0x0610, .hi = 0x061A }, .{ .lo = 0x064B, .hi = 0x065F }, .{ .lo = 0x0670, .hi = 0x0670 }, .{ .lo = 0x06D6, .hi = 0x06DC }, .{ .lo = 0x06DF, .hi = 0x06E4 }, .{ .lo = 0x06E7, .hi = 0x06E8 }, .{ .lo = 0x06EA, .hi = 0x06ED }, .{ .lo = 0x0711, .hi = 0x0711 }, .{ .lo = 0x0730, .hi = 0x074A }, .{ .lo = 0x07A6, .hi = 0x07B0 }, .{ .lo = 0x07EB, .hi = 0x07F3 }, .{ .lo = 0x0816, .hi = 0x0819 }, .{ .lo = 0x081B, .hi = 0x0823 }, .{ .lo = 0x0825, .hi = 0x0827 }, .{ .lo = 0x0829, .hi = 0x082D }, .{ .lo = 0x0859, .hi = 0x085B }, .{ .lo = 0x08E3, .hi = 0x0902 }, .{ .lo = 0x093A, .hi = 0x093A }, .{ .lo = 0x093C, .hi = 0x093C }, .{ .lo = 0x0941, .hi = 0x0948 }, .{ .lo = 0x094D, .hi = 0x094D }, .{ .lo = 0x0951, .hi = 0x0957 }, .{ .lo = 0x0962, .hi = 0x0963 }, .{ .lo = 0x0E31, .hi = 0x0E31 }, .{ .lo = 0x0E34, .hi = 0x0E3A }, .{ .lo = 0x0E47, .hi = 0x0E4E }, .{ .lo = 0x135D, .hi = 0x135F }, .{ .lo = 0x1AB0, .hi = 0x1AFF }, // Combining Diacritical Marks Extended .{ .lo = 0x1DC0, .hi = 0x1DFF }, // Combining Diacritical Marks Supplement .{ .lo = 0x200B, .hi = 0x200F }, // ZWSP, ZWNJ, ZWJ, LRM, RLM .{ .lo = 0x202A, .hi = 0x202E }, // bidi embeddings .{ .lo = 0x2060, .hi = 0x2064 }, .{ .lo = 0x2066, .hi = 0x206F }, .{ .lo = 0x20D0, .hi = 0x20FF }, // Combining Marks for Symbols .{ .lo = 0xFE00, .hi = 0xFE0F }, // Variation Selectors .{ .lo = 0xFE20, .hi = 0xFE2F }, // Combining Half Marks .{ .lo = 0xE0100, .hi = 0xE01EF }, // Variation Selectors Supplement};
fn inRanges(ranges: []const Range, cp: u21) bool { var lo: usize = 0; var hi: usize = ranges.len; while (lo < hi) { const mid = lo + (hi - lo) / 2; const r = ranges[mid]; if (cp < r.lo) { hi = mid; } else if (cp > r.hi) { lo = mid + 1; } else { return true; } } return false;}
/// Returns the terminal cell width of `cp`: 0 for control characters,/// combining marks, and zero-width formatting; 2 for wide/fullwidth code/// points; 1 otherwise.pub fn codepointWidth(cp: u21) u8 { if (cp == 0) return 0; // C0 controls, DEL, and C1 controls. if (cp < 0x20 or (cp >= 0x7f and cp < 0xa0)) return 0; if (cp < 0x300) return 1; // fast path: Latin/punctuation if (inRanges(&zero_width, cp)) return 0; if (inRanges(&wide, cp)) return 2; return 1;}
test "codepointWidth ascii" { const std = @import("std"); try std.testing.expectEqual(@as(u8, 1), codepointWidth('A')); try std.testing.expectEqual(@as(u8, 0), codepointWidth(0x07)); // BEL try std.testing.expectEqual(@as(u8, 0), codepointWidth(0x7f)); // DEL}
test "codepointWidth wide" { const std = @import("std"); try std.testing.expectEqual(@as(u8, 2), codepointWidth('世')); // U+4E16 try std.testing.expectEqual(@as(u8, 2), codepointWidth(0x1F389)); // 🎉 try std.testing.expectEqual(@as(u8, 2), codepointWidth(0xFF21)); // fullwidth A}
test "codepointWidth zero" { const std = @import("std"); try std.testing.expectEqual(@as(u8, 0), codepointWidth(0x0301)); // combining acute try std.testing.expectEqual(@as(u8, 0), codepointWidth(0x200D)); // ZWJ try std.testing.expectEqual(@as(u8, 0), codepointWidth(0xFE0F)); // VS-16}