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
|
Фаза 14: мультиаккаунт, в работе.
|
||||||
|
|
||||||
Защита от запуска двух экземпляров tele-tui с одним аккаунтом + логирование ошибок TDLib.
|
Цель фазы: безопасные профили Telegram-аккаунтов, переключение аккаунтов и подготовка к более быстрому multi-client UX.
|
||||||
|
|
||||||
**Проблема**: При запуске второго экземпляра с тем же аккаунтом, TDLib не может залочить свою БД. `set_tdlib_parameters` молча падает (`let _ = ...`), и приложение зависает на "Инициализация TDLib...".
|
## Уже сделано
|
||||||
|
|
||||||
**Решение**: Advisory file locks через `fs2` (flock):
|
- Профили аккаунтов: `AccountProfile`, `accounts.toml`, XDG data paths.
|
||||||
- **Lock файл**: `~/.local/share/tele-tui/accounts/{name}/tele-tui.lock`
|
- Миграция старого `tdlib_data/` в per-account directory.
|
||||||
- **Автоматическое освобождение** при crash/SIGKILL (ядро ОС закрывает file descriptors)
|
- CLI `--account <name>`.
|
||||||
- **При старте**: acquire lock ДО `enable_raw_mode()` → ошибка выводится в обычный терминал
|
- Account switcher modal по `Ctrl+A`: выбор и добавление аккаунта.
|
||||||
- **При переключении аккаунтов**: acquire new → release old → switch (при ошибке — остаёмся на старом)
|
- Single-client переключение через `recreate_client()`.
|
||||||
- **Логирование**: `set_tdlib_parameters` ошибки теперь логируются через `tracing::error!`
|
- Footer показывает имя аккаунта, если он не `default`.
|
||||||
|
- Per-account lock file защищает TDLib database от двух процессов.
|
||||||
**Новые файлы:**
|
- TDLib receive loop передаёт `client_id`; UI применяет updates только активного клиента.
|
||||||
- `src/accounts/lock.rs` — `acquire_lock()`, `release_lock()`, `account_lock_path()` + 4 теста
|
- `pending_chat_init` не должен блокировать первый redraw; reply-info и photo downloads уходят в фоновые tasks.
|
||||||
|
- Keybindings стали детерминированными; русская vim-раскладка: `h/j/k/l` -> `р/о/л/д`.
|
||||||
**Модифицированные файлы:**
|
- `AudioPlayer` проверяет наличие `ffplay`.
|
||||||
- `Cargo.toml` — зависимость `fs2 = "0.4"`
|
- `message_grouping` группирует альбомы без клонирования сообщений.
|
||||||
- `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()`
|
- Быстрые hotkeys `Ctrl+1`..`Ctrl+9` для аккаунтов без модалки.
|
||||||
|
- Удаление/переименование аккаунта в account switcher.
|
||||||
---
|
- Бейджи непрочитанных с других аккаунтов.
|
||||||
|
- Решить, нужен ли переход от single-client reinit к одновременным клиентам.
|
||||||
### Photo Albums (Media Groups) — DONE
|
- Добавить/уточнить tests для accounts + TDLib update routing.
|
||||||
|
|
||||||
Фото-альбомы (несколько фото в одном сообщении) теперь группируются в один пузырь с сеткой фото.
|
## Риски
|
||||||
|
|
||||||
**Проблема**: TDLib отправляет альбомы как отдельные `Message` с общим `media_album_id: i64`. Ранее проект это поле игнорировал — каждое фото отображалось как отдельный пузырь.
|
- Multi-account код должен фильтровать TDLib updates по `client_id`.
|
||||||
|
- Инициализация чата и фоновые downloads не должны блокировать первый redraw.
|
||||||
**Решение:**
|
- Read/unread статус исходящих сообщений зависит от корректной TDLib metadata.
|
||||||
|
- Конфликтующие keybindings должны оставаться детерминированными.
|
||||||
1. **Data Model** — `media_album_id: i64` в `MessageMetadata`, `MessageBuilder`, getter `MessageInfo::media_album_id()`. Оба конвертера (async + sync) передают поле из TDLib.
|
- Переключение аккаунтов требует проверки lock release/acquire и auth flow.
|
||||||
|
|
||||||
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`).
|
- Главный state хранится в `App<T: TdClientTrait>`, чтобы тесты могли использовать `FakeTdClient`.
|
||||||
|
- Методы `App` разбиты на traits: navigation, messages, compose, search, modal.
|
||||||
4. **`render_album_bubble()`** — сетка фото (до 3 в ряд), `DeferredImageRender` с `x_offset` для каждого фото, общая подпись и timestamp, индикация выбора, статусы загрузки.
|
- UI рендерится только при `needs_redraw`; текстовый интерфейс целится в 60 FPS.
|
||||||
|
- Фото под feature `images`: inline Halfblocks + modal iTerm2/Sixel.
|
||||||
5. **Integration** — `Album` arm в `render_message_list`, `x_offset` в second pass. Без feature `images` — fallback через отдельные bubble.
|
- Голосовые сообщения проигрываются через `ffplay`, cache живёт в `~/.cache/tele-tui/voice/`.
|
||||||
|
- Credentials читаются из `~/.config/tele-tui/credentials`, fallback `.env`.
|
||||||
**Модифицированные файлы:**
|
- TDLib data аккаунтов хранится в `~/.local/share/tele-tui/accounts/{name}/tdlib_data/`.
|
||||||
- `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)
|
|
||||||
|
|||||||
123
DEVELOPMENT.md
123
DEVELOPMENT.md
@@ -1,110 +1,43 @@
|
|||||||
# Правила локальной разработки
|
# Разработка
|
||||||
|
|
||||||
> **Обязательно к прочтению перед началом работы!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Инструменты
|
## Инструменты
|
||||||
|
|
||||||
### MCP серверы
|
- Для поиска используй `rg`.
|
||||||
- **Serena** — для работы с кодом (символьная навигация, редактирование)
|
- Для точечной навигации по Rust-коду можно использовать Serena.
|
||||||
- **Context7** — для получения актуальной документации по библиотекам
|
- Для актуальной документации библиотек используй Context7, если это нужно для изменения.
|
||||||
|
|
||||||
Используй эти инструменты для эффективной работы с кодовой базой.
|
## Cargo-команды
|
||||||
|
|
||||||
---
|
Агентам нельзя самостоятельно запускать:
|
||||||
|
|
||||||
## Правила работы
|
```bash
|
||||||
|
|
||||||
### 1. Никогда не запускай сервисы самостоятельно
|
|
||||||
|
|
||||||
**ЗАПРЕЩЕНО** запускать `cargo run`, `cargo build` и подобные команды.
|
|
||||||
|
|
||||||
**Вместо этого попроси пользователя запустить:**
|
|
||||||
|
|
||||||
```
|
|
||||||
Запусти, пожалуйста:
|
|
||||||
cargo run
|
cargo run
|
||||||
|
cargo build
|
||||||
|
cargo test
|
||||||
|
cargo check
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Тестирование — только ручное
|
Исключение: пользователь прямо попросил запустить конкретную cargo-команду.
|
||||||
|
|
||||||
После завершения задачи:
|
В финальном ответе после изменения укажи, какие cargo-команды не запускались, и дай минимальную ручную проверку.
|
||||||
1. Опиши сценарии для проверки
|
|
||||||
2. Попроси пользователя проверить вручную
|
|
||||||
3. Дождись фидбека
|
|
||||||
|
|
||||||
**Формат:**
|
## Scope
|
||||||
```
|
|
||||||
Готово! Проверь, пожалуйста:
|
|
||||||
|
|
||||||
1. Открой cargo run
|
- Делай одну логическую правку за раз.
|
||||||
2. понавигируйся в списке чатов кнопками h j k l
|
- Не смешивай feature work, рефакторинг и документацию без необходимости.
|
||||||
3. Нажми Enter для открытия чата
|
- Не откатывай чужие изменения в dirty worktree.
|
||||||
4. Убедись, что чат прогурзился
|
- Не используй destructive git-команды без явной просьбы.
|
||||||
|
|
||||||
Напиши, если что-то не работает.
|
## Ручная проверка
|
||||||
|
|
||||||
|
Формат после функциональной правки:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Проверь:
|
||||||
|
1. Запусти ...
|
||||||
|
2. Открой ...
|
||||||
|
3. Выполни ...
|
||||||
|
4. Ожидаемый результат: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Работа поэтапно
|
Для чисто документационных задач cargo-проверки не требуются.
|
||||||
|
|
||||||
Делай работу **небольшими итерациями**:
|
|
||||||
|
|
||||||
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
|
|
||||||
- никогда не добавляй себя в соавторов в тексте коммита
|
|
||||||
|
|||||||
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
|
# tele-tui
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
Консольный Telegram-клиент на Rust с Vim-style навигацией, Normal/Insert режимами, TDLib и поддержкой русской раскладки.
|
||||||
[](https://www.rust-lang.org/)
|
|
||||||
|
|
||||||
Консольный Telegram клиент с Vim-style навигацией.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
- **Полная интеграция с Telegram**: отправка/получение сообщений, редактирование, удаление, пересылка
|
- Авторизация через TDLib, список чатов, история и realtime updates.
|
||||||
- **Vim-style навигация**: hjkl + поддержка русской раскладки (ролд)
|
- Отправка, редактирование, удаление, reply, forward, copy и реакции.
|
||||||
- **Markdown форматирование**: жирный, курсив, подчёркивание, зачёркивание, код, спойлеры, ссылки
|
- Поиск по чатам и внутри открытого чата.
|
||||||
- **Реакции на сообщения**: emoji picker с навигацией стрелками
|
- Telegram-папки, pinned/muted/mentions/unread indicators.
|
||||||
- **Папки Telegram**: переключение между папками (1-9)
|
- Markdown entities, inline фото, fullscreen image modal и фото-альбомы.
|
||||||
- **Поиск**: по чатам (Ctrl+S) и внутри чата (Ctrl+F)
|
- Голосовые сообщения через `ffplay`.
|
||||||
- **Черновики**: автосохранение набранного текста при переключении чатов
|
- Desktop notifications, clipboard и открытие URL через feature flags.
|
||||||
- **Typing indicator**: показывает когда собеседник печатает
|
- Multi-account profiles, account switcher и per-account lock files.
|
||||||
- **Закреплённые сообщения**: отображение и переход к закреплённому сообщению
|
|
||||||
- **Копирование в буфер**: copy сообщений в системный буфер обмена
|
|
||||||
- **Профиль**: просмотр информации о пользователе/чате
|
|
||||||
- **Конфигурация**: настройка цветов и часового пояса через TOML
|
|
||||||
- **Оптимизация**: 60 FPS, умное кеширование, graceful shutdown
|
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
### Требования
|
Требования:
|
||||||
|
|
||||||
- Rust 1.70+
|
- Rust stable, рекомендовано 1.85+.
|
||||||
- TDLib (скачивается автоматически через tdlib-rs)
|
- TDLib скачивается автоматически через `tdlib-rs` feature `download-tdlib`.
|
||||||
|
- Для голосовых сообщений нужен `ffplay` из ffmpeg.
|
||||||
### Сборка
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/your-username/tele-tui.git
|
|
||||||
cd tele-tui
|
|
||||||
cargo build --release
|
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_ID=your_api_id
|
||||||
API_HASH=your_api_hash
|
API_HASH=your_api_hash
|
||||||
```
|
```
|
||||||
|
|
||||||
Или используйте `.env` файл в директории проекта:
|
Для локальной разработки можно использовать `.env` в корне проекта с теми же ключами.
|
||||||
```
|
|
||||||
API_ID=your_api_id
|
|
||||||
API_HASH=your_api_hash
|
|
||||||
```
|
|
||||||
|
|
||||||
## Использование
|
## Запуск
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run --release
|
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
|
```bash
|
||||||
cargo test
|
cargo run --release -- --account work
|
||||||
```
|
```
|
||||||
|
|
||||||
### Snapshot тесты
|
Runtime-конфиг создаётся в `~/.config/tele-tui/config.toml`; пример лежит в [config.toml.example](config.toml.example).
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
- [INSTALL.md](INSTALL.md) — подробная инструкция по установке
|
- [AGENT.md](AGENT.md) - краткие правила для агента.
|
||||||
- [HOTKEYS.md](HOTKEYS.md) — все горячие клавиши
|
- [DEVELOPMENT.md](DEVELOPMENT.md) - локальные правила разработки и проверок.
|
||||||
- [FAQ.md](FAQ.md) — часто задаваемые вопросы
|
- [CONTEXT.md](CONTEXT.md) - текущий статус, риски и следующие шаги.
|
||||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — как внести вклад
|
- [docs/HOTKEYS.md](docs/HOTKEYS.md) - горячие клавиши.
|
||||||
- [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) — структура проекта
|
- [docs/PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md) - карта подсистем.
|
||||||
- [SECURITY.md](SECURITY.md) — политика безопасности
|
- [docs/TDLIB_INTEGRATION.md](docs/TDLIB_INTEGRATION.md) - проектные заметки по TDLib.
|
||||||
- [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) — текущий статус разработки
|
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
|
|||||||
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
|
TDLib авторизация идёт через `updateAuthorizationState`:
|
||||||
- **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
|
|
||||||
|
|
||||||
### 2. tdlib-rs (Рекомендуется)
|
```text
|
||||||
- **GitHub**: [FedericoBruzzone/tdlib-rs](https://github.com/FedericoBruzzone/tdlib-rs)
|
WaitTdlibParameters -> WaitPhoneNumber -> WaitCode -> WaitPassword? -> Ready
|
||||||
- **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"] }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**build.rs:**
|
UI должен показывать понятное состояние и не блокировать event loop дольше необходимого.
|
||||||
```rust
|
|
||||||
fn main() {
|
|
||||||
tdlib_rs::build::build(None);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Вариант 2: Локальная установка TDLib
|
## Updates
|
||||||
|
|
||||||
Если TDLib уже установлен (версия 1.8.29):
|
- Receive loop передаёт в UI пару `(client_id, Update)`.
|
||||||
|
- UI применяет update только если `client_id == active_client_id`.
|
||||||
|
- При переключении аккаунта pending updates старого клиента должны игнорироваться.
|
||||||
|
|
||||||
```bash
|
Это критично для multi-account: без фильтрации старый клиент может испортить state нового аккаунта.
|
||||||
export LOCAL_TDLIB_PATH=$HOME/lib/tdlib
|
|
||||||
```
|
|
||||||
|
|
||||||
```toml
|
## Messages
|
||||||
[dependencies]
|
|
||||||
tdlib-rs = { version = "0.3", features = ["local-tdlib"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Вариант 3: Через pkg-config
|
- История грузится порциями, открытие чата не должно блокировать первый redraw.
|
||||||
|
- Reply-info и downloads уходят в фоновые задачи.
|
||||||
|
- Read/unread для исходящих сообщений должен опираться на `last_read_outbox_message_id`.
|
||||||
|
- Send/edit/delete/forward/search проходят через `TdClientTrait`, чтобы тесты могли использовать `FakeTdClient`.
|
||||||
|
|
||||||
```bash
|
## Folders
|
||||||
export PKG_CONFIG_PATH=$HOME/lib/tdlib/lib/pkgconfig/:$PKG_CONFIG_PATH
|
|
||||||
export LD_LIBRARY_PATH=$HOME/lib/tdlib/lib/:$LD_LIBRARY_PATH
|
|
||||||
```
|
|
||||||
|
|
||||||
```toml
|
Telegram-папки доступны через TDLib chat folders и переключаются клавишами `1`-`9`.
|
||||||
[dependencies]
|
|
||||||
tdlib-rs = { version = "0.3", features = ["pkg-config"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Архитектура TDLib
|
## Практические правила
|
||||||
|
|
||||||
### Основные компоненты
|
- Не добавлять долгие `await` в UI loop без явной причины.
|
||||||
|
- Не смешивать updates разных `client_id`.
|
||||||
```
|
- Не обходить `TdClientTrait` в коде, который должен тестироваться.
|
||||||
┌─────────────────────────────────────────────────────────┐
|
- Не хранить TDLib database в корне проекта; использовать XDG account paths.
|
||||||
│ 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. ⬜ Реализовать обработку обновлений в реальном времени
|
|
||||||
|
|||||||
Reference in New Issue
Block a user