mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
Refactor shortcut overlay binding construction (#60962)
## What changed Separate shortcut binding definitions from rendering in the TUI shortcut overlay. Store optional bindings, paired alternatives, and repeated-key labels in typed entries, then format them through a shared rendering path. Use `KeyBinding` values for Page Up and Page Down instead of hard-coded labels. GitOrigin-RevId: da5d61eb94aefa3334612005fe614f58e6bfcae8
This commit is contained in:
committed by
copyberry
parent
1b7c025258
commit
64acaba91c
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
use super::footer::FooterProps;
|
use super::footer::FooterProps;
|
||||||
use crate::key_hint;
|
use crate::key_hint;
|
||||||
|
use crate::key_hint::KeyBinding;
|
||||||
|
use crate::key_hint::ShortcutHint;
|
||||||
use crate::multi_agents::next_agent_shortcut;
|
use crate::multi_agents::next_agent_shortcut;
|
||||||
use crate::multi_agents::previous_agent_shortcut;
|
use crate::multi_agents::previous_agent_shortcut;
|
||||||
use crate::shortcut_help::Group;
|
use crate::shortcut_help::Group;
|
||||||
@@ -23,122 +25,148 @@ use ratatui::text::Line;
|
|||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
pub(super) fn lines(props: &FooterProps, width: u16) -> Vec<Line<'static>> {
|
struct Entry {
|
||||||
|
keys: [Option<ShortcutHint>; 2],
|
||||||
|
action: &'static str,
|
||||||
|
repeat_label: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
fn hint(key: Option<ShortcutHint>, action: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
keys: [key, None],
|
||||||
|
action,
|
||||||
|
repeat_label: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(key: KeyBinding, action: &'static str) -> Self {
|
||||||
|
Self::hint(Some(key.into()), action)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pair(action: &'static str, keys: [KeyBinding; 2]) -> Self {
|
||||||
|
Self {
|
||||||
|
keys: keys.map(|key| Some(key.into())),
|
||||||
|
action,
|
||||||
|
repeat_label: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bindings(props: &FooterProps) -> [(&'static str, Vec<Entry>); 3] {
|
||||||
|
use KeyCode as K;
|
||||||
let hints = props.key_hints;
|
let hints = props.key_hints;
|
||||||
let mut compose = Group {
|
let paste = if props.is_wsl {
|
||||||
title: "Compose",
|
key_hint::ctrl_alt(K::Char('v'))
|
||||||
entries: vec![
|
} else {
|
||||||
Shortcut::new(key_hint::plain(KeyCode::Char('/')), "Commands"),
|
key_hint::ctrl(K::Char('v'))
|
||||||
Shortcut::new(key_hint::plain(KeyCode::Char('@')), "Mention files"),
|
|
||||||
Shortcut::new(key_hint::plain(KeyCode::Char('!')), "Shell command"),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
compose.push(hints.insert_newline, "New line");
|
let compose = vec![
|
||||||
compose.entries.push(Shortcut::new(
|
Entry::key(key_hint::plain(K::Char('/')), "Commands"),
|
||||||
if props.is_wsl {
|
Entry::key(key_hint::plain(K::Char('@')), "Mention files"),
|
||||||
key_hint::ctrl_alt(KeyCode::Char('v'))
|
Entry::key(key_hint::plain(K::Char('!')), "Shell command"),
|
||||||
} else {
|
Entry::hint(hints.insert_newline, "New line"),
|
||||||
key_hint::ctrl(KeyCode::Char('v'))
|
Entry::key(paste, "Paste image"),
|
||||||
},
|
Entry::hint(hints.external_editor, "External editor"),
|
||||||
"Paste image",
|
Entry::hint(hints.history_search, "Search history"),
|
||||||
));
|
Entry {
|
||||||
compose.push(hints.external_editor, "External editor");
|
keys: [hints.edit_previous, None],
|
||||||
compose.push(hints.history_search, "Search history");
|
|
||||||
if let Some(key) = hints.edit_previous {
|
|
||||||
let label = key.display_label();
|
|
||||||
compose.entries.push(Shortcut {
|
|
||||||
key: if props.esc_backtrack_hint {
|
|
||||||
label
|
|
||||||
} else {
|
|
||||||
format!("{label} {label}")
|
|
||||||
},
|
|
||||||
action: "Edit last message",
|
action: "Edit last message",
|
||||||
});
|
repeat_label: !props.esc_backtrack_hint,
|
||||||
}
|
|
||||||
|
|
||||||
let mut session = Group {
|
|
||||||
title: "Session",
|
|
||||||
entries: Vec::new(),
|
|
||||||
};
|
|
||||||
session.push(
|
|
||||||
hints.queue,
|
|
||||||
if props.is_task_running || props.queue_submissions {
|
|
||||||
"Queue message"
|
|
||||||
} else {
|
|
||||||
"Send message"
|
|
||||||
},
|
},
|
||||||
);
|
];
|
||||||
if props.collaboration_modes_enabled {
|
|
||||||
session
|
let session = vec![
|
||||||
.entries
|
Entry::hint(
|
||||||
.push(Shortcut::new(key_hint::shift(KeyCode::Tab), "Change mode"));
|
hints.queue,
|
||||||
}
|
if props.is_task_running || props.queue_submissions {
|
||||||
session.push(hints.reasoning_down, "Less reasoning");
|
"Queue message"
|
||||||
session.push(hints.reasoning_up, "More reasoning");
|
} else {
|
||||||
session.push(hints.toggle_voice, "Voice");
|
"Send message"
|
||||||
session.push(hints.agents, "Agents (empty prompt)");
|
},
|
||||||
session.entries.push(Shortcut {
|
|
||||||
key: format!(
|
|
||||||
"{} / {}",
|
|
||||||
previous_agent_shortcut().display_label(),
|
|
||||||
next_agent_shortcut().display_label()
|
|
||||||
),
|
),
|
||||||
action: "Subagents (empty prompt)",
|
Entry::hint(
|
||||||
});
|
props
|
||||||
session.push(hints.focus_activity, "Inspect activity");
|
.collaboration_modes_enabled
|
||||||
session.entries.push(Shortcut::new(
|
.then_some(key_hint::shift(K::Tab).into()),
|
||||||
key_hint::ctrl(KeyCode::Char('c')),
|
"Change mode",
|
||||||
if props.is_task_running {
|
),
|
||||||
"Interrupt"
|
Entry::hint(hints.reasoning_down, "Less reasoning"),
|
||||||
} else {
|
Entry::hint(hints.reasoning_up, "More reasoning"),
|
||||||
"Quit"
|
Entry::hint(hints.toggle_voice, "Voice"),
|
||||||
},
|
Entry::hint(hints.agents, "Agents (empty prompt)"),
|
||||||
));
|
Entry::pair(
|
||||||
|
"Subagents (empty prompt)",
|
||||||
|
[previous_agent_shortcut(), next_agent_shortcut()],
|
||||||
|
),
|
||||||
|
Entry::hint(hints.focus_activity, "Inspect activity"),
|
||||||
|
Entry::key(
|
||||||
|
key_hint::ctrl(K::Char('c')),
|
||||||
|
if props.is_task_running {
|
||||||
|
"Interrupt"
|
||||||
|
} else {
|
||||||
|
"Quit"
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
let mut transcript = Group {
|
let alt_shift = KeyModifiers::ALT | KeyModifiers::SHIFT;
|
||||||
title: "Transcript (open first)",
|
let transcript = vec![
|
||||||
entries: Vec::new(),
|
Entry::hint(hints.show_transcript, "Open transcript"),
|
||||||
};
|
Entry::hint(hints.find_transcript, "Find text"),
|
||||||
transcript.push(hints.show_transcript, "Open transcript");
|
Entry::pair(
|
||||||
transcript.push(hints.find_transcript, "Find text");
|
"Scroll",
|
||||||
transcript.entries.extend([
|
[key_hint::plain(K::PageUp), key_hint::plain(K::PageDown)],
|
||||||
Shortcut {
|
),
|
||||||
key: "pgup / pgdn".into(),
|
Entry::key(key_hint::ctrl(K::Char(' ')), "Start selection"),
|
||||||
action: "Scroll",
|
Entry::pair(
|
||||||
},
|
"Top / latest",
|
||||||
Shortcut::new(key_hint::ctrl(KeyCode::Char(' ')), "Start selection"),
|
[key_hint::ctrl(K::Home), key_hint::ctrl(K::End)],
|
||||||
Shortcut {
|
),
|
||||||
key: format!(
|
|
||||||
"{} / {}",
|
|
||||||
key_hint::ctrl(KeyCode::Home).display_label(),
|
|
||||||
key_hint::ctrl(KeyCode::End).display_label()
|
|
||||||
),
|
|
||||||
action: "Top / latest",
|
|
||||||
},
|
|
||||||
// Keep both jump alternatives: the terminal may be on another OS over SSH.
|
// Keep both jump alternatives: the terminal may be on another OS over SSH.
|
||||||
Shortcut {
|
Entry::pair(
|
||||||
key: format!(
|
"Top / latest",
|
||||||
"{} / {}",
|
[
|
||||||
key_hint::KeyBinding::new(
|
KeyBinding::new(K::Char(','), alt_shift),
|
||||||
KeyCode::Char(','),
|
KeyBinding::new(K::Char('.'), alt_shift),
|
||||||
KeyModifiers::ALT | KeyModifiers::SHIFT
|
],
|
||||||
)
|
),
|
||||||
.display_label(),
|
];
|
||||||
key_hint::KeyBinding::new(
|
|
||||||
KeyCode::Char('.'),
|
|
||||||
KeyModifiers::ALT | KeyModifiers::SHIFT
|
|
||||||
)
|
|
||||||
.display_label()
|
|
||||||
),
|
|
||||||
action: "Top / latest",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
[
|
||||||
|
("Compose", compose),
|
||||||
|
("Session", session),
|
||||||
|
("Transcript (open first)", transcript),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn lines(props: &FooterProps, width: u16) -> Vec<Line<'static>> {
|
||||||
|
let groups = bindings(props).map(|(title, entries)| {
|
||||||
|
let entries = entries
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let mut keys = entry
|
||||||
|
.keys
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(ShortcutHint::display_label);
|
||||||
|
let key = keys.next()?;
|
||||||
|
Some(Shortcut {
|
||||||
|
key: if entry.repeat_label {
|
||||||
|
format!("{key} {key}")
|
||||||
|
} else if let Some(alternative) = keys.next() {
|
||||||
|
format!("{key} / {alternative}")
|
||||||
|
} else {
|
||||||
|
key
|
||||||
|
},
|
||||||
|
action: entry.action,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Group { title, entries }
|
||||||
|
});
|
||||||
let mut result = vec![Line::from("Keyboard shortcuts").bold(), Line::default()];
|
let mut result = vec![Line::from("Keyboard shortcuts").bold(), Line::default()];
|
||||||
result.extend(crate::shortcut_help::group_lines(
|
result.extend(crate::shortcut_help::group_lines(groups, width));
|
||||||
[compose, session, transcript],
|
|
||||||
width,
|
|
||||||
));
|
|
||||||
let width = usize::from(width.max(/*other*/ 1));
|
let width = usize::from(width.max(/*other*/ 1));
|
||||||
result.push(Line::default());
|
result.push(Line::default());
|
||||||
result.extend(footer_lines(width));
|
result.extend(footer_lines(width));
|
||||||
|
|||||||
Reference in New Issue
Block a user