alf_tui 0.5.0

A smolderingly-nimble Rust TUI to rediscover your custom shell.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
//! Application state management for the TUI.

use super::themes::Theme;
use crate::models::AliasEntry;

/// Action to take when the user selects an entry
#[derive(Debug, Clone, Copy)]
pub enum ExitAction {
   /// Populate shell with entry and execute it immediately
   Execute,
   /// Populate shell with entry but do not execute
   Populate,
}

// Import all state modules
use super::state::{
   EntryData, EntryFilter, FilterState, GroupMode, InputMode, InputState, NavigationState, Panel, ScrollManager,
   SearchState, SortOrder, UiState,
};

/// Main application state
pub struct App {
   /// Entry data storage
   data: EntryData,
   /// Search state
   search: SearchState,
   /// UI state (panels, scroll offsets, help modal)
   ui: UiState,
   /// Input state (mode, pending keys)
   input: InputState,
   /// Navigation state (selection)
   navigation: NavigationState,
   /// Filter/grouping/sorting state
   filter: FilterState,
   /// Color theme
   theme: Theme,
   /// Flag to signal application should quit
   pub should_quit: bool,
   /// Action to take when exiting (if entry was selected)
   pub exit_action: Option<ExitAction>,
}

impl App {
   /// Create a new App instance with the given entries and theme
   pub fn new(
      entries: Vec<AliasEntry>,
      theme: Theme,
   ) -> Self {
      let mut app = Self {
         data: EntryData::new(entries),
         search: SearchState::new(),
         ui: UiState::default(),
         input: InputState::default(),
         navigation: NavigationState::default(),
         filter: FilterState::default(),
         theme,
         should_quit: false,
         exit_action: None,
      };

      // Apply initial filtering, grouping, and sorting to populate visible_indices
      app.update_visible_entries();

      app
   }

   /// Get the currently selected entry, if any
   pub fn selected_entry(&self) -> Option<&AliasEntry> {
      self.data.get_visible_entry(self.navigation.selected_index())
   }

   /// Update visible entries based on current filter and search query
   pub fn update_visible_entries(&mut self) {
      self.filter.update_visible_entries(&mut self.data, self.search.query());
      self.navigation.clamp(self.data.visible_count());
      self.ui.reset_detail_scroll();
   }

   // ===== Accessor methods for state =====

   /// Get reference to all entries
   pub fn entries(&self) -> &[AliasEntry] {
      self.data.entries()
   }

   /// Get reference to visible indices
   pub fn visible_indices(&self) -> &[usize] {
      self.data.visible_indices()
   }

   /// Get the search query
   pub fn search_query(&self) -> &str {
      self.search.query()
   }

   /// Get the cursor position
   pub fn cursor_position(&self) -> usize {
      self.search.cursor_position()
   }

   /// Get the selected index
   pub fn selected_index(&self) -> usize {
      self.navigation.selected_index()
   }

   /// Get the input mode
   pub fn input_mode(&self) -> InputMode {
      self.input.mode()
   }

   /// Get the active panel
   pub fn active_panel(&self) -> Panel {
      self.ui.active_panel()
   }

   /// Get the current filter
   pub fn filter(&self) -> EntryFilter {
      self.filter.filter()
   }

   /// Get list scroll offset
   pub fn list_scroll_offset(&self) -> usize {
      self.ui.list_scroll_offset()
   }

   /// Get description scroll offset
   pub fn description_scroll_offset(&self) -> usize {
      self.ui.description_scroll_offset()
   }

   /// Get description max scroll
   pub fn description_max_scroll(&self) -> usize {
      self.ui.description_max_scroll()
   }

   /// Get script scroll offset
   pub fn script_scroll_offset(&self) -> usize {
      self.ui.script_scroll_offset()
   }

   /// Get script max scroll
   pub fn script_max_scroll(&self) -> usize {
      self.ui.script_max_scroll()
   }

   /// Get pending key
   pub fn pending_key(&self) -> Option<char> {
      self.input.pending_key()
   }

   /// Get pending key time
   pub fn pending_key_time(&self) -> Option<std::time::Instant> {
      self.input.pending_key_time()
   }

   /// Get show help flag
   pub fn show_help(&self) -> bool {
      self.ui.show_help()
   }

   /// Get help scroll offset
   pub fn help_scroll_offset(&self) -> usize {
      self.ui.help_scroll_offset()
   }

   /// Get help max scroll
   pub fn help_max_scroll(&self) -> usize {
      self.ui.help_max_scroll()
   }

   /// Get group mode
   pub fn group_mode(&self) -> GroupMode {
      self.filter.group_mode()
   }

   /// Get sort order
   pub fn sort_order(&self) -> SortOrder {
      self.filter.sort_order()
   }

   // ===== Navigation methods =====

   /// Move selection up by one
   pub fn move_up(&mut self) {
      self.navigation.move_up();
      self.ui.reset_detail_scroll();
   }

   /// Move selection down by one
   pub fn move_down(&mut self) {
      self.navigation.move_down(self.data.visible_count());
      self.ui.reset_detail_scroll();
   }

   /// Jump to the top of the active panel
   pub fn move_top(&mut self) {
      ScrollManager::move_top(&mut self.ui, &mut self.navigation);
   }

   /// Jump to the bottom of the active panel
   pub fn move_bottom(&mut self) {
      ScrollManager::move_bottom(&mut self.ui, &mut self.navigation, self.data.visible_count());
   }

   /// Scroll the active panel up
   pub fn scroll_up(
      &mut self,
      amount: usize,
   ) {
      ScrollManager::scroll_up(&mut self.ui, &mut self.navigation, amount);
   }

   /// Scroll the active panel down
   pub fn scroll_down(
      &mut self,
      amount: usize,
   ) {
      ScrollManager::scroll_down(&mut self.ui, &mut self.navigation, amount, self.data.visible_count());
   }

   // ===== Panel methods =====

   /// Cycle to the next panel (forward)
   pub fn cycle_panel(&mut self) {
      self.ui.cycle_panel();
   }

   /// Cycle to the previous panel (backward)
   pub fn cycle_panel_backward(&mut self) {
      self.ui.cycle_panel_backward();
   }

   // ===== Filter methods =====

   /// Cycle the entry type filter (forward)
   pub fn cycle_filter(&mut self) {
      self.filter.cycle_filter();
      self.update_visible_entries();
   }

   /// Cycle the entry type filter (backward)
   pub fn cycle_filter_backward(&mut self) {
      self.filter.cycle_filter_backward();
      self.update_visible_entries();
   }

   /// Set a specific filter
   pub fn set_filter(
      &mut self,
      filter: EntryFilter,
   ) {
      self.filter.set_filter(filter);
      self.update_visible_entries();
   }

   // ===== Search methods =====

   /// Enter search mode
   pub fn enter_search_mode(&mut self) {
      self.input.enter_search();
   }

   /// Exit search mode, keeping the current query
   pub fn exit_search_keep_query(&mut self) {
      self.input.exit_search();
   }

   /// Exit search mode, clearing the query
   pub fn exit_search_clear_query(&mut self) {
      self.input.exit_search();
      self.search.clear();
      self.update_visible_entries();
   }

   /// Clear the search query without changing mode
   pub fn clear_search(&mut self) {
      self.search.clear();
      self.update_visible_entries();
   }

   /// Set the search query directly and update visible entries
   pub fn set_search_query(
      &mut self,
      query: String,
   ) {
      self.search.set_query(query);
      self.update_visible_entries();
   }

   /// Insert a character at the cursor position in the search query
   pub fn search_insert_char(
      &mut self,
      c: char,
   ) {
      self.search.insert_char(c);
      self.update_visible_entries();
   }

   /// Delete the character before the cursor in the search query
   pub fn search_delete_char(&mut self) {
      self.search.delete_char();
      self.update_visible_entries();
   }

   /// Move search cursor left
   pub fn search_cursor_left(&mut self) {
      self.search.move_cursor_left();
   }

   /// Move search cursor right
   pub fn search_cursor_right(&mut self) {
      self.search.move_cursor_right();
   }

   // ===== Help modal methods =====

   /// Toggle the help modal
   pub fn toggle_help(&mut self) {
      self.ui.toggle_help();
   }

   /// Scroll help modal down by one line
   pub fn help_scroll_down(&mut self) {
      ScrollManager::help_scroll_down(&mut self.ui);
   }

   /// Scroll help modal up by one line
   pub fn help_scroll_up(&mut self) {
      ScrollManager::help_scroll_up(&mut self.ui);
   }

   /// Jump to the top of the help modal
   pub fn help_jump_top(&mut self) {
      ScrollManager::help_jump_top(&mut self.ui);
   }

   /// Jump to the bottom of the help modal
   pub fn help_jump_bottom(&mut self) {
      ScrollManager::help_jump_bottom(&mut self.ui);
   }

   /// Update the maximum scroll offset for help modal based on content and visible area
   pub fn update_help_max_scroll(
      &mut self,
      total_lines: usize,
      visible_lines: usize,
   ) {
      self.ui.update_help_max_scroll(total_lines, visible_lines);
   }

   /// Update the maximum scroll offset for description panel based on content and visible area
   pub fn update_description_max_scroll(
      &mut self,
      total_lines: usize,
      visible_lines: usize,
   ) {
      self.ui.update_description_max_scroll(total_lines, visible_lines);
   }

   /// Update the maximum scroll offset for script panel based on content and visible area
   pub fn update_script_max_scroll(
      &mut self,
      total_lines: usize,
      visible_lines: usize,
   ) {
      self.ui.update_script_max_scroll(total_lines, visible_lines);
   }

   // ===== Grouping and sorting methods =====

   /// Cycle to the next group mode
   pub fn cycle_group_mode(&mut self) {
      self.filter.cycle_group_mode();
      self.update_visible_entries();
   }

   /// Cycle to the previous group mode
   pub fn cycle_group_mode_backward(&mut self) {
      self.filter.cycle_group_mode_backward();
      self.update_visible_entries();
   }

   /// Toggle sort order
   pub fn toggle_sort_order(&mut self) {
      self.filter.toggle_sort_order();
      self.update_visible_entries();
   }

   // ===== Pending key methods =====

   /// Check if pending key has timed out (2 seconds)
   pub fn is_pending_key_expired(&self) -> bool {
      self.input.is_pending_key_expired()
   }

   /// Clear pending key state
   pub fn clear_pending_key(&mut self) {
      self.input.clear_pending_key();
   }

   /// Set pending key with timestamp
   pub fn set_pending_key(
      &mut self,
      key: char,
   ) {
      self.input.set_pending_key(key);
   }

   // ===== Application lifecycle =====

   /// Select the current entry with the given action and exit gracefully
   pub fn select_entry(
      &mut self,
      action: ExitAction,
   ) {
      if self.selected_entry().is_some() {
         self.exit_action = Some(action);
      }
      self.should_quit = true;
   }

   /// Update application state (called each tick)
   pub fn tick(&mut self) {
      // Check and clear expired pending keys
      if self.input.is_pending_key_expired() {
         self.input.clear_pending_key();
      }
   }

   // ===== Theme cycling methods =====

   /// Cycle to the next theme in alphabetical order (wraps around)
   pub fn cycle_theme_next(&mut self) {
      let themes = Theme::available_themes();
      let current = themes.iter().position(|n| n == &self.theme.name).unwrap_or(0);
      let next = (current + 1) % themes.len();
      if let Some(t) = Theme::from_name(&themes[next]) {
         self.theme = t;
      }
   }

   /// Cycle to the previous theme in alphabetical order (wraps around)
   pub fn cycle_theme_prev(&mut self) {
      let themes = Theme::available_themes();
      let current = themes.iter().position(|n| n == &self.theme.name).unwrap_or(0);
      let prev = (current + themes.len() - 1) % themes.len();
      if let Some(t) = Theme::from_name(&themes[prev]) {
         self.theme = t;
      }
   }

   // ===== Theme accessor =====

   /// Get reference to the current theme
   pub fn theme(&self) -> &Theme {
      &self.theme
   }
}

#[cfg(test)]
#[path = "app_tests.rs"]
pub(crate) mod app_tests;