purple_ssh/app/
ui_state.rs1use ratatui::widgets::ListState;
4
5use crate::ui::theme::ThemeDef;
6
7#[derive(Debug, Default)]
9pub struct PickerState {
10 pub open: bool,
11 pub list: ListState,
12}
13
14#[allow(dead_code)]
15impl PickerState {
16 pub fn open_at(&mut self, index: usize) {
18 self.open = true;
19 self.list.select(Some(index));
20 }
21
22 pub fn close(&mut self) {
24 self.open = false;
25 self.list.select(None);
26 }
27}
28
29#[derive(Debug, Default)]
31pub struct ThemePickerState {
32 pub list: ListState,
33 pub builtins: Vec<ThemeDef>,
34 pub custom: Vec<ThemeDef>,
35 pub saved_name: String,
36 pub original: Option<ThemeDef>,
37}
38
39#[derive(Debug, Default)]
43pub struct RegionPickerState {
44 pub open: bool,
45 pub cursor: usize,
46}
47
48#[derive(Debug, Default)]
49pub struct UiSelection {
50 pub(in crate::app) list_state: ListState,
51 pub(in crate::app) key_picker: PickerState,
52 pub(in crate::app) password_picker: PickerState,
53 pub(in crate::app) proxyjump_picker: PickerState,
54 pub(in crate::app) vault_role_picker: PickerState,
55 pub(in crate::app) tag_picker_state: ListState,
56 pub(in crate::app) bulk_tag_editor_state: ListState,
57 pub(in crate::app) theme_picker: ThemePickerState,
58 pub(in crate::app) provider_list_state: ListState,
59 pub(in crate::app) tunnel_list_state: ListState,
60 pub(in crate::app) tunnels_overview_state: ListState,
61 pub(in crate::app) containers_overview_state: ListState,
62 pub(in crate::app) tunnel_host_picker_state: ListState,
66 pub(in crate::app) tunnel_host_picker_query: String,
70 pub(in crate::app) container_host_picker_state: ListState,
74 pub(in crate::app) container_host_picker_query: String,
75 pub(in crate::app) snippet_picker_state: ListState,
76 pub(in crate::app) snippet_search: Option<String>,
77 pub(in crate::app) region_picker: RegionPickerState,
78 pub(in crate::app) help_scroll: u16,
79 pub(in crate::app) detail_scroll: u16,
80 pub(in crate::app) detail_toggle_pending: bool,
82 pub(in crate::app) welcome_opened: Option<std::time::Instant>,
84 pub(in crate::app) esc_quit_hint_shown: bool,
86 pub(in crate::app) known_hosts_count: usize,
88 pub(in crate::app) pending_connect: Option<(String, Option<String>)>,
90}
91
92impl UiSelection {
93 pub fn new_with_initial_selection(initial: Option<usize>) -> Self {
97 let mut s = Self::default();
98 if let Some(pos) = initial {
99 s.list_state.select(Some(pos));
100 }
101 s
102 }
103
104 pub fn queue_connect(&mut self, alias: String, askpass: Option<String>) {
109 self.pending_connect = Some((alias, askpass));
110 }
111
112 pub fn open_snippet_search(&mut self) {
114 self.snippet_search = Some(String::new());
115 }
116
117 pub fn close_snippet_search(&mut self) {
119 self.snippet_search = None;
120 }
121
122 pub fn list_state(&self) -> &ListState {
123 &self.list_state
124 }
125
126 pub fn list_state_mut(&mut self) -> &mut ListState {
127 &mut self.list_state
128 }
129
130 pub fn key_picker(&self) -> &PickerState {
131 &self.key_picker
132 }
133
134 pub fn key_picker_mut(&mut self) -> &mut PickerState {
135 &mut self.key_picker
136 }
137
138 pub fn password_picker(&self) -> &PickerState {
139 &self.password_picker
140 }
141
142 pub fn password_picker_mut(&mut self) -> &mut PickerState {
143 &mut self.password_picker
144 }
145
146 pub fn proxyjump_picker(&self) -> &PickerState {
147 &self.proxyjump_picker
148 }
149
150 pub fn proxyjump_picker_mut(&mut self) -> &mut PickerState {
151 &mut self.proxyjump_picker
152 }
153
154 pub fn vault_role_picker(&self) -> &PickerState {
155 &self.vault_role_picker
156 }
157
158 pub fn vault_role_picker_mut(&mut self) -> &mut PickerState {
159 &mut self.vault_role_picker
160 }
161
162 pub fn tag_picker_state(&self) -> &ListState {
163 &self.tag_picker_state
164 }
165
166 pub fn tag_picker_state_mut(&mut self) -> &mut ListState {
167 &mut self.tag_picker_state
168 }
169
170 pub fn bulk_tag_editor_state(&self) -> &ListState {
171 &self.bulk_tag_editor_state
172 }
173
174 pub fn bulk_tag_editor_state_mut(&mut self) -> &mut ListState {
175 &mut self.bulk_tag_editor_state
176 }
177
178 pub fn theme_picker(&self) -> &ThemePickerState {
179 &self.theme_picker
180 }
181
182 pub fn theme_picker_mut(&mut self) -> &mut ThemePickerState {
183 &mut self.theme_picker
184 }
185
186 pub fn provider_list_state(&self) -> &ListState {
187 &self.provider_list_state
188 }
189
190 pub fn provider_list_state_mut(&mut self) -> &mut ListState {
191 &mut self.provider_list_state
192 }
193
194 pub fn tunnel_list_state(&self) -> &ListState {
195 &self.tunnel_list_state
196 }
197
198 pub fn tunnel_list_state_mut(&mut self) -> &mut ListState {
199 &mut self.tunnel_list_state
200 }
201
202 pub fn tunnels_overview_state(&self) -> &ListState {
203 &self.tunnels_overview_state
204 }
205
206 pub fn tunnels_overview_state_mut(&mut self) -> &mut ListState {
207 &mut self.tunnels_overview_state
208 }
209
210 pub fn containers_overview_state(&self) -> &ListState {
211 &self.containers_overview_state
212 }
213
214 pub fn containers_overview_state_mut(&mut self) -> &mut ListState {
215 &mut self.containers_overview_state
216 }
217
218 pub fn tunnel_host_picker_state(&self) -> &ListState {
219 &self.tunnel_host_picker_state
220 }
221
222 pub fn tunnel_host_picker_state_mut(&mut self) -> &mut ListState {
223 &mut self.tunnel_host_picker_state
224 }
225
226 pub fn tunnel_host_picker_query(&self) -> &String {
227 &self.tunnel_host_picker_query
228 }
229
230 pub fn tunnel_host_picker_query_mut(&mut self) -> &mut String {
231 &mut self.tunnel_host_picker_query
232 }
233
234 pub fn set_tunnel_host_picker_query(&mut self, query: String) {
235 self.tunnel_host_picker_query = query;
236 }
237
238 pub fn container_host_picker_state(&self) -> &ListState {
239 &self.container_host_picker_state
240 }
241
242 pub fn container_host_picker_state_mut(&mut self) -> &mut ListState {
243 &mut self.container_host_picker_state
244 }
245
246 pub fn container_host_picker_query(&self) -> &String {
247 &self.container_host_picker_query
248 }
249
250 pub fn container_host_picker_query_mut(&mut self) -> &mut String {
251 &mut self.container_host_picker_query
252 }
253
254 pub fn set_container_host_picker_query(&mut self, query: String) {
255 self.container_host_picker_query = query;
256 }
257
258 pub fn snippet_picker_state(&self) -> &ListState {
259 &self.snippet_picker_state
260 }
261
262 pub fn snippet_picker_state_mut(&mut self) -> &mut ListState {
263 &mut self.snippet_picker_state
264 }
265
266 pub fn snippet_search(&self) -> Option<&String> {
267 self.snippet_search.as_ref()
268 }
269
270 pub fn snippet_search_mut(&mut self) -> Option<&mut String> {
271 self.snippet_search.as_mut()
272 }
273
274 pub fn region_picker(&self) -> &RegionPickerState {
275 &self.region_picker
276 }
277
278 pub fn region_picker_mut(&mut self) -> &mut RegionPickerState {
279 &mut self.region_picker
280 }
281
282 pub fn help_scroll(&self) -> u16 {
283 self.help_scroll
284 }
285
286 pub fn set_help_scroll(&mut self, scroll: u16) {
287 self.help_scroll = scroll;
288 }
289
290 pub fn detail_scroll(&self) -> u16 {
291 self.detail_scroll
292 }
293
294 pub fn set_detail_scroll(&mut self, scroll: u16) {
295 self.detail_scroll = scroll;
296 }
297
298 pub fn detail_toggle_pending(&self) -> bool {
299 self.detail_toggle_pending
300 }
301
302 pub fn set_detail_toggle_pending(&mut self, pending: bool) {
303 self.detail_toggle_pending = pending;
304 }
305
306 pub fn welcome_opened(&self) -> Option<std::time::Instant> {
307 self.welcome_opened
308 }
309
310 pub fn set_welcome_opened(&mut self, when: Option<std::time::Instant>) {
311 self.welcome_opened = when;
312 }
313
314 pub fn esc_quit_hint_shown(&self) -> bool {
315 self.esc_quit_hint_shown
316 }
317
318 pub fn set_esc_quit_hint_shown(&mut self, shown: bool) {
319 self.esc_quit_hint_shown = shown;
320 }
321
322 pub fn known_hosts_count(&self) -> usize {
323 self.known_hosts_count
324 }
325
326 pub fn set_known_hosts_count(&mut self, count: usize) {
327 self.known_hosts_count = count;
328 }
329
330 pub fn pending_connect(&self) -> Option<&(String, Option<String>)> {
331 self.pending_connect.as_ref()
332 }
333
334 pub fn take_pending_connect(&mut self) -> Option<(String, Option<String>)> {
336 self.pending_connect.take()
337 }
338}
339
340impl ThemePickerState {
341 pub fn reset(&mut self) {
350 self.builtins = Vec::new();
351 self.custom = Vec::new();
352 self.saved_name = String::new();
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn queue_connect_sets_pending_connect_to_some() {
362 let mut s = UiSelection::default();
363 s.queue_connect("web1".into(), Some("vault:foo".into()));
364 assert_eq!(
365 s.pending_connect,
366 Some(("web1".to_string(), Some("vault:foo".to_string())))
367 );
368 }
369
370 #[test]
371 fn queue_connect_with_no_askpass_stores_none() {
372 let mut s = UiSelection::default();
373 s.queue_connect("web1".into(), None);
374 assert_eq!(s.pending_connect, Some(("web1".to_string(), None)));
375 }
376
377 #[test]
378 fn queue_connect_overwrites_existing_pending() {
379 let mut s = UiSelection::default();
380 s.queue_connect("first".into(), None);
381 s.queue_connect("second".into(), Some("p".into()));
382 assert_eq!(
383 s.pending_connect,
384 Some(("second".to_string(), Some("p".to_string())))
385 );
386 }
387
388 #[test]
389 fn open_snippet_search_sets_empty_query() {
390 let mut s = UiSelection::default();
391 s.open_snippet_search();
392 assert_eq!(s.snippet_search.as_deref(), Some(""));
393 }
394
395 #[test]
396 fn open_snippet_search_overwrites_existing_query_with_empty() {
397 let mut s = UiSelection {
402 snippet_search: Some("old".to_string()),
403 ..Default::default()
404 };
405 s.open_snippet_search();
406 assert_eq!(s.snippet_search.as_deref(), Some(""));
407 }
408
409 #[test]
410 fn close_snippet_search_clears_query() {
411 let mut s = UiSelection {
412 snippet_search: Some("query".to_string()),
413 ..Default::default()
414 };
415 s.close_snippet_search();
416 assert!(s.snippet_search.is_none());
417 }
418
419 #[test]
420 fn close_snippet_search_is_idempotent() {
421 let mut s = UiSelection::default();
422 s.close_snippet_search();
423 s.close_snippet_search();
424 assert!(s.snippet_search.is_none());
425 }
426
427 #[test]
428 fn theme_picker_reset_clears_lists_and_saved_name() {
429 let mut t = ThemePickerState {
430 builtins: vec![ThemeDef::purple_purple()],
431 custom: vec![ThemeDef::purple_purple(), ThemeDef::purple_purple()],
432 saved_name: "Solarized".to_string(),
433 ..Default::default()
434 };
435 t.reset();
436 assert!(t.builtins.is_empty());
437 assert!(t.custom.is_empty());
438 assert!(t.saved_name.is_empty());
439 }
440
441 #[test]
442 fn theme_picker_reset_preserves_original_and_list_cursor() {
443 let mut t = ThemePickerState {
444 builtins: vec![ThemeDef::purple_purple()],
445 original: Some(ThemeDef::purple_purple()),
446 ..Default::default()
447 };
448 t.list.select(Some(2));
449 t.reset();
450 assert!(t.original.is_some(), "original must survive reset()");
451 assert_eq!(t.list.selected(), Some(2));
452 }
453}