Skip to main content

carch_core/ui/
state.rs

1use std::collections::{HashMap, VecDeque};
2use std::path::PathBuf;
3
4use fuzzy_matcher::skim::SkimMatcherV2;
5use ratatui::layout::Rect;
6use ratatui::text::Text;
7
8use super::popups::run_script::RunScriptPopup;
9use super::theme::Theme;
10
11#[derive(PartialEq, Eq, Debug, Clone, Copy)]
12pub enum AppMode {
13    Normal,
14    Search,
15    Confirm,
16    Help,
17    Preview,
18    RunScript,
19    Description,
20    ThemeSelector,
21    RootWarning,
22    TermuxWarning,
23}
24
25#[derive(PartialEq, Eq, Debug, Clone, Copy)]
26pub enum FocusedPanel {
27    Categories,
28    Scripts,
29}
30
31pub struct StatefulList<T> {
32    pub state: ratatui::widgets::ListState,
33    pub items: Vec<T>,
34}
35
36impl<T> Default for StatefulList<T> {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl<T> StatefulList<T> {
43    pub fn new() -> StatefulList<T> {
44        StatefulList { state: ratatui::widgets::ListState::default(), items: Vec::new() }
45    }
46
47    pub fn with_items(items: Vec<T>) -> StatefulList<T> {
48        let mut list = StatefulList::new();
49        list.items = items;
50        if !list.items.is_empty() {
51            list.state.select(Some(0));
52        }
53        list
54    }
55
56    pub fn next(&mut self) {
57        if self.items.is_empty() {
58            self.state.select(None);
59            return;
60        }
61        let i = match self.state.selected() {
62            Some(i) if i + 1 < self.items.len() => i + 1,
63            _ => 0,
64        };
65        self.state.select(Some(i));
66    }
67
68    pub fn previous(&mut self) {
69        if self.items.is_empty() {
70            self.state.select(None);
71            return;
72        }
73        let last = self.items.len() - 1;
74        let i = match self.state.selected() {
75            Some(0) | None => last,
76            Some(i) => i - 1,
77        };
78        self.state.select(Some(i));
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn next_on_empty_does_not_panic() {
88        let mut list: StatefulList<u32> = StatefulList::new();
89        list.next();
90        list.next();
91        list.next();
92        assert_eq!(list.state.selected(), None);
93    }
94
95    #[test]
96    fn previous_on_empty_does_not_panic() {
97        let mut list: StatefulList<u32> = StatefulList::new();
98        list.previous();
99        list.previous();
100        assert_eq!(list.state.selected(), None);
101    }
102
103    #[test]
104    fn next_wraps_to_start() {
105        let mut list = StatefulList::with_items(vec!["a", "b", "c"]);
106        assert_eq!(list.state.selected(), Some(0));
107        list.next();
108        assert_eq!(list.state.selected(), Some(1));
109        list.next();
110        assert_eq!(list.state.selected(), Some(2));
111        list.next();
112        assert_eq!(list.state.selected(), Some(0));
113    }
114
115    #[test]
116    fn previous_wraps_to_end() {
117        let mut list = StatefulList::with_items(vec!["a", "b", "c"]);
118        list.previous();
119        assert_eq!(list.state.selected(), Some(2));
120        list.previous();
121        assert_eq!(list.state.selected(), Some(1));
122    }
123
124    #[test]
125    fn single_item_next_keeps_selection() {
126        let mut list = StatefulList::with_items(vec!["only"]);
127        list.next();
128        assert_eq!(list.state.selected(), Some(0));
129        list.previous();
130        assert_eq!(list.state.selected(), Some(0));
131    }
132}
133
134#[derive(Clone, Debug)]
135pub struct ScriptItem {
136    pub category: String,
137    pub name:     String,
138    pub path:     PathBuf,
139}
140
141#[derive(Default, Clone)]
142pub struct UiOptions {
143    pub log_mode:     bool,
144    pub theme:        String,
145    pub theme_locked: bool,
146    pub is_root:      bool,
147    pub log_path:     Option<String>,
148}
149
150#[derive(Default)]
151pub struct PreviewState {
152    pub content:    String,
153    pub scroll:     u16,
154    pub max_scroll: u16,
155    pub cache:      HashMap<PathBuf, Text<'static>>,
156}
157
158#[derive(Clone, Debug)]
159pub struct SearchResult {
160    pub item:    ScriptItem,
161    pub score:   i64,
162    pub indices: Vec<usize>,
163}
164
165#[derive(Default)]
166pub struct SearchState {
167    pub input:           String,
168    pub results:         Vec<SearchResult>,
169    pub cursor_position: usize,
170    pub selected_idx:    usize,
171    pub autocomplete:    Option<String>,
172    pub matcher:         SkimMatcherV2,
173}
174
175#[derive(Default)]
176pub struct MultiSelectState {
177    pub enabled: bool,
178    pub scripts: Vec<PathBuf>,
179}
180
181#[derive(Default)]
182pub struct HelpState {
183    pub scroll:     u16,
184    pub max_scroll: u16,
185}
186
187#[derive(Default)]
188pub struct DescriptionState {
189    pub content:    Option<String>,
190    pub scroll:     u16,
191    pub max_scroll: u16,
192}
193
194#[derive(Default)]
195pub struct ThemeSelectorState {
196    pub selected: usize,
197}
198
199pub struct App {
200    pub mode:          AppMode,
201    pub quit:          bool,
202    pub focused_panel: FocusedPanel,
203    pub log_mode:      bool,
204    pub modules_dir:   PathBuf,
205    pub theme:         Theme,
206    pub theme_locked:  bool,
207    pub distro:        String,
208
209    pub scripts:     StatefulList<ScriptItem>,
210    pub categories:  StatefulList<String>,
211    pub all_scripts: HashMap<String, Vec<ScriptItem>>,
212
213    pub script_panel_area:      Rect,
214    pub preview:                PreviewState,
215    pub search:                 SearchState,
216    pub multi_select:           MultiSelectState,
217    pub help:                   HelpState,
218    pub description:            DescriptionState,
219    pub run_script_popup:       Option<RunScriptPopup>,
220    pub script_execution_queue: VecDeque<PathBuf>,
221    pub theme_selector:         ThemeSelectorState,
222
223    pub needs_redraw: bool,
224    pub last_size:    Rect,
225    pub log_path:     Option<String>,
226}