normalize key hints (#4586)
render key hints the same everywhere. | Before | After | |--------|-------| | <img width="816" height="172" alt="Screenshot 2025-10-01 at 5 15 42 PM" src="https://github.com/user-attachments/assets/f88d5db4-04bb-4e89-b571-568222c41e4b" /> | <img width="672" height="137" alt="Screenshot 2025-10-01 at 5 13 56 PM" src="https://github.com/user-attachments/assets/1fee6a71-f313-4620-8d9a-10766dc4e195" /> | | <img width="816" height="172" alt="Screenshot 2025-10-01 at 5 17 01 PM" src="https://github.com/user-attachments/assets/5170ab35-88b7-4131-b485-ecebea9f0835" /> | <img width="816" height="174" alt="Screenshot 2025-10-01 at 5 14 24 PM" src="https://github.com/user-attachments/assets/6b6bc64c-25b9-4824-b2d7-56f60370870a" /> | | <img width="816" height="172" alt="Screenshot 2025-10-01 at 5 17 29 PM" src="https://github.com/user-attachments/assets/2313b36a-e0a8-4cd2-82be-7d0fe7793c19" /> | <img width="816" height="134" alt="Screenshot 2025-10-01 at 5 14 37 PM" src="https://github.com/user-attachments/assets/e18934e8-8e9d-4f46-9809-39c8cb6ee893" /> | | <img width="816" height="172" alt="Screenshot 2025-10-01 at 5 17 40 PM" src="https://github.com/user-attachments/assets/0cc69e4e-8cce-420a-b3e4-be75a7e2c8f5" /> | <img width="816" height="134" alt="Screenshot 2025-10-01 at 5 14 56 PM" src="https://github.com/user-attachments/assets/329a5121-ae4a-4829-86e5-4c813543770c" /> |
This commit is contained in:
@@ -11,6 +11,7 @@ use crate::bottom_pane::list_selection_view::SelectionViewParams;
|
|||||||
use crate::diff_render::DiffSummary;
|
use crate::diff_render::DiffSummary;
|
||||||
use crate::exec_command::strip_bash_lc_and_escape;
|
use crate::exec_command::strip_bash_lc_and_escape;
|
||||||
use crate::history_cell;
|
use crate::history_cell;
|
||||||
|
use crate::key_hint;
|
||||||
use crate::render::highlight::highlight_bash_to_lines;
|
use crate::render::highlight::highlight_bash_to_lines;
|
||||||
use crate::render::renderable::ColumnRenderable;
|
use crate::render::renderable::ColumnRenderable;
|
||||||
use crate::render::renderable::Renderable;
|
use crate::render::renderable::Renderable;
|
||||||
@@ -116,7 +117,13 @@ impl ApprovalOverlay {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let params = SelectionViewParams {
|
let params = SelectionViewParams {
|
||||||
footer_hint: Some("Press Enter to confirm or Esc to cancel".to_string()),
|
footer_hint: Some(Line::from(vec![
|
||||||
|
"Press ".into(),
|
||||||
|
key_hint::plain(KeyCode::Enter).into(),
|
||||||
|
" to confirm or ".into(),
|
||||||
|
key_hint::plain(KeyCode::Esc).into(),
|
||||||
|
" to cancel".into(),
|
||||||
|
])),
|
||||||
items,
|
items,
|
||||||
header,
|
header,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use std::cell::RefCell;
|
|||||||
|
|
||||||
use crate::render::renderable::Renderable;
|
use crate::render::renderable::Renderable;
|
||||||
|
|
||||||
use super::popup_consts::STANDARD_POPUP_HINT_LINE;
|
use super::popup_consts::standard_popup_hint_line;
|
||||||
|
|
||||||
use super::CancellationEvent;
|
use super::CancellationEvent;
|
||||||
use super::bottom_pane_view::BottomPaneView;
|
use super::bottom_pane_view::BottomPaneView;
|
||||||
@@ -221,7 +221,7 @@ impl Renderable for CustomPromptView {
|
|||||||
|
|
||||||
let hint_y = hint_blank_y.saturating_add(1);
|
let hint_y = hint_blank_y.saturating_add(1);
|
||||||
if hint_y < area.y.saturating_add(area.height) {
|
if hint_y < area.y.saturating_add(area.height) {
|
||||||
Paragraph::new(STANDARD_POPUP_HINT_LINE).render(
|
Paragraph::new(standard_popup_hint_line()).render(
|
||||||
Rect {
|
Rect {
|
||||||
x: area.x,
|
x: area.x,
|
||||||
y: hint_y,
|
y: hint_y,
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
use crate::key_hint;
|
||||||
|
use crate::key_hint::KeyBinding;
|
||||||
|
use crate::render::line_utils::prefix_lines;
|
||||||
use crate::ui_consts::FOOTER_INDENT_COLS;
|
use crate::ui_consts::FOOTER_INDENT_COLS;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyModifiers;
|
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
use ratatui::text::Span;
|
use ratatui::text::Span;
|
||||||
use ratatui::widgets::WidgetRef;
|
use ratatui::widgets::Paragraph;
|
||||||
use std::iter;
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct FooterProps {
|
pub(crate) struct FooterProps {
|
||||||
@@ -61,15 +63,12 @@ pub(crate) fn footer_height(props: FooterProps) -> u16 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) {
|
pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) {
|
||||||
let lines = footer_lines(props);
|
Paragraph::new(prefix_lines(
|
||||||
for (idx, line) in lines.into_iter().enumerate() {
|
footer_lines(props),
|
||||||
let y = area.y + idx as u16;
|
" ".repeat(FOOTER_INDENT_COLS).into(),
|
||||||
if y >= area.y + area.height {
|
" ".repeat(FOOTER_INDENT_COLS).into(),
|
||||||
break;
|
))
|
||||||
}
|
.render(area, buf);
|
||||||
let row = Rect::new(area.x, y, area.width, 1);
|
|
||||||
line.render_ref(row, buf);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
|
fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
|
||||||
@@ -81,7 +80,10 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
|
|||||||
if props.is_task_running {
|
if props.is_task_running {
|
||||||
vec![context_window_line(props.context_window_percent)]
|
vec![context_window_line(props.context_window_percent)]
|
||||||
} else {
|
} else {
|
||||||
vec![dim_line(indent_text("? for shortcuts"))]
|
vec![Line::from(vec![
|
||||||
|
key_hint::plain(KeyCode::Char('?')).into(),
|
||||||
|
" for shortcuts".dim(),
|
||||||
|
])]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState {
|
FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState {
|
||||||
@@ -110,27 +112,36 @@ fn ctrl_c_reminder_line(state: CtrlCReminderState) -> Line<'static> {
|
|||||||
} else {
|
} else {
|
||||||
"quit"
|
"quit"
|
||||||
};
|
};
|
||||||
let text = format!("ctrl + c again to {action}");
|
Line::from(vec![
|
||||||
dim_line(indent_text(&text))
|
key_hint::ctrl(KeyCode::Char('c')).into(),
|
||||||
|
format!(" again to {action}").into(),
|
||||||
|
])
|
||||||
|
.dim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn esc_hint_line(esc_backtrack_hint: bool) -> Line<'static> {
|
fn esc_hint_line(esc_backtrack_hint: bool) -> Line<'static> {
|
||||||
let text = if esc_backtrack_hint {
|
let esc = key_hint::plain(KeyCode::Esc);
|
||||||
"esc again to edit previous message"
|
if esc_backtrack_hint {
|
||||||
|
Line::from(vec![esc.into(), " again to edit previous message".into()]).dim()
|
||||||
} else {
|
} else {
|
||||||
"esc esc to edit previous message"
|
Line::from(vec![
|
||||||
};
|
esc.into(),
|
||||||
dim_line(indent_text(text))
|
" ".into(),
|
||||||
|
esc.into(),
|
||||||
|
" to edit previous message".into(),
|
||||||
|
])
|
||||||
|
.dim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shortcut_overlay_lines(state: ShortcutsState) -> Vec<Line<'static>> {
|
fn shortcut_overlay_lines(state: ShortcutsState) -> Vec<Line<'static>> {
|
||||||
let mut commands = String::new();
|
let mut commands = Line::from("");
|
||||||
let mut newline = String::new();
|
let mut newline = Line::from("");
|
||||||
let mut file_paths = String::new();
|
let mut file_paths = Line::from("");
|
||||||
let mut paste_image = String::new();
|
let mut paste_image = Line::from("");
|
||||||
let mut edit_previous = String::new();
|
let mut edit_previous = Line::from("");
|
||||||
let mut quit = String::new();
|
let mut quit = Line::from("");
|
||||||
let mut show_transcript = String::new();
|
let mut show_transcript = Line::from("");
|
||||||
|
|
||||||
for descriptor in SHORTCUTS {
|
for descriptor in SHORTCUTS {
|
||||||
if let Some(text) = descriptor.overlay_entry(state) {
|
if let Some(text) = descriptor.overlay_entry(state) {
|
||||||
@@ -153,14 +164,14 @@ fn shortcut_overlay_lines(state: ShortcutsState) -> Vec<Line<'static>> {
|
|||||||
paste_image,
|
paste_image,
|
||||||
edit_previous,
|
edit_previous,
|
||||||
quit,
|
quit,
|
||||||
String::new(),
|
Line::from(""),
|
||||||
show_transcript,
|
show_transcript,
|
||||||
];
|
];
|
||||||
|
|
||||||
build_columns(ordered)
|
build_columns(ordered)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_columns(entries: Vec<String>) -> Vec<Line<'static>> {
|
fn build_columns(entries: Vec<Line<'static>>) -> Vec<Line<'static>> {
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
@@ -174,7 +185,7 @@ fn build_columns(entries: Vec<String>) -> Vec<Line<'static>> {
|
|||||||
let mut entries = entries;
|
let mut entries = entries;
|
||||||
if entries.len() < target_len {
|
if entries.len() < target_len {
|
||||||
entries.extend(std::iter::repeat_n(
|
entries.extend(std::iter::repeat_n(
|
||||||
String::new(),
|
Line::from(""),
|
||||||
target_len - entries.len(),
|
target_len - entries.len(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -183,7 +194,7 @@ fn build_columns(entries: Vec<String>) -> Vec<Line<'static>> {
|
|||||||
|
|
||||||
for (idx, entry) in entries.iter().enumerate() {
|
for (idx, entry) in entries.iter().enumerate() {
|
||||||
let column = idx % COLUMNS;
|
let column = idx % COLUMNS;
|
||||||
column_widths[column] = column_widths[column].max(entry.len());
|
column_widths[column] = column_widths[column].max(entry.width());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (idx, width) in column_widths.iter_mut().enumerate() {
|
for (idx, width) in column_widths.iter_mut().enumerate() {
|
||||||
@@ -193,42 +204,30 @@ fn build_columns(entries: Vec<String>) -> Vec<Line<'static>> {
|
|||||||
entries
|
entries
|
||||||
.chunks(COLUMNS)
|
.chunks(COLUMNS)
|
||||||
.map(|chunk| {
|
.map(|chunk| {
|
||||||
let mut line = String::new();
|
let mut line = Line::from("");
|
||||||
for (col, entry) in chunk.iter().enumerate() {
|
for (col, entry) in chunk.iter().enumerate() {
|
||||||
line.push_str(entry);
|
line.extend(entry.spans.clone());
|
||||||
if col < COLUMNS - 1 {
|
if col < COLUMNS - 1 {
|
||||||
let target_width = column_widths[col];
|
let target_width = column_widths[col];
|
||||||
let padding = target_width.saturating_sub(entry.len()) + COLUMN_GAP;
|
let padding = target_width.saturating_sub(entry.width()) + COLUMN_GAP;
|
||||||
line.push_str(&" ".repeat(padding));
|
line.push_span(Span::from(" ".repeat(padding)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let indented = indent_text(&line);
|
line.dim()
|
||||||
dim_line(indented)
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn indent_text(text: &str) -> String {
|
|
||||||
let mut indented = String::with_capacity(FOOTER_INDENT_COLS + text.len());
|
|
||||||
indented.extend(iter::repeat_n(' ', FOOTER_INDENT_COLS));
|
|
||||||
indented.push_str(text);
|
|
||||||
indented
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dim_line(text: String) -> Line<'static> {
|
|
||||||
Line::from(text).dim()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn context_window_line(percent: Option<u8>) -> Line<'static> {
|
fn context_window_line(percent: Option<u8>) -> Line<'static> {
|
||||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||||
spans.push(indent_text("").into());
|
|
||||||
match percent {
|
match percent {
|
||||||
Some(percent) => {
|
Some(percent) => {
|
||||||
spans.push(format!("{percent}%").bold());
|
spans.push(format!("{percent}%").bold());
|
||||||
spans.push(" context left".dim());
|
spans.push(" context left".dim());
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
spans.push("? for shortcuts".dim());
|
spans.push(key_hint::plain(KeyCode::Char('?')).into());
|
||||||
|
spans.push(" for shortcuts".dim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
@@ -247,9 +246,7 @@ enum ShortcutId {
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
struct ShortcutBinding {
|
struct ShortcutBinding {
|
||||||
code: KeyCode,
|
key: KeyBinding,
|
||||||
modifiers: KeyModifiers,
|
|
||||||
overlay_text: &'static str,
|
|
||||||
condition: DisplayCondition,
|
condition: DisplayCondition,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,20 +285,24 @@ impl ShortcutDescriptor {
|
|||||||
self.bindings.iter().find(|binding| binding.matches(state))
|
self.bindings.iter().find(|binding| binding.matches(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overlay_entry(&self, state: ShortcutsState) -> Option<String> {
|
fn overlay_entry(&self, state: ShortcutsState) -> Option<Line<'static>> {
|
||||||
let binding = self.binding_for(state)?;
|
let binding = self.binding_for(state)?;
|
||||||
let label = match self.id {
|
let mut line = Line::from(vec![self.prefix.into(), binding.key.into()]);
|
||||||
|
match self.id {
|
||||||
ShortcutId::EditPrevious => {
|
ShortcutId::EditPrevious => {
|
||||||
if state.esc_backtrack_hint {
|
if state.esc_backtrack_hint {
|
||||||
" again to edit previous message"
|
line.push_span(" again to edit previous message");
|
||||||
} else {
|
} else {
|
||||||
" esc to edit previous message"
|
line.extend(vec![
|
||||||
|
" ".into(),
|
||||||
|
key_hint::plain(KeyCode::Esc).into(),
|
||||||
|
" to edit previous message".into(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => self.label,
|
_ => line.push_span(self.label),
|
||||||
};
|
};
|
||||||
let text = format!("{}{}{}", self.prefix, binding.overlay_text, label);
|
Some(line)
|
||||||
Some(text)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,9 +310,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
|
|||||||
ShortcutDescriptor {
|
ShortcutDescriptor {
|
||||||
id: ShortcutId::Commands,
|
id: ShortcutId::Commands,
|
||||||
bindings: &[ShortcutBinding {
|
bindings: &[ShortcutBinding {
|
||||||
code: KeyCode::Char('/'),
|
key: key_hint::plain(KeyCode::Char('/')),
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
overlay_text: "/",
|
|
||||||
condition: DisplayCondition::Always,
|
condition: DisplayCondition::Always,
|
||||||
}],
|
}],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
@@ -321,15 +320,11 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
|
|||||||
id: ShortcutId::InsertNewline,
|
id: ShortcutId::InsertNewline,
|
||||||
bindings: &[
|
bindings: &[
|
||||||
ShortcutBinding {
|
ShortcutBinding {
|
||||||
code: KeyCode::Enter,
|
key: key_hint::shift(KeyCode::Enter),
|
||||||
modifiers: KeyModifiers::SHIFT,
|
|
||||||
overlay_text: "shift + enter",
|
|
||||||
condition: DisplayCondition::WhenShiftEnterHint,
|
condition: DisplayCondition::WhenShiftEnterHint,
|
||||||
},
|
},
|
||||||
ShortcutBinding {
|
ShortcutBinding {
|
||||||
code: KeyCode::Char('j'),
|
key: key_hint::ctrl(KeyCode::Char('j')),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
|
||||||
overlay_text: "ctrl + j",
|
|
||||||
condition: DisplayCondition::WhenNotShiftEnterHint,
|
condition: DisplayCondition::WhenNotShiftEnterHint,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -339,9 +334,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
|
|||||||
ShortcutDescriptor {
|
ShortcutDescriptor {
|
||||||
id: ShortcutId::FilePaths,
|
id: ShortcutId::FilePaths,
|
||||||
bindings: &[ShortcutBinding {
|
bindings: &[ShortcutBinding {
|
||||||
code: KeyCode::Char('@'),
|
key: key_hint::plain(KeyCode::Char('@')),
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
overlay_text: "@",
|
|
||||||
condition: DisplayCondition::Always,
|
condition: DisplayCondition::Always,
|
||||||
}],
|
}],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
@@ -350,9 +343,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
|
|||||||
ShortcutDescriptor {
|
ShortcutDescriptor {
|
||||||
id: ShortcutId::PasteImage,
|
id: ShortcutId::PasteImage,
|
||||||
bindings: &[ShortcutBinding {
|
bindings: &[ShortcutBinding {
|
||||||
code: KeyCode::Char('v'),
|
key: key_hint::ctrl(KeyCode::Char('v')),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
|
||||||
overlay_text: "ctrl + v",
|
|
||||||
condition: DisplayCondition::Always,
|
condition: DisplayCondition::Always,
|
||||||
}],
|
}],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
@@ -361,9 +352,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
|
|||||||
ShortcutDescriptor {
|
ShortcutDescriptor {
|
||||||
id: ShortcutId::EditPrevious,
|
id: ShortcutId::EditPrevious,
|
||||||
bindings: &[ShortcutBinding {
|
bindings: &[ShortcutBinding {
|
||||||
code: KeyCode::Esc,
|
key: key_hint::plain(KeyCode::Esc),
|
||||||
modifiers: KeyModifiers::NONE,
|
|
||||||
overlay_text: "esc",
|
|
||||||
condition: DisplayCondition::Always,
|
condition: DisplayCondition::Always,
|
||||||
}],
|
}],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
@@ -372,9 +361,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
|
|||||||
ShortcutDescriptor {
|
ShortcutDescriptor {
|
||||||
id: ShortcutId::Quit,
|
id: ShortcutId::Quit,
|
||||||
bindings: &[ShortcutBinding {
|
bindings: &[ShortcutBinding {
|
||||||
code: KeyCode::Char('c'),
|
key: key_hint::ctrl(KeyCode::Char('c')),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
|
||||||
overlay_text: "ctrl + c",
|
|
||||||
condition: DisplayCondition::Always,
|
condition: DisplayCondition::Always,
|
||||||
}],
|
}],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
@@ -383,9 +370,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
|
|||||||
ShortcutDescriptor {
|
ShortcutDescriptor {
|
||||||
id: ShortcutId::ShowTranscript,
|
id: ShortcutId::ShowTranscript,
|
||||||
bindings: &[ShortcutBinding {
|
bindings: &[ShortcutBinding {
|
||||||
code: KeyCode::Char('t'),
|
key: key_hint::ctrl(KeyCode::Char('t')),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
|
||||||
overlay_text: "ctrl + t",
|
|
||||||
condition: DisplayCondition::Always,
|
condition: DisplayCondition::Always,
|
||||||
}],
|
}],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub(crate) struct SelectionItem {
|
|||||||
pub(crate) struct SelectionViewParams {
|
pub(crate) struct SelectionViewParams {
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub subtitle: Option<String>,
|
pub subtitle: Option<String>,
|
||||||
pub footer_hint: Option<String>,
|
pub footer_hint: Option<Line<'static>>,
|
||||||
pub items: Vec<SelectionItem>,
|
pub items: Vec<SelectionItem>,
|
||||||
pub is_searchable: bool,
|
pub is_searchable: bool,
|
||||||
pub search_placeholder: Option<String>,
|
pub search_placeholder: Option<String>,
|
||||||
@@ -65,7 +65,7 @@ impl Default for SelectionViewParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ListSelectionView {
|
pub(crate) struct ListSelectionView {
|
||||||
footer_hint: Option<String>,
|
footer_hint: Option<Line<'static>>,
|
||||||
items: Vec<SelectionItem>,
|
items: Vec<SelectionItem>,
|
||||||
state: ScrollState,
|
state: ScrollState,
|
||||||
complete: bool,
|
complete: bool,
|
||||||
@@ -416,7 +416,7 @@ impl Renderable for ListSelectionView {
|
|||||||
width: footer_area.width.saturating_sub(2),
|
width: footer_area.width.saturating_sub(2),
|
||||||
height: footer_area.height,
|
height: footer_area.height,
|
||||||
};
|
};
|
||||||
Line::from(hint.clone().dim()).render(hint_area, buf);
|
hint.clone().dim().render(hint_area, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +425,7 @@ impl Renderable for ListSelectionView {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::app_event::AppEvent;
|
use crate::app_event::AppEvent;
|
||||||
use crate::bottom_pane::popup_consts::STANDARD_POPUP_HINT_LINE;
|
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||||
use insta::assert_snapshot;
|
use insta::assert_snapshot;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
@@ -455,7 +455,7 @@ mod tests {
|
|||||||
SelectionViewParams {
|
SelectionViewParams {
|
||||||
title: Some("Select Approval Mode".to_string()),
|
title: Some("Select Approval Mode".to_string()),
|
||||||
subtitle: subtitle.map(str::to_string),
|
subtitle: subtitle.map(str::to_string),
|
||||||
footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -517,7 +517,7 @@ mod tests {
|
|||||||
let mut view = ListSelectionView::new(
|
let mut view = ListSelectionView::new(
|
||||||
SelectionViewParams {
|
SelectionViewParams {
|
||||||
title: Some("Select Approval Mode".to_string()),
|
title: Some("Select Approval Mode".to_string()),
|
||||||
footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
is_searchable: true,
|
is_searchable: true,
|
||||||
search_placeholder: Some("Type to search branches".to_string()),
|
search_placeholder: Some("Type to search branches".to_string()),
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
//! Shared popup-related constants for bottom pane widgets.
|
//! Shared popup-related constants for bottom pane widgets.
|
||||||
|
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
use ratatui::text::Line;
|
||||||
|
|
||||||
|
use crate::key_hint;
|
||||||
|
|
||||||
/// Maximum number of rows any popup should attempt to display.
|
/// Maximum number of rows any popup should attempt to display.
|
||||||
/// Keep this consistent across all popups for a uniform feel.
|
/// Keep this consistent across all popups for a uniform feel.
|
||||||
pub(crate) const MAX_POPUP_ROWS: usize = 8;
|
pub(crate) const MAX_POPUP_ROWS: usize = 8;
|
||||||
|
|
||||||
/// Standard footer hint text used by popups.
|
/// Standard footer hint text used by popups.
|
||||||
pub(crate) const STANDARD_POPUP_HINT_LINE: &str = "Press Enter to confirm or Esc to go back";
|
pub(crate) fn standard_popup_hint_line() -> Line<'static> {
|
||||||
|
Line::from(vec![
|
||||||
|
"Press ".into(),
|
||||||
|
key_hint::plain(KeyCode::Enter).into(),
|
||||||
|
" to confirm or ".into(),
|
||||||
|
key_hint::plain(KeyCode::Esc).into(),
|
||||||
|
" to go back".into(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: tui/src/bottom_pane/chat_composer.rs
|
source: tui/src/bottom_pane/chat_composer.rs
|
||||||
assertion_line: 1497
|
|
||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: tui/src/bottom_pane/footer.rs
|
source: tui/src/bottom_pane/footer.rs
|
||||||
assertion_line: 389
|
|
||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
" / for commands shift + enter for newline "
|
" / for commands shift + enter for newline "
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ expression: render_lines(&view)
|
|||||||
› 1. Read Only (current) Codex can read files
|
› 1. Read Only (current) Codex can read files
|
||||||
2. Full Access Codex can edit files
|
2. Full Access Codex can edit files
|
||||||
|
|
||||||
Press Enter to confirm or Esc to go back
|
Press enter to confirm or esc to go back
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ expression: render_lines(&view)
|
|||||||
› 1. Read Only (current) Codex can read files
|
› 1. Read Only (current) Codex can read files
|
||||||
2. Full Access Codex can edit files
|
2. Full Access Codex can edit files
|
||||||
|
|
||||||
Press Enter to confirm or Esc to go back
|
Press enter to confirm or esc to go back
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ use crate::bottom_pane::SelectionAction;
|
|||||||
use crate::bottom_pane::SelectionItem;
|
use crate::bottom_pane::SelectionItem;
|
||||||
use crate::bottom_pane::SelectionViewParams;
|
use crate::bottom_pane::SelectionViewParams;
|
||||||
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
|
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
|
||||||
use crate::bottom_pane::popup_consts::STANDARD_POPUP_HINT_LINE;
|
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||||
use crate::clipboard_paste::paste_image_to_temp_png;
|
use crate::clipboard_paste::paste_image_to_temp_png;
|
||||||
use crate::diff_render::display_path_for;
|
use crate::diff_render::display_path_for;
|
||||||
use crate::exec_cell::CommandOutput;
|
use crate::exec_cell::CommandOutput;
|
||||||
@@ -1625,7 +1625,7 @@ impl ChatWidget {
|
|||||||
subtitle: Some(
|
subtitle: Some(
|
||||||
"Switch between OpenAI models for this and future Codex CLI session".to_string(),
|
"Switch between OpenAI models for this and future Codex CLI session".to_string(),
|
||||||
),
|
),
|
||||||
footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
@@ -1668,7 +1668,7 @@ impl ChatWidget {
|
|||||||
|
|
||||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||||
title: Some("Select Approval Mode".to_string()),
|
title: Some("Select Approval Mode".to_string()),
|
||||||
footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
@@ -1843,7 +1843,7 @@ impl ChatWidget {
|
|||||||
|
|
||||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||||
title: Some("Select a review preset".into()),
|
title: Some("Select a review preset".into()),
|
||||||
footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
@@ -1879,7 +1879,7 @@ impl ChatWidget {
|
|||||||
|
|
||||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||||
title: Some("Select a base branch".to_string()),
|
title: Some("Select a base branch".to_string()),
|
||||||
footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
is_searchable: true,
|
is_searchable: true,
|
||||||
search_placeholder: Some("Type to search branches".to_string()),
|
search_placeholder: Some("Type to search branches".to_string()),
|
||||||
@@ -1920,7 +1920,7 @@ impl ChatWidget {
|
|||||||
|
|
||||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||||
title: Some("Select a commit to review".to_string()),
|
title: Some("Select a commit to review".to_string()),
|
||||||
footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
is_searchable: true,
|
is_searchable: true,
|
||||||
search_placeholder: Some("Type to search commits".to_string()),
|
search_placeholder: Some("Type to search commits".to_string()),
|
||||||
@@ -2145,7 +2145,7 @@ pub(crate) fn show_review_commit_picker_with_entries(
|
|||||||
|
|
||||||
chat.bottom_pane.show_selection_view(SelectionViewParams {
|
chat.bottom_pane.show_selection_view(SelectionViewParams {
|
||||||
title: Some("Select a commit to review".to_string()),
|
title: Some("Select a commit to review".to_string()),
|
||||||
footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
is_searchable: true,
|
is_searchable: true,
|
||||||
search_placeholder: Some("Type to search commits".to_string()),
|
search_placeholder: Some("Type to search commits".to_string()),
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ expression: terminal.backend().vt100().screen().contents()
|
|||||||
rest of the session
|
rest of the session
|
||||||
3. Cancel Do not run the command
|
3. Cancel Do not run the command
|
||||||
|
|
||||||
Press Enter to confirm or Esc to cancel
|
Press enter to confirm or esc to cancel
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ expression: terminal.backend()
|
|||||||
" rest of the session "
|
" rest of the session "
|
||||||
" 3. Cancel Do not run the command "
|
" 3. Cancel Do not run the command "
|
||||||
" "
|
" "
|
||||||
" Press Enter to confirm or Esc to cancel "
|
" Press enter to confirm or esc to cancel "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ expression: terminal.backend()
|
|||||||
"› 1. Approve Apply the proposed changes "
|
"› 1. Approve Apply the proposed changes "
|
||||||
" 2. Cancel Do not apply the changes "
|
" 2. Cancel Do not apply the changes "
|
||||||
" "
|
" "
|
||||||
" Press Enter to confirm or Esc to cancel "
|
" Press enter to confirm or esc to cancel "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
source: tui/src/chatwidget/tests.rs
|
source: tui/src/chatwidget/tests.rs
|
||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
" Thinking (0s • Esc to interrupt) "
|
" Thinking (0s • esc to interrupt) "
|
||||||
"› Ask Codex to do anything "
|
"› Ask Codex to do anything "
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ expression: term.backend().vt100().screen().contents()
|
|||||||
└ Search Change Approved
|
└ Search Change Approved
|
||||||
Read diff_render.rs
|
Read diff_render.rs
|
||||||
|
|
||||||
Investigating rendering code (0s • Esc to interrupt)
|
Investigating rendering code (0s • esc to interrupt)
|
||||||
|
|
||||||
|
|
||||||
› Summarize recent commits
|
› Summarize recent commits
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Buffer {
|
|||||||
" rest of the session ",
|
" rest of the session ",
|
||||||
" 3. Cancel Do not run the command ",
|
" 3. Cancel Do not run the command ",
|
||||||
" ",
|
" ",
|
||||||
" Press Enter to confirm or Esc to cancel ",
|
" Press enter to confirm or esc to cancel ",
|
||||||
" ",
|
" ",
|
||||||
],
|
],
|
||||||
styles: [
|
styles: [
|
||||||
@@ -37,6 +37,6 @@ Buffer {
|
|||||||
x: 34, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
x: 34, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||||
x: 56, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
x: 56, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||||
x: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
x: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||||
x: 41, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
x: 0, y: 14, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ source: tui/src/chatwidget/tests.rs
|
|||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
" "
|
" "
|
||||||
" Analyzing (0s • Esc to interrupt) "
|
" Analyzing (0s • esc to interrupt) "
|
||||||
" "
|
" "
|
||||||
" "
|
" "
|
||||||
"› Ask Codex to do anything "
|
"› Ask Codex to do anything "
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ expression: terminal.backend()
|
|||||||
" rest of the session "
|
" rest of the session "
|
||||||
" 3. Cancel Do not run the command "
|
" 3. Cancel Do not run the command "
|
||||||
" "
|
" "
|
||||||
" Press Enter to confirm or Esc to cancel "
|
" Press enter to confirm or esc to cancel "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -1,23 +1,86 @@
|
|||||||
|
use crossterm::event::KeyCode;
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use crossterm::event::KeyEventKind;
|
||||||
|
use crossterm::event::KeyModifiers;
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
use ratatui::text::Span;
|
use ratatui::text::Span;
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
const ALT_PREFIX: &str = "alt + ";
|
||||||
const ALT_PREFIX: &str = "⌥";
|
const CTRL_PREFIX: &str = "ctrl + ";
|
||||||
#[cfg(all(not(test), target_os = "macos"))]
|
const SHIFT_PREFIX: &str = "shift + ";
|
||||||
const ALT_PREFIX: &str = "⌥";
|
|
||||||
#[cfg(all(not(test), not(target_os = "macos")))]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
const ALT_PREFIX: &str = "Alt+";
|
pub(crate) struct KeyBinding {
|
||||||
|
key: KeyCode,
|
||||||
|
modifiers: KeyModifiers,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyBinding {
|
||||||
|
pub(crate) const fn new(key: KeyCode, modifiers: KeyModifiers) -> Self {
|
||||||
|
Self { key, modifiers }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_press(&self, event: KeyEvent) -> bool {
|
||||||
|
self.key == event.code
|
||||||
|
&& self.modifiers == event.modifiers
|
||||||
|
&& (event.kind == KeyEventKind::Press || event.kind == KeyEventKind::Repeat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn plain(key: KeyCode) -> KeyBinding {
|
||||||
|
KeyBinding::new(key, KeyModifiers::NONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn alt(key: KeyCode) -> KeyBinding {
|
||||||
|
KeyBinding::new(key, KeyModifiers::ALT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn shift(key: KeyCode) -> KeyBinding {
|
||||||
|
KeyBinding::new(key, KeyModifiers::SHIFT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn ctrl(key: KeyCode) -> KeyBinding {
|
||||||
|
KeyBinding::new(key, KeyModifiers::CONTROL)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modifiers_to_string(modifiers: KeyModifiers) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
if modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
result.push_str(CTRL_PREFIX);
|
||||||
|
}
|
||||||
|
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
|
result.push_str(SHIFT_PREFIX);
|
||||||
|
}
|
||||||
|
if modifiers.contains(KeyModifiers::ALT) {
|
||||||
|
result.push_str(ALT_PREFIX);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KeyBinding> for Span<'static> {
|
||||||
|
fn from(binding: KeyBinding) -> Self {
|
||||||
|
(&binding).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&KeyBinding> for Span<'static> {
|
||||||
|
fn from(binding: &KeyBinding) -> Self {
|
||||||
|
let KeyBinding { key, modifiers } = binding;
|
||||||
|
let modifiers = modifiers_to_string(*modifiers);
|
||||||
|
let key = match key {
|
||||||
|
KeyCode::Enter => "enter".to_string(),
|
||||||
|
KeyCode::Up => "↑".to_string(),
|
||||||
|
KeyCode::Down => "↓".to_string(),
|
||||||
|
KeyCode::Left => "←".to_string(),
|
||||||
|
KeyCode::Right => "→".to_string(),
|
||||||
|
KeyCode::PageUp => "pgup".to_string(),
|
||||||
|
KeyCode::PageDown => "pgdn".to_string(),
|
||||||
|
_ => format!("{key}").to_ascii_lowercase(),
|
||||||
|
};
|
||||||
|
Span::styled(format!("{modifiers}{key}"), key_hint_style())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn key_hint_style() -> Style {
|
fn key_hint_style() -> Style {
|
||||||
Style::default().bold()
|
Style::default().dim()
|
||||||
}
|
|
||||||
|
|
||||||
fn modifier_span(prefix: &str, key: impl Display) -> Span<'static> {
|
|
||||||
Span::styled(format!("{prefix}{key}"), key_hint_style())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn alt(key: impl Display) -> Span<'static> {
|
|
||||||
modifier_span(ALT_PREFIX, key)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,16 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::history_cell::HistoryCell;
|
use crate::history_cell::HistoryCell;
|
||||||
|
use crate::key_hint;
|
||||||
|
use crate::key_hint::KeyBinding;
|
||||||
use crate::render::renderable::Renderable;
|
use crate::render::renderable::Renderable;
|
||||||
use crate::tui;
|
use crate::tui;
|
||||||
use crate::tui::TuiEvent;
|
use crate::tui::TuiEvent;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use crossterm::event::KeyEventKind;
|
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::buffer::Cell;
|
use ratatui::buffer::Cell;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Color;
|
|
||||||
use ratatui::style::Style;
|
|
||||||
use ratatui::style::Styled;
|
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
use ratatui::text::Span;
|
use ratatui::text::Span;
|
||||||
@@ -61,23 +59,40 @@ impl Overlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const KEY_UP: KeyBinding = key_hint::plain(KeyCode::Up);
|
||||||
|
const KEY_DOWN: KeyBinding = key_hint::plain(KeyCode::Down);
|
||||||
|
const KEY_PAGE_UP: KeyBinding = key_hint::plain(KeyCode::PageUp);
|
||||||
|
const KEY_PAGE_DOWN: KeyBinding = key_hint::plain(KeyCode::PageDown);
|
||||||
|
const KEY_SPACE: KeyBinding = key_hint::plain(KeyCode::Char(' '));
|
||||||
|
const KEY_HOME: KeyBinding = key_hint::plain(KeyCode::Home);
|
||||||
|
const KEY_END: KeyBinding = key_hint::plain(KeyCode::End);
|
||||||
|
const KEY_Q: KeyBinding = key_hint::plain(KeyCode::Char('q'));
|
||||||
|
const KEY_ESC: KeyBinding = key_hint::plain(KeyCode::Esc);
|
||||||
|
const KEY_ENTER: KeyBinding = key_hint::plain(KeyCode::Enter);
|
||||||
|
const KEY_CTRL_T: KeyBinding = key_hint::ctrl(KeyCode::Char('t'));
|
||||||
|
const KEY_CTRL_C: KeyBinding = key_hint::ctrl(KeyCode::Char('c'));
|
||||||
|
|
||||||
// Common pager navigation hints rendered on the first line
|
// Common pager navigation hints rendered on the first line
|
||||||
const PAGER_KEY_HINTS: &[(&str, &str)] = &[
|
const PAGER_KEY_HINTS: &[(&[KeyBinding], &str)] = &[
|
||||||
("↑/↓", "scroll"),
|
(&[KEY_UP, KEY_DOWN], "to scroll"),
|
||||||
("PgUp/PgDn", "page"),
|
(&[KEY_PAGE_UP, KEY_PAGE_DOWN], "to page"),
|
||||||
("Home/End", "jump"),
|
(&[KEY_HOME, KEY_END], "to jump"),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Render a single line of key hints from (key, description) pairs.
|
// Render a single line of key hints from (key(s), description) pairs.
|
||||||
fn render_key_hints(area: Rect, buf: &mut Buffer, pairs: &[(&str, &str)]) {
|
fn render_key_hints(area: Rect, buf: &mut Buffer, pairs: &[(&[KeyBinding], &str)]) {
|
||||||
let key_hint_style = Style::default().fg(Color::Cyan);
|
|
||||||
let mut spans: Vec<Span<'static>> = vec![" ".into()];
|
let mut spans: Vec<Span<'static>> = vec![" ".into()];
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
for (key, desc) in pairs {
|
for (keys, desc) in pairs {
|
||||||
if !first {
|
if !first {
|
||||||
spans.push(" ".into());
|
spans.push(" ".into());
|
||||||
}
|
}
|
||||||
spans.push(Span::from(key.to_string()).set_style(key_hint_style));
|
for (i, key) in keys.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
spans.push("/".into());
|
||||||
|
}
|
||||||
|
spans.push(Span::from(key));
|
||||||
|
}
|
||||||
spans.push(" ".into());
|
spans.push(" ".into());
|
||||||
spans.push(Span::from(desc.to_string()));
|
spans.push(Span::from(desc.to_string()));
|
||||||
first = false;
|
first = false;
|
||||||
@@ -214,48 +229,24 @@ impl PagerView {
|
|||||||
|
|
||||||
fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) -> Result<()> {
|
fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) -> Result<()> {
|
||||||
match key_event {
|
match key_event {
|
||||||
KeyEvent {
|
e if KEY_UP.is_press(e) => {
|
||||||
code: KeyCode::Up,
|
|
||||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||||
}
|
}
|
||||||
KeyEvent {
|
e if KEY_DOWN.is_press(e) => {
|
||||||
code: KeyCode::Down,
|
|
||||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.scroll_offset = self.scroll_offset.saturating_add(1);
|
self.scroll_offset = self.scroll_offset.saturating_add(1);
|
||||||
}
|
}
|
||||||
KeyEvent {
|
e if KEY_PAGE_UP.is_press(e) => {
|
||||||
code: KeyCode::PageUp,
|
|
||||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let area = self.content_area(tui.terminal.viewport_area);
|
let area = self.content_area(tui.terminal.viewport_area);
|
||||||
self.scroll_offset = self.scroll_offset.saturating_sub(area.height as usize);
|
self.scroll_offset = self.scroll_offset.saturating_sub(area.height as usize);
|
||||||
}
|
}
|
||||||
KeyEvent {
|
e if KEY_PAGE_DOWN.is_press(e) || KEY_SPACE.is_press(e) => {
|
||||||
code: KeyCode::PageDown | KeyCode::Char(' '),
|
|
||||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let area = self.content_area(tui.terminal.viewport_area);
|
let area = self.content_area(tui.terminal.viewport_area);
|
||||||
self.scroll_offset = self.scroll_offset.saturating_add(area.height as usize);
|
self.scroll_offset = self.scroll_offset.saturating_add(area.height as usize);
|
||||||
}
|
}
|
||||||
KeyEvent {
|
e if KEY_HOME.is_press(e) => {
|
||||||
code: KeyCode::Home,
|
|
||||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.scroll_offset = 0;
|
self.scroll_offset = 0;
|
||||||
}
|
}
|
||||||
KeyEvent {
|
e if KEY_END.is_press(e) => {
|
||||||
code: KeyCode::End,
|
|
||||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.scroll_offset = usize::MAX;
|
self.scroll_offset = usize::MAX;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -434,9 +425,11 @@ impl TranscriptOverlay {
|
|||||||
let line1 = Rect::new(area.x, area.y, area.width, 1);
|
let line1 = Rect::new(area.x, area.y, area.width, 1);
|
||||||
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
|
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
|
||||||
render_key_hints(line1, buf, PAGER_KEY_HINTS);
|
render_key_hints(line1, buf, PAGER_KEY_HINTS);
|
||||||
let mut pairs: Vec<(&str, &str)> = vec![("q", "quit"), ("Esc", "edit prev")];
|
|
||||||
|
let mut pairs: Vec<(&[KeyBinding], &str)> =
|
||||||
|
vec![(&[KEY_Q], "to quit"), (&[KEY_ESC], "to edit prev")];
|
||||||
if self.highlight_cell.is_some() {
|
if self.highlight_cell.is_some() {
|
||||||
pairs.push(("⏎", "edit message"));
|
pairs.push((&[KEY_ENTER], "to edit message"));
|
||||||
}
|
}
|
||||||
render_key_hints(line2, buf, &pairs);
|
render_key_hints(line2, buf, &pairs);
|
||||||
}
|
}
|
||||||
@@ -454,23 +447,7 @@ impl TranscriptOverlay {
|
|||||||
pub(crate) fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> {
|
pub(crate) fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> {
|
||||||
match event {
|
match event {
|
||||||
TuiEvent::Key(key_event) => match key_event {
|
TuiEvent::Key(key_event) => match key_event {
|
||||||
KeyEvent {
|
e if KEY_Q.is_press(e) || KEY_CTRL_C.is_press(e) || KEY_CTRL_T.is_press(e) => {
|
||||||
code: KeyCode::Char('q'),
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| KeyEvent {
|
|
||||||
code: KeyCode::Char('t'),
|
|
||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| KeyEvent {
|
|
||||||
code: KeyCode::Char('c'),
|
|
||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.is_done = true;
|
self.is_done = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -516,7 +493,7 @@ impl StaticOverlay {
|
|||||||
let line1 = Rect::new(area.x, area.y, area.width, 1);
|
let line1 = Rect::new(area.x, area.y, area.width, 1);
|
||||||
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
|
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
|
||||||
render_key_hints(line1, buf, PAGER_KEY_HINTS);
|
render_key_hints(line1, buf, PAGER_KEY_HINTS);
|
||||||
let pairs = [("q", "quit")];
|
let pairs: Vec<(&[KeyBinding], &str)> = vec![(&[KEY_Q], "to quit")];
|
||||||
render_key_hints(line2, buf, &pairs);
|
render_key_hints(line2, buf, &pairs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,17 +510,7 @@ impl StaticOverlay {
|
|||||||
pub(crate) fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> {
|
pub(crate) fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> {
|
||||||
match event {
|
match event {
|
||||||
TuiEvent::Key(key_event) => match key_event {
|
TuiEvent::Key(key_event) => match key_event {
|
||||||
KeyEvent {
|
e if KEY_Q.is_press(e) || KEY_CTRL_C.is_press(e) => {
|
||||||
code: KeyCode::Char('q'),
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| KeyEvent {
|
|
||||||
code: KeyCode::Char('c'),
|
|
||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
||||||
kind: KeyEventKind::Press,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.is_done = true;
|
self.is_done = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ use tokio_stream::StreamExt;
|
|||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::key_hint;
|
||||||
use crate::text_formatting::truncate_text;
|
use crate::text_formatting::truncate_text;
|
||||||
use crate::tui::FrameRequester;
|
use crate::tui::FrameRequester;
|
||||||
use crate::tui::Tui;
|
use crate::tui::Tui;
|
||||||
@@ -678,16 +679,18 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
|
|||||||
|
|
||||||
// Hint line
|
// Hint line
|
||||||
let hint_line: Line = vec![
|
let hint_line: Line = vec![
|
||||||
"Enter".bold(),
|
key_hint::plain(KeyCode::Enter).into(),
|
||||||
" to resume ".into(),
|
" to resume ".dim(),
|
||||||
"• ".dim(),
|
" ".dim(),
|
||||||
"Esc".bold(),
|
key_hint::plain(KeyCode::Esc).into(),
|
||||||
" to start new ".into(),
|
" to start new ".dim(),
|
||||||
"• ".dim(),
|
" ".dim(),
|
||||||
"Ctrl+C".into(),
|
key_hint::ctrl(KeyCode::Char('c')).into(),
|
||||||
" to quit ".into(),
|
" to quit ".dim(),
|
||||||
"• ".dim(),
|
" ".dim(),
|
||||||
"↑/↓".into(),
|
key_hint::plain(KeyCode::Up).into(),
|
||||||
|
"/".dim(),
|
||||||
|
key_hint::plain(KeyCode::Down).into(),
|
||||||
" to browse".dim(),
|
" to browse".dim(),
|
||||||
]
|
]
|
||||||
.into();
|
.into();
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ expression: term.backend()
|
|||||||
"~ "
|
"~ "
|
||||||
"~ "
|
"~ "
|
||||||
"───────────────────────────────── 100% ─"
|
"───────────────────────────────── 100% ─"
|
||||||
" ↑/↓ scroll PgUp/PgDn page Home/End "
|
" ↑/↓ to scroll pgup/pgdn to page hom"
|
||||||
" q quit "
|
" q to quit "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ expression: snapshot
|
|||||||
1 +hello
|
1 +hello
|
||||||
2 +world
|
2 +world
|
||||||
─────────────────────────────────────────────────────────────────────────── 0% ─
|
─────────────────────────────────────────────────────────────────────────── 0% ─
|
||||||
↑/↓ scroll PgUp/PgDn page Home/End jump
|
↑/↓ to scroll pgup/pgdn to page home/end to jump
|
||||||
q quit Esc edit prev
|
q to quit esc to edit prev
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ expression: term.backend()
|
|||||||
" "
|
" "
|
||||||
"gamma "
|
"gamma "
|
||||||
"───────────────────────────────── 100% ─"
|
"───────────────────────────────── 100% ─"
|
||||||
" ↑/↓ scroll PgUp/PgDn page Home/End "
|
" ↑/↓ to scroll pgup/pgdn to page hom"
|
||||||
" q quit Esc edit prev "
|
" q to quit esc to edit prev "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
source: tui/src/status_indicator_widget.rs
|
source: tui/src/status_indicator_widget.rs
|
||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
" Working (0s • Esc "
|
" Working (0s • esc "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
source: tui/src/status_indicator_widget.rs
|
source: tui/src/status_indicator_widget.rs
|
||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
" Working (0s • Esc to interrupt) "
|
" Working (0s • esc to interrupt) "
|
||||||
" "
|
" "
|
||||||
" ↳ first "
|
" ↳ first "
|
||||||
" ↳ second "
|
" ↳ second "
|
||||||
" ⌥↑ edit "
|
" alt + ↑ edit "
|
||||||
" "
|
" "
|
||||||
" "
|
" "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
source: tui/src/status_indicator_widget.rs
|
source: tui/src/status_indicator_widget.rs
|
||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
" Working (0s • Esc to interrupt) "
|
" Working (0s • esc to interrupt) "
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::time::Duration;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
@@ -164,7 +165,7 @@ impl WidgetRef for StatusIndicatorWidget {
|
|||||||
spans.extend(vec![
|
spans.extend(vec![
|
||||||
" ".into(),
|
" ".into(),
|
||||||
format!("({pretty_elapsed} • ").dim(),
|
format!("({pretty_elapsed} • ").dim(),
|
||||||
"Esc".dim().bold(),
|
key_hint::plain(KeyCode::Esc).into(),
|
||||||
" to interrupt)".dim(),
|
" to interrupt)".dim(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -188,8 +189,14 @@ impl WidgetRef for StatusIndicatorWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !self.queued_messages.is_empty() {
|
if !self.queued_messages.is_empty() {
|
||||||
let shortcut = key_hint::alt("↑");
|
lines.push(
|
||||||
lines.push(Line::from(vec![" ".into(), shortcut, " edit".into()]).dim());
|
Line::from(vec![
|
||||||
|
" ".into(),
|
||||||
|
key_hint::alt(KeyCode::Up).into(),
|
||||||
|
" edit".into(),
|
||||||
|
])
|
||||||
|
.dim(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let paragraph = Paragraph::new(lines);
|
let paragraph = Paragraph::new(lines);
|
||||||
|
|||||||
Reference in New Issue
Block a user