diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index b3c1eea6d9..3b02c59334 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1,4 +1,3 @@ -use codex_core::protocol::TokenUsageInfo; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; @@ -19,8 +18,14 @@ use super::chat_composer_history::ChatComposerHistory; use super::command_popup::CommandItem; use super::command_popup::CommandPopup; use super::file_search_popup::FileSearchPopup; +use super::footer::FooterMode; use super::footer::FooterProps; +use super::footer::esc_hint_mode; +use super::footer::footer_height; +use super::footer::prompt_mode; use super::footer::render_footer; +use super::footer::reset_mode_after_activity; +use super::footer::toggle_shortcut_mode; use super::paste_burst::CharDecision; use super::paste_burst::PasteBurst; use crate::bottom_pane::paste_burst::FlushResult; @@ -74,7 +79,6 @@ pub(crate) struct ChatComposer { dismissed_file_popup_token: Option, current_file_query: Option, pending_pastes: Vec<(String, String)>, - token_usage_info: Option, has_focus: bool, attached_images: Vec, placeholder_text: String, @@ -84,6 +88,7 @@ pub(crate) struct ChatComposer { // When true, disables paste-burst logic and inserts characters immediately. disable_paste_burst: bool, custom_prompts: Vec, + footer_mode: FooterMode, } /// Popup state – at most one can be visible at any time. @@ -93,7 +98,7 @@ enum ActivePopup { File(FileSearchPopup), } -const FOOTER_HINT_HEIGHT: u16 = 1; +const FOOTER_SPACING_HEIGHT: u16 = 1; impl ChatComposer { pub fn new( @@ -117,7 +122,6 @@ impl ChatComposer { dismissed_file_popup_token: None, current_file_query: None, pending_pastes: Vec::new(), - token_usage_info: None, has_focus: has_input_focus, attached_images: Vec::new(), placeholder_text, @@ -125,6 +129,7 @@ impl ChatComposer { paste_burst: PasteBurst::default(), disable_paste_burst: false, custom_prompts: Vec::new(), + footer_mode: FooterMode::ShortcutPrompt, }; // Apply configuration via the setter to keep side-effects centralized. this.set_disable_paste_burst(disable_paste_burst); @@ -132,26 +137,41 @@ impl ChatComposer { } pub fn desired_height(&self, width: u16) -> u16 { + let footer_props = self.footer_props(); + let footer_hint_height = footer_height(footer_props); + let footer_spacing = if footer_hint_height > 0 { + FOOTER_SPACING_HEIGHT + } else { + 0 + }; + let footer_total_height = footer_hint_height + footer_spacing; self.textarea .desired_height(width.saturating_sub(LIVE_PREFIX_COLS)) + 2 + match &self.active_popup { - ActivePopup::None => FOOTER_HINT_HEIGHT, + ActivePopup::None => footer_total_height, ActivePopup::Command(c) => c.calculate_required_height(width), ActivePopup::File(c) => c.calculate_required_height(), } } fn layout_areas(&self, area: Rect) -> [Rect; 3] { + let footer_props = self.footer_props(); + let footer_hint_height = footer_height(footer_props); + let footer_spacing = if footer_hint_height > 0 { + FOOTER_SPACING_HEIGHT + } else { + 0 + }; + let footer_total_height = footer_hint_height + footer_spacing; let popup_constraint = match &self.active_popup { ActivePopup::Command(popup) => { Constraint::Max(popup.calculate_required_height(area.width)) } ActivePopup::File(popup) => Constraint::Max(popup.calculate_required_height()), - ActivePopup::None => Constraint::Max(FOOTER_HINT_HEIGHT), + ActivePopup::None => Constraint::Max(footer_total_height), }; let mut area = area; - // Leave an empty row at the top, unless there isn't room. if area.height > 1 { area.height -= 1; area.y += 1; @@ -175,13 +195,6 @@ impl ChatComposer { self.textarea.is_empty() } - /// Update the cached *context-left* percentage and refresh the placeholder - /// text. The UI relies on the placeholder to convey the remaining - /// context when the composer is empty. - pub(crate) fn set_token_usage(&mut self, token_info: Option) { - self.token_usage_info = token_info; - } - /// Record the history metadata advertised by `SessionConfiguredEvent` so /// that the composer can navigate cross-session history. pub(crate) fn set_history_metadata(&mut self, log_id: u64, entry_count: usize) { @@ -319,6 +332,11 @@ impl ChatComposer { pub fn set_ctrl_c_quit_hint(&mut self, show: bool, has_focus: bool) { self.ctrl_c_quit_hint = show; + if show { + self.footer_mode = prompt_mode(); + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } self.set_has_focus(has_focus); } @@ -354,6 +372,18 @@ impl ChatComposer { /// Handle key event when the slash-command popup is visible. fn handle_key_event_with_slash_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { + if self.handle_shortcut_overlay_key(&key_event) { + return (InputResult::None, true); + } + if key_event.code == KeyCode::Esc { + let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + if next_mode != self.footer_mode { + self.footer_mode = next_mode; + return (InputResult::None, true); + } + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } let ActivePopup::Command(popup) = &mut self.active_popup else { unreachable!(); }; @@ -478,6 +508,18 @@ impl ChatComposer { /// Handle key events when file search popup is visible. fn handle_key_event_with_file_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { + if self.handle_shortcut_overlay_key(&key_event) { + return (InputResult::None, true); + } + if key_event.code == KeyCode::Esc { + let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + if next_mode != self.footer_mode { + self.footer_mode = next_mode; + return (InputResult::None, true); + } + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } let ActivePopup::File(popup) = &mut self.active_popup else { unreachable!(); }; @@ -729,6 +771,18 @@ impl ChatComposer { /// Handle key event when no popup is visible. fn handle_key_event_without_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { + if self.handle_shortcut_overlay_key(&key_event) { + return (InputResult::None, true); + } + if key_event.code == KeyCode::Esc { + let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + if next_mode != self.footer_mode { + self.footer_mode = next_mode; + return (InputResult::None, true); + } + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } match key_event { KeyEvent { code: KeyCode::Char('d'), @@ -872,6 +926,10 @@ impl ChatComposer { let now = Instant::now(); self.handle_paste_burst_flush(now); + if !matches!(input.code, KeyCode::Esc) { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } + // If we're capturing a burst and receive Enter, accumulate it instead of inserting. if matches!(input.code, KeyCode::Enter) && self.paste_burst.is_active() @@ -1145,6 +1203,39 @@ impl ChatComposer { false } + fn handle_shortcut_overlay_key(&mut self, key_event: &KeyEvent) -> bool { + if key_event.kind == KeyEventKind::Press + && key_event.modifiers.is_empty() + && matches!(key_event.code, KeyCode::Char('?')) + { + let next = toggle_shortcut_mode(self.footer_mode, self.ctrl_c_quit_hint); + let changed = next != self.footer_mode; + self.footer_mode = next; + changed + } else { + false + } + } + + fn footer_props(&self) -> FooterProps { + FooterProps { + mode: self.footer_mode(), + esc_backtrack_hint: self.esc_backtrack_hint, + use_shift_enter_hint: self.use_shift_enter_hint, + is_task_running: self.is_task_running, + } + } + + fn footer_mode(&self) -> FooterMode { + if matches!(self.footer_mode, FooterMode::EscHint) { + FooterMode::EscHint + } else if self.ctrl_c_quit_hint { + FooterMode::CtrlCReminder + } else { + self.footer_mode + } + } + /// Synchronize `self.command_popup` with the current text in the /// textarea. This must be called after every modification that can change /// the text so the popup is shown/updated/hidden as appropriate. @@ -1228,10 +1319,18 @@ impl ChatComposer { pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; + if running { + self.footer_mode = prompt_mode(); + } } pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) { self.esc_backtrack_hint = show; + if show { + self.footer_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + } else { + self.footer_mode = reset_mode_after_activity(self.footer_mode); + } } } @@ -1252,20 +1351,26 @@ impl WidgetRef for ChatComposer { popup.render_ref(popup_rect, buf); } ActivePopup::None => { - let mut hint_rect = popup_rect; - hint_rect.x += 2; - hint_rect.width = hint_rect.width.saturating_sub(2); - render_footer( - hint_rect, - buf, - FooterProps { - ctrl_c_quit_hint: self.ctrl_c_quit_hint, - is_task_running: self.is_task_running, - esc_backtrack_hint: self.esc_backtrack_hint, - use_shift_enter_hint: self.use_shift_enter_hint, - token_usage_info: self.token_usage_info.as_ref(), - }, - ); + let footer_hint_height = footer_height(self.footer_props()); + let footer_spacing = if footer_hint_height > 0 { + FOOTER_SPACING_HEIGHT + } else { + 0 + }; + let hint_rect = if footer_spacing > 0 && footer_hint_height > 0 { + let [_, hint_rect] = Layout::vertical([ + Constraint::Length(footer_spacing), + Constraint::Length(footer_hint_height), + ]) + .areas(popup_rect); + hint_rect + } else { + popup_rect + }; + let mut footer_rect = hint_rect; + footer_rect.x = footer_rect.x.saturating_add(2); + footer_rect.width = footer_rect.width.saturating_sub(2); + render_footer(footer_rect, buf, self.footer_props()); } } let style = user_message_style(terminal_palette::default_bg()); @@ -1304,6 +1409,7 @@ mod tests { use crate::bottom_pane::InputResult; use crate::bottom_pane::chat_composer::AttachedImage; use crate::bottom_pane::chat_composer::LARGE_PASTE_CHAR_THRESHOLD; + use crate::bottom_pane::footer::footer_height; use crate::bottom_pane::textarea::TextArea; use tokio::sync::mpsc::unbounded_channel; @@ -1334,7 +1440,7 @@ mod tests { let mut hint_row: Option<(u16, String)> = None; for y in 0..area.height { let row = row_to_string(y); - if row.contains(" send") { + if row.contains("? for shortcuts") { hint_row = Some((y, row)); break; } @@ -1361,6 +1467,81 @@ mod tests { ); } + fn snapshot_composer_state(name: &str, enhanced_keys_supported: bool, setup: F) + where + F: FnOnce(&mut ChatComposer), + { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + let width = 100; + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + enhanced_keys_supported, + "Ask Codex to do anything".to_string(), + false, + ); + setup(&mut composer); + let footer_lines = footer_height(composer.footer_props()); + let height = footer_lines + 8; + let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); + terminal + .draw(|f| f.render_widget_ref(composer, f.area())) + .unwrap(); + insta::assert_snapshot!(name, terminal.backend()); + } + + #[test] + fn footer_mode_snapshots() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + snapshot_composer_state("footer_mode_shortcut_overlay", true, |composer| { + composer.set_esc_backtrack_hint(true); + let _ = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + }); + + snapshot_composer_state("footer_mode_ctrl_c_quit", true, |composer| { + composer.set_ctrl_c_quit_hint(true, true); + }); + + snapshot_composer_state("footer_mode_ctrl_c_interrupt", true, |composer| { + composer.set_task_running(true); + composer.set_ctrl_c_quit_hint(true, true); + }); + + snapshot_composer_state("footer_mode_ctrl_c_then_esc_hint", true, |composer| { + composer.set_ctrl_c_quit_hint(true, true); + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + }); + + snapshot_composer_state("footer_mode_esc_hint_from_overlay", true, |composer| { + let _ = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + }); + + snapshot_composer_state("footer_mode_esc_hint_backtrack", true, |composer| { + composer.set_esc_backtrack_hint(true); + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + }); + + snapshot_composer_state( + "footer_mode_overlay_then_external_esc_hint", + true, + |composer| { + let _ = composer + .handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); + composer.set_esc_backtrack_hint(true); + }, + ); + } + #[test] fn test_current_at_token_basic_cases() { let test_cases = vec![ diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 77e2c106e5..2e63493e31 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -1,149 +1,190 @@ -use codex_core::protocol::TokenUsageInfo; -use codex_protocol::num_format::format_si_suffix; use crossterm::event::KeyCode; use crossterm::event::KeyModifiers; use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::style::Color; -use ratatui::style::Modifier; -use ratatui::style::Style; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::WidgetRef; -use crate::key_hint; - #[derive(Clone, Copy, Debug)] -pub(crate) struct FooterProps<'a> { - pub(crate) ctrl_c_quit_hint: bool, - pub(crate) is_task_running: bool, +pub(crate) struct FooterProps { + pub(crate) mode: FooterMode, pub(crate) esc_backtrack_hint: bool, pub(crate) use_shift_enter_hint: bool, - pub(crate) token_usage_info: Option<&'a TokenUsageInfo>, + pub(crate) is_task_running: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum FooterMode { + CtrlCReminder, + ShortcutPrompt, + ShortcutOverlay, + EscHint, +} + +pub(crate) fn toggle_shortcut_mode(current: FooterMode, ctrl_c_hint: bool) -> FooterMode { + if ctrl_c_hint { + return current; + } + match current { + FooterMode::ShortcutOverlay | FooterMode::CtrlCReminder => FooterMode::ShortcutPrompt, + _ => FooterMode::ShortcutOverlay, + } +} + +pub(crate) fn esc_hint_mode(current: FooterMode, is_task_running: bool) -> FooterMode { + if is_task_running { + current + } else { + FooterMode::EscHint + } +} + +pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { + match current { + FooterMode::EscHint | FooterMode::ShortcutOverlay => FooterMode::ShortcutPrompt, + other => other, + } +} + +pub(crate) fn prompt_mode() -> FooterMode { + FooterMode::ShortcutPrompt +} + +pub(crate) fn footer_height(props: FooterProps) -> u16 { + footer_lines(props).len() as u16 +} + +pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) { + let lines = footer_lines(props); + for (idx, line) in lines.into_iter().enumerate() { + let y = area.y + idx as u16; + if y >= area.y + area.height { + break; + } + let row = Rect::new(area.x, y, area.width, 1); + line.render_ref(row, buf); + } +} + +fn footer_lines(props: FooterProps) -> Vec> { + match props.mode { + FooterMode::CtrlCReminder => { + vec![ctrl_c_reminder_line(CtrlCReminderState { + is_task_running: props.is_task_running, + })] + } + FooterMode::ShortcutPrompt => vec![Line::from(vec!["? for shortcuts".dim()])], + FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState { + use_shift_enter_hint: props.use_shift_enter_hint, + esc_backtrack_hint: props.esc_backtrack_hint, + is_task_running: props.is_task_running, + }), + FooterMode::EscHint => { + vec![esc_hint_line(ShortcutsState { + use_shift_enter_hint: props.use_shift_enter_hint, + esc_backtrack_hint: props.esc_backtrack_hint, + is_task_running: props.is_task_running, + })] + } + } } #[derive(Clone, Copy, Debug)] struct CtrlCReminderState { - pub(crate) is_task_running: bool, + is_task_running: bool, } #[derive(Clone, Copy, Debug)] struct ShortcutsState { - pub(crate) use_shift_enter_hint: bool, - pub(crate) esc_backtrack_hint: bool, + use_shift_enter_hint: bool, + esc_backtrack_hint: bool, + is_task_running: bool, } -#[derive(Clone, Copy, Debug)] -enum FooterContent { - Shortcuts(ShortcutsState), - CtrlCReminder(CtrlCReminderState), +fn ctrl_c_reminder_line(state: CtrlCReminderState) -> Line<'static> { + let action = if state.is_task_running { + "interrupt" + } else { + "quit" + }; + Line::from(vec![ + Span::from(format!(" ctrl + c again to {action}")).dim(), + ]) } -pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps<'_>) { - let content = if props.ctrl_c_quit_hint { - FooterContent::CtrlCReminder(CtrlCReminderState { - is_task_running: props.is_task_running, - }) +fn esc_hint_line(state: ShortcutsState) -> Line<'static> { + let text = if state.esc_backtrack_hint { + " esc again to edit previous message" } else { - FooterContent::Shortcuts(ShortcutsState { - use_shift_enter_hint: props.use_shift_enter_hint, - esc_backtrack_hint: props.esc_backtrack_hint, - }) + " esc esc to edit previous message" }; + Line::from(vec![Span::from(text).dim()]) +} - let mut spans = footer_spans(content); - if let Some(token_usage_info) = props.token_usage_info { - append_token_usage_spans(&mut spans, token_usage_info); +fn shortcut_overlay_lines(state: ShortcutsState) -> Vec> { + let mut rendered = Vec::new(); + for descriptor in SHORTCUTS { + if let Some(text) = descriptor.overlay_entry(state) { + rendered.push(text); + } } - - let spans = spans - .into_iter() - .map(|span| span.patch_style(Style::default().dim())) - .collect::>(); - Line::from(spans).render_ref(area, buf); + build_columns(rendered) } -fn footer_spans(content: FooterContent) -> Vec> { - match content { - FooterContent::Shortcuts(state) => shortcuts_spans(state), - FooterContent::CtrlCReminder(state) => ctrl_c_reminder_spans(state), +fn build_columns(entries: Vec) -> Vec> { + if entries.is_empty() { + return Vec::new(); } -} -fn append_token_usage_spans(spans: &mut Vec>, token_usage_info: &TokenUsageInfo) { - let token_usage = &token_usage_info.total_token_usage; - spans.push(" ".into()); - spans.push( - Span::from(format!( - "{} tokens used", - format_si_suffix(token_usage.blended_total()) - )) - .style(Style::default().add_modifier(Modifier::DIM)), - ); - - let last_token_usage = &token_usage_info.last_token_usage; - if let Some(context_window) = token_usage_info.model_context_window { - let percent_remaining: u8 = if context_window > 0 { - last_token_usage.percent_of_context_window_remaining(context_window) - } else { - 100 - }; + const COLUMNS: usize = 3; + const MAX_PADDED_WIDTHS: [usize; COLUMNS - 1] = [24, 28]; - let context_style = if percent_remaining < 20 { - Style::default().fg(Color::Yellow) - } else { - Style::default().add_modifier(Modifier::DIM) - }; - spans.push(" ".into()); - spans.push(Span::styled( - format!("{percent_remaining}% context left"), - context_style, - )); + let rows = entries.len().div_ceil(COLUMNS); + let mut column_widths = [0usize; COLUMNS]; + + for (idx, entry) in entries.iter().enumerate() { + let column = idx % COLUMNS; + column_widths[column] = column_widths[column].max(entry.len()); } -} -fn shortcuts_spans(state: ShortcutsState) -> Vec> { - let mut spans = Vec::new(); - for descriptor in SHORTCUTS { - if let Some(segment) = descriptor.footer_segment(state) { - if !segment.prefix.is_empty() { - spans.push(segment.prefix.into()); + let mut lines = Vec::new(); + for row in 0..rows { + let mut line = String::from(" "); + for col in 0..COLUMNS { + let idx = row * COLUMNS + col; + if idx >= entries.len() { + continue; + } + let entry = &entries[idx]; + if col < COLUMNS - 1 { + let max_width = MAX_PADDED_WIDTHS[col]; + let target_width = column_widths[col].min(max_width); + let pad_width = target_width + 2; + line.push_str(&format!("{entry: Vec> { - let followup = if state.is_task_running { - " to interrupt" - } else { - " to quit" - }; - vec![ - " ".into(), - key_hint::ctrl('C'), - " again".into(), - followup.into(), - ] -} -#[derive(Clone, Copy, Debug)] -struct FooterSegment { - prefix: &'static str, - binding: ShortcutBinding, - label: &'static str, + lines } -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ShortcutId { - Send, + Commands, + FilePaths, InsertNewline, - ShowTranscript, + PasteImage, Quit, + ShowTranscript, + ToggleOverlay, EditPrevious, } @@ -151,30 +192,13 @@ enum ShortcutId { struct ShortcutBinding { code: KeyCode, modifiers: KeyModifiers, - display: ShortcutDisplay, + overlay_text: &'static str, condition: DisplayCondition, } impl ShortcutBinding { - fn span(&self) -> Span<'static> { - self.display.into_span() - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum ShortcutDisplay { - Plain(&'static str), - Ctrl(char), - Shift(char), -} - -impl ShortcutDisplay { - fn into_span(self) -> Span<'static> { - match self { - ShortcutDisplay::Plain(text) => key_hint::plain(text), - ShortcutDisplay::Ctrl(ch) => key_hint::ctrl(ch), - ShortcutDisplay::Shift(ch) => key_hint::shift(ch), - } + fn matches(&self, state: ShortcutsState) -> bool { + self.condition.matches(state) } } @@ -198,49 +222,66 @@ impl DisplayCondition { struct ShortcutDescriptor { id: ShortcutId, bindings: &'static [ShortcutBinding], - footer_label: &'static str, - footer_prefix: &'static str, + prefix: &'static str, + label: &'static str, } impl ShortcutDescriptor { - fn binding_for(&self, state: ShortcutsState) -> Option { - self.bindings - .iter() - .find(|binding| binding.condition.matches(state)) - .copied() + fn binding_for(&self, state: ShortcutsState) -> Option<&'static ShortcutBinding> { + self.bindings.iter().find(|binding| binding.matches(state)) } - fn should_show(&self, state: ShortcutsState) -> bool { - match self.id { - ShortcutId::EditPrevious => state.esc_backtrack_hint, - _ => true, - } - } - - fn footer_segment(&self, state: ShortcutsState) -> Option { - if !self.should_show(state) { - return None; - } + fn overlay_entry(&self, state: ShortcutsState) -> Option { let binding = self.binding_for(state)?; - Some(FooterSegment { - prefix: self.footer_prefix, - binding, - label: self.footer_label, - }) + let label = match self.id { + ShortcutId::Quit => { + if state.is_task_running { + " to interrupt" + } else { + self.label + } + } + ShortcutId::EditPrevious => { + if state.esc_backtrack_hint { + " again to edit previous message" + } else { + " esc to edit previous message" + } + } + _ => self.label, + }; + let text = match self.id { + ShortcutId::Quit if state.is_task_running => { + format!("{}{} to interrupt", self.prefix, binding.overlay_text) + } + _ => format!("{}{}{}", self.prefix, binding.overlay_text, label), + }; + Some(text) } } const SHORTCUTS: &[ShortcutDescriptor] = &[ ShortcutDescriptor { - id: ShortcutId::Send, + id: ShortcutId::Commands, bindings: &[ShortcutBinding { - code: KeyCode::Enter, + code: KeyCode::Char('/'), modifiers: KeyModifiers::NONE, - display: ShortcutDisplay::Plain("⏎"), + overlay_text: "/", condition: DisplayCondition::Always, }], - footer_label: " send ", - footer_prefix: "", + prefix: "", + label: " for commands", + }, + ShortcutDescriptor { + id: ShortcutId::FilePaths, + bindings: &[ShortcutBinding { + code: KeyCode::Char('@'), + modifiers: KeyModifiers::NONE, + overlay_text: "@", + condition: DisplayCondition::Always, + }], + prefix: "", + label: " for file paths", }, ShortcutDescriptor { id: ShortcutId::InsertNewline, @@ -248,138 +289,154 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[ ShortcutBinding { code: KeyCode::Enter, modifiers: KeyModifiers::SHIFT, - display: ShortcutDisplay::Shift('⏎'), + overlay_text: "shift + enter", condition: DisplayCondition::WhenShiftEnterHint, }, ShortcutBinding { code: KeyCode::Char('j'), modifiers: KeyModifiers::CONTROL, - display: ShortcutDisplay::Ctrl('J'), + overlay_text: "ctrl + j", condition: DisplayCondition::WhenNotShiftEnterHint, }, ], - footer_label: " newline ", - footer_prefix: "", + prefix: "", + label: " for newline", }, ShortcutDescriptor { - id: ShortcutId::ShowTranscript, + id: ShortcutId::PasteImage, bindings: &[ShortcutBinding { - code: KeyCode::Char('t'), + code: KeyCode::Char('v'), modifiers: KeyModifiers::CONTROL, - display: ShortcutDisplay::Ctrl('T'), + overlay_text: "ctrl + v", condition: DisplayCondition::Always, }], - footer_label: " transcript ", - footer_prefix: "", + prefix: "", + label: " to paste images", }, ShortcutDescriptor { id: ShortcutId::Quit, bindings: &[ShortcutBinding { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, - display: ShortcutDisplay::Ctrl('C'), + overlay_text: "ctrl + c", + condition: DisplayCondition::Always, + }], + prefix: "", + label: " to exit", + }, + ShortcutDescriptor { + id: ShortcutId::ShowTranscript, + bindings: &[ShortcutBinding { + code: KeyCode::Char('t'), + modifiers: KeyModifiers::CONTROL, + overlay_text: "ctrl + t", condition: DisplayCondition::Always, }], - footer_label: " quit", - footer_prefix: "", + prefix: "", + label: " to view transcript", + }, + ShortcutDescriptor { + id: ShortcutId::ToggleOverlay, + bindings: &[ShortcutBinding { + code: KeyCode::Char('?'), + modifiers: KeyModifiers::NONE, + overlay_text: "?", + condition: DisplayCondition::Always, + }], + prefix: "", + label: " to hide shortcuts", }, ShortcutDescriptor { id: ShortcutId::EditPrevious, bindings: &[ShortcutBinding { code: KeyCode::Esc, modifiers: KeyModifiers::NONE, - display: ShortcutDisplay::Plain("Esc"), + overlay_text: "esc", condition: DisplayCondition::Always, }], - footer_label: " edit prev", - footer_prefix: " ", + prefix: "", + label: "", }, ]; #[cfg(test)] mod tests { use super::*; - use codex_core::protocol::TokenUsage; use insta::assert_snapshot; use ratatui::Terminal; use ratatui::backend::TestBackend; - fn snapshot_footer(name: &str, props: FooterProps<'_>) { - let mut terminal = Terminal::new(TestBackend::new(80, 3)).unwrap(); + fn snapshot_footer(name: &str, props: FooterProps) { + let height = footer_height(props).max(1); + let mut terminal = Terminal::new(TestBackend::new(80, height)).unwrap(); terminal .draw(|f| { - let area = Rect::new(0, 0, f.area().width, 1); + let area = Rect::new(0, 0, f.area().width, height); render_footer(area, f.buffer_mut(), props); }) .unwrap(); assert_snapshot!(name, terminal.backend()); } - fn token_usage(total_tokens: u64, last_tokens: u64, context_window: u64) -> TokenUsageInfo { - let usage = TokenUsage { - input_tokens: total_tokens, - cached_input_tokens: 0, - output_tokens: 0, - reasoning_output_tokens: 0, - total_tokens, - }; - let last = TokenUsage { - input_tokens: last_tokens, - cached_input_tokens: 0, - output_tokens: 0, - reasoning_output_tokens: 0, - total_tokens: last_tokens, - }; - TokenUsageInfo { - total_token_usage: usage, - last_token_usage: last, - model_context_window: Some(context_window), - } - } - #[test] fn footer_snapshots() { snapshot_footer( "footer_shortcuts_default", FooterProps { - ctrl_c_quit_hint: false, - is_task_running: false, + mode: FooterMode::ShortcutPrompt, esc_backtrack_hint: false, use_shift_enter_hint: false, - token_usage_info: None, + is_task_running: false, }, ); snapshot_footer( "footer_shortcuts_shift_and_esc", FooterProps { - ctrl_c_quit_hint: false, - is_task_running: false, + mode: FooterMode::ShortcutOverlay, esc_backtrack_hint: true, use_shift_enter_hint: true, - token_usage_info: Some(&token_usage(4_200, 900, 8_000)), + is_task_running: false, }, ); snapshot_footer( "footer_ctrl_c_quit_idle", FooterProps { - ctrl_c_quit_hint: true, - is_task_running: false, + mode: FooterMode::CtrlCReminder, esc_backtrack_hint: false, use_shift_enter_hint: false, - token_usage_info: None, + is_task_running: false, }, ); snapshot_footer( "footer_ctrl_c_quit_running", FooterProps { - ctrl_c_quit_hint: true, + mode: FooterMode::CtrlCReminder, + esc_backtrack_hint: false, + use_shift_enter_hint: false, is_task_running: true, + }, + ); + + snapshot_footer( + "footer_esc_hint_idle", + FooterProps { + mode: FooterMode::EscHint, esc_backtrack_hint: false, use_shift_enter_hint: false, - token_usage_info: None, + is_task_running: false, + }, + ); + + snapshot_footer( + "footer_esc_hint_primed", + FooterProps { + mode: FooterMode::EscHint, + esc_backtrack_hint: true, + use_shift_enter_hint: false, + is_task_running: false, }, ); } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 42af749bb7..b6c93d2813 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -4,7 +4,6 @@ use std::path::PathBuf; use crate::app_event_sender::AppEventSender; use crate::tui::FrameRequester; use bottom_pane_view::BottomPaneView; -use codex_core::protocol::TokenUsageInfo; use codex_file_search::FileMatch; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -373,13 +372,6 @@ impl BottomPane { !self.is_task_running && self.view_stack.is_empty() && !self.composer.popup_active() } - /// Update the *context-window remaining* indicator in the composer. This - /// is forwarded directly to the underlying `ChatComposer`. - pub(crate) fn set_token_usage(&mut self, token_info: Option) { - self.composer.set_token_usage(token_info); - self.request_redraw(); - } - pub(crate) fn show_view(&mut self, view: Box) { self.push_view(view); } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap index 3f57cab0f6..3c84d708b0 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap index beccbaf7c7..3bdd7260ff 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap new file mode 100644 index 0000000000..6f8ad7b98c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" ctrl + c again to interrupt " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap new file mode 100644 index 0000000000..f7917277fd --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap new file mode 100644 index 0000000000..99d263da31 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap new file mode 100644 index 0000000000..404aa48d8c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap new file mode 100644 index 0000000000..99d263da31 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap new file mode 100644 index 0000000000..404aa48d8c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap new file mode 100644 index 0000000000..c08e7f2d83 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap @@ -0,0 +1,15 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" / for commands @ for file paths shift + enter for newline " +" ctrl + v to paste images ctrl + c to exit ctrl + t to view transcript " +" ? to hide shortcuts esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap index 263c7cc549..53cfb9d76d 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap index beb2d8e464..52474a1fd8 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap index 41f0f03d8b..d4c6bdedf6 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" ? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap index 32482ba1cd..31a1b743b8 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap @@ -2,6 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" ⌃C again to quit " -" " -" " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap index 8c9c2b0d46..9979372a1b 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap @@ -2,6 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" ⌃C again to interrupt " -" " -" " +" ctrl + c again to interrupt " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap new file mode 100644 index 0000000000..b2333b025f --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap new file mode 100644 index 0000000000..20f9b178b4 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap index 8c8a10df85..aff75fdcdf 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap @@ -2,6 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -"⏎ send ⌃J newline ⌃T transcript ⌃C quit " -" " -" " +"? for shortcuts " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap index 9b94d89f98..96c10733dc 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap @@ -2,6 +2,6 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -"⏎ send ⇧⏎ newline ⌃T transcript ⌃C quit Esc edit prev 4.20K tokens use" -" " -" " +" / for commands @ for file paths shift + enter for ne" +" ctrl + v to paste images ctrl + c to exit ctrl + t to view tra" +" ? to hide shortcuts esc again to edit previous message " diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 0bf77f9994..85ecad284a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -393,7 +393,6 @@ impl ChatWidget { pub(crate) fn set_token_info(&mut self, info: Option) { if info.is_some() { - self.bottom_pane.set_token_usage(info.clone()); self.token_info = info; } } @@ -1960,7 +1959,6 @@ impl ChatWidget { pub(crate) fn clear_token_usage(&mut self) { self.token_info = None; - self.bottom_pane.set_token_usage(None); } pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap index 248a57f8f3..1698f2c77f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap @@ -14,4 +14,5 @@ expression: term.backend().vt100().screen().contents() › Summarize recent commits - ⏎ send ⌃J newline ⌃T transcript ⌃C quit + + ? for shortcuts diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap index 9c94206a44..fcd146282b 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap @@ -8,5 +8,6 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +" " +" ? for shortcuts " " " diff --git a/codex-rs/tui/src/key_hint.rs b/codex-rs/tui/src/key_hint.rs index 80f91d50b2..f68a493e9c 100644 --- a/codex-rs/tui/src/key_hint.rs +++ b/codex-rs/tui/src/key_hint.rs @@ -10,20 +10,6 @@ const ALT_PREFIX: &str = "⌥"; #[cfg(all(not(test), not(target_os = "macos")))] const ALT_PREFIX: &str = "Alt+"; -#[cfg(test)] -const CTRL_PREFIX: &str = "⌃"; -#[cfg(all(not(test), target_os = "macos"))] -const CTRL_PREFIX: &str = "⌃"; -#[cfg(all(not(test), not(target_os = "macos")))] -const CTRL_PREFIX: &str = "Ctrl+"; - -#[cfg(test)] -const SHIFT_PREFIX: &str = "⇧"; -#[cfg(all(not(test), target_os = "macos"))] -const SHIFT_PREFIX: &str = "⇧"; -#[cfg(all(not(test), not(target_os = "macos")))] -const SHIFT_PREFIX: &str = "Shift+"; - fn key_hint_style() -> Style { Style::default().bold() } @@ -32,18 +18,6 @@ fn modifier_span(prefix: &str, key: impl Display) -> Span<'static> { Span::styled(format!("{prefix}{key}"), key_hint_style()) } -pub(crate) fn ctrl(key: impl Display) -> Span<'static> { - modifier_span(CTRL_PREFIX, key) -} - pub(crate) fn alt(key: impl Display) -> Span<'static> { modifier_span(ALT_PREFIX, key) } - -pub(crate) fn shift(key: impl Display) -> Span<'static> { - modifier_span(SHIFT_PREFIX, key) -} - -pub(crate) fn plain(key: impl Display) -> Span<'static> { - Span::styled(format!("{key}"), key_hint_style()) -}