refactor: add hotkey mapping configuration (P3.10)
- Add HotkeysConfig structure in src/config.rs - Implement matches(key: KeyCode, action: &str) method - Support for 10 configurable hotkeys: * Navigation: up, down, left, right (vim + russian + arrows) * Actions: reply, forward, delete, copy, react, profile - Add support for char keys and special keys (Up, Down, Delete, etc) - Add default values for all hotkeys (english + russian layouts) - Write 9 unit tests (all passing) - Add rustdoc documentation with examples - Update REFACTORING_ROADMAP.md (Priority 3: 4/4 tasks, 100%) - Update CONTEXT.md with implementation details - Overall refactoring progress: 12/17 tasks (71%) Priority 3 is now 100% complete! 🎉 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
340
src/config.rs
340
src/config.rs
@@ -1,3 +1,4 @@
|
||||
use crossterm::event::KeyCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -8,6 +9,8 @@ pub struct Config {
|
||||
pub general: GeneralConfig,
|
||||
#[serde(default)]
|
||||
pub colors: ColorsConfig,
|
||||
#[serde(default)]
|
||||
pub hotkeys: HotkeysConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -40,6 +43,49 @@ pub struct ColorsConfig {
|
||||
pub reaction_other: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HotkeysConfig {
|
||||
/// Навигация вверх (vim: k, рус: р, стрелка: Up)
|
||||
#[serde(default = "default_up_keys")]
|
||||
pub up: Vec<String>,
|
||||
|
||||
/// Навигация вниз (vim: j, рус: о, стрелка: Down)
|
||||
#[serde(default = "default_down_keys")]
|
||||
pub down: Vec<String>,
|
||||
|
||||
/// Навигация влево (vim: h, рус: р, стрелка: Left)
|
||||
#[serde(default = "default_left_keys")]
|
||||
pub left: Vec<String>,
|
||||
|
||||
/// Навигация вправо (vim: l, рус: д, стрелка: Right)
|
||||
#[serde(default = "default_right_keys")]
|
||||
pub right: Vec<String>,
|
||||
|
||||
/// Reply — ответить на сообщение (англ: r, рус: к)
|
||||
#[serde(default = "default_reply_keys")]
|
||||
pub reply: Vec<String>,
|
||||
|
||||
/// Forward — переслать сообщение (англ: f, рус: а)
|
||||
#[serde(default = "default_forward_keys")]
|
||||
pub forward: Vec<String>,
|
||||
|
||||
/// Delete — удалить сообщение (англ: d, рус: в, Delete key)
|
||||
#[serde(default = "default_delete_keys")]
|
||||
pub delete: Vec<String>,
|
||||
|
||||
/// Copy — копировать сообщение (англ: y, рус: н)
|
||||
#[serde(default = "default_copy_keys")]
|
||||
pub copy: Vec<String>,
|
||||
|
||||
/// React — добавить реакцию (англ: e, рус: у)
|
||||
#[serde(default = "default_react_keys")]
|
||||
pub react: Vec<String>,
|
||||
|
||||
/// Profile — открыть профиль (англ: i, рус: ш)
|
||||
#[serde(default = "default_profile_keys")]
|
||||
pub profile: Vec<String>,
|
||||
}
|
||||
|
||||
// Дефолтные значения
|
||||
fn default_timezone() -> String {
|
||||
"+03:00".to_string()
|
||||
@@ -65,6 +111,46 @@ fn default_reaction_other_color() -> String {
|
||||
"gray".to_string()
|
||||
}
|
||||
|
||||
fn default_up_keys() -> Vec<String> {
|
||||
vec!["k".to_string(), "р".to_string(), "Up".to_string()]
|
||||
}
|
||||
|
||||
fn default_down_keys() -> Vec<String> {
|
||||
vec!["j".to_string(), "о".to_string(), "Down".to_string()]
|
||||
}
|
||||
|
||||
fn default_left_keys() -> Vec<String> {
|
||||
vec!["h".to_string(), "р".to_string(), "Left".to_string()]
|
||||
}
|
||||
|
||||
fn default_right_keys() -> Vec<String> {
|
||||
vec!["l".to_string(), "д".to_string(), "Right".to_string()]
|
||||
}
|
||||
|
||||
fn default_reply_keys() -> Vec<String> {
|
||||
vec!["r".to_string(), "к".to_string()]
|
||||
}
|
||||
|
||||
fn default_forward_keys() -> Vec<String> {
|
||||
vec!["f".to_string(), "а".to_string()]
|
||||
}
|
||||
|
||||
fn default_delete_keys() -> Vec<String> {
|
||||
vec!["d".to_string(), "в".to_string(), "Delete".to_string()]
|
||||
}
|
||||
|
||||
fn default_copy_keys() -> Vec<String> {
|
||||
vec!["y".to_string(), "н".to_string()]
|
||||
}
|
||||
|
||||
fn default_react_keys() -> Vec<String> {
|
||||
vec!["e".to_string(), "у".to_string()]
|
||||
}
|
||||
|
||||
fn default_profile_keys() -> Vec<String> {
|
||||
vec!["i".to_string(), "ш".to_string()]
|
||||
}
|
||||
|
||||
impl Default for GeneralConfig {
|
||||
fn default() -> Self {
|
||||
Self { timezone: default_timezone() }
|
||||
@@ -83,11 +169,147 @@ impl Default for ColorsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HotkeysConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
up: default_up_keys(),
|
||||
down: default_down_keys(),
|
||||
left: default_left_keys(),
|
||||
right: default_right_keys(),
|
||||
reply: default_reply_keys(),
|
||||
forward: default_forward_keys(),
|
||||
delete: default_delete_keys(),
|
||||
copy: default_copy_keys(),
|
||||
react: default_react_keys(),
|
||||
profile: default_profile_keys(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HotkeysConfig {
|
||||
/// Проверяет, соответствует ли клавиша указанному действию
|
||||
///
|
||||
/// # Аргументы
|
||||
///
|
||||
/// * `key` - Код нажатой клавиши
|
||||
/// * `action` - Название действия ("up", "down", "reply", "forward", и т.д.)
|
||||
///
|
||||
/// # Возвращает
|
||||
///
|
||||
/// `true` если клавиша соответствует действию, иначе `false`
|
||||
///
|
||||
/// # Примеры
|
||||
///
|
||||
/// ```no_run
|
||||
/// use tele_tui::config::Config;
|
||||
/// use crossterm::event::KeyCode;
|
||||
///
|
||||
/// let config = Config::default();
|
||||
///
|
||||
/// // Проверяем клавишу 'k' для действия "up"
|
||||
/// assert!(config.hotkeys.matches(KeyCode::Char('k'), "up"));
|
||||
///
|
||||
/// // Проверяем русскую клавишу 'р' для действия "up"
|
||||
/// assert!(config.hotkeys.matches(KeyCode::Char('р'), "up"));
|
||||
///
|
||||
/// // Проверяем стрелку вверх
|
||||
/// assert!(config.hotkeys.matches(KeyCode::Up, "up"));
|
||||
///
|
||||
/// // Проверяем клавишу 'r' для действия "reply"
|
||||
/// assert!(config.hotkeys.matches(KeyCode::Char('r'), "reply"));
|
||||
/// ```
|
||||
pub fn matches(&self, key: KeyCode, action: &str) -> bool {
|
||||
let keys = match action {
|
||||
"up" => &self.up,
|
||||
"down" => &self.down,
|
||||
"left" => &self.left,
|
||||
"right" => &self.right,
|
||||
"reply" => &self.reply,
|
||||
"forward" => &self.forward,
|
||||
"delete" => &self.delete,
|
||||
"copy" => &self.copy,
|
||||
"react" => &self.react,
|
||||
"profile" => &self.profile,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
self.key_matches(key, keys)
|
||||
}
|
||||
|
||||
/// Вспомогательная функция для проверки соответствия KeyCode списку строк
|
||||
fn key_matches(&self, key: KeyCode, keys: &[String]) -> bool {
|
||||
for key_str in keys {
|
||||
match key_str.as_str() {
|
||||
// Специальные клавиши
|
||||
"Up" => {
|
||||
if matches!(key, KeyCode::Up) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"Down" => {
|
||||
if matches!(key, KeyCode::Down) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"Left" => {
|
||||
if matches!(key, KeyCode::Left) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"Right" => {
|
||||
if matches!(key, KeyCode::Right) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"Delete" => {
|
||||
if matches!(key, KeyCode::Delete) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"Enter" => {
|
||||
if matches!(key, KeyCode::Enter) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"Esc" => {
|
||||
if matches!(key, KeyCode::Esc) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"Backspace" => {
|
||||
if matches!(key, KeyCode::Backspace) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
"Tab" => {
|
||||
if matches!(key, KeyCode::Tab) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Символьные клавиши (буквы, цифры)
|
||||
key_char if key_char.len() == 1 => {
|
||||
if let KeyCode::Char(ch) = key {
|
||||
if let Some(expected_ch) = key_char.chars().next() {
|
||||
if ch == expected_ch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
general: GeneralConfig::default(),
|
||||
colors: ColorsConfig::default(),
|
||||
hotkeys: HotkeysConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,3 +537,121 @@ impl Config {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hotkeys_matches_char_keys() {
|
||||
let hotkeys = HotkeysConfig::default();
|
||||
|
||||
// Test reply keys (r, к)
|
||||
assert!(hotkeys.matches(KeyCode::Char('r'), "reply"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('к'), "reply"));
|
||||
|
||||
// Test forward keys (f, а)
|
||||
assert!(hotkeys.matches(KeyCode::Char('f'), "forward"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('а'), "forward"));
|
||||
|
||||
// Test delete keys (d, в)
|
||||
assert!(hotkeys.matches(KeyCode::Char('d'), "delete"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('в'), "delete"));
|
||||
|
||||
// Test copy keys (y, н)
|
||||
assert!(hotkeys.matches(KeyCode::Char('y'), "copy"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('н'), "copy"));
|
||||
|
||||
// Test react keys (e, у)
|
||||
assert!(hotkeys.matches(KeyCode::Char('e'), "react"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('у'), "react"));
|
||||
|
||||
// Test profile keys (i, ш)
|
||||
assert!(hotkeys.matches(KeyCode::Char('i'), "profile"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('ш'), "profile"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hotkeys_matches_arrow_keys() {
|
||||
let hotkeys = HotkeysConfig::default();
|
||||
|
||||
// Test navigation arrows
|
||||
assert!(hotkeys.matches(KeyCode::Up, "up"));
|
||||
assert!(hotkeys.matches(KeyCode::Down, "down"));
|
||||
assert!(hotkeys.matches(KeyCode::Left, "left"));
|
||||
assert!(hotkeys.matches(KeyCode::Right, "right"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hotkeys_matches_vim_keys() {
|
||||
let hotkeys = HotkeysConfig::default();
|
||||
|
||||
// Test vim navigation keys
|
||||
assert!(hotkeys.matches(KeyCode::Char('k'), "up"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('j'), "down"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('h'), "left"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('l'), "right"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hotkeys_matches_russian_vim_keys() {
|
||||
let hotkeys = HotkeysConfig::default();
|
||||
|
||||
// Test russian vim navigation keys
|
||||
assert!(hotkeys.matches(KeyCode::Char('р'), "up"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('о'), "down"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('р'), "left"));
|
||||
assert!(hotkeys.matches(KeyCode::Char('д'), "right"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hotkeys_matches_special_delete_key() {
|
||||
let hotkeys = HotkeysConfig::default();
|
||||
|
||||
// Test Delete key for delete action
|
||||
assert!(hotkeys.matches(KeyCode::Delete, "delete"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hotkeys_does_not_match_wrong_keys() {
|
||||
let hotkeys = HotkeysConfig::default();
|
||||
|
||||
// Test wrong keys don't match
|
||||
assert!(!hotkeys.matches(KeyCode::Char('x'), "reply"));
|
||||
assert!(!hotkeys.matches(KeyCode::Char('z'), "forward"));
|
||||
assert!(!hotkeys.matches(KeyCode::Char('q'), "delete"));
|
||||
assert!(!hotkeys.matches(KeyCode::Enter, "copy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hotkeys_does_not_match_wrong_actions() {
|
||||
let hotkeys = HotkeysConfig::default();
|
||||
|
||||
// Test valid keys don't match wrong actions
|
||||
assert!(!hotkeys.matches(KeyCode::Char('r'), "forward"));
|
||||
assert!(!hotkeys.matches(KeyCode::Char('f'), "reply"));
|
||||
assert!(!hotkeys.matches(KeyCode::Char('d'), "copy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hotkeys_unknown_action() {
|
||||
let hotkeys = HotkeysConfig::default();
|
||||
|
||||
// Unknown actions should return false
|
||||
assert!(!hotkeys.matches(KeyCode::Char('r'), "unknown_action"));
|
||||
assert!(!hotkeys.matches(KeyCode::Enter, "foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_default_includes_hotkeys() {
|
||||
let config = Config::default();
|
||||
|
||||
// Verify hotkeys are included in default config
|
||||
assert_eq!(config.hotkeys.reply, vec!["r", "к"]);
|
||||
assert_eq!(config.hotkeys.forward, vec!["f", "а"]);
|
||||
assert_eq!(config.hotkeys.delete, vec!["d", "в", "Delete"]);
|
||||
assert_eq!(config.hotkeys.copy, vec!["y", "н"]);
|
||||
assert_eq!(config.hotkeys.react, vec!["e", "у"]);
|
||||
assert_eq!(config.hotkeys.profile, vec!["i", "ш"]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user