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(in crate::app) store: SnippetStore,
11    pub(in crate::app) form: SnippetForm,
12    pub(in crate::app) pending: Option<(Snippet, Vec<String>)>,
13    pub(in crate::app) output: Option<SnippetOutputState>,
14    pub(in crate::app) param_form: Option<SnippetParamFormState>,
15    pub(in crate::app) pending_terminal: bool,
16    pub(in crate::app) form_baseline: Option<SnippetFormBaseline>,
17    pub(in crate::app) 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    pub fn store(&self) -> &SnippetStore {
37        &self.store
38    }
39
40    pub fn store_mut(&mut self) -> &mut SnippetStore {
41        &mut self.store
42    }
43
44    pub fn form(&self) -> &SnippetForm {
45        &self.form
46    }
47
48    pub fn form_mut(&mut self) -> &mut SnippetForm {
49        &mut self.form
50    }
51
52    pub fn output(&self) -> Option<&SnippetOutputState> {
53        self.output.as_ref()
54    }
55
56    pub fn output_mut(&mut self) -> Option<&mut SnippetOutputState> {
57        self.output.as_mut()
58    }
59
60    pub fn set_output(&mut self, output: Option<SnippetOutputState>) {
61        self.output = output;
62    }
63
64    pub fn take_output(&mut self) -> Option<SnippetOutputState> {
65        self.output.take()
66    }
67
68    pub fn param_form(&self) -> Option<&SnippetParamFormState> {
69        self.param_form.as_ref()
70    }
71
72    pub fn param_form_mut(&mut self) -> Option<&mut SnippetParamFormState> {
73        self.param_form.as_mut()
74    }
75
76    pub fn set_param_form(&mut self, param_form: Option<SnippetParamFormState>) {
77        self.param_form = param_form;
78    }
79
80    pub fn pending_delete(&self) -> Option<usize> {
81        self.pending_delete
82    }
83
84    pub fn take_pending_delete(&mut self) -> Option<usize> {
85        self.pending_delete.take()
86    }
87
88    pub fn pending(&self) -> Option<&(Snippet, Vec<String>)> {
89        self.pending.as_ref()
90    }
91
92    pub fn take_pending(&mut self) -> Option<(Snippet, Vec<String>)> {
93        self.pending.take()
94    }
95
96    pub fn set_pending(&mut self, value: Option<(Snippet, Vec<String>)>) {
97        self.pending = value;
98    }
99
100    pub fn pending_terminal(&self) -> bool {
101        self.pending_terminal
102    }
103
104    pub fn set_pending_terminal(&mut self, value: bool) {
105        self.pending_terminal = value;
106    }
107
108    pub fn form_baseline(&self) -> Option<&SnippetFormBaseline> {
109        self.form_baseline.as_ref()
110    }
111
112    pub fn set_form_baseline(&mut self, baseline: Option<SnippetFormBaseline>) {
113        self.form_baseline = baseline;
114    }
115
116    /// Construct with snippet store loaded from disk.
117    pub fn with_store_loaded() -> Self {
118        Self {
119            store: crate::snippet::SnippetStore::load(),
120            ..Self::default()
121        }
122    }
123
124    /// Open a delete confirmation for the snippet at `idx`. The renderer
125    /// reads `pending_delete` to draw the confirm overlay.
126    pub fn request_delete(&mut self, idx: usize) {
127        self.pending_delete = Some(idx);
128    }
129
130    /// Dismiss a pending delete confirmation. Idempotent.
131    pub fn cancel_delete(&mut self) {
132        self.pending_delete = None;
133    }
134
135    /// Close the parameter substitution form. Clears the form state and
136    /// the terminal-submit flag that decide whether the next Enter sends
137    /// the resolved command to the foreground terminal or to background
138    /// output capture. Idempotent.
139    pub fn close_param_form(&mut self) {
140        self.param_form = None;
141        self.pending_terminal = false;
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn default_is_empty() {
151        let s = SnippetState::default();
152        assert!(s.pending.is_none());
153        assert!(s.output.is_none());
154        assert!(s.param_form.is_none());
155        assert!(!s.pending_terminal);
156        assert!(s.form_baseline.is_none());
157        assert!(s.pending_delete.is_none());
158    }
159
160    #[test]
161    fn request_delete_sets_pending_delete_to_some_idx() {
162        let mut s = SnippetState::default();
163        s.request_delete(3);
164        assert_eq!(s.pending_delete, Some(3));
165    }
166
167    #[test]
168    fn cancel_delete_clears_pending_delete() {
169        let mut s = SnippetState {
170            pending_delete: Some(2),
171            ..Default::default()
172        };
173        s.cancel_delete();
174        assert!(s.pending_delete.is_none());
175    }
176
177    #[test]
178    fn request_delete_overwrites_existing_pending() {
179        let mut s = SnippetState {
180            pending_delete: Some(1),
181            ..Default::default()
182        };
183        s.request_delete(7);
184        assert_eq!(s.pending_delete, Some(7));
185    }
186
187    #[test]
188    fn close_param_form_clears_param_form_and_pending_terminal() {
189        let mut s = SnippetState {
190            param_form: Some(SnippetParamFormState::new(&[])),
191            pending_terminal: true,
192            ..Default::default()
193        };
194        s.close_param_form();
195        assert!(s.param_form.is_none());
196        assert!(!s.pending_terminal);
197    }
198
199    #[test]
200    fn close_param_form_preserves_pending_output_and_store() {
201        use crate::snippet::Snippet;
202        let mut s = SnippetState {
203            param_form: Some(SnippetParamFormState::new(&[])),
204            pending_terminal: true,
205            pending: Some((
206                Snippet {
207                    name: "ls".into(),
208                    command: "ls -la".into(),
209                    description: String::new(),
210                },
211                vec!["host-a".into()],
212            )),
213            ..Default::default()
214        };
215
216        s.close_param_form();
217
218        assert!(
219            s.pending.is_some(),
220            "pending stays for the consumer to read"
221        );
222        assert!(s.pending_delete.is_none());
223    }
224
225    #[test]
226    fn close_param_form_is_idempotent_when_already_none() {
227        let mut s = SnippetState::default();
228        s.close_param_form();
229        s.close_param_form();
230        assert!(s.param_form.is_none());
231        assert!(!s.pending_terminal);
232    }
233}