1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
use crate::app::{AppState, Action};
use crate::errors::ComponentError;
use crate::input::InputEvent;
use std::time::{Duration, Instant};
/// Handles keyboard shortcuts by looking up commands in the registry.
/// Supports both single-character and multi-character shortcuts.
pub struct ShortcutHandler {
/// Buffer for multi-character shortcuts (e.g., "lb", "cp")
buffer: String,
/// Timestamp of last keypress for timeout
last_keypress: Option<Instant>,
/// Timeout for multi-character shortcuts (500ms)
timeout: Duration,
}
impl ShortcutHandler {
pub fn new() -> Self {
Self {
buffer: String::new(),
last_keypress: None,
timeout: Duration::from_millis(500),
}
}
/// Handle a keypress and return an action if a shortcut matches.
/// Returns None if no shortcut matches or if more characters are needed.
pub fn handle_keypress(
&mut self,
event: InputEvent,
state: &AppState,
) -> Result<Option<Action>, ComponentError> {
if let InputEvent::Key(key) = &event {
if key.kind != crossterm::event::KeyEventKind::Press {
return Ok(None);
}
// Check for control/modifier keys first
if key.modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
// Handle Ctrl+key combinations
if let crossterm::event::KeyCode::Char('c') = key.code {
return Ok(Some(Action::Quit));
}
return Ok(None);
}
// Handle character keys
if let crossterm::event::KeyCode::Char(c) = key.code {
// Special case: 't' opens theme picker (not a command shortcut)
if c == 't' && !state.command_palette {
self.buffer.clear();
return Ok(Some(Action::ShowThemePicker));
}
// Special case: 'c' in commit mode should not trigger shortcuts
if c == 'c' && state.commit_mode {
self.buffer.clear(); // Clear buffer when in commit mode
return Ok(None);
}
let now = Instant::now();
// Check if buffer has timed out
if let Some(last) = self.last_keypress {
if now.duration_since(last) > self.timeout {
self.buffer.clear();
}
}
// Add character to buffer
self.buffer.push(c);
self.last_keypress = Some(now);
// Try to find a command with this shortcut
let registry = crate::palette::registry::CommandRegistry::instance();
// Try exact match first
if let Some(cmd) = registry.by_key(&self.buffer, state) {
// Try to create action - some commands require additional input
if let Some(action) = cmd.create_action(state) {
// Command can execute immediately
self.buffer.clear();
return Ok(Some(action));
}
// Command exists but create_action returned None (needs input or disabled)
// Clear buffer and don't execute - let user use palette instead
self.buffer.clear();
return Ok(None);
}
// Check if any command starts with this buffer (multi-character shortcut)
let enabled = registry.enabled_commands(state);
let has_prefix_match = enabled.iter().any(|cmd| cmd.key.starts_with(&self.buffer));
if !has_prefix_match {
// No command starts with this buffer, clear it
self.buffer.clear();
}
// Otherwise, keep buffer and wait for more characters
} else {
// Non-character key pressed - clear buffer to avoid conflicts
self.buffer.clear();
}
}
Ok(None)
}
/// Clear the shortcut buffer (called when other input is received)
#[allow(dead_code)]
pub fn clear_buffer(&mut self) {
self.buffer.clear();
self.last_keypress = None;
}
}
impl Default for ShortcutHandler {
fn default() -> Self {
Self::new()
}
}