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:
Eric Traut
2026-09-19 17:46:43 +00:00
committed by copyberry
parent a5290028a2
commit 132c2be239
199 changed files with 3424 additions and 1150 deletions
+30 -20
View File
@@ -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}"
);
}
}
}