Files
task_app_rust/src/main.rs
T

229 lines
6.9 KiB
Rust
Raw Normal View History

mod app;
2026-06-20 19:35:19 +01:00
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::<SyncCommand>(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,
};
draw(frame, view);
})?;
if let Event::Key(key) = event::read()? {
app.handle_key(key);
}
}
2026-06-20 19:35:19 +01:00
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
Ok(())
2026-06-20 19:35:19 +01:00
}
async fn run_sync_engine(
db: Arc<Db>,
api: Arc<ApiClient>,
network_status: Arc<Mutex<NetworkStatus>>,
rx: &mut tokio::sync::mpsc::Receiver<SyncCommand>,
) {
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<Db>,
api: &Arc<ApiClient>,
network_status: &Arc<Mutex<NetworkStatus>>,
) {
*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<Db>,
api: &Arc<ApiClient>,
network_status: &Arc<Mutex<NetworkStatus>>,
) {
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::<Task>(&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::<Task>(&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
};
}