use std::{path::Path, str::FromStr, time::Duration}; use clap::Parser; use jocalsend::{Config, DEFAULT_INTERVAL, JocalService, JocalTasks, error::Result}; use log::{error, info}; use ratatui::DefaultTerminal; use ratatui_explorer::FileExplorer; use tokio::task::JoinSet; use tui_logger::{LevelFilter, init_logger, set_env_filter_from_env}; mod app; use app::{App, CurrentScreen, Peer}; mod cli; use cli::Cli; fn main() -> Result<()> { // just in case we need to display the help let _ = Cli::parse(); if std::env::var("RUST_LOG").is_err() { unsafe { std::env::set_var("RUST_LOG", "jocalsend"); } } init_logger(LevelFilter::Info).map_err(|e| std::io::Error::other(format!("{e}")))?; set_env_filter_from_env(None); let config = Config::new()?; let mut terminal = ratatui::init(); let result = start_and_run(&mut terminal, config); ratatui::restore(); result } #[tokio::main(flavor = "multi_thread")] async fn start_and_run(terminal: &mut DefaultTerminal, config: Config) -> Result<()> { let (service, event_listener) = JocalService::new(config.clone()).await?; let mut app = App::new(service, event_listener); let cli = Cli::parse(); if let Some(text) = cli.text { let i = app.input(); *i = i.clone().with_value(text.clone()); app.text().replace(text); } if let Some(file) = cli.file { let path = std::path::PathBuf::from_str(&file.to_string_lossy()) .unwrap_or_else(|_| panic!("Could not create path from {file:?}")); set_file_selection(&path, app.files()); } let mut handles = JoinSet::new(); app.service.start(&mut handles).await; let shutdown = shutdown(&mut handles); let mut shutdown = std::pin::pin!(shutdown); loop { terminal.draw(|frame| app.draw(frame))?; if app.screen() == CurrentScreen::Stopping { tokio::select! { _ = shutdown.as_mut() => { break; } _ = tokio::time::sleep(DEFAULT_INTERVAL) => {} } } else { app.handle_events().await?; let peers = app.service.peers.lock().await; app.peers.clear(); peers.iter().for_each(|(fingerprint, (addr, device))| { let alias = device.alias.clone(); let peer = Peer { alias, fingerprint: fingerprint.to_owned(), addr: addr.to_owned(), }; app.peers.push(peer); }); let mut stale_rx_requests = Vec::with_capacity(app.receive_requests.len()); let now = chrono::Utc::now().timestamp_millis() as u64; for (id, request) in app.receive_requests.iter() { if request.tx.is_closed() || (now - id.timestamp()) > 60_000 { stale_rx_requests.push(*id); } } for id in stale_rx_requests { app.receive_requests.remove(&id); } } } Ok(()) } async fn shutdown(handles: &mut JoinSet) { let mut timeout = tokio::time::interval(Duration::from_secs(5)); timeout.tick().await; loop { tokio::select! { join_result = handles.join_next() => { match join_result { Some(handle) => match handle { Ok(h) => info!("Stopped {h:?}"), Err(e) => error!("Got error {e:?}"), } None => break, } } _ = timeout.tick() => { info!("Exit timeout reached, aborting all unjoined tasks"); handles.abort_all(); break; }, } } } fn set_file_selection(path: &Path, explorer: &mut FileExplorer) { let parent = path.parent().map(|f| f.to_path_buf()).unwrap_or("/".into()); let _ = explorer.set_cwd(parent); let files = explorer.files(); let mut idx = None; for (i, f) in files.iter().enumerate() { if f.name() == path.file_name().unwrap().to_string_lossy() { idx = Some(i); break; } } if let Some(idx) = idx { explorer.set_selected_idx(idx); } }