1use crate::{
2 app::{
3 app_helper::reset_preview_boards,
4 handle_exit,
5 state::{AppState, AppStatus, Focus},
6 App, AppReturn,
7 },
8 constants::RANDOM_SEARCH_TERM,
9 io::{io_handler::refresh_visible_boards_and_cards, IoEvent},
10 ui::{widgets::Widget, PopUp, View},
11};
12use log::{debug, error, info};
13use std::{
14 fmt::{self, Display},
15 vec,
16};
17use strum::{EnumIter, EnumString, IntoEnumIterator};
18
19#[derive(Debug)]
20pub struct CommandPaletteWidget {
21 pub already_in_user_input_mode: bool,
22 pub available_commands: Vec<CommandPaletteActions>,
23 pub board_search_results: Option<Vec<(String, (u64, u64))>>,
24 pub card_search_results: Option<Vec<(String, (u64, u64))>>,
25 pub command_search_results: Option<Vec<CommandPaletteActions>>,
26 pub last_focus: Option<Focus>,
27 pub last_search_string: String,
28}
29
30impl CommandPaletteWidget {
31 pub fn new(debug_mode: bool) -> Self {
32 let available_commands = CommandPaletteActions::all(debug_mode);
33 Self {
34 already_in_user_input_mode: false,
35 available_commands,
36 board_search_results: None,
37 card_search_results: None,
38 command_search_results: None,
39 last_focus: None,
40 last_search_string: RANDOM_SEARCH_TERM.to_string(),
41 }
42 }
43
44 pub fn reset(&mut self, app_state: &mut AppState) {
45 self.board_search_results = None;
46 self.card_search_results = None;
47 self.command_search_results = None;
48 self.last_search_string = RANDOM_SEARCH_TERM.to_string();
49 app_state.text_buffers.command_palette.reset();
50 Self::reset_list_states(app_state);
51 }
52
53 pub fn reset_list_states(app_state: &mut AppState) {
54 app_state
55 .app_list_states
56 .command_palette_command_search
57 .select(None);
58 app_state
59 .app_list_states
60 .command_palette_card_search
61 .select(None);
62 app_state
63 .app_list_states
64 .command_palette_board_search
65 .select(None);
66 }
67
68 pub async fn handle_command(app: &mut App<'_>) -> AppReturn {
69 if let Some(command_index) = app
70 .state
71 .app_list_states
72 .command_palette_command_search
73 .selected()
74 {
75 if let Some(command) =
76 if let Some(search_results) = &app.widgets.command_palette.command_search_results {
77 search_results.get(command_index)
78 } else {
79 None
80 }
81 {
82 match command {
83 CommandPaletteActions::Quit => {
84 info!("Quitting");
85 return handle_exit(app).await;
86 }
87 CommandPaletteActions::ConfigMenu => {
88 app.close_popup();
89 app.set_view(View::ConfigMenu);
90 app.state.app_table_states.config.select(Some(0));
91 }
92 CommandPaletteActions::MainMenu => {
93 app.close_popup();
94 app.set_view(View::MainMenu);
95 app.state.app_list_states.main_menu.select(Some(0));
96 }
97 CommandPaletteActions::HelpMenu => {
98 app.close_popup();
99 app.set_view(View::HelpMenu);
100 app.state.app_table_states.help.select(Some(0));
101 }
102 CommandPaletteActions::SaveKanbanState => {
103 app.close_popup();
104 app.dispatch(IoEvent::SaveLocalData).await;
105 }
106 CommandPaletteActions::NewBoard => {
107 if View::views_with_kanban_board().contains(&app.state.current_view) {
108 app.close_popup();
109 app.set_view(View::NewBoard);
110 } else {
111 app.close_popup();
112 app.send_error_toast("Cannot create a new board in this view", None);
113 }
114 }
115 CommandPaletteActions::NewCard => {
116 if View::views_with_kanban_board().contains(&app.state.current_view) {
117 if app.state.current_board_id.is_none() {
118 app.send_error_toast("No board Selected / Available", None);
119 app.close_popup();
120 app.state.app_status = AppStatus::Initialized;
121 return AppReturn::Continue;
122 }
123 app.close_popup();
124 app.set_view(View::NewCard);
125 } else {
126 app.close_popup();
127 app.send_error_toast("Cannot create a new card in this view", None);
128 }
129 }
130 CommandPaletteActions::ResetUI => {
131 app.close_popup();
132 app.set_view(app.config.default_view);
133 app.dispatch(IoEvent::ResetVisibleBoardsandCards).await;
134 }
135 CommandPaletteActions::ChangeView => {
136 app.close_popup();
137 app.set_popup(PopUp::ChangeView);
138 }
139 CommandPaletteActions::ChangeCurrentCardStatus => {
140 if !View::views_with_kanban_board().contains(&app.state.current_view) {
141 app.send_error_toast("Cannot change card status in this view", None);
142 return AppReturn::Continue;
143 }
144 if let Some(current_board_id) = app.state.current_board_id {
145 if let Some(current_board) =
146 app.boards.get_mut_board_with_id(current_board_id)
147 {
148 if let Some(current_card_id) = app.state.current_card_id {
149 if current_board
150 .cards
151 .get_card_with_id(current_card_id)
152 .is_some()
153 {
154 app.close_popup();
155 app.set_popup(PopUp::CardStatusSelector);
156 app.state.app_status = AppStatus::Initialized;
157 app.state
158 .app_list_states
159 .card_status_selector
160 .select(Some(0));
161 return AppReturn::Continue;
162 }
163 }
164 }
165 }
166 app.send_error_toast("Could not find current card", None);
167 }
168 CommandPaletteActions::ChangeCurrentCardPriority => {
169 if !View::views_with_kanban_board().contains(&app.state.current_view) {
170 app.send_error_toast("Cannot change card priority in this view", None);
171 return AppReturn::Continue;
172 }
173 if let Some(current_board_id) = app.state.current_board_id {
174 if let Some(current_board) =
175 app.boards.get_mut_board_with_id(current_board_id)
176 {
177 if let Some(current_card_id) = app.state.current_card_id {
178 if current_board
179 .cards
180 .get_card_with_id(current_card_id)
181 .is_some()
182 {
183 app.close_popup();
184 app.set_popup(PopUp::CardPrioritySelector);
185 app.state.app_status = AppStatus::Initialized;
186 app.state
187 .app_list_states
188 .card_priority_selector
189 .select(Some(0));
190 return AppReturn::Continue;
191 }
192 }
193 }
194 }
195 app.send_error_toast("Could not find current card", None);
196 }
197 CommandPaletteActions::LoadASaveLocal => {
198 app.close_popup();
199 reset_preview_boards(app);
200 app.set_view(View::LoadLocalSave);
201 }
202 CommandPaletteActions::DebugMenu => {
203 app.state.debug_menu_toggled = !app.state.debug_menu_toggled;
204 app.close_popup();
205 }
206 CommandPaletteActions::ChangeTheme => {
207 app.close_popup();
208 app.set_popup(PopUp::ChangeTheme);
209 }
210 CommandPaletteActions::CreateATheme => {
211 app.set_view(View::CreateTheme);
212 app.close_popup();
213 }
214 CommandPaletteActions::FilterByTag => {
215 let tags = app.calculate_tags();
216 if tags.is_empty() {
217 app.send_warning_toast("No tags found to filter with", None);
218 } else {
219 app.close_popup();
220 app.set_popup(PopUp::FilterByTag);
221 app.state.all_available_tags = Some(tags);
222 }
223 }
224 CommandPaletteActions::ClearFilter => {
225 if app.filtered_boards.is_empty() {
226 app.send_warning_toast("No filters to clear", None);
227 return AppReturn::Continue;
228 } else {
229 app.send_info_toast("All Filters Cleared", None);
230 }
231 app.state.filter_tags = None;
232 app.state.all_available_tags = None;
233 app.state.app_list_states.filter_by_tag_list.select(None);
234 app.close_popup();
235 app.filtered_boards.reset();
236 refresh_visible_boards_and_cards(app);
237 }
238 CommandPaletteActions::ChangeDateFormat => {
239 app.close_popup();
240 app.set_popup(PopUp::ChangeDateFormatPopup);
241 }
242 CommandPaletteActions::NoCommandsFound => {
243 app.close_popup();
244 app.state.app_status = AppStatus::Initialized;
245 return AppReturn::Continue;
246 }
247 CommandPaletteActions::Login => {
248 if app.state.user_login_data.auth_token.is_some() {
249 app.send_error_toast("Already logged in", None);
250 app.close_popup();
251 app.state.app_status = AppStatus::Initialized;
252 return AppReturn::Continue;
253 }
254 app.set_view(View::Login);
255 app.close_popup();
256 }
257 CommandPaletteActions::Logout => {
258 app.dispatch(IoEvent::Logout).await;
259 app.close_popup();
260 }
261 CommandPaletteActions::SignUp => {
262 app.set_view(View::SignUp);
263 app.close_popup();
264 }
265 CommandPaletteActions::ResetPassword => {
266 app.set_view(View::ResetPassword);
267 app.close_popup();
268 }
269 CommandPaletteActions::SyncLocalData => {
270 app.dispatch(IoEvent::SyncLocalData).await;
271 app.close_popup();
272 }
273 CommandPaletteActions::LoadASaveCloud => {
274 if app.state.user_login_data.auth_token.is_some() {
275 app.set_view(View::LoadCloudSave);
276 reset_preview_boards(app);
277 app.dispatch(IoEvent::GetCloudData).await;
278 app.close_popup();
279 } else {
280 error!("Not logged in");
281 app.send_error_toast("Not logged in", None);
282 app.close_popup();
283 app.state.app_status = AppStatus::Initialized;
284 return AppReturn::Continue;
285 }
286 }
287 CommandPaletteActions::MoveBoardLeft => {
288 if let Some(current_board_id) = app.state.current_board_id {
289 let current_board_index = app.boards.get_board_index(current_board_id);
290 if current_board_index.is_none() {
291 app.send_error_toast("No board selected", None);
292 return AppReturn::Continue;
293 }
294 let current_board_index = current_board_index.unwrap();
295 let board_name = app
296 .boards
297 .get_board_with_id(current_board_id)
298 .unwrap()
299 .name
300 .clone();
301 if current_board_index == 0 {
302 app.send_error_toast(
303 format!("'{}' is already the first board", board_name).as_str(),
304 None,
305 );
306 return AppReturn::Continue;
307 }
308 let swap_result = app
309 .boards
310 .swap(current_board_index, current_board_index - 1);
311
312 if swap_result.is_err() {
313 app.send_error_toast(
314 format!("Could not move '{}' to the left", board_name).as_str(),
315 None,
316 );
317 }
318
319 app.close_popup();
320 app.send_info_toast(
321 format!("'{}' moved to the left", board_name).as_str(),
322 None,
323 );
324 refresh_visible_boards_and_cards(app);
325 } else {
326 app.send_error_toast("No board selected", None);
327 }
328 }
329 CommandPaletteActions::MoveBoardRight => {
330 if let Some(current_board_id) = app.state.current_board_id {
331 let current_board_index = app.boards.get_board_index(current_board_id);
332 if current_board_index.is_none() {
333 app.send_error_toast("No board selected", None);
334 return AppReturn::Continue;
335 }
336 let current_board_index = current_board_index.unwrap();
337 let board_name = app
338 .boards
339 .get_board_with_id(current_board_id)
340 .unwrap()
341 .name
342 .clone();
343 if current_board_index == app.boards.get_boards().len() - 1 {
344 app.send_error_toast(
345 format!("'{}' is already the last board", board_name).as_str(),
346 None,
347 );
348 return AppReturn::Continue;
349 }
350 let swap_result = app
351 .boards
352 .swap(current_board_index, current_board_index + 1);
353
354 if swap_result.is_err() {
355 app.send_error_toast(
356 format!("Could not move '{}' to the right", board_name)
357 .as_str(),
358 None,
359 );
360 }
361
362 app.close_popup();
363 app.send_info_toast(
364 format!("'{}' moved to the right", board_name).as_str(),
365 None,
366 );
367 refresh_visible_boards_and_cards(app);
368 } else {
369 app.send_error_toast("No board selected", None);
370 }
371 }
372 }
373 app.widgets.command_palette.reset(&mut app.state);
374 } else {
375 debug!("No command found for the command palette");
376 }
377 } else {
378 return AppReturn::Continue;
379 }
380 if app.widgets.command_palette.already_in_user_input_mode {
381 app.widgets.command_palette.already_in_user_input_mode = false;
382 app.widgets.command_palette.last_focus = None;
383 }
384 if app.state.z_stack.last() != Some(&PopUp::CustomHexColorPromptFG)
385 || app.state.z_stack.last() != Some(&PopUp::CustomHexColorPromptBG)
386 {
387 app.state.app_status = AppStatus::Initialized;
388 }
389 AppReturn::Continue
390 }
391}
392
393impl Widget for CommandPaletteWidget {
394 fn update(app: &mut App) {
395 if let Some(PopUp::CommandPalette) = app.state.z_stack.last() {
396 if app
397 .state
398 .text_buffers
399 .command_palette
400 .get_joined_lines()
401 .to_lowercase()
402 == app.widgets.command_palette.last_search_string
403 {
404 return;
405 }
406 let current_search_string = app.state.text_buffers.command_palette.get_joined_lines();
407 let current_search_string = current_search_string.to_lowercase();
408 let search_results = app
409 .widgets
410 .command_palette
411 .available_commands
412 .iter()
413 .filter(|action| {
414 action
415 .to_string()
416 .to_lowercase()
417 .contains(¤t_search_string)
418 })
419 .cloned()
420 .collect::<Vec<CommandPaletteActions>>();
421
422 let mut command_search_results = if search_results.is_empty() {
424 if current_search_string.is_empty() {
425 CommandPaletteActions::all(app.debug_mode)
426 } else {
427 let all_actions = CommandPaletteActions::all(app.debug_mode);
428 let mut results = vec![];
429 for action in all_actions {
430 if action
431 .to_string()
432 .to_lowercase()
433 .starts_with(¤t_search_string)
434 {
435 results.push(action);
436 }
437 }
438 results
439 }
440 } else {
441 let mut ordered_command_search_results = vec![];
442 let mut extra_command_results = vec![];
443 for result in search_results {
444 if result
445 .to_string()
446 .to_lowercase()
447 .starts_with(¤t_search_string)
448 {
449 ordered_command_search_results.push(result);
450 } else {
451 extra_command_results.push(result);
452 }
453 }
454 ordered_command_search_results.extend(extra_command_results);
455 ordered_command_search_results
456 };
457 if command_search_results.is_empty() {
458 command_search_results = vec![CommandPaletteActions::NoCommandsFound]
459 }
460
461 let mut card_search_results: Vec<(String, (u64, u64))> = vec![];
462 if !current_search_string.is_empty() {
463 for board in app.boards.get_boards() {
464 for card in board.cards.get_all_cards() {
465 let search_helper =
466 if card.name.to_lowercase().contains(¤t_search_string) {
467 format!("{} - Matched in Name", card.name)
468 } else if card
469 .description
470 .to_lowercase()
471 .contains(¤t_search_string)
472 {
473 format!("{} - Matched in Description", card.name)
474 } else if card
475 .tags
476 .iter()
477 .any(|tag| tag.to_lowercase().contains(¤t_search_string))
478 {
479 format!("{} - Matched in Tags", card.name)
480 } else if card.comments.iter().any(|comment| {
481 comment.to_lowercase().contains(¤t_search_string)
482 }) {
483 format!("{} - Matched in Comments", card.name)
484 } else {
485 String::new()
486 };
487 if !search_helper.is_empty() {
488 card_search_results.push((search_helper, card.id));
489 }
490 }
491 }
492 }
493 if card_search_results.is_empty() {
494 app.widgets.command_palette.card_search_results = None;
495 } else {
496 app.widgets.command_palette.card_search_results = Some(card_search_results.clone());
497 }
498
499 let mut board_search_results: Vec<(String, (u64, u64))> = vec![];
500 if !current_search_string.is_empty() {
501 for board in app.boards.get_boards() {
502 let search_helper =
503 if board.name.to_lowercase().contains(¤t_search_string) {
504 format!("{} - Matched in Name", board.name)
505 } else if board
506 .description
507 .to_lowercase()
508 .contains(¤t_search_string)
509 {
510 format!("{} - Matched in Description", board.name)
511 } else {
512 String::new()
513 };
514 if !search_helper.is_empty() {
515 board_search_results.push((search_helper, board.id));
516 }
517 }
518 }
519 if board_search_results.is_empty() {
520 app.widgets.command_palette.board_search_results = None;
521 } else {
522 app.widgets.command_palette.board_search_results =
523 Some(board_search_results.clone());
524 }
525
526 app.widgets.command_palette.command_search_results = Some(command_search_results);
527 app.widgets.command_palette.last_search_string = current_search_string;
528 if let Some(search_results) = &app.widgets.command_palette.command_search_results {
529 if !search_results.is_empty() {
530 app.state
531 .app_list_states
532 .command_palette_command_search
533 .select(Some(0));
534 }
535 }
536 }
537 }
538}
539
540#[derive(Clone, Debug, PartialEq, EnumIter, EnumString)]
541pub enum CommandPaletteActions {
542 ChangeCurrentCardStatus,
543 ChangeCurrentCardPriority,
544 ChangeDateFormat,
545 ChangeTheme,
546 ChangeView,
547 ClearFilter,
548 ConfigMenu,
549 CreateATheme,
550 DebugMenu,
551 FilterByTag,
552 HelpMenu,
553 LoadASaveCloud,
554 LoadASaveLocal,
555 Login,
556 Logout,
557 MainMenu,
558 NewBoard,
559 NewCard,
560 NoCommandsFound,
561 Quit,
562 ResetPassword,
563 ResetUI,
564 SaveKanbanState,
565 SignUp,
566 SyncLocalData,
567 MoveBoardLeft,
568 MoveBoardRight,
569}
570
571impl Display for CommandPaletteActions {
572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
573 match self {
574 Self::ChangeCurrentCardStatus => write!(f, "Change Current Card Status"),
575 Self::ChangeCurrentCardPriority => write!(f, "Change Current Card Priority"),
576 Self::ChangeDateFormat => write!(f, "Change Date Format"),
577 Self::ChangeTheme => write!(f, "Change Theme"),
578 Self::ChangeView => write!(f, "Change View"),
579 Self::ClearFilter => write!(f, "Clear Filter"),
580 Self::CreateATheme => write!(f, "Create a Theme"),
581 Self::DebugMenu => write!(f, "Toggle Debug Panel"),
582 Self::FilterByTag => write!(f, "Filter by Tag"),
583 Self::LoadASaveCloud => write!(f, "Load a Save (Cloud)"),
584 Self::LoadASaveLocal => write!(f, "Load a Save (Local)"),
585 Self::Login => write!(f, "Login"),
586 Self::Logout => write!(f, "Logout"),
587 Self::NewBoard => write!(f, "New Board"),
588 Self::NewCard => write!(f, "New Card"),
589 Self::NoCommandsFound => write!(f, "No Commands Found"),
590 Self::ConfigMenu => write!(f, "Configure"),
591 Self::HelpMenu => write!(f, "Open Help Menu"),
592 Self::MainMenu => write!(f, "Open Main Menu"),
593 Self::Quit => write!(f, "Quit"),
594 Self::ResetPassword => write!(f, "Reset Password"),
595 Self::ResetUI => write!(f, "Reset UI"),
596 Self::SaveKanbanState => write!(f, "Save Kanban State"),
597 Self::SignUp => write!(f, "Sign Up"),
598 Self::SyncLocalData => write!(f, "Sync Local Data"),
599 Self::MoveBoardLeft => write!(f, "Move Current Board Left"),
600 Self::MoveBoardRight => write!(f, "Move Current Board Right"),
601 }
602 }
603}
604
605impl CommandPaletteActions {
606 pub fn all(debug_mode: bool) -> Vec<Self> {
607 let mut all = CommandPaletteActions::iter().collect::<Vec<Self>>();
608 all.sort_by_key(|a| a.to_string());
610 all.retain(|action| !matches!(action, Self::NoCommandsFound));
612
613 if cfg!(debug_assertions) || debug_mode {
614 all
615 } else {
616 all.into_iter()
617 .filter(|action| !matches!(action, Self::DebugMenu))
618 .collect()
619 }
620 }
621}