mod app; mod input; mod tdlib; 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::time::Duration; use tdlib_rs::enums::Update; use app::{App, AppScreen}; use input::{handle_auth_input, handle_main_input}; use tdlib::client::AuthState; use utils::disable_tdlib_logs; #[tokio::main] async fn main() -> Result<(), io::Error> { // Загружаем переменные окружения из .env let _ = dotenvy::dotenv(); // Отключаем логи 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(); 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<()> { // Канал для передачи updates из polling задачи в main loop let (update_tx, mut update_rx) = tokio::sync::mpsc::unbounded_channel::(); // Запускаем polling TDLib receive() в отдельной задаче tokio::spawn(async move { loop { // receive() блокирующий, поэтому запускаем в blocking thread let result = tokio::task::spawn_blocking(|| tdlib_rs::receive()).await; if let Ok(Some((update, _client_id))) = result { let _ = update_tx.send(update); } } }); // Запускаем инициализацию 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; }); loop { // Обрабатываем updates от TDLib из канала (неблокирующе) while let Ok(update) = update_rx.try_recv() { app.td_client.handle_update(update); } // Обрабатываем очередь сообщений для отметки как прочитанных 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; } // Синхронизируем сообщения из td_client в app (для новых сообщений в реальном времени) if app.selected_chat_id.is_some() && !app.td_client.current_chat_messages.is_empty() { // Синхронизируем все сообщения (включая обновлённые имена и is_read) for td_msg in &app.td_client.current_chat_messages { if let Some(app_msg) = app.current_messages.iter_mut().find(|m| m.id == td_msg.id) { // Обновляем существующее сообщение app_msg.sender_name = td_msg.sender_name.clone(); app_msg.is_read = td_msg.is_read; } else { // Добавляем новое сообщение app.current_messages.push(td_msg.clone()); } } } // Обновляем состояние экрана на основе auth_state update_screen_state(app).await; terminal.draw(|f| ui::render(f, app))?; // Используем poll для неблокирующего чтения событий if event::poll(Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { // Global quit command if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { return Ok(()); } match app.screen { AppScreen::Loading => { // В состоянии загрузки игнорируем ввод } AppScreen::Auth => handle_auth_input(app, key.code).await, AppScreen::Main => handle_main_input(app, key).await, } } } } } async fn update_screen_state(app: &mut App) { use tokio::time::timeout; let prev_screen = app.screen.clone(); 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()); // Запрашиваем загрузку чатов с таймаутом let _ = timeout(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.clone(); 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()); } } }