style: auto-format entire codebase with cargo fmt (stable rustfmt.toml)
Some checks failed
ci/woodpecker/pr/check Pipeline failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled

This commit is contained in:
Mikhail Kilin
2026-02-22 17:09:51 +03:00
parent 2442a90e23
commit 264f183510
90 changed files with 1632 additions and 1450 deletions

View File

@@ -4,7 +4,6 @@
/// - Загрузку из конфигурационного файла
/// - Множественные binding для одной команды (EN/RU раскладки)
/// - Type-safe команды через enum
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -49,12 +48,12 @@ pub enum Command {
SelectMessage,
// Media
ViewImage, // v - просмотр фото
ViewImage, // v - просмотр фото
// Voice playback
TogglePlayback, // Space - play/pause
SeekForward, // → - seek +5s
SeekBackward, // ← - seek -5s
TogglePlayback, // Space - play/pause
SeekForward, // → - seek +5s
SeekBackward, // ← - seek -5s
// Input
SubmitMessage,
@@ -83,31 +82,19 @@ pub struct KeyBinding {
impl KeyBinding {
pub fn new(key: KeyCode) -> Self {
Self {
key,
modifiers: KeyModifiers::NONE,
}
Self { key, modifiers: KeyModifiers::NONE }
}
pub fn with_ctrl(key: KeyCode) -> Self {
Self {
key,
modifiers: KeyModifiers::CONTROL,
}
Self { key, modifiers: KeyModifiers::CONTROL }
}
pub fn with_shift(key: KeyCode) -> Self {
Self {
key,
modifiers: KeyModifiers::SHIFT,
}
Self { key, modifiers: KeyModifiers::SHIFT }
}
pub fn with_alt(key: KeyCode) -> Self {
Self {
key,
modifiers: KeyModifiers::ALT,
}
Self { key, modifiers: KeyModifiers::ALT }
}
pub fn matches(&self, event: &KeyEvent) -> bool {
@@ -128,50 +115,65 @@ impl Keybindings {
let mut bindings = HashMap::new();
// Navigation
bindings.insert(Command::MoveUp, vec![
KeyBinding::new(KeyCode::Up),
KeyBinding::new(KeyCode::Char('k')),
KeyBinding::new(KeyCode::Char('р')), // RU (custom mapping, not standard ЙЦУКЕН)
]);
bindings.insert(Command::MoveDown, vec![
KeyBinding::new(KeyCode::Down),
KeyBinding::new(KeyCode::Char('j')),
KeyBinding::new(KeyCode::Char('о')), // RU
]);
bindings.insert(Command::MoveLeft, vec![
KeyBinding::new(KeyCode::Left),
KeyBinding::new(KeyCode::Char('h')),
KeyBinding::new(KeyCode::Char('л')), // RU (custom mapping, not standard ЙЦУКЕН)
]);
bindings.insert(Command::MoveRight, vec![
KeyBinding::new(KeyCode::Right),
KeyBinding::new(KeyCode::Char('l')),
KeyBinding::new(KeyCode::Char('д')), // RU
]);
bindings.insert(Command::PageUp, vec![
KeyBinding::new(KeyCode::PageUp),
KeyBinding::with_ctrl(KeyCode::Char('u')),
]);
bindings.insert(Command::PageDown, vec![
KeyBinding::new(KeyCode::PageDown),
KeyBinding::with_ctrl(KeyCode::Char('d')),
]);
bindings.insert(
Command::MoveUp,
vec![
KeyBinding::new(KeyCode::Up),
KeyBinding::new(KeyCode::Char('k')),
KeyBinding::new(KeyCode::Char('р')), // RU (custom mapping, not standard ЙЦУКЕН)
],
);
bindings.insert(
Command::MoveDown,
vec![
KeyBinding::new(KeyCode::Down),
KeyBinding::new(KeyCode::Char('j')),
KeyBinding::new(KeyCode::Char('о')), // RU
],
);
bindings.insert(
Command::MoveLeft,
vec![
KeyBinding::new(KeyCode::Left),
KeyBinding::new(KeyCode::Char('h')),
KeyBinding::new(KeyCode::Char('л')), // RU (custom mapping, not standard ЙЦУКЕН)
],
);
bindings.insert(
Command::MoveRight,
vec![
KeyBinding::new(KeyCode::Right),
KeyBinding::new(KeyCode::Char('l')),
KeyBinding::new(KeyCode::Char('д')), // RU
],
);
bindings.insert(
Command::PageUp,
vec![
KeyBinding::new(KeyCode::PageUp),
KeyBinding::with_ctrl(KeyCode::Char('u')),
],
);
bindings.insert(
Command::PageDown,
vec![
KeyBinding::new(KeyCode::PageDown),
KeyBinding::with_ctrl(KeyCode::Char('d')),
],
);
// Global
bindings.insert(Command::Quit, vec![
KeyBinding::new(KeyCode::Char('q')),
KeyBinding::new(KeyCode::Char('й')), // RU
KeyBinding::with_ctrl(KeyCode::Char('c')),
]);
bindings.insert(Command::OpenSearch, vec![
KeyBinding::with_ctrl(KeyCode::Char('s')),
]);
bindings.insert(Command::OpenSearchInChat, vec![
KeyBinding::with_ctrl(KeyCode::Char('f')),
]);
bindings.insert(Command::Help, vec![
KeyBinding::new(KeyCode::Char('?')),
]);
bindings.insert(
Command::Quit,
vec![
KeyBinding::new(KeyCode::Char('q')),
KeyBinding::new(KeyCode::Char('й')), // RU
KeyBinding::with_ctrl(KeyCode::Char('c')),
],
);
bindings.insert(Command::OpenSearch, vec![KeyBinding::with_ctrl(KeyCode::Char('s'))]);
bindings.insert(Command::OpenSearchInChat, vec![KeyBinding::with_ctrl(KeyCode::Char('f'))]);
bindings.insert(Command::Help, vec![KeyBinding::new(KeyCode::Char('?'))]);
// Chat list
// Note: Enter обрабатывается через Command::SubmitMessage в handle_enter_key()
@@ -188,90 +190,114 @@ impl Keybindings {
9 => Command::SelectFolder9,
_ => unreachable!(),
};
bindings.insert(cmd, vec![
KeyBinding::new(KeyCode::Char(char::from_digit(i, 10).unwrap())),
]);
bindings.insert(
cmd,
vec![KeyBinding::new(KeyCode::Char(
char::from_digit(i, 10).unwrap(),
))],
);
}
// Message actions
// Note: EditMessage (Up) обрабатывается напрямую в handle_open_chat_keyboard_input
// в зависимости от контекста (пустой инпут). Не привязываем здесь, чтобы не
// конфликтовать с Command::MoveUp в списке чатов.
bindings.insert(Command::DeleteMessage, vec![
KeyBinding::new(KeyCode::Delete),
KeyBinding::new(KeyCode::Char('d')),
KeyBinding::new(KeyCode::Char('в')), // RU
]);
bindings.insert(Command::ReplyMessage, vec![
KeyBinding::new(KeyCode::Char('r')),
KeyBinding::new(KeyCode::Char('к')), // RU
]);
bindings.insert(Command::ForwardMessage, vec![
KeyBinding::new(KeyCode::Char('f')),
KeyBinding::new(KeyCode::Char('а')), // RU
]);
bindings.insert(Command::CopyMessage, vec![
KeyBinding::new(KeyCode::Char('y')),
KeyBinding::new(KeyCode::Char('н')), // RU
]);
bindings.insert(Command::ReactMessage, vec![
KeyBinding::new(KeyCode::Char('e')),
KeyBinding::new(KeyCode::Char('у')), // RU
]);
bindings.insert(
Command::DeleteMessage,
vec![
KeyBinding::new(KeyCode::Delete),
KeyBinding::new(KeyCode::Char('d')),
KeyBinding::new(KeyCode::Char('в')), // RU
],
);
bindings.insert(
Command::ReplyMessage,
vec![
KeyBinding::new(KeyCode::Char('r')),
KeyBinding::new(KeyCode::Char('к')), // RU
],
);
bindings.insert(
Command::ForwardMessage,
vec![
KeyBinding::new(KeyCode::Char('f')),
KeyBinding::new(KeyCode::Char('а')), // RU
],
);
bindings.insert(
Command::CopyMessage,
vec![
KeyBinding::new(KeyCode::Char('y')),
KeyBinding::new(KeyCode::Char('н')), // RU
],
);
bindings.insert(
Command::ReactMessage,
vec![
KeyBinding::new(KeyCode::Char('e')),
KeyBinding::new(KeyCode::Char('у')), // RU
],
);
// Note: SelectMessage обрабатывается через Command::SubmitMessage в handle_enter_key()
// Media
bindings.insert(Command::ViewImage, vec![
KeyBinding::new(KeyCode::Char('v')),
KeyBinding::new(KeyCode::Char('м')), // RU
]);
bindings.insert(
Command::ViewImage,
vec![
KeyBinding::new(KeyCode::Char('v')),
KeyBinding::new(KeyCode::Char('м')), // RU
],
);
// Voice playback
bindings.insert(Command::TogglePlayback, vec![
KeyBinding::new(KeyCode::Char(' ')),
]);
bindings.insert(Command::SeekForward, vec![
KeyBinding::new(KeyCode::Right),
]);
bindings.insert(Command::SeekBackward, vec![
KeyBinding::new(KeyCode::Left),
]);
bindings.insert(Command::TogglePlayback, vec![KeyBinding::new(KeyCode::Char(' '))]);
bindings.insert(Command::SeekForward, vec![KeyBinding::new(KeyCode::Right)]);
bindings.insert(Command::SeekBackward, vec![KeyBinding::new(KeyCode::Left)]);
// Input
bindings.insert(Command::SubmitMessage, vec![
KeyBinding::new(KeyCode::Enter),
]);
bindings.insert(Command::Cancel, vec![
KeyBinding::new(KeyCode::Esc),
]);
bindings.insert(Command::SubmitMessage, vec![KeyBinding::new(KeyCode::Enter)]);
bindings.insert(Command::Cancel, vec![KeyBinding::new(KeyCode::Esc)]);
bindings.insert(Command::NewLine, vec![]);
bindings.insert(Command::DeleteChar, vec![
KeyBinding::new(KeyCode::Backspace),
]);
bindings.insert(Command::DeleteWord, vec![
KeyBinding::with_ctrl(KeyCode::Backspace),
KeyBinding::with_ctrl(KeyCode::Char('w')),
]);
bindings.insert(Command::MoveToStart, vec![
KeyBinding::new(KeyCode::Home),
KeyBinding::with_ctrl(KeyCode::Char('a')),
]);
bindings.insert(Command::MoveToEnd, vec![
KeyBinding::new(KeyCode::End),
KeyBinding::with_ctrl(KeyCode::Char('e')),
]);
bindings.insert(Command::DeleteChar, vec![KeyBinding::new(KeyCode::Backspace)]);
bindings.insert(
Command::DeleteWord,
vec![
KeyBinding::with_ctrl(KeyCode::Backspace),
KeyBinding::with_ctrl(KeyCode::Char('w')),
],
);
bindings.insert(
Command::MoveToStart,
vec![
KeyBinding::new(KeyCode::Home),
KeyBinding::with_ctrl(KeyCode::Char('a')),
],
);
bindings.insert(
Command::MoveToEnd,
vec![
KeyBinding::new(KeyCode::End),
KeyBinding::with_ctrl(KeyCode::Char('e')),
],
);
// Vim mode
bindings.insert(Command::EnterInsertMode, vec![
KeyBinding::new(KeyCode::Char('i')),
KeyBinding::new(KeyCode::Char('ш')), // RU
]);
bindings.insert(
Command::EnterInsertMode,
vec![
KeyBinding::new(KeyCode::Char('i')),
KeyBinding::new(KeyCode::Char('ш')), // RU
],
);
// Profile
bindings.insert(Command::OpenProfile, vec![
KeyBinding::with_ctrl(KeyCode::Char('u')), // Ctrl+U instead of Ctrl+I
KeyBinding::with_ctrl(KeyCode::Char('г')), // RU
]);
bindings.insert(
Command::OpenProfile,
vec![
KeyBinding::with_ctrl(KeyCode::Char('u')), // Ctrl+U instead of Ctrl+I
KeyBinding::with_ctrl(KeyCode::Char('г')), // RU
],
);
Self { bindings }
}
@@ -395,9 +421,10 @@ mod key_code_serde {
let s = String::deserialize(deserializer)?;
if s.starts_with("Char('") && s.ends_with("')") {
let c = s.chars().nth(6).ok_or_else(|| {
serde::de::Error::custom("Invalid Char format")
})?;
let c = s
.chars()
.nth(6)
.ok_or_else(|| serde::de::Error::custom("Invalid Char format"))?;
return Ok(KeyCode::Char(c));
}

View File

@@ -284,10 +284,22 @@ mod tests {
let keybindings = &config.keybindings;
// Test that keybindings exist for common commands
assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)) == Some(Command::ReplyMessage));
assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('к'), KeyModifiers::NONE)) == Some(Command::ReplyMessage));
assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE)) == Some(Command::ForwardMessage));
assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('а'), KeyModifiers::NONE)) == Some(Command::ForwardMessage));
assert!(
keybindings.get_command(&KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE))
== Some(Command::ReplyMessage)
);
assert!(
keybindings.get_command(&KeyEvent::new(KeyCode::Char('к'), KeyModifiers::NONE))
== Some(Command::ReplyMessage)
);
assert!(
keybindings.get_command(&KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE))
== Some(Command::ForwardMessage)
);
assert!(
keybindings.get_command(&KeyEvent::new(KeyCode::Char('а'), KeyModifiers::NONE))
== Some(Command::ForwardMessage)
);
}
#[test]
@@ -355,10 +367,24 @@ mod tests {
#[test]
fn test_config_validate_valid_all_standard_colors() {
let colors = [
"black", "red", "green", "yellow", "blue", "magenta",
"cyan", "gray", "grey", "white", "darkgray", "darkgrey",
"lightred", "lightgreen", "lightyellow", "lightblue",
"lightmagenta", "lightcyan"
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"gray",
"grey",
"white",
"darkgray",
"darkgrey",
"lightred",
"lightgreen",
"lightyellow",
"lightblue",
"lightmagenta",
"lightcyan",
];
for color in colors {
@@ -369,11 +395,7 @@ mod tests {
config.colors.reaction_chosen = color.to_string();
config.colors.reaction_other = color.to_string();
assert!(
config.validate().is_ok(),
"Color '{}' should be valid",
color
);
assert!(config.validate().is_ok(), "Color '{}' should be valid", color);
}
}