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}
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 form_is_dirty(&self) -> bool {
114 match &self.form_baseline {
115 Some(b) => {
116 self.form.name != b.name
117 || self.form.command != b.command
118 || self.form.description != b.description
119 }
120 None => false,
121 }
122 }
123
124 pub fn set_form_baseline(&mut self, baseline: Option<SnippetFormBaseline>) {
125 self.form_baseline = baseline;
126 }
127
128 pub fn with_store_loaded() -> Self {
130 Self {
131 store: crate::snippet::SnippetStore::load(),
132 ..Self::default()
133 }
134 }
135
136 pub fn request_delete(&mut self, idx: usize) {
139 self.pending_delete = Some(idx);
140 }
141
142 pub fn cancel_delete(&mut self) {
144 self.pending_delete = None;
145 }
146
147 pub fn close_param_form(&mut self) {
152 self.param_form = None;
153 self.pending_terminal = false;
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn default_is_empty() {
163 let s = SnippetState::default();
164 assert!(s.pending.is_none());
165 assert!(s.output.is_none());
166 assert!(s.param_form.is_none());
167 assert!(!s.pending_terminal);
168 assert!(s.form_baseline.is_none());
169 assert!(s.pending_delete.is_none());
170 }
171
172 #[test]
173 fn request_delete_sets_pending_delete_to_some_idx() {
174 let mut s = SnippetState::default();
175 s.request_delete(3);
176 assert_eq!(s.pending_delete, Some(3));
177 }
178
179 #[test]
180 fn cancel_delete_clears_pending_delete() {
181 let mut s = SnippetState {
182 pending_delete: Some(2),
183 ..Default::default()
184 };
185 s.cancel_delete();
186 assert!(s.pending_delete.is_none());
187 }
188
189 #[test]
190 fn request_delete_overwrites_existing_pending() {
191 let mut s = SnippetState {
192 pending_delete: Some(1),
193 ..Default::default()
194 };
195 s.request_delete(7);
196 assert_eq!(s.pending_delete, Some(7));
197 }
198
199 #[test]
200 fn close_param_form_clears_param_form_and_pending_terminal() {
201 let mut s = SnippetState {
202 param_form: Some(SnippetParamFormState::new(&[])),
203 pending_terminal: true,
204 ..Default::default()
205 };
206 s.close_param_form();
207 assert!(s.param_form.is_none());
208 assert!(!s.pending_terminal);
209 }
210
211 #[test]
212 fn close_param_form_preserves_pending_output_and_store() {
213 use crate::snippet::Snippet;
214 let mut s = SnippetState {
215 param_form: Some(SnippetParamFormState::new(&[])),
216 pending_terminal: true,
217 pending: Some((
218 Snippet {
219 name: "ls".into(),
220 command: "ls -la".into(),
221 description: String::new(),
222 },
223 vec!["host-a".into()],
224 )),
225 ..Default::default()
226 };
227
228 s.close_param_form();
229
230 assert!(
231 s.pending.is_some(),
232 "pending stays for the consumer to read"
233 );
234 assert!(s.pending_delete.is_none());
235 }
236
237 #[test]
238 fn close_param_form_is_idempotent_when_already_none() {
239 let mut s = SnippetState::default();
240 s.close_param_form();
241 s.close_param_form();
242 assert!(s.param_form.is_none());
243 assert!(!s.pending_terminal);
244 }
245
246 fn state_matching_baseline() -> SnippetState {
247 let mut s = SnippetState::default();
248 s.form.name = "deploy".into();
249 s.form.command = "make deploy".into();
250 s.form.description = "ship it".into();
251 s.set_form_baseline(Some(SnippetFormBaseline {
252 name: "deploy".into(),
253 command: "make deploy".into(),
254 description: "ship it".into(),
255 }));
256 s
257 }
258
259 #[test]
260 fn form_is_dirty_is_false_without_a_baseline() {
261 let mut s = SnippetState::default();
262 s.form.name = "edited".into();
263 assert!(!s.form_is_dirty());
264 }
265
266 #[test]
267 fn form_is_dirty_is_false_when_form_equals_baseline() {
268 assert!(!state_matching_baseline().form_is_dirty());
269 }
270
271 fn assert_field_change_is_dirty(field: &str, mutate: impl FnOnce(&mut SnippetForm)) {
272 let mut s = state_matching_baseline();
273 mutate(&mut s.form);
274 assert!(s.form_is_dirty(), "a change in {field} must read dirty");
275 }
276
277 #[test]
278 fn form_is_dirty_detects_a_change_in_each_field() {
279 assert_field_change_is_dirty("name", |f| f.name.push('x'));
280 assert_field_change_is_dirty("command", |f| f.command.push('x'));
281 assert_field_change_is_dirty("description", |f| f.description.push('x'));
282 }
283}