//! Main screen input router. //! //! Dispatches keyboard events to specialized handlers based on current app mode. //! Priority order: modals → search → compose → chat → chat list. use crate::app::App; use crate::app::methods::{ compose::ComposeMethods, messages::MessageMethods, modal::ModalMethods, navigation::NavigationMethods, search::SearchMethods, }; use crate::tdlib::TdClientTrait; use crate::input::handlers::{ handle_global_commands, modal::{ handle_profile_mode, handle_profile_open, handle_delete_confirmation, handle_reaction_picker_mode, handle_pinned_mode, }, search::{handle_chat_search_mode, handle_message_search_mode}, compose::handle_forward_mode, chat_list::handle_chat_list_navigation, chat::{ handle_message_selection, handle_enter_key, handle_open_chat_keyboard_input, }, }; use crossterm::event::KeyEvent; /// Обработка клавиши Esc /// /// Обрабатывает отмену текущего действия или закрытие чата: /// - В режиме выбора сообщения: отменить выбор /// - В режиме редактирования: отменить редактирование /// - В режиме ответа: отменить ответ /// - В открытом чате: сохранить черновик и закрыть чат async fn handle_escape_key(app: &mut App) { // Закрываем модальное окно изображения если открыто #[cfg(feature = "images")] if app.image_modal.is_some() { app.image_modal = None; app.needs_redraw = true; return; } // Early return для режима выбора сообщения if app.is_selecting_message() { app.chat_state = crate::app::ChatState::Normal; return; } // Early return для режима редактирования if app.is_editing() { app.cancel_editing(); return; } // Early return для режима ответа if app.is_replying() { app.cancel_reply(); return; } // Закрытие чата с сохранением черновика let Some(chat_id) = app.selected_chat_id else { return; }; // Сохраняем черновик если есть текст в инпуте if !app.message_input.is_empty() && !app.is_editing() && !app.is_replying() { let draft_text = app.message_input.clone(); let _ = app.td_client.set_draft_message(chat_id, draft_text).await; } else if app.message_input.is_empty() { // Очищаем черновик если инпут пустой let _ = app.td_client.set_draft_message(chat_id, String::new()).await; } app.close_chat(); } /// Главный обработчик ввода - роутер для всех режимов приложения pub async fn handle(app: &mut App, key: KeyEvent) { // Глобальные команды (работают всегда) if handle_global_commands(app, key).await { return; } // Получаем команду из keybindings let command = app.get_command(key); // Модальное окно просмотра изображения (приоритет высокий) #[cfg(feature = "images")] if app.image_modal.is_some() { handle_image_modal_mode(app, key).await; return; } // Режим профиля if app.is_profile_mode() { handle_profile_mode(app, key, command).await; return; } // Режим поиска по сообщениям if app.is_message_search_mode() { handle_message_search_mode(app, key, command).await; return; } // Режим просмотра закреплённых сообщений if app.is_pinned_mode() { handle_pinned_mode(app, key, command).await; return; } // Обработка ввода в режиме выбора реакции if app.is_reaction_picker_mode() { handle_reaction_picker_mode(app, key, command).await; return; } // Модалка подтверждения удаления if app.is_confirm_delete_shown() { handle_delete_confirmation(app, key).await; return; } // Режим выбора чата для пересылки if app.is_forwarding() { handle_forward_mode(app, key, command).await; return; } // Режим поиска if app.is_searching { handle_chat_search_mode(app, key, command).await; return; } // Обработка команд через keybindings match command { Some(crate::config::Command::SubmitMessage) => { // Enter - открыть чат, отправить сообщение или редактировать handle_enter_key(app).await; return; } Some(crate::config::Command::Cancel) => { // Esc - отменить выбор/редактирование/reply или закрыть чат handle_escape_key(app).await; return; } Some(crate::config::Command::OpenProfile) => { // Открыть профиль (обычно 'i') if app.selected_chat_id.is_some() { handle_profile_open(app).await; return; } } _ => {} } // Режим открытого чата if app.selected_chat_id.is_some() { // Режим выбора сообщения для редактирования/удаления if app.is_selecting_message() { handle_message_selection(app, key, command).await; return; } handle_open_chat_keyboard_input(app, key).await; } else { // В режиме списка чатов - навигация стрелками и переключение папок handle_chat_list_navigation(app, key, command).await; } } /// Обработка модального окна просмотра изображения /// /// Hotkeys: /// - Esc/q: закрыть модальное окно /// - ←: предыдущее фото в чате /// - →: следующее фото в чате #[cfg(feature = "images")] async fn handle_image_modal_mode(app: &mut App, key: KeyEvent) { use crossterm::event::KeyCode; match key.code { KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('й') => { // Закрываем модальное окно app.image_modal = None; app.needs_redraw = true; } KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('р') => { // Предыдущее фото в чате navigate_to_adjacent_photo(app, Direction::Previous).await; } KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('д') => { // Следующее фото в чате navigate_to_adjacent_photo(app, Direction::Next).await; } _ => {} } } #[cfg(feature = "images")] enum Direction { Previous, Next, } /// Переключение на соседнее фото в чате #[cfg(feature = "images")] async fn navigate_to_adjacent_photo(app: &mut App, direction: Direction) { use crate::tdlib::PhotoDownloadState; let Some(current_modal) = &app.image_modal else { return; }; let current_msg_id = current_modal.message_id; let messages = app.td_client.current_chat_messages(); // Находим текущее сообщение let Some(current_idx) = messages.iter().position(|m| m.id() == current_msg_id) else { return; }; // Ищем следующее/предыдущее сообщение с фото let search_range: Box> = match direction { Direction::Previous => Box::new((0..current_idx).rev()), Direction::Next => Box::new((current_idx + 1)..messages.len()), }; for idx in search_range { if let Some(photo) = messages[idx].photo_info() { if let PhotoDownloadState::Downloaded(path) = &photo.download_state { // Нашли фото - открываем его app.image_modal = Some(crate::tdlib::ImageModalState { message_id: messages[idx].id(), photo_path: path.clone(), photo_width: photo.width, photo_height: photo.height, }); app.needs_redraw = true; return; } } } // Если не нашли фото - показываем сообщение let msg = match direction { Direction::Previous => "Нет предыдущих фото", Direction::Next => "Нет следующих фото", }; app.status_message = Some(msg.to_string()); }