safari 1.0.0

Terminal UI for capturing and restoring Safari sessions on macOS
Documentation
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

pub const SESSION_FORMAT_VERSION: u8 = 1;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionTab {
    pub title: String,
    pub url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionWindow {
    pub title: String,
    #[serde(default)]
    pub tabs: Vec<SessionTab>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SafariSession {
    #[serde(default = "default_format_version")]
    pub format_version: u8,
    pub captured_at: String,
    #[serde(default)]
    pub windows: Vec<SessionWindow>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionSummary {
    pub path: PathBuf,
    pub file_name: String,
    pub captured_at: String,
    pub window_count: usize,
    pub tab_count: usize,
}

const fn default_format_version() -> u8 {
    SESSION_FORMAT_VERSION
}

impl SafariSession {
    pub fn new(captured_at: String, windows: Vec<SessionWindow>) -> Self {
        Self {
            format_version: SESSION_FORMAT_VERSION,
            captured_at,
            windows,
        }
    }

    pub fn window_count(&self) -> usize {
        self.windows.len()
    }

    pub fn tab_count(&self) -> usize {
        self.windows.iter().map(|window| window.tabs.len()).sum()
    }

    pub fn is_empty(&self) -> bool {
        self.windows.is_empty()
    }
}

impl SessionSummary {
    pub fn from_session(path: PathBuf, session: &SafariSession) -> Self {
        let file_name = path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("unknown.json")
            .to_string();

        Self {
            path,
            file_name,
            captured_at: session.captured_at.clone(),
            window_count: session.window_count(),
            tab_count: session.tab_count(),
        }
    }
}