Files
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

103 lines
3.9 KiB
Rust

//! Guard against width-unstable glyphs in TUI source (issue seen 2026-07-02).
//!
//! Unicode 16 reclassified several symbol ranges from narrow (1 cell) to wide
//! (2 cells): U+2630..U+2637 (trigrams), U+268A..U+268F (monograms/digrams),
//! U+4DC0..U+4DFF (hexagrams), U+1D300..U+1D356 and U+1D360..U+1D376 (Tai Xuan
//! Jing), among others. Terminals with Unicode 16 width tables (kitty >= 0.40)
//! render them 2 cells wide, while `unicode-width` builds pinned to older
//! tables count 1. Any such glyph in a rendered line shears the whole row:
//! everything after it shifts by one column, which pushed the right-docked
//! info-widget borders off screen when swarm "Plan" lines used U+2630.
//!
//! This test scans TUI-rendering crates for those codepoints in non-comment
//! source so the bug class cannot silently return. If you genuinely need one
//! of these glyphs, pad the row defensively and add an exception here with a
//! comment explaining why the shear cannot happen.
use std::path::{Path, PathBuf};
/// Codepoint ranges whose East Asian Width changed narrow -> wide in
/// Unicode 16 (diff of unicode-width 0.2.0 vs 0.2.2 width tables), restricted
/// to symbol blocks plausible as TUI icons. Script/format chars (e.g. Tagalog,
/// Balinese) are omitted: they only appear inside user content, which the
/// renderer already width-measures with the bundled unicode-width.
const UNSTABLE_RANGES: &[(u32, u32, &str)] = &[
(0x2630, 0x2637, "Yijing trigrams"),
(0x268A, 0x268F, "Yijing monograms/digrams"),
(0x31E4, 0x31E5, "CJK strokes added in Unicode 16"),
(0x4DC0, 0x4DFF, "Yijing hexagrams"),
(0x1D300, 0x1D356, "Tai Xuan Jing symbols"),
(0x1D360, 0x1D376, "counting rod numerals"),
];
fn unstable(ch: char) -> Option<&'static str> {
let cp = ch as u32;
UNSTABLE_RANGES
.iter()
.find(|&&(lo, hi, _)| cp >= lo && cp <= hi)
.map(|&(_, _, name)| name)
}
fn scan_file(path: &Path, violations: &mut Vec<String>) {
let Ok(text) = std::fs::read_to_string(path) else {
return;
};
for (lineno, line) in text.lines().enumerate() {
// Skip pure comment lines so docs may *mention* these glyphs.
let trimmed = line.trim_start();
if trimmed.starts_with("//") || trimmed.starts_with("*") {
continue;
}
for ch in line.chars() {
if let Some(block) = unstable(ch) {
violations.push(format!(
"{}:{}: U+{:04X} '{}' ({}) is width-unstable across Unicode versions",
path.display(),
lineno + 1,
ch as u32,
ch,
block
));
}
}
}
}
fn scan_dir(dir: &Path, violations: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
scan_dir(&path, violations);
} else if path.extension().is_some_and(|e| e == "rs") {
scan_file(&path, violations);
}
}
}
#[test]
fn no_width_unstable_glyphs_in_tui_sources() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let crates_root = manifest.parent().expect("crates dir");
let mut violations = Vec::new();
// All crates that produce user-visible terminal lines.
for krate in [
"jcode-tui",
"jcode-tui-messages",
"jcode-tui-markdown",
"jcode-tui-render",
"jcode-tui-tool-display",
"jcode-render-core",
] {
scan_dir(&crates_root.join(krate).join("src"), &mut violations);
}
assert!(
violations.is_empty(),
"width-unstable glyphs found (these shear rows on Unicode-16 terminals \
like kitty >= 0.40; use a width-stable alternative such as ≡ for ☰):\n{}",
violations.join("\n")
);
}