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, with_timeout_ignore}; #[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( terminal: &mut Terminal, app: &mut App, ) -> 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::(); // Запускаем 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 задачи (с таймаутом) with_timeout_ignore(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(app: &mut App) -> 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 }