mirror of
https://github.com/openai/codex.git
synced 2026-11-06 00:30:29 +00:00
Support two-stroke TUI key chords (#36511)
## What changed - Accept two-stroke bindings such as `ctrl-x ctrl-s` in TUI keymap configuration while preserving arrays as alternative bindings. - Route chords through the active TUI context, show pending and configured chord hints, and cancel pending chords on `esc`, context changes, or a one-second timeout. - Extend `/keymap` to capture, add, replace, and display key chords. - Reject chords that conflict with active single-key bindings, overlapping-context chords, or reserved terminal shortcuts. ## Testing - Add coverage for chord parsing, validation, matching, timeout and cancellation behavior, context dispatch, key capture, and rendered shortcut hints. GitOrigin-RevId: dfdede03f518d4936ad8621ebf5537026f2b282e
This commit is contained in:
1 parent
a1dd74b535
commit
1e85ca099e
61 files changed
+3503
-660
No files matched your search
@@ -7,6 +7,83 @@ use super::*;
|
||||
use crate::app_backtrack::SIDE_EDIT_PREVIOUS_UNAVAILABLE_MESSAGE;
|
||||
|
||||
impl App {
|
||||
pub(super) fn route_key_chord_event(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
key_event: KeyEvent,
|
||||
) -> Option<KeyEvent> {
|
||||
let contexts = self.active_keymap_contexts();
|
||||
let was_pending = self.key_chord_matcher.is_pending();
|
||||
match self.key_chord_matcher.advance(
|
||||
key_event,
|
||||
&self.keymap.chords,
|
||||
contexts,
|
||||
tokio::time::Instant::now(),
|
||||
) {
|
||||
crate::keymap::KeyChordMatch::PassThrough => {
|
||||
if was_pending && !self.key_chord_matcher.is_pending() {
|
||||
self.chat_widget.set_footer_hint_override(/*items*/ None);
|
||||
}
|
||||
Some(key_event)
|
||||
}
|
||||
crate::keymap::KeyChordMatch::Pending(prefix) => {
|
||||
if self.backtrack.primed {
|
||||
self.reset_backtrack_state();
|
||||
}
|
||||
self.chat_widget.set_footer_hint_override(Some(vec![
|
||||
(
|
||||
format!("{} …", prefix.display_label()),
|
||||
"waiting for next key".to_string(),
|
||||
),
|
||||
("esc".to_string(), "cancel".to_string()),
|
||||
]));
|
||||
tui.frame_requester()
|
||||
.schedule_frame_in(crate::keymap::KEY_CHORD_TIMEOUT);
|
||||
None
|
||||
}
|
||||
crate::keymap::KeyChordMatch::Completed(dispatch_event) => {
|
||||
self.chat_widget.set_footer_hint_override(/*items*/ None);
|
||||
Some(dispatch_event)
|
||||
}
|
||||
crate::keymap::KeyChordMatch::Cancelled => {
|
||||
self.chat_widget.set_footer_hint_override(/*items*/ None);
|
||||
None
|
||||
}
|
||||
crate::keymap::KeyChordMatch::Ignored => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn expire_pending_key_chord(&mut self) {
|
||||
let contexts = self.active_keymap_contexts();
|
||||
if self
|
||||
.key_chord_matcher
|
||||
.expire(contexts, tokio::time::Instant::now())
|
||||
{
|
||||
self.chat_widget.set_footer_hint_override(/*items*/ None);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn cancel_pending_key_chord(&mut self) {
|
||||
if self.key_chord_matcher.cancel() {
|
||||
self.chat_widget.set_footer_hint_override(/*items*/ None);
|
||||
}
|
||||
}
|
||||
|
||||
fn active_keymap_contexts(&self) -> crate::keymap::KeymapContextSet {
|
||||
if self.overlay.is_some() {
|
||||
return crate::keymap::KeymapContextSet::new(crate::keymap::KeymapContext::Pager);
|
||||
}
|
||||
|
||||
let contexts = self.chat_widget.keymap_contexts();
|
||||
if self.chat_widget.no_modal_or_popup_active() {
|
||||
contexts
|
||||
.with(crate::keymap::KeymapContext::Global)
|
||||
.with(crate::keymap::KeymapContext::Chat)
|
||||
} else {
|
||||
contexts
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn launch_external_editor(&mut self, tui: &mut tui::Tui) {
|
||||
let editor_cmd = match external_editor::resolve_editor_command() {
|
||||
Ok(cmd) => cmd,
|
||||
|
||||
Reference in new issue
Block a user