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}