use anyhow::Result;
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
Terminal,
};
use std::{io, time::Duration};
pub mod app;
pub mod ui;
use crate::backend::{safari, storage};
use app::{App, CurrentScreen, PendingAction};
pub fn run() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let res = (|| -> Result<()> {
let mut app = App::new()?;
run_app(&mut terminal, &mut app)
})();
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
res
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
loop {
terminal.draw(|f| ui::draw(f, app))?;
app.tick();
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
return Ok(());
}
if app.pending_action.is_some() {
handle_confirmation(key.code, app)?;
continue;
}
match app.current_screen {
CurrentScreen::Home => match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down | KeyCode::Char('j') => app.next(),
KeyCode::Up | KeyCode::Char('k') => app.previous(),
KeyCode::Char('g') => app.first(),
KeyCode::Char('G') => app.last(),
KeyCode::Char('n') => {
capture_session(terminal, app)?;
}
KeyCode::Char('d') => {
app.request_delete_selected();
}
KeyCode::Enter => {
app.open_selected_session();
}
KeyCode::Char('r') => {
restore_preview(terminal, app)?;
}
KeyCode::Char('f') => match app.refresh_sessions() {
Ok(()) => app.set_success("Backup list refreshed."),
Err(error) => app.set_error(format!("Refresh failed: {error}")),
},
_ => {}
},
CurrentScreen::Detail => match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
app.close_detail();
}
KeyCode::Tab => app.toggle_detail_focus(),
KeyCode::Left | KeyCode::Char('h') => app.focus_windows(),
KeyCode::Right | KeyCode::Char('l') => app.focus_tabs(),
KeyCode::Down | KeyCode::Char('j') => match app.detail_focus {
app::DetailFocus::Windows => app.next_detail_window(),
app::DetailFocus::Tabs => app.next_detail_tab(),
},
KeyCode::Up | KeyCode::Char('k') => match app.detail_focus {
app::DetailFocus::Windows => app.previous_detail_window(),
app::DetailFocus::Tabs => app.previous_detail_tab(),
},
KeyCode::Enter => app.focus_tabs(),
KeyCode::Char(' ') => app.toggle_current_window_selection(),
KeyCode::Char('a') => app.toggle_all_window_selection(),
KeyCode::Char('u') => app.clear_window_selection(),
KeyCode::Char('r') => {
restore_detail_window(terminal, app)?;
}
KeyCode::Char('s') => {
restore_marked_windows(terminal, app)?;
}
KeyCode::Char('o') => {
open_selected_url(terminal, app)?;
}
_ => {}
},
}
}
}
}
}
fn capture_session<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
app.set_info("Capturing current Safari session...");
terminal.draw(|f| ui::draw(f, app))?;
match safari::get_current_session() {
Ok(session) => match storage::save_session(&session) {
Ok(path) => {
app.refresh_sessions()?;
app.select_path(&path);
app.set_success("Backup captured.");
}
Err(error) => app.set_error(format!("Save failed: {error}")),
},
Err(error) => app.set_error(format!("Capture failed: {error}")),
}
Ok(())
}
fn restore_preview<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
let Some(session) = app.selected_session().cloned() else {
app.set_info("No backup selected.");
return Ok(());
};
app.set_info("Restoring Safari windows...");
terminal.draw(|f| ui::draw(f, app))?;
match safari::restore_session(&session) {
Ok(()) => app.set_success("Safari session restored."),
Err(error) => app.set_error(format!("Restore failed: {error}")),
}
Ok(())
}
fn restore_detail_window<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
let Some(session) = app.current_window_session() else {
app.set_info("No window selected.");
return Ok(());
};
app.set_info("Restoring selected Safari window...");
terminal.draw(|f| ui::draw(f, app))?;
match safari::restore_session(&session) {
Ok(()) => app.set_success("Safari window restored."),
Err(error) => app.set_error(format!("Restore failed: {error}")),
}
Ok(())
}
fn restore_marked_windows<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
let Some(session) = app.marked_windows_session() else {
app.set_info("No marked windows. Press space in the window list first.");
return Ok(());
};
app.set_info(format!(
"Restoring {} marked Safari window(s)...",
session.window_count()
));
terminal.draw(|f| ui::draw(f, app))?;
match safari::restore_session(&session) {
Ok(()) => app.set_success(format!(
"Restored {} marked window(s).",
session.window_count()
)),
Err(error) => app.set_error(format!("Restore failed: {error}")),
}
Ok(())
}
fn open_selected_url<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
let Some(url) = app.selected_tab_url().map(str::to_owned) else {
app.set_info("No tab selected.");
return Ok(());
};
app.set_info("Opening selected URL in Safari...");
terminal.draw(|f| ui::draw(f, app))?;
match safari::open_url(&url) {
Ok(()) => app.set_success("Selected URL opened in Safari."),
Err(error) => app.set_error(format!("Open URL failed: {error}")),
}
Ok(())
}
fn handle_confirmation(code: KeyCode, app: &mut App) -> Result<()> {
match (app.pending_action, code) {
(Some(PendingAction::DeleteSelected), KeyCode::Char('y'))
| (Some(PendingAction::DeleteSelected), KeyCode::Enter) => {
if let Some(summary) = app.selected_summary() {
let path = summary.path.clone();
match storage::delete_session(&path) {
Ok(()) => {
app.clear_pending_action();
app.refresh_sessions()?;
app.set_success("Backup deleted.");
}
Err(error) => {
app.clear_pending_action();
app.set_error(format!("Delete failed: {error}"));
}
}
} else {
app.clear_pending_action();
}
}
(Some(PendingAction::DeleteSelected), KeyCode::Esc)
| (Some(PendingAction::DeleteSelected), KeyCode::Char('n')) => {
app.clear_pending_action();
app.set_info("Delete cancelled.");
}
_ => {}
}
Ok(())
}