/// Модуль для обработки клавиш с использованием 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, /// ) -> 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, ) -> 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, ) -> 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, ) -> 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, ) -> 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, ) -> 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)>, } impl KeyHandlerChain { /// Создаёт новую цепочку pub fn new() -> Self { Self { handlers: Vec::new(), } } /// Добавляет обработчик в цепочку pub fn add(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, ) -> 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 } }