# Refactoring Roadmap Этот документ содержит список технического долга и планов по рефакторингу кодовой базы. ## Приоритет 1: Критичные улучшения ### 1. Схлопнуть состояния чата в enum **Проблема**: Сейчас состояния чата хранятся как отдельные boolean поля в `App`: ```rust is_message_selection_mode: bool, is_editing_mode: bool, is_reply_mode: bool, is_forward_mode: bool, is_delete_confirmation: bool, is_reaction_picker_mode: bool, is_profile_mode: bool, is_search_in_chat_mode: bool, ``` **Решение**: Создать enum `ChatState`: ```rust enum ChatState { Normal, MessageSelection { selected_message_id: i64, }, Editing { message_id: i64, original_text: String, }, Reply { message_id: i64, preview_text: String, }, Forward { message_id: i64, selected_chat_index: usize, }, DeleteConfirmation { message_id: i64, }, ReactionPicker { message_id: i64, available_reactions: Vec, selected_index: usize, }, Profile { info: ProfileInfo, }, SearchInChat { query: String, results: Vec, current_index: usize, }, } ``` **Преимущества**: - Невозможно иметь несколько состояний одновременно (type-safe) - Проще обрабатывать переходы между состояниями - Меньше полей в `App` - Данные, связанные с состоянием, хранятся вместе с ним **Затронутые файлы**: - `src/app/mod.rs` (добавить enum, убрать boolean поля) - `src/input/main_input.rs` (изменить логику обработки на match) - `src/ui/messages.rs` (изменить рендеринг на match) --- ### 2. Разделить TdClient на несколько модулей **Проблема**: `TdClient` в `src/tdlib/client.rs` (~1500+ строк) делает слишком много: - Авторизация - Управление чатами - Управление сообщениями - Кеширование пользователей - Реакции - Network state **Решение**: Разделить на модули: ``` src/tdlib/ ├── mod.rs # Экспорт публичных типов ├── client.rs # Основной TdClient ├── auth.rs # AuthManager ├── chats.rs # ChatManager ├── messages.rs # MessageManager ├── users.rs # UserCache └── reactions.rs # ReactionManager ``` **Преимущества**: - Принцип единственной ответственности - Проще тестировать отдельные модули - Легче найти и изменить код --- ### 3. Вынести константы в отдельный модуль **Проблема**: Магические числа разбросаны по всему коду: ```rust // В разных местах: 500 // MAX_MESSAGES_IN_CHAT 500 // MAX_USER_CACHE_SIZE 200 // MAX_CHATS 8 // Emoji picker columns 10 // Max input height 16 // Poll timeout (60 FPS) ``` **Решение**: Создать `src/constants.rs`: ```rust // Memory limits pub const MAX_MESSAGES_IN_CHAT: usize = 500; pub const MAX_USER_CACHE_SIZE: usize = 500; pub const MAX_CHATS: usize = 200; pub const MAX_CHAT_USER_IDS: usize = 500; // UI constants pub const EMOJI_PICKER_COLUMNS: usize = 8; pub const EMOJI_PICKER_ROWS: usize = 6; pub const MAX_INPUT_HEIGHT: usize = 10; pub const MIN_TERMINAL_WIDTH: u16 = 80; pub const MIN_TERMINAL_HEIGHT: u16 = 20; // Performance pub const POLL_TIMEOUT_MS: u64 = 16; // 60 FPS pub const SHUTDOWN_TIMEOUT_SECS: u64 = 2; pub const LAZY_LOAD_USERS_PER_TICK: usize = 5; // TDLib pub const TDLIB_CHAT_LIMIT: i32 = 50; pub const TDLIB_MESSAGE_LIMIT: i32 = 50; ``` **Преимущества**: - Единое место для всех констант - Проще изменить значения - Самодокументирующийся код --- ## Приоритет 2: Улучшение типобезопасности ### 4. Newtype pattern для ID **Проблема**: Везде используется `i64` для `chat_id`, `message_id`, `user_id` — легко перепутать. **Решение**: Создать `src/types.rs`: ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ChatId(pub i64); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MessageId(pub i64); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct UserId(pub i64); impl From for ChatId { fn from(id: i64) -> Self { ChatId(id) } } // Аналогично для MessageId и UserId ``` **Преимущества**: - Невозможно случайно передать message_id вместо chat_id - Компилятор поймает ошибки - Улучшенная читаемость --- ### 5. Создать enum для ошибок **Проблема**: Везде используется `Result` — теряется контекст ошибок. **Решение**: Создать `src/error.rs`: ```rust #[derive(Debug, thiserror::Error)] pub enum TeletuiError { #[error("TDLib error: {0}")] TdLib(String), #[error("Configuration error: {0}")] Config(String), #[error("Network error: {0}")] Network(String), #[error("Authentication error: {0}")] Auth(String), #[error("Invalid timezone format: {0}")] InvalidTimezone(String), #[error("Invalid color: {0}")] InvalidColor(String), #[error("IO error: {0}")] Io(#[from] std::io::Error), } pub type Result = std::result::Result; ``` **Зависимости**: `thiserror = "1.0"` **Преимущества**: - Типобезопасная обработка ошибок - Понятные сообщения об ошибках - Возможность pattern matching --- ### 6. Группировка полей MessageInfo **Проблема**: `MessageInfo` имеет слишком много плоских полей (~15+). **Решение**: Группировать в логические структуры: ```rust pub struct MessageInfo { pub metadata: MessageMetadata, pub content: MessageContent, pub state: MessageState, pub interactions: MessageInteractions, } pub struct MessageMetadata { pub id: MessageId, pub chat_id: ChatId, pub sender_id: UserId, pub date: i32, } pub struct MessageContent { pub text: String, pub formatted_text: Option, pub media_type: Option, } pub struct MessageState { pub is_outgoing: bool, pub is_edited: bool, pub is_pinned: bool, } pub struct MessageInteractions { pub reply_to_message_id: Option, pub forward_info: Option, pub reactions: Vec, pub read_count: i32, } ``` **Преимущества**: - Логическая группировка данных - Проще добавлять новые поля - Меньше параметров в конструкторах --- ## Приоритет 3: Архитектурные улучшения ### 7. Выделить UI компоненты **Проблема**: Код рендеринга дублируется, сложно переиспользовать. **Решение**: Создать `src/ui/components/`: ``` src/ui/components/ ├── mod.rs ├── modal.rs # Базовый компонент модалки ├── input_field.rs # Поле ввода с курсором ├── message_bubble.rs # Пузырь сообщения ├── chat_list_item.rs # Элемент списка чатов └── emoji_picker.rs # Picker эмодзи ``` Каждый компонент — функция: ```rust pub fn render_modal( frame: &mut Frame, area: Rect, title: &str, render_content: F, ) where F: FnOnce(&mut Frame, Rect), { // Общий код для всех модалок } ``` **Преимущества**: - Переиспользуемые компоненты - Консистентный UI - Проще тестировать --- ### 8. Вынести форматирование в отдельный модуль **Проблема**: Markdown форматирование захардкожено в `messages.rs` (~200+ строк). **Решение**: Создать `src/formatting.rs`: ```rust pub struct FormattedSpan { pub text: String, pub style: Style, } pub fn format_text_entities( text: &str, entities: &[TextEntity], ) -> Vec { // Вся логика форматирования } ``` **Преимущества**: - Разделение ответственности - Можно тестировать отдельно - Переиспользование в других местах --- ### 9. Вынести логику группировки сообщений **Проблема**: Логика группировки сообщений смешана с рендерингом в `messages.rs`. **Решение**: Создать `src/message_grouping.rs`: ```rust pub enum MessageGroup { DateSeparator(String), SenderHeader(String), Message(MessageInfo), } pub fn group_messages(messages: &[MessageInfo]) -> Vec { // Логика группировки по дате и отправителю } ``` **Преимущества**: - Чистое разделение логики и представления - Легче тестировать группировку - Можно переиспользовать --- ### 10. Hotkey mapping в конфиг **Проблема**: Хоткеи захардкожены в коде, нельзя настроить. **Решение**: Добавить в `config.toml`: ```toml [hotkeys] # Навигация up = ["k", "р", "Up"] down = ["j", "о", "Down"] left = ["h", "р", "Left"] right = ["l", "д", "Right"] # Действия reply = ["r", "к"] forward = ["f", "а"] delete = ["d", "в", "Delete"] copy = ["y", "н"] react = ["e", "у"] ``` Парсить в `src/config.rs`: ```rust pub struct Hotkeys { pub up: Vec, pub down: Vec, // ... } impl Hotkeys { pub fn matches(&self, key: KeyCode, action: &str) -> bool { // Проверка совпадения } } ``` **Преимущества**: - Пользовательская настройка хоткеев - Проще добавлять новые действия - Документация хоткеев в конфиге --- ## Приоритет 4: Качество кода ### 11. Добавить юнит-тесты **Проблема**: Нет тестов, сложно убедиться в корректности. **Решение**: Добавить тесты для: ```rust // tests/utils_test.rs #[cfg(test)] mod tests { use super::*; #[test] fn test_format_timestamp_with_tz() { let timestamp = 1640000000; // 2021-12-20 09:33:20 UTC assert_eq!( format_timestamp_with_tz(timestamp, "+03:00"), "12:33" ); } #[test] fn test_parse_timezone_offset() { assert_eq!(parse_timezone_offset("+03:00"), 3); assert_eq!(parse_timezone_offset("-05:00"), -5); assert_eq!(parse_timezone_offset("invalid"), 3); // fallback } } // tests/config_test.rs #[test] fn test_parse_color() { let config = Config::default(); assert_eq!(config.parse_color("red"), Color::Red); assert_eq!(config.parse_color("invalid"), Color::White); // fallback } // tests/grouping_test.rs #[test] fn test_message_grouping_by_date() { // ... } ``` **Запуск**: `cargo test` --- ### 12. Добавить rustdoc комментарии **Проблема**: Публичное API не документировано. **Решение**: Добавить doc-комментарии: ```rust /// TDLib client wrapper for Telegram integration. /// /// Handles authentication, chat management, message operations, /// and user caching. /// /// # Examples /// /// ```no_run /// let mut client = TdClient::new(api_id, api_hash).await?; /// client.start_authorization().await?; /// ``` pub struct TdClient { // ... } /// Loads configuration from ~/.config/tele-tui/config.toml /// /// Creates default config if file doesn't exist. /// /// # Returns /// /// Always returns a valid `Config`, using defaults if loading fails. pub fn load() -> Self { // ... } ``` **Генерация**: `cargo doc --open` --- ### 13. Config валидация **Проблема**: Невалидные значения в конфиге молча игнорируются. **Решение**: Добавить валидацию: ```rust impl Config { pub fn validate(&self) -> Result<(), TeletuiError> { // Проверка timezone if !self.general.timezone.starts_with('+') && !self.general.timezone.starts_with('-') { return Err(TeletuiError::InvalidTimezone( format!("Timezone must start with + or -: {}", self.general.timezone) )); } // Проверка цветов let valid_colors = [ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "gray", "white", "darkgray", "lightred", "lightgreen", "lightyellow", "lightblue", "lightmagenta", "lightcyan" ]; for color_name in [ &self.colors.incoming_message, &self.colors.outgoing_message, &self.colors.selected_message, &self.colors.reaction_chosen, &self.colors.reaction_other, ] { if !valid_colors.contains(&color_name.to_lowercase().as_str()) { return Err(TeletuiError::InvalidColor( format!("Unknown color: {}", color_name) )); } } Ok(()) } } ``` Вызывать при загрузке: ```rust pub fn load() -> Self { let config = // ... загрузка из файла if let Err(e) = config.validate() { eprintln!("Config validation error: {}", e); return Self::default(); } config } ``` --- ### 14. Async/await консистентность **Проблема**: Местами блокирующие вызовы в async контексте. **Решение**: Ревью и исправление: - Использовать `tokio::fs` вместо `std::fs` для файловых операций в async - Использовать `tokio::time::sleep` вместо `std::thread::sleep` - Обернуть блокирующие вызовы в `spawn_blocking` --- ## Приоритет 5: Опциональные улучшения ### 15. Feature flags для зависимостей **Проблема**: Все зависимости всегда включены. **Решение**: В `Cargo.toml`: ```toml [features] default = ["clipboard", "url-open"] clipboard = ["dep:arboard"] url-open = ["dep:open"] ``` **Преимущества**: - Уменьшение размера бинарника - Опциональная функциональность --- ### 16. LRU cache обобщение **Проблема**: Отдельные LRU кеши для `user_names` и `user_statuses`. **Решение**: Создать обобщённый `LruCache` или использовать готовый крейт `lru = "0.12"`. --- ### 17. Tracing вместо println! **Проблема**: Используется `eprintln!` для логов. **Решение**: Использовать `tracing`: ```rust use tracing::{info, warn, error, debug}; // Вместо eprintln!("Warning: Could not load config: {}", e); // Использовать warn!("Could not load config: {}", e); ``` Добавить в `Cargo.toml`: ```toml tracing = "0.1" tracing-subscriber = "0.3" ``` --- ## Метрики прогресса - [ ] Priority 1: 0/3 задач - [ ] Priority 2: 0/3 задач - [ ] Priority 3: 0/4 задач - [ ] Priority 4: 0/4 задач - [ ] Priority 5: 0/3 задач **Всего**: 0/17 задач --- ## Предусловие: Тесты **ВАЖНО**: Перед началом рефакторинга необходимо написать тесты! См. [TESTING_ROADMAP.md](TESTING_ROADMAP.md) для плана покрытия тестами. Минимальное покрытие для начала рефакторинга: - ✅ Фаза 0: Инфраструктура (helpers, fake client) - ✅ Snapshot тесты для основных экранов (chat list, messages) - ✅ Integration тесты для критичных flow (send, edit, navigation) **Зачем**: Тесты гарантируют, что рефакторинг не сломает функциональность. --- ## Порядок выполнения Рекомендуется выполнять в следующем порядке: 1. **P1.3** — Константы (быстро, малый риск) 2. **P1.1** — ChatState enum (высокий impact) 3. **P2.5** — Error enum (улучшает весь код) 4. **P4.11** — Тесты для utils (базовая проверка) 5. **P1.2** — Разделить TdClient (большой рефакторинг) 6. **P2.4** — Newtype для ID (широкие изменения) 7. **P3.7** — UI компоненты (постепенно) 8. **P3.8** — Форматирование (изоляция логики) 9. **P3.9** — Группировка сообщений (изоляция логики) 10. Остальные по необходимости --- ## Принципы рефакторинга 1. **Один PR = одна задача** — не смешивать рефакторинг разных областей 2. **Тесты прежде всего** — добавить тесты перед рефакторингом 3. **Обратная совместимость** — сохранять работоспособность на каждом шаге 4. **Маленькие шаги** — лучше 10 маленьких PR, чем 1 огромный 5. **Документация** — обновлять документацию после изменений --- ## Примечания - Этот документ живой и будет обновляться - Новые пункты добавляются по мере обнаружения - После завершения задачи отмечать в метриках - При появлении блокеров — документировать в соответствующей секции