Files
ghostty-rs/crates/ghostty-unicode/build_support/ucd.rs
T
ddidderr 8bcb85a29a style: apply nightly rustfmt import grouping across the workspace
rustfmt.toml sets group_imports, imports_granularity and imports_layout,
which are nightly-only; agents formatted with stable rustfmt, which
ignored them. This is a formatting-only pass with `cargo +nightly fmt`.

Test Plan:
- cargo clippy --workspace --all-targets -- -D warnings
- cargo test --workspace
2026-10-12 05:03:37 +02:00

427 lines
13 KiB
Rust

//! Direct computation of Ghostty's per-codepoint Unicode properties from the
//! vendored UCD files. This reproduces what `uucode` computes with Ghostty's
//! `src/build/uucode_config.zig` (and uucode's `x/config_x/wcwidth.zig` and
//! `x/config_x/grapheme_break.zig`).
//!
//! It is used by `build.rs` to generate the lookup tables, and by the
//! `props_uucode` test module to verify that the generated tables match.
//!
//! This file is the Rust counterpart of `props_uucode.zig` /
//! `symbols_uucode.zig` (`get`).
#![allow(dead_code)]
#![allow(
clippy::unwrap_used,
clippy::single_match_else,
clippy::checked_conversions,
clippy::trivially_copy_pass_by_ref,
clippy::expect_used
)]
use std::{fs, path::Path};
use crate::props::{GraphemeBreakNoControl as Gb, Properties};
/// Highest codepoint known to the UCD (`uucode.config.max_code_point`).
pub const MAX_CODE_POINT: u32 = 0x10FFFF;
const N: usize = MAX_CODE_POINT as usize + 1;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Gc {
Cn,
Cc,
Cs,
Co,
Zl,
Zp,
Mn,
Me,
Other,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Eaw {
Neutral,
Wide,
Fullwidth,
Other,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum OrigGb {
Other,
Prepend,
Cr,
Lf,
Control,
Extend,
RegionalIndicator,
SpacingMark,
L,
V,
T,
Lv,
Lvt,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Incb {
None,
Linker,
Consonant,
Extend,
}
#[derive(Clone, Copy, Default)]
struct EmojiData {
presentation: bool,
modifier: bool,
modifier_base: bool,
extended_pictographic: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Block {
None,
Symbol,
Other,
}
pub struct Ucd {
gc: Vec<Gc>,
eaw: Vec<Eaw>,
orig_gb: Vec<OrigGb>,
incb: Vec<Incb>,
default_ignorable: Vec<bool>,
emoji: Vec<EmojiData>,
emoji_vs_base: Vec<bool>,
block: Vec<Block>,
}
fn trim(line: &str) -> &str {
let line = match line.find('#') {
Some(i) => &line[..i],
None => line,
};
line.trim_matches([' ', '\t', '\r'])
}
fn parse_cp(s: &str) -> u32 {
u32::from_str_radix(s.trim(), 16).unwrap_or_else(|_| panic!("bad codepoint {s:?}"))
}
fn parse_range(s: &str) -> (u32, u32) {
match s.split_once("..") {
Some((a, b)) => (parse_cp(a), parse_cp(b)),
None => {
let c = parse_cp(s);
(c, c)
}
}
}
/// Iterate `range ; value ; rest...` data lines (comments stripped).
fn data_lines(content: &str) -> impl Iterator<Item = (u32, u32, Vec<&str>)> {
content.lines().filter_map(|line| {
let t = trim(line);
if t.is_empty() {
return None;
}
let mut parts = t.split(';').map(str::trim);
let (a, b) = parse_range(parts.next()?);
Some((a, b, parts.collect()))
})
}
fn read(dir: &Path, rel: &str) -> String {
let p = dir.join(rel);
fs::read_to_string(&p).unwrap_or_else(|e| panic!("reading {}: {e}", p.display()))
}
fn fill<T: Copy>(v: &mut [T], a: u32, b: u32, val: T) {
for cp in a..=b {
v[cp as usize] = val;
}
}
impl Ucd {
/// Load from a directory containing the vendored UCD files.
#[must_use]
pub fn load(dir: &Path) -> Self {
// General category.
let mut gc = vec![Gc::Cn; N];
for (a, b, parts) in data_lines(&read(dir, "extracted/DerivedGeneralCategory.txt")) {
let v = match parts[0] {
"Cc" => Gc::Cc,
"Cs" => Gc::Cs,
"Co" => Gc::Co,
"Zl" => Gc::Zl,
"Zp" => Gc::Zp,
"Mn" => Gc::Mn,
"Me" => Gc::Me,
"Cn" => Gc::Cn,
_ => Gc::Other,
};
fill(&mut gc, a, b, v);
}
// East Asian width, including the `@missing` directives (only
// Neutral and Wide occur).
let mut eaw = vec![Eaw::Neutral; N];
let content = read(dir, "extracted/DerivedEastAsianWidth.txt");
for line in content.lines() {
let t = line.trim_matches([' ', '\t', '\r']);
if let Some(rest) = t.strip_prefix("# @missing:") {
let mut parts = rest.split(';').map(str::trim);
let (a, b) = parse_range(parts.next().unwrap());
match parts.next().unwrap() {
"Neutral" => {}
"Wide" => fill(&mut eaw, a, b, Eaw::Wide),
other => panic!("Unknown @missing EastAsianWidth value: {other}"),
}
continue;
}
let t = trim(t);
if t.is_empty() {
continue;
}
let mut parts = t.split(';').map(str::trim);
let (a, b) = parse_range(parts.next().unwrap());
let v = match parts.next().unwrap() {
"W" => Eaw::Wide,
"F" => Eaw::Fullwidth,
"N" => Eaw::Neutral,
_ => Eaw::Other,
};
fill(&mut eaw, a, b, v);
}
// Original grapheme break property (unknown values are `other`).
let mut orig_gb = vec![OrigGb::Other; N];
for (a, b, parts) in data_lines(&read(dir, "auxiliary/GraphemeBreakProperty.txt")) {
let v = match parts[0] {
"Prepend" => OrigGb::Prepend,
"CR" => OrigGb::Cr,
"LF" => OrigGb::Lf,
"Control" => OrigGb::Control,
"Extend" => OrigGb::Extend,
"Regional_Indicator" => OrigGb::RegionalIndicator,
"SpacingMark" => OrigGb::SpacingMark,
"L" => OrigGb::L,
"V" => OrigGb::V,
"T" => OrigGb::T,
"LV" => OrigGb::Lv,
"LVT" => OrigGb::Lvt,
_ => OrigGb::Other,
};
fill(&mut orig_gb, a, b, v);
}
// Derived core properties: Default_Ignorable_Code_Point and InCB.
let mut incb = vec![Incb::None; N];
let mut default_ignorable = vec![false; N];
for (a, b, parts) in data_lines(&read(dir, "DerivedCoreProperties.txt")) {
match parts[0] {
"Default_Ignorable_Code_Point" => fill(&mut default_ignorable, a, b, true),
"InCB" => {
let v = match parts[1] {
"Linker" => Incb::Linker,
"Consonant" => Incb::Consonant,
"Extend" => Incb::Extend,
other => panic!("Unknown InCB value: {other}"),
};
fill(&mut incb, a, b, v);
}
_ => {}
}
}
// Emoji data.
let mut emoji = vec![EmojiData::default(); N];
for (a, b, parts) in data_lines(&read(dir, "emoji/emoji-data.txt")) {
for cp in a..=b {
let e = &mut emoji[cp as usize];
match parts[0] {
"Emoji_Presentation" => e.presentation = true,
"Emoji_Modifier" => e.modifier = true,
"Emoji_Modifier_Base" => e.modifier_base = true,
"Extended_Pictographic" => e.extended_pictographic = true,
_ => {}
}
}
}
// Emoji variation sequences (base codepoints of VS15/VS16 sequences).
let mut emoji_vs_base = vec![false; N];
let content = read(dir, "emoji/emoji-variation-sequences.txt");
for line in content.lines() {
let t = trim(line);
if t.is_empty() {
continue;
}
// "0023 FE0E ; text style; # ..."
let first = t.split(';').next().unwrap();
let mut it = first.split_whitespace();
let cp = parse_cp(it.next().unwrap());
let vs = parse_cp(it.next().unwrap());
assert!(vs == 0xFE0E || vs == 0xFE0F, "Unknown VS: {vs:x}");
emoji_vs_base[cp as usize] = true;
}
// Blocks (only the ones that matter for `is_symbol`).
let mut block = vec![Block::None; N];
for (a, b, parts) in data_lines(&read(dir, "Blocks.txt")) {
let v = match parts[0] {
"Arrows"
| "Dingbats"
| "Emoticons"
| "Miscellaneous Symbols"
| "Enclosed Alphanumerics"
| "Enclosed Alphanumeric Supplement"
| "Miscellaneous Symbols and Pictographs"
| "Transport and Map Symbols" => Block::Symbol,
_ => Block::Other,
};
fill(&mut block, a, b, v);
}
Self {
gc,
eaw,
orig_gb,
incb,
default_ignorable,
emoji,
emoji_vs_base,
block,
}
}
/// `grapheme_break_no_control` for a codepoint (uucode tables.zig +
/// `x/config_x/grapheme_break.zig`).
fn grapheme_break_no_control(&self, cp: u32) -> Gb {
let i = cp as usize;
let emoji = self.emoji[i];
let orig = self.orig_gb[i];
if emoji.modifier {
Gb::EmojiModifier
} else if emoji.modifier_base {
Gb::EmojiModifierBase
} else if emoji.extended_pictographic {
Gb::ExtendedPictographic
} else {
match self.incb[i] {
Incb::None => match orig {
OrigGb::Extend => {
// `zwnj` is the only grapheme break `extend` that is
// `none` for Indic conjunct break.
assert_eq!(cp, 0x200C, "unexpected extend without InCB: {cp:x}");
Gb::Zwnj
}
OrigGb::Control | OrigGb::Cr | OrigGb::Lf | OrigGb::Other => Gb::Other,
OrigGb::Prepend => Gb::Prepend,
OrigGb::RegionalIndicator => Gb::RegionalIndicator,
OrigGb::SpacingMark => Gb::SpacingMark,
OrigGb::L => Gb::L,
OrigGb::V => Gb::V,
OrigGb::T => Gb::T,
OrigGb::Lv => Gb::Lv,
OrigGb::Lvt => Gb::Lvt,
},
Incb::Extend => {
if cp == 0x200D {
Gb::Zwj
} else {
Gb::IndicConjunctBreakExtend
}
}
Incb::Linker => Gb::IndicConjunctBreakLinker,
Incb::Consonant => Gb::IndicConjunctBreakConsonant,
}
}
}
/// Port of `x/config_x/wcwidth.zig` `compute`: returns
/// `(wcwidth_standalone, wcwidth_zero_in_grapheme)`.
fn wcwidth(&self, cp: u32) -> (u8, bool) {
let i = cp as usize;
let gc = self.gc[i];
let gb = self.grapheme_break_no_control(cp);
let eaw = self.eaw[i];
let width: u8 = if matches!(gc, Gc::Cc | Gc::Cs | Gc::Zl | Gc::Zp) {
0
} else if cp == 0x00AD {
// Soft hyphen
1
} else if self.default_ignorable[i] {
0
} else if cp == 0x2E3A {
// Two-em dash
2
} else if cp == 0x2E3B {
// Three-em dash
3
} else if eaw == Eaw::Wide || eaw == Eaw::Fullwidth || gb == Gb::RegionalIndicator {
2
} else {
1
};
let standalone = if cp == 0x20E3 { 2 } else { width };
let zero = width == 0
|| self.emoji[i].modifier
|| gc == Gc::Mn
|| gc == Gc::Me
|| gb == Gb::V
|| gb == Gb::T
|| gb == Gb::Prepend;
(standalone, zero)
}
/// Port of `props_uucode.zig` `get`.
#[must_use]
pub fn get(&self, cp: u32) -> Properties {
if cp > MAX_CODE_POINT {
return Properties {
width: 1,
width_zero_in_grapheme: true,
grapheme_break: Gb::Other,
emoji_vs_base: false,
};
}
let i = cp as usize;
let gb = self.grapheme_break_no_control(cp);
let (standalone, zero) = self.wcwidth(cp);
// See uucode_config.zig `computeWidth`: emoji modifiers and prepend
// code points keep their standalone width.
let width = if zero && !self.emoji[i].modifier && gb != Gb::Prepend {
0
} else {
standalone.min(2)
};
Properties {
width,
width_zero_in_grapheme: zero,
grapheme_break: gb,
emoji_vs_base: self.emoji_vs_base[i],
}
}
/// Port of `symbols_uucode.zig` (`is_symbol`, see `computeIsSymbol`).
#[must_use]
pub fn is_symbol(&self, cp: u32) -> bool {
if cp > MAX_CODE_POINT {
return false;
}
let i = cp as usize;
self.gc[i] == Gc::Co || self.block[i] == Block::Symbol
}
/// uucode `is_emoji_presentation` (used by the font codepoint resolver).
#[must_use]
pub fn is_emoji_presentation(&self, cp: u32) -> bool {
cp <= MAX_CODE_POINT && self.emoji[cp as usize].presentation
}
}