Files
telegram-tui/src/main.rs
Mikhail Kilin 0768283e8a refactor: eliminate code duplication - extract helpers and use retry utils
Extracted duplicate code and unified timeout handling across the codebase.

Changes:
- Extracted open_chat_and_load_data() function (eliminates 52 lines of duplication)
- Replaced manual y/н/Enter handling with handle_yes_no() from modal_handler (2 places)
- Replaced 7 direct tokio::time::timeout calls with retry utils (auth, main_input, main)
- Added with_timeout_ignore() for non-critical operations
- Fixed modal_handler.rs bug: corrected Russian 'y' key (д → н)
- Removed unused imports in handlers/mod.rs and utils/mod.rs

Impact:
- main_input.rs: 1164 → 958 lines (-206 lines, -18%)
- Code duplication: 52 lines eliminated
- Direct timeout calls: 7 → 1 (-86%)
- DRY principle applied throughout

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-02 14:20:33 +03:00

261 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
mod app;
mod config;
mod constants;
mod formatting;
mod input;
mod message_grouping;
mod tdlib;
mod types;
mod ui;
mod utils;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tdlib_rs::enums::Update;
use app::{App, AppScreen};
use constants::{POLL_TIMEOUT_MS, SHUTDOWN_TIMEOUT_SECS};
use input::{handle_auth_input, handle_main_input};
use tdlib::AuthState;
use utils::disable_tdlib_logs;
#[tokio::main]
async fn main() -> Result<(), io::Error> {
// Загружаем переменные окружения из .env
let _ = dotenvy::dotenv();
// Инициализируем tracing subscriber для логирования
// Уровень логов можно настроить через переменную окружения RUST_LOG
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"))
)
.init();
// Загружаем конфигурацию (создаёт дефолтный если отсутствует)
let config = config::Config::load();
// Отключаем логи TDLib ДО создания клиента
disable_tdlib_logs();
// Setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// Create app state
let mut app = App::new(config);
// Запускаем инициализацию TDLib в фоне (только для реального клиента)
let client_id = app.td_client.client_id();
let api_id = app.td_client.api_id;
let api_hash = app.td_client.api_hash.clone();
tokio::spawn(async move {
let _ = tdlib_rs::functions::set_tdlib_parameters(
false, // use_test_dc
"tdlib_data".to_string(), // database_directory
"".to_string(), // files_directory
"".to_string(), // database_encryption_key
true, // use_file_database
true, // use_chat_info_database
true, // use_message_database
false, // use_secret_chats
api_id,
api_hash,
"en".to_string(), // system_language_code
"Desktop".to_string(), // device_model
"".to_string(), // system_version
env!("CARGO_PKG_VERSION").to_string(), // application_version
client_id,
)
.await;
});
let res = run_app(&mut terminal, &mut app).await;
// Restore terminal
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("Error: {:?}", err);
}
Ok(())
}
async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
terminal: &mut Terminal<B>,
app: &mut App<T>,
) -> io::Result<()> {
// Флаг для остановки polling задачи
let should_stop = Arc::new(AtomicBool::new(false));
let should_stop_clone = should_stop.clone();
// Канал для передачи updates из polling задачи в main loop
let (update_tx, mut update_rx) = tokio::sync::mpsc::unbounded_channel::<Update>();
// Запускаем polling TDLib receive() в отдельной задаче
let polling_handle = tokio::spawn(async move {
while !should_stop_clone.load(Ordering::Relaxed) {
// receive() с таймаутом 0.1 сек чтобы периодически проверять флаг
let result = tokio::task::spawn_blocking(|| tdlib_rs::receive()).await;
if let Ok(Some((update, _client_id))) = result {
if update_tx.send(update).is_err() {
break; // Канал закрыт, выходим
}
}
}
});
loop {
// Обрабатываем updates от TDLib из канала (неблокирующе)
let mut had_updates = false;
while let Ok(update) = update_rx.try_recv() {
app.td_client.handle_update(update);
had_updates = true;
}
// Помечаем UI как требующий перерисовки если были обновления
if had_updates {
app.needs_redraw = true;
}
// Очищаем устаревший typing status
if app.td_client.clear_stale_typing_status() {
app.needs_redraw = true;
}
// Обрабатываем очередь сообщений для отметки как прочитанных
if !app.td_client.pending_view_messages().is_empty() {
app.td_client.process_pending_view_messages().await;
}
// Обрабатываем очередь user_id для загрузки имён
if !app.td_client.pending_user_ids().is_empty() {
app.td_client.process_pending_user_ids().await;
}
// Обновляем состояние экрана на основе auth_state
let screen_changed = update_screen_state(app).await;
if screen_changed {
app.needs_redraw = true;
}
// Рендерим только если есть изменения
if app.needs_redraw {
terminal.draw(|f| ui::render(f, app))?;
app.needs_redraw = false;
}
// Используем poll с коротким таймаутом для быстрой реакции на ввод
// 16ms ≈ 60 FPS потенциально, но рендерим только при изменениях
if event::poll(Duration::from_millis(POLL_TIMEOUT_MS))? {
match event::read()? {
Event::Key(key) => {
// Global quit command
if key.code == KeyCode::Char('c')
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
// Graceful shutdown
should_stop.store(true, Ordering::Relaxed);
// Закрываем TDLib клиент
let _ = tdlib_rs::functions::close(app.td_client.client_id()).await;
// Ждём завершения polling задачи (с таймаутом)
let _ = tokio::time::timeout(Duration::from_secs(SHUTDOWN_TIMEOUT_SECS), polling_handle).await;
return Ok(());
}
match app.screen {
AppScreen::Loading => {
// В состоянии загрузки игнорируем ввод
}
AppScreen::Auth => handle_auth_input(app, key.code).await,
AppScreen::Main => handle_main_input(app, key).await,
}
// Любой ввод требует перерисовки
app.needs_redraw = true;
}
Event::Resize(_, _) => {
// При изменении размера терминала нужна перерисовка
app.needs_redraw = true;
}
_ => {}
}
}
}
}
/// Возвращает true если состояние изменилось и требуется перерисовка
async fn update_screen_state<T: tdlib::TdClientTrait>(app: &mut App<T>) -> bool {
use utils::with_timeout_ignore;
let prev_screen = app.screen.clone();
let prev_status = app.status_message.clone();
let prev_error = app.error_message.clone();
let prev_chats_len = app.chats.len();
match &app.td_client.auth_state() {
AuthState::WaitTdlibParameters => {
app.screen = AppScreen::Loading;
app.status_message = Some("Инициализация TDLib...".to_string());
}
AuthState::WaitPhoneNumber | AuthState::WaitCode | AuthState::WaitPassword => {
app.screen = AppScreen::Auth;
app.is_loading = false;
}
AuthState::Ready => {
if prev_screen != AppScreen::Main {
app.screen = AppScreen::Main;
app.is_loading = true;
app.status_message = Some("Загрузка чатов...".to_string());
// Запрашиваем загрузку чатов с таймаутом (игнорируем ошибки)
with_timeout_ignore(Duration::from_secs(5), app.td_client.load_chats(50)).await;
}
// Синхронизируем чаты из td_client в app
if !app.td_client.chats().is_empty() {
app.chats = app.td_client.chats().to_vec();
if app.chat_list_state.selected().is_none() && !app.chats.is_empty() {
app.chat_list_state.select(Some(0));
}
// Убираем статус загрузки когда чаты появились
if app.is_loading {
app.is_loading = false;
app.status_message = None;
}
}
}
AuthState::Closed => {
app.status_message = Some("Соединение закрыто".to_string());
}
AuthState::Error(e) => {
app.error_message = Some(e.clone());
}
}
// Проверяем, изменилось ли что-то
app.screen != prev_screen
|| app.status_message != prev_status
|| app.error_message != prev_error
|| app.chats.len() != prev_chats_len
}