From fd6585e86381219f71e431955b1b1666453773fa Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Wed, 15 Apr 2026 11:54:16 -0300 Subject: [PATCH] fix(tui): align table emoji widths with terminals Compute markdown table cell widths with per-character terminal cell widths so boxed table padding matches xterm.js for emoji presentation, keycap, and ZWJ sequences. This keeps emoji tables rendered as boxed tables while preventing border columns from drifting during resize in xterm-based terminals. --- codex-rs/tui/src/markdown_render.rs | 32 +++++++++++++++++++---- codex-rs/tui/src/markdown_render_tests.rs | 18 +++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index e61062f9a5..257c25cf34 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -63,7 +63,7 @@ use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::sync::LazyLock; -use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use url::Url; struct MarkdownStyles { @@ -1172,7 +1172,7 @@ where if word_count > 0 { total_words += word_count; total_cells += 1; - total_cell_width += plain.width(); + total_cell_width += Self::terminal_cell_width(&plain); } } @@ -1182,7 +1182,7 @@ where total_words as f64 / total_cells as f64 }; let avg_cell_width = if total_cells == 0 { - header_plain.width() as f64 + Self::terminal_cell_width(&header_plain) as f64 } else { total_cell_width as f64 / total_cells as f64 }; @@ -1504,7 +1504,10 @@ where #[inline] fn spans_display_width(spans: &[Span<'_>]) -> usize { - spans.iter().map(|span| span.content.width()).sum() + spans + .iter() + .map(|span| Self::terminal_cell_width(&span.content)) + .sum() } #[inline] @@ -1523,7 +1526,17 @@ where #[inline] fn longest_token_width(text: &str) -> usize { - text.split_whitespace().map(str::width).max().unwrap_or(0) + text.split_whitespace() + .map(Self::terminal_cell_width) + .max() + .unwrap_or(0) + } + + #[inline] + fn terminal_cell_width(text: &str) -> usize { + text.chars() + .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0)) + .sum() } fn push_inline_style(&mut self, style: Style) { @@ -2176,6 +2189,15 @@ mod tests { } } + #[test] + fn terminal_cell_width_matches_xterm_table_cases() { + assert_eq!(W::terminal_cell_width("🅰️"), 1); + assert_eq!(W::terminal_cell_width("↔️"), 1); + assert_eq!(W::terminal_cell_width("1️⃣"), 1); + assert_eq!(W::terminal_cell_width("👩‍💻"), 4); + assert_eq!(W::terminal_cell_width("🔤"), 2); + } + // ===== Column-metrics unit tests ===== #[test] diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index 24e6e41935..0a788ad491 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -4,7 +4,7 @@ use ratatui::text::Line; use ratatui::text::Span; use ratatui::text::Text; use std::path::Path; -use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use crate::markdown_render::COLON_LOCATION_SUFFIX_RE; use crate::markdown_render::HASH_LOCATION_SUFFIX_RE; @@ -16,6 +16,12 @@ fn render_markdown_text_for_cwd(input: &str, cwd: &Path) -> Text<'static> { render_markdown_text_with_width_and_cwd(input, /*width*/ None, Some(cwd)) } +fn terminal_cell_width(text: &str) -> usize { + text.chars() + .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0)) + .sum() +} + #[test] fn empty() { assert_eq!(render_markdown_text(""), Text::default()); @@ -1480,13 +1486,15 @@ fn table_with_emoji_sequence_cells_has_consistent_display_width() { }) .map(String::as_str) .collect(); - let expected_width = table_lines + let first_table_line = table_lines .first() - .expect("expected unicode table to render as a box") - .width(); + .expect("expected unicode table to render as a box"); + let expected_width = terminal_cell_width(first_table_line); assert!( - table_lines.iter().all(|line| line.width() == expected_width), + table_lines + .iter() + .all(|line| terminal_cell_width(line) == expected_width), "expected every rendered table row to have width {expected_width}: {table_lines:?}" ); assert!(