jocalsend 1.6.180339887

A TUI for LocalSend
Documentation
use std::{path::Path, str::FromStr, time::Duration};

use clap::Parser;
use jocalsend::{Config, DEFAULT_INTERVAL, JocalService, JocalTask, error::Result};
use log::{error, info};
use ratatui::DefaultTerminal;
use ratatui_explorer::FileExplorer;
use tokio::task::JoinSet;
use tui_logger::{LevelFilter, init_logger};

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();

    init_logger(LevelFilter::Info).map_err(|e| std::io::Error::other(format!("{e}")))?;

    tui_logger::set_env_filter_from_string(
        &std::env::var("RUST_LOG").unwrap_or("jocalsend".to_string()),
    );

    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 = app.service.start().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.read().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 {
                if let Some(req) = app.receive_requests.get(&id) {
                    info!(
                        "Removing stale transfer request from {} for {}",
                        req.alias,
                        req.file_names().join(",")
                    );
                    app.receive_requests.remove(&id);
                }
            }
        }
    }

    Ok(())
}

async fn shutdown(handles: &mut JoinSet<JocalTask>) {
    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);
    }
}