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
328 lines
11 KiB
Rust
328 lines
11 KiB
Rust
//! 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::methods::{
|
||
compose::ComposeMethods, messages::MessageMethods, modal::ModalMethods,
|
||
navigation::NavigationMethods, search::SearchMethods,
|
||
};
|
||
use crate::app::App;
|
||
use crate::app::InputMode;
|
||
use crate::input::handlers::{
|
||
chat::{handle_enter_key, handle_message_selection, handle_open_chat_keyboard_input},
|
||
chat_list::handle_chat_list_navigation,
|
||
compose::handle_forward_mode,
|
||
handle_global_commands,
|
||
modal::{
|
||
handle_account_switcher, handle_delete_confirmation, handle_pinned_mode,
|
||
handle_profile_mode, handle_profile_open, handle_reaction_picker_mode,
|
||
},
|
||
search::{handle_chat_search_mode, handle_message_search_mode},
|
||
};
|
||
use crate::tdlib::TdClientTrait;
|
||
use crossterm::event::KeyEvent;
|
||
|
||
/// Обработка клавиши Esc в Normal mode
|
||
///
|
||
/// Закрывает чат с сохранением черновика
|
||
async fn handle_escape_normal<T: TdClientTrait>(app: &mut App<T>) {
|
||
// Закрываем модальное окно изображения если открыто
|
||
#[cfg(feature = "images")]
|
||
if app.image_modal.is_some() {
|
||
app.image_modal = None;
|
||
app.needs_redraw = true;
|
||
return;
|
||
}
|
||
|
||
// Закрытие чата с сохранением черновика
|
||
let Some(chat_id) = app.selected_chat_id else {
|
||
return;
|
||
};
|
||
|
||
// Сохраняем черновик если есть текст в инпуте
|
||
if !app.message_input.is_empty() {
|
||
let draft_text = app.message_input.clone();
|
||
let _ = app.td_client.set_draft_message(chat_id, draft_text).await;
|
||
} else {
|
||
// Очищаем черновик если инпут пустой
|
||
let _ = app
|
||
.td_client
|
||
.set_draft_message(chat_id, String::new())
|
||
.await;
|
||
}
|
||
|
||
app.close_chat();
|
||
}
|
||
|
||
/// Обработка клавиши Esc в Insert mode
|
||
///
|
||
/// Отменяет Reply/Editing и возвращает в Normal + MessageSelection
|
||
fn handle_escape_insert<T: TdClientTrait>(app: &mut App<T>) {
|
||
if app.is_editing() {
|
||
app.cancel_editing();
|
||
}
|
||
if app.is_replying() {
|
||
app.cancel_reply();
|
||
}
|
||
app.input_mode = InputMode::Normal;
|
||
app.start_message_selection();
|
||
}
|
||
|
||
/// Главный обработчик ввода - роутер для всех режимов приложения
|
||
pub async fn handle<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||
let command = app.get_command(key);
|
||
|
||
// 0. Account switcher (глобальный оверлей — highest priority)
|
||
if app.account_switcher.is_some() {
|
||
handle_account_switcher(app, key, command).await;
|
||
return;
|
||
}
|
||
|
||
// 1. Insert mode + чат открыт → только текст, Enter, Esc
|
||
// (Ctrl+C обрабатывается в main.rs до вызова router)
|
||
if app.selected_chat_id.is_some() && app.input_mode == InputMode::Insert {
|
||
// Модальные окна всё равно обрабатываем (image modal, delete confirmation etc.)
|
||
#[cfg(feature = "images")]
|
||
if app.image_modal.is_some() {
|
||
handle_image_modal_mode(app, key).await;
|
||
return;
|
||
}
|
||
if app.is_confirm_delete_shown() {
|
||
handle_delete_confirmation(app, key).await;
|
||
return;
|
||
}
|
||
if app.is_reaction_picker_mode() {
|
||
handle_reaction_picker_mode(app, key, command).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_forwarding() {
|
||
handle_forward_mode(app, key, command).await;
|
||
return;
|
||
}
|
||
|
||
match command {
|
||
Some(crate::config::Command::Cancel) => {
|
||
handle_escape_insert(app);
|
||
return;
|
||
}
|
||
Some(crate::config::Command::SubmitMessage) => {
|
||
handle_enter_key(app).await;
|
||
return;
|
||
}
|
||
Some(crate::config::Command::DeleteWord) => {
|
||
// Ctrl+W → удалить слово
|
||
if app.cursor_position > 0 {
|
||
let chars: Vec<char> = app.message_input.chars().collect();
|
||
let mut new_pos = app.cursor_position;
|
||
// Пропускаем пробелы
|
||
while new_pos > 0 && chars[new_pos - 1] == ' ' {
|
||
new_pos -= 1;
|
||
}
|
||
// Пропускаем слово
|
||
while new_pos > 0 && chars[new_pos - 1] != ' ' {
|
||
new_pos -= 1;
|
||
}
|
||
let new_input: String = chars[..new_pos]
|
||
.iter()
|
||
.chain(chars[app.cursor_position..].iter())
|
||
.collect();
|
||
app.message_input = new_input;
|
||
app.cursor_position = new_pos;
|
||
}
|
||
return;
|
||
}
|
||
Some(crate::config::Command::MoveToStart) => {
|
||
app.cursor_position = 0;
|
||
return;
|
||
}
|
||
Some(crate::config::Command::MoveToEnd) => {
|
||
app.cursor_position = app.message_input.chars().count();
|
||
return;
|
||
}
|
||
_ => {}
|
||
}
|
||
// Весь остальной ввод → текст
|
||
handle_open_chat_keyboard_input(app, key).await;
|
||
return;
|
||
}
|
||
|
||
// 3. Глобальные команды (Ctrl+R, Ctrl+S, Ctrl+P, Ctrl+F)
|
||
if handle_global_commands(app, key).await {
|
||
return;
|
||
}
|
||
|
||
// 4. Модальное окно просмотра изображения
|
||
#[cfg(feature = "images")]
|
||
if app.image_modal.is_some() {
|
||
handle_image_modal_mode(app, key).await;
|
||
return;
|
||
}
|
||
|
||
// 5. Режим профиля
|
||
if app.is_profile_mode() {
|
||
handle_profile_mode(app, key, command).await;
|
||
return;
|
||
}
|
||
|
||
// 6. Режим поиска по сообщениям
|
||
if app.is_message_search_mode() {
|
||
handle_message_search_mode(app, key, command).await;
|
||
return;
|
||
}
|
||
|
||
// 7. Режим просмотра закреплённых сообщений
|
||
if app.is_pinned_mode() {
|
||
handle_pinned_mode(app, key, command).await;
|
||
return;
|
||
}
|
||
|
||
// 8. Обработка ввода в режиме выбора реакции
|
||
if app.is_reaction_picker_mode() {
|
||
handle_reaction_picker_mode(app, key, command).await;
|
||
return;
|
||
}
|
||
|
||
// 9. Модалка подтверждения удаления
|
||
if app.is_confirm_delete_shown() {
|
||
handle_delete_confirmation(app, key).await;
|
||
return;
|
||
}
|
||
|
||
// 10. Режим выбора чата для пересылки
|
||
if app.is_forwarding() {
|
||
handle_forward_mode(app, key, command).await;
|
||
return;
|
||
}
|
||
|
||
// 11. Режим поиска чатов
|
||
if app.is_searching {
|
||
handle_chat_search_mode(app, key, command).await;
|
||
return;
|
||
}
|
||
|
||
// 12. Normal mode commands (Enter, Esc, Profile)
|
||
match command {
|
||
Some(crate::config::Command::SubmitMessage) => {
|
||
handle_enter_key(app).await;
|
||
return;
|
||
}
|
||
Some(crate::config::Command::Cancel) => {
|
||
handle_escape_normal(app).await;
|
||
return;
|
||
}
|
||
Some(crate::config::Command::OpenProfile) => {
|
||
if app.selected_chat_id.is_some() {
|
||
handle_profile_open(app).await;
|
||
return;
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
|
||
// 13. Normal mode в чате → MessageSelection
|
||
if app.selected_chat_id.is_some() {
|
||
// Auto-enter MessageSelection if not already in it
|
||
if !app.is_selecting_message() {
|
||
app.start_message_selection();
|
||
}
|
||
handle_message_selection(app, key, command).await;
|
||
} else {
|
||
// 14. Список чатов
|
||
handle_chat_list_navigation(app, key, command).await;
|
||
}
|
||
}
|
||
|
||
/// Обработка модального окна просмотра изображения
|
||
///
|
||
/// Hotkeys:
|
||
/// - Esc/q: закрыть модальное окно
|
||
/// - ←: предыдущее фото в чате
|
||
/// - →: следующее фото в чате
|
||
#[cfg(feature = "images")]
|
||
async fn handle_image_modal_mode<T: TdClientTrait>(app: &mut App<T>, 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<T: TdClientTrait>(app: &mut App<T>, 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<dyn Iterator<Item = usize>> = 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());
|
||
}
|