From e2a9ee05f4f6e805bf705131ddee87e9cc22e35d Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 10 Sep 2026 00:48:17 +0000 Subject: [PATCH] Extract shared footer hint wrapping in the TUI (#44354) ## What changed Replace duplicated footer wrapping in the MCP elicitation and request-user-input overlays with `wrap_hint_rows`. Preserve whole-hint grouping, display-width and separator accounting, oversized hints, and the single empty row for empty input. ## Testing Add unit tests for Unicode display widths, whole-hint wrapping, oversized hints at zero width, and empty input. GitOrigin-RevId: 4d8ead0acbfa3bf9241baedb86d2476566d26ebc --- .../src/bottom_pane/mcp_server_elicitation.rs | 44 +++---------------- .../src/bottom_pane/request_user_input/mod.rs | 44 +++---------------- codex-rs/tui/src/footer_hint.rs | 40 +++++++++++++++++ codex-rs/tui/src/footer_hint_tests.rs | 39 ++++++++++++++++ codex-rs/tui/src/lib.rs | 1 + 5 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 codex-rs/tui/src/footer_hint.rs create mode 100644 codex-rs/tui/src/footer_hint_tests.rs diff --git a/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs b/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs index 058c40215e..a433df5e50 100644 --- a/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs +++ b/codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs @@ -1716,44 +1716,12 @@ impl BottomPaneView for McpServerElicitationOverlay { } fn wrap_footer_tips(width: u16, tips: Vec) -> Vec> { - let max_width = width.max(1) as usize; - let separator_width = UnicodeWidthStr::width(FOOTER_SEPARATOR); - if tips.is_empty() { - return vec![Vec::new()]; - } - - let mut lines = Vec::new(); - let mut current = Vec::new(); - let mut used = 0usize; - - for tip in tips { - let tip_width = UnicodeWidthStr::width(tip.text.as_str()).min(max_width); - let extra = if current.is_empty() { - tip_width - } else { - separator_width.saturating_add(tip_width) - }; - if !current.is_empty() && used.saturating_add(extra) > max_width { - lines.push(current); - current = Vec::new(); - used = 0; - } - if current.is_empty() { - used = tip_width; - } else { - used = used - .saturating_add(separator_width) - .saturating_add(tip_width); - } - current.push(tip); - } - - if current.is_empty() { - lines.push(Vec::new()); - } else { - lines.push(current); - } - lines + crate::footer_hint::wrap_hint_rows( + tips, + width, + UnicodeWidthStr::width(FOOTER_SEPARATOR), + |tip| UnicodeWidthStr::width(tip.text.as_str()), + ) } #[cfg(test)] diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index 6b3dcda5ba..06ac2fe6b2 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -655,44 +655,12 @@ impl RequestUserInputOverlay { } fn wrap_footer_tips(&self, width: u16, tips: Vec) -> Vec> { - let max_width = width.max(1) as usize; - let separator_width = UnicodeWidthStr::width(TIP_SEPARATOR); - if tips.is_empty() { - return vec![Vec::new()]; - } - - let mut lines: Vec> = Vec::new(); - let mut current: Vec = Vec::new(); - let mut used = 0usize; - - for tip in tips { - let tip_width = UnicodeWidthStr::width(tip.text.as_str()).min(max_width); - let extra = if current.is_empty() { - tip_width - } else { - separator_width.saturating_add(tip_width) - }; - if !current.is_empty() && used.saturating_add(extra) > max_width { - lines.push(current); - current = Vec::new(); - used = 0; - } - if current.is_empty() { - used = tip_width; - } else { - used = used - .saturating_add(separator_width) - .saturating_add(tip_width); - } - current.push(tip); - } - - if current.is_empty() { - lines.push(Vec::new()); - } else { - lines.push(current); - } - lines + crate::footer_hint::wrap_hint_rows( + tips, + width, + UnicodeWidthStr::width(TIP_SEPARATOR), + |tip| UnicodeWidthStr::width(tip.text.as_str()), + ) } pub(super) fn footer_required_height(&self, width: u16) -> u16 { diff --git a/codex-rs/tui/src/footer_hint.rs b/codex-rs/tui/src/footer_hint.rs new file mode 100644 index 0000000000..55be50457b --- /dev/null +++ b/codex-rs/tui/src/footer_hint.rs @@ -0,0 +1,40 @@ +//! Packs footer hints into rows without separating a shortcut from its label. +//! Callers retain ownership of styling and of clipping or wrapping oversized hints. + +pub(crate) fn wrap_hint_rows( + hints: impl IntoIterator, + width: u16, + separator_width: usize, + hint_width: impl Fn(&T) -> usize, +) -> Vec> { + let width = usize::from(width.max(1)); + let mut rows = Vec::new(); + let mut row = Vec::new(); + let mut used = 0usize; + for hint in hints { + let hint_width = hint_width(&hint).min(width); + let extra = if row.is_empty() { + hint_width + } else { + separator_width.saturating_add(hint_width) + }; + if !row.is_empty() && used.saturating_add(extra) > width { + rows.push(std::mem::take(&mut row)); + used = 0; + } + if row.is_empty() { + used = hint_width; + } else { + used = used + .saturating_add(separator_width) + .saturating_add(hint_width); + } + row.push(hint); + } + rows.push(row); + rows +} + +#[cfg(test)] +#[path = "footer_hint_tests.rs"] +mod tests; diff --git a/codex-rs/tui/src/footer_hint_tests.rs b/codex-rs/tui/src/footer_hint_tests.rs new file mode 100644 index 0000000000..a3792703f3 --- /dev/null +++ b/codex-rs/tui/src/footer_hint_tests.rs @@ -0,0 +1,39 @@ +use super::wrap_hint_rows; +use pretty_assertions::assert_eq; +use unicode_width::UnicodeWidthStr; + +#[test] +fn wraps_whole_hints_using_display_width() { + let hints = ["↑ 上", "↓ 下", "enter select"]; + assert_eq!( + wrap_hint_rows( + hints, + /*width*/ 9, + /*separator_width*/ 1, + |hint| { UnicodeWidthStr::width(*hint) } + ), + vec![vec!["↑ 上", "↓ 下"], vec!["enter select"]] + ); +} + +#[test] +fn retains_oversized_hints_and_an_empty_row() { + assert_eq!( + wrap_hint_rows( + ["ctrl+x stop", "esc back"], + /*width*/ 0, + /*separator_width*/ 3, + |hint| hint.len() + ), + vec![vec!["ctrl+x stop"], vec!["esc back"]] + ); + assert_eq!( + wrap_hint_rows( + Vec::::new(), + /*width*/ 40, + /*separator_width*/ 3, + String::len + ), + vec![Vec::::new()] + ); +} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index d10eeb17c2..43591eceed 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -155,6 +155,7 @@ mod ide_context; mod inline_visualization; pub(crate) mod insert_history; pub use insert_history::insert_history_lines; +mod footer_hint; mod key_hint; mod keymap; mod keymap_setup;