refactor: prepare handlers structure for future input refactoring
Preparation for splitting large input file (#2): - Created src/input/handlers/ structure (7 modules) - clipboard.rs (~100 lines) - clipboard operations extracted - global.rs (~90 lines) - global commands (Ctrl+R/S/P/F) extracted - Stubs: profile.rs, search.rs, modal.rs, messages.rs, chat_list.rs - main_input.rs remains monolithic (1139 lines) - Attempted full migration broke navigation - rolled back - Handlers remain as preparation for gradual migration Updated documentation: - REFACTORING_OPPORTUNITIES.md: #2.1 status updated - CONTEXT.md: Added lesson about careful refactoring Lesson learned: Critical input logic requires careful step-by-step refactoring with functionality verification after each step. Tests: 563 passed, 0 failed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
23
CONTEXT.md
23
CONTEXT.md
@@ -332,6 +332,29 @@ reaction_other = "gray"
|
|||||||
|
|
||||||
## Последние обновления (2026-02-01)
|
## Последние обновления (2026-02-01)
|
||||||
|
|
||||||
|
### Рефакторинг — Подготовка к разделению больших файлов (#2) ⏳ (2026-02-01)
|
||||||
|
|
||||||
|
**Что сделано**:
|
||||||
|
- ✅ Создана модульная структура `src/input/handlers/` (подготовка):
|
||||||
|
- `clipboard.rs` (~100 строк) - извлечены операции с буфером обмена
|
||||||
|
- `global.rs` (~90 строк) - извлечены глобальные команды (Ctrl+R/S/P/F)
|
||||||
|
- Заглушки: `profile.rs`, `search.rs`, `modal.rs`, `messages.rs`, `chat_list.rs`
|
||||||
|
- ⏳ `main_input.rs` остаётся монолитным (1139 строк)
|
||||||
|
- Попытка полной миграции привела к поломке навигации - откачено
|
||||||
|
- Handlers остаются как подготовка к постепенной миграции
|
||||||
|
|
||||||
|
**Статус Большие файлы (#2.1)**: ⏳ Подготовка (2/7)
|
||||||
|
- ✅ Структура handlers создана
|
||||||
|
- ✅ clipboard.rs извлечён (не используется, подготовка)
|
||||||
|
- ✅ global.rs извлечён (не используется, подготовка)
|
||||||
|
- ⏳ Требуется постепенная миграция с тщательным тестированием
|
||||||
|
|
||||||
|
**Урок**: Критичная логика ввода требует осторожного рефакторинга с проверкой функциональности после каждого шага.
|
||||||
|
|
||||||
|
**Все тесты проходят**: 563 passed; 0 failed ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Рефакторинг — Быстрые победы (Вариант 1) ✅ (2026-02-01)
|
### Рефакторинг — Быстрые победы (Вариант 1) ✅ (2026-02-01)
|
||||||
|
|
||||||
**Что сделано**:
|
**Что сделано**:
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
## 2. Большие файлы/функции
|
## 2. Большие файлы/функции
|
||||||
|
|
||||||
**Приоритет:** 🔴 Высокий
|
**Приоритет:** 🔴 Высокий
|
||||||
**Статус:** ❌ Не начато
|
**Статус:** ✅ Частично выполнено (2026-02-01)
|
||||||
**Объем:** 4 файла, 1000+ строк каждый
|
**Объем:** 4 файла, 1000+ строк каждый
|
||||||
|
|
||||||
### Проблемы
|
### Проблемы
|
||||||
@@ -83,14 +83,15 @@
|
|||||||
|
|
||||||
### Решение
|
### Решение
|
||||||
|
|
||||||
#### 2.1. Разделить `src/input/main_input.rs`
|
#### 2.1. Разделить `src/input/main_input.rs` - ⏳ В процессе (2026-02-01)
|
||||||
|
|
||||||
- [ ] Создать `src/input/handlers/chat_list_input.rs`
|
- [x] Создана структура `src/input/handlers/` (7 модулей) - ПОДГОТОВКА
|
||||||
- [ ] Создать `src/input/handlers/messages_input.rs`
|
- [x] Создан `handlers/clipboard.rs` (~100 строк) - извлечён из main_input
|
||||||
- [ ] Создать `src/input/handlers/compose_input.rs`
|
- [x] Создан `handlers/global.rs` (~90 строк) - извлечён из main_input
|
||||||
- [ ] Создать `src/input/handlers/search_input.rs`
|
- [x] Созданы заглушки: `profile.rs`, `search.rs`, `modal.rs`, `messages.rs`, `chat_list.rs`
|
||||||
- [ ] Создать `src/input/handlers/modal_input.rs`
|
- [ ] Постепенно мигрировать логику в handlers (требуется тщательное тестирование)
|
||||||
- [ ] Главный `handle()` делегирует по screen state
|
|
||||||
|
**Примечание**: Попытка полного переноса была откачена из-за поломки навигации. Handlers остаются как подготовка к будущей миграции. Текущий подход: извлекать независимые модули (clipboard, global), не трогая критичную логику ввода.
|
||||||
|
|
||||||
#### 2.2. Разделить `src/tdlib/client.rs`
|
#### 2.2. Разделить `src/tdlib/client.rs`
|
||||||
|
|
||||||
|
|||||||
10
src/input/handlers/chat_list.rs
Normal file
10
src/input/handlers/chat_list.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//! Chat list navigation input handling
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
|
||||||
|
/// Обрабатывает ввод в списке чатов
|
||||||
|
pub async fn handle_chat_list_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement chat list input handling
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
101
src/input/handlers/clipboard.rs
Normal file
101
src/input/handlers/clipboard.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
//! Clipboard operations for copying messages
|
||||||
|
|
||||||
|
use crate::tdlib::MessageInfo;
|
||||||
|
|
||||||
|
/// Копирует текст в системный буфер обмена
|
||||||
|
#[cfg(feature = "clipboard")]
|
||||||
|
pub fn copy_to_clipboard(text: &str) -> Result<(), String> {
|
||||||
|
use arboard::Clipboard;
|
||||||
|
|
||||||
|
let mut clipboard =
|
||||||
|
Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?;
|
||||||
|
clipboard
|
||||||
|
.set_text(text)
|
||||||
|
.map_err(|e| format!("Не удалось скопировать: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Заглушка для copy_to_clipboard когда feature "clipboard" выключена
|
||||||
|
#[cfg(not(feature = "clipboard"))]
|
||||||
|
pub fn copy_to_clipboard(_text: &str) -> Result<(), String> {
|
||||||
|
Err("Копирование в буфер обмена недоступно (требуется feature 'clipboard')".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Форматирует сообщение для копирования с контекстом
|
||||||
|
pub fn format_message_for_clipboard(msg: &MessageInfo) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
|
||||||
|
// Добавляем forward контекст если есть
|
||||||
|
if let Some(forward) = msg.forward_from() {
|
||||||
|
result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем reply контекст если есть
|
||||||
|
if let Some(reply) = msg.reply_to() {
|
||||||
|
result.push_str(&format!("┌ {}: {}\n", reply.sender_name, reply.text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем основной текст с markdown форматированием
|
||||||
|
result.push_str(&convert_entities_to_markdown(msg.text(), msg.entities()));
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Конвертирует текст с entities в markdown
|
||||||
|
fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String {
|
||||||
|
use tdlib_rs::enums::TextEntityType;
|
||||||
|
|
||||||
|
if entities.is_empty() {
|
||||||
|
return text.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём вектор символов для работы с unicode
|
||||||
|
let chars: Vec<char> = text.chars().collect();
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < chars.len() {
|
||||||
|
// Ищем entity, который начинается в текущей позиции
|
||||||
|
let mut entity_found = false;
|
||||||
|
|
||||||
|
for entity in entities {
|
||||||
|
if entity.offset as usize == i {
|
||||||
|
entity_found = true;
|
||||||
|
let end = (entity.offset + entity.length) as usize;
|
||||||
|
let entity_text: String = chars[i..end.min(chars.len())].iter().collect();
|
||||||
|
|
||||||
|
// Применяем форматирование в зависимости от типа
|
||||||
|
let formatted = match &entity.r#type {
|
||||||
|
TextEntityType::Bold => format!("**{}**", entity_text),
|
||||||
|
TextEntityType::Italic => format!("*{}*", entity_text),
|
||||||
|
TextEntityType::Underline => format!("__{}__", entity_text),
|
||||||
|
TextEntityType::Strikethrough => format!("~~{}~~", entity_text),
|
||||||
|
TextEntityType::Code | TextEntityType::Pre | TextEntityType::PreCode(_) => {
|
||||||
|
format!("`{}`", entity_text)
|
||||||
|
}
|
||||||
|
TextEntityType::TextUrl(url_info) => {
|
||||||
|
format!("[{}]({})", entity_text, url_info.url)
|
||||||
|
}
|
||||||
|
TextEntityType::Url => format!("<{}>", entity_text),
|
||||||
|
TextEntityType::Mention | TextEntityType::MentionName(_) => {
|
||||||
|
format!("@{}", entity_text.trim_start_matches('@'))
|
||||||
|
}
|
||||||
|
TextEntityType::Spoiler => format!("||{}||", entity_text),
|
||||||
|
_ => entity_text,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push_str(&formatted);
|
||||||
|
i = end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !entity_found {
|
||||||
|
result.push(chars[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
85
src/input/handlers/global.rs
Normal file
85
src/input/handlers/global.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
//! Global commands that work from any screen
|
||||||
|
//!
|
||||||
|
//! Handles Ctrl+ combinations:
|
||||||
|
//! - Ctrl+R: Refresh chats
|
||||||
|
//! - Ctrl+S: Start search
|
||||||
|
//! - Ctrl+P: View pinned messages
|
||||||
|
//! - Ctrl+F: Search messages in chat
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::types::ChatId;
|
||||||
|
use crate::utils::{with_timeout, with_timeout_msg};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Обрабатывает глобальные команды (Ctrl+ combinations).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// `true` если команда была обработана, `false` если нет
|
||||||
|
pub async fn handle_global_commands(app: &mut App, key: KeyEvent) -> bool {
|
||||||
|
let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('r') if has_ctrl => {
|
||||||
|
// Ctrl+R - обновить список чатов
|
||||||
|
app.status_message = Some("Обновление чатов...".to_string());
|
||||||
|
let _ = with_timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await;
|
||||||
|
app.status_message = None;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyCode::Char('s') if has_ctrl => {
|
||||||
|
// Ctrl+S - начать поиск (только если чат не открыт)
|
||||||
|
if app.selected_chat_id.is_none() {
|
||||||
|
app.start_search();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyCode::Char('p') if has_ctrl => {
|
||||||
|
// Ctrl+P - режим просмотра закреплённых сообщений
|
||||||
|
handle_pinned_messages(app).await;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyCode::Char('f') if has_ctrl => {
|
||||||
|
// Ctrl+F - поиск по сообщениям в открытом чате
|
||||||
|
if app.selected_chat_id.is_some()
|
||||||
|
&& !app.is_pinned_mode()
|
||||||
|
&& !app.is_message_search_mode()
|
||||||
|
{
|
||||||
|
app.enter_message_search_mode();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обрабатывает загрузку и отображение закреплённых сообщений
|
||||||
|
async fn handle_pinned_messages(app: &mut App) {
|
||||||
|
if app.selected_chat_id.is_some() && !app.is_pinned_mode() {
|
||||||
|
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||||
|
app.status_message = Some("Загрузка закреплённых...".to_string());
|
||||||
|
match with_timeout_msg(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
app.td_client.get_pinned_messages(ChatId::new(chat_id)),
|
||||||
|
"Таймаут загрузки",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(messages) => {
|
||||||
|
let messages: Vec<crate::tdlib::MessageInfo> = messages;
|
||||||
|
if messages.is_empty() {
|
||||||
|
app.status_message = Some("Нет закреплённых сообщений".to_string());
|
||||||
|
} else {
|
||||||
|
app.enter_pinned_mode(messages);
|
||||||
|
app.status_message = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
app.error_message = Some(e);
|
||||||
|
app.status_message = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/input/handlers/messages.rs
Normal file
10
src/input/handlers/messages.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//! Message input handling when chat is open
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
|
||||||
|
/// Обрабатывает ввод когда открыт чат
|
||||||
|
pub async fn handle_messages_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement messages input handling
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
26
src/input/handlers/mod.rs
Normal file
26
src/input/handlers/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//! Input handlers organized by screen/mode
|
||||||
|
//!
|
||||||
|
//! This module contains handlers for different input contexts:
|
||||||
|
//! - global: Global commands (Ctrl+R, Ctrl+S, etc.)
|
||||||
|
//! - profile: Profile mode input
|
||||||
|
//! - search: Search modes (chat search, message search)
|
||||||
|
//! - modal: Modal modes (pinned, reactions, delete, forward)
|
||||||
|
//! - messages: Message input when chat is open
|
||||||
|
//! - chat_list: Chat list navigation
|
||||||
|
//! - clipboard: Clipboard operations
|
||||||
|
|
||||||
|
pub mod chat_list;
|
||||||
|
pub mod clipboard;
|
||||||
|
pub mod global;
|
||||||
|
pub mod messages;
|
||||||
|
pub mod modal;
|
||||||
|
pub mod profile;
|
||||||
|
pub mod search;
|
||||||
|
|
||||||
|
pub use chat_list::*;
|
||||||
|
pub use clipboard::*;
|
||||||
|
pub use global::*;
|
||||||
|
pub use messages::*;
|
||||||
|
pub use modal::*;
|
||||||
|
pub use profile::*;
|
||||||
|
pub use search::*;
|
||||||
34
src/input/handlers/modal.rs
Normal file
34
src/input/handlers/modal.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//! Modal mode input handling
|
||||||
|
//!
|
||||||
|
//! Handles input for modal states:
|
||||||
|
//! - Pinned messages view
|
||||||
|
//! - Reaction picker
|
||||||
|
//! - Delete confirmation
|
||||||
|
//! - Forward mode
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
|
||||||
|
/// Обрабатывает ввод в режиме закреплённых сообщений
|
||||||
|
pub async fn handle_pinned_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement pinned messages input handling
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обрабатывает ввод в режиме выбора реакции
|
||||||
|
pub async fn handle_reaction_picker_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement reaction picker input handling
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обрабатывает ввод в режиме подтверждения удаления
|
||||||
|
pub async fn handle_delete_confirmation_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement delete confirmation input handling
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обрабатывает ввод в режиме пересылки
|
||||||
|
pub async fn handle_forward_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement forward mode input handling
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
31
src/input/handlers/profile.rs
Normal file
31
src/input/handlers/profile.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
//! Profile mode input handling
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
|
||||||
|
/// Обрабатывает ввод в режиме профиля
|
||||||
|
pub async fn handle_profile_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement profile input handling
|
||||||
|
// Временно делегируем обратно в main_input
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Возвращает количество доступных действий в профиле
|
||||||
|
pub fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Всегда есть: назад, посмотреть фото
|
||||||
|
count += 2;
|
||||||
|
|
||||||
|
// Уведомления (только для групп)
|
||||||
|
if profile.is_group {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выход из группы (только для групп)
|
||||||
|
if profile.is_group {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
16
src/input/handlers/search.rs
Normal file
16
src/input/handlers/search.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//! Search mode input handling (chat search and message search)
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
|
||||||
|
/// Обрабатывает ввод в режиме поиска чатов
|
||||||
|
pub async fn handle_chat_search_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement chat search input handling
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Обрабатывает ввод в режиме поиска сообщений
|
||||||
|
pub async fn handle_message_search_input(app: &mut App, key: KeyEvent) {
|
||||||
|
// TODO: Implement message search input handling
|
||||||
|
let _ = (app, key);
|
||||||
|
}
|
||||||
1139
src/input/main_input.rs.backup
Normal file
1139
src/input/main_input.rs.backup
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
mod auth;
|
mod auth;
|
||||||
|
pub mod handlers;
|
||||||
mod main_input;
|
mod main_input;
|
||||||
|
|
||||||
pub use auth::handle as handle_auth_input;
|
pub use auth::handle as handle_auth_input;
|
||||||
|
|||||||
Reference in New Issue
Block a user