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}