mod app; mod domain; mod infrastructure; mod ui; use std::io; use std::sync::Arc; use crossterm::event::{self, Event}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::ExecutableCommand; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use tokio::sync::Mutex; use crate::app::{App, SyncCommand}; use crate::domain::models::*; use crate::infrastructure::api::ApiClient; use crate::infrastructure::db::Db; use crate::ui::{draw, AppView, NetworkStatus}; fn main() -> io::Result<()> { let db_path = dirs::data_dir() .unwrap_or_else(|| std::path::PathBuf::from(".")) .join("task_app") .join("tasks.db"); std::fs::create_dir_all(db_path.parent().unwrap()).ok(); let db = Arc::new(Db::new(db_path.to_str().unwrap()).expect("Failed to open database")); enable_raw_mode()?; let mut stdout = io::stdout(); stdout.execute(EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let api_client = Arc::new(ApiClient::new( std::env::var("GOOGLE_CLIENT_ID").unwrap_or_default(), std::env::var("GOOGLE_CLIENT_SECRET").unwrap_or_default(), )); let network_status = Arc::new(Mutex::new(NetworkStatus::Online)); let (sync_tx, mut sync_rx) = tokio::sync::mpsc::channel::(32); let mut app = App::new(db.clone(), api_client.clone(), sync_tx.clone()); let network_clone = network_status.clone(); let db_clone = db.clone(); let api_clone = api_client.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { run_sync_engine(db_clone, api_clone, network_clone, &mut sync_rx).await; }); }); // Trigger initial sync if already authenticated if !app.needs_auth { let _ = sync_tx.try_send(SyncCommand::InitialSync); } while !app.should_quit { // Poll auth status (non-blocking) app.poll_auth(); // Check if initial sync has loaded data into DB app.check_initial_load(); terminal.draw(|frame| { let status = { let guard = network_status.blocking_lock(); guard.clone() }; app.network_status = status; let view = AppView { lists: &app.lists, tasks: &app.tasks, selected_list: app.selected_list, selected_task: app.selected_task, focus: app.focus.clone(), show_popup: app.show_popup.as_ref(), popup_input: &app.popup_input, popup_cursor: app.popup_cursor, draft_date: app.draft_date, network_status: &app.network_status, task_list_scroll: app.task_list_scroll, detail_scroll: app.detail_scroll, auth_error: app.auth_error.as_deref(), }; draw(frame, view); })?; if let Event::Key(key) = event::read()? { app.handle_key(key); } } disable_raw_mode()?; io::stdout().execute(LeaveAlternateScreen)?; Ok(()) } async fn run_sync_engine( db: Arc, api: Arc, network_status: Arc>, rx: &mut tokio::sync::mpsc::Receiver, ) { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30)); loop { tokio::select! { _ = interval.tick() => { process_sync_queue(&db, &api, &network_status).await; } cmd = rx.recv() => { match cmd { Some(SyncCommand::TriggerSync) => { process_sync_queue(&db, &api, &network_status).await; } Some(SyncCommand::InitialSync) => { run_initial_sync(&db, &api, &network_status).await; } Some(SyncCommand::Shutdown) | None => break, } } } } } async fn run_initial_sync( db: &Arc, api: &Arc, network_status: &Arc>, ) { *network_status.lock().await = NetworkStatus::Syncing; match api.fetch_lists().await { Ok(lists) => { db.replace_all_lists(&lists).ok(); for list in &lists { if let Ok(tasks) = api.fetch_tasks(&list.id).await { db.replace_all_tasks(&list.id, &tasks).ok(); } } *network_status.lock().await = NetworkStatus::Online; } Err(_) => { *network_status.lock().await = NetworkStatus::Offline; } } } async fn process_sync_queue( db: &Arc, api: &Arc, network_status: &Arc>, ) { let token_available = api.load_token().await.is_some(); if !token_available { *network_status.lock().await = NetworkStatus::Offline; return; } let items = db.drain_sync(); if items.is_empty() { *network_status.lock().await = NetworkStatus::Online; return; } *network_status.lock().await = NetworkStatus::Syncing; let mut all_ok = true; for item in &items { let result = match item.action { SyncAction::Create => { let task = serde_json::from_str::(&item.payload).unwrap_or_else(|_| Task { id: item.task_id.clone(), list_id: item.list_id.clone(), title: String::new(), notes: None, status: TaskStatus::NeedsAction, due: None, position: 0, }); api.create_task(&item.list_id, &task).await } SyncAction::Update => { let task = serde_json::from_str::(&item.payload).unwrap_or_else(|_| Task { id: item.task_id.clone(), list_id: item.list_id.clone(), title: String::new(), notes: None, status: TaskStatus::NeedsAction, due: None, position: 0, }); api.update_task(&item.list_id, &task).await } SyncAction::Delete => { api.delete_task(&item.list_id, &item.task_id).await } SyncAction::Reorder => { api.move_task(&item.list_id, &item.task_id, None, None).await } }; if result.is_err() { let _ = db.push_sync( item.action.clone(), &item.task_id, &item.list_id, &item.payload, ); all_ok = false; break; } } *network_status.lock().await = if all_ok { NetworkStatus::Online } else { NetworkStatus::Offline }; }