fix(cli): draw grapheme clusters whole in the theme preview
The +list-themes interactive preview dropped the second emoji of ZWJ clusters (e.g. woman + ZWJ + rocket) in the search box. Zig's libvaxis enables Unicode grapheme mode 2027 for its session and sizes text per grapheme cluster; the Rust preview did neither, so the terminal split the cluster at the ZWJ and the cursor column was computed from summed per-codepoint widths (4 instead of 2). - The preview sends ?2027h on entry and ?2027l on exit like vaxis. - tui::gwidth follows vaxis's unicode width rules per grapheme cluster (VS15/VS16, emoji presentation, regional-indicator pairs) using the ghostty-unicode emoji table; the preview's print path uses it. +list-colors and +list-keybinds pretty output (also using gwidth) stays byte-identical to /usr/bin/ghostty at widths 40/80/132. Test Plan: - cargo test -p ghostty-cli --test list_themes_pty zwj_emoji_cluster_is_drawn_whole_in_search_box - cargo test --workspace --no-fail-fast
This commit is contained in:
1 parent
03045f6ece
commit
f687a8c585
5 files changed
+84
-5
No files matched your search
@@ -18,6 +18,7 @@ ghostty-crash.workspace = true
|
||||
ghostty-font.workspace = true
|
||||
ghostty-input.workspace = true
|
||||
ghostty-os.workspace = true
|
||||
ghostty-unicode.workspace = true
|
||||
ghostty-vt.workspace = true
|
||||
log = "0.4.34"
|
||||
ratatui = "0.30.2"
|
||||
|
||||
@@ -1570,7 +1570,7 @@ impl Win<'_, '_> {
|
||||
let x = self.area.x + col;
|
||||
let y = self.area.y + row;
|
||||
let max = usize::from(self.area.width - col);
|
||||
let (nx, _) = self.buf.borrow_mut().set_stringn(x, y, text, max, style);
|
||||
let nx = self.set_graphemes(x, y, text, max, style);
|
||||
if (link.is_some() || ul.is_some()) && nx > x {
|
||||
self.decos.borrow_mut().push(Deco {
|
||||
x,
|
||||
@@ -1583,6 +1583,33 @@ impl Win<'_, '_> {
|
||||
nx - self.area.x
|
||||
}
|
||||
|
||||
/// Write `text` grapheme by grapheme, each sized with `vaxis.gwidth`
|
||||
/// (not ratatui's width), stopping when a grapheme no longer fits.
|
||||
fn set_graphemes(&self, x: u16, y: u16, text: &str, max: usize, style: Style) -> u16 {
|
||||
let mut buf = self.buf.borrow_mut();
|
||||
let mut remaining = max.min(usize::from(buf.area.right().saturating_sub(x)));
|
||||
let mut cx = x;
|
||||
for g in text.graphemes(true) {
|
||||
if g.contains(char::is_control) {
|
||||
continue;
|
||||
}
|
||||
let w = gw(g);
|
||||
if w == 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(rest) = remaining.checked_sub(usize::from(w)) else {
|
||||
break;
|
||||
};
|
||||
remaining = rest;
|
||||
buf[(cx, y)].set_symbol(g).set_style(style);
|
||||
for i in 1..w {
|
||||
buf[(cx + i, y)].reset();
|
||||
}
|
||||
cx += w;
|
||||
}
|
||||
cx
|
||||
}
|
||||
|
||||
/// `Window.print`: several segments one after another.
|
||||
fn print_segments(&self, segments: &[(&str, Style)], row: u16, col: u16) -> u16 {
|
||||
let mut col = col;
|
||||
@@ -1711,7 +1738,7 @@ fn run_loop(app: &mut Preview) -> Result<(), Error> {
|
||||
// Alternate screen, title, mouse reporting.
|
||||
write!(
|
||||
tty,
|
||||
"\x1b[?1049h\x1b]2;\u{1f47b} Ghostty Theme Preview \u{1f47b}\x1b\\\
|
||||
"\x1b[?1049h\x1b[?2027h\x1b]2;\u{1f47b} Ghostty Theme Preview \u{1f47b}\x1b\\\
|
||||
\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h"
|
||||
)?;
|
||||
|
||||
@@ -1767,7 +1794,7 @@ fn run_loop(app: &mut Preview) -> Result<(), Error> {
|
||||
let _ = terminal.show_cursor();
|
||||
let _ = write!(
|
||||
tty,
|
||||
"{}\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1049l",
|
||||
"{}\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2027l\x1b[?1049l",
|
||||
Csi::Mode(Mode::ResetDecPrivateMode(theme_mode)),
|
||||
);
|
||||
let _ = tty.flush();
|
||||
|
||||
@@ -64,10 +64,49 @@ pub struct Row {
|
||||
rows: Vec<Vec<Cell>>,
|
||||
}
|
||||
|
||||
/// Display width of a string (`Window.gwidth` with unicode mode on).
|
||||
/// Display width of one grapheme (`vaxis.gwidth` `.unicode` method).
|
||||
fn grapheme_width(g: &str) -> u16 {
|
||||
let mut width = 0u16;
|
||||
let (mut vs16, mut vs15, mut emoji, mut ri) = (false, false, false, 0u8);
|
||||
for c in g.chars() {
|
||||
match c {
|
||||
'\u{fe0f}' => {
|
||||
vs16 = true;
|
||||
continue;
|
||||
}
|
||||
'\u{fe0e}' => {
|
||||
vs15 = true;
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if ghostty_unicode::emoji_table::table().get(u32::from(c)) {
|
||||
emoji = true;
|
||||
}
|
||||
if ('\u{1F1E6}'..='\u{1F1FF}').contains(&c) {
|
||||
ri += 1;
|
||||
}
|
||||
// 0x1f3fb..=0x1f3ff are wide in vaxis' EAW table too.
|
||||
let w = c.width().unwrap_or(0);
|
||||
width = width.max(u16::try_from(w).unwrap_or(2));
|
||||
}
|
||||
if vs15 {
|
||||
width = width.max(1);
|
||||
} else if vs16 || emoji || ri == 2 {
|
||||
width = width.max(2);
|
||||
}
|
||||
width
|
||||
}
|
||||
|
||||
/// Display width of a string (`Window.gwidth` with unicode mode on): the sum
|
||||
/// of the widths of its grapheme clusters, as `vaxis.gwidth` computes it.
|
||||
#[must_use]
|
||||
pub fn gwidth(s: &str) -> u16 {
|
||||
let w: usize = s.chars().map(|c| c.width().unwrap_or(0)).sum();
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
let w: usize = s
|
||||
.graphemes(true)
|
||||
.map(|g| usize::from(grapheme_width(g)))
|
||||
.sum();
|
||||
u16::try_from(w).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
@@ -432,6 +471,10 @@ mod tests {
|
||||
let r = row.print_segment("a\u{4e2d}b", Style::default(), 0);
|
||||
assert_eq!(4, r.col);
|
||||
assert_eq!(2, gwidth("\u{4e2d}"));
|
||||
assert_eq!(2, gwidth("\u{1F469}\u{200D}\u{1F680}"));
|
||||
assert_eq!(2, gwidth("\u{2764}\u{FE0F}"));
|
||||
assert_eq!(2, gwidth("\u{1F1FA}\u{1F1F8}"));
|
||||
assert_eq!(0, gwidth("\u{200B}"));
|
||||
let r = row.print_segment("zz", Style::default(), 3);
|
||||
assert!(!r.overflow);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user