purple_ssh/app/
snippet_state.rs1use crate::app::SnippetFormBaseline;
2use crate::app::forms::{SnippetForm, SnippetOutputState, SnippetParamFormState};
3use crate::snippet::{Snippet, SnippetStore};
4
5pub 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 pub(in crate::app) flow_targets: Vec<String>,
23 pub(in crate::app) form_editing: Option<usize>,
27 pub(in crate::app) param_snippet: Option<Snippet>,
29 pub(in crate::app) output_snippet_name: Option<String>,
31}
32
33impl Default for SnippetState {
34 fn default() -> Self {
35 Self {
36 store: SnippetStore::default(),
37 form: SnippetForm::new(),
38 pending: None,
39 output: None,
40 param_form: None,
41 pending_terminal: false,
42 form_baseline: None,
43 pending_delete: None,
44 flow_targets: Vec::new(),
45 form_editing: None,
46 param_snippet: None,
47 output_snippet_name: None,
48 }
49 }
50}
51
52impl SnippetState {
53 pub fn store(&self) -> &SnippetStore {
54 &self.store
55 }
56
57 pub fn store_mut(&mut self) -> &mut SnippetStore {
58 &mut self.store
59 }
60
61 pub fn form(&self) -> &SnippetForm {
62 &self.form
63 }
64
65 pub fn form_mut(&mut self) -> &mut SnippetForm {
66 &mut self.form
67 }
68
69 pub fn output(&self) -> Option<&SnippetOutputState> {
70 self.output.as_ref()
71 }
72
73 pub fn output_mut(&mut self) -> Option<&mut SnippetOutputState> {
74 self.output.as_mut()
75 }
76
77 pub fn set_output(&mut self, output: Option<SnippetOutputState>) {
78 self.output = output;
79 }
80
81 pub fn take_output(&mut self) -> Option<SnippetOutputState> {
82 self.output.take()
83 }
84
85 pub fn param_form(&self) -> Option<&SnippetParamFormState> {
86 self.param_form.as_ref()
87 }
88
89 pub fn param_form_mut(&mut self) -> Option<&mut SnippetParamFormState> {
90 self.param_form.as_mut()
91 }
92
93 pub fn set_param_form(&mut self, param_form: Option<SnippetParamFormState>) {
94 self.param_form = param_form;
95 }
96
97 pub fn pending_delete(&self) -> Option<usize> {
98 self.pending_delete
99 }
100
101 pub fn take_pending_delete(&mut self) -> Option<usize> {
102 self.pending_delete.take()
103 }
104
105 pub fn pending(&self) -> Option<&(Snippet, Vec<String>)> {
106 self.pending.as_ref()
107 }
108
109 pub fn take_pending(&mut self) -> Option<(Snippet, Vec<String>)> {
110 self.pending.take()
111 }
112
113 pub fn set_pending(&mut self, value: Option<(Snippet, Vec<String>)>) {
114 self.pending = value;
115 }
116
117 pub fn flow_targets(&self) -> &[String] {
118 &self.flow_targets
119 }
120
121 pub fn set_flow_targets(&mut self, targets: Vec<String>) {
122 self.flow_targets = targets;
123 }
124
125 pub fn clear_flow_targets(&mut self) {
126 self.flow_targets.clear();
127 }
128
129 pub fn form_editing(&self) -> Option<usize> {
130 self.form_editing
131 }
132
133 pub fn set_form_editing(&mut self, editing: Option<usize>) {
134 self.form_editing = editing;
135 }
136
137 pub fn param_snippet(&self) -> Option<&Snippet> {
138 self.param_snippet.as_ref()
139 }
140
141 pub fn set_param_snippet(&mut self, snippet: Option<Snippet>) {
142 self.param_snippet = snippet;
143 }
144
145 pub fn output_snippet_name(&self) -> Option<&str> {
146 self.output_snippet_name.as_deref()
147 }
148
149 pub fn set_output_snippet_name(&mut self, name: Option<String>) {
150 self.output_snippet_name = name;
151 }
152
153 pub fn pending_terminal(&self) -> bool {
154 self.pending_terminal
155 }
156
157 pub fn set_pending_terminal(&mut self, value: bool) {
158 self.pending_terminal = value;
159 }
160
161 pub fn form_baseline(&self) -> Option<&SnippetFormBaseline> {
162 self.form_baseline.as_ref()
163 }
164
165 pub fn form_is_dirty(&self) -> bool {
167 match &self.form_baseline {
168 Some(b) => {
169 self.form.name != b.name
170 || self.form.command != b.command
171 || self.form.description != b.description
172 }
173 None => false,
174 }
175 }
176
177 pub fn set_form_baseline(&mut self, baseline: Option<SnippetFormBaseline>) {
178 self.form_baseline = baseline;
179 }
180
181 pub fn with_store_loaded(paths: Option<&crate::runtime::env::Paths>) -> Self {
183 Self {
184 store: crate::snippet::SnippetStore::load(paths),
185 ..Self::default()
186 }
187 }
188
189 pub fn request_delete(&mut self, idx: usize) {
192 self.pending_delete = Some(idx);
193 }
194
195 pub fn cancel_delete(&mut self) {
197 self.pending_delete = None;
198 }
199
200 pub fn close_param_form(&mut self) {
205 self.param_form = None;
206 self.pending_terminal = false;
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn default_is_empty() {
216 let s = SnippetState::default();
217 assert!(s.pending.is_none());
218 assert!(s.output.is_none());
219 assert!(s.param_form.is_none());
220 assert!(!s.pending_terminal);
221 assert!(s.form_baseline.is_none());
222 assert!(s.pending_delete.is_none());
223 }
224
225 #[test]
226 fn request_delete_sets_pending_delete_to_some_idx() {
227 let mut s = SnippetState::default();
228 s.request_delete(3);
229 assert_eq!(s.pending_delete, Some(3));
230 }
231
232 #[test]
233 fn cancel_delete_clears_pending_delete() {
234 let mut s = SnippetState {
235 pending_delete: Some(2),
236 ..Default::default()
237 };
238 s.cancel_delete();
239 assert!(s.pending_delete.is_none());
240 }
241
242 #[test]
243 fn request_delete_overwrites_existing_pending() {
244 let mut s = SnippetState {
245 pending_delete: Some(1),
246 ..Default::default()
247 };
248 s.request_delete(7);
249 assert_eq!(s.pending_delete, Some(7));
250 }
251
252 #[test]
253 fn close_param_form_clears_param_form_and_pending_terminal() {
254 let mut s = SnippetState {
255 param_form: Some(SnippetParamFormState::new(&[])),
256 pending_terminal: true,
257 ..Default::default()
258 };
259 s.close_param_form();
260 assert!(s.param_form.is_none());
261 assert!(!s.pending_terminal);
262 }
263
264 #[test]
265 fn close_param_form_preserves_pending_output_and_store() {
266 use crate::snippet::Snippet;
267 let mut s = SnippetState {
268 param_form: Some(SnippetParamFormState::new(&[])),
269 pending_terminal: true,
270 pending: Some((
271 Snippet {
272 name: "ls".into(),
273 command: "ls -la".into(),
274 description: String::new(),
275 },
276 vec!["host-a".into()],
277 )),
278 ..Default::default()
279 };
280
281 s.close_param_form();
282
283 assert!(
284 s.pending.is_some(),
285 "pending stays for the consumer to read"
286 );
287 assert!(s.pending_delete.is_none());
288 }
289
290 #[test]
291 fn close_param_form_is_idempotent_when_already_none() {
292 let mut s = SnippetState::default();
293 s.close_param_form();
294 s.close_param_form();
295 assert!(s.param_form.is_none());
296 assert!(!s.pending_terminal);
297 }
298
299 fn state_matching_baseline() -> SnippetState {
300 let mut s = SnippetState::default();
301 s.form.name = "deploy".into();
302 s.form.command = "make deploy".into();
303 s.form.description = "ship it".into();
304 s.set_form_baseline(Some(SnippetFormBaseline {
305 name: "deploy".into(),
306 command: "make deploy".into(),
307 description: "ship it".into(),
308 }));
309 s
310 }
311
312 #[test]
313 fn form_is_dirty_is_false_without_a_baseline() {
314 let mut s = SnippetState::default();
315 s.form.name = "edited".into();
316 assert!(!s.form_is_dirty());
317 }
318
319 #[test]
320 fn form_is_dirty_is_false_when_form_equals_baseline() {
321 assert!(!state_matching_baseline().form_is_dirty());
322 }
323
324 fn assert_field_change_is_dirty(field: &str, mutate: impl FnOnce(&mut SnippetForm)) {
325 let mut s = state_matching_baseline();
326 mutate(&mut s.form);
327 assert!(s.form_is_dirty(), "a change in {field} must read dirty");
328 }
329
330 #[test]
331 fn form_is_dirty_detects_a_change_in_each_field() {
332 assert_field_change_is_dirty("name", |f| f.name.push('x'));
333 assert_field_change_is_dirty("command", |f| f.command.push('x'));
334 assert_field_change_is_dirty("description", |f| f.description.push('x'));
335 }
336}