safari 1.0.0

Terminal UI for capturing and restoring Safari sessions on macOS
Documentation
use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};
use safari::{backend, tui};

#[derive(Parser)]
#[command(name = "safari")]
#[command(about = "Capture and restore Safari sessions on macOS", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Capture the current Safari session
    #[command(alias = "dump")]
    Capture,
    /// Restore a saved Safari session
    #[command(alias = "reopen")]
    Restore {
        /// Restore the Nth session shown by `list`
        #[arg(short, long)]
        index: Option<usize>,
    },
    /// List saved Safari sessions
    List,
    /// Launch the terminal UI
    Tui,
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Some(Commands::Capture) => {
            let session = backend::safari::get_current_session()?;
            let path = backend::storage::save_session(&session)?;
            println!(
                "Saved {} window(s) and {} tab(s) to {}",
                session.window_count(),
                session.tab_count(),
                path.display()
            );
        }
        Some(Commands::Restore { index }) => {
            let sessions = backend::storage::list_sessions()?;
            let selected = resolve_session(index, &sessions)?;
            let session = backend::storage::load_session(&selected.path)?;
            backend::safari::restore_session(&session)?;
            println!(
                "Restored {} ({}, {} tabs)",
                backend::utils::format_timestamp(&selected.captured_at),
                selected.window_count,
                selected.tab_count
            );
        }
        Some(Commands::List) => {
            let sessions = backend::storage::list_sessions()?;
            if sessions.is_empty() {
                println!(
                    "No backups found in {}",
                    backend::storage::storage_dir()?.display()
                );
            } else {
                println!(
                    "{:<4} {:<16} {:<7} {:<6} File",
                    "#", "Captured", "Win", "Tabs"
                );
                for (index, session) in sessions.iter().enumerate() {
                    println!(
                        "{:<4} {:<16} {:<7} {:<6} {}",
                        index + 1,
                        backend::utils::format_timestamp(&session.captured_at),
                        session.window_count,
                        session.tab_count,
                        session.file_name
                    );
                }
            }
        }
        Some(Commands::Tui) | None => {
            tui::run()?;
        }
    }

    Ok(())
}

fn resolve_session<'a>(
    index: Option<usize>,
    sessions: &'a [backend::models::SessionSummary],
) -> Result<&'a backend::models::SessionSummary> {
    if sessions.is_empty() {
        return Err(anyhow!("No saved Safari sessions were found"));
    }

    match index {
        Some(0) => Err(anyhow!("Session indexes start at 1")),
        Some(index) => sessions
            .get(index - 1)
            .ok_or_else(|| anyhow!("Session {index} does not exist")),
        None => Ok(&sessions[0]),
    }
}