mirror of
https://github.com/openai/codex.git
synced 2026-10-20 08:19:50 +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:
@@ -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());
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+7
-7
@@ -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
|
||||
|
||||
+7
-7
@@ -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
|
||||
|
||||
+10
-10
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
@@ -164,8 +164,8 @@ async fn capped_resize_reflow_prepends_transcript_notice_without_changing_transc
|
||||
.map(rendered_line_text)
|
||||
.collect::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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
|
||||
|
||||
@@ -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(" · "))));
|
||||
}
|
||||
|
||||
@@ -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<_>>(),
|
||||
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(),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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??;
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: "render_bottom_popup(&app.chat_widget, 80)"
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
ctrl+c quit
|
||||
ctrl+c quit
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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()),
|
||||
]
|
||||
|
||||
+1
-1
@@ -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()),
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-2
@@ -16,5 +16,4 @@ expression: buffer_text(&buffer)
|
||||
long draft
|
||||
|
||||
|
||||
enter submit ctrl + ] skip
|
||||
⌥ + ↓ main prompt
|
||||
enter submit ctrl+] skip ⌥+↓ main prompt
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -10,5 +10,5 @@ expression: buffer_text(&buffer)
|
||||
multiple rows
|
||||
2. Other
|
||||
|
||||
enter submit ctrl + ] skip
|
||||
⌥ + ↓ main prompt
|
||||
enter submit ctrl+] skip
|
||||
⌥+↓ main prompt
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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::<AppEvent>();
|
||||
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::<AppEvent>();
|
||||
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<_>>(),
|
||||
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<_>>(),
|
||||
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]
|
||||
|
||||
+14
-14
@@ -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:
|
||||
|
||||
+7
-7
@@ -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
|
||||
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<CollaborationModeIndicator>,
|
||||
show_cycle_hint: bool,
|
||||
) -> Option<Line<'static>> {
|
||||
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<u16> {
|
||||
@@ -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<Line<'st
|
||||
|
||||
if let Some(active_agent_label) = props.active_agent_label.as_ref() {
|
||||
if let Some(existing) = line.as_mut() {
|
||||
existing.spans.push(" · ".dim());
|
||||
existing.spans.push(active_agent_label.clone().dim());
|
||||
existing.spans.push(" · ".set_style(secondary_text_style()));
|
||||
existing
|
||||
.spans
|
||||
.push(active_agent_label.clone().set_style(secondary_text_style()));
|
||||
} else {
|
||||
line = Some(Line::from(active_agent_label.clone()).dim());
|
||||
line = Some(Line::from(active_agent_label.clone()).set_style(secondary_text_style()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -814,7 +844,9 @@ pub(crate) fn passive_footer_status_line(props: &FooterProps) -> Option<Line<'st
|
||||
&& let Some(key) = props.key_hints.agents
|
||||
&& let Some(line) = line.as_mut()
|
||||
{
|
||||
line.extend(vec![" · ".dim(), key.into(), " for agents".dim()]);
|
||||
line.push_span(" · ".set_style(secondary_text_style()));
|
||||
line.extend(key.spans());
|
||||
line.push_span(" for agents".set_style(secondary_text_style()));
|
||||
}
|
||||
|
||||
line
|
||||
@@ -873,19 +905,18 @@ pub(crate) fn footer_hint_items_width(items: &[(String, String)]) -> 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<Line<'static>> {
|
||||
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<Line<'static>>) -> Vec<Line<'static>> {
|
||||
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<Line<'static>>) -> Vec<Line<'static>> {
|
||||
pub(crate) fn context_window_line(percent: Option<i64>, used_tokens: Option<i64>) -> 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]
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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<dyn Fn(&AppEventSender) + Send + Sync>;
|
||||
|
||||
@@ -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<String>,
|
||||
pub subtitle: Option<String>,
|
||||
@@ -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<Line<'static>>,
|
||||
footer_hint: Option<Line<'static>>,
|
||||
@@ -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::<Vec<_>>())
|
||||
.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<usize> {
|
||||
@@ -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<Line<'_>> {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ShortcutHint>,
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -2512,7 +2512,7 @@ mod tests {
|
||||
let tip_texts = tips.iter().map(|tip| tip.text.as_str()).collect::<Vec<_>>();
|
||||
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::<Vec<_>>()
|
||||
.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
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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::<AppEvent>();
|
||||
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::<String>()
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.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<_>>(),
|
||||
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::<Vec<_>>();
|
||||
assert_eq!(
|
||||
arrows
|
||||
.iter()
|
||||
.map(|(_, cell, _)| cell.symbol())
|
||||
.collect::<Vec<_>>(),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Style>,
|
||||
pub name_prefix_spans: Vec<Span<'static>>,
|
||||
pub display_shortcut: Option<ShortcutHint>,
|
||||
pub match_indices: Option<Vec<usize>>, // indices to bold (char positions)
|
||||
@@ -326,11 +330,36 @@ fn wrap_row_lines(
|
||||
wrap_standard_row(row, desc_col, width, description_layout)
|
||||
}
|
||||
|
||||
fn apply_row_state_style(lines: &mut [Line<'static>], selected: bool, is_disabled: bool) {
|
||||
fn apply_row_state_style(
|
||||
lines: &mut [Line<'static>],
|
||||
selected: bool,
|
||||
is_disabled: bool,
|
||||
selection_style: Option<Style>,
|
||||
) {
|
||||
if selected {
|
||||
for line in lines.iter_mut() {
|
||||
if let Some(style) = selection_style {
|
||||
line.style = style;
|
||||
}
|
||||
line.spans.iter_mut().for_each(|span| {
|
||||
span.style = accent_style();
|
||||
let Some(selected) = selection_style else {
|
||||
span.style = accent_style();
|
||||
return;
|
||||
};
|
||||
let secondary = span.style.add_modifier.contains(Modifier::DIM);
|
||||
let foreground = span.style.fg.or(selected.fg).unwrap_or_default();
|
||||
span.style = selected.patch(span.style).not_dim();
|
||||
span.style.bg = selected.bg;
|
||||
if selected.add_modifier.contains(Modifier::REVERSED) {
|
||||
span.style.fg = selected.fg;
|
||||
span.style = span.style.reversed();
|
||||
} else {
|
||||
span.style.fg = Some(crate::style::readable_color_on(foreground, selected.bg));
|
||||
span.style = span.style.not_reversed();
|
||||
}
|
||||
if secondary {
|
||||
span.style = span.style.not_bold();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -448,14 +477,15 @@ fn adjust_start_for_wrapped_selection_visibility(
|
||||
start_idx
|
||||
}
|
||||
|
||||
/// Render a list of rows using the provided ScrollState, with shared styling
|
||||
/// and behavior for selection popups.
|
||||
/// Returns the number of terminal lines actually rendered (including the
|
||||
/// single-line empty placeholder when shown).
|
||||
/// Counts painted rows and items and reports content hidden outside the viewport.
|
||||
/// The line count includes the single-line empty placeholder when shown.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct RenderedRows {
|
||||
pub(crate) lines: u16,
|
||||
pub(crate) items: usize,
|
||||
pub(crate) has_above: bool,
|
||||
/// Includes a description whose final wrapped lines were clipped.
|
||||
pub(crate) has_below: bool,
|
||||
}
|
||||
|
||||
fn render_rows_inner(
|
||||
@@ -475,6 +505,7 @@ fn render_rows_inner(
|
||||
return RenderedRows {
|
||||
lines: u16::from(area.height > 0),
|
||||
items: 0,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -509,6 +540,7 @@ fn render_rows_inner(
|
||||
let mut cur_y = area.y;
|
||||
let mut rendered_lines: u16 = 0;
|
||||
let mut rendered_items = 0;
|
||||
let mut clipped = false;
|
||||
for (i, row) in rows_all.iter().enumerate().skip(start_idx).take(max_items) {
|
||||
if cur_y >= area.y + area.height {
|
||||
break;
|
||||
@@ -516,10 +548,12 @@ fn render_rows_inner(
|
||||
|
||||
let mut wrapped =
|
||||
wrap_row_lines(row, desc_col, area.width, column_width.description_layout);
|
||||
clipped |= wrapped.len() > usize::from(area.bottom().saturating_sub(cur_y));
|
||||
apply_row_state_style(
|
||||
&mut wrapped,
|
||||
Some(i) == state.selected_idx && !row.is_disabled,
|
||||
row.is_disabled,
|
||||
row.selection_style,
|
||||
);
|
||||
|
||||
// Render the wrapped lines.
|
||||
@@ -549,6 +583,8 @@ fn render_rows_inner(
|
||||
RenderedRows {
|
||||
lines: rendered_lines,
|
||||
items: rendered_items,
|
||||
has_above: start_idx > 0,
|
||||
has_below: clipped || start_idx + rendered_items < rows_all.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,6 +687,7 @@ pub(crate) fn render_rows_single_line_with_col_width_mode(
|
||||
return RenderedRows {
|
||||
lines: u16::from(area.height > 0),
|
||||
items: 0,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -685,16 +722,12 @@ pub(crate) fn render_rows_single_line_with_col_width_mode(
|
||||
}
|
||||
|
||||
let mut full_line = build_full_line(row, desc_col, column_width.description_layout);
|
||||
if Some(i) == state.selected_idx && !row.is_disabled {
|
||||
full_line.spans.iter_mut().for_each(|span| {
|
||||
span.style = accent_style();
|
||||
});
|
||||
}
|
||||
if row.is_disabled {
|
||||
full_line.spans.iter_mut().for_each(|span| {
|
||||
span.style = span.style.dim();
|
||||
});
|
||||
}
|
||||
apply_row_state_style(
|
||||
std::slice::from_mut(&mut full_line),
|
||||
Some(i) == state.selected_idx && !row.is_disabled,
|
||||
row.is_disabled,
|
||||
row.selection_style,
|
||||
);
|
||||
|
||||
let full_line = truncate_line_with_ellipsis_if_overflow(full_line, area.width as usize);
|
||||
full_line.render(
|
||||
@@ -713,6 +746,8 @@ pub(crate) fn render_rows_single_line_with_col_width_mode(
|
||||
RenderedRows {
|
||||
lines: rendered_lines,
|
||||
items: rendered_lines as usize,
|
||||
has_above: start_idx > 0,
|
||||
has_below: start_idx + usize::from(rendered_lines) < rows_all.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,6 +847,7 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
|
||||
#[test]
|
||||
@@ -944,6 +980,109 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_preserves_semantic_spans_and_secondary_hierarchy() {
|
||||
for (fg, bg) in [
|
||||
((32, 32, 32), (255, 255, 255)),
|
||||
((230, 230, 230), (18, 20, 30)),
|
||||
] {
|
||||
crate::terminal_palette::with_test_default_colors(
|
||||
crate::terminal_probe::DefaultColors { fg, bg },
|
||||
|| {
|
||||
for selected in [
|
||||
crate::style::selection_style(),
|
||||
Style::default()
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset)
|
||||
.bold()
|
||||
.reversed(),
|
||||
] {
|
||||
for single_line in [true, false] {
|
||||
let rows = [GenericDisplayRow {
|
||||
name_prefix_spans: vec![
|
||||
"L".underlined(),
|
||||
"M ".into(),
|
||||
"Ready ".green().italic(),
|
||||
],
|
||||
name: "provider".into(),
|
||||
description: Some("secondary details".into()),
|
||||
selection_style: Some(selected),
|
||||
..Default::default()
|
||||
}];
|
||||
let state = ScrollState {
|
||||
selected_idx: Some(0),
|
||||
scroll_top: 0,
|
||||
};
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 48, /*height*/ 1,
|
||||
);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
if single_line {
|
||||
render_rows_single_line_with_col_width_mode(
|
||||
area,
|
||||
&mut buffer,
|
||||
&rows,
|
||||
&state,
|
||||
/*max_results*/ 1,
|
||||
"",
|
||||
ColumnWidthConfig::default(),
|
||||
);
|
||||
} else {
|
||||
render_rows(
|
||||
area,
|
||||
&mut buffer,
|
||||
&rows,
|
||||
&state,
|
||||
/*max_results*/ 1,
|
||||
"",
|
||||
);
|
||||
}
|
||||
let line = buffer
|
||||
.content
|
||||
.iter()
|
||||
.map(ratatui::buffer::Cell::symbol)
|
||||
.collect::<String>();
|
||||
let status = &buffer[(3, 0)];
|
||||
let description_x =
|
||||
line.find("secondary").expect("visible secondary text") as u16;
|
||||
assert_eq!(
|
||||
(
|
||||
buffer[(0, 0)].modifier,
|
||||
status.modifier,
|
||||
buffer[(description_x, 0)].modifier
|
||||
),
|
||||
(
|
||||
Modifier::BOLD
|
||||
| Modifier::UNDERLINED
|
||||
| (selected.add_modifier & Modifier::REVERSED),
|
||||
Modifier::BOLD
|
||||
| Modifier::ITALIC
|
||||
| (selected.add_modifier & Modifier::REVERSED),
|
||||
selected.add_modifier & Modifier::REVERSED,
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
status.fg,
|
||||
if selected.add_modifier.contains(Modifier::REVERSED) {
|
||||
Color::Reset
|
||||
} else {
|
||||
Color::Green
|
||||
}
|
||||
);
|
||||
assert_eq!(buffer[(0, 0)].fg, selected.fg.unwrap());
|
||||
assert!(
|
||||
buffer
|
||||
.content
|
||||
.iter()
|
||||
.all(|cell| cell.bg == selected.bg.unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_rows_use_the_shared_accent_style() {
|
||||
let rows = vec![GenericDisplayRow {
|
||||
|
||||
@@ -120,7 +120,7 @@ fn build_name_spans(row: &GenericDisplayRow, name_limit: usize) -> Vec<Span<'sta
|
||||
fn append_shortcut(row: &GenericDisplayRow, spans: &mut Vec<Span<'static>>) {
|
||||
if let Some(display_shortcut) = row.display_shortcut {
|
||||
spans.push(" (".into());
|
||||
spans.push(display_shortcut.into());
|
||||
spans.extend(display_shortcut.spans());
|
||||
spans.push(")".into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! Tab headers for selection lists, including a single-row filled picker variant.
|
||||
//!
|
||||
//! Filled tabs always retain the active tab and reserve arrows for hidden neighbors.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Styled;
|
||||
@@ -6,13 +10,21 @@ use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::style::accent_style;
|
||||
|
||||
use super::SelectionItem;
|
||||
use super::picker_style::active_tab_style;
|
||||
|
||||
const TAB_GAP_WIDTH: usize = 2;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) enum TabAppearance {
|
||||
Legacy,
|
||||
Filled,
|
||||
}
|
||||
|
||||
pub(crate) struct SelectionTab {
|
||||
pub(crate) id: String,
|
||||
pub(crate) label: String,
|
||||
@@ -20,22 +32,39 @@ pub(crate) struct SelectionTab {
|
||||
pub(crate) items: Vec<SelectionItem>,
|
||||
}
|
||||
|
||||
pub(crate) fn tab_bar_height(tabs: &[SelectionTab], active_idx: usize, width: u16) -> u16 {
|
||||
pub(super) fn tab_bar_height(
|
||||
tabs: &[SelectionTab],
|
||||
active_idx: usize,
|
||||
width: u16,
|
||||
appearance: TabAppearance,
|
||||
) -> u16 {
|
||||
if tabs.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
if matches!(appearance, TabAppearance::Filled) {
|
||||
return u16::from(width > 0);
|
||||
}
|
||||
tab_bar_lines(tabs, active_idx, width)
|
||||
.len()
|
||||
.try_into()
|
||||
.unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
pub(crate) fn render_tab_bar(
|
||||
pub(super) fn render_tab_bar(
|
||||
tabs: &[SelectionTab],
|
||||
active_idx: usize,
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
appearance: TabAppearance,
|
||||
) {
|
||||
if matches!(appearance, TabAppearance::Filled) {
|
||||
let labels = tabs
|
||||
.iter()
|
||||
.map(|tab| tab.label.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
render_filled_tab_bar(&labels, active_idx, area, buf);
|
||||
return;
|
||||
}
|
||||
for (offset, line) in tab_bar_lines(tabs, active_idx, area.width)
|
||||
.into_iter()
|
||||
.take(area.height as usize)
|
||||
@@ -53,6 +82,67 @@ pub(crate) fn render_tab_bar(
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a fixed-height tab strip while keeping the active tab visible.
|
||||
fn render_filled_tab_bar(labels: &[&str], active_idx: usize, area: Rect, buf: &mut Buffer) {
|
||||
if labels.is_empty() || area.is_empty() {
|
||||
return;
|
||||
}
|
||||
let active_idx = active_idx.min(labels.len() - 1);
|
||||
let widths = labels
|
||||
.iter()
|
||||
.map(|label| Line::from(*label).width() + 2)
|
||||
.collect::<Vec<_>>();
|
||||
let occupied = |start: usize, end: usize| {
|
||||
widths[start..end].iter().sum::<usize>() + end - start - 1
|
||||
+ usize::from(start > 0) * 2
|
||||
+ usize::from(end < labels.len()) * 2
|
||||
};
|
||||
let mut start = active_idx;
|
||||
let mut end = active_idx + 1;
|
||||
while start > 0 && occupied(start - 1, end) <= usize::from(area.width) {
|
||||
start -= 1;
|
||||
}
|
||||
while end < labels.len() && occupied(start, end + 1) <= usize::from(area.width) {
|
||||
end += 1;
|
||||
}
|
||||
|
||||
// Tiny strips prioritize a readable active label over navigation hints.
|
||||
let show_left = start > 0 && area.width >= 5;
|
||||
let show_right = end < labels.len() && area.width >= 7;
|
||||
let mut x = area.x;
|
||||
if show_left {
|
||||
Line::from("‹")
|
||||
.dim()
|
||||
.render(Rect::new(x, area.y, /*width*/ 1, /*height*/ 1), buf);
|
||||
x += 2;
|
||||
}
|
||||
let right = area.right().saturating_sub(u16::from(show_right) * 2);
|
||||
for idx in start..end {
|
||||
let width = widths[idx].min(usize::from(right.saturating_sub(x))) as u16;
|
||||
let line = Line::from(format!(" {} ", labels[idx]));
|
||||
let line = truncate_line_with_ellipsis_if_overflow(line, usize::from(width));
|
||||
let line = if idx == active_idx {
|
||||
line.style(active_tab_style())
|
||||
} else {
|
||||
line.dim()
|
||||
};
|
||||
let tab = Rect::new(x, area.y, width, /*height*/ 1);
|
||||
line.render(tab, buf);
|
||||
x = x.saturating_add(width).saturating_add(/*rhs*/ 1);
|
||||
}
|
||||
if show_right {
|
||||
Line::from("›").dim().render(
|
||||
Rect::new(
|
||||
area.right() - 1,
|
||||
area.y,
|
||||
/*width*/ 1,
|
||||
/*height*/ 1,
|
||||
),
|
||||
buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_bar_lines(tabs: &[SelectionTab], active_idx: usize, width: u16) -> Vec<Line<'static>> {
|
||||
if tabs.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -104,3 +194,7 @@ fn tab_unit(label: &str, active: bool) -> Vec<Span<'static>> {
|
||||
vec![label.to_string().dim()]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "selection_tabs_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Filled tabs preserve active visibility and Unicode clipping on narrow terminals.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
|
||||
fn rendered_strip(labels: &[&str], active_idx: usize, width: u16) -> (String, Buffer) {
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, width, /*height*/ 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
buf.set_style(
|
||||
area,
|
||||
Style::default().red().bg(Color::Yellow).underlined().dim(),
|
||||
);
|
||||
render_filled_tab_bar(labels, active_idx, area, &mut buf);
|
||||
let text = (0..width).map(|x| buf[(x, 0)].symbol()).collect::<String>();
|
||||
(text.trim_end().to_owned(), buf)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filled_tabs_window_around_active_tab() {
|
||||
crate::terminal_palette::with_test_default_colors(
|
||||
crate::terminal_probe::DefaultColors {
|
||||
fg: (30, 30, 30),
|
||||
bg: (255, 255, 255),
|
||||
},
|
||||
|| {
|
||||
let labels = [
|
||||
"All",
|
||||
"Common",
|
||||
"Customized (3)",
|
||||
"Unbound (4)",
|
||||
"App",
|
||||
"Composer",
|
||||
"Debug",
|
||||
];
|
||||
let strips =
|
||||
[0, 2, 6].map(|active_idx| rendered_strip(&labels, active_idx, /*width*/ 30).0);
|
||||
insta::assert_snapshot!(strips.join("\n"));
|
||||
for active_idx in 0..labels.len() {
|
||||
let (text, buf) = rendered_strip(&labels, active_idx, /*width*/ 30);
|
||||
assert!(text.contains(labels[active_idx]), "{text}");
|
||||
let focused = buf
|
||||
.content()
|
||||
.iter()
|
||||
.filter(|cell| cell.bg == active_tab_style().bg.unwrap())
|
||||
.map(|cell| {
|
||||
assert_eq!(cell.modifier, Modifier::BOLD);
|
||||
cell.symbol()
|
||||
})
|
||||
.collect::<String>();
|
||||
assert_eq!(focused, format!(" {} ", labels[active_idx]));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filled_tabs_truncate_unicode_without_hiding_active_tab() {
|
||||
let labels = ["All", "文件系统 plugins", "Debug"];
|
||||
let strips = [1, 4, 9, 16].map(|width| rendered_strip(&labels, /*active_idx*/ 1, width).0);
|
||||
insta::assert_snapshot!(strips.join("\n"));
|
||||
}
|
||||
|
||||
// Non-Windows tests have an unprobed default palette; Windows retains its console probe.
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn filled_tab_unknown_palette_uses_readable_underlined_defaults() {
|
||||
let (_, buf) = rendered_strip(&["All"], /*active_idx*/ 0, /*width*/ 5);
|
||||
assert_eq!(
|
||||
buf.content()
|
||||
.iter()
|
||||
.map(|cell| (cell.fg, cell.bg, cell.modifier))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(
|
||||
Color::Reset,
|
||||
Color::Reset,
|
||||
Modifier::BOLD | Modifier::UNDERLINED
|
||||
);
|
||||
5
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -143,6 +143,7 @@ impl SkillPopup {
|
||||
_ => None,
|
||||
};
|
||||
GenericDisplayRow {
|
||||
selection_style: None,
|
||||
name,
|
||||
name_prefix_spans: Vec::new(),
|
||||
match_indices: indices,
|
||||
|
||||
@@ -594,8 +594,8 @@ mod tests {
|
||||
let view = SkillsToggleView::new(Vec::new(), tx, keymap);
|
||||
let rendered = render_lines(&view, /*width*/ 72);
|
||||
|
||||
assert!(rendered.contains("ctrl + t"));
|
||||
assert!(rendered.contains("ctrl + x"));
|
||||
assert!(rendered.contains("ctrl+t"));
|
||||
assert!(rendered.contains("ctrl+x"));
|
||||
assert!(!rendered.contains("enter"));
|
||||
assert!(!rendered.contains("esc"));
|
||||
}
|
||||
|
||||
+8
-4
@@ -24,14 +24,18 @@ Buffer {
|
||||
x: 57, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 10, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC,
|
||||
x: 33, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 6, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 0, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 28, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 53, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 53, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 54, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 45, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 45, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 46, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 48, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 48, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 51, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 8, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 13, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 28, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 31, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
]
|
||||
}
|
||||
|
||||
+7
-7
@@ -10,13 +10,13 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" / 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 "
|
||||
" ← for agents (empty prompt) "
|
||||
" "
|
||||
" customize shortcuts with /keymap "
|
||||
|
||||
+7
-7
@@ -10,12 +10,12 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" / for commands ! for shell commands "
|
||||
" ctrl + x enter for newline tab to submit message "
|
||||
" @ for file paths ctrl + v to paste images "
|
||||
" ctrl + g 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+x enter for newline tab to submit message "
|
||||
" @ for file paths ctrl+v to paste images "
|
||||
" ctrl+g 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 "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ctrl + c again to quit "
|
||||
" ctrl+c again to quit "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ctrl + c again to quit "
|
||||
" ctrl+c again to quit "
|
||||
|
||||
+7
-7
@@ -10,12 +10,12 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" / for commands ! for shell commands "
|
||||
" shift + enter for newline tab to submit message "
|
||||
" @ for file paths ctrl + v to paste images "
|
||||
" ctrl + g to edit in external editor esc again 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 "
|
||||
" shift+enter for newline tab to submit message "
|
||||
" @ for file paths ctrl+v to paste images "
|
||||
" ctrl+g to edit in external editor esc again 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 "
|
||||
|
||||
+7
-7
@@ -10,12 +10,12 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" / for commands ! for shell commands "
|
||||
" shift + enter for newline tab to queue 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 "
|
||||
" shift+enter for newline tab to queue 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 "
|
||||
|
||||
+3
-2
@@ -19,9 +19,10 @@ Buffer {
|
||||
x: 2, y: 1, fg: Reset, bg: Rgb(244, 244, 244), underline: Reset, modifier: DIM,
|
||||
x: 26, y: 1, fg: Reset, bg: Rgb(244, 244, 244), underline: Reset, modifier: NONE,
|
||||
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 2, y: 5, fg: Rgb(110, 112, 108), bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 3, y: 5, fg: Rgb(118, 119, 117), bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 17, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 29, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 5, fg: Rgb(118, 119, 117), bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 46, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ Buffer {
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 52, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 6, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ Buffer {
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 65, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 5, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ Buffer {
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 68, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: tui/src/bottom_pane/footer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" ctrl + c again to quit "
|
||||
" ctrl+c again to quit "
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: tui/src/bottom_pane/footer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" ctrl + c again to quit "
|
||||
" ctrl+c again to quit "
|
||||
|
||||
+7
-7
@@ -2,12 +2,12 @@
|
||||
source: tui/src/bottom_pane/footer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" / 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 "
|
||||
" shift + tab to change mode 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 "
|
||||
" shift+tab to change mode ctrl+t to view transcript "
|
||||
" "
|
||||
" customize shortcuts with /keymap "
|
||||
|
||||
+7
-7
@@ -2,12 +2,12 @@
|
||||
source: tui/src/bottom_pane/footer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" / for commands ! for shell commands "
|
||||
" ctrl + j for newline tab to queue 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 interrupt "
|
||||
" ⌥ + , reasoning down ⌥ + . reasoning up "
|
||||
" ctrl + t to view transcript "
|
||||
" / for commands ! for shell commands "
|
||||
" ctrl+j for newline tab to queue 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 interrupt "
|
||||
" ⌥+, reasoning down ⌥+. reasoning up "
|
||||
" ctrl+t to view transcript "
|
||||
" "
|
||||
" customize shortcuts with /keymap "
|
||||
|
||||
+7
-7
@@ -2,12 +2,12 @@
|
||||
source: tui/src/bottom_pane/footer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" / for commands ! for shell commands "
|
||||
" shift + enter for newline tab to submit message "
|
||||
" @ for file paths ctrl + v to paste images "
|
||||
" ctrl + g to edit in external editor esc again 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 "
|
||||
" shift+enter for newline tab to submit message "
|
||||
" @ for file paths ctrl+v to paste images "
|
||||
" ctrl+g to edit in external editor esc again 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 "
|
||||
|
||||
+6
-7
@@ -1,33 +1,32 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/footer.rs
|
||||
assertion_line: 1355
|
||||
expression: "(backend.to_string(), style_runs)"
|
||||
---
|
||||
(
|
||||
"\" voice ● listen ctrl+m mute \"\n",
|
||||
"\" voice ● listen · ctrl+m mute \"\n",
|
||||
[
|
||||
(
|
||||
0,
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset),
|
||||
),
|
||||
(
|
||||
3,
|
||||
2,
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold(),
|
||||
),
|
||||
(
|
||||
8,
|
||||
7,
|
||||
Style::new().red().bg(Color::Reset).underline_color(Color::Reset),
|
||||
),
|
||||
(
|
||||
10,
|
||||
9,
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset),
|
||||
),
|
||||
(
|
||||
20,
|
||||
19,
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).bold(),
|
||||
),
|
||||
(
|
||||
26,
|
||||
25,
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset),
|
||||
),
|
||||
],
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/selection_picker_tests.rs
|
||||
assertion_line: 79
|
||||
expression: text
|
||||
---
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
|
||||
All Common Customized (3) Unbound (4) App
|
||||
|
||||
Type to search shortcuts
|
||||
↑
|
||||
Action 01 Ctrl+1
|
||||
Action 02 Ctrl+2
|
||||
Action 03 Ctrl+3
|
||||
Action 04 Ctrl+4
|
||||
Action 05 Ctrl+5
|
||||
Action 06 Ctrl+6
|
||||
Action 07 Ctrl+7
|
||||
› Action 08 Ctrl+8
|
||||
↓
|
||||
left/right group · enter edit · esc close
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user