Skip to main content

carch_core/ui/
events.rs

1use std::path::PathBuf;
2use std::{env, fs};
3
4use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5use log::info;
6
7use super::popups::run_script::RunScriptPopup;
8use super::state::{App, AppMode, FocusedPanel};
9use crate::VALID_THEMES;
10
11impl App {
12    pub fn handle_search_input(&mut self, key: KeyEvent) {
13        match key.code {
14            KeyCode::Esc => {
15                if self.log_mode {
16                    info!("Closed search.");
17                }
18                self.toggle_search_mode();
19            }
20            KeyCode::Enter if !self.search.results.is_empty() => {
21                let selected_item = self.search.results[self.search.selected_idx].clone();
22                if self.log_mode {
23                    info!(
24                        "Selected script from search: {}/{}",
25                        selected_item.item.category, selected_item.item.name
26                    );
27                }
28
29                if let Some(category_idx) =
30                    self.categories.items.iter().position(|c| *c == selected_item.item.category)
31                {
32                    self.categories.state.select(Some(category_idx));
33                    self.update_script_list();
34
35                    if let Some(script_idx) =
36                        self.scripts.items.iter().position(|s| s.name == selected_item.item.name)
37                    {
38                        self.scripts.state.select(Some(script_idx));
39                    }
40                }
41
42                self.update_preview();
43                self.toggle_search_mode();
44                self.focused_panel = FocusedPanel::Scripts;
45                self.mode = AppMode::Normal;
46            }
47            KeyCode::Down if !self.search.results.is_empty() => {
48                self.search.selected_idx =
49                    (self.search.selected_idx + 1) % self.search.results.len();
50            }
51            KeyCode::Up if !self.search.results.is_empty() => {
52                self.search.selected_idx = if self.search.selected_idx > 0 {
53                    self.search.selected_idx - 1
54                } else {
55                    self.search.results.len() - 1
56                };
57            }
58            KeyCode::Tab => {
59                if let Some(autocomplete) = self.search.autocomplete.take() {
60                    self.search.input = autocomplete;
61                    self.search.cursor_position = self.search.input.len();
62                    self.perform_search();
63                    self.update_autocomplete();
64                }
65            }
66            KeyCode::Char(c) => {
67                self.search.input.push(c);
68                self.search.cursor_position += 1;
69                self.perform_search();
70                self.update_autocomplete();
71                self.search.selected_idx = 0;
72            }
73            KeyCode::Backspace if self.search.cursor_position > 0 => {
74                self.search.input.remove(self.search.cursor_position - 1);
75                self.search.cursor_position -= 1;
76                self.perform_search();
77                self.update_autocomplete();
78                self.search.selected_idx = 0;
79            }
80            KeyCode::Left => {
81                if self.search.cursor_position > 0 {
82                    self.search.cursor_position -= 1;
83                }
84                self.search.autocomplete = None;
85            }
86            KeyCode::Right => {
87                if self.search.cursor_position < self.search.input.len() {
88                    self.search.cursor_position += 1;
89                }
90                if self.search.cursor_position == self.search.input.len()
91                    && let Some(autocomplete) = self.search.autocomplete.take()
92                {
93                    self.search.input = autocomplete;
94                    self.search.cursor_position = self.search.input.len();
95                    self.perform_search();
96                }
97            }
98            _ => {}
99        }
100    }
101
102    pub fn handle_key_normal_mode(&mut self, key: KeyEvent) {
103        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
104            if self.log_mode {
105                info!("Quit TUI.");
106            }
107            self.quit = true;
108            return;
109        }
110        match key.code {
111            KeyCode::Char('q') => {
112                if self.multi_select.enabled {
113                    if self.log_mode {
114                        info!("Disabled multi-select mode.");
115                    }
116                    self.toggle_multi_select_mode();
117                } else {
118                    if self.log_mode {
119                        info!("Quit TUI.");
120                    }
121                    self.quit = true;
122                }
123            }
124            KeyCode::Esc if self.multi_select.enabled => {
125                if self.log_mode {
126                    info!("Disabled multi-select mode.");
127                }
128                self.toggle_multi_select_mode();
129            }
130            KeyCode::Char('j') | KeyCode::Down => {
131                self.next();
132            }
133            KeyCode::Char('k') | KeyCode::Up => {
134                self.previous();
135            }
136            KeyCode::Char('h') | KeyCode::Left => {
137                self.focused_panel = FocusedPanel::Categories;
138                self.scripts.state.select(None);
139            }
140            KeyCode::Char('l') | KeyCode::Right => {
141                if self.focused_panel == FocusedPanel::Scripts {
142                    if self.scripts.state.selected().is_some()
143                        && !(self.multi_select.enabled && self.multi_select.scripts.is_empty())
144                    {
145                        if self.log_mode {
146                            info!("Entered confirmation mode.");
147                        }
148                        self.mode = AppMode::Confirm;
149                    }
150                } else {
151                    self.focused_panel = FocusedPanel::Scripts;
152                    if !self.scripts.items.is_empty() {
153                        self.scripts.state.select(Some(0));
154                    }
155                }
156            }
157            KeyCode::Home => {
158                self.top();
159            }
160            KeyCode::End => {
161                self.bottom();
162            }
163            KeyCode::Char('/') => {
164                if self.log_mode {
165                    info!("Opened search.");
166                }
167                self.toggle_search_mode();
168            }
169            KeyCode::Char('p') => {
170                if self.log_mode {
171                    info!("Opened preview.");
172                }
173                self.toggle_preview_mode();
174            }
175            KeyCode::Char('m') => {
176                if self.log_mode {
177                    info!("Toggled multi-select mode.");
178                }
179                self.toggle_multi_select_mode();
180            }
181            KeyCode::Char('?') => {
182                if self.log_mode {
183                    info!("Opened help.");
184                }
185                self.toggle_help_mode();
186            }
187            KeyCode::Char('d') => {
188                if self.log_mode {
189                    info!("Opened description.");
190                }
191                self.toggle_description_popup();
192            }
193            KeyCode::Char('t') => {
194                if self.log_mode {
195                    info!("Opened theme selector.");
196                }
197                self.toggle_theme_selector();
198            }
199            KeyCode::Enter
200                if self.focused_panel == FocusedPanel::Scripts
201                    && self.scripts.state.selected().is_some()
202                    && !(self.multi_select.enabled && self.multi_select.scripts.is_empty()) =>
203            {
204                if self.log_mode {
205                    info!("Entered confirmation mode.");
206                }
207                self.mode = AppMode::Confirm;
208            }
209            KeyCode::Char(' ') if self.multi_select.enabled => {
210                if self.log_mode {
211                    info!("Toggled script selection in multi-select mode.");
212                }
213                self.toggle_script_selection();
214            }
215            _ => {}
216        }
217    }
218
219    pub fn handle_key_preview_mode(&mut self, key: KeyEvent) {
220        match key.code {
221            KeyCode::Char('q') | KeyCode::Esc | KeyCode::Char('p') | KeyCode::Char('h') => {
222                if self.log_mode {
223                    info!("Closed preview.");
224                }
225                self.mode = AppMode::Normal;
226            }
227            KeyCode::Char('j') | KeyCode::Down => {
228                self.scroll_preview_down();
229            }
230            KeyCode::Char('k') | KeyCode::Up => {
231                self.scroll_preview_up();
232            }
233            KeyCode::PageDown => {
234                self.scroll_preview_page_down();
235            }
236            KeyCode::PageUp => {
237                self.scroll_preview_page_up();
238            }
239            _ => {}
240        }
241    }
242
243    pub fn handle_key_confirmation_mode(&mut self, key: KeyEvent) {
244        match key.code {
245            KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Char('l') | KeyCode::Right => {
246                if self.log_mode {
247                    if self.multi_select.enabled && !self.multi_select.scripts.is_empty() {
248                        info!(
249                            "Confirmed execution of {} scripts.",
250                            self.multi_select.scripts.len()
251                        );
252                    } else if let Some(script_path) = self.get_script_path() {
253                        info!("Confirmed execution of script: {}", script_path.display());
254                    }
255                }
256
257                if self.multi_select.enabled && !self.multi_select.scripts.is_empty() {
258                    self.script_execution_queue = self.multi_select.scripts.clone().into();
259                } else if let Some(script_path) = self.get_script_path() {
260                    self.script_execution_queue.push_back(script_path);
261                }
262
263                if !self.script_execution_queue.is_empty() {
264                    let script_path = self
265                        .script_execution_queue
266                        .pop_front()
267                        .expect("queue checked is_empty above");
268                    match RunScriptPopup::new(
269                        script_path,
270                        self.log_mode,
271                        self.theme.clone(),
272                        self.log_path.clone(),
273                    ) {
274                        Ok(popup) => {
275                            self.run_script_popup = Some(popup);
276                            self.mode = AppMode::RunScript;
277                        }
278                        Err(e) => {
279                            log::error!("Failed to start script popup: {e}");
280                            self.run_script_popup = None;
281                            self.mode = AppMode::Normal;
282                        }
283                    }
284                } else {
285                    self.mode = AppMode::Normal;
286                }
287            }
288            KeyCode::Char('n')
289            | KeyCode::Char('N')
290            | KeyCode::Esc
291            | KeyCode::Char('h')
292            | KeyCode::Char('q')
293            | KeyCode::Left => {
294                if self.log_mode {
295                    info!("Cancelled script execution.");
296                }
297                self.mode = AppMode::Normal;
298            }
299            _ => {}
300        }
301    }
302
303    pub fn handle_key_help_mode(&mut self, key: KeyEvent) {
304        match key.code {
305            KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('?') => {
306                if self.log_mode {
307                    info!("Closed help.");
308                }
309                self.mode = AppMode::Normal;
310                self.help.scroll = 0;
311            }
312            KeyCode::Char('j') | KeyCode::Down => {
313                self.help.scroll = (self.help.scroll + 1).min(self.help.max_scroll);
314            }
315            KeyCode::Char('k') | KeyCode::Up => {
316                self.help.scroll = self.help.scroll.saturating_sub(1);
317            }
318            KeyCode::Home => {
319                self.help.scroll = 0;
320            }
321            KeyCode::End => {
322                self.help.scroll = self.help.max_scroll;
323            }
324            KeyCode::PageDown => {
325                self.help.scroll = (self.help.scroll + 10).min(self.help.max_scroll);
326            }
327            KeyCode::PageUp => {
328                self.help.scroll = self.help.scroll.saturating_sub(10);
329            }
330            _ => {}
331        }
332    }
333
334    pub fn handle_key_description_mode(&mut self, key: KeyEvent) {
335        match key.code {
336            KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('d') => {
337                if self.log_mode {
338                    info!("Closed description.");
339                }
340                self.mode = AppMode::Normal;
341                self.description.content = None;
342                self.description.scroll = 0;
343            }
344            KeyCode::Char('j') | KeyCode::Down => {
345                self.description.scroll =
346                    (self.description.scroll + 1).min(self.description.max_scroll);
347            }
348            KeyCode::Char('k') | KeyCode::Up => {
349                self.description.scroll = self.description.scroll.saturating_sub(1);
350            }
351            _ => {}
352        }
353    }
354
355    pub fn toggle_theme_selector(&mut self) {
356        self.theme_selector.selected =
357            VALID_THEMES.iter().position(|t| theme_display_name(t) == self.theme.name).unwrap_or(0);
358        self.mode = AppMode::ThemeSelector;
359    }
360
361    pub fn handle_key_theme_selector_mode(&mut self, key: KeyEvent) {
362        match key.code {
363            KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('t') => {
364                if self.log_mode {
365                    info!("Cancelled theme selection.");
366                }
367                self.mode = AppMode::Normal;
368            }
369            KeyCode::Enter => {
370                if let Some(theme) = VALID_THEMES.get(self.theme_selector.selected) {
371                    if self.log_mode {
372                        info!("Set default theme to: {}", theme_display_name(theme));
373                    }
374                    save_theme_state(theme);
375                    self.theme = match *theme {
376                        "dracula" => super::theme::Theme::dracula(),
377                        "gruvbox" => super::theme::Theme::gruvbox(),
378                        "nord" => super::theme::Theme::nord(),
379                        "rose-pine" => super::theme::Theme::rose_pine(),
380                        _ => super::theme::Theme::catppuccin_mocha(),
381                    };
382                    self.theme_locked = true;
383                }
384                self.mode = AppMode::Normal;
385            }
386            KeyCode::Char('j') | KeyCode::Down => {
387                let len = VALID_THEMES.len();
388                self.theme_selector.selected = (self.theme_selector.selected + 1) % len;
389            }
390            KeyCode::Char('k') | KeyCode::Up => {
391                let len = VALID_THEMES.len();
392                self.theme_selector.selected = (self.theme_selector.selected + len - 1) % len;
393            }
394            _ => {}
395        }
396    }
397}
398
399fn theme_display_name(name: &str) -> &str {
400    match name {
401        "catppuccin-mocha" => "Catppuccin Mocha",
402        "dracula" => "Dracula",
403        "gruvbox" => "Gruvbox",
404        "nord" => "Nord",
405        "rose-pine" => "Rosé Pine",
406        _ => name,
407    }
408}
409
410fn state_path() -> Option<PathBuf> {
411    let home = env::var("HOME").ok()?;
412    Some(PathBuf::from(home).join(".config/carch/state.toml"))
413}
414
415fn save_theme_state(theme: &str) {
416    let path = match state_path() {
417        Some(p) => p,
418        None => return,
419    };
420    if let Some(parent) = path.parent() {
421        let _ = fs::create_dir_all(parent);
422    }
423    let contents = format!("theme = \"{}\"\n", theme);
424    let _ = fs::write(&path, contents);
425}