543 lines
20 KiB
Rust
543 lines
20 KiB
Rust
// src/config/binds/config.rs
|
|
|
|
use serde::Deserialize;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use crossterm::event::{KeyCode, KeyModifiers};
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
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 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, Box<dyn std::error::Error>> {
|
|
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)
|
|
.map_err(|e| format!("Failed to read config file at {:?}: {}", config_path, e))?;
|
|
let config: Config = toml::from_str(&config_str)?;
|
|
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 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,
|
|
is_edit_mode: bool,
|
|
command_mode: bool,
|
|
key: KeyCode,
|
|
modifiers: KeyModifiers
|
|
) -> Option<&str> {
|
|
match (command_mode, is_edit_mode) {
|
|
(true, _) => self.get_command_action_for_key(key, modifiers),
|
|
(_, true) => self.get_edit_action_for_key(key, modifiers)
|
|
.or_else(|| self.get_common_action(key, modifiers)),
|
|
_ => self.get_read_only_action_for_key(key, modifiers)
|
|
.or_else(|| self.get_common_action(key, modifiers))
|
|
// Add global bindings check for read-only mode
|
|
.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 {
|
|
// For multi-character bindings without modifiers, handle them in matches_key_sequence.
|
|
if binding.len() > 1 && !binding.contains('+') {
|
|
return match binding.to_lowercase().as_str() {
|
|
"left" => key == KeyCode::Left,
|
|
"right" => key == KeyCode::Right,
|
|
"up" => key == KeyCode::Up,
|
|
"down" => key == KeyCode::Down,
|
|
"esc" => key == KeyCode::Esc,
|
|
"enter" => key == KeyCode::Enter,
|
|
"delete" => key == KeyCode::Delete,
|
|
"backspace" => key == KeyCode::Backspace,
|
|
"tab" => key == KeyCode::Tab,
|
|
"backtab" => key == KeyCode::BackTab,
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
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() {
|
|
"ctrl" => expected_modifiers |= KeyModifiers::CONTROL,
|
|
"shift" => expected_modifiers |= KeyModifiers::SHIFT,
|
|
"alt" => expected_modifiers |= KeyModifiers::ALT,
|
|
"left" => expected_key = Some(KeyCode::Left),
|
|
"right" => expected_key = Some(KeyCode::Right),
|
|
"up" => expected_key = Some(KeyCode::Up),
|
|
"down" => expected_key = Some(KeyCode::Down),
|
|
"esc" => expected_key = Some(KeyCode::Esc),
|
|
"enter" => expected_key = Some(KeyCode::Enter),
|
|
"delete" => expected_key = Some(KeyCode::Delete),
|
|
"backspace" => expected_key = Some(KeyCode::Backspace),
|
|
"tab" => expected_key = Some(KeyCode::Tab),
|
|
"backtab" => expected_key = Some(KeyCode::BackTab),
|
|
":" => expected_key = Some(KeyCode::Char(':')),
|
|
part => {
|
|
if part.len() == 1 {
|
|
let c = part.chars().next().unwrap();
|
|
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
|
|
}
|
|
}
|