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