vivo 0.11.2

restic backup orchestrator with multi-remote sync and SOPS-encrypted secrets
Documentation
use std::time::{Duration, Instant};

use crate::backup_config::BackupConfig;

#[derive(Clone, Copy, PartialEq)]
pub enum Pane {
    Tasks,
    Fields,
    Remotes,
    Calls,
}

pub const FIELD_NAMES: &[&str] =
    &["Name", "Description", "Repo", "Directory", "Exclude file", "Files from"];

pub struct TaskEntry {
    pub name: String,
    pub description: Option<String>,
    pub repo: Option<String>,
    pub directory: Option<String>,
    pub exclude_file: Option<String>,
    pub files_from: Option<String>,
    pub remotes: Vec<RemoteEntry>,
    pub calls: Vec<String>,
}

pub struct RemoteEntry {
    pub url: String,
    pub credentials: String,
    pub mc_max_workers: Option<u32>,
    pub mc_limit_upload: Option<String>,
}

pub struct App {
    pub tasks: Vec<TaskEntry>,
    pub selected_task: usize,
    pub selected_remote: usize,
    pub selected_field: usize,
    pub selected_call: usize,
    pub focused_pane: Pane,
    pub config_path: String,
    pub default_task: String,
    pub should_quit: bool,
    pub status_message: Option<String>,
    pub status_expires_at: Option<Instant>,
    pub needs_clear: bool,
}

impl App {
    pub fn new(config: &BackupConfig, config_path: String) -> Self {
        let tasks = config
            .tasks
            .iter()
            .map(|t| TaskEntry {
                name: t.name.clone(),
                description: t.description().map(str::to_owned),
                repo: t.backup_repo().map(str::to_owned),
                directory: t.backup_directory().map(str::to_owned),
                exclude_file: t.backup_exclude_file().map(str::to_owned),
                files_from: t.backup_files_from().map(str::to_owned),
                remotes: t
                    .backup_remote_entries()
                    .into_iter()
                    .map(|r| RemoteEntry {
                        url: r.url.clone(),
                        credentials: r.credentials.clone(),
                        mc_max_workers: r.mc_max_workers,
                        mc_limit_upload: r.mc_limit_upload.clone(),
                    })
                    .collect(),
                calls: t.call_names().into_iter().map(str::to_owned).collect(),
            })
            .collect();

        App {
            tasks,
            selected_task: 0,
            selected_remote: 0,
            selected_field: 0,
            selected_call: 0,
            focused_pane: Pane::Tasks,
            config_path,
            default_task: config.default_task.clone(),
            should_quit: false,
            status_message: None,
            status_expires_at: None,
            needs_clear: false,
        }
    }

    pub fn set_status(&mut self, msg: String) {
        self.status_expires_at = Some(Instant::now() + Duration::from_secs(2));
        self.status_message = Some(msg);
    }

    pub fn tick_status(&mut self) {
        if let Some(exp) = self.status_expires_at {
            if Instant::now() >= exp {
                self.status_message = None;
                self.status_expires_at = None;
            }
        }
    }

    pub fn current_remotes(&self) -> &[RemoteEntry] {
        self.tasks
            .get(self.selected_task)
            .map(|t| t.remotes.as_slice())
            .unwrap_or(&[])
    }

    pub fn current_calls(&self) -> &[String] {
        self.tasks
            .get(self.selected_task)
            .map(|t| t.calls.as_slice())
            .unwrap_or(&[])
    }

    pub fn reload(&mut self) {
        if let Ok(content) = std::fs::read_to_string(&self.config_path) {
            if let Ok(config) =
                knuffel::parse::<BackupConfig>(&self.config_path, &content)
            {
                let prev_task = self.selected_task;
                let prev_remote = self.selected_remote;
                let prev_field = self.selected_field;
                let prev_call = self.selected_call;
                let prev_pane = self.focused_pane;
                let prev_msg = self.status_message.take();
                let prev_exp = self.status_expires_at;
                let new = App::new(&config, self.config_path.clone());
                *self = new;
                self.selected_task =
                    prev_task.min(self.tasks.len().saturating_sub(1));
                self.selected_remote =
                    prev_remote.min(self.current_remotes().len().saturating_sub(1));
                self.selected_field = prev_field.min(FIELD_NAMES.len() - 1);
                self.selected_call =
                    prev_call.min(self.current_calls().len().saturating_sub(1));
                self.focused_pane = prev_pane;
                self.status_message = prev_msg;
                self.status_expires_at = prev_exp;
            }
        }
    }
}