mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): improve TUI keymap coverage (#20798)
## Summary - normalize terminal-emitted C0 control characters through configurable editor keymaps, covering raw control-key fallbacks like Shift+Enter-as-LF in terminals from #20555 and #20898, plus part of the modified-Enter behavior in #20580 - add default-unbound keymap actions for toggling Fast mode and killing the current composer line, giving #20698 users a configurable zsh-style Ctrl+U option without changing the existing default Ctrl+U behavior - wire the new actions through gated /keymap picker entries, schema generation, and snapshot coverage Fixes #20555. Fixes #20898. ## Testing - just write-config-schema - just fmt - cargo test -p codex-config - cargo test -p codex-tui keymap::tests - cargo test -p codex-tui bottom_pane::textarea::tests - cargo test -p codex-tui keymap_setup::tests - cargo insta pending-snapshots - just fix -p codex-tui - git diff --check - just argument-comment-lint
This commit is contained in:
committed by
GitHub
Unverified
parent
cc16995cc6
commit
48402be6fa
@@ -104,6 +104,8 @@ pub struct TuiGlobalKeymap {
|
||||
pub toggle_shortcuts: Option<KeybindingsSpec>,
|
||||
/// Toggle Vim mode for the composer input.
|
||||
pub toggle_vim_mode: Option<KeybindingsSpec>,
|
||||
/// Toggle Fast mode.
|
||||
pub toggle_fast_mode: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Chat context keybindings.
|
||||
@@ -169,6 +171,8 @@ pub struct TuiEditorKeymap {
|
||||
pub delete_forward_word: Option<KeybindingsSpec>,
|
||||
/// Kill text from cursor to line start.
|
||||
pub kill_line_start: Option<KeybindingsSpec>,
|
||||
/// Kill the current line.
|
||||
pub kill_whole_line: Option<KeybindingsSpec>,
|
||||
/// Kill text from cursor to line end.
|
||||
pub kill_line_end: Option<KeybindingsSpec>,
|
||||
/// Yank the kill buffer.
|
||||
|
||||
@@ -2441,6 +2441,7 @@
|
||||
"insert_newline": null,
|
||||
"kill_line_end": null,
|
||||
"kill_line_start": null,
|
||||
"kill_whole_line": null,
|
||||
"move_down": null,
|
||||
"move_left": null,
|
||||
"move_line_end": null,
|
||||
@@ -2458,6 +2459,7 @@
|
||||
"open_transcript": null,
|
||||
"queue": null,
|
||||
"submit": null,
|
||||
"toggle_fast_mode": null,
|
||||
"toggle_shortcuts": null,
|
||||
"toggle_vim_mode": null
|
||||
},
|
||||
@@ -2811,6 +2813,14 @@
|
||||
],
|
||||
"description": "Kill text from cursor to line start."
|
||||
},
|
||||
"kill_whole_line": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Kill the current line."
|
||||
},
|
||||
"move_down": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2938,6 +2948,14 @@
|
||||
],
|
||||
"description": "Submit the current composer draft."
|
||||
},
|
||||
"toggle_fast_mode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Toggle Fast mode."
|
||||
},
|
||||
"toggle_shortcuts": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3018,6 +3036,7 @@
|
||||
"insert_newline": null,
|
||||
"kill_line_end": null,
|
||||
"kill_line_start": null,
|
||||
"kill_whole_line": null,
|
||||
"move_down": null,
|
||||
"move_left": null,
|
||||
"move_line_end": null,
|
||||
@@ -3042,6 +3061,7 @@
|
||||
"open_transcript": null,
|
||||
"queue": null,
|
||||
"submit": null,
|
||||
"toggle_fast_mode": null,
|
||||
"toggle_shortcuts": null,
|
||||
"toggle_vim_mode": null
|
||||
}
|
||||
|
||||
@@ -129,6 +129,14 @@ impl App {
|
||||
return;
|
||||
}
|
||||
|
||||
if app_keymap_shortcuts_available
|
||||
&& self.keymap.app.toggle_fast_mode.is_pressed(key_event)
|
||||
&& self.chat_widget.can_toggle_fast_mode_from_keybinding()
|
||||
{
|
||||
self.chat_widget.toggle_fast_mode_from_ui();
|
||||
return;
|
||||
}
|
||||
|
||||
if app_keymap_shortcuts_available && self.keymap.app.open_transcript.is_pressed(key_event) {
|
||||
// Enter alternate screen and set viewport to full size.
|
||||
let _ = tui.enter_alt_screen();
|
||||
|
||||
@@ -569,6 +569,10 @@ impl TextArea {
|
||||
self.kill_to_beginning_of_line();
|
||||
return;
|
||||
}
|
||||
if keymap.kill_whole_line.is_pressed(event) {
|
||||
self.kill_current_line();
|
||||
return;
|
||||
}
|
||||
if keymap.kill_line_end.is_pressed(event) {
|
||||
self.kill_to_end_of_line();
|
||||
return;
|
||||
@@ -780,7 +784,7 @@ impl TextArea {
|
||||
|
||||
fn handle_vim_operator(&mut self, op: VimOperator, event: KeyEvent) -> bool {
|
||||
if op == VimOperator::Delete && self.vim_operator_keymap.delete_line.is_pressed(event) {
|
||||
self.delete_current_line();
|
||||
self.kill_current_line();
|
||||
return true;
|
||||
}
|
||||
if op == VimOperator::Yank && self.vim_operator_keymap.yank_line.is_pressed(event) {
|
||||
@@ -1116,7 +1120,7 @@ impl TextArea {
|
||||
self.yank_line_range(range);
|
||||
}
|
||||
|
||||
fn delete_current_line(&mut self) {
|
||||
fn kill_current_line(&mut self) {
|
||||
let range = self.current_line_range_with_newline();
|
||||
self.kill_line_range(range);
|
||||
}
|
||||
@@ -2447,6 +2451,51 @@ mod tests {
|
||||
assert_eq!(t.cursor(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kill_current_line_removes_current_line_linewise() {
|
||||
let mut t = ta_with("abc\ndef\nghi");
|
||||
t.set_cursor(/*pos*/ 5);
|
||||
|
||||
t.kill_current_line();
|
||||
|
||||
assert_eq!(t.text(), "abc\nghi");
|
||||
assert_eq!(t.cursor(), 4);
|
||||
assert_eq!(t.kill_buffer, "def\n");
|
||||
assert_eq!(t.kill_buffer_kind, KillBufferKind::Linewise);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kill_current_line_keeps_previous_newline_for_final_line() {
|
||||
let mut t = ta_with("abc\ndef");
|
||||
t.set_cursor(/*pos*/ 5);
|
||||
|
||||
t.kill_current_line();
|
||||
|
||||
assert_eq!(t.text(), "abc\n");
|
||||
assert_eq!(t.cursor(), 4);
|
||||
assert_eq!(t.kill_buffer, "def");
|
||||
assert_eq!(t.kill_buffer_kind, KillBufferKind::Linewise);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kill_whole_line_keymap_dispatch_uses_linewise_kill() {
|
||||
let mut t = ta_with("abc\ndef\nghi");
|
||||
t.set_cursor(/*pos*/ 5);
|
||||
let mut keymap = RuntimeKeymap::defaults().editor;
|
||||
keymap.kill_line_start.clear();
|
||||
keymap.kill_whole_line = vec![key_hint::ctrl(KeyCode::Char('u'))];
|
||||
|
||||
t.input_with_keymap(
|
||||
KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL),
|
||||
&keymap,
|
||||
);
|
||||
|
||||
assert_eq!(t.text(), "abc\nghi");
|
||||
assert_eq!(t.cursor(), 4);
|
||||
assert_eq!(t.kill_buffer, "def\n");
|
||||
assert_eq!(t.kill_buffer_kind, KillBufferKind::Linewise);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_forward_word_variants() {
|
||||
let mut t = ta_with("hello world ");
|
||||
@@ -2668,6 +2717,17 @@ mod tests {
|
||||
assert_eq!(t.cursor(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c0_line_feed_inserts_newline_through_insert_newline_keymap() {
|
||||
let mut t = ta_with("ab");
|
||||
t.set_cursor(/*pos*/ 1);
|
||||
|
||||
t.input(KeyEvent::new(KeyCode::Char('\u{000a}'), KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(t.text(), "a\nb");
|
||||
assert_eq!(t.cursor(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c0_control_chars_respect_unbound_editor_movement() {
|
||||
let mut t = ta_with("a\nb");
|
||||
|
||||
@@ -9313,6 +9313,12 @@ impl ChatWidget {
|
||||
self.config.features.enabled(Feature::FastMode)
|
||||
}
|
||||
|
||||
pub(crate) fn can_toggle_fast_mode_from_keybinding(&self) -> bool {
|
||||
self.fast_mode_enabled()
|
||||
&& !self.is_user_turn_pending_or_running()
|
||||
&& self.bottom_pane.no_modal_or_popup_active()
|
||||
}
|
||||
|
||||
pub(crate) fn set_realtime_audio_device(
|
||||
&mut self,
|
||||
kind: RealtimeAudioDeviceKind,
|
||||
@@ -9367,6 +9373,15 @@ impl ChatWidget {
|
||||
.send(AppEvent::PersistServiceTierSelection { service_tier });
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_fast_mode_from_ui(&mut self) {
|
||||
let next_tier = if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) {
|
||||
None
|
||||
} else {
|
||||
Some(ServiceTier::Fast)
|
||||
};
|
||||
self.set_service_tier_selection(next_tier);
|
||||
}
|
||||
|
||||
pub(crate) fn current_model(&self) -> &str {
|
||||
if !self.collaboration_modes_enabled() {
|
||||
return self.current_collaboration_mode.model();
|
||||
|
||||
@@ -30,9 +30,10 @@ impl ChatWidget {
|
||||
pub(crate) fn open_keymap_picker(&mut self) {
|
||||
match RuntimeKeymap::from_config(&self.config.tui_keymap) {
|
||||
Ok(runtime_keymap) => {
|
||||
let params = keymap_setup::build_keymap_picker_params(
|
||||
let params = keymap_setup::build_keymap_picker_params_with_filter(
|
||||
&runtime_keymap,
|
||||
&self.config.tui_keymap,
|
||||
self.keymap_action_filter(),
|
||||
);
|
||||
self.bottom_pane.show_selection_view(params);
|
||||
}
|
||||
@@ -120,9 +121,10 @@ impl ChatWidget {
|
||||
action: &str,
|
||||
runtime_keymap: &RuntimeKeymap,
|
||||
) {
|
||||
let params = keymap_setup::build_keymap_picker_params_for_selected_action(
|
||||
let params = keymap_setup::build_keymap_picker_params_for_selected_action_with_filter(
|
||||
runtime_keymap,
|
||||
&self.config.tui_keymap,
|
||||
self.keymap_action_filter(),
|
||||
context,
|
||||
action,
|
||||
);
|
||||
@@ -135,9 +137,10 @@ impl ChatWidget {
|
||||
params,
|
||||
);
|
||||
if !replaced {
|
||||
let params = keymap_setup::build_keymap_picker_params_for_selected_action(
|
||||
let params = keymap_setup::build_keymap_picker_params_for_selected_action_with_filter(
|
||||
runtime_keymap,
|
||||
&self.config.tui_keymap,
|
||||
self.keymap_action_filter(),
|
||||
context,
|
||||
action,
|
||||
);
|
||||
@@ -146,6 +149,12 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn keymap_action_filter(&self) -> keymap_setup::KeymapActionFilter {
|
||||
keymap_setup::KeymapActionFilter {
|
||||
fast_mode_enabled: self.fast_mode_enabled(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a committed keymap edit to the live chat widget.
|
||||
///
|
||||
/// The caller is responsible for persisting the config file before invoking this method. This
|
||||
|
||||
@@ -178,12 +178,7 @@ impl ChatWidget {
|
||||
self.open_model_popup();
|
||||
}
|
||||
SlashCommand::Fast => {
|
||||
let next_tier = if matches!(self.current_service_tier(), Some(ServiceTier::Fast)) {
|
||||
None
|
||||
} else {
|
||||
Some(ServiceTier::Fast)
|
||||
};
|
||||
self.set_service_tier_selection(next_tier);
|
||||
self.toggle_fast_mode_from_ui();
|
||||
}
|
||||
SlashCommand::Realtime => {
|
||||
if !self.realtime_conversation_enabled() {
|
||||
|
||||
@@ -1817,6 +1817,51 @@ async fn fast_slash_command_updates_and_persists_local_service_tier() {
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fast_keybinding_toggle_uses_same_events_as_fast_slash_command() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
|
||||
chat.set_feature_enabled(Feature::FastMode, /*enabled*/ true);
|
||||
|
||||
chat.toggle_fast_mode_from_ui();
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
service_tier: Some(Some(ServiceTier::Fast)),
|
||||
..
|
||||
})
|
||||
)),
|
||||
"expected fast-mode override app event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::PersistServiceTierSelection {
|
||||
service_tier: Some(ServiceTier::Fast),
|
||||
}
|
||||
)),
|
||||
"expected fast-mode persistence app event; events: {events:?}"
|
||||
);
|
||||
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fast_keybinding_toggle_requires_feature_and_idle_surface() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
|
||||
chat.set_feature_enabled(Feature::FastMode, /*enabled*/ false);
|
||||
|
||||
assert!(!chat.can_toggle_fast_mode_from_keybinding());
|
||||
|
||||
chat.set_feature_enabled(Feature::FastMode, /*enabled*/ true);
|
||||
assert!(chat.can_toggle_fast_mode_from_keybinding());
|
||||
|
||||
chat.bottom_pane.set_task_running(/*running*/ true);
|
||||
assert!(!chat.can_toggle_fast_mode_from_keybinding());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_turn_carries_service_tier_after_fast_toggle() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//!
|
||||
//! This module provides `KeyBinding`, the runtime representation of a single
|
||||
//! keybinding (key code + modifier set), along with matching logic that handles
|
||||
//! cross-terminal inconsistencies in how shifted letters are reported.
|
||||
//! cross-terminal inconsistencies in how shifted letters and raw C0 control
|
||||
//! characters are reported.
|
||||
//!
|
||||
//! It also supplies rendering helpers that convert bindings into styled
|
||||
//! `ratatui::text::Span` values for UI hint display.
|
||||
@@ -26,10 +27,10 @@ const SHIFT_PREFIX: &str = "shift + ";
|
||||
|
||||
/// One concrete key event that can trigger a TUI action.
|
||||
///
|
||||
/// Matching via `is_press` handles both exact equality and a shifted-letter
|
||||
/// compatibility fallback for terminals that report uppercase letters without
|
||||
/// the SHIFT modifier flag. This means a binding defined as `shift-a` will
|
||||
/// match a terminal event of either `Shift+a` or plain `A`.
|
||||
/// Matching via `is_press` handles exact equality plus compatibility fallbacks
|
||||
/// for terminals that report uppercase letters without SHIFT and Ctrl keys as
|
||||
/// raw C0 control characters. This means a binding defined as `shift-a` will
|
||||
/// match either `Shift+a` or plain `A`, and `ctrl-j` will match raw LF.
|
||||
///
|
||||
/// This does not model multi-key chords or partial matches; callers that need
|
||||
/// sequences must keep that state outside this type.
|
||||
@@ -45,13 +46,13 @@ impl KeyBinding {
|
||||
}
|
||||
|
||||
pub(crate) fn from_event(event: KeyEvent) -> Self {
|
||||
let (key, modifiers) = normalize_shifted_ascii_char(event.code, event.modifiers);
|
||||
let (key, modifiers) = normalize_key_parts(event.code, event.modifiers);
|
||||
Self { key, modifiers }
|
||||
}
|
||||
|
||||
pub fn is_press(&self, event: KeyEvent) -> bool {
|
||||
normalize_shifted_ascii_char(self.key, self.modifiers)
|
||||
== normalize_shifted_ascii_char(event.code, event.modifiers)
|
||||
normalize_key_parts(self.key, self.modifiers)
|
||||
== normalize_key_parts(event.code, event.modifiers)
|
||||
&& (event.kind == KeyEventKind::Press || event.kind == KeyEventKind::Repeat)
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ impl KeyBinding {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_shifted_ascii_char(
|
||||
pub(crate) fn normalize_key_parts(
|
||||
key: KeyCode,
|
||||
mut modifiers: KeyModifiers,
|
||||
) -> (KeyCode, KeyModifiers) {
|
||||
@@ -96,13 +97,11 @@ fn normalize_shifted_ascii_char(
|
||||
}
|
||||
|
||||
fn c0_control_char_to_ctrl_char(ch: char) -> Option<char> {
|
||||
match ch {
|
||||
'\u{0002}' => Some('b'),
|
||||
'\u{0006}' => Some('f'),
|
||||
'\u{000e}' => Some('n'),
|
||||
'\u{0010}' => Some('p'),
|
||||
'\u{0012}' => Some('r'),
|
||||
'\u{0013}' => Some('s'),
|
||||
let code = u32::from(ch);
|
||||
match code {
|
||||
0x00 => Some(' '),
|
||||
0x01..=0x1a => char::from_u32(code - 0x01 + u32::from('a')),
|
||||
0x1c..=0x1f => char::from_u32(code - 0x1c + u32::from('4')),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -256,6 +255,68 @@ mod tests {
|
||||
assert!(!binding.is_press(KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::ALT)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_bindings_match_all_supported_c0_control_char_events() {
|
||||
let cases = [
|
||||
(' ', '\u{0000}'),
|
||||
('a', '\u{0001}'),
|
||||
('b', '\u{0002}'),
|
||||
('c', '\u{0003}'),
|
||||
('d', '\u{0004}'),
|
||||
('e', '\u{0005}'),
|
||||
('f', '\u{0006}'),
|
||||
('g', '\u{0007}'),
|
||||
('h', '\u{0008}'),
|
||||
('i', '\u{0009}'),
|
||||
('j', '\u{000a}'),
|
||||
('k', '\u{000b}'),
|
||||
('l', '\u{000c}'),
|
||||
('m', '\u{000d}'),
|
||||
('n', '\u{000e}'),
|
||||
('o', '\u{000f}'),
|
||||
('p', '\u{0010}'),
|
||||
('q', '\u{0011}'),
|
||||
('r', '\u{0012}'),
|
||||
('s', '\u{0013}'),
|
||||
('t', '\u{0014}'),
|
||||
('u', '\u{0015}'),
|
||||
('v', '\u{0016}'),
|
||||
('w', '\u{0017}'),
|
||||
('x', '\u{0018}'),
|
||||
('y', '\u{0019}'),
|
||||
('z', '\u{001a}'),
|
||||
('4', '\u{001c}'),
|
||||
('5', '\u{001d}'),
|
||||
('6', '\u{001e}'),
|
||||
('7', '\u{001f}'),
|
||||
];
|
||||
|
||||
for (ctrl_char, c0_char) in cases {
|
||||
assert!(
|
||||
ctrl(KeyCode::Char(ctrl_char))
|
||||
.is_press(KeyEvent::new(KeyCode::Char(c0_char), KeyModifiers::NONE)),
|
||||
"expected raw C0 {c0_char:?} to match ctrl-{ctrl_char}"
|
||||
);
|
||||
assert!(
|
||||
!ctrl(KeyCode::Char(ctrl_char))
|
||||
.is_press(KeyEvent::new(KeyCode::Char(c0_char), KeyModifiers::ALT)),
|
||||
"expected modified raw C0 {c0_char:?} not to match ctrl-{ctrl_char}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_binding_does_not_match_ambiguous_c0_escape_or_delete() {
|
||||
assert!(
|
||||
!ctrl(KeyCode::Char('['))
|
||||
.is_press(KeyEvent::new(KeyCode::Char('\u{001b}'), KeyModifiers::NONE,))
|
||||
);
|
||||
assert!(
|
||||
!ctrl(KeyCode::Char('?'))
|
||||
.is_press(KeyEvent::new(KeyCode::Char('\u{007f}'), KeyModifiers::NONE,))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_search_ctrl_bindings_match_c0_control_char_events() {
|
||||
assert!(
|
||||
|
||||
@@ -63,6 +63,8 @@ pub(crate) struct AppKeymap {
|
||||
pub(crate) clear_terminal: Vec<KeyBinding>,
|
||||
/// Toggle Vim mode for the composer input.
|
||||
pub(crate) toggle_vim_mode: Vec<KeyBinding>,
|
||||
/// Toggle Fast mode.
|
||||
pub(crate) toggle_fast_mode: Vec<KeyBinding>,
|
||||
}
|
||||
|
||||
/// Chat-level keybindings evaluated at the app event layer.
|
||||
@@ -120,6 +122,7 @@ pub(crate) struct EditorKeymap {
|
||||
pub(crate) delete_backward_word: Vec<KeyBinding>,
|
||||
pub(crate) delete_forward_word: Vec<KeyBinding>,
|
||||
pub(crate) kill_line_start: Vec<KeyBinding>,
|
||||
pub(crate) kill_whole_line: Vec<KeyBinding>,
|
||||
pub(crate) kill_line_end: Vec<KeyBinding>,
|
||||
pub(crate) yank: Vec<KeyBinding>,
|
||||
}
|
||||
@@ -369,6 +372,11 @@ impl RuntimeKeymap {
|
||||
&defaults.app.toggle_vim_mode,
|
||||
"tui.keymap.global.toggle_vim_mode",
|
||||
)?,
|
||||
toggle_fast_mode: resolve_bindings(
|
||||
keymap.global.toggle_fast_mode.as_ref(),
|
||||
&defaults.app.toggle_fast_mode,
|
||||
"tui.keymap.global.toggle_fast_mode",
|
||||
)?,
|
||||
};
|
||||
|
||||
let chat = ChatKeymap {
|
||||
@@ -417,6 +425,7 @@ impl RuntimeKeymap {
|
||||
delete_backward_word: resolve_local!(keymap, defaults, editor, delete_backward_word),
|
||||
delete_forward_word: resolve_local!(keymap, defaults, editor, delete_forward_word),
|
||||
kill_line_start: resolve_local!(keymap, defaults, editor, kill_line_start),
|
||||
kill_whole_line: resolve_local!(keymap, defaults, editor, kill_whole_line),
|
||||
kill_line_end: resolve_local!(keymap, defaults, editor, kill_line_end),
|
||||
yank: resolve_local!(keymap, defaults, editor, yank),
|
||||
};
|
||||
@@ -536,6 +545,7 @@ impl RuntimeKeymap {
|
||||
copy: default_bindings![ctrl(KeyCode::Char('o'))],
|
||||
clear_terminal: default_bindings![ctrl(KeyCode::Char('l'))],
|
||||
toggle_vim_mode: default_bindings![],
|
||||
toggle_fast_mode: default_bindings![],
|
||||
},
|
||||
chat: ChatKeymap {
|
||||
decrease_reasoning_effort: default_bindings![alt(KeyCode::Char(','))],
|
||||
@@ -594,6 +604,7 @@ impl RuntimeKeymap {
|
||||
alt(KeyCode::Char('d'))
|
||||
],
|
||||
kill_line_start: default_bindings![ctrl(KeyCode::Char('u'))],
|
||||
kill_whole_line: default_bindings![],
|
||||
kill_line_end: default_bindings![ctrl(KeyCode::Char('k'))],
|
||||
yank: default_bindings![ctrl(KeyCode::Char('y'))],
|
||||
},
|
||||
@@ -727,6 +738,7 @@ impl RuntimeKeymap {
|
||||
("copy", self.app.copy.as_slice()),
|
||||
("clear_terminal", self.app.clear_terminal.as_slice()),
|
||||
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
|
||||
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
|
||||
(
|
||||
"chat.decrease_reasoning_effort",
|
||||
self.chat.decrease_reasoning_effort.as_slice(),
|
||||
@@ -767,6 +779,7 @@ impl RuntimeKeymap {
|
||||
("copy", self.app.copy.as_slice()),
|
||||
("clear_terminal", self.app.clear_terminal.as_slice()),
|
||||
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
|
||||
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
|
||||
(
|
||||
"chat.decrease_reasoning_effort",
|
||||
self.chat.decrease_reasoning_effort.as_slice(),
|
||||
@@ -808,6 +821,7 @@ impl RuntimeKeymap {
|
||||
("copy", self.app.copy.as_slice()),
|
||||
("clear_terminal", self.app.clear_terminal.as_slice()),
|
||||
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
|
||||
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
|
||||
],
|
||||
[
|
||||
("list.move_up", self.list.move_up.as_slice()),
|
||||
@@ -856,6 +870,7 @@ impl RuntimeKeymap {
|
||||
),
|
||||
("composer.submit", self.composer.submit.as_slice()),
|
||||
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
|
||||
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
|
||||
(
|
||||
"composer.history_search_previous",
|
||||
self.composer.history_search_previous.as_slice(),
|
||||
@@ -903,6 +918,10 @@ impl RuntimeKeymap {
|
||||
"editor.kill_line_start",
|
||||
self.editor.kill_line_start.as_slice(),
|
||||
),
|
||||
(
|
||||
"editor.kill_whole_line",
|
||||
self.editor.kill_whole_line.as_slice(),
|
||||
),
|
||||
("editor.kill_line_end", self.editor.kill_line_end.as_slice()),
|
||||
("editor.yank", self.editor.yank.as_slice()),
|
||||
],
|
||||
@@ -936,6 +955,7 @@ impl RuntimeKeymap {
|
||||
self.editor.delete_forward_word.as_slice(),
|
||||
),
|
||||
("kill_line_start", self.editor.kill_line_start.as_slice()),
|
||||
("kill_whole_line", self.editor.kill_whole_line.as_slice()),
|
||||
("kill_line_end", self.editor.kill_line_end.as_slice()),
|
||||
("yank", self.editor.yank.as_slice()),
|
||||
],
|
||||
@@ -1389,6 +1409,7 @@ fn parse_keybinding(spec: &str) -> Option<KeyBinding> {
|
||||
"page-up" => KeyCode::PageUp,
|
||||
"page-down" => KeyCode::PageDown,
|
||||
"space" => KeyCode::Char(' '),
|
||||
"minus" => KeyCode::Char('-'),
|
||||
other if other.len() == 1 => KeyCode::Char(char::from(other.as_bytes()[0])),
|
||||
other if other.starts_with('f') => {
|
||||
let number = other[1..].parse::<u8>().ok()?;
|
||||
@@ -1587,6 +1608,7 @@ mod tests {
|
||||
runtime.app.clear_terminal,
|
||||
vec![key_hint::ctrl(KeyCode::Char('l'))]
|
||||
);
|
||||
assert_eq!(runtime.app.toggle_fast_mode, Vec::new());
|
||||
assert_eq!(
|
||||
runtime.chat.decrease_reasoning_effort,
|
||||
vec![key_hint::alt(KeyCode::Char(','))]
|
||||
@@ -1607,6 +1629,7 @@ mod tests {
|
||||
runtime.composer.history_search_next,
|
||||
vec![key_hint::ctrl(KeyCode::Char('s'))]
|
||||
);
|
||||
assert_eq!(runtime.editor.kill_whole_line, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1732,6 +1755,61 @@ mod tests {
|
||||
assert_eq!(runtime.app.copy, vec![key_hint::alt(KeyCode::Char('.'))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kill_whole_line_can_be_assigned_without_default_binding() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.editor.kill_whole_line = Some(one("ctrl-shift-u"));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");
|
||||
|
||||
assert_eq!(
|
||||
runtime.editor.kill_whole_line,
|
||||
vec![KeyBinding::new(
|
||||
KeyCode::Char('u'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kill_whole_line_conflicts_until_kill_line_start_is_unbound() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.editor.kill_whole_line = Some(one("ctrl-u"));
|
||||
|
||||
expect_conflict(&keymap, "kill_line_start", "kill_whole_line");
|
||||
|
||||
keymap.editor.kill_line_start = Some(KeybindingsSpec::Many(vec![]));
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("remapped key should be free");
|
||||
assert_eq!(
|
||||
runtime.editor.kill_whole_line,
|
||||
vec![key_hint::ctrl(KeyCode::Char('u'))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_fast_mode_can_be_assigned_without_default_binding() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.global.toggle_fast_mode = Some(one("ctrl-shift-f"));
|
||||
|
||||
let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");
|
||||
|
||||
assert_eq!(
|
||||
runtime.app.toggle_fast_mode,
|
||||
vec![KeyBinding::new(
|
||||
KeyCode::Char('f'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_fast_mode_conflicts_with_existing_main_surface_bindings() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.global.toggle_fast_mode = Some(one("ctrl-l"));
|
||||
|
||||
expect_conflict(&keymap, "clear_terminal", "toggle_fast_mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_main_bindings_that_collide_with_remaining_fixed_shortcuts() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
@@ -1773,6 +1851,7 @@ mod tests {
|
||||
("page-up", KeyCode::PageUp),
|
||||
("page-down", KeyCode::PageDown),
|
||||
("space", KeyCode::Char(' ')),
|
||||
("minus", KeyCode::Char('-')),
|
||||
];
|
||||
|
||||
for (spec, expected_key) in cases {
|
||||
@@ -1790,6 +1869,22 @@ mod tests {
|
||||
assert_eq!(parse_keybinding("ff"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_minus_alias_and_legacy_literal_minus() {
|
||||
assert_eq!(
|
||||
parse_keybinding("alt-minus").map(|binding| binding.parts()),
|
||||
Some((KeyCode::Char('-'), KeyModifiers::ALT))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_keybinding("alt--").map(|binding| binding.parts()),
|
||||
Some((KeyCode::Char('-'), KeyModifiers::ALT))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_keybinding("-").map(|binding| binding.parts()),
|
||||
Some((KeyCode::Char('-'), KeyModifiers::NONE))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_empty_array_unbinds_action() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
|
||||
@@ -21,10 +21,15 @@ mod actions;
|
||||
mod debug;
|
||||
mod picker;
|
||||
|
||||
pub(crate) use actions::KeymapActionFilter;
|
||||
pub(crate) use debug::build_keymap_debug_view;
|
||||
pub(crate) use picker::KEYMAP_PICKER_VIEW_ID;
|
||||
#[cfg(test)]
|
||||
pub(crate) use picker::build_keymap_picker_params;
|
||||
#[cfg(test)]
|
||||
pub(crate) use picker::build_keymap_picker_params_for_selected_action;
|
||||
pub(crate) use picker::build_keymap_picker_params_for_selected_action_with_filter;
|
||||
pub(crate) use picker::build_keymap_picker_params_with_filter;
|
||||
|
||||
use codex_config::types::KeybindingSpec;
|
||||
use codex_config::types::KeybindingsSpec;
|
||||
@@ -708,6 +713,9 @@ fn key_parts_to_config_key_spec(
|
||||
code: KeyCode,
|
||||
mut modifiers: KeyModifiers,
|
||||
) -> Result<String, String> {
|
||||
let (code, normalized_modifiers) = crate::key_hint::normalize_key_parts(code, modifiers);
|
||||
modifiers = normalized_modifiers;
|
||||
|
||||
let supported_modifiers = KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT;
|
||||
if !modifiers.difference(supported_modifiers).is_empty() {
|
||||
return Err(
|
||||
@@ -738,7 +746,7 @@ fn key_parts_to_config_key_spec(
|
||||
KeyCode::Char(' ') => "space".to_string(),
|
||||
KeyCode::Char(mut ch) => {
|
||||
if ch == '-' {
|
||||
return Err("The `-` key cannot be represented in `tui.keymap` yet.".to_string());
|
||||
return Ok(format_key_spec(modifiers, "minus"));
|
||||
}
|
||||
if !ch.is_ascii() || ch.is_ascii_control() {
|
||||
return Err("Only printable ASCII keys can be stored in `tui.keymap`.".to_string());
|
||||
@@ -754,18 +762,22 @@ fn key_parts_to_config_key_spec(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(format_key_spec(modifiers, &key))
|
||||
}
|
||||
|
||||
fn format_key_spec(modifiers: KeyModifiers, key: &str) -> String {
|
||||
let mut parts = Vec::new();
|
||||
if modifiers.contains(KeyModifiers::CONTROL) {
|
||||
parts.push("ctrl".to_string());
|
||||
parts.push("ctrl");
|
||||
}
|
||||
if modifiers.contains(KeyModifiers::ALT) {
|
||||
parts.push("alt".to_string());
|
||||
parts.push("alt");
|
||||
}
|
||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||
parts.push("shift".to_string());
|
||||
parts.push("shift");
|
||||
}
|
||||
parts.push(key);
|
||||
Ok(parts.join("-"))
|
||||
parts.join("-")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -821,6 +833,12 @@ mod tests {
|
||||
render_buffer(&buf)
|
||||
}
|
||||
|
||||
fn fast_mode_action_filter() -> KeymapActionFilter {
|
||||
KeymapActionFilter {
|
||||
fast_mode_enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_buffer(buf: &Buffer) -> String {
|
||||
let area = buf.area();
|
||||
(0..area.height)
|
||||
@@ -891,7 +909,11 @@ mod tests {
|
||||
#[test]
|
||||
fn picker_covers_every_replaceable_action() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());
|
||||
let params = build_keymap_picker_params_with_filter(
|
||||
&runtime,
|
||||
&TuiKeymap::default(),
|
||||
fast_mode_action_filter(),
|
||||
);
|
||||
let all_tab = selection_tab(¶ms, KEYMAP_ALL_TAB_ID);
|
||||
|
||||
assert!(params.items.is_empty());
|
||||
@@ -913,6 +935,57 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_hides_fast_mode_action_when_feature_is_disabled() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());
|
||||
let all_tab = selection_tab(¶ms, KEYMAP_ALL_TAB_ID);
|
||||
|
||||
assert!(
|
||||
all_tab
|
||||
.items
|
||||
.iter()
|
||||
.all(|item| item.name != "Toggle Fast Mode")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_shows_fast_mode_action_when_feature_is_enabled() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
let params = build_keymap_picker_params_with_filter(
|
||||
&runtime,
|
||||
&TuiKeymap::default(),
|
||||
fast_mode_action_filter(),
|
||||
);
|
||||
let all_tab = selection_tab(¶ms, KEYMAP_ALL_TAB_ID);
|
||||
let common_tab = selection_tab(¶ms, KEYMAP_COMMON_TAB_ID);
|
||||
let app_tab = selection_tab(¶ms, "app-shortcuts");
|
||||
let unbound_tab = selection_tab(¶ms, KEYMAP_UNBOUND_TAB_ID);
|
||||
|
||||
for tab in [all_tab, common_tab, app_tab, unbound_tab] {
|
||||
assert!(
|
||||
tab.items.iter().any(|item| item.name == "Toggle Fast Mode"),
|
||||
"expected Toggle Fast Mode in {}",
|
||||
tab.label
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_picker_fast_mode_enabled_snapshot() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
let params = build_keymap_picker_params_with_filter(
|
||||
&runtime,
|
||||
&TuiKeymap::default(),
|
||||
fast_mode_action_filter(),
|
||||
);
|
||||
|
||||
assert_snapshot!(
|
||||
"keymap_picker_fast_mode_enabled",
|
||||
render_picker(params, /*width*/ 120)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_common_tab_lists_curated_actions() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
@@ -1050,10 +1123,13 @@ mod tests {
|
||||
let params = build_keymap_picker_params(&runtime, &TuiKeymap::default());
|
||||
let unbound_tab = selection_tab(¶ms, KEYMAP_UNBOUND_TAB_ID);
|
||||
|
||||
assert_eq!(unbound_tab.items.len(), 1);
|
||||
assert_eq!(unbound_tab.items.len(), 2);
|
||||
assert_eq!(unbound_tab.items[0].name, "Toggle Vim Mode");
|
||||
assert_eq!(unbound_tab.items[0].description.as_deref(), Some("unbound"));
|
||||
assert!(!unbound_tab.items[0].is_disabled);
|
||||
assert_eq!(unbound_tab.items[1].name, "Kill Whole Line");
|
||||
assert_eq!(unbound_tab.items[1].description.as_deref(), Some("unbound"));
|
||||
assert!(!unbound_tab.items[1].is_disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1534,21 +1610,46 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_capture_serializes_c0_control_fallbacks() {
|
||||
fn key_capture_serializes_c0_control_chars_as_ctrl_bindings() {
|
||||
assert_eq!(
|
||||
key_event_to_config_key_spec(KeyEvent::new(
|
||||
KeyCode::Char('\u{000a}'),
|
||||
KeyModifiers::NONE,
|
||||
)),
|
||||
Ok("ctrl-j".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
key_event_to_config_key_spec(KeyEvent::new(
|
||||
KeyCode::Char('\u{0015}'),
|
||||
KeyModifiers::NONE,
|
||||
)),
|
||||
Ok("ctrl-u".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
key_event_to_config_key_spec(KeyEvent::new(
|
||||
KeyCode::Char('\u{0010}'),
|
||||
KeyModifiers::NONE
|
||||
KeyModifiers::NONE,
|
||||
)),
|
||||
Ok("ctrl-p".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_capture_rejects_unrepresentable_keys() {
|
||||
assert!(
|
||||
key_event_to_config_key_spec(KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE))
|
||||
.is_err()
|
||||
fn key_capture_serializes_minus_as_named_key() {
|
||||
assert_eq!(
|
||||
key_event_to_config_key_spec(KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE)),
|
||||
Ok("minus".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
key_event_to_config_key_spec(KeyEvent::new(KeyCode::Char('-'), KeyModifiers::ALT)),
|
||||
Ok("alt-minus".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
key_event_to_config_key_spec(KeyEvent::new(
|
||||
KeyCode::Char('-'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT,
|
||||
)),
|
||||
Ok("ctrl-alt-minus".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ pub(super) struct KeymapActionDescriptor {
|
||||
pub(super) action: &'static str,
|
||||
/// Short user-facing explanation of what the action does.
|
||||
pub(super) description: &'static str,
|
||||
/// Feature required before the action appears in `/keymap`.
|
||||
required_feature: Option<KeymapActionFeature>,
|
||||
}
|
||||
|
||||
const fn action(
|
||||
@@ -43,6 +45,42 @@ const fn action(
|
||||
context_label,
|
||||
action,
|
||||
description,
|
||||
required_feature: None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn gated_action(
|
||||
context: &'static str,
|
||||
context_label: &'static str,
|
||||
action: &'static str,
|
||||
description: &'static str,
|
||||
required_feature: KeymapActionFeature,
|
||||
) -> KeymapActionDescriptor {
|
||||
KeymapActionDescriptor {
|
||||
context,
|
||||
context_label,
|
||||
action,
|
||||
description,
|
||||
required_feature: Some(required_feature),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum KeymapActionFeature {
|
||||
FastMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct KeymapActionFilter {
|
||||
pub(crate) fast_mode_enabled: bool,
|
||||
}
|
||||
|
||||
impl KeymapActionDescriptor {
|
||||
pub(super) fn is_visible(self, filter: KeymapActionFilter) -> bool {
|
||||
match self.required_feature {
|
||||
None => true,
|
||||
Some(KeymapActionFeature::FastMode) => filter.fast_mode_enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +91,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
|
||||
action("global", "Global", "copy", "Copy the last agent response to the clipboard."),
|
||||
action("global", "Global", "clear_terminal", "Clear the terminal UI."),
|
||||
action("global", "Global", "toggle_vim_mode", "Turn Vim composer mode on or off."),
|
||||
gated_action("global", "Global", "toggle_fast_mode", "Turn Fast mode on or off.", KeymapActionFeature::FastMode),
|
||||
action("chat", "Chat", "decrease_reasoning_effort", "Decrease reasoning effort."),
|
||||
action("chat", "Chat", "increase_reasoning_effort", "Increase reasoning effort."),
|
||||
action("chat", "Chat", "edit_queued_message", "Edit the most recently queued message."),
|
||||
@@ -75,6 +114,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
|
||||
action("editor", "Editor", "delete_backward_word", "Delete the previous word."),
|
||||
action("editor", "Editor", "delete_forward_word", "Delete the next word."),
|
||||
action("editor", "Editor", "kill_line_start", "Delete from cursor to line start."),
|
||||
action("editor", "Editor", "kill_whole_line", "Delete the current line."),
|
||||
action("editor", "Editor", "kill_line_end", "Delete from cursor to line end."),
|
||||
action("editor", "Editor", "yank", "Paste the kill buffer."),
|
||||
action("vim_normal", "Vim normal", "enter_insert", "Enter insert mode at the cursor."),
|
||||
@@ -172,6 +212,7 @@ pub(super) fn binding_slot<'a>(
|
||||
("global", "copy") => Some(&mut keymap.global.copy),
|
||||
("global", "clear_terminal") => Some(&mut keymap.global.clear_terminal),
|
||||
("global", "toggle_vim_mode") => Some(&mut keymap.global.toggle_vim_mode),
|
||||
("global", "toggle_fast_mode") => Some(&mut keymap.global.toggle_fast_mode),
|
||||
("chat", "decrease_reasoning_effort") => Some(&mut keymap.chat.decrease_reasoning_effort),
|
||||
("chat", "increase_reasoning_effort") => Some(&mut keymap.chat.increase_reasoning_effort),
|
||||
("chat", "edit_queued_message") => Some(&mut keymap.chat.edit_queued_message),
|
||||
@@ -194,6 +235,7 @@ pub(super) fn binding_slot<'a>(
|
||||
("editor", "delete_backward_word") => Some(&mut keymap.editor.delete_backward_word),
|
||||
("editor", "delete_forward_word") => Some(&mut keymap.editor.delete_forward_word),
|
||||
("editor", "kill_line_start") => Some(&mut keymap.editor.kill_line_start),
|
||||
("editor", "kill_whole_line") => Some(&mut keymap.editor.kill_whole_line),
|
||||
("editor", "kill_line_end") => Some(&mut keymap.editor.kill_line_end),
|
||||
("editor", "yank") => Some(&mut keymap.editor.yank),
|
||||
("vim_normal", "enter_insert") => Some(&mut keymap.vim_normal.enter_insert),
|
||||
@@ -273,6 +315,7 @@ pub(super) fn bindings_for_action<'a>(
|
||||
("global", "copy") => Some(runtime_keymap.app.copy.as_slice()),
|
||||
("global", "clear_terminal") => Some(runtime_keymap.app.clear_terminal.as_slice()),
|
||||
("global", "toggle_vim_mode") => Some(runtime_keymap.app.toggle_vim_mode.as_slice()),
|
||||
("global", "toggle_fast_mode") => Some(runtime_keymap.app.toggle_fast_mode.as_slice()),
|
||||
("chat", "decrease_reasoning_effort") => Some(runtime_keymap.chat.decrease_reasoning_effort.as_slice()),
|
||||
("chat", "increase_reasoning_effort") => Some(runtime_keymap.chat.increase_reasoning_effort.as_slice()),
|
||||
("chat", "edit_queued_message") => Some(runtime_keymap.chat.edit_queued_message.as_slice()),
|
||||
@@ -295,6 +338,7 @@ pub(super) fn bindings_for_action<'a>(
|
||||
("editor", "delete_backward_word") => Some(runtime_keymap.editor.delete_backward_word.as_slice()),
|
||||
("editor", "delete_forward_word") => Some(runtime_keymap.editor.delete_forward_word.as_slice()),
|
||||
("editor", "kill_line_start") => Some(runtime_keymap.editor.kill_line_start.as_slice()),
|
||||
("editor", "kill_whole_line") => Some(runtime_keymap.editor.kill_whole_line.as_slice()),
|
||||
("editor", "kill_line_end") => Some(runtime_keymap.editor.kill_line_end.as_slice()),
|
||||
("editor", "yank") => Some(runtime_keymap.editor.yank.as_slice()),
|
||||
("vim_normal", "enter_insert") => Some(runtime_keymap.vim_normal.enter_insert.as_slice()),
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
|
||||
use super::actions::KEYMAP_ACTIONS;
|
||||
use super::actions::KeymapActionFilter;
|
||||
use super::actions::action_label;
|
||||
use super::actions::bindings_for_action;
|
||||
use super::actions::format_binding_summary;
|
||||
@@ -59,6 +60,7 @@ const KEYMAP_COMMON_ACTIONS: &[(&str, &str)] = &[
|
||||
("composer", "submit"),
|
||||
("editor", "insert_newline"),
|
||||
("composer", "queue"),
|
||||
("global", "toggle_fast_mode"),
|
||||
("global", "open_external_editor"),
|
||||
("global", "copy"),
|
||||
("global", "toggle_vim_mode"),
|
||||
@@ -116,32 +118,69 @@ const KEYMAP_CONTEXT_TABS: &[KeymapContextTab] = &[
|
||||
},
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn build_keymap_picker_params(
|
||||
runtime_keymap: &RuntimeKeymap,
|
||||
keymap_config: &TuiKeymap,
|
||||
) -> SelectionViewParams {
|
||||
build_keymap_picker_params_with_filter(
|
||||
runtime_keymap,
|
||||
keymap_config,
|
||||
KeymapActionFilter::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_keymap_picker_params_with_filter(
|
||||
runtime_keymap: &RuntimeKeymap,
|
||||
keymap_config: &TuiKeymap,
|
||||
action_filter: KeymapActionFilter,
|
||||
) -> SelectionViewParams {
|
||||
build_keymap_picker_params_for_action(
|
||||
runtime_keymap,
|
||||
keymap_config,
|
||||
action_filter,
|
||||
/*selected_action*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn build_keymap_picker_params_for_selected_action(
|
||||
runtime_keymap: &RuntimeKeymap,
|
||||
keymap_config: &TuiKeymap,
|
||||
context: &str,
|
||||
action: &str,
|
||||
) -> SelectionViewParams {
|
||||
build_keymap_picker_params_for_action(runtime_keymap, keymap_config, Some((context, action)))
|
||||
build_keymap_picker_params_for_selected_action_with_filter(
|
||||
runtime_keymap,
|
||||
keymap_config,
|
||||
KeymapActionFilter::default(),
|
||||
context,
|
||||
action,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_keymap_picker_params_for_selected_action_with_filter(
|
||||
runtime_keymap: &RuntimeKeymap,
|
||||
keymap_config: &TuiKeymap,
|
||||
action_filter: KeymapActionFilter,
|
||||
context: &str,
|
||||
action: &str,
|
||||
) -> SelectionViewParams {
|
||||
build_keymap_picker_params_for_action(
|
||||
runtime_keymap,
|
||||
keymap_config,
|
||||
action_filter,
|
||||
Some((context, action)),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_keymap_picker_params_for_action(
|
||||
runtime_keymap: &RuntimeKeymap,
|
||||
keymap_config: &TuiKeymap,
|
||||
action_filter: KeymapActionFilter,
|
||||
selected_action: Option<(&str, &str)>,
|
||||
) -> SelectionViewParams {
|
||||
let rows = build_keymap_rows(runtime_keymap, keymap_config);
|
||||
let rows = build_keymap_rows(runtime_keymap, keymap_config, action_filter);
|
||||
let total = rows.len();
|
||||
let custom_count = rows.iter().filter(|row| row.custom_binding).count();
|
||||
let unbound_count = rows.iter().filter(|row| row.is_unbound()).count();
|
||||
@@ -287,9 +326,12 @@ fn keymap_debug_tab() -> SelectionTab {
|
||||
fn build_keymap_rows(
|
||||
runtime_keymap: &RuntimeKeymap,
|
||||
keymap_config: &TuiKeymap,
|
||||
action_filter: KeymapActionFilter,
|
||||
) -> Vec<KeymapActionRow> {
|
||||
KEYMAP_ACTIONS
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|descriptor| descriptor.is_visible(action_filter))
|
||||
.map(|descriptor| {
|
||||
let bindings =
|
||||
bindings_for_action(runtime_keymap, descriptor.context, descriptor.action)
|
||||
|
||||
+2
-2
@@ -5,9 +5,9 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
85 actions, 1 customized, 1 unbound.
|
||||
86 actions, 1 customized, 2 unbound.
|
||||
|
||||
[All] Common Customized (1) Unbound (1) App Composer Editor Vim Navigation Approval Debug
|
||||
[All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
Type to search shortcuts
|
||||
› Global Open Transcript ctrl-t
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: tui/src/keymap_setup.rs
|
||||
expression: "render_picker(params, 120)"
|
||||
---
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
87 actions, 0 customized, 3 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
Type to search shortcuts
|
||||
› Global Open Transcript ctrl-t
|
||||
Global Open External Editor ctrl-g
|
||||
Global Copy ctrl-o
|
||||
Global Clear Terminal ctrl-l
|
||||
Global - Toggle Vim Mode unbound
|
||||
Global - Toggle Fast Mode unbound
|
||||
Chat Decrease Reasoning Effort alt-,
|
||||
Chat Increase Reasoning Effort alt-.
|
||||
|
||||
left/right group · enter edit shortcut · * custom · - unbound · esc close
|
||||
+3
-3
@@ -2,13 +2,13 @@
|
||||
source: tui/src/keymap_setup.rs
|
||||
expression: snapshot
|
||||
---
|
||||
tab: All (85 selectable)
|
||||
tab: All (86 selectable)
|
||||
tab: Common (19 selectable)
|
||||
tab: Customized (0) (0 selectable)
|
||||
tab: Unbound (1) (1 selectable)
|
||||
tab: Unbound (2) (2 selectable)
|
||||
tab: App (8 selectable)
|
||||
tab: Composer (5 selectable)
|
||||
tab: Editor (16 selectable)
|
||||
tab: Editor (17 selectable)
|
||||
tab: Vim (34 selectable)
|
||||
tab: Navigation (14 selectable)
|
||||
tab: Approval (8 selectable)
|
||||
|
||||
+2
-2
@@ -5,9 +5,9 @@ expression: "render_picker(params, 78)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
85 actions, 0 customized, 1 unbound.
|
||||
86 actions, 0 customized, 2 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (1) App Composer Editor Vim
|
||||
[All] Common Customized (0) Unbound (2) App Composer Editor Vim
|
||||
Navigation Approval Debug
|
||||
|
||||
Type to search shortcuts
|
||||
|
||||
@@ -5,9 +5,9 @@ expression: "render_picker(params, 120)"
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
85 actions, 0 customized, 1 unbound.
|
||||
86 actions, 0 customized, 2 unbound.
|
||||
|
||||
[All] Common Customized (0) Unbound (1) App Composer Editor Vim Navigation Approval Debug
|
||||
[All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug
|
||||
|
||||
Type to search shortcuts
|
||||
› Global Open Transcript ctrl-t
|
||||
|
||||
Reference in New Issue
Block a user