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