mirror of
https://github.com/openai/codex.git
synced 2026-10-28 16:06:15 +00:00
Fix TUI layout for halfwidth Japanese sound marks (#35962)
## What changed - Use Ratatui-compatible terminal widths throughout TUI layout, preserving full-width counters for long lines. - Wrap and truncate on grapheme boundaries so halfwidth kana sound marks and emoji sequences stay intact. - Apply the corrected sizing to text input, selection popups, status and history cells, Markdown, hyperlinks, and scrollable cloud diffs. ## Testing - Add focused unit and snapshot coverage for wrapping, truncation, cursor and mask alignment, popup columns, status output, session headers, Markdown tables, process output, and scrollable diffs. GitOrigin-RevId: 294ec0bcff743c6bb16d55ec26308524ce741879
This commit is contained in:
@@ -1,16 +1,38 @@
|
||||
//! Width guards for transcript rendering with fixed prefix columns.
|
||||
//! Terminal display-width helpers and guards for fixed prefix columns.
|
||||
//!
|
||||
//! Several rendering paths reserve a fixed number of columns for bullets,
|
||||
//! gutters, or labels before laying out content. When the terminal is very
|
||||
//! narrow, those reserved columns can consume the entire width, leaving zero
|
||||
//! or negative space for content.
|
||||
//!
|
||||
//! These helpers centralise the subtraction and enforce a strict-positive
|
||||
//! The display-width helpers match Ratatui's terminal-cell semantics while retaining `usize`
|
||||
//! precision for long lines. The guards centralise subtraction and enforce a strict-positive
|
||||
//! contract: they return `Some(n)` where `n > 0`, or `None` when no usable
|
||||
//! content width remains. Callers treat `None` as "render prefix-only
|
||||
//! fallback" rather than attempting wrapped rendering at zero width, which
|
||||
//! would produce empty or unstable output.
|
||||
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Returns the display width Ratatui uses for terminal text without its `u16` limit.
|
||||
pub(crate) fn display_width(text: &str) -> usize {
|
||||
UnicodeWidthStr::width(text)
|
||||
+ text
|
||||
.chars()
|
||||
.filter(|ch| matches!(ch, '\u{FF9E}' | '\u{FF9F}'))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Returns a scalar's terminal width, treating halfwidth sound marks as visible cells.
|
||||
pub(crate) fn char_width(ch: char) -> usize {
|
||||
if matches!(ch, '\u{FF9E}' | '\u{FF9F}') {
|
||||
1
|
||||
} else {
|
||||
UnicodeWidthChar::width(ch).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns usable content width after reserving fixed columns.
|
||||
///
|
||||
/// Guarantees a strict positive width (`Some(n)` where `n > 0`) or `None` when
|
||||
@@ -36,7 +58,22 @@ pub(crate) fn usable_content_width_u16(total_width: u16, reserved_cols: u16) ->
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::line_truncation::line_width;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::text::Line;
|
||||
|
||||
#[test]
|
||||
fn display_width_matches_ratatui_halfwidth_sound_marks_without_overflow() {
|
||||
assert_eq!(display_width("ガパ"), 4);
|
||||
assert_eq!(display_width("ガ゙"), 3);
|
||||
assert_eq!(display_width("界゙"), 3);
|
||||
assert_eq!(char_width('\u{FF9E}'), 1);
|
||||
assert_eq!(char_width('\u{FF9F}'), 1);
|
||||
|
||||
let text = "a".repeat(65_536);
|
||||
assert_eq!(display_width(&text), 65_536);
|
||||
assert_eq!(line_width(&Line::from(text)), 65_536);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usable_content_width_returns_none_when_reserved_exhausts_width() {
|
||||
|
||||
Reference in New Issue
Block a user