Some checks 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
Извлечены state модули и сервисы из монолитных файлов для улучшения структуры: State модули: - auth_state.rs: состояние авторизации - chat_list_state.rs: состояние списка чатов - compose_state.rs: состояние ввода сообщений - message_view_state.rs: состояние просмотра сообщений - ui_state.rs: UI состояние Сервисы и утилиты: - chat_filter.rs: централизованная фильтрация чатов (470+ строк) - message_service.rs: сервис работы с сообщениями (17KB) - key_handler.rs: trait для обработки клавиш (380+ строк) Config модуль: - config.rs -> config/mod.rs: основной конфиг - config/keybindings.rs: настраиваемые горячие клавиши (420+ строк) Тесты: 626 passed ✅ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
451 lines
14 KiB
Rust
451 lines
14 KiB
Rust
/// Модуль для обработки клавиш с использованием trait-based подхода
|
||
///
|
||
/// Позволяет каждому экрану/режиму определить свою логику обработки клавиш,
|
||
/// избегая огромных match блоков в одном месте.
|
||
|
||
use crate::app::App;
|
||
use crate::config::Command;
|
||
use crate::tdlib::{TdClient, TdClientTrait};
|
||
use crossterm::event::KeyEvent;
|
||
|
||
/// Результат обработки клавиши
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum KeyResult {
|
||
/// Клавиша обработана, продолжить работу
|
||
Handled,
|
||
|
||
/// Клавиша обработана, нужна перерисовка UI
|
||
HandledNeedsRedraw,
|
||
|
||
/// Клавиша не обработана (fallback на глобальные команды)
|
||
NotHandled,
|
||
|
||
/// Выход из приложения
|
||
Quit,
|
||
}
|
||
|
||
impl KeyResult {
|
||
/// Проверяет нужна ли перерисовка
|
||
pub fn needs_redraw(&self) -> bool {
|
||
matches!(self, KeyResult::HandledNeedsRedraw)
|
||
}
|
||
|
||
/// Проверяет был ли запрос выхода
|
||
pub fn should_quit(&self) -> bool {
|
||
matches!(self, KeyResult::Quit)
|
||
}
|
||
}
|
||
|
||
/// Trait для обработки клавиш на конкретном экране/в режиме
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```ignore
|
||
/// struct ChatListHandler;
|
||
///
|
||
/// impl KeyHandler for ChatListHandler {
|
||
/// fn handle_key(
|
||
/// &self,
|
||
/// app: &mut App,
|
||
/// key: KeyEvent,
|
||
/// command: Option<Command>,
|
||
/// ) -> KeyResult {
|
||
/// match command {
|
||
/// Some(Command::MoveUp) => {
|
||
/// app.move_chat_selection_up();
|
||
/// KeyResult::HandledNeedsRedraw
|
||
/// }
|
||
/// Some(Command::OpenChat) => {
|
||
/// // Open selected chat
|
||
/// KeyResult::HandledNeedsRedraw
|
||
/// }
|
||
/// _ => KeyResult::NotHandled,
|
||
/// }
|
||
/// }
|
||
/// }
|
||
/// ```
|
||
pub trait KeyHandler {
|
||
/// Обрабатывает нажатие клавиши
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `app` - Mutable reference на состояние приложения
|
||
/// * `key` - Событие клавиши от crossterm
|
||
/// * `command` - Опциональная команда из keybindings (если привязана)
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// `KeyResult` - результат обработки (обработана/не обработана/выход)
|
||
fn handle_key(
|
||
&self,
|
||
app: &mut App,
|
||
key: KeyEvent,
|
||
command: Option<Command>,
|
||
) -> KeyResult;
|
||
|
||
/// Приоритет обработчика (для цепочки обработчиков)
|
||
///
|
||
/// Обработчики с более высоким приоритетом вызываются первыми.
|
||
/// По умолчанию 0.
|
||
fn priority(&self) -> i32 {
|
||
0
|
||
}
|
||
}
|
||
|
||
/// Глобальный обработчик клавиш (работает на всех экранах)
|
||
pub struct GlobalKeyHandler;
|
||
|
||
impl KeyHandler for GlobalKeyHandler {
|
||
fn handle_key(
|
||
&self,
|
||
app: &mut App,
|
||
_key: KeyEvent,
|
||
command: Option<Command>,
|
||
) -> KeyResult {
|
||
match command {
|
||
Some(Command::Quit) => KeyResult::Quit,
|
||
|
||
Some(Command::OpenSearch) if !app.is_searching() => {
|
||
// TODO: implement enter_search_mode or use existing method
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::Cancel) => {
|
||
// Cancel различных режимов
|
||
if app.is_searching() {
|
||
// TODO: implement exit_search_mode or use existing method
|
||
KeyResult::HandledNeedsRedraw
|
||
} else {
|
||
KeyResult::NotHandled
|
||
}
|
||
}
|
||
|
||
_ => KeyResult::NotHandled,
|
||
}
|
||
}
|
||
|
||
fn priority(&self) -> i32 {
|
||
-100 // Низкий приоритет - fallback для всех экранов
|
||
}
|
||
}
|
||
|
||
/// Обработчик для списка чатов
|
||
pub struct ChatListKeyHandler;
|
||
|
||
impl KeyHandler for ChatListKeyHandler {
|
||
fn handle_key(
|
||
&self,
|
||
app: &mut App,
|
||
_key: KeyEvent,
|
||
command: Option<Command>,
|
||
) -> KeyResult {
|
||
match command {
|
||
Some(Command::MoveUp) => {
|
||
// TODO: implement chat selection navigation
|
||
// app.chat_list_state is ListState, use .select()
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::MoveDown) => {
|
||
// TODO: implement chat selection navigation
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::OpenChat) => {
|
||
// Обработка открытия чата будет в async контексте
|
||
// Здесь только возвращаем что команда распознана
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
// Папки 1-9
|
||
Some(Command::SelectFolder1) => {
|
||
app.set_selected_folder_id(Some(1));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
Some(Command::SelectFolder2) => {
|
||
app.set_selected_folder_id(Some(2));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
Some(Command::SelectFolder3) => {
|
||
app.set_selected_folder_id(Some(3));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
Some(Command::SelectFolder4) => {
|
||
app.set_selected_folder_id(Some(4));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
Some(Command::SelectFolder5) => {
|
||
app.set_selected_folder_id(Some(5));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
Some(Command::SelectFolder6) => {
|
||
app.set_selected_folder_id(Some(6));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
Some(Command::SelectFolder7) => {
|
||
app.set_selected_folder_id(Some(7));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
Some(Command::SelectFolder8) => {
|
||
app.set_selected_folder_id(Some(8));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
Some(Command::SelectFolder9) => {
|
||
app.set_selected_folder_id(Some(9));
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
_ => KeyResult::NotHandled,
|
||
}
|
||
}
|
||
|
||
fn priority(&self) -> i32 {
|
||
10 // Средний приоритет
|
||
}
|
||
}
|
||
|
||
/// Обработчик для просмотра сообщений
|
||
pub struct MessageViewKeyHandler;
|
||
|
||
impl KeyHandler for MessageViewKeyHandler {
|
||
fn handle_key(
|
||
&self,
|
||
app: &mut App,
|
||
_key: KeyEvent,
|
||
command: Option<Command>,
|
||
) -> KeyResult {
|
||
match command {
|
||
Some(Command::MoveUp) => {
|
||
if app.message_view_state().message_scroll_offset > 0 {
|
||
app.message_view_state().message_scroll_offset -= 1;
|
||
KeyResult::HandledNeedsRedraw
|
||
} else {
|
||
KeyResult::Handled
|
||
}
|
||
}
|
||
|
||
Some(Command::MoveDown) => {
|
||
app.message_view_state().message_scroll_offset += 1;
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::PageUp) => {
|
||
app.message_view_state().message_scroll_offset = app.message_view_state().message_scroll_offset.saturating_sub(10);
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::PageDown) => {
|
||
app.message_view_state().message_scroll_offset += 10;
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::OpenSearchInChat) => {
|
||
// Открыть поиск в чате
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::OpenProfile) => {
|
||
// Открыть профиль
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
_ => KeyResult::NotHandled,
|
||
}
|
||
}
|
||
|
||
fn priority(&self) -> i32 {
|
||
10 // Средний приоритет
|
||
}
|
||
}
|
||
|
||
/// Обработчик для режима выбора сообщения
|
||
pub struct MessageSelectionKeyHandler;
|
||
|
||
impl KeyHandler for MessageSelectionKeyHandler {
|
||
fn handle_key(
|
||
&self,
|
||
_app: &mut App,
|
||
_key: KeyEvent,
|
||
command: Option<Command>,
|
||
) -> KeyResult {
|
||
match command {
|
||
Some(Command::DeleteMessage) => {
|
||
// Показать модалку подтверждения удаления
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::ReplyMessage) => {
|
||
// Войти в режим ответа
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::ForwardMessage) => {
|
||
// Войти в режим пересылки
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::CopyMessage) => {
|
||
// Скопировать текст в буфер
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::ReactMessage) => {
|
||
// Открыть emoji picker
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
Some(Command::Cancel) => {
|
||
// Выйти из режима выбора
|
||
KeyResult::HandledNeedsRedraw
|
||
}
|
||
|
||
_ => KeyResult::NotHandled,
|
||
}
|
||
}
|
||
|
||
fn priority(&self) -> i32 {
|
||
20 // Высокий приоритет - режимы должны обрабатываться первыми
|
||
}
|
||
}
|
||
|
||
/// Цепочка обработчиков клавиш
|
||
///
|
||
/// Позволяет комбинировать несколько обработчиков в порядке приоритета.
|
||
pub struct KeyHandlerChain {
|
||
handlers: Vec<(i32, Box<dyn KeyHandler>)>,
|
||
}
|
||
|
||
impl KeyHandlerChain {
|
||
/// Создаёт новую цепочку
|
||
pub fn new() -> Self {
|
||
Self {
|
||
handlers: Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Добавляет обработчик в цепочку
|
||
pub fn add<H: KeyHandler + 'static>(mut self, handler: H) -> Self {
|
||
let priority = handler.priority();
|
||
self.handlers.push((priority, Box::new(handler)));
|
||
// Сортируем по убыванию приоритета
|
||
self.handlers.sort_by(|a, b| b.0.cmp(&a.0));
|
||
self
|
||
}
|
||
|
||
/// Обрабатывает клавишу, вызывая обработчики по порядку
|
||
///
|
||
/// Останавливается на первом обработчике, который вернул Handled/HandledNeedsRedraw/Quit
|
||
pub fn handle(
|
||
&self,
|
||
app: &mut App,
|
||
key: KeyEvent,
|
||
command: Option<Command>,
|
||
) -> KeyResult {
|
||
for (_priority, handler) in &self.handlers {
|
||
let result = handler.handle_key(app, key, command);
|
||
if result != KeyResult::NotHandled {
|
||
return result;
|
||
}
|
||
}
|
||
KeyResult::NotHandled
|
||
}
|
||
}
|
||
|
||
impl Default for KeyHandlerChain {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crossterm::event::KeyCode;
|
||
|
||
#[test]
|
||
fn test_key_result_needs_redraw() {
|
||
assert!(!KeyResult::Handled.needs_redraw());
|
||
assert!(KeyResult::HandledNeedsRedraw.needs_redraw());
|
||
assert!(!KeyResult::NotHandled.needs_redraw());
|
||
assert!(!KeyResult::Quit.needs_redraw());
|
||
}
|
||
|
||
#[test]
|
||
fn test_key_result_should_quit() {
|
||
assert!(!KeyResult::Handled.should_quit());
|
||
assert!(!KeyResult::HandledNeedsRedraw.should_quit());
|
||
assert!(!KeyResult::NotHandled.should_quit());
|
||
assert!(KeyResult::Quit.should_quit());
|
||
}
|
||
|
||
// TODO: Enable these tests after App trait integration
|
||
// #[test]
|
||
// fn test_global_handler_quit() {
|
||
// let handler = GlobalKeyHandler;
|
||
// let mut app = App::new_for_test();
|
||
//
|
||
// let result = handler.handle_key(
|
||
// &mut app,
|
||
// KeyEvent::from(KeyCode::Char('q')),
|
||
// Some(Command::Quit),
|
||
// );
|
||
//
|
||
// assert_eq!(result, KeyResult::Quit);
|
||
// }
|
||
|
||
// #[test]
|
||
// fn test_chat_list_handler_navigation() {
|
||
// let handler = ChatListKeyHandler;
|
||
// let mut app = App::new_for_test();
|
||
//
|
||
// // Test move up (should be handled even at top)
|
||
// let result = handler.handle_key(
|
||
// &mut app,
|
||
// KeyEvent::from(KeyCode::Up),
|
||
// Some(Command::MoveUp),
|
||
// );
|
||
//
|
||
// assert_eq!(result, KeyResult::Handled);
|
||
// }
|
||
|
||
// #[test]
|
||
// fn test_handler_chain() {
|
||
// let chain = KeyHandlerChain::new()
|
||
// .add(ChatListKeyHandler)
|
||
// .add(GlobalKeyHandler);
|
||
//
|
||
// let mut app = App::new_for_test();
|
||
//
|
||
// // ChatListHandler should handle MoveUp first
|
||
// let result = chain.handle(
|
||
// &mut app,
|
||
// KeyEvent::from(KeyCode::Up),
|
||
// Some(Command::MoveUp),
|
||
// );
|
||
//
|
||
// assert_eq!(result, KeyResult::Handled);
|
||
//
|
||
// // GlobalHandler should handle Quit
|
||
// let result = chain.handle(
|
||
// &mut app,
|
||
// KeyEvent::from(KeyCode::Char('q')),
|
||
// Some(Command::Quit),
|
||
// );
|
||
//
|
||
// assert_eq!(result, KeyResult::Quit);
|
||
// }
|
||
|
||
#[test]
|
||
fn test_handler_priority() {
|
||
let handler1 = ChatListKeyHandler;
|
||
let handler2 = MessageSelectionKeyHandler;
|
||
let handler3 = GlobalKeyHandler;
|
||
|
||
assert_eq!(handler1.priority(), 10);
|
||
assert_eq!(handler2.priority(), 20);
|
||
assert_eq!(handler3.priority(), -100);
|
||
|
||
// В цепочке должны быть отсортированы: MessageSelection > ChatList > Global
|
||
}
|
||
}
|