usenet_reborn 0.2.2

Terminal-based Usenet NNTP client written in Rust with ratatui/crossterm.
mod bootstrap;
mod compose;
mod config;
mod counter;
mod db;
mod help;
mod links;
mod nntp_client;
mod password;
mod refresh;
mod subscriptions;
mod terminal;
mod threads;
mod ui;
mod vimode;

use bootstrap::locate_paths;
use config::AppConfig;
use db as cache;
use nntp_client::NntpClient;
use password::get_password;
use std::error::Error;
use std::sync::Arc;
use subscriptions::load_subscriptions;
use terminal::{init_terminal, restore_terminal};
use tokio::sync::Mutex;
use ui::{run_ui, AppState};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Step 0: Locate and validate all paths (config, subscriptions, cache DB)
    let paths = locate_paths()?;

    // 1) Load configuration
    let cfg = AppConfig::load(&paths.config)?;
    println!("Configuration loaded from {}: ", paths.config.display());

    // 2) Fetch or prompt for the password
    let password = if cfg.usenet.server.password.trim().is_empty() {
        get_password(&cfg.usenet.server.username, &cfg.usenet.server.server)?
    } else {
        cfg.usenet.server.password.clone()
    };

    println!("About to connect to server... ");
    // 3) Connect to NNTP server
    let client = NntpClient::connect(
        &cfg.usenet.server.server,
        &cfg.usenet.server.username,
        &password,
    )
    .await?;
    let client = Arc::new(Mutex::new(client));

    println!("Connection suscesfull!!!");

    // 4) Initialize our SQLite cache (creates cache.sqlite3 in cwd)
    let conn = cache::init_db(paths.cache_db.to_str().expect("Invalid cache path"))?;
    let db = Arc::new(Mutex::new(conn));

    // 5) Load subscribed newsgroups from ./subscriptions
    let subs = match load_subscriptions(&paths.subscriptions) {
        Ok(s) if !s.is_empty() => s,
        Ok(_) => {
            eprintln!(
                "Found '{}' but it is empty. Please list one newsgroup per line.",
                paths.subscriptions.display()
            );
            std::process::exit(1);
        }
        Err(e) => {
            eprintln!("Failed to open '{}': {}", paths.subscriptions.display(), e);
            eprintln!(
                "Create a '{}' file with one newsgroup per line.",
                paths.subscriptions.display()
            );
            std::process::exit(1);
        }
    };

    // 6) Initialize the terminal UI
    let mut terminal = init_terminal()?;

    // 7) Build initial AppState
    let app_state = AppState::new(subs, cfg.display.threaded_view);

    // 8) Run the UI loop, passing both client and DB
    // TODO: we need to directly send from and email to
    // TODO: compose.rs sending it to ui.rs first is just a
    // TODO: round about for not reason.
    let ui_result = run_ui(
        &mut terminal,
        app_state,
        client,
        db,
        cfg.headers.from,
        cfg.headers.email,
    )
    .await;

    // 9) Tear down terminal
    restore_terminal(terminal)?;

    // 10) Report UI errors
    if let Err(e) = ui_result {
        eprintln!("Error running UI: {}", e);
    }
    Ok(())
}