use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use anyhow::Result;
use crate::backend::models::{SafariSession, SessionSummary, SessionTab, SessionWindow};
use crate::backend::storage;
const STATUS_TTL: Duration = Duration::from_secs(4);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CurrentScreen {
Home,
Detail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PendingAction {
DeleteSelected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusKind {
Info,
Success,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetailFocus {
Windows,
Tabs,
}
#[derive(Debug, Clone)]
pub struct StatusMessage {
pub kind: StatusKind,
pub text: String,
created_at: Instant,
}
pub struct App {
pub storage_dir: PathBuf,
pub sessions: Vec<SessionSummary>,
pub selected_session_index: usize,
pub current_screen: CurrentScreen,
pub preview_session: Option<SafariSession>,
pub current_session_data: Option<SafariSession>,
pub detail_focus: DetailFocus,
pub detail_selected_window_index: usize,
pub detail_selected_tab_index: usize,
pub marked_window_indexes: BTreeSet<usize>,
pub pending_action: Option<PendingAction>,
pub status: Option<StatusMessage>,
}
impl App {
pub fn new() -> Result<Self> {
let storage_dir = storage::storage_dir()?;
let mut app = Self {
storage_dir,
sessions: Vec::new(),
selected_session_index: 0,
current_screen: CurrentScreen::Home,
preview_session: None,
current_session_data: None,
detail_focus: DetailFocus::Windows,
detail_selected_window_index: 0,
detail_selected_tab_index: 0,
marked_window_indexes: BTreeSet::new(),
pending_action: None,
status: None,
};
app.refresh_sessions()?;
if app.sessions.is_empty() {
app.set_info("No backups yet. Press n to capture the current Safari session.");
} else {
app.set_info(format!("Loaded {} backup(s).", app.sessions.len()));
}
Ok(app)
}
pub fn refresh_sessions(&mut self) -> Result<()> {
let selected_path = self.selected_summary().map(|summary| summary.path.clone());
self.sessions = storage::list_sessions()?;
self.selected_session_index = selected_path
.and_then(|path| {
self.sessions
.iter()
.position(|summary| summary.path == path)
})
.unwrap_or_else(|| {
self.selected_session_index
.min(self.sessions.len().saturating_sub(1))
});
self.load_preview();
Ok(())
}
pub fn next(&mut self) {
if self.selected_session_index + 1 < self.sessions.len() {
self.selected_session_index += 1;
self.load_preview();
}
}
pub fn previous(&mut self) {
if self.selected_session_index > 0 {
self.selected_session_index -= 1;
self.load_preview();
}
}
pub fn first(&mut self) {
if !self.sessions.is_empty() {
self.selected_session_index = 0;
self.load_preview();
}
}
pub fn last(&mut self) {
if !self.sessions.is_empty() {
self.selected_session_index = self.sessions.len() - 1;
self.load_preview();
}
}
pub fn open_selected_session(&mut self) {
if let Some(session) = self.preview_session.clone() {
self.current_session_data = Some(session);
self.current_screen = CurrentScreen::Detail;
self.reset_detail_state();
}
}
pub fn close_detail(&mut self) {
self.current_screen = CurrentScreen::Home;
self.current_session_data = None;
self.reset_detail_state();
}
pub fn request_delete_selected(&mut self) {
if self.selected_summary().is_some() {
self.pending_action = Some(PendingAction::DeleteSelected);
}
}
pub fn clear_pending_action(&mut self) {
self.pending_action = None;
}
pub fn tick(&mut self) {
if let Some(status) = &self.status {
if status.created_at.elapsed() >= STATUS_TTL {
self.status = None;
}
}
}
pub fn set_info<T>(&mut self, text: T)
where
T: Into<String>,
{
self.status = Some(StatusMessage {
kind: StatusKind::Info,
text: text.into(),
created_at: Instant::now(),
});
}
pub fn set_success<T>(&mut self, text: T)
where
T: Into<String>,
{
self.status = Some(StatusMessage {
kind: StatusKind::Success,
text: text.into(),
created_at: Instant::now(),
});
}
pub fn set_error<T>(&mut self, text: T)
where
T: Into<String>,
{
self.status = Some(StatusMessage {
kind: StatusKind::Error,
text: text.into(),
created_at: Instant::now(),
});
}
pub fn select_path(&mut self, path: &Path) {
if let Some(index) = self
.sessions
.iter()
.position(|summary| summary.path == path)
{
self.selected_session_index = index;
self.load_preview();
}
}
pub fn selected_summary(&self) -> Option<&SessionSummary> {
self.sessions.get(self.selected_session_index)
}
pub fn selected_session(&self) -> Option<&SafariSession> {
self.preview_session.as_ref()
}
pub fn detail_session(&self) -> Option<&SafariSession> {
self.current_session_data.as_ref()
}
pub fn selected_detail_window(&self) -> Option<&SessionWindow> {
self.detail_session()?
.windows
.get(self.detail_selected_window_index)
}
pub fn selected_detail_tab(&self) -> Option<&SessionTab> {
self.selected_detail_window()?
.tabs
.get(self.detail_selected_tab_index)
}
pub fn focus_windows(&mut self) {
self.detail_focus = DetailFocus::Windows;
}
pub fn focus_tabs(&mut self) {
if self.selected_detail_window().is_some() {
self.detail_focus = DetailFocus::Tabs;
self.clamp_detail_selection();
}
}
pub fn toggle_detail_focus(&mut self) {
match self.detail_focus {
DetailFocus::Windows => self.focus_tabs(),
DetailFocus::Tabs => self.focus_windows(),
}
}
pub fn next_detail_window(&mut self) {
let window_count = self
.detail_session()
.map(|session| session.windows.len())
.unwrap_or(0);
if self.detail_selected_window_index + 1 < window_count {
self.detail_selected_window_index += 1;
self.clamp_detail_selection();
}
}
pub fn previous_detail_window(&mut self) {
if self.detail_selected_window_index > 0 {
self.detail_selected_window_index -= 1;
self.clamp_detail_selection();
}
}
pub fn next_detail_tab(&mut self) {
let tab_count = self
.selected_detail_window()
.map(|window| window.tabs.len())
.unwrap_or(0);
if self.detail_selected_tab_index + 1 < tab_count {
self.detail_selected_tab_index += 1;
}
}
pub fn previous_detail_tab(&mut self) {
if self.detail_selected_tab_index > 0 {
self.detail_selected_tab_index -= 1;
}
}
pub fn toggle_current_window_selection(&mut self) {
let index = self.detail_selected_window_index;
if self.marked_window_indexes.contains(&index) {
self.marked_window_indexes.remove(&index);
} else if self.selected_detail_window().is_some() {
self.marked_window_indexes.insert(index);
}
}
pub fn toggle_all_window_selection(&mut self) {
let Some(session) = self.detail_session() else {
return;
};
if self.marked_window_indexes.len() == session.windows.len() {
self.marked_window_indexes.clear();
return;
}
self.marked_window_indexes = (0..session.windows.len()).collect();
}
pub fn clear_window_selection(&mut self) {
self.marked_window_indexes.clear();
}
pub fn marked_window_count(&self) -> usize {
self.marked_window_indexes.len()
}
pub fn current_window_session(&self) -> Option<SafariSession> {
let session = self.detail_session()?;
let window = session
.windows
.get(self.detail_selected_window_index)?
.clone();
Some(SafariSession::new(
session.captured_at.clone(),
vec![window],
))
}
pub fn marked_windows_session(&self) -> Option<SafariSession> {
if self.marked_window_indexes.is_empty() {
return None;
}
let session = self.detail_session()?;
let windows: Vec<SessionWindow> = self
.marked_window_indexes
.iter()
.filter_map(|index| session.windows.get(*index).cloned())
.collect();
if windows.is_empty() {
None
} else {
Some(SafariSession::new(session.captured_at.clone(), windows))
}
}
pub fn selected_tab_url(&self) -> Option<&str> {
self.selected_detail_tab().map(|tab| tab.url.as_str())
}
fn load_preview(&mut self) {
self.preview_session = self
.selected_summary()
.and_then(|summary| storage::load_session(&summary.path).ok());
if self.sessions.is_empty() {
self.selected_session_index = 0;
self.preview_session = None;
self.current_session_data = None;
}
}
fn reset_detail_state(&mut self) {
self.detail_focus = DetailFocus::Windows;
self.detail_selected_window_index = 0;
self.detail_selected_tab_index = 0;
self.marked_window_indexes.clear();
self.clamp_detail_selection();
}
fn clamp_detail_selection(&mut self) {
let Some((window_count, tab_count)) = self.detail_session().map(|session| {
let window_count = session.windows.len();
let safe_window_index = self
.detail_selected_window_index
.min(window_count.saturating_sub(1));
let tab_count = session
.windows
.get(safe_window_index)
.map(|window| window.tabs.len())
.unwrap_or(0);
(window_count, tab_count)
}) else {
self.detail_selected_window_index = 0;
self.detail_selected_tab_index = 0;
return;
};
self.detail_selected_window_index = self
.detail_selected_window_index
.min(window_count.saturating_sub(1));
self.detail_selected_tab_index = self
.detail_selected_tab_index
.min(tab_count.saturating_sub(1));
self.marked_window_indexes
.retain(|index| *index < window_count);
}
}