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    // Held at `pub(crate)` until the dedicated forms seal lands; the
12    // SnippetForm has too many per-field mutations to wrap behind
13    // methods in this commit.
14    pub(crate) form: SnippetForm,
15    pub(in crate::app) pending: Option<(Snippet, Vec<String>)>,
16    // Held at `pub(crate)` because the output state is mutated through
17    // multi-line patterns the forthcoming forms seal will route through
18    // dedicated methods.
19    pub(crate) output: Option<SnippetOutputState>,
20    pub(crate) param_form: Option<SnippetParamFormState>,
21    pub(in crate::app) pending_terminal: bool,
22    pub(in crate::app) form_baseline: Option<SnippetFormBaseline>,
23    // Held at `pub(crate)` so `if let Some(idx) = ...pending_delete`
24    // multi-line patterns continue to compile.
25    pub(crate) pending_delete: Option<usize>,
26}
27
28impl Default for SnippetState {
29    fn default() -> Self {
30        Self {
31            store: SnippetStore::default(),
32            form: SnippetForm::new(),
33            pending: None,
34            output: None,
35            param_form: None,
36            pending_terminal: false,
37            form_baseline: None,
38            pending_delete: None,
39        }
40    }
41}
42
43impl SnippetState {
44    pub fn store(&self) -> &SnippetStore {
45        &self.store
46    }
47
48    pub fn store_mut(&mut self) -> &mut SnippetStore {
49        &mut self.store
50    }
51
52    pub fn pending(&self) -> Option<&(Snippet, Vec<String>)> {
53        self.pending.as_ref()
54    }
55
56    pub fn take_pending(&mut self) -> Option<(Snippet, Vec<String>)> {
57        self.pending.take()
58    }
59
60    pub fn set_pending(&mut self, value: Option<(Snippet, Vec<String>)>) {
61        self.pending = value;
62    }
63
64    pub fn pending_terminal(&self) -> bool {
65        self.pending_terminal
66    }
67
68    pub fn set_pending_terminal(&mut self, value: bool) {
69        self.pending_terminal = value;
70    }
71
72    pub fn form_baseline(&self) -> Option<&SnippetFormBaseline> {
73        self.form_baseline.as_ref()
74    }
75
76    pub fn set_form_baseline(&mut self, baseline: Option<SnippetFormBaseline>) {
77        self.form_baseline = baseline;
78    }
79
80    /// Construct with snippet store loaded from disk.
81    pub fn with_store_loaded() -> Self {
82        Self {
83            store: crate::snippet::SnippetStore::load(),
84            ..Self::default()
85        }
86    }
87
88    /// Open a delete confirmation for the snippet at `idx`. The renderer
89    /// reads `pending_delete` to draw the confirm overlay.
90    pub fn request_delete(&mut self, idx: usize) {
91        self.pending_delete = Some(idx);
92    }
93
94    /// Dismiss a pending delete confirmation. Idempotent.
95    pub fn cancel_delete(&mut self) {
96        self.pending_delete = None;
97    }
98
99    /// Close the parameter substitution form. Clears the form state and
100    /// the terminal-submit flag that decide whether the next Enter sends
101    /// the resolved command to the foreground terminal or to background
102    /// output capture. Idempotent.
103    pub fn close_param_form(&mut self) {
104        self.param_form = None;
105        self.pending_terminal = false;
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn default_is_empty() {
115        let s = SnippetState::default();
116        assert!(s.pending.is_none());
117        assert!(s.output.is_none());
118        assert!(s.param_form.is_none());
119        assert!(!s.pending_terminal);
120        assert!(s.form_baseline.is_none());
121        assert!(s.pending_delete.is_none());
122    }
123
124    #[test]
125    fn request_delete_sets_pending_delete_to_some_idx() {
126        let mut s = SnippetState::default();
127        s.request_delete(3);
128        assert_eq!(s.pending_delete, Some(3));
129    }
130
131    #[test]
132    fn cancel_delete_clears_pending_delete() {
133        let mut s = SnippetState {
134            pending_delete: Some(2),
135            ..Default::default()
136        };
137        s.cancel_delete();
138        assert!(s.pending_delete.is_none());
139    }
140
141    #[test]
142    fn request_delete_overwrites_existing_pending() {
143        let mut s = SnippetState {
144            pending_delete: Some(1),
145            ..Default::default()
146        };
147        s.request_delete(7);
148        assert_eq!(s.pending_delete, Some(7));
149    }
150
151    #[test]
152    fn close_param_form_clears_param_form_and_pending_terminal() {
153        let mut s = SnippetState {
154            param_form: Some(SnippetParamFormState::new(&[])),
155            pending_terminal: true,
156            ..Default::default()
157        };
158        s.close_param_form();
159        assert!(s.param_form.is_none());
160        assert!(!s.pending_terminal);
161    }
162
163    #[test]
164    fn close_param_form_preserves_pending_output_and_store() {
165        use crate::snippet::Snippet;
166        let mut s = SnippetState {
167            param_form: Some(SnippetParamFormState::new(&[])),
168            pending_terminal: true,
169            pending: Some((
170                Snippet {
171                    name: "ls".into(),
172                    command: "ls -la".into(),
173                    description: String::new(),
174                },
175                vec!["host-a".into()],
176            )),
177            ..Default::default()
178        };
179
180        s.close_param_form();
181
182        assert!(
183            s.pending.is_some(),
184            "pending stays for the consumer to read"
185        );
186        assert!(s.pending_delete.is_none());
187    }
188
189    #[test]
190    fn close_param_form_is_idempotent_when_already_none() {
191        let mut s = SnippetState::default();
192        s.close_param_form();
193        s.close_param_form();
194        assert!(s.param_form.is_none());
195        assert!(!s.pending_terminal);
196    }
197}