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
@@ -71,7 +71,25 @@ fn mermaid_styles_follow_the_supplied_theme() {
("light", themes.get(EmbeddedThemeName::SolarizedLight)),
("fallback", &fallback),
] {
let lines = super::render("flowchart LR; A[请求] --> B[Reply]", Some(40), theme).unwrap();
let colors = if name == "light" {
crate::terminal_probe::DefaultColors {
fg: (30, 30, 30),
bg: (255, 255, 255),
}
} else {
crate::terminal_probe::DefaultColors {
fg: (220, 220, 220),
bg: (20, 20, 20),
}
};
let lines = crate::terminal_palette::with_test_default_colors(colors, || {
super::render(
"flowchart LR; A[请求] --> B[Reply]",
/*width*/ Some(40),
theme,
)
.unwrap()
});
let styled = lines
.into_iter()
.map(|line| {
@@ -1,6 +1,5 @@
---
source: tui/src/markdown_render/mermaid_tests.rs
assertion_line: 88
expression: "cases.join(\"\\n\\n\")"
---
dark
@@ -6,11 +6,11 @@ expression: presentations
Text::from_iter([
Line::from_iter([
Span::from("plain "),
Span::from("plain").cyan().underlined(),
Span::from("plain").fg(Color::Reset).underlined(),
Span::from(" "),
Span::from("code").fg(Color::Rgb(166, 227, 161)),
Span::from(" "),
Span::from("code").cyan().underlined(),
Span::from("code").fg(Color::Reset).underlined(),
]),
Line::default(),
Line::from_iter([
@@ -20,23 +20,23 @@ expression: presentations
Line::from(Span::from("━━━━━━━━━━").dim()),
Line::from_iter([
Span::from(" "),
Span::from("docs").cyan().underlined(),
Span::from("docs").fg(Color::Reset).underlined(),
]),
]),
Text::from_iter([
Line::from_iter([
Span::from("plain "),
Span::from("plain").cyan().underlined(),
Span::from("plain").fg(Color::Reset).underlined(),
Span::from(" ("),
Span::from("https://example.com").cyan().underlined(),
Span::from("https://example.com").fg(Color::Reset).underlined(),
Span::from(")"),
Span::from(" "),
Span::from("code").fg(Color::Rgb(166, 227, 161)),
]),
Line::from_iter([
Span::from("code").cyan().underlined(),
Span::from("code").fg(Color::Reset).underlined(),
Span::from(" ("),
Span::from("https://example.com").cyan().underlined(),
Span::from("https://example.com").fg(Color::Reset).underlined(),
Span::from(")"),
]),
Line::default(),
@@ -47,9 +47,9 @@ expression: presentations
Line::from(Span::from("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim()),
Line::from_iter([
Span::from(" "),
Span::from("docs").cyan().underlined(),
Span::from("docs").fg(Color::Reset).underlined(),
Span::from(" ("),
Span::from("https://example.com/docs").cyan().underlined(),
Span::from("https://example.com/docs").fg(Color::Reset).underlined(),
Span::from(")"),
]),
]),
@@ -1,6 +1,7 @@
use super::WebLinkDisplay;
use crate::markdown_render::render_markdown_lines_with_width_cwd_and_hidden_link_destinations;
use crate::markdown_render::render_streaming_markdown_lines_with_width_and_cwd;
use crate::style::accent_color;
use crate::terminal_hyperlinks::HyperlinkLine;
use crate::terminal_hyperlinks::TerminalHyperlink;
use crate::terminal_hyperlinks::visible_lines;
@@ -49,23 +50,29 @@ fn supporting_terminals_render_only_the_styled_label_and_keep_its_target() {
] {
let display = WebLinkDisplay::for_terminal(&terminal(name), /*term*/ None);
for (markdown, label) in [
("[label](https://example.com)", "label".cyan().underlined()),
(
"[label](https://example.com)",
"label".fg(accent_color()).underlined(),
),
(
"[`label`](https://example.com)",
"label".cyan().underlined(),
"label".fg(accent_color()).underlined(),
),
(
"[**label**](https://example.com)",
"label".cyan().bold().underlined(),
"label".fg(accent_color()).bold().underlined(),
),
(
"[*label*](https://example.com)",
"label".cyan().italic().underlined(),
"label".fg(accent_color()).italic().underlined(),
),
(
"[<b>](https://example.com)",
"<b>".fg(accent_color()).underlined(),
),
("[<b>](https://example.com)", "<b>".cyan().underlined()),
(
"[https://example.com](https://example.com)",
"https://example.com".cyan().underlined(),
"https://example.com".fg(accent_color()).underlined(),
),
] {
let label_width = label.width();