Skip to main content

carch_core/ui/
actions.rs

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_key(|b| std::cmp::Reverse(b.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}