feat(unicode,datastruct): port Unicode tables and data structures

ghostty-unicode: Ghostty computes codepoint properties with the uucode
Zig package (v0.2.0, Unicode 17.0.0). There is no Rust equivalent, so
a trimmed UCD set is vendored in ucd/ and build.rs re-implements
uucode's derivations: cell width rules incl. Ghostty's overrides for
emoji modifiers and Prepend, grapheme break classes with the
compile-time grapheme_break state table, the is_symbol set, and emoji
presentation. Lookups are three array indexes per codepoint. Validated
against the official GraphemeBreakTest.txt.

ghostty-datastruct: split_tree (used for GTK splits; f16 ratios so
rounding and debug diagrams match Zig), circ_buf, lru, cache_table,
blocking_queue, segmented_pool, message_data, comparison,
array_list_collection. intrusive_linked_list is not needed (lru is
index based). lru now reports the evicted (old) key, Zig reported the
new key, which appears to be a bug.

Not yet covered: case_folding_full (needed by input bindings).

Test Plan:
- cargo test -p ghostty-unicode (16) -p ghostty-datastruct (88)
- cargo clippy for both crates with -D warnings
This commit is contained in:
ddidderr committed 2026-10-11 21:54:57 +02:00
1 parent b96984b020
commit 5b8c92b98c
31 files changed
+18468 -3

No files matched your search

+184 -1
View File
@@ -1 +1,184 @@
//! ghostty-unicode: see docs/rewrite/ARCHITECTURE.md for the module map.
//! Unicode properties for Ghostty: codepoint width, grapheme breaking, and
//! the "is symbol" table used by the renderer for constraint width.
//!
//! Port of `src/unicode/*`. Ghostty derives these from the `uucode` package
//! configured by `src/build/uucode_config.zig` (Unicode 17.0.0); here the
//! same values are generated at build time (`build.rs`) from vendored UCD
//! files. See `docs/rewrite/notes/ghostty-unicode.md`.
#![cfg_attr(test, allow(clippy::unwrap_used))]
pub mod emoji_table;
pub mod grapheme;
pub mod lut;
pub mod props;
pub mod props_table;
pub mod symbols_table;
#[cfg(test)]
#[path = "../build_support/ucd.rs"]
mod props_uucode;
pub use grapheme::{BreakState, grapheme_break};
pub use props::{GraphemeBreakNoControl, Properties};
pub use props_table::table;
/// Unicode version of the vendored data.
pub const UNICODE_VERSION: (u8, u8, u8) = (17, 0, 0);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unicode_props_tables_match_uucode() {
let ucd =
props_uucode::Ucd::load(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("ucd"));
for cp in 0..=0x1F_FFFFu32 {
assert_eq!(table().get(cp), ucd.get(cp), "mismatch cp=U+{cp:x}");
}
}
#[test]
fn unicode_symbols_tables_match_uucode() {
let ucd =
props_uucode::Ucd::load(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("ucd"));
for cp in 0..=0x1F_FFFFu32 {
assert_eq!(
symbols_table::table().get(cp),
ucd.is_symbol(cp),
"mismatch cp=U+{cp:x}"
);
assert_eq!(emoji_table::table().get(cp), ucd.is_emoji_presentation(cp));
}
}
fn w(cp: u32) -> u8 {
table().get(cp).width
}
#[test]
fn widths_ascii() {
for cp in 0x20..0x7F {
assert_eq!(w(cp), 1, "{cp:x}");
}
// Control characters are zero width (handled before lookup by the
// terminal).
for cp in (0..0x20).chain(0x7F..0xA0) {
assert_eq!(w(cp), 0, "{cp:x}");
}
}
#[test]
fn widths_cjk_and_fullwidth() {
assert_eq!(w('中' as u32), 2);
assert_eq!(w('あ' as u32), 2);
assert_eq!(w('한' as u32), 2);
assert_eq!(w(0xFF21), 2); // fullwidth A
assert_eq!(w(0x3000), 2); // ideographic space
assert_eq!(w(0xFF61), 1); // halfwidth ideographic full stop
assert_eq!(w(0x20000), 2); // CJK ext B
assert_eq!(w(0x3FFFD), 2);
// Ambiguous width is narrow.
assert_eq!(w(0x00B1), 1);
assert_eq!(w(0x2460), 1);
}
#[test]
fn widths_emoji() {
assert_eq!(w(0x1F600), 2); // grinning face
assert_eq!(w(0x1F680), 2);
assert_eq!(w(0x2764), 1); // heart, text default
assert_eq!(w(0x231A), 2); // watch, emoji presentation
assert!(table().get(0x2764).emoji_vs_base);
assert!(!table().get(0x1F600).emoji_vs_base);
assert!(emoji_table::table().get(0x1F600));
assert!(!emoji_table::table().get(0x2764));
}
#[test]
fn widths_zero_and_combining() {
assert_eq!(w(0x0301), 0); // combining acute
assert_eq!(w(0x20DD), 0); // enclosing circle
assert_eq!(w(0x200B), 0); // ZWSP (default ignorable)
assert_eq!(w(0x200D), 0); // ZWJ
assert_eq!(w(0x200C), 0); // ZWNJ
assert_eq!(w(0xFE0E), 0); // VS15
assert_eq!(w(0xFE0F), 0); // VS16
assert_eq!(w(0xE0100), 0); // VS17
assert_eq!(w(0x2028), 0); // line separator
assert_eq!(w(0xD800), 0); // surrogate
assert_eq!(w(0x00AD), 1); // soft hyphen
assert_eq!(w(0x2E3A), 2); // two-em dash
assert_eq!(w(0x2E3B), 2); // three-em dash clamped to 2
for cp in [0x0301, 0x200D, 0xFE0F, 0x1F3FB, 0x1160, 0x11A8] {
assert!(table().get(cp).width_zero_in_grapheme, "{cp:x}");
}
assert!(!table().get('a' as u32).width_zero_in_grapheme);
assert!(!table().get(0x1F600).width_zero_in_grapheme);
}
#[test]
fn widths_emoji_modifier_prepend_ri_keycap() {
// Emoji modifiers display as colour patches on their own.
for cp in 0x1F3FB..=0x1F3FF {
assert_eq!(w(cp), 2);
assert!(table().get(cp).width_zero_in_grapheme);
assert_eq!(
table().get(cp).grapheme_break,
GraphemeBreakNoControl::EmojiModifier
);
}
// Prepend characters keep a width.
assert_eq!(w(0x0600), 1); // Arabic number sign (prepend, Cf)
assert_eq!(
table().get(0x0600).grapheme_break,
GraphemeBreakNoControl::Prepend
);
assert!(table().get(0x0600).width_zero_in_grapheme);
assert_eq!(w(0x0D4E), 1); // Malayalam letter dot reph
// Regional indicators are wide.
assert_eq!(w(0x1F1E6), 2);
assert_eq!(w(0x1F1FF), 2);
assert_eq!(
table().get(0x1F1E6).grapheme_break,
GraphemeBreakNoControl::RegionalIndicator
);
// Combining enclosing keycap is zero width in the table.
assert_eq!(w(0x20E3), 0);
// Hangul jamo.
assert_eq!(w(0x1100), 2); // choseong
assert_eq!(w(0x1160), 0); // jungseong, zero (V)
assert_eq!(w(0x11A8), 0); // jongseong (T)
}
#[test]
fn out_of_range_defaults() {
// Beyond the Unicode range (props_uucode.zig `get`).
let p = table().get(0x11_0000);
assert_eq!(p.width, 1);
assert!(p.width_zero_in_grapheme);
assert_eq!(p.grapheme_break, GraphemeBreakNoControl::Other);
assert!(!p.emoji_vs_base);
assert_eq!(table().get(0x1F_FFFF), p);
}
#[test]
fn symbols() {
let t = symbols_table::table();
assert!(t.get(0xE000)); // private use
assert!(t.get(0xF0001)); // supplementary private use
assert!(t.get(0x2190)); // arrows
assert!(t.get(0x2713)); // dingbats
assert!(t.get(0x1F600)); // emoticons
assert!(t.get(0x2600)); // misc symbols
assert!(t.get(0x2460)); // enclosed alphanumerics
assert!(t.get(0x1F100)); // enclosed alphanumeric supplement
assert!(t.get(0x1F300)); // misc symbols & pictographs
assert!(t.get(0x1F680)); // transport & map
assert!(!t.get('a' as u32));
assert!(!t.get('中' as u32));
assert!(!t.get(0x2500)); // box drawing
assert!(!t.get(0x11_0000));
}
}