Skip to main content

alf/tui/
app.rs

1//! Application state management for the TUI.
2
3use super::themes::Theme;
4use crate::models::AliasEntry;
5
6/// Action to take when the user selects an entry
7#[derive(Debug, Clone, Copy)]
8pub enum ExitAction {
9   /// Populate shell with entry and execute it immediately
10   Execute,
11   /// Populate shell with entry but do not execute
12   Populate,
13}
14
15// Import all state modules
16use super::state::{
17   EntryData, EntryFilter, FilterState, GroupMode, InputMode, InputState, NavigationState, Panel, ScrollManager,
18   SearchState, SortOrder, UiState,
19};
20
21/// Main application state
22pub struct App {
23   /// Entry data storage
24   data: EntryData,
25   /// Search state
26   search: SearchState,
27   /// UI state (panels, scroll offsets, help modal)
28   ui: UiState,
29   /// Input state (mode, pending keys)
30   input: InputState,
31   /// Navigation state (selection)
32   navigation: NavigationState,
33   /// Filter/grouping/sorting state
34   filter: FilterState,
35   /// Color theme
36   theme: Theme,
37   /// Flag to signal application should quit
38   pub should_quit: bool,
39   /// Action to take when exiting (if entry was selected)
40   pub exit_action: Option<ExitAction>,
41}
42
43impl App {
44   /// Create a new App instance with the given entries and theme
45   pub fn new(
46      entries: Vec<AliasEntry>,
47      theme: Theme,
48   ) -> Self {
49      let mut app = Self {
50         data: EntryData::new(entries),
51         search: SearchState::new(),
52         ui: UiState::default(),
53         input: InputState::default(),
54         navigation: NavigationState::default(),
55         filter: FilterState::default(),
56         theme,
57         should_quit: false,
58         exit_action: None,
59      };
60
61      // Apply initial filtering, grouping, and sorting to populate visible_indices
62      app.update_visible_entries();
63
64      app
65   }
66
67   /// Get the currently selected entry, if any
68   pub fn selected_entry(&self) -> Option<&AliasEntry> {
69      self.data.get_visible_entry(self.navigation.selected_index())
70   }
71
72   /// Update visible entries based on current filter and search query
73   pub fn update_visible_entries(&mut self) {
74      self.filter.update_visible_entries(&mut self.data, self.search.query());
75      self.navigation.clamp(self.data.visible_count());
76      self.ui.reset_detail_scroll();
77   }
78
79   // ===== Accessor methods for state =====
80
81   /// Get reference to all entries
82   pub fn entries(&self) -> &[AliasEntry] {
83      self.data.entries()
84   }
85
86   /// Get reference to visible indices
87   pub fn visible_indices(&self) -> &[usize] {
88      self.data.visible_indices()
89   }
90
91   /// Get the search query
92   pub fn search_query(&self) -> &str {
93      self.search.query()
94   }
95
96   /// Get the cursor position
97   pub fn cursor_position(&self) -> usize {
98      self.search.cursor_position()
99   }
100
101   /// Get the selected index
102   pub fn selected_index(&self) -> usize {
103      self.navigation.selected_index()
104   }
105
106   /// Get the input mode
107   pub fn input_mode(&self) -> InputMode {
108      self.input.mode()
109   }
110
111   /// Get the active panel
112   pub fn active_panel(&self) -> Panel {
113      self.ui.active_panel()
114   }
115
116   /// Get the current filter
117   pub fn filter(&self) -> EntryFilter {
118      self.filter.filter()
119   }
120
121   /// Get list scroll offset
122   pub fn list_scroll_offset(&self) -> usize {
123      self.ui.list_scroll_offset()
124   }
125
126   /// Get description scroll offset
127   pub fn description_scroll_offset(&self) -> usize {
128      self.ui.description_scroll_offset()
129   }
130
131   /// Get description max scroll
132   pub fn description_max_scroll(&self) -> usize {
133      self.ui.description_max_scroll()
134   }
135
136   /// Get script scroll offset
137   pub fn script_scroll_offset(&self) -> usize {
138      self.ui.script_scroll_offset()
139   }
140
141   /// Get script max scroll
142   pub fn script_max_scroll(&self) -> usize {
143      self.ui.script_max_scroll()
144   }
145
146   /// Get pending key
147   pub fn pending_key(&self) -> Option<char> {
148      self.input.pending_key()
149   }
150
151   /// Get pending key time
152   pub fn pending_key_time(&self) -> Option<std::time::Instant> {
153      self.input.pending_key_time()
154   }
155
156   /// Get show help flag
157   pub fn show_help(&self) -> bool {
158      self.ui.show_help()
159   }
160
161   /// Get help scroll offset
162   pub fn help_scroll_offset(&self) -> usize {
163      self.ui.help_scroll_offset()
164   }
165
166   /// Get help max scroll
167   pub fn help_max_scroll(&self) -> usize {
168      self.ui.help_max_scroll()
169   }
170
171   /// Get group mode
172   pub fn group_mode(&self) -> GroupMode {
173      self.filter.group_mode()
174   }
175
176   /// Get sort order
177   pub fn sort_order(&self) -> SortOrder {
178      self.filter.sort_order()
179   }
180
181   // ===== Navigation methods =====
182
183   /// Move selection up by one
184   pub fn move_up(&mut self) {
185      self.navigation.move_up();
186      self.ui.reset_detail_scroll();
187   }
188
189   /// Move selection down by one
190   pub fn move_down(&mut self) {
191      self.navigation.move_down(self.data.visible_count());
192      self.ui.reset_detail_scroll();
193   }
194
195   /// Jump to the top of the active panel
196   pub fn move_top(&mut self) {
197      ScrollManager::move_top(&mut self.ui, &mut self.navigation);
198   }
199
200   /// Jump to the bottom of the active panel
201   pub fn move_bottom(&mut self) {
202      ScrollManager::move_bottom(&mut self.ui, &mut self.navigation, self.data.visible_count());
203   }
204
205   /// Scroll the active panel up
206   pub fn scroll_up(
207      &mut self,
208      amount: usize,
209   ) {
210      ScrollManager::scroll_up(&mut self.ui, &mut self.navigation, amount);
211   }
212
213   /// Scroll the active panel down
214   pub fn scroll_down(
215      &mut self,
216      amount: usize,
217   ) {
218      ScrollManager::scroll_down(&mut self.ui, &mut self.navigation, amount, self.data.visible_count());
219   }
220
221   // ===== Panel methods =====
222
223   /// Cycle to the next panel (forward)
224   pub fn cycle_panel(&mut self) {
225      self.ui.cycle_panel();
226   }
227
228   /// Cycle to the previous panel (backward)
229   pub fn cycle_panel_backward(&mut self) {
230      self.ui.cycle_panel_backward();
231   }
232
233   // ===== Filter methods =====
234
235   /// Cycle the entry type filter (forward)
236   pub fn cycle_filter(&mut self) {
237      self.filter.cycle_filter();
238      self.update_visible_entries();
239   }
240
241   /// Cycle the entry type filter (backward)
242   pub fn cycle_filter_backward(&mut self) {
243      self.filter.cycle_filter_backward();
244      self.update_visible_entries();
245   }
246
247   /// Set a specific filter
248   pub fn set_filter(
249      &mut self,
250      filter: EntryFilter,
251   ) {
252      self.filter.set_filter(filter);
253      self.update_visible_entries();
254   }
255
256   // ===== Search methods =====
257
258   /// Enter search mode
259   pub fn enter_search_mode(&mut self) {
260      self.input.enter_search();
261   }
262
263   /// Exit search mode, keeping the current query
264   pub fn exit_search_keep_query(&mut self) {
265      self.input.exit_search();
266   }
267
268   /// Exit search mode, clearing the query
269   pub fn exit_search_clear_query(&mut self) {
270      self.input.exit_search();
271      self.search.clear();
272      self.update_visible_entries();
273   }
274
275   /// Clear the search query without changing mode
276   pub fn clear_search(&mut self) {
277      self.search.clear();
278      self.update_visible_entries();
279   }
280
281   /// Set the search query directly and update visible entries
282   pub fn set_search_query(
283      &mut self,
284      query: String,
285   ) {
286      self.search.set_query(query);
287      self.update_visible_entries();
288   }
289
290   /// Insert a character at the cursor position in the search query
291   pub fn search_insert_char(
292      &mut self,
293      c: char,
294   ) {
295      self.search.insert_char(c);
296      self.update_visible_entries();
297   }
298
299   /// Delete the character before the cursor in the search query
300   pub fn search_delete_char(&mut self) {
301      self.search.delete_char();
302      self.update_visible_entries();
303   }
304
305   /// Move search cursor left
306   pub fn search_cursor_left(&mut self) {
307      self.search.move_cursor_left();
308   }
309
310   /// Move search cursor right
311   pub fn search_cursor_right(&mut self) {
312      self.search.move_cursor_right();
313   }
314
315   // ===== Help modal methods =====
316
317   /// Toggle the help modal
318   pub fn toggle_help(&mut self) {
319      self.ui.toggle_help();
320   }
321
322   /// Scroll help modal down by one line
323   pub fn help_scroll_down(&mut self) {
324      ScrollManager::help_scroll_down(&mut self.ui);
325   }
326
327   /// Scroll help modal up by one line
328   pub fn help_scroll_up(&mut self) {
329      ScrollManager::help_scroll_up(&mut self.ui);
330   }
331
332   /// Jump to the top of the help modal
333   pub fn help_jump_top(&mut self) {
334      ScrollManager::help_jump_top(&mut self.ui);
335   }
336
337   /// Jump to the bottom of the help modal
338   pub fn help_jump_bottom(&mut self) {
339      ScrollManager::help_jump_bottom(&mut self.ui);
340   }
341
342   /// Update the maximum scroll offset for help modal based on content and visible area
343   pub fn update_help_max_scroll(
344      &mut self,
345      total_lines: usize,
346      visible_lines: usize,
347   ) {
348      self.ui.update_help_max_scroll(total_lines, visible_lines);
349   }
350
351   /// Update the maximum scroll offset for description panel based on content and visible area
352   pub fn update_description_max_scroll(
353      &mut self,
354      total_lines: usize,
355      visible_lines: usize,
356   ) {
357      self.ui.update_description_max_scroll(total_lines, visible_lines);
358   }
359
360   /// Update the maximum scroll offset for script panel based on content and visible area
361   pub fn update_script_max_scroll(
362      &mut self,
363      total_lines: usize,
364      visible_lines: usize,
365   ) {
366      self.ui.update_script_max_scroll(total_lines, visible_lines);
367   }
368
369   // ===== Grouping and sorting methods =====
370
371   /// Cycle to the next group mode
372   pub fn cycle_group_mode(&mut self) {
373      self.filter.cycle_group_mode();
374      self.update_visible_entries();
375   }
376
377   /// Cycle to the previous group mode
378   pub fn cycle_group_mode_backward(&mut self) {
379      self.filter.cycle_group_mode_backward();
380      self.update_visible_entries();
381   }
382
383   /// Toggle sort order
384   pub fn toggle_sort_order(&mut self) {
385      self.filter.toggle_sort_order();
386      self.update_visible_entries();
387   }
388
389   // ===== Pending key methods =====
390
391   /// Check if pending key has timed out (2 seconds)
392   pub fn is_pending_key_expired(&self) -> bool {
393      self.input.is_pending_key_expired()
394   }
395
396   /// Clear pending key state
397   pub fn clear_pending_key(&mut self) {
398      self.input.clear_pending_key();
399   }
400
401   /// Set pending key with timestamp
402   pub fn set_pending_key(
403      &mut self,
404      key: char,
405   ) {
406      self.input.set_pending_key(key);
407   }
408
409   // ===== Application lifecycle =====
410
411   /// Select the current entry with the given action and exit gracefully
412   pub fn select_entry(
413      &mut self,
414      action: ExitAction,
415   ) {
416      if self.selected_entry().is_some() {
417         self.exit_action = Some(action);
418      }
419      self.should_quit = true;
420   }
421
422   /// Update application state (called each tick)
423   pub fn tick(&mut self) {
424      // Check and clear expired pending keys
425      if self.input.is_pending_key_expired() {
426         self.input.clear_pending_key();
427      }
428   }
429
430   // ===== Theme cycling methods =====
431
432   /// Cycle to the next theme in alphabetical order (wraps around)
433   pub fn cycle_theme_next(&mut self) {
434      let themes = Theme::available_themes();
435      let current = themes.iter().position(|n| n == &self.theme.name).unwrap_or(0);
436      let next = (current + 1) % themes.len();
437      if let Some(t) = Theme::from_name(&themes[next]) {
438         self.theme = t;
439      }
440   }
441
442   /// Cycle to the previous theme in alphabetical order (wraps around)
443   pub fn cycle_theme_prev(&mut self) {
444      let themes = Theme::available_themes();
445      let current = themes.iter().position(|n| n == &self.theme.name).unwrap_or(0);
446      let prev = (current + themes.len() - 1) % themes.len();
447      if let Some(t) = Theme::from_name(&themes[prev]) {
448         self.theme = t;
449      }
450   }
451
452   // ===== Theme accessor =====
453
454   /// Get reference to the current theme
455   pub fn theme(&self) -> &Theme {
456      &self.theme
457   }
458}
459
460#[cfg(test)]
461#[path = "app_tests.rs"]
462pub(crate) mod app_tests;