Addresses the partial ports listed in docs/rewrite/notes/review-completeness-codex.md (details in docs/rewrite/notes/gaps-X2.md): - Flatpak: the exit watcher no longer consumes the host command; a shared FlatpakSignalHandle lets closing a surface still SIGHUP the host process group. - Child environment is byte-preserving end to end (ghostty_os::EnvMap of OsStrings) and Snap paths are filtered like filterSnapPaths. - argv handling no longer panics on non-UTF-8 arguments. - URI percent-decoding keeps raw bytes; OSC 7 pwd is passed through without lossy conversion. - Keybinding comparison/hashing uses full Unicode case folding from a table generated from CaseFolding.txt (Unicode 17.0.0). - Link regexes set Oniguruma's whole-search retry limit through the new minimal ghostty-onig crate (one SAFETY-commented FFI call). - OSC 9 NUL-capacity check ordering matches Zig. - Wayland registry binds/clears globals advertised after startup. - Custom shaders may pass sampler2D to helper functions (naga adapter). - +list-themes preview renders OSC 8 links, styled underlines and follows color-scheme reports. Also includes the inspector's integration edits in shared files (renderer state hook, termio VT recording, core/apprt-gtk wiring), X1's config/input/cli additions for completion generation, and a fix for a race in the termio backpressure test (it wrote before `stty raw` applied, so the cooked line discipline dropped bytes past MAX_CANON). Test Plan: - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings
186 lines
6.4 KiB
Rust
186 lines
6.4 KiB
Rust
//! 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 case_folding;
|
|
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));
|
|
}
|
|
}
|