Skip to main content

purple_ssh/app/
keys_state.rs

1//! Consolidated Keys-tab state. Owns the discovered key list, the
2//! cursor for the master pane, the persistent activity log, and the
3//! in-flight push state.
4//!
5//! Mirrors the `TunnelState` / `ContainersOverviewState` pattern: one
6//! sub-struct per top-level tab so the `App` god-struct stays flat and
7//! every tab has a single field to consult, mutate, or snapshot.
8
9use ratatui::widgets::ListState;
10
11use super::KeyPushState;
12use crate::key_activity::KeyActivityLog;
13use crate::ssh_keys::SshKeyInfo;
14
15#[derive(Default)]
16pub struct KeysState {
17    /// Discovered SSH key files under `~/.ssh/`. Populated by
18    /// `ssh_keys::discover_keys` at startup, after host reloads, and
19    /// after successful pushes. Empty until first discover completes.
20    pub(in crate::app) list: Vec<SshKeyInfo>,
21    /// Cursor in the Keys-tab master pane. `select()` index matches
22    /// either `list` directly or `filtered_key_indices(list, query)`
23    /// when a search query is active (translation happens at use sites).
24    pub(in crate::app) list_state: ListState,
25    /// Persistent per-alias activity log. Loaded once at startup,
26    /// appended on every connect, flushed to `~/.purple/key_activity.json`.
27    /// Drives the activity chart and last-touch hints in the Keys tab.
28    pub(in crate::app) activity: KeyActivityLog,
29    /// Push (ssh-copy-id equivalent) run state.
30    pub(in crate::app) push: KeyPushState,
31}
32
33impl KeysState {
34    pub fn list(&self) -> &Vec<SshKeyInfo> {
35        &self.list
36    }
37
38    pub fn list_mut(&mut self) -> &mut Vec<SshKeyInfo> {
39        &mut self.list
40    }
41
42    pub fn set_list(&mut self, list: Vec<SshKeyInfo>) {
43        self.list = list;
44    }
45
46    pub fn list_state(&self) -> &ListState {
47        &self.list_state
48    }
49
50    pub fn list_state_mut(&mut self) -> &mut ListState {
51        &mut self.list_state
52    }
53
54    pub fn activity(&self) -> &KeyActivityLog {
55        &self.activity
56    }
57
58    pub fn activity_mut(&mut self) -> &mut KeyActivityLog {
59        &mut self.activity
60    }
61
62    pub fn set_activity(&mut self, activity: KeyActivityLog) {
63        self.activity = activity;
64    }
65
66    pub fn push(&self) -> &KeyPushState {
67        &self.push
68    }
69
70    pub fn push_mut(&mut self) -> &mut KeyPushState {
71        &mut self.push
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    fn key(name: &str) -> SshKeyInfo {
80        SshKeyInfo {
81            name: name.to_string(),
82            display_path: format!("~/.ssh/{name}"),
83            key_type: "ED25519".into(),
84            bits: "256".into(),
85            fingerprint: String::new(),
86            comment: String::new(),
87            linked_hosts: vec![],
88            bishop_art: String::new(),
89            strength_score: 90,
90            encrypted: false,
91            agent_loaded: false,
92            is_certificate: false,
93            mtime_ts: None,
94        }
95    }
96
97    #[test]
98    fn default_is_empty() {
99        let s = KeysState::default();
100        assert!(s.list().is_empty());
101        assert!(s.list_state().selected().is_none());
102    }
103
104    #[test]
105    fn set_list_replaces_contents() {
106        let mut s = KeysState::default();
107        s.set_list(vec![key("a")]);
108        assert_eq!(s.list().len(), 1);
109        s.set_list(vec![]);
110        assert!(s.list().is_empty());
111    }
112
113    #[test]
114    fn list_mut_allows_in_place_mutation() {
115        let mut s = KeysState::default();
116        s.list_mut().push(key("a"));
117        assert_eq!(s.list().len(), 1);
118    }
119
120    #[test]
121    fn list_state_mut_tracks_selection() {
122        let mut s = KeysState::default();
123        s.list_state_mut().select(Some(2));
124        assert_eq!(s.list_state().selected(), Some(2));
125    }
126}