purple-ssh 3.15.11

Open-source terminal SSH manager that keeps ~/.ssh/config in sync with your cloud infra. Spin up a VM on AWS, GCP, Azure, Hetzner or 12 other cloud providers and it appears in your host list. Destroy it and the entry dims. Search hundreds of hosts, transfer files, manage Docker and Podman over SSH, sign Vault SSH certs. Rust TUI, MIT licensed.
Documentation
//! Auto-reload mtime tracking and form conflict mtimes.

use std::path::{Path, PathBuf};
use std::time::SystemTime;

use crate::ssh_config::model::SshConfigFile;

/// Auto-reload mtime tracking.
#[derive(Default)]
pub struct ReloadState {
    pub config_path: PathBuf,
    pub last_modified: Option<SystemTime>,
    pub include_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
    pub include_dir_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
    /// mtime of `~/.ssh/` itself. Changes when a key file is created,
    /// renamed or removed; combined with `key_file_mtimes` this gives a
    /// full add/remove/modify signal without needing a real watcher.
    pub keys_dir_mtime: Option<SystemTime>,
    /// mtime per known `*.pub` (or private) key path. Touch-only edits
    /// (re-encrypt, passphrase change) move the file mtime without
    /// touching the parent directory, so we track both.
    pub key_file_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
}

/// Form conflict detection mtimes.
#[derive(Default)]
pub struct ConflictState {
    pub form_mtime: Option<SystemTime>,
    pub form_include_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
    pub form_include_dir_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
    pub provider_form_mtime: Option<SystemTime>,
}

impl ReloadState {
    /// Build from a loaded config: captures initial mtimes for the main file
    /// and every Include'd file and directory.
    pub fn from_config(config: &SshConfigFile) -> Self {
        let config_path = config.path.clone();
        let last_modified = get_mtime(&config_path);
        let include_mtimes = snapshot_include_mtimes(config);
        let include_dir_mtimes = snapshot_include_dir_mtimes(config);
        Self {
            config_path,
            last_modified,
            include_mtimes,
            include_dir_mtimes,
            keys_dir_mtime: None,
            key_file_mtimes: Vec::new(),
        }
    }
}

/// Get the modification time of a file.
pub fn get_mtime(path: &Path) -> Option<SystemTime> {
    std::fs::metadata(path).ok()?.modified().ok()
}

/// Snapshot mtimes of all resolved Include files.
pub fn snapshot_include_mtimes(config: &SshConfigFile) -> Vec<(PathBuf, Option<SystemTime>)> {
    config
        .include_paths()
        .into_iter()
        .map(|p| {
            let mtime = get_mtime(&p);
            (p, mtime)
        })
        .collect()
}

/// Snapshot mtimes of parent directories of Include glob patterns.
pub fn snapshot_include_dir_mtimes(config: &SshConfigFile) -> Vec<(PathBuf, Option<SystemTime>)> {
    config
        .include_glob_dirs()
        .into_iter()
        .map(|p| {
            let mtime = get_mtime(&p);
            (p, mtime)
        })
        .collect()
}

/// Snapshot the mtime of every discovered key's public-key file. The
/// caller passes the live `discover_keys` result; we resolve each
/// `display_path` (with the leading `~` expanded) back to an absolute
/// path under `ssh_dir` so we can stat it cheaply on each tick.
pub fn snapshot_key_mtimes(
    ssh_dir: &Path,
    keys: &[crate::ssh_keys::SshKeyInfo],
) -> Vec<(PathBuf, Option<SystemTime>)> {
    keys.iter()
        .map(|k| {
            let pub_path = ssh_dir.join(format!("{}.pub", k.name));
            let mtime = get_mtime(&pub_path);
            (pub_path, mtime)
        })
        .collect()
}