diff --git a/codex-rs/tui/src/analytics/render.rs b/codex-rs/tui/src/analytics/render.rs index 5f37e27460..595fbcea81 100644 --- a/codex-rs/tui/src/analytics/render.rs +++ b/codex-rs/tui/src/analytics/render.rs @@ -9,14 +9,12 @@ use crate::analytics::models::AccountAnalyticsValue; use crate::analytics::sections::Section; use crate::keymap::ListAction; use crate::style::accent_style; -use crate::style::footer_hint_key_style; use crate::wrapping::RtOptions; use crate::wrapping::word_wrap_lines; use ratatui::buffer::Buffer; use ratatui::layout::Constraint; use ratatui::layout::Layout; use ratatui::layout::Rect; -use ratatui::style::Color; use ratatui::style::Styled; use ratatui::style::Stylize; use ratatui::text::Line; @@ -171,11 +169,7 @@ impl AnalyticsView { spans.push(" · ".into()); } let (keys, description) = control.rsplit_once(' ').unwrap_or((control, "")); - spans.push( - keys.to_owned() - .fg(Color::Reset) - .patch_style(footer_hint_key_style().bold().not_dim()), - ); + spans.extend(crate::key_hint::key_label_spans(keys)); if !description.is_empty() { spans.push(format!(" {description}").into()); } diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__duplicate_message_dates_render_the_full_other_remainder.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__duplicate_message_dates_render_the_full_other_remainder.snap index 6a5d3333c5..e50c24e2f2 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__duplicate_message_dates_render_the_full_other_remainder.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__duplicate_message_dates_render_the_full_other_remainder.snap @@ -23,8 +23,8 @@ Palette: 2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 3: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset) 4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -6: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +6: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 22:1 24:0 1: 0:0 1:2 3:0 18:3 30:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__missing_zero_and_narrow_charts_keep_the_selection_visible.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__missing_zero_and_narrow_charts_keep_the_selection_visible.snap index bc23bf58b1..a0dd98e27d 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__missing_zero_and_narrow_charts_keep_the_selection_visible.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__missing_zero_and_narrow_charts_keep_the_selection_visible.snap @@ -17,8 +17,8 @@ Palette: 2: Style::new().fg(Color::Rgb(165, 165, 165)).bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 4: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -6: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +5: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold() +6: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 30:1 31:0 1: 0:0 1:2 2:0 27:3 34:0 @@ -43,8 +43,8 @@ Palette: 2: Style::new().fg(Color::Rgb(165, 165, 165)).bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 4: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -6: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +5: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold() +6: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 30:1 31:0 1: 0:0 1:2 2:0 27:3 34:0 @@ -68,9 +68,9 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() +4: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold() 5: Style::new().fg(Color::Rgb(165, 165, 165)).bg(Color::Reset).underline_color(Color::Reset) -6: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +6: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 6:1 7:0 1: 0:0 5:2 7:0 @@ -93,7 +93,7 @@ Palette: 0: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 1: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() +3: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold() 4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) Rows (column:palette): 0: 0:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__narrow_long_range_charts_never_truncate_dates.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__narrow_long_range_charts_never_truncate_dates.snap index fd494d74a1..7a25e2484f 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__narrow_long_range_charts_never_truncate_dates.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__narrow_long_range_charts_never_truncate_dates.snap @@ -16,7 +16,7 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() Rows (column:palette): 0: 0:0 1: 0:1 @@ -40,8 +40,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 1:1 @@ -65,8 +65,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 4:1 @@ -90,8 +90,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 5:1 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__oversized_axes_are_hidden_instead_of_truncated.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__oversized_axes_are_hidden_instead_of_truncated.snap index f082b06e12..fd6afa476b 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__oversized_axes_are_hidden_instead_of_truncated.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__oversized_axes_are_hidden_instead_of_truncated.snap @@ -17,8 +17,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1:1 5:0 1: 0:0 1:2 6:0 @@ -44,8 +44,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 15:1 19:0 1: 0:0 1:2 5:0 12:3 24:0 @@ -71,8 +71,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 18:1 22:0 1: 0:0 1:2 5:0 14:3 26:0 @@ -98,8 +98,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 22:1 26:0 1: 0:0 1:2 5:0 19:3 31:0 @@ -125,8 +125,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 4:1 24:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__partial_cells_use_the_midpoint_and_descend_from_the_baseline.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__partial_cells_use_the_midpoint_and_descend_from_the_baseline.snap index f8b8f2f1a6..7370a839bf 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__partial_cells_use_the_midpoint_and_descend_from_the_baseline.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__partial_cells_use_the_midpoint_and_descend_from_the_baseline.snap @@ -14,8 +14,8 @@ Palette: 1: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) 2: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 4:1 1: 0:1 1:2 5:1 8:3 9:1 @@ -37,8 +37,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 3: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset).reversed() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 8:1 9:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__selected_callout_keeps_the_authoritative_daily_total.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__selected_callout_keeps_the_authoritative_daily_total.snap index b9a309eac9..8b6deef67c 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__selected_callout_keeps_the_authoritative_daily_total.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__selected_callout_keeps_the_authoritative_daily_total.snap @@ -17,8 +17,8 @@ Palette: 2: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 3: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset) 4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -6: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +6: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 1:1 3:0 21:2 26:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__seven_day_totals.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__seven_day_totals.snap index a1f322d99a..74c241a4b2 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__seven_day_totals.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__seven_day_totals.snap @@ -20,8 +20,8 @@ Palette: 1: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 2: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 3: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 91:1 94:0 1: 0:0 1:1 4:0 77:1 80:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__signed_chart_dark.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__signed_chart_dark.snap index 82660a65c4..a76a24d098 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__signed_chart_dark.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__signed_chart_dark.snap @@ -24,8 +24,8 @@ Palette: 4: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset).reversed() 5: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset) 6: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() -7: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -8: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +7: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold() +8: Style::new().fg(Color::Rgb(99, 168, 248)).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 3:1 5:0 9:2 13:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__signed_chart_light.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__signed_chart_light.snap index f100f1c7e8..10525e141a 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__signed_chart_light.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__signed_chart_light.snap @@ -24,8 +24,8 @@ Palette: 4: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset).reversed() 5: Style::new().magenta().bg(Color::Reset).underline_color(Color::Reset) 6: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() -7: Style::new().fg(Color::Rgb(0, 95, 135)).bg(Color::Reset).underline_color(Color::Reset).bold() -8: Style::new().fg(Color::Rgb(0, 95, 135)).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +7: Style::new().fg(Color::Rgb(28, 100, 200)).bg(Color::Reset).underline_color(Color::Reset).bold() +8: Style::new().fg(Color::Rgb(28, 100, 200)).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 3:1 5:0 9:2 13:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__tiny_segments_keep_their_series_color_beside_a_large_day.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__tiny_segments_keep_their_series_color_beside_a_large_day.snap index cd4be0edf0..87340fc974 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__tiny_segments_keep_their_series_color_beside_a_large_day.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__tiny_segments_keep_their_series_color_beside_a_large_day.snap @@ -17,8 +17,8 @@ Palette: 1: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() 2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 11:1 17:0 1: 0:0 1:2 4:0 8:3 20:0 @@ -50,8 +50,8 @@ Palette: 2: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset) 4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).reversed() -5: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -6: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +5: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +6: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 12:1 18:0 1: 0:0 2:2 5:0 9:3 21:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__zero_credit_history_uses_a_readable_scale.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__zero_credit_history_uses_a_readable_scale.snap index 8c4964a90b..85b4bef1f3 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__zero_credit_history_uses_a_readable_scale.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__plot__tests__zero_credit_history_uses_a_readable_scale.snap @@ -15,8 +15,8 @@ Palette: 0: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset) 1: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim() 2: Style::new().white().bg(Color::Reset).underline_color(Color::Reset).bold() -3: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold() -4: Style::new().cyan().bg(Color::Reset).underline_color(Color::Reset).bold().underlined() +3: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold() +4: Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold().underlined() Rows (column:palette): 0: 0:0 1: 0:0 3:1 4:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_1.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_1.snap index bfbea75055..460aadeb4e 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_1.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_1.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,14 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: White|Reset|Reset|BOLD 8: Cyan|Reset|Reset|NONE -9: Cyan|Reset|Reset|BOLD | UNDERLINED +9: Rgb(99, 168, 248)|Reset|Reset|BOLD | UNDERLINED +10: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 @@ -99,5 +99,5 @@ Rows (column:palette): 37: 0:0 2:6 4:0 38: 0:0 2:5 4:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 12:1 13:2 22:1 23:2 34:1 37:2 45:1 46:2 118:0 +40: 0:0 2:10 9:2 20:10 21:2 34:10 37:2 44:10 49:2 118:0 +41: 0:0 2:10 3:2 12:10 13:2 22:10 23:2 34:10 37:2 45:10 46:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_2.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_2.snap index 4b02d8fb9d..c696e96599 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_2.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_2.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,14 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: White|Reset|Reset|BOLD 6: Green|Reset|Reset|NONE 7: Magenta|Reset|Reset|NONE 8: Cyan|Reset|Reset|NONE -9: Cyan|Reset|Reset|BOLD | UNDERLINED +9: Rgb(99, 168, 248)|Reset|Reset|BOLD | UNDERLINED +10: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 @@ -99,5 +99,5 @@ Rows (column:palette): 37: 0:0 2:6 4:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0 +40: 0:0 2:10 9:2 20:10 21:2 34:10 37:2 44:10 49:2 118:0 +41: 0:0 2:10 3:2 12:10 13:2 24:10 27:2 35:10 36:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_3.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_3.snap index 2374745fef..fccb190361 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_3.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_3.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,14 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: White|Reset|Reset|BOLD 8: Cyan|Reset|Reset|NONE -9: Cyan|Reset|Reset|BOLD | UNDERLINED +9: Rgb(99, 168, 248)|Reset|Reset|BOLD | UNDERLINED +10: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 @@ -99,5 +99,5 @@ Rows (column:palette): 37: 0:0 2:5 4:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0 +40: 0:0 2:10 9:2 20:10 21:2 34:10 37:2 44:10 49:2 118:0 +41: 0:0 2:10 3:2 12:10 13:2 24:10 27:2 35:10 36:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_4.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_4.snap index b23c8ebfdb..936e828772 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_4.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_4.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,8 +50,9 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM +5: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 @@ -94,5 +94,5 @@ Rows (column:palette): 37: 0:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 14:1 17:2 25:1 26:2 118:0 +40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0 +41: 0:0 2:5 3:2 14:5 17:2 25:5 26:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_6.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_6.snap index 0a4af96b8e..fa9bac691b 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_6.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_6.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,14 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: White|Reset|Reset|BOLD 6: Green|Reset|Reset|NONE 7: Magenta|Reset|Reset|NONE 8: Cyan|Reset|Reset|NONE -9: Cyan|Reset|Reset|BOLD | UNDERLINED +9: Rgb(99, 168, 248)|Reset|Reset|BOLD | UNDERLINED +10: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 @@ -99,5 +99,5 @@ Rows (column:palette): 37: 0:0 2:6 4:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0 +40: 0:0 2:10 9:2 20:10 21:2 34:10 37:2 44:10 49:2 118:0 +41: 0:0 2:10 3:2 12:10 13:2 24:10 27:2 35:10 36:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_8.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_8.snap index d13c95b507..5cb858f651 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_8.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_business_8.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,8 +50,9 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM +5: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 @@ -94,5 +94,5 @@ Rows (column:palette): 37: 0:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 118:0 -41: 0:0 2:1 3:2 11:1 12:2 23:1 26:2 34:1 35:2 118:0 +40: 0:0 2:5 9:2 20:5 21:2 118:0 +41: 0:0 2:5 3:2 11:5 12:2 23:5 26:2 34:5 35:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_1.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_1.snap index 623a419afe..c712bee228 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_1.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_1.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,14 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: White|Reset|Reset|BOLD 7: Magenta|Reset|Reset|NONE 8: Cyan|Reset|Reset|NONE -9: Cyan|Reset|Reset|BOLD | UNDERLINED +9: Rgb(99, 168, 248)|Reset|Reset|BOLD | UNDERLINED +10: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 @@ -99,5 +99,5 @@ Rows (column:palette): 37: 0:0 2:5 4:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 12:1 13:2 22:1 23:2 34:1 37:2 45:1 46:2 118:0 +40: 0:0 2:10 9:2 20:10 21:2 34:10 37:2 44:10 49:2 118:0 +41: 0:0 2:10 3:2 12:10 13:2 22:10 23:2 34:10 37:2 45:10 46:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_2.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_2.snap index ec95172894..38f4881a07 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_2.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_2.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,14 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: White|Reset|Reset|BOLD 6: Green|Reset|Reset|NONE 7: Magenta|Reset|Reset|NONE 8: Cyan|Reset|Reset|NONE -9: Cyan|Reset|Reset|BOLD | UNDERLINED +9: Rgb(99, 168, 248)|Reset|Reset|BOLD | UNDERLINED +10: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 @@ -99,5 +99,5 @@ Rows (column:palette): 37: 0:0 2:6 4:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0 +40: 0:0 2:10 9:2 20:10 21:2 34:10 37:2 44:10 49:2 118:0 +41: 0:0 2:10 3:2 12:10 13:2 24:10 27:2 35:10 36:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_4.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_4.snap index ec82b2cc00..a1cb1fa49c 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_4.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_4.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,8 +50,9 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM +5: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 @@ -94,5 +94,5 @@ Rows (column:palette): 37: 0:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 29:1 30:2 43:1 46:2 53:1 58:2 118:0 -41: 0:0 2:1 3:2 14:1 17:2 25:1 26:2 118:0 +40: 0:0 2:5 9:2 20:5 21:2 29:5 30:2 43:5 46:2 53:5 58:2 118:0 +41: 0:0 2:5 3:2 14:5 17:2 25:5 26:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_5.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_5.snap index c2f68771d2..afffb965e7 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_5.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_5.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,14 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: White|Reset|Reset|BOLD 6: Green|Reset|Reset|NONE 7: Magenta|Reset|Reset|NONE 8: Cyan|Reset|Reset|NONE -9: Cyan|Reset|Reset|BOLD | UNDERLINED +9: Rgb(99, 168, 248)|Reset|Reset|BOLD | UNDERLINED +10: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 @@ -99,5 +99,5 @@ Rows (column:palette): 37: 0:0 2:6 4:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 12:1 13:2 22:1 23:2 34:1 37:2 45:1 46:2 118:0 +40: 0:0 2:10 9:2 20:10 21:2 34:10 37:2 44:10 49:2 118:0 +41: 0:0 2:10 3:2 12:10 13:2 22:10 23:2 34:10 37:2 45:10 46:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_6.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_6.snap index 7055b2e97e..55c7516092 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_6.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_6.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,14 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: White|Reset|Reset|BOLD 6: Green|Reset|Reset|NONE 7: Magenta|Reset|Reset|NONE 8: Cyan|Reset|Reset|NONE -9: Cyan|Reset|Reset|BOLD | UNDERLINED +9: Rgb(99, 168, 248)|Reset|Reset|BOLD | UNDERLINED +10: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 @@ -99,5 +99,5 @@ Rows (column:palette): 37: 0:0 2:6 4:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0 -41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0 +40: 0:0 2:10 9:2 20:10 21:2 34:10 37:2 44:10 49:2 118:0 +41: 0:0 2:10 3:2 12:10 13:2 24:10 27:2 35:10 36:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_8.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_8.snap index 28745da194..122ec68267 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_8.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_dark_consumer_8.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,8 +50,9 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(165, 165, 165)|Reset|Reset|NONE -3: Cyan|Reset|Reset|BOLD +3: Rgb(99, 168, 248)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM +5: Rgb(230, 230, 230)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 @@ -94,5 +94,5 @@ Rows (column:palette): 37: 0:0 38: 0:0 39: 0:0 -40: 0:0 2:1 9:2 20:1 21:2 118:0 -41: 0:0 2:1 3:2 11:1 12:2 23:1 26:2 34:1 35:2 118:0 +40: 0:0 2:5 9:2 20:5 21:2 118:0 +41: 0:0 2:5 3:2 11:5 12:2 23:5 26:2 34:5 35:2 118:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_1.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_1.snap index 1f63635aea..9861d8e10a 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_1.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_1.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,13 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: Cyan|Reset|Reset|NONE -8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED -9: Black|Reset|Reset|BOLD +8: Rgb(28, 100, 200)|Reset|Reset|BOLD | UNDERLINED +9: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_2.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_2.snap index a15617282c..7e6cd04148 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_2.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_2.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,13 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: Cyan|Reset|Reset|NONE -8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED -9: Black|Reset|Reset|BOLD +8: Rgb(28, 100, 200)|Reset|Reset|BOLD | UNDERLINED +9: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_3.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_3.snap index 49269593e2..a28457a2cd 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_3.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_3.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,13 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: Cyan|Reset|Reset|NONE -8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED -9: Black|Reset|Reset|BOLD +8: Rgb(28, 100, 200)|Reset|Reset|BOLD | UNDERLINED +9: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_4.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_4.snap index 8662eb5e28..e8858d06c5 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_4.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_4.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,9 +50,9 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM -5: Black|Reset|Reset|BOLD +5: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_6.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_6.snap index 9f093b8d45..9567ca60cb 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_6.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_6.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,13 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: Cyan|Reset|Reset|NONE -8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED -9: Black|Reset|Reset|BOLD +8: Rgb(28, 100, 200)|Reset|Reset|BOLD | UNDERLINED +9: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_8.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_8.snap index ca433ec2fa..fce5f75cf4 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_8.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_business_8.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,9 +50,9 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM -5: Black|Reset|Reset|BOLD +5: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_1.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_1.snap index e645f00b8c..c200c6da5d 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_1.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_1.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,13 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: Cyan|Reset|Reset|NONE -8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED -9: Black|Reset|Reset|BOLD +8: Rgb(28, 100, 200)|Reset|Reset|BOLD | UNDERLINED +9: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_2.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_2.snap index 1ce6707eae..4503d2ceb0 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_2.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_2.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,13 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: Cyan|Reset|Reset|NONE -8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED -9: Black|Reset|Reset|BOLD +8: Rgb(28, 100, 200)|Reset|Reset|BOLD | UNDERLINED +9: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_4.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_4.snap index 7ad65de1c7..fadeb43c44 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_4.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_4.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,9 +50,9 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM -5: Black|Reset|Reset|BOLD +5: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_5.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_5.snap index c3bc5b7441..12f5d44320 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_5.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_5.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,13 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: Cyan|Reset|Reset|NONE -8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED -9: Black|Reset|Reset|BOLD +8: Rgb(28, 100, 200)|Reset|Reset|BOLD | UNDERLINED +9: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_6.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_6.snap index 1637106ca7..71f2b5f8eb 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_6.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_6.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,13 +50,13 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM 5: Green|Reset|Reset|NONE 6: Magenta|Reset|Reset|NONE 7: Cyan|Reset|Reset|NONE -8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED -9: Black|Reset|Reset|BOLD +8: Rgb(28, 100, 200)|Reset|Reset|BOLD | UNDERLINED +9: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 2:3 6:0 11:2 34:0 diff --git a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_8.snap b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_8.snap index 11182eb8a3..ef848bd058 100644 --- a/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_8.snap +++ b/codex-rs/tui/src/analytics/snapshots/codex_tui__analytics__tests__terminal_styles__analytics_light_consumer_8.snap @@ -1,6 +1,5 @@ --- source: tui/src/analytics/styles_tests.rs -assertion_line: 145 expression: snapshot --- 120x42 @@ -51,9 +50,9 @@ Palette (foreground|background|underline|modifiers): 0: Reset|Reset|Reset|NONE 1: Reset|Reset|Reset|BOLD 2: Rgb(76, 76, 76)|Reset|Reset|NONE -3: Rgb(0, 95, 135)|Reset|Reset|BOLD +3: Rgb(28, 100, 200)|Reset|Reset|BOLD 4: Reset|Reset|Reset|DIM -5: Black|Reset|Reset|BOLD +5: Rgb(38, 38, 38)|Reset|Reset|BOLD Rows (column:palette): 0: 0:0 2:1 11:2 28:0 1: 0:0 diff --git a/codex-rs/tui/src/app/resize_reflow_tests.rs b/codex-rs/tui/src/app/resize_reflow_tests.rs index 56b6d2b81a..e0ff1156d3 100644 --- a/codex-rs/tui/src/app/resize_reflow_tests.rs +++ b/codex-rs/tui/src/app/resize_reflow_tests.rs @@ -164,8 +164,8 @@ async fn capped_resize_reflow_prepends_transcript_notice_without_changing_transc .map(rendered_line_text) .collect::>() .join("\n"), - @r" - Earlier messages are available — press ctrl + t to view the full transcript + @" + Earlier messages are available — press ctrl+t to view the full transcript cell 8 cell 9 @@ -193,10 +193,10 @@ async fn capped_resize_reflow_counts_wrapped_notice_rows() { .map(rendered_line_text) .collect::>() .join("\n"), - @r" + @" Earlier messages are - available — press ctrl + t - to view the full transcript + available — press ctrl+t to + view the full transcript cell 9 cell 10 @@ -243,8 +243,8 @@ async fn paginated_resize_reflow_prepends_transcript_notice_for_unloaded_history .map(rendered_line_text) .collect::>() .join("\n"), - @r" - Earlier messages are available — press ctrl + t to view the full transcript + @" + Earlier messages are available — press ctrl+t to view the full transcript cell 0 cell 1 diff --git a/codex-rs/tui/src/app/side.rs b/codex-rs/tui/src/app/side.rs index d809bed2dc..f4006ba36a 100644 --- a/codex-rs/tui/src/app/side.rs +++ b/codex-rs/tui/src/app/side.rs @@ -281,7 +281,7 @@ impl App { ) { label_parts.push(format!("{} to switch", binding.display_label())); } - label_parts.push("ctrl + c to close".to_string()); + label_parts.push("ctrl+c to close".to_string()); self.chat_widget .set_side_conversation_context_label(Some(format!("Side {}", label_parts.join(" · ")))); } diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index e1b9b516d3..dee16226e2 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -5090,11 +5090,11 @@ async fn side_parent_status_prioritizes_input_over_approval() -> Result<()> { ); assert_snapshot!( format!("{input_footer}\n{approval_footer}\n{cleared_footer}"), - @r" - Side from main thread · main needs input · ctrl + / to switch · ctrl + c to close - Side from main thread · main needs approval · ctrl + / to switch · ctrl + c to close - Side from main thread · ctrl + / to switch · ctrl + c to close - " + @" + Side from main thread · main needs input · ctrl+/ to switch · ctrl+c to close + Side from main thread · main needs approval · ctrl+/ to switch · ctrl+c to close + Side from main thread · ctrl+/ to switch · ctrl+c to close + " ); Ok(()) @@ -6506,8 +6506,7 @@ async fn capped_resize_reflow_renders_recent_suffix_only() { .map(rendered_line_text) .collect::>(), vec![ - "Earlier messages are available — press ctrl + t to view the full transcript" - .to_string(), + "Earlier messages are available — press ctrl+t to view the full transcript".to_string(), String::new(), "cell 18".to_string(), String::new(), diff --git a/codex-rs/tui/src/app/tests/key_chords.rs b/codex-rs/tui/src/app/tests/key_chords.rs index 9b33abd62b..04436675a5 100644 --- a/codex-rs/tui/src/app/tests/key_chords.rs +++ b/codex-rs/tui/src/app/tests/key_chords.rs @@ -102,11 +102,11 @@ async fn completed_global_chord_reuses_the_existing_action_handler() -> Result<( insta::assert_snapshot!( render_bottom_popup(&app.chat_widget, /*width*/ 80) .replace(&test_path_display("/tmp/project"), "/tmp/project"), - @r" - › Ask Codex to do anything + @" + › Ask Codex to do anything - ctrl + x … waiting for next key esc cancel - " + ctrl+x … waiting for next key · esc cancel + " ); press(&mut app, &mut tui, &mut app_server, ctrl('t')).await?; @@ -597,7 +597,7 @@ async fn dashboard_chord_hint_survives_refresh_and_clears_on_cancel() -> Result< let _ = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None); insta::assert_snapshot!( render_bottom_popup(&app.chat_widget, /*width*/ 80).lines().last().unwrap(), - @" ctrl + x … waiting for next key esc cancel" + @" ctrl+x … waiting for next key esc cancel" ); assert_eq!( app.route_key_chord_event(&mut tui, KeyCode::Esc.into()), diff --git a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs index f0a3502056..8b752eddd1 100644 --- a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs +++ b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs @@ -4331,15 +4331,15 @@ fn session_lifecycle_avoids_redundant_subagent_metadata_reads() -> Result<()> { render_bottom_popup(&app.chat_widget, /*width*/ 80) .replace(&root_thread_id.to_string(), "[root]") .replace(&child_thread_id.to_string(), "[child]"), - @r###" - Subagents - Select an agent to watch. ⌥ + ← previous, ⌥ + → next. + @" + Subagents + Select an agent to watch. ⌥+← previous, ⌥+→ next. - › 1. • Main [default] (current) [root] - 2. • /root/worker [child] + › 1. • Main [default] (current) [root] + 2. • /root/worker [child] - Press enter to confirm or esc to go back - "### + Press enter to confirm or esc to go back + " ); assert_eq!(take_backfill_counts(&requests), (0, 0)); tokio::time::timeout(Duration::from_secs(5), started_rx).await??; diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__reconnect_failed.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__reconnect_failed.snap index 86c54bfc86..f0ab661ff0 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__reconnect_failed.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__reconnect_failed.snap @@ -7,4 +7,4 @@ expression: "render_bottom_popup(&app.chat_widget, 80)" › Ask Codex to do anything - ctrl+c quit + ctrl+c quit diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__restored_conversation.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__restored_conversation.snap index e6a6142008..3f6cf1fb82 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__restored_conversation.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__restored_conversation.snap @@ -4,7 +4,7 @@ expression: "render_bottom_popup(&app.chat_widget, 80)" --- • Queued follow-up inputs ↳ old queued input - ⌥ + ↑ edit last queued message + ⌥+↑ edit last queued message › kept draft diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__buffered_replay__misalignment_replayed_input.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__buffered_replay__misalignment_replayed_input.snap index 02cd67bd44..62c4ad734a 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__buffered_replay__misalignment_replayed_input.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__buffered_replay__misalignment_replayed_input.snap @@ -4,7 +4,7 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&app.chat_widget, 80)) --- • Queued follow-up inputs ↳ keep queued - ⌥ + ↑ edit last queued message + ⌥+↑ edit last queued message › keep draft diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__disconnect__offline_draft.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__disconnect__offline_draft.snap index 4722388cf8..6313aaf5c7 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__disconnect__offline_draft.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__disconnect__offline_draft.snap @@ -7,8 +7,8 @@ expression: "format!(\"{}\\n{}\",\nlines_to_single_string(&app.transcript_cells. • Queued follow-up inputs ↳ queued before disconnect - ⌥ + ↑ edit last queued message + ⌥+↑ edit last queued message › keep this draft! - ctrl+c quit + ctrl+c quit diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__pagination_completion_tests__older_pagination_completion_footers_follow_answers_without_overlap_duplicates.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__pagination_completion_tests__older_pagination_completion_footers_follow_answers_without_overlap_duplicates.snap index 5aa6a8f8db..eedea6813b 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__pagination_completion_tests__older_pagination_completion_footers_follow_answers_without_overlap_duplicates.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__pagination_completion_tests__older_pagination_completion_footers_follow_answers_without_overlap_duplicates.snap @@ -2,7 +2,7 @@ source: tui/src/app/tests/pagination_completion_tests.rs expression: "snapshots.join(\"\\n\\n--- next overlapping page ---\\n\\n\")" --- -Earlier messages are available — press ctrl + t to view the full transcript +Earlier messages are available — press ctrl+t to view the full transcript • Middle answer Worked for 2m 5s · Sep 6, 2000 at 2:33 PM diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_warnings_tests__mcp_startup_summary.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_warnings_tests__mcp_startup_summary.snap index 6b5d082fa8..50fa1e3bf0 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_warnings_tests__mcp_startup_summary.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_warnings_tests__mcp_startup_summary.snap @@ -3,5 +3,5 @@ source: tui/src/app/tests/startup_warnings_tests.rs expression: warnings.display_lines(100) --- [ - Line::from(Span::from("⚠ 3 MCP startup issues (2 need sign-in) · ctrl + t for details").yellow().dim()), + Line::from(Span::from("⚠ 3 MCP startup issues (2 need sign-in) · ctrl+t for details").yellow().dim()), ] diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_warnings_tests__mixed_startup_summary.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_warnings_tests__mixed_startup_summary.snap index 9576e9ffae..09f5f9c9a1 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_warnings_tests__mixed_startup_summary.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_warnings_tests__mixed_startup_summary.snap @@ -3,5 +3,5 @@ source: tui/src/app/tests/startup_warnings_tests.rs expression: "app.transcript_cells[1].display_lines(100)" --- [ - Line::from(Span::from("⚠ 4 startup issues (3 MCP; 2 need sign-in) · ctrl + t for details").yellow().dim()), + Line::from(Span::from("⚠ 4 startup issues (3 MCP; 2 need sign-in) · ctrl+t for details").yellow().dim()), ] diff --git a/codex-rs/tui/src/app/tests/startup_warnings_tests.rs b/codex-rs/tui/src/app/tests/startup_warnings_tests.rs index 9f76d19342..31f9914acd 100644 --- a/codex-rs/tui/src/app/tests/startup_warnings_tests.rs +++ b/codex-rs/tui/src/app/tests/startup_warnings_tests.rs @@ -142,7 +142,7 @@ async fn startup_warnings_wait_for_splash_and_coalesce_with_full_details() -> Re let transcript = app.transcript_cells[1].transcript_lines(/*width*/ 80); insta::assert_snapshot!(format!("display:\n{}\n\ntranscript:\n{}", lines_to_single_string(&display), lines_to_single_string(&transcript)), @" display: - ⚠ 2 startup issues · ctrl + t for details + ⚠ 2 startup issues · ctrl+t for details transcript: ⚠ Skill manifest is invalid. diff --git a/codex-rs/tui/src/bottom_pane/async_questions/render.rs b/codex-rs/tui/src/bottom_pane/async_questions/render.rs index 59415de15c..d65cb21d5c 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/render.rs +++ b/codex-rs/tui/src/bottom_pane/async_questions/render.rs @@ -188,25 +188,19 @@ impl AsyncQuestions { if self.keymap.chat.interrupt_turn.contains(&binding) || self.keymap.chat.edit_queued_message.contains(&binding))) { - tips.push( - Span::styled( - format!("{} submit", key.display_label()), - crate::style::accent_style(), - ) - .bold(), - ); + tips.push(crate::footer_hint::shortcut(&key.display_label(), "submit")); } if let Some(key) = chat_hint("skip_question") { - tips.push(format!("{} skip", key.display_label()).dim()); + tips.push(crate::footer_hint::shortcut(&key.display_label(), "skip")); } - tips.extend(option_tip); + tips.extend(option_tip.map(Line::from)); if let Some(key) = chat_hint("prompt_stack_back") { let label = if self.state.current_idx > 0 { "prev question" } else { "main prompt" }; - tips.push(format!("{} {label}", key.display_label()).dim()); + tips.push(crate::footer_hint::shortcut(&key.display_label(), label)); } let next = if self.state.current_idx + 1 < self.state.pending.len() { Some("next question") @@ -218,7 +212,7 @@ impl AsyncQuestions { if let Some(label) = next && let Some(key) = self.next_hint { - tips.push(format!("{} {label}", key.display_label()).dim()); + tips.push(crate::footer_hint::shortcut(&key.display_label(), label)); } let mut lines = Vec::new(); let mut line = Line::default(); @@ -230,7 +224,7 @@ impl AsyncQuestions { line.spans.push(TIP_SEPARATOR.into()); } } - line.spans.push(tip); + line.spans.extend(tip.spans); } lines.push(line); lines diff --git a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_capped_other.snap b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_capped_other.snap index 6e0a26e293..09e5783d7b 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_capped_other.snap +++ b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_capped_other.snap @@ -16,5 +16,4 @@ expression: buffer_text(&buffer) long draft - enter submit ctrl + ] skip - ⌥ + ↓ main prompt + enter submit ctrl+] skip ⌥+↓ main prompt diff --git a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_inline_history_search.snap b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_inline_history_search.snap index 58e4bce628..6b2f5d253a 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_inline_history_search.snap +++ b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_inline_history_search.snap @@ -11,5 +11,5 @@ expression: buffer_text(&buffer) › 13. earlier answer reverse-i-search: e enter accept · esc - enter submit ctrl + ] skip option 13/13 - ⌥ + ↓ prev question + enter submit ctrl+] skip option 13/13 + ⌥+↓ prev question diff --git a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_named_other.snap b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_named_other.snap index bceeebde78..20706d1bfe 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_named_other.snap +++ b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_named_other.snap @@ -9,5 +9,4 @@ expression: buffer_text(&buffer) › 1. Other 2. Other (write an answer) - enter submit ctrl + ] skip - ⌥ + ↓ prev question + enter submit ctrl+] skip ⌥+↓ prev question diff --git a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_remapped_choice_footer.snap b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_remapped_choice_footer.snap index f4a07f722a..9f8bb1483e 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_remapped_choice_footer.snap +++ b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_remapped_choice_footer.snap @@ -11,5 +11,5 @@ expression: buffer_text(&buffer) 2. Option 2 is long enough to wrap across rows - f9 submit ctrl + ] skip option 1/13 - ⌥ + ↓ prev question + f9 submit ctrl+] skip option 1/13 + ⌥+↓ prev question diff --git a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_named_option.snap b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_named_option.snap index ee4c3d579d..eaeaae1bdd 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_named_option.snap +++ b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_named_option.snap @@ -10,5 +10,5 @@ expression: buffer_text(&buffer) multiple rows 2. Other - enter submit ctrl + ] skip - ⌥ + ↓ main prompt + enter submit ctrl+] skip + ⌥+↓ main prompt diff --git a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_other.snap b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_other.snap index 423d83438e..d99153d310 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_other.snap +++ b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_other.snap @@ -12,5 +12,5 @@ expression: buffer_text(&buffer) wraps across several option rows - enter submit ctrl + ] skip - ⌥ + ↓ main prompt + enter submit ctrl+] skip + ⌥+↓ main prompt diff --git a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_selected_option.snap b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_selected_option.snap index 5b1c3c3609..48245c4716 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_selected_option.snap +++ b/codex-rs/tui/src/bottom_pane/async_questions/snapshots/codex_tui__bottom_pane__async_questions__tests__question_wrapped_selected_option.snap @@ -9,5 +9,5 @@ expression: buffer_text(&clipped) long enough to wrap across multiple rows - enter submit ctrl + ] skip - option 2/3 ⌥ + ↓ main prompt + enter submit ctrl+] skip + option 2/3 ⌥+↓ main prompt diff --git a/codex-rs/tui/src/bottom_pane/async_questions/state_tests.rs b/codex-rs/tui/src/bottom_pane/async_questions/state_tests.rs index 354a260127..12f3c35fe6 100644 --- a/codex-rs/tui/src/bottom_pane/async_questions/state_tests.rs +++ b/codex-rs/tui/src/bottom_pane/async_questions/state_tests.rs @@ -371,7 +371,7 @@ fn existing_cross_context_keymaps_load_without_misleading_submit_hints() { let mut editor = editor(); editor.navigate(/*forward*/ true); editor.set_keymap(&keymap); - insta::allow_duplicates! { insta::assert_snapshot!(editor.footer_lines(/*width*/ 100, /*option_tip*/ None)[0].to_string(), @"ctrl + ] skip ⌥ + ↓ prev question"); } + insta::allow_duplicates! { insta::assert_snapshot!(editor.footer_lines(/*width*/ 100, /*option_tip*/ None)[0].to_string(), @"ctrl+] skip ⌥+↓ prev question"); } editor.handle_key_event(KeyEvent::from(KeyCode::F(12))); assert!(editor.submission.is_none()); } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs b/codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs index 5ce24e7392..528a75c4a7 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs @@ -407,7 +407,7 @@ impl ChatComposer { } fn history_search_action_key_span(key: KeyCode) -> Span<'static> { - Span::from(key_hint::plain(key)).cyan().bold().not_dim() + Span::from(key_hint::plain(key)) } /// Returns byte ranges that should be highlighted in the current composer preview. @@ -900,63 +900,79 @@ mod tests { #[test] fn history_search_footer_action_hints_are_emphasized() { - let (tx, _rx) = unbounded_channel::(); - let sender = AppEventSender::new(tx); - let mut composer = ChatComposer::new( - /*has_input_focus*/ true, - sender, - /*enhanced_keys_supported*/ true, - "Ask Codex to do anything".to_string(), - /*disable_paste_burst*/ false, + crate::terminal_palette::with_test_default_colors( + crate::terminal_probe::DefaultColors { + fg: (240, 240, 240), + bg: (24, 24, 24), + }, + || { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + /*has_input_focus*/ true, + sender, + /*enhanced_keys_supported*/ true, + "Ask Codex to do anything".to_string(), + /*disable_paste_burst*/ false, + ); + composer + .history + .record_local_submission(HistoryEntry::new("cargo test".to_string())); + + let _ = composer + .handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)); + let _ = composer + .handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + + let line = composer + .history_search_footer_line() + .expect("expected history search footer line"); + assert_eq!( + line.spans + .iter() + .map(|span| span.content.as_ref()) + .collect::>(), + vec![ + "reverse-i-search: ", + "c", + " ", + "enter", + " accept", + " · ", + "esc", + " cancel" + ] + ); + + let query_style = line.spans[1].style; + assert_eq!(query_style.fg, Some(ratatui::style::Color::Cyan)); + + let enter_style = line.spans[3].style; + assert_eq!( + enter_style.fg, + Some(crate::terminal_palette::rgb_color((240, 240, 240))) + ); + assert!(enter_style.add_modifier.contains(Modifier::BOLD)); + assert!(enter_style.sub_modifier.contains(Modifier::DIM)); + + let accept_style = line.spans[4].style; + assert!(accept_style.add_modifier.contains(Modifier::DIM)); + + let separator_style = line.spans[5].style; + assert!(separator_style.add_modifier.contains(Modifier::DIM)); + + let esc_style = line.spans[6].style; + assert_eq!( + esc_style.fg, + Some(crate::terminal_palette::rgb_color((240, 240, 240))) + ); + assert!(esc_style.add_modifier.contains(Modifier::BOLD)); + assert!(esc_style.sub_modifier.contains(Modifier::DIM)); + + let cancel_style = line.spans[7].style; + assert!(cancel_style.add_modifier.contains(Modifier::DIM)); + }, ); - composer - .history - .record_local_submission(HistoryEntry::new("cargo test".to_string())); - - let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)); - let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); - - let line = composer - .history_search_footer_line() - .expect("expected history search footer line"); - assert_eq!( - line.spans - .iter() - .map(|span| span.content.as_ref()) - .collect::>(), - vec![ - "reverse-i-search: ", - "c", - " ", - "enter", - " accept", - " · ", - "esc", - " cancel" - ] - ); - - let query_style = line.spans[1].style; - assert_eq!(query_style.fg, Some(ratatui::style::Color::Cyan)); - - let enter_style = line.spans[3].style; - assert_eq!(enter_style.fg, Some(ratatui::style::Color::Cyan)); - assert!(enter_style.add_modifier.contains(Modifier::BOLD)); - assert!(enter_style.sub_modifier.contains(Modifier::DIM)); - - let accept_style = line.spans[4].style; - assert!(accept_style.add_modifier.contains(Modifier::DIM)); - - let separator_style = line.spans[5].style; - assert!(separator_style.add_modifier.contains(Modifier::DIM)); - - let esc_style = line.spans[6].style; - assert_eq!(esc_style.fg, Some(ratatui::style::Color::Cyan)); - assert!(esc_style.add_modifier.contains(Modifier::BOLD)); - assert!(esc_style.sub_modifier.contains(Modifier::DIM)); - - let cancel_style = line.spans[7].style; - assert!(cancel_style.add_modifier.contains(Modifier::DIM)); } #[test] diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__shortcut_help_hides_visible_sparkles_without_restarting_their_deadline.snap b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__shortcut_help_hides_visible_sparkles_without_restarting_their_deadline.snap index a9ac149b27..d2048569da 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__shortcut_help_hides_visible_sparkles_without_restarting_their_deadline.snap +++ b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__shortcut_help_hides_visible_sparkles_without_restarting_their_deadline.snap @@ -12,13 +12,13 @@ shortcut help: › Ask Codex to do anything - / for commands ! for shell commands - ctrl + j for newline tab to submit message - @ for file paths ctrl + v to paste images - ctrl + g to edit in external editor esc esc to edit previous message - ctrl + r search history ctrl + c to exit - ⌥ + , reasoning down ⌥ + . reasoning up - ctrl + t to view transcript + / for commands ! for shell commands + ctrl+j for newline tab to submit message + @ for file paths ctrl+v to paste images + ctrl+g to edit in external editor esc esc to edit previous message + ctrl+r search history ctrl+c to exit + ⌥+, reasoning down ⌥+. reasoning up + ctrl+t to view transcript customize shortcuts with /keymap after closing: @@ -37,13 +37,13 @@ shortcut help: › Ask Codex to do anything - / for commands ! for shell commands - ctrl + j for newline tab to submit message - @ for file paths ctrl + v to paste images - ctrl + g to edit in external editor esc esc to edit previous message - ctrl + r search history ctrl + c to exit - ⌥ + , reasoning down ⌥ + . reasoning up - ctrl + t to view transcript + / for commands ! for shell commands + ctrl+j for newline tab to submit message + @ for file paths ctrl+v to paste images + ctrl+g to edit in external editor esc esc to edit previous message + ctrl+r search history ctrl+c to exit + ⌥+, reasoning down ⌥+. reasoning up + ctrl+t to view transcript customize shortcuts with /keymap after closing: diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__shortcut_help_preserves_an_unused_sparkle_but_literal_question_marks_do_not.snap b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__shortcut_help_preserves_an_unused_sparkle_but_literal_question_marks_do_not.snap index 69f211f7e9..5b4cdacb05 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__shortcut_help_preserves_an_unused_sparkle_but_literal_question_marks_do_not.snap +++ b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__shortcut_help_preserves_an_unused_sparkle_but_literal_question_marks_do_not.snap @@ -6,13 +6,13 @@ shortcut help: › Ask Codex to do anything - / for commands ! for shell commands - ctrl + j for newline tab to submit message - @ for file paths ctrl + v to paste images - ctrl + g to edit in external editor esc esc to edit previous message - ctrl + r search history ctrl + c to exit - ⌥ + , reasoning down ⌥ + . reasoning up - ctrl + t to view transcript + / for commands ! for shell commands + ctrl+j for newline tab to submit message + @ for file paths ctrl+v to paste images + ctrl+g to edit in external editor esc esc to edit previous message + ctrl+r search history ctrl+c to exit + ⌥+, reasoning down ⌥+. reasoning up + ctrl+t to view transcript customize shortcuts with /keymap diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 26050f7300..05108a1d1c 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -209,6 +209,7 @@ impl CommandPopup { let name = format!("/{}", item.command()); let description = item.description().to_string(); GenericDisplayRow { + selection_style: None, name, name_prefix_spans: Vec::new(), match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), diff --git a/codex-rs/tui/src/bottom_pane/file_search_popup.rs b/codex-rs/tui/src/bottom_pane/file_search_popup.rs index fe903a0b37..0982826470 100644 --- a/codex-rs/tui/src/bottom_pane/file_search_popup.rs +++ b/codex-rs/tui/src/bottom_pane/file_search_popup.rs @@ -118,6 +118,7 @@ impl WidgetRef for &FileSearchPopup { self.matches .iter() .map(|m| GenericDisplayRow { + selection_style: None, name: m.path.to_string_lossy().to_string(), name_prefix_spans: Vec::new(), match_indices: m diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index d5cbd09067..64f97630b8 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -46,10 +46,12 @@ use crate::key_hint::KeyBinding; use crate::key_hint::ShortcutHint; use crate::render::line_utils::prefix_lines; use crate::status::format_tokens_compact; +use crate::style::secondary_text_style; use crate::ui_consts::FOOTER_INDENT_COLS; use crossterm::event::KeyCode; use ratatui::buffer::Buffer; use ratatui::layout::Rect; +use ratatui::style::Styled; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; @@ -149,11 +151,14 @@ impl CollaborationModeIndicator { } } - fn styled_span(self, show_cycle_hint: bool) -> Span<'static> { - let label = self.label(show_cycle_hint); - match self { - CollaborationModeIndicator::Plan => Span::from(label).magenta(), + fn styled_line(self, show_cycle_hint: bool) -> Line<'static> { + let mut line = Line::from(self.label(/*show_cycle_hint*/ false).magenta()); + if show_cycle_hint { + line.push_span(" (".set_style(secondary_text_style())); + line.extend(key_hint::shift(KeyCode::Tab).spans()); + line.push_span(" to cycle)".set_style(secondary_text_style())); } + line } } @@ -316,36 +321,40 @@ fn left_side_line( SummaryHintKind::None => {} SummaryHintKind::Shortcuts => { if let Some(key) = key_hints.agents { - line.push_span(key); - line.push_span(" for agents".dim()); + line.extend(key.spans()); + line.push_span(" for agents".set_style(secondary_text_style())); if key_hints.toggle_shortcuts.is_some() { - line.push_span(" · ".dim()); + line.push_span(" · ".set_style(secondary_text_style())); } } if let Some(key) = key_hints.toggle_shortcuts { - line.push_span(key); - line.push_span(" for shortcuts".dim()); + line.extend(key.spans()); + line.push_span(" for shortcuts".set_style(secondary_text_style())); } } SummaryHintKind::QueueMessage => { if let Some(key) = key_hints.queue { - line.push_span(key); - line.push_span(" to queue message".dim()); + line.extend(key.spans()); + line.push_span(" to queue message".set_style(secondary_text_style())); } } SummaryHintKind::QueueShort => { if let Some(key) = key_hints.queue { - line.push_span(key); - line.push_span(" to queue".dim()); + line.extend(key.spans()); + line.push_span(" to queue".set_style(secondary_text_style())); } } }; if let Some(collaboration_mode_indicator) = collaboration_mode_indicator { if !matches!(state.hint, SummaryHintKind::None) { - line.push_span(" · ".dim()); + line.push_span(" · ".set_style(secondary_text_style())); } - line.push_span(collaboration_mode_indicator.styled_span(state.show_cycle_hint)); + line.extend( + collaboration_mode_indicator + .styled_line(state.show_cycle_hint) + .spans, + ); } line @@ -547,7 +556,7 @@ pub(crate) fn mode_indicator_line( indicator: Option, show_cycle_hint: bool, ) -> Option> { - indicator.map(|indicator| Line::from(vec![indicator.styled_span(show_cycle_hint)])) + indicator.map(|indicator| indicator.styled_line(show_cycle_hint)) } pub(crate) fn goal_status_indicator_line( @@ -600,7 +609,7 @@ pub(crate) fn status_line_right_indicator_line( .flatten() { if let Some(line) = line.as_mut() { - line.push_span(" · ".dim()); + line.push_span(" · ".set_style(secondary_text_style())); for span in indicator.spans { line.push_span(span); } @@ -613,11 +622,28 @@ pub(crate) fn status_line_right_indicator_line( } pub(crate) fn side_conversation_context_line(label: &str) -> Line<'static> { - if let Some(rest) = label.strip_prefix("Side ") { - Line::from(vec!["Side".magenta().bold(), format!(" {rest}").magenta()]) + let mut line = Line::default(); + let rest = if let Some(rest) = label.strip_prefix("Side ") { + line.extend(["Side".magenta().bold(), " ".into()]); + rest } else { - Line::from(vec![Span::from(label.to_string()).magenta()]) + label + }; + for (index, part) in rest.split(" · ").enumerate() { + if index > 0 { + line.push_span(" · ".set_style(secondary_text_style())); + } + if let Some((keys, action)) = [" to switch", " to close", " for side"] + .into_iter() + .find_map(|action| part.strip_suffix(action).map(|keys| (keys, action))) + { + line.extend(key_hint::key_label_spans(keys)); + line.push_span(action.set_style(secondary_text_style())); + } else { + line.push_span(part.to_owned().magenta()); + } } + line } fn right_aligned_x(area: Rect, content_width: u16) -> Option { @@ -736,7 +762,9 @@ fn footer_from_props_lines( FooterMode::QuitShortcutReminder => { vec![quit_shortcut_reminder_line(props.quit_shortcut_key)] } - FooterMode::HistorySearch => vec![Line::from("reverse-i-search: ").dim()], + FooterMode::HistorySearch => { + vec![Line::from("reverse-i-search: ").set_style(secondary_text_style())] + } FooterMode::ComposerEmpty => { let state = LeftSideState { hint: if show_shortcuts_hint { @@ -803,10 +831,12 @@ pub(crate) fn passive_footer_status_line(props: &FooterProps) -> Option Option u16 { pub(crate) fn footer_hint_items_line(items: &[(String, String)]) -> Line<'static> { let mut spans = Vec::with_capacity(items.len() * 4); for (idx, (key, label)) in items.iter().enumerate() { - spans.push(" ".into()); - spans.push(key.clone().bold()); + if idx > 0 { + spans.push(" · ".set_style(secondary_text_style())); + } + spans.extend(key_hint::key_label_spans(key)); if idx == 0 && key == "voice" && let Some(label) = label.strip_prefix("● ") { spans.push(" ●".red()); - spans.push(format!(" {label}").into()); + spans.push(format!(" {label}").set_style(secondary_text_style())); } else { - spans.push(format!(" {label}").into()); - } - if idx + 1 != items.len() { - spans.push((if items[0].0 == "voice" { " " } else { " " }).into()); + spans.push(format!(" {label}").set_style(secondary_text_style())); } } Line::from(spans) @@ -903,21 +934,23 @@ struct ShortcutsState { } fn quit_shortcut_reminder_line(key: KeyBinding) -> Line<'static> { - Line::from(vec![key.into(), " again to quit".into()]).dim() + let mut line = Line::from(key.spans()); + line.push_span(" again to quit".set_style(secondary_text_style())); + line } fn esc_hint_line(esc_backtrack_hint: bool) -> Line<'static> { let esc = key_hint::plain(KeyCode::Esc); if esc_backtrack_hint { - Line::from(vec![esc.into(), " again to edit previous message".into()]).dim() + let mut line = Line::from(esc.spans()); + line.push_span(" again to edit previous message".set_style(secondary_text_style())); + line } else { - Line::from(vec![ - esc.into(), - " ".into(), - esc.into(), - " to edit previous message".into(), - ]) - .dim() + let mut line = Line::from(esc.spans()); + line.push_span(" ".set_style(secondary_text_style())); + line.extend(esc.spans()); + line.push_span(" to edit previous message".set_style(secondary_text_style())); + line } } @@ -979,14 +1012,20 @@ fn shortcut_overlay_lines(state: ShortcutsState) -> Vec> { let mut lines = build_columns(ordered); if let Some(key) = state.key_hints.agents { lines.push(Line::from(vec![ - key.into(), - " for agents (empty prompt)".into(), + key.display_label() + .fg(crate::style::accent_color_on(/*background*/ None)) + .not_bold() + .not_dim(), + " for agents (empty prompt)".set_style(secondary_text_style()), ])); } lines.push(Line::from("")); lines.push(Line::from(vec![ - "customize shortcuts with ".into(), - "/keymap".cyan(), + "customize shortcuts with ".set_style(secondary_text_style()), + "/keymap" + .fg(crate::style::accent_color_on(/*background*/ None)) + .not_bold() + .not_dim(), ])); lines } @@ -1033,7 +1072,7 @@ fn build_columns(entries: Vec>) -> Vec> { line.push_span(Span::from(" ".repeat(padding))); } } - line.dim() + line.set_style(secondary_text_style()) }) .collect() } @@ -1041,15 +1080,21 @@ fn build_columns(entries: Vec>) -> Vec> { pub(crate) fn context_window_line(percent: Option, used_tokens: Option) -> Line<'static> { if let Some(percent) = percent { let percent = percent.clamp(0, 100); - return Line::from(vec![Span::from(format!("{percent}% context left")).dim()]); + return Line::from(vec![ + Span::from(format!("{percent}% context left")).set_style(secondary_text_style()), + ]); } if let Some(tokens) = used_tokens { let used_fmt = format_tokens_compact(tokens); - return Line::from(vec![Span::from(format!("{used_fmt} used")).dim()]); + return Line::from(vec![ + Span::from(format!("{used_fmt} used")).set_style(secondary_text_style()), + ]); } - Line::from(vec![Span::from("100% context left").dim()]) + Line::from(vec![ + Span::from("100% context left").set_style(secondary_text_style()), + ]) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -1134,7 +1179,12 @@ impl ShortcutDescriptor { .binding_for(state) .map(|binding| ShortcutHint::Single(binding.key)), }?; - let mut line = Line::from(vec![self.prefix.into(), key.into()]); + let reference_key = key + .display_label() + .fg(crate::style::accent_color_on(/*background*/ None)) + .not_bold() + .not_dim(); + let mut line = Line::from(vec![self.prefix.into(), reference_key.clone()]); match self.id { ShortcutId::QueueMessageTab => { if state.is_task_running || state.queue_submissions { @@ -1149,7 +1199,7 @@ impl ShortcutDescriptor { } else { line.extend(vec![ " ".into(), - key.into(), + reference_key, " to edit previous message".into(), ]); } @@ -1310,6 +1360,10 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[ }, ]; +#[cfg(test)] +#[path = "footer_typography_tests.rs"] +mod typography_tests; + #[cfg(test)] mod tests { use super::*; @@ -1324,7 +1378,7 @@ mod tests { #[test] fn voice_live_microphone_indicator_is_red() { let line = footer_hint_items_line(&[("voice".into(), "● listen".into())]); - assert_eq!(line.spans[2].style.fg, Some(ratatui::style::Color::Red)); + assert_eq!(line.spans[1].style.fg, Some(ratatui::style::Color::Red)); } #[test] diff --git a/codex-rs/tui/src/bottom_pane/footer_typography_tests.rs b/codex-rs/tui/src/bottom_pane/footer_typography_tests.rs new file mode 100644 index 0000000000..576c673e94 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/footer_typography_tests.rs @@ -0,0 +1,88 @@ +//! Legacy help keeps accent keys while ordinary footer keys clear inherited emphasis. + +use super::*; +use pretty_assertions::assert_eq; +use ratatui::Terminal; +use ratatui::backend::TestBackend; +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; + +#[test] +fn help_reference_and_footer_preserve_distinct_key_styles() { + crate::terminal_palette::with_test_default_colors( + crate::terminal_probe::DefaultColors { + fg: (32, 32, 32), + bg: (255, 255, 255), + }, + || { + let help = shortcut_overlay_lines(ShortcutsState { + use_shift_enter_hint: false, + esc_backtrack_hint: false, + is_task_running: false, + queue_submissions: false, + is_wsl: false, + collaboration_modes_enabled: false, + key_hints: FooterKeyHints::default_bindings(), + }); + let help_height = help.len() as u16; + let footer = footer_hint_items_line(&[ + ("ctrl+o".into(), "then".into()), + ("o".into(), "copy".into()), + ]); + let mut terminal = Terminal::new(TestBackend::new( + /*width*/ 80, + /*height*/ help_height + 1, + )) + .unwrap(); + terminal + .draw(|frame| { + let inherited = Style::default().fg(Color::Green).bold().dim(); + frame.render_widget( + Paragraph::new(help).style(inherited), + Rect::new(/*x*/ 0, /*y*/ 0, /*width*/ 80, help_height), + ); + frame.render_widget( + Paragraph::new(footer).style(inherited), + Rect::new( + /*x*/ 0, + help_height, + /*width*/ 80, + /*height*/ 1, + ), + ); + }) + .unwrap(); + let buffer = terminal.backend().buffer(); + let styles = [ + (0, 0), + (2, 0), + (0, help_height), + (4, help_height), + (7, help_height), + ] + .map(|position| { + let cell = &buffer[position]; + (cell.fg, cell.modifier) + }); + let key_color = key_hint::ctrl(KeyCode::Char('o')).spans()[0] + .style + .fg + .unwrap(); + let secondary_color = secondary_text_style().fg.unwrap(); + assert_eq!( + styles, + [ + ( + crate::style::accent_color_on(/*background*/ None), + Modifier::empty() + ), + (secondary_color, Modifier::empty()), + (key_color, Modifier::BOLD), + (key_color, Modifier::BOLD), + (secondary_color, Modifier::empty()), + ] + ); + }, + ); +} diff --git a/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs b/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs index 88318fb08a..1729e4b611 100644 --- a/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs +++ b/codex-rs/tui/src/bottom_pane/hooks_browser_view.rs @@ -1093,16 +1093,7 @@ mod tests { "hooks_browser_events_with_review_column", render_lines(&view, /*width*/ 112) ); - assert_eq!( - view.event_table_lines()[1].spans[3].style.fg, - Some(Color::Cyan) - ); - assert!( - view.event_table_lines()[1].spans[3] - .style - .add_modifier - .contains(ratatui::style::Modifier::BOLD) - ); + assert_eq!(view.event_table_lines()[1].spans[3].style, accent_style()); } #[test] diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index 57c3e8793e..47298afe5e 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -1,3 +1,6 @@ +//! Render selectable lists while existing callers retain the legacy presentation. +//! Picker appearance opts into stable controls, full-width selection, and separate overflow rows. + use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -11,6 +14,10 @@ use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; +use ratatui::widgets::Wrap; + +use super::selection_picker_layout::PickerLayoutSizes; +use super::selection_picker_layout::picker_areas; use super::selection_popup_common::render_menu_surface; use super::selection_popup_common::wrap_styled_line; @@ -21,12 +28,17 @@ use crate::key_hint::KeyBindingListExt; use crate::key_hint::ShortcutHint; use crate::key_hint::is_plain_text_key_event; use crate::keymap::ListKeymap; +use crate::render::Insets; +use crate::render::RectExt as _; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; use super::CancellationEvent; use super::bottom_pane_view::BottomPaneView; use super::bottom_pane_view::ViewCompletion; +pub(crate) use super::picker_style::PickerSurface; +use super::picker_style::render_scroll_indicators; +use super::picker_style::selection_style; use super::popup_consts::MAX_POPUP_ROWS; use super::scroll_state::ScrollState; use super::selection_popup_common::ColumnWidthConfig; @@ -37,6 +49,7 @@ use super::selection_popup_common::render_rows_single_line_with_col_width_mode; use super::selection_popup_common::render_rows_with_col_width_mode; pub(crate) use super::selection_row_layout::SelectionDescriptionLayout; use super::selection_tabs::SelectionTab; +use super::selection_tabs::TabAppearance; use super::selection_tabs::render_tab_bar; use super::selection_tabs::tab_bar_height; use unicode_width::UnicodeWidthStr; @@ -105,6 +118,15 @@ pub(crate) enum SelectionRowDisplay { SingleLine, } +/// Visual treatment is opt-in while existing workflows retain their current layout. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) enum SelectionAppearance { + #[default] + Legacy, + /// Full-width focus and separate overflow rows on a terminal or panel surface. + Picker, +} + /// One selectable item in the generic selection list. pub(crate) type SelectionAction = Box; @@ -180,6 +202,12 @@ pub(crate) struct SelectionItem { /// `description_layout` optionally moves descriptions below labels when their /// column would become too narrow. pub(crate) struct SelectionViewParams { + pub appearance: SelectionAppearance, + pub picker_surface: PickerSurface, + /// Upper row budget; compact completion menus retain the default of eight. + pub max_visible_rows: usize, + /// Keep the result area at its configured row budget when filtering shrinks it. + pub reserve_result_rows: bool, pub view_id: Option<&'static str>, pub title: Option, pub subtitle: Option, @@ -234,6 +262,10 @@ pub(crate) struct SelectionViewParams { impl Default for SelectionViewParams { fn default() -> Self { Self { + appearance: SelectionAppearance::default(), + picker_surface: PickerSurface::default(), + max_visible_rows: MAX_POPUP_ROWS, + reserve_result_rows: false, view_id: None, title: None, subtitle: None, @@ -270,6 +302,10 @@ impl Default for SelectionViewParams { /// visible rows and source items and for preserving selection while filters /// change. pub(crate) struct ListSelectionView { + appearance: SelectionAppearance, + picker_surface: PickerSurface, + max_visible_rows: usize, + reserve_result_rows: bool, view_id: Option<&'static str>, footer_note: Option>, footer_hint: Option>, @@ -384,11 +420,20 @@ impl ListSelectionView { if params.title.is_some() || params.subtitle.is_some() { let title = params.title.map(|title| Line::from(title.bold())); let subtitle = params.subtitle.map(|subtitle| Line::from(subtitle.dim())); - header = Box::new(ColumnRenderable::with([ - header, - Box::new(title), - Box::new(subtitle), - ])); + header = match params.appearance { + SelectionAppearance::Legacy => Box::new(ColumnRenderable::with([ + header, + Box::new(title), + Box::new(subtitle), + ])), + SelectionAppearance::Picker => Box::new(ColumnRenderable::with([ + header, + Box::new( + Paragraph::new(title.into_iter().chain(subtitle).collect::>()) + .wrap(Wrap { trim: false }), + ), + ])), + }; } let active_tab_idx = params.initial_tab_id.as_ref().and_then(|initial_tab_id| { params @@ -403,6 +448,10 @@ impl ListSelectionView { }; let has_initial_selected_idx = params.initial_selected_idx.is_some(); let mut s = Self { + appearance: params.appearance, + picker_surface: params.picker_surface, + max_visible_rows: params.max_visible_rows.max(/*other*/ 1), + reserve_result_rows: params.reserve_result_rows, view_id: params.view_id, footer_note: params.footer_note, footer_hint: params.footer_hint, @@ -472,13 +521,31 @@ impl ListSelectionView { self.items.as_mut_slice() } - fn active_header(&self) -> &dyn Renderable { + fn active_header(&self, width: u16) -> &dyn Renderable { self.active_tab_idx .and_then(|idx| self.tabs.get(idx)) + .filter(|tab| { + self.appearance == SelectionAppearance::Legacy + || tab.header.desired_height(width) > 0 + }) .map(|tab| tab.header.as_ref()) .unwrap_or(self.header.as_ref()) } + fn header_height(&self, width: u16) -> u16 { + let active_height = self.active_header(width).desired_height(width); + if self.appearance == SelectionAppearance::Picker && self.reserve_result_rows { + self.tabs + .iter() + .map(|tab| tab.header.desired_height(width)) + .max() + .unwrap_or(active_height) + .max(active_height) + } else { + active_height + } + } + fn active_footer_hint(&self) -> Option<&Line<'static>> { if let Some(action) = self .selected_actual_idx() @@ -502,8 +569,14 @@ impl ListSelectionView { .map(|tab| tab.id.as_str()) } - fn max_visible_rows(len: usize) -> usize { - MAX_POPUP_ROWS.min(len.max(1)) + fn visible_rows(&self, len: usize) -> usize { + let rendered = self.rendered_item_count.get(); + let limit = if self.appearance == SelectionAppearance::Picker && rendered > 0 { + self.max_visible_rows.min(rendered) + } else { + self.max_visible_rows + }; + limit.min(len.max(/*other*/ 1)) } fn selected_actual_idx(&self) -> Option { @@ -572,7 +645,7 @@ impl ListSelectionView { .or_else(|| self.first_enabled_visible_idx()) .or_else(|| (len > 0).then_some(0)); - let visible = Self::max_visible_rows(len); + let visible = self.visible_rows(len); self.state.clamp_selection(len); self.state.ensure_visible(len, visible); @@ -598,6 +671,8 @@ impl ListSelectionView { .to_string() .len(); let mut enabled_row_number = 0; + let row_selection_style = + (self.appearance == SelectionAppearance::Picker).then(selection_style); self.filtered_indices .iter() .enumerate() @@ -646,6 +721,7 @@ impl ListSelectionView { .or_else(|| item.description.clone()); let wrap_indent = description.is_none().then_some(wrap_prefix_width); GenericDisplayRow { + selection_style: row_selection_style, name: name_with_marker, name_prefix_spans, display_shortcut: item.display_shortcut, @@ -677,6 +753,9 @@ impl ListSelectionView { }; self.active_tab_idx = Some(next_idx); self.search_query.clear(); + if self.appearance == SelectionAppearance::Picker { + self.rendered_item_count.set(/*val*/ 0); + } self.state.reset(); self.apply_filter(); if self.state.selected_idx.is_none() { @@ -729,7 +808,7 @@ impl ListSelectionView { let before = self.selected_actual_idx(); let len = self.visible_len(); self.state.move_up_wrap(len); - let visible = Self::max_visible_rows(len); + let visible = self.visible_rows(len); self.skip_disabled_up(); self.state.ensure_visible(len, visible); if self.selected_actual_idx() != before { @@ -741,7 +820,7 @@ impl ListSelectionView { let before = self.selected_actual_idx(); let len = self.visible_len(); self.state.move_down_wrap(len); - let visible = Self::max_visible_rows(len); + let visible = self.visible_rows(len); self.skip_disabled_down(); self.state.ensure_visible(len, visible); if self.selected_actual_idx() != before { @@ -752,7 +831,7 @@ impl ListSelectionView { fn page_up(&mut self) { let before = self.selected_actual_idx(); let len = self.visible_len(); - let visible = Self::max_visible_rows(len); + let visible = self.visible_rows(len); self.state.page_up_clamped(len, visible); self.skip_disabled_up_clamped(); self.state.ensure_visible(len, visible); @@ -764,7 +843,7 @@ impl ListSelectionView { fn page_down(&mut self) { let before = self.selected_actual_idx(); let len = self.visible_len(); - let visible = Self::max_visible_rows(len); + let visible = self.visible_rows(len); self.state.page_down_clamped(len, visible); self.skip_disabled_down_clamped(); self.state.ensure_visible(len, visible); @@ -776,7 +855,7 @@ impl ListSelectionView { fn jump_top(&mut self) { let before = self.selected_actual_idx(); let len = self.visible_len(); - let visible = Self::max_visible_rows(len); + let visible = self.visible_rows(len); self.state.jump_top(len, visible); self.skip_disabled_down_clamped(); self.state.ensure_visible(len, visible); @@ -788,7 +867,7 @@ impl ListSelectionView { fn jump_bottom(&mut self) { let before = self.selected_actual_idx(); let len = self.visible_len(); - let visible = Self::max_visible_rows(len); + let visible = self.visible_rows(len); self.state.jump_bottom(len, visible); self.skip_disabled_up_clamped(); self.state.ensure_visible(len, visible); @@ -864,8 +943,52 @@ impl ListSelectionView { self.last_selected_actual_idx.take() } - fn rows_width(total_width: u16) -> u16 { - total_width.saturating_sub(2) + fn tab_appearance(&self) -> TabAppearance { + match self.appearance { + SelectionAppearance::Legacy => TabAppearance::Legacy, + SelectionAppearance::Picker => TabAppearance::Filled, + } + } + + fn footer_hint_lines(&self, width: u16) -> Vec> { + let Some(hint) = self.active_footer_hint() else { + return Vec::new(); + }; + if self.appearance == SelectionAppearance::Legacy { + return vec![hint.clone()]; + } + let mut lines = wrap_styled_line(hint, width); + if self.reserve_result_rows { + let height = self + .tab_footer_hints + .iter() + .map(|(_, hint)| wrap_styled_line(hint, width).len()) + .chain( + self.footer_hint + .iter() + .map(|hint| wrap_styled_line(hint, width).len()), + ) + .max() + .unwrap_or(lines.len()); + lines.resize(height.max(lines.len()), Line::default()); + } + lines + } + + fn result_area_height(&self, measured_height: u16) -> u16 { + if self.appearance == SelectionAppearance::Picker && self.reserve_result_rows { + let reserved_height = self.max_visible_rows.min(usize::from(u16::MAX)) as u16; + measured_height.max(reserved_height) + } else { + measured_height + } + } + + fn rows_width(&self, total_width: u16) -> u16 { + match self.appearance { + SelectionAppearance::Legacy => total_width.saturating_sub(/*rhs*/ 2), + SelectionAppearance::Picker => total_width, + } } fn clear_to_terminal_bg(buf: &mut Buffer, area: Rect) { @@ -1184,12 +1307,13 @@ impl Renderable for ListSelectionView { // When side-by-side is active, measure the list at the reduced width // that accounts for the gap and side panel. let effective_rows_width = if let Some(side_w) = self.side_layout_width(inner_width) { - Self::rows_width(width).saturating_sub(SIDE_CONTENT_GAP + side_w) + // Leave the preview's right inset outside the full-width focus row. + width.saturating_sub(MENU_SURFACE_HORIZONTAL_INSET / 2 + SIDE_CONTENT_GAP + side_w) } else { - Self::rows_width(width) + self.rows_width(width) }; - // Measure wrapped height for up to MAX_POPUP_ROWS items. + // The shared measurement API reserves one column; compensate to match painted rows. let rows = self.build_rows(); let column_width = ColumnWidthConfig::new(self.col_width_mode, self.name_column_width) .with_description_layout(self.description_layout); @@ -1197,20 +1321,41 @@ impl Renderable for ListSelectionView { SelectionRowDisplay::Wrapped => measure_rows_height_with_col_width_mode( &rows, &self.state, - MAX_POPUP_ROWS, - effective_rows_width.saturating_add(1), + self.max_visible_rows, + effective_rows_width.saturating_add(/*rhs*/ 1), column_width, ), - SelectionRowDisplay::SingleLine => rows.len().clamp(1, MAX_POPUP_ROWS) as u16, + SelectionRowDisplay::SingleLine => { + rows.len().clamp(/*min*/ 1, self.max_visible_rows) as u16 + } }; - let header = self.active_header(); - let tab_height = tab_bar_height(&self.tabs, self.active_tab_idx.unwrap_or(0), inner_width); - let mut height = header.desired_height(inner_width); - height = height.saturating_add(tab_height + u16::from(tab_height > 0)); - height = height.saturating_add(rows_height + 2 + self.header_gap); + let rows_height = self.result_area_height(rows_height); + let tab_height = tab_bar_height( + &self.tabs, + self.active_tab_idx.unwrap_or(/*default*/ 0), + inner_width, + self.tab_appearance(), + ); + let mut height = self.header_height(inner_width); + let header_gap = if self.appearance == SelectionAppearance::Picker && height == 0 { + 0 + } else { + self.header_gap + }; + let tab_gap = u16::from( + tab_height > 0 + && (self.appearance == SelectionAppearance::Legacy || self.is_searchable), + ); + height = height.saturating_add(tab_height + tab_gap); + height = height.saturating_add(rows_height + 2 + header_gap); + if self.appearance == SelectionAppearance::Picker + && self.picker_surface == PickerSurface::Panel + { + height = height.saturating_add(/*rhs*/ 1); + } if self.is_searchable { - height = height.saturating_add(1); + height = height.saturating_add(/*rhs*/ 1); } // Side content: when the terminal is wide enough the panel sits beside @@ -1230,14 +1375,15 @@ impl Renderable for ListSelectionView { let note_lines = wrap_styled_line(note, note_width); height = height.saturating_add(note_lines.len() as u16); } - if self.active_footer_hint().is_some() { - height = height.saturating_add(1); - } + height = height.saturating_add( + self.footer_hint_lines(width.saturating_sub(/*rhs*/ 2)) + .len() as u16, + ); height } fn render(&self, area: Rect, buf: &mut Buffer) { - self.rendered_item_count.set(0); + self.rendered_item_count.set(/*val*/ 0); if area.height == 0 || area.width == 0 { return; } @@ -1248,28 +1394,69 @@ impl Renderable for ListSelectionView { .as_ref() .map(|note| wrap_styled_line(note, note_width)); let note_height = note_lines.as_ref().map_or(0, |lines| lines.len() as u16); - let footer_rows = note_height + u16::from(self.active_footer_hint().is_some()); + let hint_lines = self.footer_hint_lines(note_width); + let hint_height = hint_lines.len() as u16; + let inner_width = popup_content_width(area.width); + let header_height = self.header_height(inner_width); + let tab_height = tab_bar_height( + &self.tabs, + self.active_tab_idx.unwrap_or(/*default*/ 0), + inner_width, + self.tab_appearance(), + ); + let mut footer_rows = note_height + hint_height; + if self.appearance == SelectionAppearance::Picker { + // Wrapped hints yield before the title, controls, and selected result. + let minimum_content = u16::from(self.picker_surface == PickerSurface::Panel) + + header_height.min(/*other*/ 1) + + tab_height + + u16::from(self.is_searchable) + + 1; + footer_rows = footer_rows.min(area.height.saturating_sub(minimum_content)); + } let [content_area, footer_area] = Layout::vertical([Constraint::Fill(1), Constraint::Length(footer_rows)]).areas(area); let outer_content_area = content_area; // Paint the shared menu surface and then layout inside the returned inset. - let content_area = render_menu_surface(outer_content_area, buf); + let content_area = match self.appearance { + SelectionAppearance::Legacy => render_menu_surface(outer_content_area, buf), + SelectionAppearance::Picker => { + Self::clear_to_terminal_bg(buf, outer_content_area); + if self.picker_surface == PickerSurface::Panel { + ratatui::widgets::Block::default() + .style(crate::style::user_message_style()) + .render(outer_content_area, buf); + } + // The list owns its vertical spacer rows so hints remain adjacent + // to results even when search, tabs, or a header are present. + let area = outer_content_area.inset(Insets::vh(/*v*/ 0, /*h*/ 2)); + if self.picker_surface == PickerSurface::Panel { + Rect::new( + area.x, + area.y.saturating_add(/*rhs*/ 1), + area.width, + area.height.saturating_sub(/*rhs*/ 1), + ) + } else { + area + } + } + }; - let inner_width = popup_content_width(outer_content_area.width); let side_w = self.side_layout_width(inner_width); // When side-by-side is active, shrink the list to make room. - let full_rows_width = Self::rows_width(outer_content_area.width); + let full_rows_width = self.rows_width(outer_content_area.width); let effective_rows_width = if let Some(sw) = side_w { - full_rows_width.saturating_sub(SIDE_CONTENT_GAP + sw) + outer_content_area + .width + .saturating_sub(MENU_SURFACE_HORIZONTAL_INSET / 2 + SIDE_CONTENT_GAP + sw) } else { full_rows_width }; - let header = self.active_header(); - let header_height = header.desired_height(inner_width); - let tab_height = tab_bar_height(&self.tabs, self.active_tab_idx.unwrap_or(0), inner_width); + let header = self.active_header(inner_width); let rows = self.build_rows(); let column_width = ColumnWidthConfig::new(self.col_width_mode, self.name_column_width) .with_description_layout(self.description_layout); @@ -1277,13 +1464,17 @@ impl Renderable for ListSelectionView { SelectionRowDisplay::Wrapped => measure_rows_height_with_col_width_mode( &rows, &self.state, - MAX_POPUP_ROWS, - effective_rows_width.saturating_add(1), + self.max_visible_rows, + effective_rows_width.saturating_add(/*rhs*/ 1), column_width, ), - SelectionRowDisplay::SingleLine => rows.len().clamp(1, MAX_POPUP_ROWS) as u16, + SelectionRowDisplay::SingleLine => { + rows.len().clamp(/*min*/ 1, self.max_visible_rows) as u16 + } }; + let rows_height = self.result_area_height(rows_height); + // Stacked (fallback) side content height — only used when not side-by-side. let stacked_side_h = if side_w.is_none() { self.stacked_side_content().desired_height(inner_width) @@ -1298,23 +1489,60 @@ impl Renderable for ListSelectionView { tabs_area, _, search_area, + above_area, list_area, + below_area, _, stacked_side_area, - ] = Layout::vertical([ - Constraint::Max(header_height), - Constraint::Max(self.header_gap), - Constraint::Length(tab_height), - Constraint::Length(u16::from(tab_height > 0)), - Constraint::Length(if self.is_searchable { 1 } else { 0 }), - Constraint::Length(rows_height), - Constraint::Length(stacked_gap), - Constraint::Length(stacked_side_h), - ]) - .areas(content_area); + ] = if self.appearance == SelectionAppearance::Picker { + picker_areas( + content_area, + PickerLayoutSizes { + header: header_height, + header_gap: self.header_gap, + tabs: tab_height, + search: u16::from(self.is_searchable), + rows: rows_height, + side: stacked_side_h, + }, + ) + } else { + let [ + header, + header_gap, + tabs, + tab_gap, + search, + rows, + side_gap, + side, + ] = Layout::vertical([ + Constraint::Max(header_height), + Constraint::Max(self.header_gap), + Constraint::Length(tab_height), + Constraint::Length(u16::from(tab_height > 0)), + Constraint::Length(if self.is_searchable { 1 } else { 0 }), + Constraint::Length(rows_height), + Constraint::Length(stacked_gap), + Constraint::Length(stacked_side_h), + ]) + .areas(content_area); + [ + header, + header_gap, + tabs, + tab_gap, + search, + Rect::default(), + rows, + Rect::default(), + side_gap, + side, + ] + }; // -- Header -- - if header_area.height < header_height { + if self.appearance == SelectionAppearance::Legacy && header_area.height < header_height { let [header_area, elision_area] = Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(header_area); header.render(header_area, buf); @@ -1328,7 +1556,13 @@ impl Renderable for ListSelectionView { // -- Tabs -- if tab_height > 0 { - render_tab_bar(&self.tabs, self.active_tab_idx.unwrap_or(0), tabs_area, buf); + render_tab_bar( + &self.tabs, + self.active_tab_idx.unwrap_or(/*default*/ 0), + tabs_area, + buf, + self.tab_appearance(), + ); } // -- Search bar -- @@ -1348,7 +1582,7 @@ impl Renderable for ListSelectionView { // -- List rows -- if list_area.height > 0 { let render_area = Rect { - x: if rows.is_empty() { + x: if rows.is_empty() && self.appearance == SelectionAppearance::Legacy { list_area.x } else { list_area.x.saturating_sub(2) @@ -1378,6 +1612,21 @@ impl Renderable for ListSelectionView { ), }; self.rendered_item_count.set(rendered_rows.items); + if self.appearance == SelectionAppearance::Picker + && above_area.height > 0 + && below_area.height > 0 + { + render_scroll_indicators( + Rect::new( + render_area.x, + above_area.y, + render_area.width, + below_area.bottom().saturating_sub(above_area.y), + ), + buf, + rendered_rows, + ); + } } // -- Side content (preview panel) -- @@ -1432,11 +1681,7 @@ impl Renderable for ListSelectionView { if footer_area.height > 0 { let [note_area, hint_area] = Layout::vertical([ Constraint::Length(note_height), - Constraint::Length(if self.active_footer_hint().is_some() { - 1 - } else { - 0 - }), + Constraint::Length(hint_height), ]) .areas(footer_area); @@ -1461,14 +1706,21 @@ impl Renderable for ListSelectionView { } } - if let Some(hint) = self.active_footer_hint() { + if !hint_lines.is_empty() { let hint_area = Rect { x: hint_area.x + 2, y: hint_area.y, width: hint_area.width.saturating_sub(2), height: hint_area.height, }; - hint.clone().dim().render(hint_area, buf); + match self.appearance { + SelectionAppearance::Legacy => { + hint_lines[0].clone().dim().render(hint_area, buf) + } + SelectionAppearance::Picker => { + Paragraph::new(hint_lines).dim().render(hint_area, buf); + } + } } } } @@ -2474,13 +2726,13 @@ mod tests { assert_eq!(view.selected_actual_idx(), Some(7)); let selected = view.state.selected_idx.expect("selection should be set"); assert!(view.state.scroll_top <= selected); - assert!(selected < view.state.scroll_top + ListSelectionView::max_visible_rows(/*len*/ 12)); + assert!(selected < view.state.scroll_top + view.visible_rows(/*len*/ 12)); view.handle_key_event(KeyEvent::from(KeyCode::End)); assert_eq!(view.selected_actual_idx(), Some(7)); let selected = view.state.selected_idx.expect("selection should be set"); assert!(view.state.scroll_top <= selected); - assert!(selected < view.state.scroll_top + ListSelectionView::max_visible_rows(/*len*/ 12)); + assert!(selected < view.state.scroll_top + view.visible_rows(/*len*/ 12)); } #[test] @@ -2999,3 +3251,7 @@ mod tests { assert_eq!(cell.style().bg, Some(Color::Reset)); } } + +#[cfg(test)] +#[path = "selection_picker_tests.rs"] +mod picker_tests; diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 689ccfef21..0facd39743 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -143,6 +143,8 @@ pub(crate) use footer::goal_status_indicator_line; pub(crate) use list_selection_view::ColumnWidthMode; pub(crate) use list_selection_view::ListSelectionView; pub(crate) use list_selection_view::OnSelectionChangedCallback; +pub(crate) use list_selection_view::PickerSurface; +pub(crate) use list_selection_view::SelectionAppearance; pub(crate) use list_selection_view::SelectionDescriptionLayout; pub(crate) use list_selection_view::SelectionRowDisplay; pub(crate) use list_selection_view::SelectionToggle; @@ -174,8 +176,10 @@ pub(crate) use title_setup::preview_line_for_title_items; mod paste_burst; mod pending_input_preview; mod pending_thread_approvals; +mod picker_style; pub(crate) mod popup_consts; mod scroll_state; +mod selection_picker_layout; mod selection_popup_common; mod selection_row_layout; mod selection_tabs; diff --git a/codex-rs/tui/src/bottom_pane/pending_input_preview.rs b/codex-rs/tui/src/bottom_pane/pending_input_preview.rs index 34ea0990c2..93328d52a6 100644 --- a/codex-rs/tui/src/bottom_pane/pending_input_preview.rs +++ b/codex-rs/tui/src/bottom_pane/pending_input_preview.rs @@ -97,11 +97,9 @@ impl PendingInputPreview { if !self.pending_steers.is_empty() { let mut header = vec!["Messages to be submitted after next tool call".into()]; if let Some(interrupt_binding) = self.interrupt_binding { - header.extend(vec![ - " (press ".dim(), - interrupt_binding.into(), - " to interrupt and send immediately)".dim(), - ]); + header.push(" (press ".dim()); + header.extend(interrupt_binding.spans()); + header.push(" to interrupt and send immediately)".dim()); } Self::push_section_header(&mut lines, width, Line::from(header)); @@ -171,14 +169,10 @@ impl PendingInputPreview { && !has_questions && let Some(edit_binding) = self.edit_binding { - lines.push( - Line::from(vec![ - " ".into(), - edit_binding.into(), - " edit last queued message".into(), - ]) - .dim(), - ); + let mut hint = Line::from(" "); + hint.spans.extend(edit_binding.spans()); + hint.spans.push(" edit last queued message".dim()); + lines.push(hint); } Paragraph::new(lines).into() diff --git a/codex-rs/tui/src/bottom_pane/picker_style.rs b/codex-rs/tui/src/bottom_pane/picker_style.rs new file mode 100644 index 0000000000..1dace86ad8 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/picker_style.rs @@ -0,0 +1,68 @@ +//! Shared visual tokens and overflow hints for the picker component. +//! +//! Resolve RGB colors when rendering so terminal palette fallbacks stay effective. + +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Style; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Widget; + +use super::selection_popup_common::RenderedRows; +use crate::color::is_light; +pub(crate) use crate::style::selection_style; +use crate::terminal_palette::best_color; +use crate::terminal_palette::default_bg; +use ratatui::style::Color; + +/// The shared picker can blend into the transcript or form a shaded browser panel. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) enum PickerSurface { + #[default] + Terminal, + Panel, +} + +pub(super) fn active_tab_style() -> Style { + let fallback = Style::default() + .fg(Color::Reset) + .bg(Color::Reset) + .bold() + .not_dim() + .underlined(); + let Some(background) = default_bg() else { + return fallback; + }; + let fill = best_color(if is_light(background) { + (220, 220, 220) + } else { + (76, 76, 76) + }); + if fill == Color::Reset { + return fallback; + } + Style::default() + .fg(crate::style::readable_color_on(Color::Reset, Some(fill))) + .bg(fill) + .bold() + .not_dim() + .not_underlined() +} + +/// Draw overflow hints against the background already painted beneath each cell. +pub(super) fn render_scroll_indicators(area: Rect, buf: &mut Buffer, rendered: RenderedRows) { + if area.height < 3 || area.width == 0 { + return; + } + for (visible, symbol, y) in [ + (rendered.has_above, "↑", area.y), + (rendered.has_below, "↓", area.bottom() - 1), + ] { + if visible { + Line::from(symbol) + .fg(crate::style::accent_color_on(Some(buf[(area.x, y)].bg))) + .render(Rect::new(area.x, y, /*width*/ 1, /*height*/ 1), buf); + } + } +} diff --git a/codex-rs/tui/src/bottom_pane/popup_consts.rs b/codex-rs/tui/src/bottom_pane/popup_consts.rs index e2c7c8a56b..7ea945fa40 100644 --- a/codex-rs/tui/src/bottom_pane/popup_consts.rs +++ b/codex-rs/tui/src/bottom_pane/popup_consts.rs @@ -1,5 +1,6 @@ //! Shared popup-related constants for bottom pane widgets. +use ratatui::style::Stylize; use ratatui::text::Line; use crate::key_hint; @@ -38,24 +39,16 @@ pub(crate) fn accept_cancel_hint_line( cancel: Option, cancel_label: &'static str, ) -> Line<'static> { - match (accept, cancel) { - (Some(accept), Some(cancel)) => Line::from(vec![ - "Press ".into(), - accept.into(), - format!(" {accept_label} or ").into(), - cancel.into(), - format!(" {cancel_label}").into(), - ]), - (Some(accept), None) => Line::from(vec![ - "Press ".into(), - accept.into(), - format!(" {accept_label}").into(), - ]), - (None, Some(cancel)) => Line::from(vec![ - "Press ".into(), - cancel.into(), - format!(" {cancel_label}").into(), - ]), - (None, None) => Line::from(""), + let mut spans = Vec::new(); + if let Some(accept) = accept { + spans.push("Press ".dim()); + spans.extend(accept.spans()); + spans.push(format!(" {accept_label}").dim()); } + if let Some(cancel) = cancel { + spans.push(if spans.is_empty() { "Press " } else { " or " }.dim()); + spans.extend(cancel.spans()); + spans.push(format!(" {cancel_label}").dim()); + } + spans.into() } diff --git a/codex-rs/tui/src/bottom_pane/questions.rs b/codex-rs/tui/src/bottom_pane/questions.rs index c1053995f8..19f1a87a8c 100644 --- a/codex-rs/tui/src/bottom_pane/questions.rs +++ b/codex-rs/tui/src/bottom_pane/questions.rs @@ -62,7 +62,10 @@ impl BottomPane { countdown.dim(), ])]; if let Some(binding) = self.pending_input_preview.edit_binding { - lines.push(Line::from(vec![" ".into(), binding.into(), " to answer".into()]).dim()); + let mut hint = Line::from(" "); + hint.spans.extend(binding.spans()); + hint.spans.push(" to answer".dim()); + lines.push(hint); } Some(lines) } 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 06ac2fe6b2..56eeab294b 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 @@ -2512,7 +2512,7 @@ mod tests { let tip_texts = tips.iter().map(|tip| tip.text.as_str()).collect::>(); assert_eq!( tip_texts, - vec!["ctrl + j to submit answer", "esc to interrupt"] + vec!["ctrl+j to submit answer", "esc to interrupt"] ); } @@ -2521,21 +2521,21 @@ mod tests { for (specs, expected_tips) in [ ( KeybindingsSpec::One(KeybindingSpec("ctrl-x enter".to_string())), - vec!["ctrl + x enter to submit answer", "esc to interrupt"], + vec!["ctrl+x enter to submit answer", "esc to interrupt"], ), ( KeybindingsSpec::Many(vec![ KeybindingSpec("ctrl-enter".to_string()), KeybindingSpec("ctrl-x enter".to_string()), ]), - vec!["ctrl + enter to submit answer", "esc to interrupt"], + vec!["ctrl+enter to submit answer", "esc to interrupt"], ), ( KeybindingsSpec::Many(vec![ KeybindingSpec("ctrl-x enter".to_string()), KeybindingSpec("ctrl-enter".to_string()), ]), - vec!["ctrl + x enter to submit answer", "esc to interrupt"], + vec!["ctrl+x enter to submit answer", "esc to interrupt"], ), ] { let (tx, _rx) = test_sender(); @@ -3755,16 +3755,16 @@ mod tests { .collect::>() .join("\n"); - insta::assert_snapshot!(snapshot, @r" + insta::assert_snapshot!(snapshot, @" - Question 1/1 (1 unanswered) - Share details. + Question 1/1 (1 unanswered) + Share details. - › Type your answer (optional) + › Type your answer (optional) - ctrl + x enter to submit answer | esc to interrupt + ctrl+x enter to submit answer | esc to interrupt "); } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform_remapped_submit.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform_remapped_submit.snap index a2aeaa4d3e..4e357e6fe0 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform_remapped_submit.snap +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform_remapped_submit.snap @@ -10,4 +10,4 @@ expression: "render_snapshot(&overlay, area)" - ctrl + j to submit answer | esc to interrupt + ctrl+j to submit answer | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/selection_picker_layout.rs b/codex-rs/tui/src/bottom_pane/selection_picker_layout.rs new file mode 100644 index 0000000000..40a1a0baa8 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/selection_picker_layout.rs @@ -0,0 +1,49 @@ +//! Fit picker chrome before assigning result rows so short panels retain their controls. +//! +//! Optional gaps and previews yield first; headers, tabs, search, and at least one +//! result remain visible whenever the available height can accommodate them. +//! Overflow spacers are reserved separately from the minimum result content. + +use ratatui::layout::Rect; + +pub(super) struct PickerLayoutSizes { + pub header: u16, + pub header_gap: u16, + pub tabs: u16, + pub search: u16, + pub rows: u16, + pub side: u16, +} + +pub(super) fn picker_areas(area: Rect, sizes: PickerLayoutSizes) -> [Rect; 10] { + let tabs = sizes.tabs.min(area.height); + let search = sizes.search.min(area.height.saturating_sub(tabs)); + let minimum_rows = sizes.rows.min(if tabs > 0 { 1 } else { 3 }); + let header = sizes + .header + .min(area.height.saturating_sub(tabs + search + minimum_rows)); + let mut remaining = area.height.saturating_sub(header + tabs + search); + let indicators = u16::from(remaining >= minimum_rows + 2); + remaining = remaining.saturating_sub(indicators * 2); + let minimum_rows = sizes.rows.min(/*other*/ 3).min(remaining); + let header_gap = if header > 0 { + sizes.header_gap.min(remaining.saturating_sub(minimum_rows)) + } else { + 0 + }; + remaining = remaining.saturating_sub(header_gap); + let tab_gap = u16::from(tabs > 0 && search > 0 && remaining > minimum_rows); + remaining = remaining.saturating_sub(tab_gap); + let side = sizes.side.min(remaining.saturating_sub(minimum_rows + 1)); + let side_gap = u16::from(side > 0); + let rows = sizes.rows.min(remaining.saturating_sub(side + side_gap)); + let mut y = area.y; + [ + header, header_gap, tabs, tab_gap, search, indicators, rows, indicators, side_gap, side, + ] + .map(|height| { + let rect = Rect::new(area.x, y, area.width, height); + y = y.saturating_add(height); + rect + }) +} diff --git a/codex-rs/tui/src/bottom_pane/selection_picker_tests.rs b/codex-rs/tui/src/bottom_pane/selection_picker_tests.rs new file mode 100644 index 0000000000..6151525ac3 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/selection_picker_tests.rs @@ -0,0 +1,183 @@ +//! Opt-in panels retain controls and page by the results that actually fit. + +use super::*; +use crate::app_event::AppEvent; +use pretty_assertions::assert_eq; +use ratatui::style::Color; +use tokio::sync::mpsc::unbounded_channel; + +fn browser() -> ListSelectionView { + let (tx, _rx) = unbounded_channel::(); + ListSelectionView::new( + SelectionViewParams { + appearance: SelectionAppearance::Picker, + picker_surface: PickerSurface::Panel, + max_visible_rows: 24, + title: Some("Keymap".to_owned()), + subtitle: Some("All configurable shortcuts.".to_owned()), + is_searchable: true, + search_placeholder: Some("Type to search shortcuts".to_owned()), + footer_hint: Some("left/right group · enter edit · esc close".into()), + row_display: SelectionRowDisplay::SingleLine, + tabs: ["All", "Common", "Customized (3)", "Unbound (4)", "App"] + .into_iter() + .map(|label| SelectionTab { + id: label.to_owned(), + label: label.to_owned(), + header: Box::new(()), + items: (0..30) + .map(|index| SelectionItem { + name: format!("Action {index:02}"), + description: Some(format!("Ctrl+{index}")), + search_value: Some(format!("Action {index:02}")), + ..Default::default() + }) + .collect(), + }) + .collect(), + ..Default::default() + }, + AppEventSender::new(tx), + crate::keymap::RuntimeKeymap::defaults().list, + ) +} + +fn render_browser(view: &ListSelectionView, width: u16, height: u16) -> (String, Buffer) { + let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height); + let mut buf = Buffer::empty(area); + view.render(area, &mut buf); + let text = (0..height) + .map(|y| { + (0..width) + .map(|x| buf[(x, y)].symbol()) + .collect::() + .trim_end() + .to_owned() + }) + .collect::>() + .join("\n"); + (text, buf) +} + +#[test] +fn short_browser_keeps_header_tabs_search_and_footer() { + let view = browser(); + let mut snapshots = Vec::new(); + for (width, height) in [(48, 6), (48, 7), (48, 13), (24, 6), (24, 7)] { + let (text, _) = render_browser(&view, width, height); + assert!(text.contains("Keymap")); + assert!(text.contains("Common")); + assert!(text.contains("Type to search")); + assert!(text.contains("left/right group")); + assert!(view.rendered_item_count() > 0); + snapshots.push(format!("{width}×{height}:\n{text}")); + } + insta::assert_snapshot!(snapshots.join("\n---\n")); +} + +#[test] +fn browser_page_navigation_uses_rows_that_fit() { + let mut view = browser(); + let (_, initial_buf) = render_browser(&view, /*width*/ 60, /*height*/ 18); + let visible = view.rendered_item_count(); + assert!(visible < view.max_visible_rows); + view.handle_key_event(KeyEvent::from(KeyCode::PageDown)); + let (text, buf) = render_browser(&view, /*width*/ 60, /*height*/ 18); + insta::assert_snapshot!(text); + assert_eq!(view.selected_actual_idx(), Some(visible)); + let selected_row = text + .lines() + .position(|line| line.starts_with('›')) + .expect("focused row"); + assert_eq!( + buf[(0, selected_row as u16)].bg, + selection_style().bg.unwrap() + ); + assert_eq!(buf[(0, 1)].bg, initial_buf[(0, 1)].bg); +} + +#[test] +fn shrinking_the_browser_keeps_selection_visible_without_navigation() { + for row_display in [ + SelectionRowDisplay::SingleLine, + SelectionRowDisplay::Wrapped, + ] { + let mut view = browser(); + view.row_display = row_display; + view.state.selected_idx = Some(7); + view.state.scroll_top = 0; + render_browser(&view, /*width*/ 60, /*height*/ 24); + let initial_count = view.rendered_item_count(); + assert!(initial_count >= 8); + + let (text, _) = render_browser(&view, /*width*/ 60, /*height*/ 13); + assert!(view.rendered_item_count() < initial_count); + assert_eq!(view.selected_actual_idx(), Some(7)); + let focused = text + .lines() + .find(|line| line.starts_with('›')) + .expect("focused row"); + assert!(focused.contains("Action 07"), "{text}"); + } +} + +#[test] +fn overflow_arrows_follow_the_painted_picker_surface() { + for (fg, bg) in [ + ((32, 32, 32), (255, 255, 255)), + ((240, 240, 240), (24, 24, 24)), + ((240, 240, 240), (80, 80, 80)), + ((32, 32, 32), (130, 130, 130)), + ] { + crate::terminal_palette::with_test_default_colors( + crate::terminal_probe::DefaultColors { fg, bg }, + || { + for surface in [PickerSurface::Panel, PickerSurface::Terminal] { + let mut view = browser(); + view.picker_surface = surface; + view.state.selected_idx = Some(15); + view.state.scroll_top = 12; + let (text, buf) = render_browser(&view, /*width*/ 60, /*height*/ 18); + let selected_y = text + .lines() + .position(|line| line.starts_with('›')) + .expect("focused row") as u16; + let selected_background = selection_style().bg.unwrap(); + assert_eq!( + (0..buf.area.width) + .map(|x| buf[(x, selected_y)].bg) + .collect::>(), + vec![selected_background; usize::from(buf.area.width)], + ); + let background = match surface { + PickerSurface::Panel => crate::style::user_message_style().bg.unwrap(), + PickerSurface::Terminal => Color::Reset, + }; + let arrows = (0..buf.area.height) + .filter(|&y| matches!(buf[(0, y)].symbol(), "↑" | "↓")) + .map(|y| (y, buf[(0, y)].clone(), buf[(1, y)].clone())) + .collect::>(); + assert_eq!( + arrows + .iter() + .map(|(_, cell, _)| cell.symbol()) + .collect::>(), + vec!["↑", "↓"], + ); + for (y, cell, adjacent) in &arrows { + assert_ne!(*y, selected_y); + assert_ne!(cell.bg, selected_background); + assert_eq!( + (cell.fg, cell.bg, adjacent.bg), + ( + crate::style::accent_color_on(Some(background)), + background, + background, + ), + ); + } + } + }, + ); + } +} diff --git a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs index aceecb92ed..04b799dac7 100644 --- a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs +++ b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs @@ -2,6 +2,8 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; // Note: Table-based layout previously used Constraint; the manual renderer // below no longer requires it. +use ratatui::style::Modifier; +use ratatui::style::Style; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; @@ -31,6 +33,8 @@ use super::selection_row_layout::wrap_stacked_row; #[derive(Default)] pub(crate) struct GenericDisplayRow { pub name: String, + /// Optional full-width selection treatment; other rows use the shared text accent. + pub selection_style: Option