chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
//! End-to-end wire proof for the macOS glyph-atlas corruption fix (#330).
|
||||
//!
|
||||
//! Renders a colored buffer through a *real* `CrosstermBackend` (the same
|
||||
//! backend the shipped binary uses) and inspects the actual ANSI bytes it
|
||||
//! writes. Under glyph-safe mode, animated colors are quantized to
|
||||
//! `Color::Indexed`, which must serialize as `38;5;<n>` (256-color) and never
|
||||
//! as `38;2;r;g;b` (truecolor). The truecolor churn is what overflows the
|
||||
//! terminal's GPU glyph atlas and garbles letters into boxes.
|
||||
|
||||
use ratatui::backend::{Backend, CrosstermBackend};
|
||||
use ratatui::buffer::Cell;
|
||||
use ratatui::style::Color;
|
||||
|
||||
/// Drive the backend to draw a single cell with the given fg color and return
|
||||
/// the raw bytes it emitted to the writer.
|
||||
fn emitted_bytes_for(fg: Color) -> Vec<u8> {
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
{
|
||||
let mut backend = CrosstermBackend::new(&mut out);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("X");
|
||||
cell.set_fg(fg);
|
||||
let content = [(0u16, 0u16, &cell)];
|
||||
backend.draw(content.into_iter()).expect("draw");
|
||||
backend.flush().expect("flush");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_color_emits_256_sgr_not_truecolor() {
|
||||
// What glyph-safe quantization produces (xterm-256 index).
|
||||
let bytes = emitted_bytes_for(Color::Indexed(111));
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
assert!(
|
||||
text.contains("38;5;111"),
|
||||
"expected 256-color SGR in emitted bytes, got: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("38;2;"),
|
||||
"256-color path must never emit truecolor SGR, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truecolor_color_emits_truecolor_sgr() {
|
||||
// Sanity check: the truecolor path (robust terminals) still works and is
|
||||
// precisely what we suppress on fragile terminals.
|
||||
let bytes = emitted_bytes_for(Color::Rgb(138, 180, 248));
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
assert!(
|
||||
text.contains("38;2;138;180;248"),
|
||||
"expected truecolor SGR, got: {text:?}"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//! 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")
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user