[feat] make approval key matching case insensitive (#1862)
This commit is contained in:
@@ -47,6 +47,8 @@ pub(crate) enum ApprovalRequest {
|
||||
}
|
||||
|
||||
/// Options displayed in the *select* mode.
|
||||
///
|
||||
/// The `key` is matched case-insensitively.
|
||||
struct SelectOption {
|
||||
label: Line<'static>,
|
||||
description: &'static str,
|
||||
@@ -187,6 +189,16 @@ impl UserApprovalWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize a key for comparison.
|
||||
/// - For `KeyCode::Char`, converts to lowercase for case-insensitive matching.
|
||||
/// - Other key codes are returned unchanged.
|
||||
fn normalize_keycode(code: KeyCode) -> KeyCode {
|
||||
match code {
|
||||
KeyCode::Char(c) => KeyCode::Char(c.to_ascii_lowercase()),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle Ctrl-C pressed by the user while the modal is visible.
|
||||
/// Behaves like pressing Escape: abort the request and close the modal.
|
||||
pub(crate) fn on_ctrl_c(&mut self) {
|
||||
@@ -210,7 +222,12 @@ impl UserApprovalWidget<'_> {
|
||||
self.send_decision(ReviewDecision::Abort);
|
||||
}
|
||||
other => {
|
||||
if let Some(opt) = self.select_options.iter().find(|opt| opt.key == other) {
|
||||
let normalized = Self::normalize_keycode(other);
|
||||
if let Some(opt) = self
|
||||
.select_options
|
||||
.iter()
|
||||
.find(|opt| Self::normalize_keycode(opt.key) == normalized)
|
||||
{
|
||||
self.send_decision(opt.decision);
|
||||
}
|
||||
}
|
||||
@@ -330,3 +347,59 @@ impl WidgetRef for &UserApprovalWidget<'_> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
#[test]
|
||||
fn lowercase_shortcut_is_accepted() {
|
||||
let (tx_raw, rx) = channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let req = ApprovalRequest::Exec {
|
||||
id: "1".to_string(),
|
||||
command: vec!["echo".to_string()],
|
||||
cwd: PathBuf::new(),
|
||||
reason: None,
|
||||
};
|
||||
let mut widget = UserApprovalWidget::new(req, tx);
|
||||
widget.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
assert!(widget.is_complete());
|
||||
let events: Vec<AppEvent> = rx.try_iter().collect();
|
||||
assert!(events.iter().any(|e| matches!(
|
||||
e,
|
||||
AppEvent::CodexOp(Op::ExecApproval {
|
||||
decision: ReviewDecision::Approved,
|
||||
..
|
||||
})
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uppercase_shortcut_is_accepted() {
|
||||
let (tx_raw, rx) = channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let req = ApprovalRequest::Exec {
|
||||
id: "2".to_string(),
|
||||
command: vec!["echo".to_string()],
|
||||
cwd: PathBuf::new(),
|
||||
reason: None,
|
||||
};
|
||||
let mut widget = UserApprovalWidget::new(req, tx);
|
||||
widget.handle_key_event(KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::NONE));
|
||||
assert!(widget.is_complete());
|
||||
let events: Vec<AppEvent> = rx.try_iter().collect();
|
||||
assert!(events.iter().any(|e| matches!(
|
||||
e,
|
||||
AppEvent::CodexOp(Op::ExecApproval {
|
||||
decision: ReviewDecision::Approved,
|
||||
..
|
||||
})
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user