Skip to main content

purple_ssh/app/
file_browser_state.rs

1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3
4/// Persistent per-host file-browser state: last-visited paths per alias.
5#[derive(Debug, Default, Clone)]
6pub struct FileBrowserState {
7    pub(in crate::app) host_paths: HashMap<String, (PathBuf, String)>,
8}
9
10impl FileBrowserState {
11    pub fn host_path(&self, alias: &str) -> Option<&(PathBuf, String)> {
12        self.host_paths.get(alias)
13    }
14
15    pub fn contains_host(&self, alias: &str) -> bool {
16        self.host_paths.contains_key(alias)
17    }
18
19    pub fn set_host_path(&mut self, alias: String, local: PathBuf, remote: String) {
20        self.host_paths.insert(alias, (local, remote));
21    }
22
23    /// Drop `host_paths` entries whose alias is no longer in
24    /// `valid_aliases`. Called from `App::reload_hosts` so a host rename
25    /// or delete cannot leave the old alias behind as a leaked entry.
26    pub fn prune_orphans(&mut self, valid_aliases: &HashSet<&str>) {
27        let pre = self.host_paths.len();
28        self.host_paths
29            .retain(|alias, _| valid_aliases.contains(alias.as_str()));
30        let dropped = pre.saturating_sub(self.host_paths.len());
31        if dropped > 0 {
32            log::debug!(
33                "[purple] reload_hosts: dropped {dropped} orphan file_browser host_paths entrie(s)"
34            );
35        }
36    }
37
38    /// Move the `host_paths` entry from `old` to `new` on host rename.
39    /// Called from `App::migrate_alias_keyed_caches` before
40    /// `reload_hosts`, whose prune step would otherwise drop the
41    /// entry under the old alias. No-op when `old == new`.
42    pub fn migrate_alias(&mut self, old: &str, new: &str) {
43        if old == new {
44            return;
45        }
46        if let Some(v) = self.host_paths.remove(old) {
47            self.host_paths.insert(new.to_string(), v);
48        }
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn prune_orphans_drops_unknown_aliases() {
58        let mut s = FileBrowserState::default();
59        s.set_host_path(
60            "keep".to_string(),
61            PathBuf::from("/a/b"),
62            "remote".to_string(),
63        );
64        s.set_host_path(
65            "drop".to_string(),
66            PathBuf::from("/x/y"),
67            "remote".to_string(),
68        );
69
70        let valid: HashSet<&str> = ["keep"].into_iter().collect();
71        s.prune_orphans(&valid);
72
73        assert!(s.contains_host("keep"));
74        assert!(!s.contains_host("drop"));
75    }
76
77    #[test]
78    fn migrate_alias_moves_host_path() {
79        let mut s = FileBrowserState::default();
80        s.set_host_path(
81            "old".to_string(),
82            PathBuf::from("/local"),
83            "/remote".to_string(),
84        );
85
86        s.migrate_alias("old", "new");
87
88        assert!(!s.contains_host("old"));
89        assert!(s.contains_host("new"));
90        let (local, remote) = s.host_path("new").expect("new alias must hold path");
91        assert_eq!(local, &PathBuf::from("/local"));
92        assert_eq!(remote, "/remote");
93    }
94}