Skip to main content

purple_ssh/app/
reload_state.rs

1//! Auto-reload mtime tracking and form conflict mtimes.
2
3use std::path::{Path, PathBuf};
4use std::time::SystemTime;
5
6use crate::ssh_config::model::SshConfigFile;
7
8/// Auto-reload mtime tracking.
9#[derive(Default)]
10pub struct ReloadState {
11    pub(in crate::app) config_path: PathBuf,
12    pub(in crate::app) last_modified: Option<SystemTime>,
13    pub(in crate::app) include_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
14    pub(in crate::app) include_dir_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
15    /// mtime of `~/.ssh/` itself. Changes when a key file is created,
16    /// renamed or removed; combined with `key_file_mtimes` this gives a
17    /// full add/remove/modify signal without needing a real watcher.
18    pub(in crate::app) keys_dir_mtime: Option<SystemTime>,
19    /// mtime per known `*.pub` (or private) key path. Touch-only edits
20    /// (re-encrypt, passphrase change) move the file mtime without
21    /// touching the parent directory, so we track both.
22    pub(in crate::app) key_file_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
23}
24
25/// Form conflict detection mtimes.
26#[derive(Default)]
27pub struct ConflictState {
28    pub form_mtime: Option<SystemTime>,
29    pub form_include_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
30    pub form_include_dir_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
31    pub provider_form_mtime: Option<SystemTime>,
32}
33
34impl ConflictState {
35    /// Clear all form mtime state (call on form cancel or successful submit).
36    pub fn clear_form_mtimes(&mut self) {
37        self.form_mtime = None;
38        self.form_include_mtimes.clear();
39        self.form_include_dir_mtimes.clear();
40        self.provider_form_mtime = None;
41    }
42}
43
44/// True if the main config or any tracked Include file/directory changed since
45/// the form's mtimes were captured. Returns false when no form mtime is set.
46pub(crate) fn config_changed(conflict: &ConflictState, config_path: &Path) -> bool {
47    match conflict.form_mtime {
48        Some(open_mtime) => {
49            if get_mtime(config_path) != Some(open_mtime) {
50                return true;
51            }
52            conflict
53                .form_include_mtimes
54                .iter()
55                .any(|(path, old_mtime)| get_mtime(path) != *old_mtime)
56                || conflict
57                    .form_include_dir_mtimes
58                    .iter()
59                    .any(|(path, old_mtime)| get_mtime(path) != *old_mtime)
60        }
61        None => false,
62    }
63}
64
65impl ReloadState {
66    pub fn config_path(&self) -> &Path {
67        &self.config_path
68    }
69
70    /// Build from a loaded config: captures initial mtimes for the main file
71    /// and every Include'd file and directory.
72    pub fn from_config(config: &SshConfigFile) -> Self {
73        let config_path = config.path.clone();
74        let last_modified = get_mtime(&config_path);
75        let include_mtimes = snapshot_include_mtimes(config);
76        let include_dir_mtimes = snapshot_include_dir_mtimes(config);
77        Self {
78            config_path,
79            last_modified,
80            include_mtimes,
81            include_dir_mtimes,
82            keys_dir_mtime: None,
83            key_file_mtimes: Vec::new(),
84        }
85    }
86}
87
88/// Get the modification time of a file.
89pub fn get_mtime(path: &Path) -> Option<SystemTime> {
90    std::fs::metadata(path).ok()?.modified().ok()
91}
92
93/// Snapshot mtimes of all resolved Include files.
94pub fn snapshot_include_mtimes(config: &SshConfigFile) -> Vec<(PathBuf, Option<SystemTime>)> {
95    config
96        .include_paths()
97        .into_iter()
98        .map(|p| {
99            let mtime = get_mtime(&p);
100            (p, mtime)
101        })
102        .collect()
103}
104
105/// Snapshot mtimes of parent directories of Include glob patterns.
106pub fn snapshot_include_dir_mtimes(config: &SshConfigFile) -> Vec<(PathBuf, Option<SystemTime>)> {
107    config
108        .include_glob_dirs()
109        .into_iter()
110        .map(|p| {
111            let mtime = get_mtime(&p);
112            (p, mtime)
113        })
114        .collect()
115}
116
117/// Snapshot the mtime of every discovered key's public-key file. The
118/// caller passes the live `discover_keys` result; we resolve each
119/// `display_path` (with the leading `~` expanded) back to an absolute
120/// path under `ssh_dir` so we can stat it cheaply on each tick.
121pub fn snapshot_key_mtimes(
122    ssh_dir: &Path,
123    keys: &[crate::ssh_keys::SshKeyInfo],
124) -> Vec<(PathBuf, Option<SystemTime>)> {
125    keys.iter()
126        .map(|k| {
127            let pub_path = ssh_dir.join(format!("{}.pub", k.name));
128            let mtime = get_mtime(&pub_path);
129            (pub_path, mtime)
130        })
131        .collect()
132}