Files
telegram-tui/src/input/main_input.rs
Mikhail Kilin 8bd08318bb
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
fixes
2026-02-14 17:57:37 +03:00

335 lines
11 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.
//! 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::InputMode;
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_account_switcher,
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 в 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());
}