Files
telegram-tui/REFACTORING_ROADMAP.md
Mikhail Kilin 7081a886ad refactor: implement newtype pattern for IDs (P2.4)
Добавлены типобезопасные обёртки ChatId, MessageId, UserId для предотвращения
смешивания разных типов идентификаторов на этапе компиляции.

Изменения:
- Создан src/types.rs с тремя newtype структурами
- Реализованы методы: new(), as_i64(), From<i64>, Display
- Добавлены traits: Hash, Eq, Serialize, Deserialize
- Обновлены 15+ модулей для использования новых типов:
  * tdlib: types.rs, chats.rs, messages.rs, users.rs, reactions.rs, client.rs
  * app: mod.rs, chat_state.rs
  * input: main_input.rs
  * tests: app_builder.rs, test_data.rs
- Исправлены 53 ошибки компиляции связанные с type conversions

Преимущества:
- Компилятор предотвращает смешивание разных типов ID
- Улучшенная читаемость кода (явные типы вместо i64)
- Самодокументирующиеся типы

Статус: Priority 2 теперь 60% (3/5 задач)
-  Error enum
-  Config validation
-  Newtype для ID
-  MessageInfo реструктуризация
-  MessageBuilder pattern

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-31 01:33:18 +03:00

20 KiB
Raw Blame History

Refactoring Roadmap

Этот документ содержит список технического долга и планов по рефакторингу кодовой базы.

Приоритет 1: Критичные улучшения

1. Схлопнуть состояния чата в enum

Проблема: Сейчас состояния чата хранятся как отдельные boolean поля в App:

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:

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<String>,
        selected_index: usize,
    },
    Profile {
        info: ProfileInfo,
    },
    SearchInChat {
        query: String,
        results: Vec<i64>,
        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. Вынести константы в отдельный модуль

Проблема: Магические числа разбросаны по всему коду:

// В разных местах:
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:

// 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 ЗАВЕРШЕНО!

Статус: ЗАВЕРШЕНО (2026-01-31)

Проблема: Везде используется i64 для chat_id, message_id, user_id — легко перепутать.

Решение: Реализовано в src/types.rs:

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChatId(pub i64);

impl ChatId {
    pub fn new(id: i64) -> Self { Self(id) }
    pub fn as_i64(&self) -> i64 { self.0 }
}

impl From<i64> for ChatId {
    fn from(id: i64) -> Self { ChatId(id) }
}

impl Display for ChatId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

// Аналогично для MessageId и UserId

Что сделано:

  • Создан src/types.rs с тремя типами: ChatId, MessageId, UserId
  • Добавлены методы new(), as_i64(), From<i64>, Display
  • Реализованы traits: Hash, Eq, Serialize, Deserialize
  • Обновлены 15+ модулей:
    • tdlib/types.rs, tdlib/chats.rs, tdlib/messages.rs, tdlib/users.rs
    • tdlib/reactions.rs, tdlib/client.rs
    • app/mod.rs, app/chat_state.rs, input/main_input.rs
    • Test helpers: app_builder.rs, test_data.rs
  • Исправлены 53 ошибки компиляции
  • Код компилируется успешно

Преимущества:

  • Невозможно случайно передать message_id вместо chat_id
  • Компилятор ловит ошибки на этапе компиляции
  • Улучшенная читаемость кода
  • Самодокументирующиеся типы

5. Создать enum для ошибок

Проблема: Везде используется Result<T, String> — теряется контекст ошибок.

Решение: Создать src/error.rs:

#[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<T> = std::result::Result<T, TeletuiError>;

Зависимости: thiserror = "1.0"

Преимущества:

  • Типобезопасная обработка ошибок
  • Понятные сообщения об ошибках
  • Возможность pattern matching

6. Группировка полей MessageInfo

Проблема: MessageInfo имеет слишком много плоских полей (~15+).

Решение: Группировать в логические структуры:

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<FormattedText>,
    pub media_type: Option<String>,
}

pub struct MessageState {
    pub is_outgoing: bool,
    pub is_edited: bool,
    pub is_pinned: bool,
}

pub struct MessageInteractions {
    pub reply_to_message_id: Option<MessageId>,
    pub forward_info: Option<ForwardInfo>,
    pub reactions: Vec<ReactionInfo>,
    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 эмодзи

Каждый компонент — функция:

pub fn render_modal<F>(
    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:

pub struct FormattedSpan {
    pub text: String,
    pub style: Style,
}

pub fn format_text_entities(
    text: &str,
    entities: &[TextEntity],
) -> Vec<FormattedSpan> {
    // Вся логика форматирования
}

Преимущества:

  • Разделение ответственности
  • Можно тестировать отдельно
  • Переиспользование в других местах

9. Вынести логику группировки сообщений

Проблема: Логика группировки сообщений смешана с рендерингом в messages.rs.

Решение: Создать src/message_grouping.rs:

pub enum MessageGroup {
    DateSeparator(String),
    SenderHeader(String),
    Message(MessageInfo),
}

pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup> {
    // Логика группировки по дате и отправителю
}

Преимущества:

  • Чистое разделение логики и представления
  • Легче тестировать группировку
  • Можно переиспользовать

10. Hotkey mapping в конфиг

Проблема: Хоткеи захардкожены в коде, нельзя настроить.

Решение: Добавить в config.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:

pub struct Hotkeys {
    pub up: Vec<char>,
    pub down: Vec<char>,
    // ...
}

impl Hotkeys {
    pub fn matches(&self, key: KeyCode, action: &str) -> bool {
        // Проверка совпадения
    }
}

Преимущества:

  • Пользовательская настройка хоткеев
  • Проще добавлять новые действия
  • Документация хоткеев в конфиге

Приоритет 4: Качество кода

11. Добавить юнит-тесты

Проблема: Нет тестов, сложно убедиться в корректности.

Решение: Добавить тесты для:

// 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-комментарии:

/// 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 валидация

Проблема: Невалидные значения в конфиге молча игнорируются.

Решение: Добавить валидацию:

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(())
    }
}

Вызывать при загрузке:

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:

[features]
default = ["clipboard", "url-open"]
clipboard = ["dep:arboard"]
url-open = ["dep:open"]

Преимущества:

  • Уменьшение размера бинарника
  • Опциональная функциональность

16. LRU cache обобщение

Проблема: Отдельные LRU кеши для user_names и user_statuses.

Решение: Создать обобщённый LruCache<K, V> или использовать готовый крейт lru = "0.12".


17. Tracing вместо println!

Проблема: Используется eprintln! для логов.

Решение: Использовать tracing:

use tracing::{info, warn, error, debug};

// Вместо
eprintln!("Warning: Could not load config: {}", e);

// Использовать
warn!("Could not load config: {}", e);

Добавить в Cargo.toml:

tracing = "0.1"
tracing-subscriber = "0.3"

Метрики прогресса

  • Priority 1: 3/3 задач ЗАВЕРШЕНО!
    • P1.1 — ChatState enum
    • P1.2 — Разделить TdClient
    • P1.3 — Константы
  • Priority 2: 3/5 задач (60%)
    • P2.5 — Error enum
    • P2.3 — Config validation
    • P2.4 — Newtype для ID
    • P2.6 — MessageInfo реструктуризация
    • P2.7 — MessageBuilder pattern
  • Priority 3: 0/4 задач
  • Priority 4: 0/4 задач
  • Priority 5: 0/3 задач

Всего: 6/17 задач (35%)


Предусловие: Тесты

ВАЖНО: Перед началом рефакторинга необходимо написать тесты!

См. 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. Документация — обновлять документацию после изменений

Примечания

  • Этот документ живой и будет обновляться
  • Новые пункты добавляются по мере обнаружения
  • После завершения задачи отмечать в метриках
  • При появлении блокеров — документировать в соответствующей секции