802 lines
31 KiB
Rust
802 lines
31 KiB
Rust
// src/config/binds/config.rs
|
|
|
|
use serde::{Deserialize, Serialize}; // Added Serialize for EditorKeybindingMode if needed elsewhere
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use anyhow::{Context, Result};
|
|
use crossterm::event::{KeyCode, KeyModifiers};
|
|
use canvas::CanvasKeyMap;
|
|
|
|
// NEW: Editor Keybinding Mode Enum
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub enum EditorKeybindingMode {
|
|
#[serde(rename = "default")]
|
|
Default,
|
|
#[serde(rename = "vim")]
|
|
Vim,
|
|
#[serde(rename = "emacs")]
|
|
Emacs,
|
|
}
|
|
|
|
impl Default for EditorKeybindingMode {
|
|
fn default() -> Self {
|
|
EditorKeybindingMode::Default
|
|
}
|
|
}
|
|
|
|
// NEW: Editor Configuration Struct
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EditorConfig {
|
|
#[serde(default)]
|
|
pub keybinding_mode: EditorKeybindingMode,
|
|
#[serde(default = "default_show_line_numbers")]
|
|
pub show_line_numbers: bool,
|
|
#[serde(default = "default_tab_width")]
|
|
pub tab_width: u8,
|
|
}
|
|
|
|
fn default_show_line_numbers() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_tab_width() -> u8 {
|
|
4
|
|
}
|
|
|
|
impl Default for EditorConfig {
|
|
fn default() -> Self {
|
|
EditorConfig {
|
|
keybinding_mode: EditorKeybindingMode::default(),
|
|
show_line_numbers: default_show_line_numbers(),
|
|
tab_width: default_tab_width(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Default)]
|
|
pub struct ColorsConfig {
|
|
#[serde(default = "default_theme")]
|
|
pub theme: String,
|
|
}
|
|
|
|
fn default_theme() -> String {
|
|
"light".to_string()
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct Config {
|
|
#[serde(rename = "keybindings")]
|
|
pub keybindings: ModeKeybindings,
|
|
#[serde(default)]
|
|
pub colors: ColorsConfig,
|
|
// NEW: Add editor configuration
|
|
#[serde(default)]
|
|
pub editor: EditorConfig,
|
|
}
|
|
|
|
// ... (rest of your Config struct and impl Config remains the same)
|
|
// Make sure ModeKeybindings is also deserializable if it's not already
|
|
#[derive(Debug, Deserialize, Default)] // Added Default here if not present
|
|
pub struct ModeKeybindings {
|
|
#[serde(default)]
|
|
pub general: HashMap<String, Vec<String>>,
|
|
#[serde(default)]
|
|
pub read_only: HashMap<String, Vec<String>>,
|
|
#[serde(default)]
|
|
pub edit: HashMap<String, Vec<String>>,
|
|
#[serde(default)]
|
|
pub highlight: HashMap<String, Vec<String>>,
|
|
#[serde(default)]
|
|
pub command: HashMap<String, Vec<String>>,
|
|
#[serde(default)]
|
|
pub common: HashMap<String, Vec<String>>,
|
|
#[serde(flatten)]
|
|
pub global: HashMap<String, Vec<String>>,
|
|
}
|
|
|
|
impl Config {
|
|
/// Loads the configuration from "config.toml" in the client crate directory.
|
|
pub fn load() -> Result<Self> {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let config_path = Path::new(manifest_dir).join("config.toml");
|
|
let config_str = std::fs::read_to_string(&config_path)
|
|
.with_context(|| format!("Failed to read config file at {:?}", config_path))?;
|
|
let config: Config = toml::from_str(&config_str)
|
|
.with_context(|| format!("Failed to parse config file: {}. Check for syntax errors or missing fields like an empty [editor] section if you added it.", config_str))?; // Enhanced error message
|
|
Ok(config)
|
|
}
|
|
|
|
pub fn get_general_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
|
self.get_action_for_key_in_mode(&self.keybindings.general, key, modifiers)
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
|
}
|
|
|
|
/// Common actions for Edit/Read-only modes
|
|
pub fn get_common_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
|
self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers)
|
|
}
|
|
|
|
/// Gets an action for a key in Read-Only mode, also checking common keybindings.
|
|
pub fn get_read_only_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
|
self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers)
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers))
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
|
}
|
|
|
|
/// Gets an action for a key in Edit mode, also checking common keybindings.
|
|
pub fn get_edit_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
|
self.get_action_for_key_in_mode(&self.keybindings.edit, key, modifiers)
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers))
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
|
}
|
|
|
|
/// Gets an action for a key in Highlight mode, also checking common/global keybindings.
|
|
pub fn get_highlight_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
|
self.get_action_for_key_in_mode(&self.keybindings.highlight, key, modifiers)
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers))
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers))
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
|
}
|
|
|
|
/// Gets an action for a key in Command mode, also checking common keybindings.
|
|
pub fn get_command_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
|
self.get_action_for_key_in_mode(&self.keybindings.command, key, modifiers)
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers))
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
|
}
|
|
|
|
/// Context-aware keybinding resolution
|
|
pub fn get_action_for_current_context(
|
|
&self,
|
|
command_mode: bool,
|
|
key: KeyCode,
|
|
modifiers: KeyModifiers
|
|
) -> Option<&str> {
|
|
if command_mode {
|
|
self.get_command_action_for_key(key, modifiers)
|
|
} else {
|
|
// fallback: read-only + common + global
|
|
self.get_read_only_action_for_key(key, modifiers)
|
|
.or_else(|| self.get_common_action(key, modifiers))
|
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
|
}
|
|
}
|
|
|
|
/// Helper function to get an action for a key in a specific mode.
|
|
pub fn get_action_for_key_in_mode<'a>(
|
|
&self,
|
|
mode_bindings: &'a HashMap<String, Vec<String>>,
|
|
key: KeyCode,
|
|
modifiers: KeyModifiers,
|
|
) -> Option<&'a str> {
|
|
for (action, bindings) in mode_bindings {
|
|
for binding in bindings {
|
|
if Self::matches_keybinding(binding, key, modifiers) {
|
|
return Some(action.as_str());
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Checks if a sequence of keys matches any keybinding.
|
|
pub fn matches_key_sequence(&self, sequence: &[KeyCode]) -> Option<&str> {
|
|
if sequence.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Convert key sequence to a string (for simple character sequences).
|
|
let sequence_str: String = sequence.iter().filter_map(|key| {
|
|
if let KeyCode::Char(c) = key {
|
|
Some(*c)
|
|
} else {
|
|
None
|
|
}
|
|
}).collect();
|
|
|
|
if sequence_str.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Check if this sequence matches any binding in the mode-specific sections.
|
|
for (action, bindings) in &self.keybindings.read_only {
|
|
for binding in bindings {
|
|
if binding == &sequence_str {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (action, bindings) in &self.keybindings.edit {
|
|
for binding in bindings {
|
|
if binding == &sequence_str {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (action, bindings) in &self.keybindings.command {
|
|
for binding in bindings {
|
|
if binding == &sequence_str {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check common keybindings
|
|
for (action, bindings) in &self.keybindings.common {
|
|
for binding in bindings {
|
|
if binding == &sequence_str {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally check global bindings
|
|
for (action, bindings) in &self.keybindings.global {
|
|
for binding in bindings {
|
|
if binding == &sequence_str {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Checks if a keybinding matches a key and modifiers.
|
|
fn matches_keybinding(
|
|
binding: &str,
|
|
key: KeyCode,
|
|
modifiers: KeyModifiers,
|
|
) -> bool {
|
|
// Special handling for shift+character combinations
|
|
if binding.to_lowercase().starts_with("shift+") {
|
|
let parts: Vec<&str> = binding.split('+').collect();
|
|
if parts.len() == 2 && parts[1].len() == 1 {
|
|
let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap();
|
|
let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap();
|
|
if let KeyCode::Char(actual_char) = key {
|
|
if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle Shift+Tab -> BackTab
|
|
if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() {
|
|
return true;
|
|
}
|
|
|
|
// Handle multi-character bindings (all standard keys without modifiers)
|
|
if binding.len() > 1 && !binding.contains('+') {
|
|
return match binding.to_lowercase().as_str() {
|
|
// Navigation keys
|
|
"left" => key == KeyCode::Left,
|
|
"right" => key == KeyCode::Right,
|
|
"up" => key == KeyCode::Up,
|
|
"down" => key == KeyCode::Down,
|
|
"home" => key == KeyCode::Home,
|
|
"end" => key == KeyCode::End,
|
|
"pageup" | "pgup" => key == KeyCode::PageUp,
|
|
"pagedown" | "pgdn" => key == KeyCode::PageDown,
|
|
|
|
// Editing keys
|
|
"insert" | "ins" => key == KeyCode::Insert,
|
|
"delete" | "del" => key == KeyCode::Delete,
|
|
"backspace" => key == KeyCode::Backspace,
|
|
|
|
// Tab keys
|
|
"tab" => key == KeyCode::Tab,
|
|
"backtab" => key == KeyCode::BackTab,
|
|
|
|
// Special keys
|
|
"enter" | "return" => key == KeyCode::Enter,
|
|
"escape" | "esc" => key == KeyCode::Esc,
|
|
"space" => key == KeyCode::Char(' '),
|
|
|
|
// Function keys F1-F24
|
|
"f1" => key == KeyCode::F(1),
|
|
"f2" => key == KeyCode::F(2),
|
|
"f3" => key == KeyCode::F(3),
|
|
"f4" => key == KeyCode::F(4),
|
|
"f5" => key == KeyCode::F(5),
|
|
"f6" => key == KeyCode::F(6),
|
|
"f7" => key == KeyCode::F(7),
|
|
"f8" => key == KeyCode::F(8),
|
|
"f9" => key == KeyCode::F(9),
|
|
"f10" => key == KeyCode::F(10),
|
|
"f11" => key == KeyCode::F(11),
|
|
"f12" => key == KeyCode::F(12),
|
|
"f13" => key == KeyCode::F(13),
|
|
"f14" => key == KeyCode::F(14),
|
|
"f15" => key == KeyCode::F(15),
|
|
"f16" => key == KeyCode::F(16),
|
|
"f17" => key == KeyCode::F(17),
|
|
"f18" => key == KeyCode::F(18),
|
|
"f19" => key == KeyCode::F(19),
|
|
"f20" => key == KeyCode::F(20),
|
|
"f21" => key == KeyCode::F(21),
|
|
"f22" => key == KeyCode::F(22),
|
|
"f23" => key == KeyCode::F(23),
|
|
"f24" => key == KeyCode::F(24),
|
|
|
|
// Lock keys
|
|
"capslock" => key == KeyCode::CapsLock,
|
|
"scrolllock" => key == KeyCode::ScrollLock,
|
|
"numlock" => key == KeyCode::NumLock,
|
|
|
|
// System keys
|
|
"printscreen" => key == KeyCode::PrintScreen,
|
|
"pause" => key == KeyCode::Pause,
|
|
"menu" => key == KeyCode::Menu,
|
|
"keypadbegin" => key == KeyCode::KeypadBegin,
|
|
|
|
// Media keys
|
|
"mediaplay" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Play),
|
|
"mediapause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Pause),
|
|
"mediaplaypause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::PlayPause),
|
|
"mediareverse" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Reverse),
|
|
"mediastop" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Stop),
|
|
"mediafastforward" => key == KeyCode::Media(crossterm::event::MediaKeyCode::FastForward),
|
|
"mediarewind" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Rewind),
|
|
"mediatracknext" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackNext),
|
|
"mediatrackprevious" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackPrevious),
|
|
"mediarecord" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Record),
|
|
"medialowervolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::LowerVolume),
|
|
"mediaraisevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::RaiseVolume),
|
|
"mediamutevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::MuteVolume),
|
|
|
|
// Multi-key sequences need special handling
|
|
"gg" => false, // This needs sequence handling
|
|
_ => {
|
|
// Handle single characters and punctuation
|
|
if binding.len() == 1 {
|
|
if let Some(c) = binding.chars().next() {
|
|
key == KeyCode::Char(c)
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Handle modifier combinations (like "Ctrl+F5", "Alt+Shift+A")
|
|
let parts: Vec<&str> = binding.split('+').collect();
|
|
let mut expected_modifiers = KeyModifiers::empty();
|
|
let mut expected_key = None;
|
|
|
|
for part in parts {
|
|
match part.to_lowercase().as_str() {
|
|
// Modifiers
|
|
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
|
|
"shift" => expected_modifiers |= KeyModifiers::SHIFT,
|
|
"alt" => expected_modifiers |= KeyModifiers::ALT,
|
|
"super" | "windows" | "cmd" => expected_modifiers |= KeyModifiers::SUPER,
|
|
"hyper" => expected_modifiers |= KeyModifiers::HYPER,
|
|
"meta" => expected_modifiers |= KeyModifiers::META,
|
|
|
|
// Navigation keys
|
|
"left" => expected_key = Some(KeyCode::Left),
|
|
"right" => expected_key = Some(KeyCode::Right),
|
|
"up" => expected_key = Some(KeyCode::Up),
|
|
"down" => expected_key = Some(KeyCode::Down),
|
|
"home" => expected_key = Some(KeyCode::Home),
|
|
"end" => expected_key = Some(KeyCode::End),
|
|
"pageup" | "pgup" => expected_key = Some(KeyCode::PageUp),
|
|
"pagedown" | "pgdn" => expected_key = Some(KeyCode::PageDown),
|
|
|
|
// Editing keys
|
|
"insert" | "ins" => expected_key = Some(KeyCode::Insert),
|
|
"delete" | "del" => expected_key = Some(KeyCode::Delete),
|
|
"backspace" => expected_key = Some(KeyCode::Backspace),
|
|
|
|
// Tab keys
|
|
"tab" => expected_key = Some(KeyCode::Tab),
|
|
"backtab" => expected_key = Some(KeyCode::BackTab),
|
|
|
|
// Special keys
|
|
"enter" | "return" => expected_key = Some(KeyCode::Enter),
|
|
"escape" | "esc" => expected_key = Some(KeyCode::Esc),
|
|
"space" => expected_key = Some(KeyCode::Char(' ')),
|
|
|
|
// Function keys
|
|
"f1" => expected_key = Some(KeyCode::F(1)),
|
|
"f2" => expected_key = Some(KeyCode::F(2)),
|
|
"f3" => expected_key = Some(KeyCode::F(3)),
|
|
"f4" => expected_key = Some(KeyCode::F(4)),
|
|
"f5" => expected_key = Some(KeyCode::F(5)),
|
|
"f6" => expected_key = Some(KeyCode::F(6)),
|
|
"f7" => expected_key = Some(KeyCode::F(7)),
|
|
"f8" => expected_key = Some(KeyCode::F(8)),
|
|
"f9" => expected_key = Some(KeyCode::F(9)),
|
|
"f10" => expected_key = Some(KeyCode::F(10)),
|
|
"f11" => expected_key = Some(KeyCode::F(11)),
|
|
"f12" => expected_key = Some(KeyCode::F(12)),
|
|
"f13" => expected_key = Some(KeyCode::F(13)),
|
|
"f14" => expected_key = Some(KeyCode::F(14)),
|
|
"f15" => expected_key = Some(KeyCode::F(15)),
|
|
"f16" => expected_key = Some(KeyCode::F(16)),
|
|
"f17" => expected_key = Some(KeyCode::F(17)),
|
|
"f18" => expected_key = Some(KeyCode::F(18)),
|
|
"f19" => expected_key = Some(KeyCode::F(19)),
|
|
"f20" => expected_key = Some(KeyCode::F(20)),
|
|
"f21" => expected_key = Some(KeyCode::F(21)),
|
|
"f22" => expected_key = Some(KeyCode::F(22)),
|
|
"f23" => expected_key = Some(KeyCode::F(23)),
|
|
"f24" => expected_key = Some(KeyCode::F(24)),
|
|
|
|
// Lock keys
|
|
"capslock" => expected_key = Some(KeyCode::CapsLock),
|
|
"scrolllock" => expected_key = Some(KeyCode::ScrollLock),
|
|
"numlock" => expected_key = Some(KeyCode::NumLock),
|
|
|
|
// System keys
|
|
"printscreen" => expected_key = Some(KeyCode::PrintScreen),
|
|
"pause" => expected_key = Some(KeyCode::Pause),
|
|
"menu" => expected_key = Some(KeyCode::Menu),
|
|
"keypadbegin" => expected_key = Some(KeyCode::KeypadBegin),
|
|
|
|
// Special characters and colon (legacy support)
|
|
":" => expected_key = Some(KeyCode::Char(':')),
|
|
|
|
// Single character (letters, numbers, punctuation)
|
|
part => {
|
|
if part.len() == 1 {
|
|
if let Some(c) = part.chars().next() {
|
|
expected_key = Some(KeyCode::Char(c));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
modifiers == expected_modifiers && Some(key) == expected_key
|
|
}
|
|
|
|
/// Gets an action for a command string.
|
|
pub fn get_action_for_command(&self, command: &str) -> Option<&str> {
|
|
// First check command mode bindings
|
|
for (action, bindings) in &self.keybindings.command {
|
|
for binding in bindings {
|
|
if binding == command {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then check common bindings
|
|
for (action, bindings) in &self.keybindings.common {
|
|
for binding in bindings {
|
|
if binding == command {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally check global bindings
|
|
for (action, bindings) in &self.keybindings.global {
|
|
for binding in bindings {
|
|
if binding == command {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Checks if a key is bound to entering Edit mode (before cursor).
|
|
pub fn is_enter_edit_mode_before(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
|
if let Some(bindings) = self.keybindings.read_only.get("enter_edit_mode_before") {
|
|
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Checks if a key is bound to entering Edit mode (after cursor).
|
|
pub fn is_enter_edit_mode_after(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
|
if let Some(bindings) = self.keybindings.read_only.get("enter_edit_mode_after") {
|
|
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Checks if a key is bound to entering Edit mode.
|
|
pub fn is_enter_edit_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
|
self.is_enter_edit_mode_before(key, modifiers) || self.is_enter_edit_mode_after(key, modifiers)
|
|
}
|
|
|
|
/// Checks if a key is bound to exiting Edit mode.
|
|
pub fn is_exit_edit_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
|
if let Some(bindings) = self.keybindings.edit.get("exit_edit_mode") {
|
|
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Checks if a key is bound to entering Command mode.
|
|
/// This method is no longer used in event.rs since we now handle command mode entry only in read-only mode directly.
|
|
pub fn is_enter_command_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
|
if let Some(bindings) = self.keybindings.command.get("enter_command_mode") {
|
|
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
|
|
/// Checks if a key is bound to exiting Command mode.
|
|
pub fn is_exit_command_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
|
if let Some(bindings) = self.keybindings.command.get("exit_command_mode") {
|
|
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Checks if a key is bound to executing a command.
|
|
pub fn is_command_execute(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
|
if let Some(bindings) = self.keybindings.command.get("command_execute") {
|
|
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
|
|
} else {
|
|
// Fall back to Enter key if no command_execute is defined.
|
|
key == KeyCode::Enter && modifiers.is_empty()
|
|
}
|
|
}
|
|
|
|
/// Checks if a key is bound to backspacing in Command mode.
|
|
pub fn is_command_backspace(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
|
if let Some(bindings) = self.keybindings.command.get("command_backspace") {
|
|
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
|
|
} else {
|
|
// Fall back to Backspace key if no command_backspace is defined.
|
|
key == KeyCode::Backspace && modifiers.is_empty()
|
|
}
|
|
}
|
|
|
|
/// Checks if a key is bound to a specific action.
|
|
pub fn has_key_for_action(&self, action: &str, key_char: char) -> bool {
|
|
// Check all mode-specific keybindings for the action
|
|
if let Some(bindings) = self.keybindings.read_only.get(action) {
|
|
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if let Some(bindings) = self.keybindings.edit.get(action) {
|
|
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if let Some(bindings) = self.keybindings.command.get(action) {
|
|
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if let Some(bindings) = self.keybindings.common.get(action) {
|
|
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if let Some(bindings) = self.keybindings.global.get(action) {
|
|
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// This method handles all keybinding formats, both with and without +
|
|
pub fn matches_key_sequence_generalized(&self, sequence: &[KeyCode]) -> Option<&str> {
|
|
if sequence.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Get string representations of the sequence
|
|
let sequence_str = sequence.iter()
|
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
|
.collect::<Vec<String>>()
|
|
.join("");
|
|
|
|
// Add the missing sequence_plus definition
|
|
let sequence_plus = sequence.iter()
|
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
|
.collect::<Vec<String>>()
|
|
.join("+");
|
|
|
|
// Check for matches in all binding formats across all modes
|
|
// First check read_only mode
|
|
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.read_only, &sequence_str, &sequence_plus, sequence) {
|
|
return Some(action);
|
|
}
|
|
|
|
// Then check edit mode
|
|
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.edit, &sequence_str, &sequence_plus, sequence) {
|
|
return Some(action);
|
|
}
|
|
|
|
// Then check command mode
|
|
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.command, &sequence_str, &sequence_plus, sequence) {
|
|
return Some(action);
|
|
}
|
|
|
|
// Then check common keybindings
|
|
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.common, &sequence_str, &sequence_plus, sequence) {
|
|
return Some(action);
|
|
}
|
|
|
|
// Finally check global bindings
|
|
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.global, &sequence_str, &sequence_plus, sequence) {
|
|
return Some(action);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Helper method to check a specific mode's bindings against a key sequence
|
|
fn check_bindings_for_sequence<'a>(
|
|
&self,
|
|
mode_bindings: &'a HashMap<String, Vec<String>>,
|
|
sequence_str: &str,
|
|
sequence_plus: &str,
|
|
sequence: &[KeyCode]
|
|
) -> Option<&'a str> {
|
|
for (action, bindings) in mode_bindings {
|
|
for binding in bindings {
|
|
let normalized_binding = binding.to_lowercase();
|
|
|
|
// Check if binding matches any of our formats
|
|
if normalized_binding == sequence_str || normalized_binding == sequence_plus {
|
|
return Some(action);
|
|
}
|
|
|
|
// Special case for + format in bindings
|
|
if binding.contains('+') {
|
|
let normalized_sequence = sequence.iter()
|
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
|
.collect::<Vec<String>>();
|
|
|
|
let binding_parts: Vec<&str> = binding.split('+').collect();
|
|
|
|
if binding_parts.len() == sequence.len() {
|
|
let matches = binding_parts.iter().enumerate().all(|(i, part)| {
|
|
part.to_lowercase() == normalized_sequence[i].to_lowercase()
|
|
});
|
|
|
|
if matches {
|
|
return Some(action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Check if the current key sequence is a prefix of a longer binding
|
|
pub fn is_key_sequence_prefix(&self, sequence: &[KeyCode]) -> bool {
|
|
if sequence.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
// Get string representation of the sequence
|
|
let sequence_str = sequence.iter()
|
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
|
.collect::<Vec<String>>()
|
|
.join("");
|
|
|
|
// Check in each mode if our sequence is a prefix
|
|
if self.is_prefix_in_mode(&self.keybindings.read_only, &sequence_str, sequence) {
|
|
return true;
|
|
}
|
|
|
|
if self.is_prefix_in_mode(&self.keybindings.edit, &sequence_str, sequence) {
|
|
return true;
|
|
}
|
|
|
|
if self.is_prefix_in_mode(&self.keybindings.command, &sequence_str, sequence) {
|
|
return true;
|
|
}
|
|
|
|
if self.is_prefix_in_mode(&self.keybindings.common, &sequence_str, sequence) {
|
|
return true;
|
|
}
|
|
|
|
if self.is_prefix_in_mode(&self.keybindings.global, &sequence_str, sequence) {
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Helper method to check if a sequence is a prefix in a specific mode
|
|
fn is_prefix_in_mode(
|
|
&self,
|
|
mode_bindings: &HashMap<String, Vec<String>>,
|
|
sequence_str: &str,
|
|
sequence: &[KeyCode]
|
|
) -> bool {
|
|
for (_, bindings) in mode_bindings {
|
|
for binding in bindings {
|
|
let normalized_binding = binding.to_lowercase();
|
|
|
|
// Check standard format
|
|
if normalized_binding.starts_with(sequence_str) &&
|
|
normalized_binding.len() > sequence_str.len() {
|
|
return true;
|
|
}
|
|
|
|
// Check + format
|
|
if binding.contains('+') {
|
|
let binding_parts: Vec<&str> = binding.split('+').collect();
|
|
let sequence_parts = sequence.iter()
|
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
|
.collect::<Vec<String>>();
|
|
|
|
if binding_parts.len() > sequence_parts.len() {
|
|
let prefix_matches = sequence_parts.iter().enumerate().all(|(i, part)| {
|
|
binding_parts.get(i).map_or(false, |b| b.to_lowercase() == part.to_lowercase())
|
|
});
|
|
|
|
if prefix_matches {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Unified action resolver for app-level actions
|
|
pub fn get_app_action(
|
|
&self,
|
|
key_code: crossterm::event::KeyCode,
|
|
modifiers: crossterm::event::KeyModifiers,
|
|
) -> Option<&str> {
|
|
// First check common actions
|
|
if let Some(action) = self.get_common_action(key_code, modifiers) {
|
|
return Some(action);
|
|
}
|
|
|
|
// Then check read-only mode actions
|
|
if let Some(action) = self.get_read_only_action_for_key(key_code, modifiers) {
|
|
return Some(action);
|
|
}
|
|
|
|
// Then check highlight mode actions
|
|
if let Some(action) = self.get_highlight_action_for_key(key_code, modifiers) {
|
|
return Some(action);
|
|
}
|
|
|
|
// Then check edit mode actions
|
|
if let Some(action) = self.get_edit_action_for_key(key_code, modifiers) {
|
|
return Some(action);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub fn build_canvas_keymap(&self) -> CanvasKeyMap {
|
|
CanvasKeyMap::from_mode_maps(
|
|
&self.keybindings.read_only,
|
|
&self.keybindings.edit,
|
|
&self.keybindings.highlight,
|
|
)
|
|
}
|
|
}
|
|
|
|
|