1use log::info;
2use std::path::{Path, PathBuf};
3use std::{fs, io};
4
5use super::state::{
6 App, AppMode, FocusedPanel, ScriptItem, SearchResult, SearchState, StatefulList,
7};
8use fuzzy_matcher::FuzzyMatcher;
9
10impl<'a> App<'a> {
11 pub fn load_scripts(&mut self, modules_dir: &Path) -> io::Result<()> {
12 let mut categories = Vec::new();
13 let mut all_scripts = std::collections::HashMap::new();
14
15 for category_entry in fs::read_dir(modules_dir)? {
16 let category_entry = category_entry?;
17 let category_path = category_entry.path();
18
19 if category_path.is_dir() {
20 let category_name =
21 category_path.file_name().unwrap_or_default().to_string_lossy().into_owned();
22 categories.push(category_name.clone());
23
24 let mut scripts_in_category = Vec::new();
25 for script_entry in fs::read_dir(&category_path)? {
26 let script_entry = script_entry?;
27 let script_path = script_entry.path();
28
29 if script_path.is_file() && script_path.extension().unwrap_or_default() == "sh"
30 {
31 let script_name = script_path
32 .file_stem()
33 .unwrap_or_default()
34 .to_string_lossy()
35 .into_owned();
36
37 let script_item = ScriptItem {
38 category: category_name.clone(),
39 name: script_name,
40 path: script_path,
41 };
42 scripts_in_category.push(script_item);
43 }
44 }
45 scripts_in_category.sort_by(|a, b| a.name.cmp(&b.name));
46 all_scripts.insert(category_name, scripts_in_category);
47 }
48 }
49
50 categories.sort();
51 self.categories = StatefulList::with_items(categories);
52 self.all_scripts = all_scripts;
53
54 self.update_script_list();
55 self.update_preview();
56
57 Ok(())
58 }
59
60 pub fn update_script_list(&mut self) {
61 if let Some(scripts) = self
62 .categories
63 .state
64 .selected()
65 .and_then(|i| self.categories.items.get(i))
66 .and_then(|name| self.all_scripts.get(name))
67 {
68 self.scripts = StatefulList::with_items(scripts.clone());
69 if self.focused_panel == FocusedPanel::Scripts && !self.scripts.items.is_empty() {
70 self.scripts.state.select(Some(0));
71 } else {
72 self.scripts.state.select(None);
73 }
74 }
75 }
76
77 pub fn update_preview(&mut self) {
78 if let Some(selected) = self.scripts.state.selected() {
79 let script_path = &self.scripts.items[selected].path;
80 if !self.preview.cache.contains_key(script_path) {
81 match fs::read_to_string(script_path) {
82 Ok(content) => {
83 self.preview.content = content;
84 self.preview.scroll = 0;
85 }
86 Err(_) => {
87 self.preview.content = "Error loading script content".to_string();
88 }
89 }
90 }
91 } else {
92 self.preview.content = "No script selected".to_string();
93 }
94 }
95
96 pub fn toggle_preview_mode(&mut self) {
97 if self.scripts.state.selected().is_some() {
98 let prev_mode = self.mode;
99 self.mode = match self.mode {
100 AppMode::Normal => AppMode::Preview,
101 AppMode::Preview => AppMode::Normal,
102 _ => AppMode::Normal,
103 };
104
105 if self.log_mode {
106 if prev_mode == AppMode::Normal && self.mode == AppMode::Preview {
107 info!("Entered preview mode");
108 } else if prev_mode == AppMode::Preview && self.mode == AppMode::Normal {
109 info!("Exited preview mode");
110 }
111 }
112 self.update_preview();
113 }
114 }
115
116 pub fn scroll_preview_up(&mut self) {
117 self.preview.scroll = self.preview.scroll.saturating_sub(1);
118 }
119
120 pub fn scroll_preview_down(&mut self) {
121 self.preview.scroll = (self.preview.scroll + 1).min(self.preview.max_scroll);
122 }
123
124 pub fn scroll_preview_page_up(&mut self) {
125 self.preview.scroll = self.preview.scroll.saturating_sub(10);
126 }
127
128 pub fn scroll_preview_page_down(&mut self) {
129 self.preview.scroll = (self.preview.scroll + 10).min(self.preview.max_scroll);
130 }
131
132 pub fn get_script_path(&self) -> Option<PathBuf> {
133 if self.mode == AppMode::Search {
134 if let Some(selected_idx) = self.search.results.get(self.search.selected_idx) {
135 return Some(selected_idx.item.path.clone());
136 }
137 } else if let Some(script_item) =
138 self.scripts.state.selected().and_then(|idx| self.scripts.items.get(idx))
139 {
140 return Some(script_item.path.clone());
141 }
142 None
143 }
144
145 pub fn toggle_search_mode(&mut self) {
146 let prev_mode = self.mode;
147 self.mode = if self.mode == AppMode::Search { AppMode::Normal } else { AppMode::Search };
148
149 if self.log_mode {
150 if prev_mode != AppMode::Search && self.mode == AppMode::Search {
151 info!("Entered search mode");
152 } else if prev_mode == AppMode::Search && self.mode != AppMode::Search {
153 info!("Exited search mode");
154 }
155 }
156
157 if self.mode == AppMode::Search {
158 self.search = SearchState::default();
159 self.perform_search();
160 }
161 }
162
163 pub fn perform_search(&mut self) {
164 self.search.results.clear();
165
166 if self.search.input.is_empty() {
167 let mut all_scripts: Vec<_> = self
168 .all_scripts
169 .values()
170 .flat_map(|scripts| scripts.iter().cloned())
171 .map(|item| SearchResult { item, score: 0, indices: Vec::new() })
172 .collect();
173 all_scripts.sort_by(|a, b| a.item.name.cmp(&b.item.name));
174 self.search.results = all_scripts;
175 return;
176 }
177
178 let mut results = Vec::new();
179 for item in self.all_scripts.values().flat_map(|scripts| scripts.iter()) {
180 let choice = format!("{}/{}", item.category, item.name);
181 if let Some((score, indices)) =
182 self.search.matcher.fuzzy_indices(&choice, &self.search.input)
183 {
184 results.push(SearchResult { item: item.clone(), score, indices });
185 }
186 }
187
188 results.sort_by(|a, b| b.score.cmp(&a.score));
189 self.search.results = results;
190 }
191
192 pub fn next(&mut self) {
193 if self.log_mode {
194 info!("Navigating next in {:?}", self.focused_panel);
195 }
196 match self.focused_panel {
197 FocusedPanel::Categories => {
198 self.categories.next();
199 self.update_script_list();
200 self.update_preview();
201 }
202 FocusedPanel::Scripts => {
203 self.scripts.next();
204 self.update_preview();
205 }
206 }
207 }
208
209 pub fn previous(&mut self) {
210 if self.log_mode {
211 info!("Navigating previous in {:?}", self.focused_panel);
212 }
213 match self.focused_panel {
214 FocusedPanel::Categories => {
215 self.categories.previous();
216 self.update_script_list();
217 self.update_preview();
218 }
219 FocusedPanel::Scripts => {
220 self.scripts.previous();
221 self.update_preview();
222 }
223 }
224 }
225
226 pub fn update_autocomplete(&mut self) {
227 self.search.autocomplete = None;
228
229 if self.search.input.is_empty() {
230 return;
231 }
232
233 let search_term = self.search.input.to_lowercase();
234 let mut best_match = None;
235 let mut shortest_len = usize::MAX;
236
237 for (category_name, scripts) in &self.all_scripts {
238 for item in scripts {
239 if item.name.to_lowercase().starts_with(&search_term)
240 && item.name.len() > search_term.len()
241 && item.name.len() < shortest_len
242 {
243 best_match = Some(item.name.clone());
244 shortest_len = item.name.len();
245 }
246
247 let full_path = format!("{}/{}", category_name, item.name);
248 if full_path.to_lowercase().starts_with(&search_term)
249 && full_path.len() > search_term.len()
250 && full_path.len() < shortest_len
251 {
252 shortest_len = full_path.len();
253 best_match = Some(full_path);
254 }
255 }
256 }
257
258 self.search.autocomplete = best_match;
259 }
260
261 pub fn toggle_multi_select_mode(&mut self) {
262 self.multi_select.enabled = !self.multi_select.enabled;
263 if !self.multi_select.enabled {
264 self.multi_select.scripts.clear();
265 }
266 }
267
268 pub fn toggle_script_selection(&mut self) {
269 if let Some(selected) = self.scripts.state.selected() {
270 let script_path = &self.scripts.items[selected].path;
271 if self.multi_select.scripts.contains(script_path) {
272 self.multi_select.scripts.retain(|p| p != script_path);
273 } else {
274 self.multi_select.scripts.push(script_path.clone());
275 }
276 }
277 }
278
279 pub fn is_script_selected(&self, script_path: &Path) -> bool {
280 self.multi_select.scripts.contains(&script_path.to_path_buf())
281 }
282
283 pub fn toggle_help_mode(&mut self) {
284 self.mode = if self.mode == AppMode::Help { AppMode::Normal } else { AppMode::Help };
285 }
286
287 pub fn top(&mut self) {
288 match self.focused_panel {
289 FocusedPanel::Categories => {
290 self.categories.state.select(Some(0));
291 self.update_script_list();
292 self.update_preview();
293 }
294 FocusedPanel::Scripts => {
295 self.scripts.state.select(Some(0));
296 self.update_preview();
297 }
298 }
299 }
300
301 pub fn bottom(&mut self) {
302 match self.focused_panel {
303 FocusedPanel::Categories => {
304 let last_idx = self.categories.items.len() - 1;
305 self.categories.state.select(Some(last_idx));
306 self.update_script_list();
307 self.update_preview();
308 }
309 FocusedPanel::Scripts => {
310 let last_idx = self.scripts.items.len() - 1;
311 self.scripts.state.select(Some(last_idx));
312 self.update_preview();
313 }
314 }
315 }
316
317 pub fn handle_key_root_warning_mode(&mut self, key: crossterm::event::KeyEvent) {
318 match key.code {
319 crossterm::event::KeyCode::Char('y') | crossterm::event::KeyCode::Char('Y') => {
320 self.mode = AppMode::Normal;
321 }
322 crossterm::event::KeyCode::Char('n') | crossterm::event::KeyCode::Char('N') => {
323 self.quit = true;
324 }
325 _ => {}
326 }
327 }
328}