Files
telegram-tui/src/input/key_handler.rs
Mikhail Kilin bd5e5be618
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
refactor: extract state modules and services from monolithic files
Извлечены 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>
2026-02-04 19:29:25 +03:00

451 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Модуль для обработки клавиш с использованием 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
}
}