docs: clean up project markdown #27

Merged
killingdruid merged 1 commits from refactor into main 2026-05-17 00:58:17 +00:00
12 changed files with 287 additions and 2099 deletions

17
AGENT.md Normal file
View File

@@ -0,0 +1,17 @@
# tele-tui: правила для агента
Проект: TUI-клиент Telegram на Rust.
## Читать перед работой
1. [DEVELOPMENT.md](DEVELOPMENT.md) - обязательные правила локальной работы.
2. [CONTEXT.md](CONTEXT.md) - текущий статус и риски.
3. [docs/PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md) - если нужна навигация по коду.
4. [docs/HOTKEYS.md](docs/HOTKEYS.md) - перед изменением ввода, режимов или keybindings.
## Правила
- Не запускай `cargo run`, `cargo build`, `cargo test`, `cargo check` без прямой команды пользователя.
- Не коммить изменения, пока пользователь не попросит.
- После функциональной правки дай короткий ручной сценарий проверки.
- Обновляй [CONTEXT.md](CONTEXT.md), только если изменились статус, риск, архитектурное решение или следующий шаг.

View File

@@ -1,29 +0,0 @@
# Telegram TUI
## Prompt
Проект - TUI интерфейс для телеграмма
Порядок чтения:
1) DEVELOPMENT.md - правило работы (обязательно)
2) CONTEXT.md - текущий статус
3) ROADMAP.md - план и задачи
4) REQUIREMENTS.md / ARCHITECTURE.md - по необходимости
5) E2E_TESTS.md - перед написанием тестов
После работы обнови CONTEXT.md файл
После прочтения скажи "Жду инструкций"
---
## Важные файлы
- [DEVELOPMENT.md](DEVELOPMENT.md) — **читай первым!** Правила локальной разработки
- [CONTEXT.md](CONTEXT.md) — текущий статус, что сделано
- [ROADMAP.md](ROADMAP.md) — план разработки, задачи по фазам
- [REQUIREMENTS.md](REQUIREMENTS.md) — требования к продукту
- [ARCHITECTURE.md](ARCHITECTURE.md) — C4, sequence diagrams, API контракты, UI прототипы
- [E2E_TESTING.md](E2E_TESTING.md) — **читай перед написанием тестов!** Гайд по e2e тестированию

View File

@@ -1,311 +1,48 @@
# Текущий контекст проекта
## Статус: Фаза 14 — Мультиаккаунт (IN PROGRESS)
### Per-Account Lock File Protection — DONE
Защита от запуска двух экземпляров tele-tui с одним аккаунтом + логирование ошибок TDLib.
**Проблема**: При запуске второго экземпляра с тем же аккаунтом, TDLib не может залочить свою БД. `set_tdlib_parameters` молча падает (`let _ = ...`), и приложение зависает на "Инициализация TDLib...".
**Решение**: Advisory file locks через `fs2` (flock):
- **Lock файл**: `~/.local/share/tele-tui/accounts/{name}/tele-tui.lock`
- **Автоматическое освобождение** при crash/SIGKILL (ядро ОС закрывает file descriptors)
- **При старте**: acquire lock ДО `enable_raw_mode()` → ошибка выводится в обычный терминал
- **При переключении аккаунтов**: acquire new → release old → switch (при ошибке — остаёмся на старом)
- **Логирование**: `set_tdlib_parameters` ошибки теперь логируются через `tracing::error!`
**Новые файлы:**
- `src/accounts/lock.rs``acquire_lock()`, `release_lock()`, `account_lock_path()` + 4 теста
**Модифицированные файлы:**
- `Cargo.toml` — зависимость `fs2 = "0.4"`
- `src/accounts/mod.rs``pub mod lock;` + re-exports
- `src/app/mod.rs` — поле `account_lock: Option<File>` в `App<T>`
- `src/main.rs` — acquire lock при старте, lock при переключении аккаунтов, логирование set_tdlib_parameters
- `src/tdlib/client.rs` — логирование set_tdlib_parameters в `recreate_client()`
---
### Photo Albums (Media Groups) — DONE
Фото-альбомы (несколько фото в одном сообщении) теперь группируются в один пузырь с сеткой фото.
**Проблема**: TDLib отправляет альбомы как отдельные `Message` с общим `media_album_id: i64`. Ранее проект это поле игнорировал — каждое фото отображалось как отдельный пузырь.
**Решение:**
1. **Data Model**`media_album_id: i64` в `MessageMetadata`, `MessageBuilder`, getter `MessageInfo::media_album_id()`. Оба конвертера (async + sync) передают поле из TDLib.
2. **Message Grouping** — новый вариант `MessageGroup::Album(Vec<MessageInfo>)`. Сообщения с одинаковым `media_album_id != 0` группируются; одиночное сообщение с album_id остаётся `Message`.
3. **Album Grid Constants**`ALBUM_PHOTO_WIDTH: 16`, `ALBUM_PHOTO_HEIGHT: 8`, `ALBUM_PHOTO_GAP: 1`, `ALBUM_GRID_MAX_COLS: 3` (3×16 + 2×1 = 50 = `INLINE_IMAGE_MAX_WIDTH`).
4. **`render_album_bubble()`** — сетка фото (до 3 в ряд), `DeferredImageRender` с `x_offset` для каждого фото, общая подпись и timestamp, индикация выбора, статусы загрузки.
5. **Integration**`Album` arm в `render_message_list`, `x_offset` в second pass. Без feature `images` — fallback через отдельные bubble.
**Модифицированные файлы:**
- `src/tdlib/types.rs``media_album_id` в `MessageMetadata`, `MessageBuilder`, getter
- `src/tdlib/messages/convert.rs` — передача `media_album_id` в builder
- `src/tdlib/message_converter.rs` — передача `media_album_id` в builder
- `src/message_grouping.rs``Album` variant + album detection + 4 новых теста
- `src/constants.rs` — album grid constants
- `src/ui/components/message_bubble.rs``x_offset` в `DeferredImageRender`, `render_album_bubble()`
- `src/ui/components/mod.rs` — export `render_album_bubble`
- `src/ui/messages.rs``Album` arm + `x_offset` в second pass
6. **Навигация j/k по альбомам** — альбом обрабатывается как одно сообщение. `select_previous_message()` / `select_next_message()` перескакивают через все сообщения альбома. `start_message_selection()` встаёт на первый элемент альбома если последнее сообщение — часть альбома.
7. **Тесты** — 4 unit-теста в `message_grouping.rs`, 5 snapshot-тестов в `tests/messages.rs`, 3 теста навигации в `tests/input_navigation.rs`.
**Дополнительно модифицированные файлы:**
- `src/app/methods/messages.rs` — навигация перескакивает альбомы
- `tests/helpers/test_data.rs``TestMessageBuilder::media_album_id()`
- `tests/messages.rs` — 5 snapshot-тестов для альбомов
- `tests/input_navigation.rs` — 3 теста навигации по альбомам
**Что НЕ меняется:** image modal (v), auto-download, одиночные фото.
---
### Оптимизация: Ленивая загрузка сообщений при открытии чата (DONE)
Чат открывается мгновенно (< 1 сек) вместо 5-30 сек для больших чатов.
**Проблема**: `open_chat_and_load_data()` блокировал UI до полной загрузки ВСЕХ сообщений (`get_chat_history(chat_id, i32::MAX)`). Для чата с 500+ сообщениями это 10+ запросов к TDLib.
**Решение**:
- Загрузка только 50 последних сообщений (один запрос) → чат виден сразу
- Фоновые задачи (reply info, pinned, photos) — на следующем тике main loop через `pending_chat_init`
- Старые сообщения подгружаются при скролле вверх (существующий `load_older_messages_if_needed`)
**Модифицированные файлы:**
- `src/app/mod.rs` — поле `pending_chat_init: Option<ChatId>`
- `src/input/handlers/chat_list.rs``open_chat_and_load_data()`: 50 сообщений + `pending_chat_init`
- `src/main.rs` — обработка `pending_chat_init` в main loop (reply info, pinned, photos)
- `src/app/methods/navigation.rs`сброс `pending_chat_init` в `close_chat()`
---
### Bugfix: Авто-загрузка фото в чате (DONE)
Фото не отображались — отсутствовал код загрузки файлов после открытия чата.
**Проблема**: `extract_media_info()` создавал `PhotoInfo` с `PhotoDownloadState::NotDownloaded`, но никакой код не инициировал `download_file()`. Фото оставались в состоянии "📷 [Фото]" без inline превью.
**Исправление:**
- **Авто-загрузка при открытии чата**: после загрузки истории сообщений скачиваются фото из последних 30 сообщений (если `auto_download_images = true` и `show_images = true`). Каждый файл — с таймаутом 5 сек.
- **Загрузка по `v`**: вместо "Фото не загружено" — скачивание + открытие модалки. Также повторная попытка при `Error`.
- Обновление `PhotoDownloadState` в сообщении после успешной/неуспешной загрузки.
**Модифицированные файлы:**
- `src/input/handlers/chat_list.rs` — авто-загрузка фото в `open_chat_and_load_data()`
- `src/input/handlers/chat.rs``handle_view_image()`: download on NotDownloaded + retry on Error
---
### Этап 2+3: Account Switcher Modal + Переключение аккаунтов (DONE)
Реализована модалка переключения аккаунтов и механизм переключения:
- **Модалка `Ctrl+A`**: глобальный оверлей поверх любого экрана (Loading/Auth/Main)
- **Навигация**: `j/k` по списку, `Enter` выбор, `a` добавление, `Esc` закрытие
- **Переключение**: close TDLib → `recreate_client(new_db_path)` → auth flow
- **Добавление аккаунта**: ввод имени в модалке → валидация → `add_account()` → переключение
- **Footer индикатор**: `[account_name]` если не "default"
- **`AccountSwitcherState`**: enum `SelectAccount` / `AddAccount` — глобальный оверлей в `App<T>`
- **`recreate_client()`**: новый метод в `TdClientTrait` — close old → new TdClient → spawn set_tdlib_parameters
**Новые файлы:**
- `src/ui/modals/account_switcher.rs` — UI рендеринг (SelectAccount + AddAccount)
- `tests/account_switcher.rs` — 12 тестов
**Модифицированные файлы:**
- `src/app/mod.rs``AccountSwitcherState` enum, 3 поля (`account_switcher`, `current_account_name`, `pending_account_switch`), 8 методов
- `src/accounts/manager.rs``add_account()` (validate + save + ensure_dir)
- `src/accounts/mod.rs` — re-export `add_account`
- `src/tdlib/trait.rs``recreate_client(&mut self, db_path)` в TdClientTrait
- `src/tdlib/client.rs` — реализация `recreate_client` (close → new → set_tdlib_parameters)
- `src/tdlib/client_impl.rs` — trait impl делегирование
- `tests/helpers/fake_tdclient_impl.rs` — no-op `recreate_client`
- `src/input/main_input.rs` — account_switcher роутинг (highest priority)
- `src/input/handlers/global.rs``Ctrl+A` → open_account_switcher
- `src/input/handlers/modal.rs``handle_account_switcher()` (SelectAccount + AddAccount input)
- `src/ui/modals/mod.rs``pub mod account_switcher;`
- `src/ui/mod.rs` — overlay поверх любого экрана
- `src/ui/footer.rs``[account_name]` индикатор
- `src/main.rs``pending_account_switch` check в run_app, `Ctrl+A` из любого экрана
### Этап 1: Инфраструктура профилей аккаунтов (DONE)
Реализована инфраструктура для мультиаккаунта:
- **Модуль `accounts/`**: `profile.rs` (типы + валидация) + `manager.rs` (загрузка/сохранение/миграция)
- **`accounts.toml`**: конфиг списка аккаунтов в `~/.config/tele-tui/accounts.toml`
- **XDG data dir**: БД TDLib хранится в `~/.local/share/tele-tui/accounts/{name}/tdlib_data/`
- **Автомиграция**: `./tdlib_data/` → XDG path при первом запуске
- **CLI флаг `--account <name>`**: выбор аккаунта при запуске
- **Параметризация `db_path`**: `TdClient::new(db_path)`, `App::new(config, db_path)`
---
## Предыдущий статус: Multiline Message Display (DONE)
### Multiline в сообщениях
- **Multiline в сообщениях**: `\n` корректно отображается в пузырях сообщений (split по `\n` + word wrap)
- **Маркер выделения**: ▶ показывается только на первой строке multiline-сообщения
- Перенос строки в инпуте отключён (Shift+Enter/Alt+Enter/Ctrl+J не вставляют `\n`)
**Файлы изменены:**
- `ui/components/message_bubble.rs``wrap_text_with_offsets()` split по `\n` + `wrap_paragraph()` + selection marker fix
---
### Vim Normal/Insert Mode (DONE)
Реализован Vim-like режим работы с двумя состояниями:
- **Normal mode** (по умолчанию при открытии чата): навигация j/k, команды d/r/f/y, автоматический вход в MessageSelection
- **Insert mode** (нажать `i`/`ш`): набор текста, Esc возвращает в Normal
- Автопереключение в Insert при Reply (`r`) и Edit (`Enter`)
- Визуальные индикаторы: `[NORMAL]`/`[INSERT]` в footer, зелёная/серая рамка compose bar
- В Insert mode блокируются все команды кроме текстового ввода и Esc
**Файлы изменены:**
- `app/chat_state.rs` — enum `InputMode`
- `app/mod.rs` — поле `input_mode` в `App<T>`
- `config/keybindings.rs``Command::EnterInsertMode` + keybinding `i`/`ш`
- `app/methods/navigation.rs``close_chat()` сбрасывает input_mode
- `input/main_input.rs` — главный роутер Insert/Normal
- `input/handlers/chat.rs` — EnterInsertMode, auto-Insert при Reply/Edit
- `input/handlers/chat_list.rs` — auto-MessageSelection при открытии чата
- `ui/footer.rs` — mode indicator
- `ui/compose_bar.rs` — visual mode differentiation
- `tests/` — обновлены для нового поведения
---
## Предыдущий статус: Фаза 12 — Прослушивание голосовых сообщений (DONE)
### Завершённые фазы (краткий итог)
| Фаза | Описание | Статус |
|------|----------|--------|
| 1 | Базовая инфраструктура (ratatui + crossterm, vim-навигация) | DONE |
| 2 | TDLib интеграция (авторизация, чаты, сообщения) | DONE |
| 3 | Улучшение UX (отправка, поиск, скролл, realtime) | DONE |
| 4 | Папки и фильтрация (загрузка папок, переключение 1-9) | DONE |
| 5 | Расширенный функционал (онлайн-статус, медиа-заглушки, muted) | DONE |
| 6 | Полировка (60 FPS, память, graceful shutdown, динамический инпут) | DONE |
| 7 | Рефакторинг памяти (единый источник данных, LRU-кэш) | DONE |
| 8 | Дополнительные фичи (markdown, edit/delete, reply/forward, блочный курсор) | DONE |
| 9 | Расширенные возможности (typing, pinned, поиск в чате, черновики, профиль, копирование, реакции, конфиг) | DONE |
| 10 | Desktop уведомления (notify-rust, muted фильтр, mentions, медиа) | DONE (83%) |
| 11 | Inline просмотр фото (ratatui-image, кэш, загрузка) | DONE |
| 12 | Прослушивание голосовых сообщений (ffplay, play/pause, seek, ticker, cache, config) | DONE |
| 13 | Глубокий рефакторинг архитектуры (7 этапов) | DONE |
### Фаза 11: Inline фото + оптимизации (подробности)
Feature-gated (`images`), 2-tier архитектура:
**Базовая реализация:**
1. **Типы**: `MediaInfo`, `PhotoInfo`, `PhotoDownloadState`, `ImageModalState`, `ImagesConfig`
2. **Зависимости**: `ratatui-image 8.1`, `image 0.25` (feature-gated)
3. **Media модуль**: `ImageCache` (LRU), dual `ImageRenderer` (inline + modal)
4. **UX**: Always-show inline preview (фикс. ширина 50 chars) + полноэкранная модалка на `v`/`м`
5. **Метаданные**: `extract_media_info()` из TDLib MessagePhoto; auto-download visible photos
**Оптимизации производительности:**
1. **Dual protocol strategy**:
- `inline_image_renderer`: Halfblocks → быстро (Unicode блоки), для навигации
- `modal_image_renderer`: iTerm2/Sixel → медленно (high quality), для просмотра
2. **Frame throttling**: inline images 15 FPS (66ms), текст 60 FPS
3. **Lazy loading**: загрузка только видимых изображений (не все сразу)
4. **LRU кэш**: max 100 протоколов, eviction старых
5. **Loading indicator**: "⏳ Загрузка..." в модалке при первом открытии
6. **Navigation hotkeys**: `←`/`→` между фото, `Esc`/`q` закрыть модалку
**UI рендеринг**:
- `message_bubble.rs`: photo status (Downloading/Error/placeholder), inline preview
- `messages.rs`: второй проход с `render_images()` + throttling + только видимые
- `modals/image_viewer.rs`: fullscreen modal с aspect ratio + loading state
### Фаза 13: Рефакторинг (подробности)
Разбиты 5 монолитных файлов (4582 строк) на модульную архитектуру:
- **input/main_input.rs** (1199→164): чистый роутер + 5 handler модулей в `handlers/`
- **app/mod.rs** (1015→371): 5 trait модулей в `methods/` (Navigation, Message, Compose, Search, Modal)
- **ui/messages.rs** (893→365): модули `modals/` (search, pinned, delete, reactions) + `compose_bar.rs`
- **tdlib/messages.rs** (836→3 файла): `messages/` (mod, convert, operations)
- **config/mod.rs** (642→3 файла): validation.rs, loader.rs
- **Очистка дублей**: ~220 строк удалено (shared components, format_user_status, scroll_to_message)
- **Документация**: PROJECT_STRUCTURE.md переписан, 16 файлов получили `//!` docs
### Фаза 12: Голосовые сообщения (подробности)
**Реализовано:**
- **AudioPlayer** на ffplay (subprocess): play, pause (SIGSTOP), resume (SIGCONT), stop
- **VoiceCache**: LRU кэш OGG файлов в `~/.cache/tele-tui/voice/` (max 100 MB)
- **Типы**: `VoiceInfo`, `VoiceDownloadState`, `PlaybackState`, `PlaybackStatus`
- **TDLib интеграция**: `download_voice_note()`, конвертация `MessageVoiceNote`
- **Хоткеи**: Space (play/pause), ←/→ (seek ±5s via ffplay restart с `-ss`)
- **Автостоп**: при навигации на другое сообщение воспроизведение останавливается
- **Ticker**: `last_playback_tick` в App + обновление position в event loop (1 FPS redraw)
- **VoiceCache**: проверка кэша перед загрузкой, кэширование после download
- **AudioConfig**: `[audio]` секция в config.toml (cache_size_mb, auto_download_voice)
- **UI**: progress bar (━●─) + waveform (▁▂▃▄▅▆▇█) + иконки статуса в `message_bubble.rs`
- **Race condition fixes**: `starting` flag + pid ownership guard в потоках AudioPlayer
- **Seek**: `resume_from()` перезапускает ffplay с `-ss` offset; MoveLeft/MoveRight как alias для SeekBackward/SeekForward
- **Resume with rewind**: пауза→продолжение откатывает на 1 секунду назад
- **Graceful shutdown**: `stop_playback()` + Drop impl для AudioPlayer
### Ключевая архитектура
```
main.rs → event loop (16ms poll)
├── input/ → роутер + handlers/ (chat, chat_list, compose, modal, search)
├── app/ → App<T: TdClientTrait> + methods/ (5 traits, 67 методов)
├── ui/ → рендеринг (messages, chat_list, modals/, compose_bar, components/)
├── audio/ → player.rs (ffplay), cache.rs (VoiceCache)
├── media/ → [feature=images] cache.rs, image_renderer.rs
└── tdlib/ → TDLib wrapper (client, auth, chats, messages/, users, reactions, types)
```
### Тестирование
500+ тестов (0 failures):
- Snapshot tests: 57 (UI компоненты)
- Integration tests: 93 (user flows)
- E2E tests: 12 (smoke + journey)
- Utils tests: 18
- Performance benchmarks: 8
### Ключевые решения
1. **Неблокирующий receive**: TDLib updates через `mpsc::channel` в отдельном потоке
2. **Trait-based App**: методы разбиты на traits — требуют `use` import на call site
3. **FakeTdClient**: mock для тестов без TDLib (реализует TdClientTrait)
4. **Оптимизация рендеринга**: `needs_redraw` флаг, рендеринг только при изменениях
5. **Конфиг**: TOML `~/.config/tele-tui/config.toml`, credentials с приоритетом (XDG → .env)
6. **Feature-gated images**: `images` feature flag для ratatui-image + image deps
7. **Dual renderer**: inline (Halfblocks, 15 FPS) + modal (iTerm2/Sixel, high quality) для баланса скорости/качества
8. **Audio via ffplay**: subprocess с SIGSTOP/SIGCONT для pause/resume, автостоп при навигации
### Зависимости (основные)
```toml
ratatui = "0.29" # TUI фреймворк
crossterm = "0.28" # Терминальный backend
tdlib-rs = "1.1" # Telegram TDLib binding
tokio = "1" # Async runtime
notify-rust = "4.11" # Desktop уведомления (feature flag)
ratatui-image = "8.1" # Inline images (feature flag)
image = "0.25" # Image decoding (feature flag)
```
Полная структура проекта: см. [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md)
Подробный план: см. [ROADMAP.md](ROADMAP.md)
# Контекст проекта
## Текущий статус
Фаза 14: мультиаккаунт, в работе.
Цель фазы: безопасные профили Telegram-аккаунтов, переключение аккаунтов и подготовка к более быстрому multi-client UX.
## Уже сделано
- Профили аккаунтов: `AccountProfile`, `accounts.toml`, XDG data paths.
- Миграция старого `tdlib_data/` в per-account directory.
- CLI `--account <name>`.
- Account switcher modal по `Ctrl+A`: выбор и добавление аккаунта.
- Single-client переключение через `recreate_client()`.
- Footer показывает имя аккаунта, если он не `default`.
- Per-account lock file защищает TDLib database от двух процессов.
- TDLib receive loop передаёт `client_id`; UI применяет updates только активного клиента.
- `pending_chat_init` не должен блокировать первый redraw; reply-info и photo downloads уходят в фоновые tasks.
- Keybindings стали детерминированными; русская vim-раскладка: `h/j/k/l` -> `р/о/л/д`.
- `AudioPlayer` проверяет наличие `ffplay`.
- `message_grouping` группирует альбомы без клонирования сообщений.
## Осталось
- Быстрые hotkeys `Ctrl+1`..`Ctrl+9` для аккаунтов без модалки.
- Удаление/переименование аккаунта в account switcher.
- Бейджи непрочитанных с других аккаунтов.
- Решить, нужен ли переход от single-client reinit к одновременным клиентам.
- Добавить/уточнить tests для accounts + TDLib update routing.
## Риски
- Multi-account код должен фильтровать TDLib updates по `client_id`.
- Инициализация чата и фоновые downloads не должны блокировать первый redraw.
- Read/unread статус исходящих сообщений зависит от корректной TDLib metadata.
- Конфликтующие keybindings должны оставаться детерминированными.
- Переключение аккаунтов требует проверки lock release/acquire и auth flow.
## Ключевые решения
- Главный state хранится в `App<T: TdClientTrait>`, чтобы тесты могли использовать `FakeTdClient`.
- Методы `App` разбиты на traits: navigation, messages, compose, search, modal.
- UI рендерится только при `needs_redraw`; текстовый интерфейс целится в 60 FPS.
- Фото под feature `images`: inline Halfblocks + modal iTerm2/Sixel.
- Голосовые сообщения проигрываются через `ffplay`, cache живёт в `~/.cache/tele-tui/voice/`.
- Credentials читаются из `~/.config/tele-tui/credentials`, fallback `.env`.
- TDLib data аккаунтов хранится в `~/.local/share/tele-tui/accounts/{name}/tdlib_data/`.

View File

@@ -1,110 +1,43 @@
# Правила локальной разработки
> **Обязательно к прочтению перед началом работы!**
---
# Разработка
## Инструменты
### MCP серверы
- **Serena** — для работы с кодом (символьная навигация, редактирование)
- **Context7** — для получения актуальной документации по библиотекам
- Для поиска используй `rg`.
- Для точечной навигации по Rust-коду можно использовать Serena.
- Для актуальной документации библиотек используй Context7, если это нужно для изменения.
Используй эти инструменты для эффективной работы с кодовой базой.
## Cargo-команды
---
Агентам нельзя самостоятельно запускать:
## Правила работы
### 1. Никогда не запускай сервисы самостоятельно
**ЗАПРЕЩЕНО** запускать `cargo run`, `cargo build` и подобные команды.
**Вместо этого попроси пользователя запустить:**
```
Запусти, пожалуйста:
```bash
cargo run
cargo build
cargo test
cargo check
```
### 2. Тестирование — только ручное
Исключение: пользователь прямо попросил запустить конкретную cargo-команду.
После завершения задачи:
1. Опиши сценарии для проверки
2. Попроси пользователя проверить вручную
3. Дождись фидбека
В финальном ответе после изменения укажи, какие cargo-команды не запускались, и дай минимальную ручную проверку.
**Формат:**
```
Готово! Проверь, пожалуйста:
## Scope
1. Открой cargo run
2. понавигируйся в списке чатов кнопками h j k l
3. Нажми Enter для открытия чата
4. Убедись, что чат прогурзился
- Делай одну логическую правку за раз.
- Не смешивай feature work, рефакторинг и документацию без необходимости.
- Не откатывай чужие изменения в dirty worktree.
- Не используй destructive git-команды без явной просьбы.
Напиши, если что-то не работает.
## Ручная проверка
Формат после функциональной правки:
```text
Проверь:
1. Запусти ...
2. Открой ...
3. Выполни ...
4. Ожидаемый результат: ...
```
### 3. Работа поэтапно
Делай работу **небольшими итерациями**:
1. **Один этап = одна логическая единица**
- Один endpoint
- Один компонент
- Одна фича
2. **После каждого этапа:**
- Сообщи что сделано
- Дай сценарий проверки
- Дождись подтверждения
3. **Не делай сразу много:**
- ❌ Весь CRUD за раз
- ✅ Сначала GET, проверили, потом POST, проверили...
---
### 4. Работа с git
НИКОГДА НЕ КОММИТЬ ИЗМЕНЕНИЯ ПОКА ТЕБЯ НЕ ПОПРОСЯТ!!!
## Чеклист перед началом работы
- [ ] Прочитал CONTEXT.md
- [ ] Прочитал ROADMAP.md (понял текущую фазу)
- [ ] Понял задачу
- [ ] Готов работать поэтапно
- [ ] Помню: не запускаю сервисы сам, прошу пользователя
---
## Пример правильного workflow
```
Пользователь: Сделай endpoint для получения списка идей
Агент:
1. Читает документацию через Context7 (NestJS, TypeORM)
2. Использует Serena для навигации по коду
3. Создаёт endpoint GET /api/ideas
4. Сообщает:
"Создал endpoint GET /api/ideas.
Запусти backend:
cd backend && npm run dev
Проверь:
curl http://localhost:4001/api/ideas
Должен вернуться пустой массив: { data: [], meta: {...} }"
5. Ждёт фидбек
6. Переходит к следующему этапу
```
## Работа с git
- никогда не добавляй себя в соавторов в тексте коммита
Для чисто документационных задач cargo-проверки не требуются.

View File

@@ -1,194 +0,0 @@
# Горячие клавиши tele-tui
## Общая навигация
| Клавиша | Русская раскладка | Действие |
|---------|-------------------|----------|
| `↑` / `k` | `р` | Вверх по списку |
| `↓` / `j` | `о` | Вниз по списку |
| `Enter` | | Открыть чат / Отправить сообщение |
| `Esc` | | Закрыть чат / Отменить действие |
| `Ctrl+C` | | Выход из приложения |
| `Ctrl+R` | | Обновить список чатов |
## Папки и поиск
| Клавиша | Действие |
|---------|----------|
| `1-9` | Переключение между папками Telegram |
| `Ctrl+S` | Открыть поиск по чатам |
| `Ctrl+F` | Открыть поиск в текущем чате |
| `n` | Следующий результат поиска |
| `N` | Предыдущий результат поиска |
## Работа с сообщениями
### Навигация и выбор
| Клавиша | Действие |
|---------|----------|
| `↑/↓` | Скролл сообщений (в открытом чате) |
| `↑` | Выбор сообщения (при пустом поле ввода) |
| `Esc` | Отменить выбор |
### Действия с сообщениями
| Клавиша | Русская раскладка | Действие |
|---------|-------------------|----------|
| `Enter` | | Редактировать выбранное сообщение |
| `r` | `к` | Ответить на сообщение (Reply) |
| `f` | `а` | Переслать сообщение (Forward) |
| `d` / `Delete` | `в` | Удалить сообщение |
| `y` | `н` | Копировать текст в буфер обмена |
| `e` | `у` | Добавить реакцию (Emoji picker) |
| `v` | `м` | Открыть изображение в полном размере |
| `Ctrl+i` | `Ctrl+ш` | Открыть профиль чата/пользователя |
## Просмотр изображений
### Режим просмотра изображения
| Клавиша | Действие |
|---------|----------|
| `v` / `м` | Открыть изображение (в режиме выбора) |
| `←` | Предыдущее изображение в чате |
| `→` | Следующее изображение в чате |
| `Esc` | Закрыть просмотр изображения |
**Примечание**: Изображения отображаются inline в чате автоматически. Используйте `v` для просмотра в полном размере.
## Прослушивание голосовых сообщений
### Управление воспроизведением
| Клавиша | Русская раскладка | Действие |
|---------|-------------------|----------|
| `Space` | | Воспроизвести/Пауза (в режиме выбора голосового) |
| `s` | `ы` | Остановить воспроизведение |
### Во время воспроизведения
| Клавиша | Действие |
|---------|----------|
| `Space` | Пауза / Возобновить |
| `s` / `ы` | Остановить |
| `←` | Перемотка назад (по умолчанию -5 сек) |
| `→` | Перемотка вперед (по умолчанию +5 сек) |
| `↑` | Увеличить громкость (+10%) |
| `↓` | Уменьшить громкость (-10%) |
**Примечание**: Голосовые сообщения показывают progress bar во время воспроизведения: `▶ ████████░░░░░░ 0:08 / 0:15`
## Модалки подтверждения
### Удаление сообщения
| Клавиша | Русская раскладка | Действие |
|---------|-------------------|----------|
| `y` / `Enter` | `н` | Подтвердить удаление |
| `n` / `Esc` | `т` | Отменить удаление |
## Emoji Picker (реакции)
| Клавиша | Действие |
|---------|----------|
| `←` | Влево по сетке эмодзи |
| `→` | Вправо по сетке эмодзи |
| `↑` | Вверх по сетке эмодзи |
| `↓` | Вниз по сетке эмодзи |
| `Enter` | Добавить/удалить реакцию |
| `Esc` | Закрыть emoji picker |
## Редактирование текста
### Навигация по тексту
| Клавиша | Действие |
|---------|----------|
| `←` | Курсор влево |
| `→` | Курсор вправо |
| `Home` | Курсор в начало строки |
| `End` | Курсор в конец строки |
### Редактирование
| Клавиша | Действие |
|---------|----------|
| `Backspace` | Удалить символ слева от курсора |
| `Delete` | Удалить символ справа от курсора |
| `Enter` | Новая строка / Отправить (зависит от контекста) |
## Режимы работы
### Режим списка чатов
- Навигация: `↑/↓`
- Открыть чат: `Enter`
- Поиск: `Ctrl+S`
- Папки: `1-9`
### Режим открытого чата
- Скролл: `↑/↓`
- Выбор сообщения: `↑` (при пустом инпуте)
- Поиск в чате: `Ctrl+F`
- Закрыть чат: `Esc`
### Режим выбора сообщения
- Редактировать: `Enter`
- Ответить: `r` / `к`
- Переслать: `f` / `а`
- Удалить: `d` / `в` / `Delete`
- Копировать: `y` / `н`
- Реакция: `e` / `у`
- Просмотр изображения: `v` / `м` (если выбрано сообщение с фото)
- Воспроизведение голосового: `Space` (если выбрано голосовое сообщение)
- Отменить: `Esc`
### Режим редактирования
- Редактировать текст: см. "Редактирование текста"
- Отправить: `Enter`
- Отменить: `Esc`
### Режим ответа (Reply)
- Редактировать ответ: см. "Редактирование текста"
- Отправить: `Enter`
- Отменить: `Esc`
### Режим пересылки (Forward)
- Выбрать чат: `↑/↓`
- Переслать: `Enter`
- Отменить: `Esc`
### Режим просмотра изображения
- Навигация: `←/→` (предыдущее/следующее изображение)
- Закрыть: `Esc`
### Режим воспроизведения голосового
- Пауза/Возобновить: `Space`
- Остановить: `s` / `ы`
- Перемотка: `←/→` (-5с / +5с)
- Громкость: `↑/↓` (+/- 10%)
## Поддержка русской раскладки
Все основные vim-клавиши поддерживают русскую раскладку:
| Английская | Русская | Действие |
|------------|---------|----------|
| `h` | `р` | Влево |
| `j` | `о` | Вниз |
| `k` | `л` | Вверх |
| `l` | `д` | Вправо |
| `r` | `к` | Reply |
| `f` | `а` | Forward |
| `d` | `в` | Delete |
| `y` | `н` | Copy (Yank) |
| `e` | `у` | Emoji reaction |
| `v` | `м` | View image |
| `s` | `ы` | Stop audio |
## Подсказки
- Текущие доступные команды всегда отображаются в нижней части экрана (footer)
- При открытой модалке доступны только действия этой модалки
- `Esc` всегда отменяет текущее действие и возвращает на шаг назад
- Блочный курсор █ показывает текущую позицию при редактировании текста

View File

@@ -1,328 +0,0 @@
# Структура проекта
## Архитектура (ASCII)
```
┌─────────────┐
│ main.rs │ Event loop (60 FPS)
└──────┬──────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ input/ │ │ app/ │ │ ui/ │
│ handlers │ │ state │ │ render │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ ┌──────┴──────┐ │
│ │ methods/ │ │
│ │ (5 traits) │ │
│ └──────┬──────┘ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────┐
│ tdlib/ │
│ TdClientTrait → TdClient │
│ messages/ | auth | chats │
└──────────────┬──────────────────┘
┌─────▼─────┐
│ TDLib C │
│ library │
└───────────┘
```
### Data Flow
```
TDLib Updates → mpsc channel → App state → UI rendering
User Input → handlers → App methods (traits) → TdClient → TDLib API
```
## Обзор директорий
```
tele-tui/
├── src/
│ ├── main.rs # Точка входа, event loop
│ ├── lib.rs # Экспорт модулей для тестов
│ ├── types.rs # ChatId, MessageId (newtype wrappers)
│ ├── constants.rs # MAX_MESSAGES_IN_CHAT, etc.
│ ├── formatting.rs # Markdown entity форматирование
│ ├── message_grouping.rs # Группировка сообщений по дате/отправителю
│ ├── notifications.rs # Desktop уведомления (NotificationManager)
│ │
│ ├── app/ # Состояние приложения
│ │ ├── mod.rs # App<T> struct, конструкторы, getters (372 loc)
│ │ ├── state.rs # AppScreen enum
│ │ ├── chat_state.rs # ChatState enum (state machine)
│ │ ├── chat_filter.rs # ChatFilter, ChatFilterCriteria
│ │ ├── chat_list_state.rs # Состояние списка чатов
│ │ ├── auth_state.rs # Состояние авторизации
│ │ ├── compose_state.rs # Состояние compose bar
│ │ ├── ui_state.rs # UI-related state
│ │ ├── message_service.rs # Сервис сообщений
│ │ ├── message_view_state.rs # Состояние просмотра сообщений
│ │ └── methods/ # Trait-based методы App (Этап 2)
│ │ ├── mod.rs # Re-exports 5 trait модулей
│ │ ├── navigation.rs # NavigationMethods (7 методов)
│ │ ├── messages.rs # MessageMethods (8 методов)
│ │ ├── compose.rs # ComposeMethods (10 методов)
│ │ ├── search.rs # SearchMethods (15 методов)
│ │ └── modal.rs # ModalMethods (27 методов)
│ │
│ ├── config/ # Конфигурация (Этап 5)
│ │ ├── mod.rs # Config struct, defaults (350 loc)
│ │ ├── keybindings.rs # Command enum, Keybindings
│ │ ├── validation.rs # validate(), parse_color()
│ │ └── loader.rs # load(), save(), credentials
│ │
│ ├── input/ # Обработка пользовательского ввода
│ │ ├── mod.rs # Роутинг по экранам
│ │ ├── auth.rs # Ввод на экране авторизации
│ │ ├── main_input.rs # Роутер главного экрана (159 loc, Этап 1)
│ │ ├── key_handler.rs # Trait-based обработка клавиш
│ │ └── handlers/ # Специализированные обработчики (Этап 1)
│ │ ├── mod.rs # Exports + scroll_to_message()
│ │ ├── global.rs # Ctrl+R/S/P/F глобальные команды
│ │ ├── chat.rs # Открытый чат: ввод, скролл, selection
│ │ ├── chat_list.rs # Навигация по списку чатов, папки
│ │ ├── compose.rs # Forward mode
│ │ ├── modal.rs # Profile, reactions, pinned, delete
│ │ ├── search.rs # Поиск чатов и сообщений
│ │ ├── clipboard.rs # Копирование в буфер обмена
│ │ └── profile.rs # Хелперы профиля
│ │
│ ├── tdlib/ # TDLib интеграция
│ │ ├── mod.rs # Экспорт публичных типов
│ │ ├── types.rs # MessageInfo, ChatInfo, ProfileInfo, etc.
│ │ ├── trait.rs # TdClientTrait (DI для тестов)
│ │ ├── client.rs # TdClient struct, конструктор
│ │ ├── client_impl.rs # impl TdClientTrait for TdClient
│ │ ├── auth.rs # Авторизация (phone, code, 2FA)
│ │ ├── chats.rs # Загрузка чатов, папок
│ │ ├── users.rs # Кеш пользователей, статусы
│ │ ├── reactions.rs # ReactionInfo, toggle_reaction
│ │ ├── chat_helpers.rs # Вспомогательные функции чатов
│ │ ├── update_handlers.rs # Обработка TDLib update events
│ │ ├── message_converter.rs # Конвертация TDLib → MessageInfo
│ │ ├── message_conversion.rs # Доп. функции конвертации
│ │ └── messages/ # Менеджер сообщений (Этап 4)
│ │ ├── mod.rs # MessageManager struct (99 loc)
│ │ ├── convert.rs # convert_message, fetch_reply_info
│ │ └── operations.rs # 11 TDLib API операций (616 loc)
│ │
│ ├── ui/ # Рендеринг интерфейса
│ │ ├── mod.rs # render() — роутинг по экранам
│ │ ├── loading.rs # Экран загрузки
│ │ ├── auth.rs # Экран авторизации
│ │ ├── main_screen.rs # Главный экран + папки
│ │ ├── footer.rs # Футер с командами и статусом сети
│ │ ├── chat_list.rs # Список чатов + онлайн-статус
│ │ ├── messages.rs # Область сообщений (364 loc, Этап 3)
│ │ ├── compose_bar.rs # Multi-mode input box (Этап 3)
│ │ ├── profile.rs # Профиль пользователя/чата
│ │ ├── modals/ # Модальные окна (Этап 3)
│ │ │ ├── mod.rs # Re-exports
│ │ │ ├── delete_confirm.rs # Подтверждение удаления
│ │ │ ├── reaction_picker.rs # Выбор реакции
│ │ │ ├── search.rs # Поиск по сообщениям
│ │ │ └── pinned.rs # Закреплённые сообщения
│ │ └── components/ # Переиспользуемые UI компоненты (Этап 6)
│ │ ├── mod.rs # Re-exports
│ │ ├── modal.rs # render_modal(), render_delete_confirm
│ │ ├── input_field.rs # render_input_field()
│ │ ├── message_bubble.rs # render_message_bubble(), sender, date
│ │ ├── message_list.rs # render_message_item(), help_bar, scroll
│ │ ├── chat_list_item.rs # render_chat_list_item()
│ │ └── emoji_picker.rs # render_emoji_picker()
│ │
│ └── utils/ # Утилиты
│ ├── mod.rs # Exports, with_timeout helpers
│ ├── formatting.rs # format_timestamp, format_date, etc.
│ ├── tdlib.rs # disable_tdlib_logs (FFI)
│ ├── validation.rs # is_non_empty и др.
│ ├── modal_handler.rs # handle_yes_no для Y/N модалок
│ └── retry.rs # Retry утилиты
├── tests/ # Интеграционные тесты
│ ├── helpers/ # Тестовая инфраструктура
│ │ ├── mod.rs
│ │ ├── app_builder.rs # TestAppBuilder (fluent API)
│ │ ├── fake_tdclient.rs # FakeTdClient (мок TDLib)
│ │ ├── fake_tdclient_impl.rs # impl TdClientTrait for FakeTdClient
│ │ ├── test_data.rs # create_test_chat, TestMessageBuilder
│ │ └── snapshot_utils.rs # Snapshot testing хелперы
│ ├── input_navigation.rs # Тесты навигации клавиатурой
│ ├── chat_list.rs # Тесты списка чатов
│ ├── messages.rs # Тесты сообщений
│ ├── send_message.rs # Тесты отправки
│ ├── edit_message.rs # Тесты редактирования
│ ├── delete_message.rs # Тесты удаления
│ ├── reply_forward.rs # Тесты reply/forward
│ ├── reactions.rs # Тесты реакций
│ ├── search.rs # Тесты поиска
│ ├── modals.rs # Тесты модальных окон
│ ├── profile.rs # Тесты профиля
│ ├── navigation.rs # Тесты навигации
│ ├── drafts.rs # Тесты черновиков
│ ├── copy.rs # Тесты копирования
│ ├── screens.rs # Тесты экранов
│ ├── footer.rs # Тесты футера
│ ├── input_field.rs # Тесты поля ввода
│ ├── config.rs # Тесты конфигурации
│ ├── network_typing.rs # Тесты typing status
│ ├── e2e_smoke.rs # Smoke тесты
│ └── e2e_user_journey.rs # E2E user journey тесты
├── .github/ # GitHub конфигурация
│ ├── ISSUE_TEMPLATE/
│ ├── workflows/ci.yml
│ └── pull_request_template.md
├── Cargo.toml # Манифест проекта
├── Cargo.lock # Точные версии зависимостей
├── build.rs # Build script (TDLib)
├── rustfmt.toml # cargo fmt конфигурация
├── .editorconfig # Настройки IDE
├── .gitignore # Git ignore
├── config.toml.example # Пример конфигурации
├── credentials.example # Пример credentials
├── CLAUDE.md # Инструкции для AI
├── CONTEXT.md # Текущий статус
├── ROADMAP.md # План развития
├── DEVELOPMENT.md # Правила разработки
├── REQUIREMENTS.md # Требования
├── ARCHITECTURE.md # C4, sequence diagrams
├── PROJECT_STRUCTURE.md # Этот файл
├── E2E_TESTING.md # Гайд по тестированию
├── HOTKEYS.md # Горячие клавиши
├── CHANGELOG.md # История изменений
├── README.md # Главная документация
├── INSTALL.md # Установка
├── FAQ.md # FAQ
├── CONTRIBUTING.md # Гайд по контрибуции
├── SECURITY.md # Безопасность
└── LICENSE # MIT лицензия
```
## Ключевые модули
### app/ — Состояние приложения
`App<T: TdClientTrait>` — главная структура, параметризована trait'ом для DI.
**State machine** (`ChatState` enum):
```
Normal → MessageSelection → Editing
→ Reply
→ Forward
→ DeleteConfirmation
→ ReactionPicker
→ Profile
→ SearchInChat
→ PinnedMessages
```
**Trait-based methods** (5 traits на `App<T>`):
| Trait | Методы | Описание |
|-------|--------|----------|
| NavigationMethods | 7 | next/previous_chat, close_chat, select_current_chat |
| MessageMethods | 8 | is_editing, is_replying, get_selected_message, etc. |
| ComposeMethods | 10 | start_reply, cancel_editing, load_draft, etc. |
| SearchMethods | 15 | start_search, enter_message_search_mode, etc. |
| ModalMethods | 27 | enter_profile_mode, exit_pinned_mode, etc. |
### input/ — Обработка ввода
**Маршрутизация** (порядок приоритетов в `main_input.rs`):
1. Global commands (Ctrl+R/S/P/F)
2. Profile mode
3. Message search mode
4. Pinned messages mode
5. Reaction picker mode
6. Delete confirmation
7. Forward mode
8. Chat search mode
9. Enter/Esc commands
10. Open chat input / Chat list navigation
### tdlib/ — Telegram интеграция
**Dependency Injection**: `TdClientTrait` позволяет подменять TdClient на `FakeTdClient` в тестах.
**MessageManager** — управление сообщениями:
- `convert.rs` — конвертация TDLib JSON → MessageInfo
- `operations.rs` — 11 API операций (get_history, send, edit, delete, forward, search, etc.)
### ui/ — Рендеринг
**Компоненты** (`ui/components/`):
| Компонент | Описание |
|-----------|----------|
| message_bubble | Рендеринг пузыря сообщения с реакциями |
| message_list | Элемент списка сообщений (search/pinned) |
| chat_list_item | Элемент списка чатов |
| input_field | Поле ввода с курсором |
| emoji_picker | Сетка выбора реакций |
| modal | Центрированная модалка |
### config/ — Конфигурация
- **mod.rs** — struct Config, GeneralConfig, ColorsConfig, NotificationsConfig
- **keybindings.rs** — Command enum (30+ команд), кастомные горячие клавиши
- **validation.rs** — валидация timezone, цветов
- **loader.rs** — загрузка из `~/.config/tele-tui/config.toml`, credentials
## Тестирование
**500+ тестов** через `cargo test` (без TDLib).
**Инфраструктура**:
- `TestAppBuilder` — fluent API для создания App с нужным состоянием
- `FakeTdClient` — мок TDLib, реализует TdClientTrait
- `TestMessageBuilder` — создание тестовых сообщений
**Типы тестов**:
- Unit-тесты — в `#[cfg(test)]` секциях модулей
- Integration-тесты — в `tests/` (навигация, отправка, UI рендеринг)
- Doc-тесты — примеры в документации
- E2E — smoke и user journey тесты
## Потоки выполнения
```
Main thread TDLib thread
│ │
│ ◄── mpsc ─────── │ td_client.receive() в Tokio task
│ │
├── poll events │
├── handle input │
├── update state │
├── render UI │
└── sleep 16ms ──► │
```
## Рантайм файлы
| Путь | Описание |
|------|----------|
| `~/.config/tele-tui/config.toml` | Пользовательская конфигурация |
| `~/.config/tele-tui/credentials` | API_ID и API_HASH |
| `tdlib_data/` | TDLib сессия (НЕ коммитится) |
## Зависимости
| Категория | Крейт | Назначение |
|-----------|-------|------------|
| UI | ratatui 0.29 | TUI framework |
| UI | crossterm 0.28 | Terminal control |
| Telegram | tdlib-rs 1.1 | TDLib bindings |
| Async | tokio 1.x | Async runtime |
| Config | serde + toml | Serialization |
| Time | chrono 0.4 | Date/time |
| System | dirs 5.0 | XDG directories |
| System | arboard 3.4 | Clipboard |
| Notify | notify-rust 4.11 | Desktop уведомления (feature) |
| URL | open 5.0 | Открытие URL (feature) |

256
README.md
View File

@@ -1,261 +1,69 @@
# tele-tui
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Rust](https://img.shields.io/badge/rust-1.70%2B-orange.svg)](https://www.rust-lang.org/)
Консольный Telegram клиент с Vim-style навигацией.
![tele-tui screenshot](docs/screenshot.png)
Консольный Telegram-клиент на Rust с Vim-style навигацией, Normal/Insert режимами, TDLib и поддержкой русской раскладки.
## Возможности
- **Полная интеграция с Telegram**: отправка/получение сообщений, редактирование, удаление, пересылка
- **Vim-style навигация**: hjkl + поддержка русской раскладки (ролд)
- **Markdown форматирование**: жирный, курсив, подчёркивание, зачёркивание, код, спойлеры, ссылки
- **Реакции на сообщения**: emoji picker с навигацией стрелками
- **Папки Telegram**: переключение между папками (1-9)
- **Поиск**: по чатам (Ctrl+S) и внутри чата (Ctrl+F)
- **Черновики**: автосохранение набранного текста при переключении чатов
- **Typing indicator**: показывает когда собеседник печатает
- **Закреплённые сообщения**: отображение и переход к закреплённому сообщению
- **Копирование в буфер**: copy сообщений в системный буфер обмена
- **Профиль**: просмотр информации о пользователе/чате
- **Конфигурация**: настройка цветов и часового пояса через TOML
- **Оптимизация**: 60 FPS, умное кеширование, graceful shutdown
- Авторизация через TDLib, список чатов, история и realtime updates.
- Отправка, редактирование, удаление, reply, forward, copy и реакции.
- Поиск по чатам и внутри открытого чата.
- Telegram-папки, pinned/muted/mentions/unread indicators.
- Markdown entities, inline фото, fullscreen image modal и фото-альбомы.
- Голосовые сообщения через `ffplay`.
- Desktop notifications, clipboard и открытие URL через feature flags.
- Multi-account profiles, account switcher и per-account lock files.
## Установка
### Требования
Требования:
- Rust 1.70+
- TDLib (скачивается автоматически через tdlib-rs)
### Сборка
- Rust stable, рекомендовано 1.85+.
- TDLib скачивается автоматически через `tdlib-rs` feature `download-tdlib`.
- Для голосовых сообщений нужен `ffplay` из ffmpeg.
```bash
git clone https://github.com/your-username/tele-tui.git
cd tele-tui
cargo build --release
```
### API Credentials
## Credentials
Получите API credentials на https://my.telegram.org/apps
Получите Telegram API credentials на <https://my.telegram.org/apps> и сохраните их в:
Создайте файл `~/.config/tele-tui/credentials`:
```text
~/.config/tele-tui/credentials
```
Формат:
```text
API_ID=your_api_id
API_HASH=your_api_hash
```
Или используйте `.env` файл в директории проекта:
```
API_ID=your_api_id
API_HASH=your_api_hash
```
Для локальной разработки можно использовать `.env` в корне проекта с теми же ключами.
## Использование
## Запуск
```bash
cargo run --release
```
При первом запуске нужно пройти авторизацию (телефон + код + опционально 2FA пароль).
## Конфигурация
Конфигурационный файл создаётся автоматически в `~/.config/tele-tui/config.toml`:
```toml
[general]
# Часовой пояс в формате "+03:00" или "-05:00"
timezone = "+03:00"
[colors]
# Поддерживаемые цвета: black, red, green, yellow, blue, magenta, cyan, gray, white,
# darkgray, lightred, lightgreen, lightyellow, lightblue, lightmagenta, lightcyan
incoming_message = "white"
outgoing_message = "green"
selected_message = "yellow"
reaction_chosen = "yellow"
reaction_other = "gray"
```
## Горячие клавиши
### Навигация
- `↑/↓` или `k/j` (р/о) — навигация по списку чатов
- `Enter` — открыть чат / отправить сообщение
- `Esc` — закрыть чат / отменить действие
- `1-9` — переключение между папками
- `Ctrl+S` — поиск по чатам
- `Ctrl+R` — обновить список чатов
- `Ctrl+C` — выход
### В открытом чате
- `↑/↓` — скролл сообщений
- `Ctrl+F` — поиск в чате
- `n/N` — следующий/предыдущий результат поиска
- `i` — информация о чате/пользователе
### Работа с сообщениями
- `↑` при пустом инпуте — выбор сообщения
- `Enter` в режиме выбора — редактировать
- `r` / `к` — ответить (reply)
- `f` / `а` — переслать (forward)
- `d` / `в` / `Delete` — удалить
- `y` / `н` — скопировать в буфер
- `e` / `у` — добавить реакцию
### Emoji Picker (реакции)
- `←/→/↑/↓` — навигация по сетке
- `Enter` — добавить/удалить реакцию
- `Esc` — закрыть picker
### Редактирование текста
- `←/→` — перемещение курсора
- `Home` — в начало строки
- `End` — в конец строки
- `Backspace` — удалить символ слева
- `Delete` — удалить символ справа
## Структура проекта
```
src/
├── main.rs # Точка входа, event loop
├── config.rs # Конфигурация (TOML), credentials
├── app/ # Состояние приложения
├── ui/ # Отрисовка интерфейса
├── input/ # Обработка ввода
├── utils.rs # Утилиты (форматирование времени, логи)
└── tdlib/ # TDLib интеграция
```
## Зависимости
- `ratatui` 0.29 — TUI framework
- `crossterm` 0.28 — terminal handling
- `tdlib-rs` 1.1 — Telegram API
- `tokio` 1.x — async runtime
- `serde` + `serde_json` — serialization
- `toml` 0.8 — config parsing
- `dirs` 5.0 — XDG directories
- `clipboard` 0.5 — clipboard access
- `chrono` 0.4 — date/time formatting
## Тестирование
tele-tui использует **snapshot тестирование** для UI и интеграционные тесты для логики.
### Запуск всех тестов
Для выбора аккаунта:
```bash
cargo test
cargo run --release -- --account work
```
### Snapshot тесты
Snapshot тесты проверяют отображение UI компонентов через виртуальный терминал:
```bash
# Прогнать snapshot тесты
cargo test --test chat_list
cargo test --test messages
# Посмотреть изменения в snapshots
cargo insta review
# Принять все новые snapshots
cargo insta accept
# Отклонить все изменения
cargo insta reject
```
### Установка cargo-insta
Для работы со snapshot тестами нужен `cargo-insta`:
```bash
cargo install cargo-insta
```
### Структура тестов
```
tests/
├── helpers/ # Тестовые утилиты
│ ├── app_builder.rs # TestAppBuilder для создания тестовых App
│ ├── test_data.rs # Builders для чатов и сообщений
│ ├── snapshot_utils.rs # Утилиты для snapshot тестов
│ └── fake_tdclient.rs # Mock TDLib клиент (для будущих integration тестов)
├── chat_list.rs # Snapshot тесты для списка чатов (9 тестов)
├── messages.rs # Snapshot тесты для сообщений (18 тестов)
├── modals.rs # Snapshot тесты для модалок (8 тестов)
└── input_field.rs # Snapshot тесты для поля ввода (7 тестов)
```
### Создание snapshot теста
```rust
use helpers::test_data::TestChatBuilder;
use helpers::app_builder::TestAppBuilder;
use helpers::snapshot_utils::{render_to_buffer, buffer_to_string};
use insta::assert_snapshot;
#[test]
fn snapshot_my_feature() {
let chat = TestChatBuilder::new("Test Chat", 123)
.unread_count(5)
.build();
let mut app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::chat_list::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
assert_snapshot!("my_feature", output);
}
```
### Покрытие тестами
**Текущий прогресс**: 81/151 тестов (54%)
- ✅ Фаза 0: Инфраструктура (100%)
- ✅ Фаза 1: UI Snapshot Tests (100%)
- Chat List, Messages, Modals, Input Field, Footer, Screens
- 🔄 Фаза 2: Integration Tests (35%)
- ✅ Send Message Flow (6 тестов)
- ✅ Edit Message Flow (6 тестов)
- ✅ Delete Message Flow (6 тестов)
- ✅ Reply & Forward Flow (8 тестов)
- 📋 Reactions, Search, Drafts, Navigation, Profile, Network (0/48)
Подробный план: [TESTING_ROADMAP.md](TESTING_ROADMAP.md)
Runtime-конфиг создаётся в `~/.config/tele-tui/config.toml`; пример лежит в [config.toml.example](config.toml.example).
## Документация
- [INSTALL.md](INSTALL.md) — подробная инструкция по установке
- [HOTKEYS.md](HOTKEYS.md) — все горячие клавиши
- [FAQ.md](FAQ.md) — часто задаваемые вопросы
- [CONTRIBUTING.md](CONTRIBUTING.md) — как внести вклад
- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) — структура проекта
- [SECURITY.md](SECURITY.md) политика безопасности
- [CHANGELOG.md](CHANGELOG.md) — история изменений
- [REQUIREMENTS.md](REQUIREMENTS.md) — функциональные требования
- [DEVELOPMENT.md](DEVELOPMENT.md) — правила разработки
- [ROADMAP.md](ROADMAP.md) — план развития проекта
- [REFACTORING_ROADMAP.md](REFACTORING_ROADMAP.md) — план рефакторинга кода
- [TESTING_ROADMAP.md](TESTING_ROADMAP.md) — план покрытия тестами
- [TESTING_PROGRESS.md](TESTING_PROGRESS.md) — прогресс тестирования
- [CONTEXT.md](CONTEXT.md) — текущий статус разработки
- [AGENT.md](AGENT.md) - краткие правила для агента.
- [DEVELOPMENT.md](DEVELOPMENT.md) - локальные правила разработки и проверок.
- [CONTEXT.md](CONTEXT.md) - текущий статус, риски и следующие шаги.
- [docs/HOTKEYS.md](docs/HOTKEYS.md) - горячие клавиши.
- [docs/PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md) - карта подсистем.
- [docs/TDLIB_INTEGRATION.md](docs/TDLIB_INTEGRATION.md) - проектные заметки по TDLib.
## Лицензия

View File

@@ -1,131 +0,0 @@
# TTUI - Требование к приложению
## Описание приложения
Терминальный интерфейс для telegram
## Функциональные требования
### Интерфейс
┌─ TTUI ───────────────────────────────────────────────────────────────────────┐
│ 1:All │ 2:Personal │ 3:Work │ 4:Bots │
├──────────────────────┬───────────────────────────────────────────────────────┤
│ 🔍 Search... │ 👤 Mom (online) │
├──────────────────────┼───────────────────────────────────────────────────────┤
│ 📌 Saved Messages │ Today, Dec 21│
│ ▌ Mom (2)│ │
│ Boss │ Mom ────────────────────────────────────────── 14:20 │
│ Rust Community │ Привет! Ты покормил кота? │
│ Durov │ │
│ News Channel │ You ─────────────────────────────────────── 14:22 ✓✓ │
│ Spam Bot │ Да, конечно. Купил ему корм. │
│ Wife │ Скоро буду дома. │
│ Team Lead │ │
│ DevOps Chat (9)│ Mom ────────────────────────────────────────── 14:23 │
│ Server Alerts │ Отлично, захвати хлеба. │
│ Gym Bro │ │
│ Design Team │ You ─────────────────────────────────────── 14:25 ✓ │
│ Project X │ Ок. │
│ HR │ │
│ Mom's Friend │ │
│ Taxi Bot │ │
│ │ │
│ │ │
│ │ │
│ │ │
│ │ │
├──────────────────────┼───────────────────────────────────────────────────────┤
│ [User: Online] │ > **message**
└──────────────────────┴───────────────────────────────────────────────────────┘
**commands**
### Список желаемого
1) футер - список папок в телеграме
2) список с чатами - лички и группы, сверху инпут для поиска чата
3) основной контент - открытый чат с сообщениями из чата, если никакой чат не открыт, то контент пустой, ничего не показываем. Снизу - инпут для ввода сообщения в чат, который открыт
4) снизу списка чата статус онлайн или нет сам пользователь приложения
5) при открытии чата должна загружаться история чата, а так же подгружаться новые сообщения от собеседника.
6) выделяем сообщения собеседника его никнеймом, группируем его сообщения и разделяем наши сообщения и сообщения собеседника, как на интерфейсе сверху
7) отображаем наше сообщение символом `✓`, если телеграм подтвердил, что сообщение отправлено, и выделяем `✓✓` если собеседник прочитал его
8) при навигации в чате выделяем строку курсивом, при выборе чата (то есть его открытии) ставим в начало символ ▌
9) `(2)` — счетчик непрочитанных (можно красить в красный/зеленый).
10) `muted` — статус чата (серый цвет).
11) `@` — пинг/меншн.
12) с видео/картинками/голосовые пока ничего не делаем, ренденим заглушку (с упоминанием что это картинка или видео и тд)
### Дополнительно реализованные возможности
13) **Markdown форматирование**: жирный, курсив, подчёркивание, зачёркивание, код, спойлеры, ссылки, упоминания
14) **Редактирование сообщений**: ↑ при пустом инпуте → выбор → Enter → редактирование
15) **Удаление сообщений**: d/в/Delete в режиме выбора → подтверждение → удаление
16) **Reply на сообщения**: r/к в режиме выбора → превью → отправка ответа
17) **Forward сообщений**: f/а в режиме выбора → выбор чата → пересылка
18) **Typing indicator**: отображение "печатает..." когда собеседник набирает текст
19) **Закреплённые сообщения**: отображение pinned message вверху чата с переходом
20) **Поиск по сообщениям**: Ctrl+F для поиска внутри чата, n/N для навигации
21) **Черновики**: автосохранение текста при переключении между чатами
22) **Профиль**: i для просмотра информации о пользователе/группе
23) **Копирование**: y/н для копирования сообщения в системный буфер
24) **Реакции**: e/у для добавления реакций, emoji picker с навигацией стрелками
25) **Конфигурация**: ~/.config/tele-tui/config.toml для настройки цветов и timezone
26) **Credentials**: приоритетная загрузка из ~/.config/tele-tui/credentials или .env
27) **Блочный курсор**: Vim-style курсор █ с навигацией ←/→/Home/End
28) **Динамический инпут**: автоматическое расширение до 10 строк
29) **Онлайн-статус**: зелёная точка ● для онлайн пользователей
30) **Индикаторы**: 📌 закреплённые чаты, 🔇 замьюченные, @ упоминания
31) **Состояние сети**: индикатор в футере (⚠ Нет сети, ⏳ Подключение...)
32) **Graceful shutdown**: корректное закрытие при Ctrl+C
### Управление
1) ctrl+c или command+c - выход из программы
2) "h j k l" - влево, вниз, вверх, вправо (навигация в левом столбце) vim-style управление
3) стрелки - управление, так же как и "h j k l"
4) "command + 1", "command + 2" и так далее - переключение между папками, которые созданы в телеграме
5) из интерфейса "**message**" - это инпут для ввода сообщения в открытый чат
6) ctrl + s - фокус в инпут поиска чата
7) Esc - закрытие открытого чата
8) command + стрелка вверх (или ctrl + k) - выделяем самый верхний чат (без открытия)
9) поддержка русской раскладки: "р о л д" соответствует "h j k l"
10) Ctrl+R - обновить список чатов
### Реализованные команды
#### Навигация
```
↑/↓ или k/j (р/о): Navigate | Enter: Open/Send | Esc: Close/Cancel | 1-9: Folders
Ctrl+S: Search Chats | Ctrl+R: Refresh | Ctrl+F: Search in Chat | Ctrl+C: Quit
```
#### Работа с сообщениями
```
↑ (пустой инпут): Select message | Enter: Edit | r/к: Reply | f/а: Forward
d/в/Delete: Delete | y/н: Copy | e/у: React | i: Profile
```
#### Emoji Picker (реакции)
```
←/→/↑/↓: Navigate | Enter: Toggle reaction | Esc: Close
```
## Технологии
Пишем на rust-е
1) ratatui 0.29 - для tui интерфейса
2) tdlib-rs 1.1 - для подключения апи телеграма (обёртка над TDLib)
3) tokio 1.x - async runtime
4) crossterm 0.28 - кроссплатформенный терминал
5) serde + serde_json 1.0 - сериализация/десериализация
6) toml 0.8 - парсинг конфигурации
7) dirs 5.0 - XDG директории (config, data)
8) clipboard 0.5 - работа с системным буфером обмена
9) chrono 0.4 - форматирование даты/времени
10) dotenvy 0.15 - загрузка .env файлов
## Нефункциональные требования
### Производительность
1) программа должна выдавать 60 фпс
2) интерфейс не должен мерцать
3) минимальное разрешение - 600 символов, максимального нет, не ограничиваем

View File

@@ -1,168 +0,0 @@
# Roadmap
## Завершённые фазы
| Фаза | Описание | Ключевые результаты |
|------|----------|---------------------|
| 1 | Базовая инфраструктура | ratatui + crossterm, vim-навигация, русская раскладка |
| 2 | TDLib интеграция | tdlib-rs, авторизация, загрузка чатов и сообщений |
| 3 | Улучшение UX | Отправка, поиск, скролл, realtime обновления |
| 4 | Папки и фильтрация | Загрузка папок из Telegram, переключение 1-9 |
| 5 | Расширенный функционал | Онлайн-статус, галочки прочтения, медиа-заглушки, muted |
| 6 | Полировка | 60 FPS, оптимизация памяти, graceful shutdown, динамический инпут |
| 7 | Рефакторинг памяти | Единый источник данных, LRU-кэш (500 users), lazy loading |
| 8 | Дополнительные фичи | Markdown, edit/delete, reply/forward, блочный курсор |
| 9 | Расширенные возможности | Typing, pinned, поиск в чате, черновики, профиль, копирование, реакции, конфиг |
| 10 | Desktop уведомления (83%) | notify-rust, muted фильтр, mentions, медиа. TODO: кастомные звуки |
| 11 | Inline просмотр фото | Dual renderer (Halfblocks + iTerm2/Sixel), throttling 15 FPS, modal viewer, lazy loading, auto-download |
| 12 | Голосовые сообщения | ffplay player, pause/resume with seek, VoiceCache, AudioConfig, progress bar + waveform UI |
| 13 | Глубокий рефакторинг | 5 файлов (4582->модули), 5 traits, shared components, docs |
---
## Фаза 11: Inline просмотр фото в чате [DONE]
**UX**: Always-show inline preview (50 chars, Halfblocks) -> `v`/`м` открывает fullscreen modal (iTerm2/Sixel) -> `←`/`→` навигация между фото.
### Реализовано:
- [x] **Dual renderer архитектура**:
- `inline_image_renderer`: Halfblocks (быстро, Unicode блоки) для навигации
- `modal_image_renderer`: iTerm2/Sixel (медленно, высокое качество) для просмотра
- [x] **Performance optimizations**:
- Frame throttling: inline 15 FPS, текст 60 FPS
- Lazy loading: только видимые изображения
- LRU cache: max 100 протоколов
- Skip partial rendering (no flickering)
- [x] **UX улучшения**:
- Always-show inline preview (фикс. ширина 50 chars)
- Fullscreen modal на `v`/`м` с aspect ratio
- Loading indicator в модалке
- Navigation hotkeys: `←`/`→` между фото, `Esc`/`q` закрыть
- [x] **Типы и API**:
- `MediaInfo`, `PhotoInfo`, `PhotoDownloadState`, `ImageModalState`
- `ImagesConfig` в config.toml
- Feature flag `images` для зависимостей
- [x] **Media модуль**:
- `cache.rs`: ImageCache (LRU)
- `image_renderer.rs`: new() + new_fast()
- [x] **UI модули**:
- `modals/image_viewer.rs`: fullscreen modal
- `messages.rs`: throttled second-pass rendering
- [x] **Авто-загрузка фото** (bugfix):
- Auto-download последних 30 фото при открытии чата (`open_chat_and_load_data`)
- Download on demand по `v` (вместо "Фото не загружено")
- Retry при ошибке загрузки
- Конфиг: `auto_download_images` + `show_images` в `[images]`
---
## Фаза 12: Прослушивание голосовых сообщений [DONE]
### Этап 1: Инфраструктура аудио [DONE]
- [x] Модуль `src/audio/`
- `player.rs` — AudioPlayer на ffplay (subprocess)
- `cache.rs` — VoiceCache (LRU, configurable size, `~/.cache/tele-tui/voice/`)
- [x] AudioPlayer API: play(), play_from(ss), pause() (SIGSTOP), resume(), resume_from(ss), stop()
- [x] Race condition fix: `starting` flag + pid ownership guard в потоках
- [x] Drop impl для AudioPlayer (убивает ffplay при выходе)
### Этап 2: Интеграция с TDLib [DONE]
- [x] Типы: `VoiceInfo`, `VoiceDownloadState`, `PlaybackState`, `PlaybackStatus`
- [x] Конвертация `MessageVoiceNote` в `message_conversion.rs`
- [x] `download_voice_note()` в TdClientTrait + client_impl + fake
- [x] Методы `has_voice()`, `voice_info()`, `voice_info_mut()` на `MessageInfo`
### Этап 3: UI для воспроизведения [DONE]
- [x] Progress bar (━●─) с позицией и длительностью
- [x] Waveform визуализация (▁▂▃▄▅▆▇█) из base64-encoded TDLib данных
- [x] Иконки статуса: ▶ Playing, ⏸ Paused, ⏹ Stopped
- [x] Throttled redraw: обновление UI только при смене секунды (не 60 FPS)
### Этап 4: Хоткеи [DONE]
- [x] Space — play/pause toggle (запуск + пауза/возобновление с откатом 1s)
- [x] ←/→ — seek ±5 сек (через `resume_from()` — перезапуск ffplay с `-ss`)
- [x] Seek работает и при воспроизведении, и на паузе (на паузе двигает позицию, при resume стартует с неё)
- [x] MoveLeft/MoveRight как alias для SeekBackward/SeekForward (HashMap non-deterministic order fix)
- [x] Автоматическая остановка при навигации на другое сообщение
- [x] Остановка ffplay при выходе из приложения (Ctrl+C)
### Этап 5: Конфигурация и кэш [DONE]
- [x] `AudioConfig` в config.toml (`cache_size_mb`, `auto_download_voice`)
- [x] `DEFAULT_AUDIO_CACHE_SIZE_MB` константа (100 MB)
- [x] Ticker для progress bar в event loop (delta-based position tracking)
- [x] VoiceCache интеграция: проверка кэша перед загрузкой, кэширование после download
### Технические детали
- **Аудио:** ffplay (subprocess), resume/seek через перезапуск с `-ss` offset
- **Race conditions:** `starting` flag предотвращает false `is_stopped()` при старте ffplay; pid ownership guard в потоках предотвращает затирание pid нового процесса старым
- **Keybinding conflict:** Left/Right привязаны к MoveLeft/MoveRight и SeekBackward/SeekForward; HashMap iteration order не гарантирован → оба варианта обрабатываются как seek в режиме выбора сообщения
- **Платформы:** macOS, Linux (везде где есть ffmpeg)
- **Хоткеи:** Space (play/pause), ←/→ (seek ±5s)
---
## Фаза 14: Мультиаккаунт
**Цель**: поддержка нескольких Telegram-аккаунтов с мгновенным переключением внутри приложения.
### UI: Индикатор в footer + хоткеи
```
┌──────────────┬───────────────────────────┐
│ Saved Msgs │ Привет! │
│ Иван Петров │ Как дела? │
│ Работа чат │ │
├──────────────┴───────────────────────────┤
│ [NORMAL] Михаил ⟨1/2⟩ Work(3) │ Ctrl+A │
└──────────────────────────────────────────┘
```
- **Footer**: текущий аккаунт + номер `⟨1/2⟩` + бейджи непрочитанных с других аккаунтов
- **Быстрое переключение**: `Ctrl+1`..`Ctrl+9` — мгновенный switch без модалки
- **Модалка управления** (`Ctrl+A`): список аккаунтов, добавление/удаление, выбор активного
### Модалка переключения аккаунтов
```
┌──────────────────────────────────┐
│ Аккаунты │
│ │
│ 1. Михаил (+7 900 ...) ● │ ← активный
│ 2. Work (+7 911 ...) (3) │ ← 3 непрочитанных
│ 3. + Добавить аккаунт │
│ │
│ [j/k навигация, Enter выбор] │
│ [d — удалить аккаунт] │
└──────────────────────────────────┘
```
### Техническая реализация: все клиенты одновременно
- **Несколько TdClient**: каждый аккаунт — отдельный `TdClient` со своим `database_directory`
- Аккаунт 1: `~/.local/share/tele-tui/accounts/1/tdlib_data/`
- Аккаунт 2: `~/.local/share/tele-tui/accounts/2/tdlib_data/`
- **Все клиенты активны**: polling updates со всех аккаунтов одновременно (уведомления, непрочитанные)
- **Мгновенное переключение**: swap активного `App.td_client` — чаты и сообщения уже загружены
- **Общий конфиг**: `~/.config/tele-tui/config.toml` (один для всех аккаунтов)
- **Профили аккаунтов**: `~/.config/tele-tui/accounts.toml` — список аккаунтов (имя, путь к БД)
### Этапы
- [x] **Этап 1: Инфраструктура профилей** (DONE)
- Структура `AccountProfile` (name, display_name, db_path)
- `accounts.toml` — хранение списка аккаунтов
- Миграция `tdlib_data/``accounts/default/tdlib_data/` (обратная совместимость)
- CLI: `--account <name>` для запуска конкретного аккаунта
- [x] **Этап 2+3: Account Switcher Modal + Переключение** (DONE)
- Подход: single-client reinit (close TDLib → new TdClient → auth)
- Модалка `Ctrl+A` — глобальный оверлей с навигацией j/k
- Footer индикатор `[account_name]` если не "default"
- `AccountSwitcherState` enum (SelectAccount / AddAccount)
- `recreate_client()` метод в TdClientTrait (close → new → set_tdlib_parameters)
- `add_account()` — создание нового аккаунта из модалки
- `pending_account_switch` флаг → обработка в main loop
- [ ] **Этап 4: Расширенные возможности мультиаккаунта**
- Хоткеи `Ctrl+1`..`Ctrl+9` — быстрое переключение
- Бейджи непрочитанных с других аккаунтов (требует множественных TdClient)

74
docs/HOTKEYS.md Normal file
View File

@@ -0,0 +1,74 @@
# Горячие клавиши
Краткий справочник актуальных команд tele-tui. Русская раскладка указана там, где команда имеет отдельный буквенный alias.
## Глобальные команды
| Клавиша | Действие |
|---------|----------|
| `Ctrl+C` | Выход |
| `Ctrl+R` | Обновить список чатов |
| `Ctrl+A` | Открыть переключатель аккаунтов |
| `Esc` | Закрыть модалку, отменить режим или вернуться назад |
| `1`-`9` | Переключить Telegram-папку |
## Навигация
| Клавиша | Русская | Действие |
|---------|---------|----------|
| `j` / `↓` | `о` | Вниз |
| `k` / `↑` | `л` | Вверх |
| `h` / `←` | `р` | Влево / seek назад в голосовом |
| `l` / `→` | `д` | Вправо / seek вперёд в голосовом |
| `Enter` | | Открыть чат, подтвердить выбор или отправить сообщение |
## Режимы ввода
| Клавиша | Русская | Действие |
|---------|---------|----------|
| `i` | `ш` | Перейти в Insert mode |
| `Esc` | | Вернуться из Insert в Normal mode |
| `Home` / `End` | | В начало / конец строки |
| `Backspace` / `Delete` | | Удалить символ слева / справа |
В Normal mode буквенные команды управляют чатом. В Insert mode вводится текст; команды чата блокируются до `Esc`.
## Поиск
| Клавиша | Действие |
|---------|----------|
| `Ctrl+S` | Поиск по чатам |
| `Ctrl+F` | Поиск в текущем чате |
| `n` | Следующий результат |
| `N` | Предыдущий результат |
## Сообщения
| Клавиша | Русская | Действие |
|---------|---------|----------|
| `↑` при пустом вводе | | Выбрать сообщение |
| `Enter` на выбранном | | Редактировать сообщение |
| `r` | `к` | Ответить |
| `f` | `а` | Переслать |
| `d` / `Delete` | `в` | Удалить |
| `y` | `н` | Скопировать текст |
| `e` | `у` | Открыть emoji picker |
| `v` | `м` | Открыть фото в модалке |
| `Space` | | Play / pause голосового сообщения |
| `Ctrl+I` | `Ctrl+Ш` | Профиль чата или пользователя |
## Модалки
| Контекст | Клавиши |
|----------|---------|
| Emoji picker | `←` `→` `↑` `↓`, `Enter`, `Esc` |
| Удаление | `y` / `н` / `Enter` подтвердить, `n` / `т` / `Esc` отменить |
| Пересылка | `j`/`k` или стрелки для выбора чата, `Enter`, `Esc` |
| Просмотр фото | `←`/`→` между фото, `Esc` закрыть |
| Аккаунты | `j`/`k`, `Enter`, `a` добавить, `Esc` закрыть |
## Подсказки
- Footer показывает доступные команды для текущего состояния.
- При открытой модалке обрабатываются только команды этой модалки.
- Русская vim-раскладка: `h/j/k/l` соответствует `р/о/л/д`.

60
docs/PROJECT_STRUCTURE.md Normal file
View File

@@ -0,0 +1,60 @@
# Структура проекта
Карта подсистем без полного дерева файлов.
## Поток данных
```text
TDLib updates -> mpsc channel -> App state -> UI render
User input -> input handlers -> App methods -> TdClientTrait -> TDLib API
```
`main.rs` держит event loop: клавиатура, TDLib updates, фоновые задачи и отрисовка по `needs_redraw`.
## Подсистемы
| Путь | Назначение |
|------|------------|
| `src/app/` | Центральное состояние `App<T: TdClientTrait>`, экраны, режимы и modal state |
| `src/app/methods/` | Trait-based методы navigation/messages/compose/search/modal |
| `src/input/` | Роутинг клавиатуры и обработчики global/chat/compose/modal/search |
| `src/tdlib/` | TDLib wrapper: auth, chats, users, reactions, updates, conversion, operations |
| `src/ui/` | Отрисовка экранов, списка чатов, сообщений, footer, compose bar и модалок |
| `src/config/` | TOML config, credentials, keybindings, validation |
| `src/accounts/` | Профили, `accounts.toml`, migration, per-account lock files |
| `src/audio/` | Голосовые сообщения через `ffplay` и `VoiceCache` |
| `src/media/` | Inline images и modal viewer под feature `images` |
| `src/utils/` | Форматирование, validation, retry, TDLib log helpers |
## State
`ChatState` описывает режим открытого чата: normal selection, editing, reply, forward, delete confirmation, reaction picker, profile, search, pinned messages.
Отдельно есть `InputMode`: в Normal mode работают команды, в Insert mode вводится текст.
## TDLib слой
- `TdClientTrait` задаёт API для приложения и тестов.
- Реальный клиент живёт в `TdClient`, тесты используют `FakeTdClient`.
- Updates должны фильтроваться по активному `client_id`, особенно для мультиаккаунта.
## Тесты
| Область | Где искать |
|---------|------------|
| Test builders и fake TDLib | `tests/helpers/` |
| Snapshot UI | `tests/*` + `tests/snapshots/` |
| Навигация и ввод | `tests/input_navigation.rs`, `tests/vim_mode.rs`, `tests/navigation.rs` |
| Аккаунты | `tests/accounts.rs`, `tests/account_switcher.rs` |
| User journeys | `tests/e2e_smoke.rs`, `tests/e2e_user_journey.rs` |
## Runtime-файлы
| Путь | Назначение |
|------|------------|
| `~/.config/tele-tui/config.toml` | Пользовательская конфигурация |
| `~/.config/tele-tui/credentials` | `API_ID` и `API_HASH` |
| `~/.config/tele-tui/accounts.toml` | Список аккаунтов |
| `~/.local/share/tele-tui/accounts/{name}/tdlib_data/` | TDLib database аккаунта |
| `~/.local/share/tele-tui/accounts/{name}/tele-tui.lock` | Lock активного аккаунта |
| `~/.cache/tele-tui/voice/` | Кэш голосовых сообщений |

View File

@@ -1,636 +1,45 @@
# Интеграция TDLib в Telegram TUI
# TDLib integration notes
## Обзор
Проект использует `tdlib-rs` с feature `download-tdlib`. Актуальные версии смотри в `Cargo.toml`.
TDLib (Telegram Database Library) — это официальная кроссплатформенная библиотека для создания Telegram клиентов. Она предоставляет полный функционал Telegram API с автоматическим управлением сессиями, кэшированием и синхронизацией.
## Runtime
## Выбор библиотеки для Rust
- Credentials: `~/.config/tele-tui/credentials`, fallback `.env`.
- TDLib data: `~/.local/share/tele-tui/accounts/{name}/tdlib_data/`.
- Каждый аккаунт защищён lock file: `~/.local/share/tele-tui/accounts/{name}/tele-tui.lock`.
Существует несколько Rust-оберток для TDLib:
## Auth flow
### 1. rust-tdlib
- **GitHub**: [antonio-antuan/rust-tdlib](https://github.com/antonio-antuan/rust-tdlib)
- **docs.rs**: https://docs.rs/rust-tdlib
- **Особенности**:
- Async/await с tokio
- Client/Worker архитектура
- Требует предварительной сборки TDLib
TDLib авторизация идёт через `updateAuthorizationState`:
### 2. tdlib-rs (Рекомендуется)
- **GitHub**: [FedericoBruzzone/tdlib-rs](https://github.com/FedericoBruzzone/tdlib-rs)
- **crates.io**: https://crates.io/crates/tdlib-rs
- **docs.rs**: https://docs.rs/tdlib/latest/tdlib/
- **Преимущества**:
-Не требует предварительной установки TDLib
- ✅ Кроссплатформенность (Windows, Linux, macOS)
- ✅ Автоматическая загрузка прекомпилированных бинарников
- ✅ Поддержка TDLib v1.8.29
- ✅ Автогенерация типов из TL схемы
## Установка tdlib-rs
### Вариант 1: Автоматическая загрузка (Рекомендуется)
**Cargo.toml:**
```toml
[dependencies]
tdlib-rs = { version = "0.3", features = ["download-tdlib"] }
tokio = { version = "1", features = ["full"] }
[build-dependencies]
tdlib-rs = { version = "0.3", features = ["download-tdlib"] }
```text
WaitTdlibParameters -> WaitPhoneNumber -> WaitCode -> WaitPassword? -> Ready
```
**build.rs:**
```rust
fn main() {
tdlib_rs::build::build(None);
}
```
UI должен показывать понятное состояние и не блокировать event loop дольше необходимого.
### Вариант 2: Локальная установка TDLib
## Updates
Если TDLib уже установлен (версия 1.8.29):
- Receive loop передаёт в UI пару `(client_id, Update)`.
- UI применяет update только если `client_id == active_client_id`.
- При переключении аккаунта pending updates старого клиента должны игнорироваться.
```bash
export LOCAL_TDLIB_PATH=$HOME/lib/tdlib
```
Это критично для multi-account: без фильтрации старый клиент может испортить state нового аккаунта.
```toml
[dependencies]
tdlib-rs = { version = "0.3", features = ["local-tdlib"] }
```
## Messages
### Вариант 3: Через pkg-config
- История грузится порциями, открытие чата не должно блокировать первый redraw.
- Reply-info и downloads уходят в фоновые задачи.
- Read/unread для исходящих сообщений должен опираться на `last_read_outbox_message_id`.
- Send/edit/delete/forward/search проходят через `TdClientTrait`, чтобы тесты могли использовать `FakeTdClient`.
```bash
export PKG_CONFIG_PATH=$HOME/lib/tdlib/lib/pkgconfig/:$PKG_CONFIG_PATH
export LD_LIBRARY_PATH=$HOME/lib/tdlib/lib/:$LD_LIBRARY_PATH
```
## Folders
```toml
[dependencies]
tdlib-rs = { version = "0.3", features = ["pkg-config"] }
```
Telegram-папки доступны через TDLib chat folders и переключаются клавишами `1`-`9`.
## Архитектура TDLib
## Практические правила
### Основные компоненты
```
┌─────────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────────┤
│ ┌────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ Client │ │ Update Stream │ │ API Requests │ │
│ └────────────┘ └──────────────┘ └────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ TDLib Client │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Auth State │ │ Local Cache │ │ API Handler │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Telegram Servers │
└─────────────────────────────────────────────────────────┘
```
### Поток работы
1. **Инициализация** → TDLib запускается с параметрами
2. **Авторизация** → Проход через стейт-машину авторизации
3. **Синхронизация** → Загрузка базовых данных (чаты, контакты)
4. **Updates Stream** → Постоянный поток обновлений от сервера
5. **API Requests** → Запросы на получение данных / отправку сообщений
## Процесс авторизации
### Стейт-машина авторизации
TDLib работает через систему состояний. Приложение получает обновления `updateAuthorizationState` и реагирует на них:
```
authorizationStateWaitTdlibParameters
↓ (вызываем setTdlibParameters)
authorizationStateWaitPhoneNumber
↓ (вызываем setAuthenticationPhoneNumber)
authorizationStateWaitCode
↓ (вызываем checkAuthenticationCode)
authorizationStateWaitPassword (опционально, если 2FA)
↓ (вызываем checkAuthenticationPassword)
authorizationStateReady ✓
```
### Шаг 1: Получение API ключей
Перед началом работы нужно:
1. Зайти на https://my.telegram.org
2. Войти с номером телефона
3. Перейти в "API development tools"
4. Создать приложение и получить `api_id` и `api_hash`
### Шаг 2: Инициализация TDLib
```rust
use tdlib::{functions, types};
async fn init_tdlib() {
// Параметры инициализации
let params = types::TdlibParameters {
database_directory: "./tdlib_db".to_string(),
use_message_database: true,
use_secret_chats: true,
api_id: env::var("API_ID").unwrap().parse().unwrap(),
api_hash: env::var("API_HASH").unwrap(),
system_language_code: "en".to_string(),
device_model: "Desktop".to_string(),
system_version: "Unknown".to_string(),
application_version: "0.1.0".to_string(),
enable_storage_optimizer: true,
ignore_file_names: false,
};
// Отправляем параметры
functions::set_tdlib_parameters(params, &client).await?;
}
```
### Шаг 3: Ввод номера телефона
```rust
async fn authenticate_with_phone(phone: String, client: &Client) {
let phone_number = types::SetAuthenticationPhoneNumber {
phone_number: phone,
settings: None,
};
functions::set_authentication_phone_number(phone_number, client).await?;
}
```
### Шаг 4: Ввод кода подтверждения
```rust
async fn verify_code(code: String, client: &Client) {
let check_code = types::CheckAuthenticationCode {
code,
};
functions::check_authentication_code(check_code, client).await?;
}
```
### Шаг 5: Ввод пароля 2FA (если включен)
```rust
async fn verify_password(password: String, client: &Client) {
let check_password = types::CheckAuthenticationPassword {
password,
};
functions::check_authentication_password(check_password, client).await?;
}
```
## Получение списка чатов
### Концепция чатов в TDLib
TDLib автоматически кэширует чаты локально. Приложение должно:
1. Подписаться на обновления `updateNewChat`
2. Вызвать `loadChats()` для загрузки чатов
3. Поддерживать локальный кэш с сортировкой
### Типы списков чатов
- **Main** — основные чаты
- **Archive** — архивные чаты
- **Folder** — пользовательские папки
### Загрузка чатов
```rust
use tdlib::{functions, types};
async fn load_chats(client: &Client) -> Result<Vec<Chat>> {
// Указываем тип списка (Main, Archive, или конкретная папка)
let chat_list = types::ChatList::Main;
// Загружаем чаты
// limit - количество чатов для загрузки
functions::load_chats(
types::LoadChats {
chat_list: Some(chat_list),
limit: 50,
},
client
).await?;
// После вызова loadChats, чаты будут приходить через updateNewChat
Ok(vec![])
}
```
### Получение информации о чате
```rust
async fn get_chat_info(chat_id: i64, client: &Client) -> Result<types::Chat> {
let chat = functions::get_chat(
types::GetChat { chat_id },
client
).await?;
Ok(chat)
}
```
### Сортировка чатов
Чаты нужно сортировать по паре `(position.order, chat.id)` в порядке убывания:
```rust
chats.sort_by(|a, b| {
let order_a = a.positions.get(0).map(|p| p.order).unwrap_or(0);
let order_b = b.positions.get(0).map(|p| p.order).unwrap_or(0);
order_b.cmp(&order_a)
.then_with(|| b.id.cmp(&a.id))
});
```
## Получение истории сообщений
### Загрузка сообщений из чата
```rust
async fn get_chat_history(
chat_id: i64,
from_message_id: i64,
limit: i32,
client: &Client
) -> Result<Vec<types::Message>> {
let history = functions::get_chat_history(
types::GetChatHistory {
chat_id,
from_message_id, // 0 для последних сообщений
offset: 0,
limit,
only_local: false,
},
client
).await?;
Ok(history.messages.unwrap_or_default())
}
```
### Пагинация сообщений
Сообщения возвращаются в обратном хронологическом порядке (новые → старые).
Для загрузки следующей страницы:
```rust
// Первая загрузка (последние сообщения)
let messages = get_chat_history(chat_id, 0, 50, &client).await?;
// Загрузка более старых сообщений
if let Some(oldest_msg) = messages.last() {
let older_messages = get_chat_history(
chat_id,
oldest_msg.id,
50,
&client
).await?;
}
```
## Обработка обновлений (Updates Stream)
### Типы обновлений
TDLib отправляет обновления через `Update` enum:
- `UpdateNewMessage` — новое сообщение
- `UpdateMessageContent` — изменение контента сообщения
- `UpdateMessageSendSucceeded` — сообщение успешно отправлено
- `UpdateMessageSendFailed` — ошибка отправки
- `UpdateChatLastMessage` — изменилось последнее сообщение чата
- `UpdateChatPosition` — изменилась позиция чата в списке
- `UpdateNewChat` — новый чат добавлен
- `UpdateUser` — обновилась информация о пользователе
- `UpdateUserStatus` — изменился статус пользователя (онлайн/оффлайн)
- `UpdateChatReadInbox` — прочитаны входящие сообщения
- `UpdateChatReadOutbox` — прочитаны исходящие сообщения
### Слушатель обновлений
```rust
use tdlib::types::Update;
async fn handle_updates(client: Client) {
loop {
match client.receive() {
Some(Update::NewMessage(update)) => {
println!("New message in chat {}: {}",
update.message.chat_id,
update.message.content
);
}
Some(Update::MessageSendSucceeded(update)) => {
println!("Message sent successfully: {}", update.message.id);
}
Some(Update::UserStatus(update)) => {
println!("User {} is now {:?}",
update.user_id,
update.status
);
}
Some(Update::NewChat(update)) => {
println!("New chat added: {}", update.chat.title);
}
_ => {}
}
}
}
```
## Отправка сообщений
### Отправка текстового сообщения
```rust
async fn send_message(
chat_id: i64,
text: String,
client: &Client
) -> Result<types::Message> {
let input_content = types::InputMessageContent::InputMessageText(
types::InputMessageText {
text: types::FormattedText {
text,
entities: vec![],
},
disable_web_page_preview: false,
clear_draft: true,
}
);
let message = functions::send_message(
types::SendMessage {
chat_id,
message_thread_id: 0,
reply_to: None,
options: None,
reply_markup: None,
input_message_content: input_content,
},
client
).await?;
Ok(message)
}
```
### Статусы доставки и прочтения
Для отображения ✓ и ✓✓:
```rust
fn get_message_status(message: &types::Message) -> &str {
if message.is_outgoing {
match &message.sending_state {
Some(types::MessageSendingState::Pending) => "", // отправляется
Some(types::MessageSendingState::Failed(_)) => "", // ошибка
None => {
// Отправлено успешно
if message.chat_id > 0 { // личный чат
// Проверяем, прочитано ли
// (нужно следить за UpdateChatReadOutbox)
"✓✓" // или "✓" если не прочитано
} else {
"" // групповой чат
}
}
}
} else {
"" // входящее сообщение
}
}
```
## Работа с папками (Folders)
### Получение списка папок
```rust
async fn get_chat_folders(client: &Client) -> Result<Vec<types::ChatFolderInfo>> {
let folders = functions::get_chat_folders(
types::GetChatFolders {},
client
).await?;
Ok(folders.chat_folders)
}
```
### Фильтрация чатов по папке
```rust
async fn get_chats_in_folder(folder_id: i32, client: &Client) {
let chat_list = types::ChatList::Folder {
chat_folder_id: folder_id
};
functions::load_chats(
types::LoadChats {
chat_list: Some(chat_list),
limit: 50,
},
client
).await?;
}
```
## Архитектура приложения
### Рекомендуемая структура
```
src/
├── main.rs # Entry point, UI loop
├── tdlib/
│ ├── mod.rs # TDLib module
│ ├── client.rs # Client wrapper
│ ├── auth.rs # Authentication logic
│ └── updates.rs # Update handlers
├── ui/
│ ├── mod.rs
│ ├── app.rs # App state
│ ├── layout.rs # UI layout
│ └── components/ # UI components
└── models/
├── chat.rs # Chat models
└── message.rs # Message models
```
### Разделение ответственности
1. **TDLib Client** — управление клиентом, запросы к API
2. **Update Handler** — обработка обновлений в фоне
3. **App State** — состояние приложения (чаты, сообщения, UI)
4. **UI Layer** — отрисовка интерфейса (ratatui)
### Коммуникация между слоями
```rust
// Используем каналы для коммуникации
use tokio::sync::mpsc;
#[derive(Debug)]
enum AppEvent {
NewMessage(Message),
ChatUpdated(Chat),
UserStatusChanged(i64, UserStatus),
}
#[tokio::main]
async fn main() {
// Канал для событий от TDLib
let (tx, mut rx) = mpsc::channel::<AppEvent>(100);
// Запускаем TDLib в отдельной задаче
tokio::spawn(async move {
run_tdlib_client(tx).await;
});
// Основной UI loop
loop {
// Проверяем события
while let Ok(event) = rx.try_recv() {
match event {
AppEvent::NewMessage(msg) => {
// Обновляем UI
}
_ => {}
}
}
// Отрисовываем UI
terminal.draw(|f| ui(f, &app))?;
// Обрабатываем ввод пользователя
handle_input()?;
}
}
```
## Пример: Минимальный клиент
```rust
use tdlib::{Client, ClientState, functions, types};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Создаем клиент
let (sender, mut receiver) = mpsc::channel(100);
let client = Client::new(sender);
// 2. Запускаем клиент
tokio::spawn(async move {
client.start().await;
});
// 3. Ждем авторизации
let mut authorized = false;
while let Some(update) = receiver.recv().await {
match update {
types::Update::AuthorizationState(state) => {
match state.authorization_state {
types::AuthorizationState::WaitTdlibParameters => {
// Отправляем параметры
init_tdlib(&client).await?;
}
types::AuthorizationState::WaitPhoneNumber => {
// Запрашиваем номер у пользователя
let phone = read_phone_from_user();
authenticate_with_phone(phone, &client).await?;
}
types::AuthorizationState::WaitCode(_) => {
// Запрашиваем код
let code = read_code_from_user();
verify_code(code, &client).await?;
}
types::AuthorizationState::Ready => {
authorized = true;
break;
}
_ => {}
}
}
_ => {}
}
}
// 4. Загружаем чаты
if authorized {
load_chats(&client).await?;
// 5. Слушаем обновления
while let Some(update) = receiver.recv().await {
handle_update(update);
}
}
Ok(())
}
```
## Best Practices
### 1. Кэширование
- Всегда включай `use_message_database: true`
- Храни кэш чатов и сообщений в памяти
- Используй `only_local: true` для быстрого доступа
### 2. Обработка ошибок
- Все TDLib функции возвращают `Result`
- Обрабатывай потерю соединения
- Переподключайся при ошибках сети
### 3. Производительность
- Не загружай все чаты сразу (используй пагинацию)
- Лимитируй количество сообщений в истории
- Используй `offset` для ленивой загрузки
### 4. UI/UX
- Показывай индикаторы загрузки
- Кэшируй отрисованные элементы
- Обновляй UI только при изменениях
## Полезные ссылки
### Официальная документация
- [TDLib Getting Started](https://core.telegram.org/tdlib/getting-started)
- [TDLib Documentation](https://core.telegram.org/tdlib/docs/)
### Rust библиотеки
- [rust-tdlib GitHub](https://github.com/antonio-antuan/rust-tdlib)
- [rust-tdlib docs.rs](https://docs.rs/rust-tdlib)
- [tdlib-rs GitHub](https://github.com/FedericoBruzzone/tdlib-rs)
- [tdlib-rs docs.rs](https://docs.rs/tdlib/latest/tdlib/)
### API Reference
- [tdlib::functions](https://docs.rs/tdlib/latest/tdlib/functions/index.html)
- [tdlib::types](https://docs.rs/tdlib-types/latest/tdlib_types/types/index.html)
## Следующие шаги
1. ✅ Изучить документацию TDLib
2. ⬜ Добавить зависимость tdlib-rs в проект
3. ⬜ Реализовать модуль авторизации
4. ⬜ Реализовать загрузку чатов
5. ⬜ Реализовать загрузку сообщений
6. ⬜ Интегрировать с существующим UI
7. ⬜ Добавить отправку сообщений
8. ⬜ Реализовать обработку обновлений в реальном времени
- Не добавлять долгие `await` в UI loop без явной причины.
- Не смешивать updates разных `client_id`.
- Не обходить `TdClientTrait` в коде, который должен тестироваться.
- Не хранить TDLib database в корне проекта; использовать XDG account paths.