docs: clean up project markdown #27
17
AGENT.md
Normal file
17
AGENT.md
Normal 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), только если изменились статус, риск, архитектурное решение или следующий шаг.
|
||||
29
CLAUDE.md
29
CLAUDE.md
@@ -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 тестированию
|
||||
|
||||
|
||||
|
||||
359
CONTEXT.md
359
CONTEXT.md
@@ -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/`.
|
||||
|
||||
123
DEVELOPMENT.md
123
DEVELOPMENT.md
@@ -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-проверки не требуются.
|
||||
|
||||
194
HOTKEYS.md
194
HOTKEYS.md
@@ -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` всегда отменяет текущее действие и возвращает на шаг назад
|
||||
- Блочный курсор █ показывает текущую позицию при редактировании текста
|
||||
@@ -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
256
README.md
@@ -1,261 +1,69 @@
|
||||
# tele-tui
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.rust-lang.org/)
|
||||
|
||||
Консольный Telegram клиент с Vim-style навигацией.
|
||||
|
||||

|
||||
Консольный 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.
|
||||
|
||||
## Лицензия
|
||||
|
||||
|
||||
131
REQUIREMENTS.md
131
REQUIREMENTS.md
@@ -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 символов, максимального нет, не ограничиваем
|
||||
168
ROADMAP.md
168
ROADMAP.md
@@ -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
74
docs/HOTKEYS.md
Normal 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
60
docs/PROJECT_STRUCTURE.md
Normal 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/` | Кэш голосовых сообщений |
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user