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}