Prevent running multiple tele-tui instances with the same account by using advisory file locks (flock). Lock is acquired before raw mode so errors print to normal terminal. Account switching acquires new lock before releasing old. Also log set_tdlib_parameters errors via tracing instead of silently discarding them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
21 KiB
Текущий контекст проекта
Статус: Фаза 14 — Мультиаккаунт (IN PROGRESS)
Per-Account Lock File Protection — DONE
Защита от запуска двух экземпляров tele-tui с одним аккаунтом + логирование ошибок TDLib.
Проблема: При запуске второго экземпляра с тем же аккаунтом, TDLib не может залочить свою БД. set_tdlib_parameters молча падает (let _ = ...), и приложение зависает на "Инициализация TDLib...".
Решение: Advisory file locks через fs2 (flock):
- Lock файл:
~/.local/share/tele-tui/accounts/{name}/tele-tui.lock - Автоматическое освобождение при crash/SIGKILL (ядро ОС закрывает file descriptors)
- При старте: acquire lock ДО
enable_raw_mode()→ ошибка выводится в обычный терминал - При переключении аккаунтов: acquire new → release old → switch (при ошибке — остаёмся на старом)
- Логирование:
set_tdlib_parametersошибки теперь логируются черезtracing::error!
Новые файлы:
src/accounts/lock.rs—acquire_lock(),release_lock(),account_lock_path()+ 4 теста
Модифицированные файлы:
Cargo.toml— зависимостьfs2 = "0.4"src/accounts/mod.rs—pub mod lock;+ re-exportssrc/app/mod.rs— полеaccount_lock: Option<File>вApp<T>src/main.rs— acquire lock при старте, lock при переключении аккаунтов, логирование set_tdlib_parameterssrc/tdlib/client.rs— логирование set_tdlib_parameters вrecreate_client()
Photo Albums (Media Groups) — DONE
Фото-альбомы (несколько фото в одном сообщении) теперь группируются в один пузырь с сеткой фото.
Проблема: TDLib отправляет альбомы как отдельные Message с общим media_album_id: i64. Ранее проект это поле игнорировал — каждое фото отображалось как отдельный пузырь.
Решение:
-
Data Model —
media_album_id: i64вMessageMetadata,MessageBuilder, getterMessageInfo::media_album_id(). Оба конвертера (async + sync) передают поле из TDLib. -
Message Grouping — новый вариант
MessageGroup::Album(Vec<MessageInfo>). Сообщения с одинаковымmedia_album_id != 0группируются; одиночное сообщение с album_id остаётсяMessage. -
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). -
render_album_bubble()— сетка фото (до 3 в ряд),DeferredImageRenderсx_offsetдля каждого фото, общая подпись и timestamp, индикация выбора, статусы загрузки. -
Integration —
Albumarm вrender_message_list,x_offsetв second pass. Без featureimages— fallback через отдельные bubble.
Модифицированные файлы:
src/tdlib/types.rs—media_album_idвMessageMetadata,MessageBuilder, gettersrc/tdlib/messages/convert.rs— передачаmedia_album_idв buildersrc/tdlib/message_converter.rs— передачаmedia_album_idв buildersrc/message_grouping.rs—Albumvariant + album detection + 4 новых тестаsrc/constants.rs— album grid constantssrc/ui/components/message_bubble.rs—x_offsetвDeferredImageRender,render_album_bubble()src/ui/components/mod.rs— exportrender_album_bubblesrc/ui/messages.rs—Albumarm +x_offsetв second pass
-
Навигация j/k по альбомам — альбом обрабатывается как одно сообщение.
select_previous_message()/select_next_message()перескакивают через все сообщения альбома.start_message_selection()встаёт на первый элемент альбома если последнее сообщение — часть альбома. -
Тесты — 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_initsrc/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: enumSelectAccount/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—AccountSwitcherStateenum, 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-exportadd_accountsrc/tdlib/trait.rs—recreate_client(&mut self, db_path)в TdClientTraitsrc/tdlib/client.rs— реализацияrecreate_client(close → new → set_tdlib_parameters)src/tdlib/client_impl.rs— trait impl делегированиеtests/helpers/fake_tdclient_impl.rs— no-oprecreate_clientsrc/input/main_input.rs— account_switcher роутинг (highest priority)src/input/handlers/global.rs—Ctrl+A→ open_account_switchersrc/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_switchcheck в 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— enumInputModeapp/mod.rs— полеinput_modeвApp<T>config/keybindings.rs—Command::EnterInsertMode+ keybindingi/шapp/methods/navigation.rs—close_chat()сбрасывает input_modeinput/main_input.rs— главный роутер Insert/Normalinput/handlers/chat.rs— EnterInsertMode, auto-Insert при Reply/Editinput/handlers/chat_list.rs— auto-MessageSelection при открытии чатаui/footer.rs— mode indicatorui/compose_bar.rs— visual mode differentiationtests/— обновлены для нового поведения
Предыдущий статус: Фаза 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 архитектура:
Базовая реализация:
- Типы:
MediaInfo,PhotoInfo,PhotoDownloadState,ImageModalState,ImagesConfig - Зависимости:
ratatui-image 8.1,image 0.25(feature-gated) - Media модуль:
ImageCache(LRU), dualImageRenderer(inline + modal) - UX: Always-show inline preview (фикс. ширина 50 chars) + полноэкранная модалка на
v/м - Метаданные:
extract_media_info()из TDLib MessagePhoto; auto-download visible photos
Оптимизации производительности:
- Dual protocol strategy:
inline_image_renderer: Halfblocks → быстро (Unicode блоки), для навигацииmodal_image_renderer: iTerm2/Sixel → медленно (high quality), для просмотра
- Frame throttling: inline images 15 FPS (66ms), текст 60 FPS
- Lazy loading: загрузка только видимых изображений (не все сразу)
- LRU кэш: max 100 протоколов, eviction старых
- Loading indicator: "⏳ Загрузка..." в модалке при первом открытии
- Navigation hotkeys:
←/→между фото,Esc/qзакрыть модалку
UI рендеринг:
message_bubble.rs: photo status (Downloading/Error/placeholder), inline previewmessages.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:
startingflag + pid ownership guard в потоках AudioPlayer - Seek:
resume_from()перезапускает ffplay с-ssoffset; 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
Ключевые решения
- Неблокирующий receive: TDLib updates через
mpsc::channelв отдельном потоке - Trait-based App: методы разбиты на traits — требуют
useimport на call site - FakeTdClient: mock для тестов без TDLib (реализует TdClientTrait)
- Оптимизация рендеринга:
needs_redrawфлаг, рендеринг только при изменениях - Конфиг: TOML
~/.config/tele-tui/config.toml, credentials с приоритетом (XDG → .env) - Feature-gated images:
imagesfeature flag для ratatui-image + image deps - Dual renderer: inline (Halfblocks, 15 FPS) + modal (iTerm2/Sixel, high quality) для баланса скорости/качества
- Audio via ffplay: subprocess с SIGSTOP/SIGCONT для pause/resume, автостоп при навигации
Зависимости (основные)
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 Подробный план: см. ROADMAP.md