mirror of
https://github.com/openai/codex.git
synced 2026-11-11 21:10:39 +00:00
Improve TUI contrast, keyboard hints, and picker layouts (#46680)
## Why Theme colors and dimmed text can lose contrast against terminal and shaded backgrounds. Pickers also need to keep controls and the selected result visible when space is limited. ## What changed - Add shared blue accents and contrast-aware foregrounds that account for painted backgrounds and terminal color depth, with terminal-default fallbacks for limited palettes. - Apply readable colors to footer text, status lines, prompt attachment labels, and diff content. Remove dimming from syntax-highlighted diff text so it does not undo contrast correction. - Use compact shortcut labels such as `ctrl+c`, emphasize complete key combinations, and separate composer footer actions with ` · `. - Add an opt-in picker presentation with full-width selection, filled tabs, separate overflow indicators, and navigation based on visible rows. Enable it for `/keymap`, reserving result space to keep controls stable across filtering and tab changes. ## Testing Add regression tests and snapshots for contrast after palette conversion, shaded diff backgrounds, shortcut typography, picker resizing and pagination, Unicode tab truncation, and stable `/keymap` geometry. GitOrigin-RevId: f3c71879d3bcc4e3b29d70e63cf377dea5914916
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
//! Shortcut picker construction for `/keymap`.
|
||||
//! Reserve the opt-in panel viewport so tab and search changes keep controls anchored.
|
||||
|
||||
use codex_config::types::TuiKeymap;
|
||||
use ratatui::style::Styled;
|
||||
@@ -9,6 +10,8 @@ use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::bottom_pane::ColumnWidthMode;
|
||||
use crate::bottom_pane::PickerSurface;
|
||||
use crate::bottom_pane::SelectionAppearance;
|
||||
use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionRowDisplay;
|
||||
use crate::bottom_pane::SelectionTab;
|
||||
@@ -294,6 +297,10 @@ fn build_keymap_picker_params_for_action(
|
||||
|
||||
SelectionViewParams {
|
||||
view_id: Some(KEYMAP_PICKER_VIEW_ID),
|
||||
appearance: SelectionAppearance::Picker,
|
||||
picker_surface: PickerSurface::Panel,
|
||||
max_visible_rows: 24,
|
||||
reserve_result_rows: true,
|
||||
header: Box::new(()),
|
||||
footer_hint: Some(keymap_picker_hint_line()),
|
||||
tab_footer_hints: vec![(KEYMAP_DEBUG_TAB_ID.to_string(), keymap_debug_hint_line())],
|
||||
@@ -462,27 +469,30 @@ fn action_count_line(count: usize) -> String {
|
||||
}
|
||||
|
||||
fn keymap_picker_hint_line() -> Line<'static> {
|
||||
let style = accent_style();
|
||||
Line::from(vec![
|
||||
"left/right".set_style(style),
|
||||
" group · ".dim(),
|
||||
"enter".set_style(style),
|
||||
" edit shortcut · ".dim(),
|
||||
"*".set_style(style),
|
||||
" custom · ".dim(),
|
||||
"-".set_style(style),
|
||||
" unbound · ".dim(),
|
||||
"esc".set_style(style),
|
||||
" close".dim(),
|
||||
])
|
||||
[
|
||||
("left/right", " group · "),
|
||||
("enter", " edit shortcut · "),
|
||||
("*", " custom · "),
|
||||
("-", " unbound · "),
|
||||
("esc", " close"),
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|(key, label)| {
|
||||
crate::key_hint::key_label_spans(key)
|
||||
.into_iter()
|
||||
.chain([label.set_style(crate::style::footer_hint_label_style())])
|
||||
})
|
||||
.collect::<Line>()
|
||||
}
|
||||
|
||||
fn keymap_debug_hint_line() -> Line<'static> {
|
||||
let style = accent_style();
|
||||
Line::from(vec![
|
||||
"enter".set_style(style),
|
||||
" start inspector · ".dim(),
|
||||
"esc".set_style(style),
|
||||
" close".dim(),
|
||||
])
|
||||
let mut spans = crate::key_hint::key_label_spans("enter");
|
||||
spans.push(" start inspector · ".set_style(crate::style::footer_hint_label_style()));
|
||||
spans.extend(crate::key_hint::key_label_spans("esc"));
|
||||
spans.push(" close".set_style(crate::style::footer_hint_label_style()));
|
||||
spans.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "picker_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Keep the production shortcut picker's controls anchored while groups and results change.
|
||||
|
||||
use super::*;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::BottomPaneView;
|
||||
use crate::bottom_pane::ListSelectionView;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
#[test]
|
||||
fn tabs_and_search_keep_production_picker_geometry_stable() {
|
||||
for width in [80, 40] {
|
||||
for height_limit in [u16::MAX, 16] {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());
|
||||
let tab_count = params.tabs.len();
|
||||
let (tx, _rx) = unbounded_channel();
|
||||
let mut view = ListSelectionView::new(params, AppEventSender::new(tx), runtime.list);
|
||||
let mut geometry = Vec::new();
|
||||
|
||||
for (tab_id, tab_text, hint) in [
|
||||
(KEYMAP_ALL_TAB_ID, "Common", "left/right"),
|
||||
(KEYMAP_CUSTOM_TAB_ID, "Customized (0)", "left/right"),
|
||||
(KEYMAP_DEBUG_TAB_ID, "Debug", "enter"),
|
||||
] {
|
||||
for _ in 0..tab_count {
|
||||
if BottomPaneView::active_tab_id(&view) == Some(tab_id) {
|
||||
break;
|
||||
}
|
||||
view.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
}
|
||||
assert_eq!(BottomPaneView::active_tab_id(&view), Some(tab_id));
|
||||
|
||||
for query in ["", "zzzz-no-matching-shortcut"] {
|
||||
view.set_search_query(query.to_owned());
|
||||
let desired_height = view.desired_height(width);
|
||||
let height = desired_height.min(height_limit);
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
view.render(area, &mut buffer);
|
||||
let lines = (0..height)
|
||||
.map(|y| {
|
||||
(0..width)
|
||||
.map(|x| buffer[(x, y)].symbol())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let search_text = if query.is_empty() {
|
||||
"Type to search shortcuts"
|
||||
} else {
|
||||
query
|
||||
};
|
||||
let header_y = lines
|
||||
.iter()
|
||||
.position(|line| line.trim() == "Keymap")
|
||||
.expect("picker title remains visible");
|
||||
let tabs_y = lines
|
||||
.iter()
|
||||
.position(|line| line.contains(tab_text))
|
||||
.expect("active group remains visible");
|
||||
let search_y = lines
|
||||
.iter()
|
||||
.position(|line| line.trim() == search_text)
|
||||
.expect("search remains visible");
|
||||
let hint_y = lines
|
||||
.iter()
|
||||
.position(|line| line.trim_start().starts_with(hint))
|
||||
.expect("group-specific footer remains visible");
|
||||
geometry.push((desired_height, height, header_y, tabs_y, search_y, hint_y));
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
geometry,
|
||||
vec![geometry[0]; geometry.len()],
|
||||
"group and search changes must keep controls anchored at width {width} and height limit {height_limit}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user