1use 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, Clone, PartialEq, Eq)]
50pub struct AwsProfileNote {
51 pub text: &'static str,
52 pub usable: bool,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct AwsProfileRow {
64 pub name: String,
65 pub note: Option<AwsProfileNote>,
66 pub region: String,
69}
70
71#[derive(Debug, Default)]
72pub struct UiSelection {
73 pub(in crate::app) list_state: ListState,
74 pub(in crate::app) key_picker: PickerState,
75 pub(in crate::app) password_picker: PickerState,
76 pub(in crate::app) proxyjump_picker: PickerState,
77 pub(in crate::app) vault_role_picker: PickerState,
78 pub(in crate::app) profile_picker: PickerState,
79 pub(in crate::app) aws_profile_rows: Vec<AwsProfileRow>,
82 pub(in crate::app) tag_picker_state: ListState,
83 pub(in crate::app) bulk_tag_editor_state: ListState,
84 pub(in crate::app) theme_picker: ThemePickerState,
85 pub(in crate::app) provider_list_state: ListState,
86 pub(in crate::app) tunnel_list_state: ListState,
87 pub(in crate::app) tunnels_overview_state: ListState,
88 pub(in crate::app) containers_overview_state: ListState,
89 pub(in crate::app) tunnel_host_picker_state: ListState,
93 pub(in crate::app) tunnel_host_picker_query: String,
97 pub(in crate::app) container_host_picker_state: ListState,
101 pub(in crate::app) container_host_picker_query: String,
102 pub(in crate::app) snippet_picker_state: ListState,
103 pub(in crate::app) snippet_search: Option<String>,
104 pub(in crate::app) region_picker: RegionPickerState,
105 pub(in crate::app) help_scroll: u16,
106 pub(in crate::app) detail_scroll: u16,
107 pub(in crate::app) detail_toggle_pending: bool,
109 pub(in crate::app) welcome_opened: Option<std::time::Instant>,
111 pub(in crate::app) esc_quit_hint_shown: bool,
113 pub(in crate::app) known_hosts_count: usize,
115 pub(in crate::app) pending_connect: Option<(String, Option<String>)>,
117}
118
119impl UiSelection {
120 pub fn new_with_initial_selection(initial: Option<usize>) -> Self {
124 let mut s = Self::default();
125 if let Some(pos) = initial {
126 s.list_state.select(Some(pos));
127 }
128 s
129 }
130
131 pub fn queue_connect(&mut self, alias: String, askpass: Option<String>) {
136 self.pending_connect = Some((alias, askpass));
137 }
138
139 pub fn open_snippet_search(&mut self) {
141 self.snippet_search = Some(String::new());
142 }
143
144 pub fn close_snippet_search(&mut self) {
146 self.snippet_search = None;
147 }
148
149 pub fn list_state(&self) -> &ListState {
150 &self.list_state
151 }
152
153 pub fn list_state_mut(&mut self) -> &mut ListState {
154 &mut self.list_state
155 }
156
157 pub fn key_picker(&self) -> &PickerState {
158 &self.key_picker
159 }
160
161 pub fn key_picker_mut(&mut self) -> &mut PickerState {
162 &mut self.key_picker
163 }
164
165 pub fn profile_picker(&self) -> &PickerState {
166 &self.profile_picker
167 }
168
169 pub fn profile_picker_mut(&mut self) -> &mut PickerState {
170 &mut self.profile_picker
171 }
172
173 pub fn aws_profile_rows(&self) -> &[AwsProfileRow] {
174 &self.aws_profile_rows
175 }
176
177 pub fn password_picker(&self) -> &PickerState {
178 &self.password_picker
179 }
180
181 pub fn password_picker_mut(&mut self) -> &mut PickerState {
182 &mut self.password_picker
183 }
184
185 pub fn proxyjump_picker(&self) -> &PickerState {
186 &self.proxyjump_picker
187 }
188
189 pub fn proxyjump_picker_mut(&mut self) -> &mut PickerState {
190 &mut self.proxyjump_picker
191 }
192
193 pub fn vault_role_picker(&self) -> &PickerState {
194 &self.vault_role_picker
195 }
196
197 pub fn vault_role_picker_mut(&mut self) -> &mut PickerState {
198 &mut self.vault_role_picker
199 }
200
201 pub fn tag_picker_state(&self) -> &ListState {
202 &self.tag_picker_state
203 }
204
205 pub fn tag_picker_state_mut(&mut self) -> &mut ListState {
206 &mut self.tag_picker_state
207 }
208
209 pub fn bulk_tag_editor_state(&self) -> &ListState {
210 &self.bulk_tag_editor_state
211 }
212
213 pub fn bulk_tag_editor_state_mut(&mut self) -> &mut ListState {
214 &mut self.bulk_tag_editor_state
215 }
216
217 pub fn theme_picker(&self) -> &ThemePickerState {
218 &self.theme_picker
219 }
220
221 pub fn theme_picker_mut(&mut self) -> &mut ThemePickerState {
222 &mut self.theme_picker
223 }
224
225 pub fn provider_list_state(&self) -> &ListState {
226 &self.provider_list_state
227 }
228
229 pub fn provider_list_state_mut(&mut self) -> &mut ListState {
230 &mut self.provider_list_state
231 }
232
233 pub fn tunnel_list_state(&self) -> &ListState {
234 &self.tunnel_list_state
235 }
236
237 pub fn tunnel_list_state_mut(&mut self) -> &mut ListState {
238 &mut self.tunnel_list_state
239 }
240
241 pub fn tunnels_overview_state(&self) -> &ListState {
242 &self.tunnels_overview_state
243 }
244
245 pub fn tunnels_overview_state_mut(&mut self) -> &mut ListState {
246 &mut self.tunnels_overview_state
247 }
248
249 pub fn containers_overview_state(&self) -> &ListState {
250 &self.containers_overview_state
251 }
252
253 pub fn containers_overview_state_mut(&mut self) -> &mut ListState {
254 &mut self.containers_overview_state
255 }
256
257 pub fn tunnel_host_picker_state(&self) -> &ListState {
258 &self.tunnel_host_picker_state
259 }
260
261 pub fn tunnel_host_picker_state_mut(&mut self) -> &mut ListState {
262 &mut self.tunnel_host_picker_state
263 }
264
265 pub fn tunnel_host_picker_query(&self) -> &String {
266 &self.tunnel_host_picker_query
267 }
268
269 pub fn tunnel_host_picker_query_mut(&mut self) -> &mut String {
270 &mut self.tunnel_host_picker_query
271 }
272
273 pub fn set_tunnel_host_picker_query(&mut self, query: String) {
274 self.tunnel_host_picker_query = query;
275 }
276
277 pub fn container_host_picker_state(&self) -> &ListState {
278 &self.container_host_picker_state
279 }
280
281 pub fn container_host_picker_state_mut(&mut self) -> &mut ListState {
282 &mut self.container_host_picker_state
283 }
284
285 pub fn container_host_picker_query(&self) -> &String {
286 &self.container_host_picker_query
287 }
288
289 pub fn container_host_picker_query_mut(&mut self) -> &mut String {
290 &mut self.container_host_picker_query
291 }
292
293 pub fn set_container_host_picker_query(&mut self, query: String) {
294 self.container_host_picker_query = query;
295 }
296
297 pub fn snippet_picker_state(&self) -> &ListState {
298 &self.snippet_picker_state
299 }
300
301 pub fn snippet_picker_state_mut(&mut self) -> &mut ListState {
302 &mut self.snippet_picker_state
303 }
304
305 pub fn snippet_search(&self) -> Option<&String> {
306 self.snippet_search.as_ref()
307 }
308
309 pub fn snippet_search_mut(&mut self) -> Option<&mut String> {
310 self.snippet_search.as_mut()
311 }
312
313 pub fn region_picker(&self) -> &RegionPickerState {
314 &self.region_picker
315 }
316
317 pub fn region_picker_mut(&mut self) -> &mut RegionPickerState {
318 &mut self.region_picker
319 }
320
321 pub fn help_scroll(&self) -> u16 {
322 self.help_scroll
323 }
324
325 pub fn set_help_scroll(&mut self, scroll: u16) {
326 self.help_scroll = scroll;
327 }
328
329 pub fn detail_scroll(&self) -> u16 {
330 self.detail_scroll
331 }
332
333 pub fn set_detail_scroll(&mut self, scroll: u16) {
334 self.detail_scroll = scroll;
335 }
336
337 pub fn detail_toggle_pending(&self) -> bool {
338 self.detail_toggle_pending
339 }
340
341 pub fn set_detail_toggle_pending(&mut self, pending: bool) {
342 self.detail_toggle_pending = pending;
343 }
344
345 pub fn welcome_opened(&self) -> Option<std::time::Instant> {
346 self.welcome_opened
347 }
348
349 pub fn set_welcome_opened(&mut self, when: Option<std::time::Instant>) {
350 self.welcome_opened = when;
351 }
352
353 pub fn esc_quit_hint_shown(&self) -> bool {
354 self.esc_quit_hint_shown
355 }
356
357 pub fn set_esc_quit_hint_shown(&mut self, shown: bool) {
358 self.esc_quit_hint_shown = shown;
359 }
360
361 pub fn known_hosts_count(&self) -> usize {
362 self.known_hosts_count
363 }
364
365 pub fn set_known_hosts_count(&mut self, count: usize) {
366 self.known_hosts_count = count;
367 }
368
369 pub fn pending_connect(&self) -> Option<&(String, Option<String>)> {
370 self.pending_connect.as_ref()
371 }
372
373 pub fn take_pending_connect(&mut self) -> Option<(String, Option<String>)> {
375 self.pending_connect.take()
376 }
377}
378
379impl ThemePickerState {
380 pub fn reset(&mut self) {
389 self.builtins = Vec::new();
390 self.custom = Vec::new();
391 self.saved_name = String::new();
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn queue_connect_sets_pending_connect_to_some() {
401 let mut s = UiSelection::default();
402 s.queue_connect("web1".into(), Some("vault:foo".into()));
403 assert_eq!(
404 s.pending_connect,
405 Some(("web1".to_string(), Some("vault:foo".to_string())))
406 );
407 }
408
409 #[test]
410 fn queue_connect_with_no_askpass_stores_none() {
411 let mut s = UiSelection::default();
412 s.queue_connect("web1".into(), None);
413 assert_eq!(s.pending_connect, Some(("web1".to_string(), None)));
414 }
415
416 #[test]
417 fn queue_connect_overwrites_existing_pending() {
418 let mut s = UiSelection::default();
419 s.queue_connect("first".into(), None);
420 s.queue_connect("second".into(), Some("p".into()));
421 assert_eq!(
422 s.pending_connect,
423 Some(("second".to_string(), Some("p".to_string())))
424 );
425 }
426
427 #[test]
428 fn open_snippet_search_sets_empty_query() {
429 let mut s = UiSelection::default();
430 s.open_snippet_search();
431 assert_eq!(s.snippet_search.as_deref(), Some(""));
432 }
433
434 #[test]
435 fn open_snippet_search_overwrites_existing_query_with_empty() {
436 let mut s = UiSelection {
441 snippet_search: Some("old".to_string()),
442 ..Default::default()
443 };
444 s.open_snippet_search();
445 assert_eq!(s.snippet_search.as_deref(), Some(""));
446 }
447
448 #[test]
449 fn close_snippet_search_clears_query() {
450 let mut s = UiSelection {
451 snippet_search: Some("query".to_string()),
452 ..Default::default()
453 };
454 s.close_snippet_search();
455 assert!(s.snippet_search.is_none());
456 }
457
458 #[test]
459 fn close_snippet_search_is_idempotent() {
460 let mut s = UiSelection::default();
461 s.close_snippet_search();
462 s.close_snippet_search();
463 assert!(s.snippet_search.is_none());
464 }
465
466 #[test]
467 fn theme_picker_reset_clears_lists_and_saved_name() {
468 let mut t = ThemePickerState {
469 builtins: vec![ThemeDef::purple_purple()],
470 custom: vec![ThemeDef::purple_purple(), ThemeDef::purple_purple()],
471 saved_name: "Solarized".to_string(),
472 ..Default::default()
473 };
474 t.reset();
475 assert!(t.builtins.is_empty());
476 assert!(t.custom.is_empty());
477 assert!(t.saved_name.is_empty());
478 }
479
480 #[test]
481 fn theme_picker_reset_preserves_original_and_list_cursor() {
482 let mut t = ThemePickerState {
483 builtins: vec![ThemeDef::purple_purple()],
484 original: Some(ThemeDef::purple_purple()),
485 ..Default::default()
486 };
487 t.list.select(Some(2));
488 t.reset();
489 assert!(t.original.is_some(), "original must survive reset()");
490 assert_eq!(t.list.selected(), Some(2));
491 }
492}