Skip to main content

purple_ssh/app/
snippet_state.rs

1use crate::app::SnippetFormBaseline;
2use crate::app::forms::{SnippetForm, SnippetOutputState, SnippetParamFormState};
3use crate::snippet::{Snippet, SnippetStore};
4
5/// Snippet-owned state grouped off the `App` god-struct. Holds the on-disk
6/// snippet store, the edit form, the pending execution payload, the output
7/// screen state, the param form, the terminal-submit flag, the dirty-check
8/// baseline and the pending-delete index. Pure state container.
9pub struct SnippetState {
10    pub store: SnippetStore,
11    pub form: SnippetForm,
12    pub pending: Option<(Snippet, Vec<String>)>,
13    pub output: Option<SnippetOutputState>,
14    pub param_form: Option<SnippetParamFormState>,
15    pub pending_terminal: bool,
16    pub form_baseline: Option<SnippetFormBaseline>,
17    pub pending_delete: Option<usize>,
18}
19
20impl Default for SnippetState {
21    fn default() -> Self {
22        Self {
23            store: SnippetStore::default(),
24            form: SnippetForm::new(),
25            pending: None,
26            output: None,
27            param_form: None,
28            pending_terminal: false,
29            form_baseline: None,
30            pending_delete: None,
31        }
32    }
33}
34
35impl SnippetState {
36    /// Construct with snippet store loaded from disk.
37    pub fn with_store_loaded() -> Self {
38        Self {
39            store: crate::snippet::SnippetStore::load(),
40            ..Self::default()
41        }
42    }
43
44    /// Open a delete confirmation for the snippet at `idx`. The renderer
45    /// reads `pending_delete` to draw the confirm overlay.
46    pub fn request_delete(&mut self, idx: usize) {
47        self.pending_delete = Some(idx);
48    }
49
50    /// Dismiss a pending delete confirmation. Idempotent.
51    pub fn cancel_delete(&mut self) {
52        self.pending_delete = None;
53    }
54
55    /// Close the parameter substitution form. Clears the form state and
56    /// the terminal-submit flag that decide whether the next Enter sends
57    /// the resolved command to the foreground terminal or to background
58    /// output capture. Idempotent.
59    pub fn close_param_form(&mut self) {
60        self.param_form = None;
61        self.pending_terminal = false;
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn default_is_empty() {
71        let s = SnippetState::default();
72        assert!(s.pending.is_none());
73        assert!(s.output.is_none());
74        assert!(s.param_form.is_none());
75        assert!(!s.pending_terminal);
76        assert!(s.form_baseline.is_none());
77        assert!(s.pending_delete.is_none());
78    }
79
80    #[test]
81    fn request_delete_sets_pending_delete_to_some_idx() {
82        let mut s = SnippetState::default();
83        s.request_delete(3);
84        assert_eq!(s.pending_delete, Some(3));
85    }
86
87    #[test]
88    fn cancel_delete_clears_pending_delete() {
89        let mut s = SnippetState {
90            pending_delete: Some(2),
91            ..Default::default()
92        };
93        s.cancel_delete();
94        assert!(s.pending_delete.is_none());
95    }
96
97    #[test]
98    fn request_delete_overwrites_existing_pending() {
99        let mut s = SnippetState {
100            pending_delete: Some(1),
101            ..Default::default()
102        };
103        s.request_delete(7);
104        assert_eq!(s.pending_delete, Some(7));
105    }
106
107    #[test]
108    fn close_param_form_clears_param_form_and_pending_terminal() {
109        let mut s = SnippetState {
110            param_form: Some(SnippetParamFormState::new(&[])),
111            pending_terminal: true,
112            ..Default::default()
113        };
114        s.close_param_form();
115        assert!(s.param_form.is_none());
116        assert!(!s.pending_terminal);
117    }
118
119    #[test]
120    fn close_param_form_preserves_pending_output_and_store() {
121        use crate::snippet::Snippet;
122        let mut s = SnippetState {
123            param_form: Some(SnippetParamFormState::new(&[])),
124            pending_terminal: true,
125            pending: Some((
126                Snippet {
127                    name: "ls".into(),
128                    command: "ls -la".into(),
129                    description: String::new(),
130                },
131                vec!["host-a".into()],
132            )),
133            ..Default::default()
134        };
135
136        s.close_param_form();
137
138        assert!(
139            s.pending.is_some(),
140            "pending stays for the consumer to read"
141        );
142        assert!(s.pending_delete.is_none());
143    }
144
145    #[test]
146    fn close_param_form_is_idempotent_when_already_none() {
147        let mut s = SnippetState::default();
148        s.close_param_form();
149        s.close_param_form();
150        assert!(s.param_form.is_none());
151        assert!(!s.pending_terminal);
152    }
153}