use crate::ui::input::actions::{Action, YankTarget};
use chrono::Local;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::collections::HashMap;
use std::time::{Duration, Instant};
use tracing::debug;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ChordSequence {
keys: Vec<KeyEvent>,
}
impl ChordSequence {
#[must_use]
pub fn new(keys: Vec<KeyEvent>) -> Self {
Self { keys }
}
#[must_use]
pub fn from_notation(notation: &str) -> Option<Self> {
let chars: Vec<char> = notation.chars().collect();
if chars.is_empty() {
return None;
}
let keys: Vec<KeyEvent> = chars
.iter()
.map(|&c| KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty()))
.collect();
Some(Self { keys })
}
pub fn to_string(&self) -> String {
self.keys.iter().map(format_key).collect::<String>()
}
}
#[derive(Debug, Clone)]
pub enum ChordResult {
SingleKey(KeyEvent),
PartialChord(String), CompleteChord(Action),
Cancelled,
}
pub struct KeyChordHandler {
chord_map: HashMap<ChordSequence, Action>,
current_chord: Vec<KeyEvent>,
chord_start: Option<Instant>,
chord_timeout: Duration,
key_history: Vec<String>,
max_history: usize,
chord_mode_active: bool,
chord_mode_description: Option<String>,
}
impl Default for KeyChordHandler {
fn default() -> Self {
Self::new()
}
}
impl KeyChordHandler {
#[must_use]
pub fn new() -> Self {
let mut handler = Self {
chord_map: HashMap::new(),
current_chord: Vec::new(),
chord_start: None,
chord_timeout: Duration::from_millis(1000), key_history: Vec::new(),
max_history: 50,
chord_mode_active: false,
chord_mode_description: None,
};
handler.setup_default_chords();
handler
}
fn setup_default_chords(&mut self) {
use crate::buffer::AppMode;
use crate::ui::input::actions::{CursorPosition, SqlClause};
self.register_chord_action("yy", Action::Yank(YankTarget::Row));
self.register_chord_action("yr", Action::Yank(YankTarget::Row)); self.register_chord_action("yc", Action::Yank(YankTarget::Column));
self.register_chord_action("ya", Action::Yank(YankTarget::All));
self.register_chord_action("yv", Action::Yank(YankTarget::Cell)); self.register_chord_action("yq", Action::Yank(YankTarget::Query));
self.register_chord_action(
"cw",
Action::SwitchModeWithCursor(
AppMode::Command,
CursorPosition::AfterClause(SqlClause::Where),
),
);
self.register_chord_action(
"cs",
Action::SwitchModeWithCursor(
AppMode::Command,
CursorPosition::AfterClause(SqlClause::Select),
),
);
self.register_chord_action(
"cf",
Action::SwitchModeWithCursor(
AppMode::Command,
CursorPosition::AfterClause(SqlClause::From),
),
);
self.register_chord_action(
"co",
Action::SwitchModeWithCursor(
AppMode::Command,
CursorPosition::AfterClause(SqlClause::OrderBy),
),
);
self.register_chord_action(
"cg",
Action::SwitchModeWithCursor(
AppMode::Command,
CursorPosition::AfterClause(SqlClause::GroupBy),
),
);
self.register_chord_action(
"ch",
Action::SwitchModeWithCursor(
AppMode::Command,
CursorPosition::AfterClause(SqlClause::Having),
),
);
self.register_chord_action(
"cl",
Action::SwitchModeWithCursor(
AppMode::Command,
CursorPosition::AfterClause(SqlClause::Limit),
),
);
}
pub fn register_chord_action(&mut self, notation: &str, action: Action) {
if let Some(chord) = ChordSequence::from_notation(notation) {
self.chord_map.insert(chord, action);
}
}
pub fn process_key(&mut self, key: KeyEvent) -> ChordResult {
self.log_key_press(&key);
if let Some(start) = self.chord_start {
if start.elapsed() > self.chord_timeout {
self.cancel_chord();
return self.process_key_internal(key);
}
}
if key.code == KeyCode::Esc && !self.current_chord.is_empty() {
self.cancel_chord();
return ChordResult::Cancelled;
}
self.process_key_internal(key)
}
fn process_key_internal(&mut self, key: KeyEvent) -> ChordResult {
debug!(
"process_key_internal: key={:?}, current_chord={:?}",
key, self.current_chord
);
self.current_chord.push(key);
if self.current_chord.len() == 1 {
self.chord_start = Some(Instant::now());
}
let current = ChordSequence::new(self.current_chord.clone());
debug!("Checking for exact match with chord: {:?}", current);
debug!(
"Registered chords: {:?}",
self.chord_map.keys().collect::<Vec<_>>()
);
if let Some(action) = self.chord_map.get(¤t) {
debug!("Found exact match! Action: {:?}", action);
let result = ChordResult::CompleteChord(action.clone());
self.reset_chord();
return result;
}
debug!("Checking for partial matches...");
let has_partial = self.chord_map.keys().any(|chord| {
chord.keys.len() > self.current_chord.len()
&& chord.keys[..self.current_chord.len()] == self.current_chord[..]
});
debug!("has_partial = {}", has_partial);
if has_partial {
let possible: Vec<String> = self
.chord_map
.iter()
.filter_map(|(chord, action)| {
if chord.keys.len() > self.current_chord.len()
&& chord.keys[..self.current_chord.len()] == self.current_chord[..]
{
let action_name = match action {
Action::Yank(YankTarget::Row) => "yank row",
Action::Yank(YankTarget::Column) => "yank column",
Action::Yank(YankTarget::All) => "yank all",
Action::Yank(YankTarget::Cell) => "yank cell",
Action::Yank(YankTarget::Query) => "yank query",
_ => "unknown",
};
Some(format!(
"{} → {}",
format_key(&chord.keys[self.current_chord.len()]),
action_name
))
} else {
None
}
})
.collect();
let description = if self.current_chord.len() == 1
&& self.current_chord[0].code == KeyCode::Char('y')
{
"Yank mode: y=row, c=column, a=all, ESC=cancel".to_string()
} else {
format!("Waiting for: {}", possible.join(", "))
};
self.chord_mode_active = true;
self.chord_mode_description = Some(description.clone());
ChordResult::PartialChord(description)
} else {
let result = if self.current_chord.len() == 1 {
ChordResult::SingleKey(key)
} else {
ChordResult::SingleKey(self.current_chord[0])
};
self.reset_chord();
result
}
}
pub fn cancel_chord(&mut self) {
self.reset_chord();
}
fn reset_chord(&mut self) {
self.current_chord.clear();
self.chord_start = None;
self.chord_mode_active = false;
self.chord_mode_description = None;
}
pub fn log_key_press(&mut self, key: &KeyEvent) {
if self.key_history.len() >= self.max_history {
self.key_history.remove(0);
}
let timestamp = Local::now().format("%H:%M:%S.%3f");
let key_str = format_key(key);
let modifiers = format_modifiers(key.modifiers);
let entry = if modifiers.is_empty() {
format!("[{timestamp}] {key_str}")
} else {
format!("[{timestamp}] {key_str} ({modifiers})")
};
self.key_history.push(entry);
}
#[must_use]
pub fn get_history(&self) -> &[String] {
&self.key_history
}
pub fn clear_history(&mut self) {
self.key_history.clear();
}
#[must_use]
pub fn is_chord_mode_active(&self) -> bool {
self.chord_mode_active
}
#[must_use]
pub fn get_chord_mode_description(&self) -> Option<&str> {
self.chord_mode_description.as_deref()
}
pub fn set_timeout(&mut self, millis: u64) {
self.chord_timeout = Duration::from_millis(millis);
}
pub fn format_debug_info(&self) -> String {
let mut output = String::new();
output.push_str("========== CHORD STATE ==========\n");
if self.current_chord.is_empty() {
output.push_str("No active chord\n");
} else {
output.push_str(&format!(
"Current chord: {}\n",
self.current_chord
.iter()
.map(format_key)
.collect::<Vec<_>>()
.join(" → ")
));
if let Some(desc) = &self.chord_mode_description {
output.push_str(&format!("Mode: {desc}\n"));
}
if let Some(start) = self.chord_start {
let elapsed = start.elapsed().as_millis();
let remaining = self.chord_timeout.as_millis().saturating_sub(elapsed);
output.push_str(&format!("Timeout in: {remaining}ms\n"));
}
}
output.push_str("\n========== REGISTERED CHORDS ==========\n");
let mut chords: Vec<_> = self.chord_map.iter().collect();
chords.sort_by_key(|(chord, _)| chord.to_string());
for (chord, action) in chords {
let action_name = match action {
Action::Yank(YankTarget::Row) => "yank_row",
Action::Yank(YankTarget::Column) => "yank_column",
Action::Yank(YankTarget::All) => "yank_all",
Action::Yank(YankTarget::Cell) => "yank_cell",
Action::Yank(YankTarget::Query) => "yank_query",
_ => "unknown",
};
output.push_str(&format!("{} → {}\n", chord.to_string(), action_name));
}
output.push_str("\n========== KEY PRESS HISTORY ==========\n");
output.push_str("(Most recent at bottom, last 50 keys)\n");
for entry in &self.key_history {
output.push_str(entry);
output.push('\n');
}
output
}
pub fn load_from_config(&mut self, _config: &HashMap<String, String>) {
}
}
fn format_key(key: &KeyEvent) -> String {
let mut result = String::new();
if key.modifiers.contains(KeyModifiers::CONTROL) {
result.push_str("Ctrl+");
}
if key.modifiers.contains(KeyModifiers::ALT) {
result.push_str("Alt+");
}
if key.modifiers.contains(KeyModifiers::SHIFT) {
result.push_str("Shift+");
}
match key.code {
KeyCode::Char(c) => result.push(c),
KeyCode::Enter => result.push_str("Enter"),
KeyCode::Esc => result.push_str("Esc"),
KeyCode::Backspace => result.push_str("Backspace"),
KeyCode::Tab => result.push_str("Tab"),
KeyCode::Delete => result.push_str("Del"),
KeyCode::Insert => result.push_str("Ins"),
KeyCode::F(n) => result.push_str(&format!("F{n}")),
KeyCode::Left => result.push('←'),
KeyCode::Right => result.push('→'),
KeyCode::Up => result.push('↑'),
KeyCode::Down => result.push('↓'),
KeyCode::Home => result.push_str("Home"),
KeyCode::End => result.push_str("End"),
KeyCode::PageUp => result.push_str("PgUp"),
KeyCode::PageDown => result.push_str("PgDn"),
_ => result.push('?'),
}
result
}
fn format_modifiers(mods: KeyModifiers) -> String {
let mut parts = Vec::new();
if mods.contains(KeyModifiers::CONTROL) {
parts.push("Ctrl");
}
if mods.contains(KeyModifiers::ALT) {
parts.push("Alt");
}
if mods.contains(KeyModifiers::SHIFT) {
parts.push("Shift");
}
parts.join("+")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chord_sequence() {
let chord = ChordSequence::from_notation("yy").unwrap();
assert_eq!(chord.keys.len(), 2);
assert_eq!(chord.to_string(), "yy");
}
#[test]
fn test_single_key() {
let mut handler = KeyChordHandler::new();
let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty());
match handler.process_key(key) {
ChordResult::SingleKey(_) => {}
_ => panic!("Expected single key"),
}
}
#[test]
fn test_chord_completion() {
let mut handler = KeyChordHandler::new();
let key1 = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty());
match handler.process_key(key1) {
ChordResult::PartialChord(_) => {}
_ => panic!("Expected partial chord"),
}
let key2 = KeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty());
match handler.process_key(key2) {
ChordResult::CompleteChord(action) => {
assert_eq!(action, Action::Yank(YankTarget::Row));
}
_ => panic!("Expected complete chord"),
}
}
}