glues_tui/
action.rs

1use {
2    super::{
3        App,
4        config::{self, LAST_PROXY_URL},
5        context::{ContextPrompt, QuitMenu},
6        logger::*,
7    },
8    crate::input::{Input, KeyCode},
9    glues_core::{EntryEvent, Event, KeyEvent, NotebookEvent, NumKey, state::EntryState},
10    ratatui::text::Line,
11};
12
13#[cfg(target_arch = "wasm32")]
14use super::config::LAST_IDB_NAMESPACE;
15
16#[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
17use ratatui::style::Stylize;
18
19#[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
20use super::theme::THEME;
21
22#[cfg(not(target_arch = "wasm32"))]
23use super::config::{
24    LAST_FILE_PATH, LAST_GIT_BRANCH, LAST_GIT_PATH, LAST_GIT_REMOTE, LAST_MONGO_CONN_STR,
25    LAST_MONGO_DB_NAME, LAST_REDB_PATH,
26};
27
28#[derive(Clone)]
29pub enum Action {
30    Tui(TuiAction),
31    Dispatch(Event),
32    PassThrough,
33    None,
34}
35
36#[derive(Clone)]
37pub enum TuiAction {
38    Alert(String),
39    Confirm {
40        message: String,
41        action: Box<Action>,
42    },
43    SaveAndConfirm {
44        // todo: there might be a better way to do this
45        message: String,
46        action: Box<Action>,
47    },
48    Prompt {
49        message: Vec<Line<'static>>,
50        action: Box<Action>,
51        default: Option<String>,
52    },
53    Help,
54    OpenThemeMenu,
55    ShowEditorKeymap,
56    SaveAndPassThrough,
57    OpenNotebookQuitMenu {
58        save_before_open: bool,
59    },
60    ReturnToEntry,
61    Quit,
62
63    OpenFile,
64    #[cfg(not(target_arch = "wasm32"))]
65    OpenRedb,
66    OpenGit(OpenGitStep),
67    OpenMongo(OpenMongoStep),
68    OpenProxy(OpenProxyStep),
69    #[cfg(target_arch = "wasm32")]
70    OpenIndexedDb,
71
72    RenameNote,
73    RemoveNote,
74    AddNote,
75    AddDirectory,
76    RenameDirectory,
77    RemoveDirectory,
78}
79
80#[derive(Clone)]
81pub enum OpenMongoStep {
82    ConnStr,
83    Database { conn_str: String },
84}
85
86#[derive(Clone)]
87pub enum OpenProxyStep {
88    Url,
89    Token { url: String },
90}
91
92#[derive(Clone)]
93pub enum OpenGitStep {
94    Path,
95    Remote { path: String },
96    Branch { path: String, remote: String },
97}
98
99impl From<TuiAction> for Action {
100    fn from(action: TuiAction) -> Self {
101        Self::Tui(action)
102    }
103}
104
105impl From<EntryEvent> for Action {
106    fn from(event: EntryEvent) -> Self {
107        Self::Dispatch(event.into())
108    }
109}
110
111impl App {
112    pub async fn handle_action(&mut self, action: Action, input: Input) -> bool {
113        match action {
114            Action::Tui(TuiAction::Quit) => {
115                return true;
116            }
117            Action::Tui(TuiAction::Help) => {
118                self.context.help = true;
119            }
120            Action::Tui(TuiAction::OpenThemeMenu) => {
121                self.context.theme_selector =
122                    Some(crate::context::theme_selector::ThemeSelector::new());
123            }
124            Action::Tui(TuiAction::ShowEditorKeymap) => {
125                self.context.editor_keymap = true;
126            }
127            Action::Tui(TuiAction::Alert(message)) => {
128                self.context.alert = Some(message);
129            }
130            Action::Tui(TuiAction::Confirm { message, action }) => {
131                self.context.confirm = Some((message, *action));
132            }
133            Action::Tui(TuiAction::SaveAndConfirm { message, action }) => {
134                self.save().await;
135                self.context.confirm = Some((message, *action));
136            }
137            Action::Tui(TuiAction::OpenNotebookQuitMenu { save_before_open }) => {
138                if save_before_open {
139                    self.save().await;
140                }
141
142                let quit_action = Action::Tui(TuiAction::Quit);
143                let menu_action = Action::Tui(TuiAction::ReturnToEntry);
144                self.context.quit_menu = Some(QuitMenu::new(
145                    "Leave the notebook?",
146                    quit_action,
147                    menu_action,
148                ));
149            }
150            Action::Tui(TuiAction::ReturnToEntry) => {
151                self.context = crate::context::Context::default();
152
153                self.glues.db = None;
154                self.glues.state = EntryState.into();
155            }
156            Action::Tui(TuiAction::Prompt {
157                message,
158                action,
159                default,
160            }) => {
161                self.context.prompt = Some(ContextPrompt::new(message, *action, default));
162            }
163            #[cfg(not(target_arch = "wasm32"))]
164            Action::Tui(TuiAction::OpenGit(OpenGitStep::Path)) => {
165                let path = self
166                    .context
167                    .take_prompt_input()
168                    .log_expect("prompt must not be none");
169                let message = vec![
170                    Line::from(format!("path: {path}").fg(THEME.hint)),
171                    Line::raw(""),
172                    Line::raw("Enter the git remote:"),
173                ];
174
175                config::update(LAST_GIT_PATH, &path).await;
176                let default = config::get(LAST_GIT_REMOTE).await;
177                let action = TuiAction::OpenGit(OpenGitStep::Remote { path }).into();
178                self.context.prompt = Some(ContextPrompt::new(message, action, default));
179            }
180            #[cfg(not(target_arch = "wasm32"))]
181            Action::Tui(TuiAction::OpenGit(OpenGitStep::Remote { path })) => {
182                let remote = self
183                    .context
184                    .take_prompt_input()
185                    .log_expect("prompt must not be none");
186                let message = vec![
187                    Line::from(format!("path: {path}").fg(THEME.hint)),
188                    Line::from(format!("remote: {remote}").fg(THEME.hint)),
189                    Line::raw(""),
190                    Line::raw("Enter the git branch:"),
191                ];
192
193                config::update(LAST_GIT_REMOTE, &remote).await;
194                let default = config::get(LAST_GIT_BRANCH).await;
195                let action = TuiAction::OpenGit(OpenGitStep::Branch { path, remote }).into();
196                self.context.prompt = Some(ContextPrompt::new(message, action, default));
197            }
198            #[cfg(not(target_arch = "wasm32"))]
199            Action::Tui(TuiAction::OpenGit(OpenGitStep::Branch { path, remote })) => {
200                let branch = self
201                    .context
202                    .take_prompt_input()
203                    .log_expect("branch must not be none");
204                let transition = self
205                    .glues
206                    .dispatch(
207                        EntryEvent::OpenGit {
208                            path,
209                            remote,
210                            branch,
211                        }
212                        .into(),
213                    )
214                    .await
215                    .log_unwrap();
216                self.handle_transition(transition).await;
217            }
218            #[cfg(not(target_arch = "wasm32"))]
219            Action::Tui(TuiAction::OpenMongo(OpenMongoStep::ConnStr)) => {
220                let conn_str = self
221                    .context
222                    .take_prompt_input()
223                    .log_expect("conn str must not be none");
224                let message = vec![
225                    Line::from(format!("conn_str: {conn_str}").fg(THEME.hint)),
226                    Line::raw(""),
227                    Line::raw("Enter the database name:"),
228                ];
229
230                config::update(LAST_MONGO_CONN_STR, &conn_str).await;
231                let default = config::get(LAST_MONGO_DB_NAME).await;
232
233                let action = TuiAction::OpenMongo(OpenMongoStep::Database { conn_str }).into();
234                self.context.prompt = Some(ContextPrompt::new(message, action, default));
235            }
236            #[cfg(not(target_arch = "wasm32"))]
237            Action::Tui(TuiAction::OpenMongo(OpenMongoStep::Database { conn_str })) => {
238                let db_name = self
239                    .context
240                    .take_prompt_input()
241                    .log_expect("database name must not be none");
242
243                config::update(LAST_MONGO_DB_NAME, &db_name).await;
244
245                let transition = self
246                    .glues
247                    .dispatch(EntryEvent::OpenMongo { conn_str, db_name }.into())
248                    .await
249                    .log_unwrap();
250                self.handle_transition(transition).await;
251            }
252            Action::Tui(TuiAction::OpenProxy(OpenProxyStep::Url)) => {
253                let url = self
254                    .context
255                    .take_prompt_input()
256                    .log_expect("proxy url must not be none");
257
258                config::update(LAST_PROXY_URL, &url).await;
259
260                let message = vec![
261                    Line::raw("Enter the authentication token (optional):"),
262                    Line::from("Leave empty to connect without a token.".fg(THEME.hint)),
263                    Line::from(
264                        "Servers without authentication accept empty tokens.".fg(THEME.hint),
265                    ),
266                ];
267                let action = TuiAction::OpenProxy(OpenProxyStep::Token { url }).into();
268                self.context.prompt = Some(ContextPrompt::new_masked(message, action, None, '*'));
269            }
270            Action::Tui(TuiAction::OpenProxy(OpenProxyStep::Token { url })) => {
271                let token_input = self
272                    .context
273                    .take_prompt_input()
274                    .log_expect("proxy token must not be none");
275
276                let token = token_input.trim().to_owned();
277                let auth_token = if token.is_empty() { None } else { Some(token) };
278
279                match self
280                    .glues
281                    .dispatch(EntryEvent::OpenProxy { url, auth_token }.into())
282                    .await
283                {
284                    Ok(transition) => {
285                        self.handle_transition(transition).await;
286                    }
287                    Err(err) => {
288                        self.context.alert = Some(err.to_string());
289                    }
290                }
291            }
292            #[cfg(target_arch = "wasm32")]
293            Action::Tui(TuiAction::OpenIndexedDb) => {
294                let namespace_input = self
295                    .context
296                    .take_prompt_input()
297                    .log_expect("IndexedDB namespace must not be none");
298
299                let namespace = {
300                    let trimmed = namespace_input.trim();
301                    if trimmed.is_empty() {
302                        "glues".to_owned()
303                    } else {
304                        trimmed.to_owned()
305                    }
306                };
307
308                config::update(LAST_IDB_NAMESPACE, &namespace).await;
309
310                let transition = self
311                    .glues
312                    .dispatch(EntryEvent::OpenIndexedDb { namespace }.into())
313                    .await
314                    .log_unwrap();
315                self.handle_transition(transition).await;
316            }
317            #[cfg(target_arch = "wasm32")]
318            Action::Tui(TuiAction::OpenFile)
319            | Action::Tui(TuiAction::OpenGit(_))
320            | Action::Tui(TuiAction::OpenMongo(_)) => {}
321            #[cfg(not(target_arch = "wasm32"))]
322            Action::Tui(TuiAction::OpenRedb) => {
323                let path = self
324                    .context
325                    .take_prompt_input()
326                    .log_expect("redb path prompt must not be none");
327                if path.is_empty() {
328                    self.context.alert = Some("The redb path cannot be empty".to_string());
329                    return false;
330                }
331
332                config::update(LAST_REDB_PATH, &path).await;
333
334                let transition = self
335                    .glues
336                    .dispatch(EntryEvent::OpenRedb(path).into())
337                    .await
338                    .log_unwrap();
339                self.handle_transition(transition).await;
340            }
341            #[cfg(not(target_arch = "wasm32"))]
342            Action::Tui(TuiAction::OpenFile) => {
343                let path = self
344                    .context
345                    .take_prompt_input()
346                    .log_expect("prompt must not be none");
347                if path.is_empty() {
348                    self.context.alert = Some("Path cannot be empty".to_string());
349                    return false;
350                }
351
352                config::update(LAST_FILE_PATH, &path).await;
353
354                let transition = self
355                    .glues
356                    .dispatch(EntryEvent::OpenFile(path).into())
357                    .await
358                    .log_unwrap();
359                self.handle_transition(transition).await;
360            }
361            Action::Tui(TuiAction::RenameNote) => {
362                let new_name = self
363                    .context
364                    .take_prompt_input()
365                    .log_expect("prompt must not be none");
366                if new_name.is_empty() {
367                    self.context.alert = Some("Note name cannot be empty".to_string());
368                    return false;
369                }
370
371                let transition = self
372                    .glues
373                    .dispatch(NotebookEvent::RenameNote(new_name).into())
374                    .await
375                    .log_unwrap();
376                self.handle_transition(transition).await;
377            }
378            Action::Tui(TuiAction::RemoveNote) => {
379                let transition = self
380                    .glues
381                    .dispatch(NotebookEvent::RemoveNote.into())
382                    .await
383                    .log_unwrap();
384                self.handle_transition(transition).await;
385            }
386            Action::Tui(TuiAction::AddNote) => {
387                let note_name = self
388                    .context
389                    .take_prompt_input()
390                    .log_expect("prompt must not be none");
391                if note_name.is_empty() {
392                    self.context.alert = Some("Note name cannot be empty".to_string());
393                    return false;
394                }
395
396                let transition = self
397                    .glues
398                    .dispatch(NotebookEvent::AddNote(note_name).into())
399                    .await
400                    .log_unwrap();
401                self.handle_transition(transition).await;
402            }
403            Action::Tui(TuiAction::AddDirectory) => {
404                let directory_name = self
405                    .context
406                    .take_prompt_input()
407                    .log_expect("prompt must not be none");
408                if directory_name.is_empty() {
409                    self.context.alert = Some("Directory name cannot be empty".to_string());
410                    return false;
411                }
412
413                let transition = self
414                    .glues
415                    .dispatch(NotebookEvent::AddDirectory(directory_name).into())
416                    .await
417                    .log_unwrap();
418                self.handle_transition(transition).await;
419            }
420            Action::Tui(TuiAction::RenameDirectory) => {
421                let new_name = self
422                    .context
423                    .take_prompt_input()
424                    .log_expect("prompt must not be none");
425                if new_name.is_empty() {
426                    self.context.alert = Some("Directory name cannot be empty".to_string());
427                    return false;
428                }
429
430                let transition = self
431                    .glues
432                    .dispatch(NotebookEvent::RenameDirectory(new_name).into())
433                    .await
434                    .log_unwrap();
435                self.handle_transition(transition).await;
436            }
437            Action::Tui(TuiAction::RemoveDirectory) => {
438                let transition = self
439                    .glues
440                    .dispatch(NotebookEvent::RemoveDirectory.into())
441                    .await
442                    .log_unwrap();
443                self.handle_transition(transition).await;
444            }
445            Action::Dispatch(event) => {
446                let transition = self.glues.dispatch(event).await.log_unwrap();
447                self.handle_transition(transition).await;
448            }
449
450            Action::Tui(TuiAction::SaveAndPassThrough) => {
451                self.save().await;
452
453                let event = match to_event(input) {
454                    Some(event) => event.into(),
455                    None => {
456                        return false;
457                    }
458                };
459
460                let transition = self.glues.dispatch(event).await.log_unwrap();
461                self.handle_transition(transition).await;
462            }
463            Action::PassThrough => {
464                let event = match to_event(input) {
465                    Some(event) => event.into(),
466                    None => {
467                        return false;
468                    }
469                };
470
471                let transition = self.glues.dispatch(event).await.log_unwrap();
472                self.handle_transition(transition).await;
473            }
474            Action::None => {}
475        };
476
477        false
478    }
479}
480
481fn to_event(input: Input) -> Option<KeyEvent> {
482    let key = match input {
483        Input::Key(key) => key,
484        _ => return None,
485    };
486    let code = key.code;
487    let ctrl = key.modifiers.ctrl;
488
489    let event = match code {
490        KeyCode::Char('h') if ctrl => KeyEvent::CtrlH,
491        KeyCode::Char('r') if ctrl => KeyEvent::CtrlR,
492        KeyCode::Char('a') => KeyEvent::A,
493        KeyCode::Char('b') => KeyEvent::B,
494        KeyCode::Char('c') => KeyEvent::C,
495        KeyCode::Char('d') => KeyEvent::D,
496        KeyCode::Char('e') => KeyEvent::E,
497        KeyCode::Char('g') => KeyEvent::G,
498        KeyCode::Char('h') => KeyEvent::H,
499        KeyCode::Char('i') => KeyEvent::I,
500        KeyCode::Char('j') => KeyEvent::J,
501        KeyCode::Char('k') => KeyEvent::K,
502        KeyCode::Char('l') => KeyEvent::L,
503        KeyCode::Char('m') => KeyEvent::M,
504        KeyCode::Char('n') => KeyEvent::N,
505        KeyCode::Char('o') => KeyEvent::O,
506        KeyCode::Char('p') => KeyEvent::P,
507        KeyCode::Char('s') => KeyEvent::S,
508        KeyCode::Char('t') => KeyEvent::T,
509        KeyCode::Char('u') => KeyEvent::U,
510        KeyCode::Char('v') => KeyEvent::V,
511        KeyCode::Char('w') => KeyEvent::W,
512        KeyCode::Char('x') => KeyEvent::X,
513        KeyCode::Char('y') => KeyEvent::Y,
514        KeyCode::Char('z') => KeyEvent::Z,
515        KeyCode::Char('A') => KeyEvent::CapA,
516        KeyCode::Char('G') => KeyEvent::CapG,
517        KeyCode::Char('H') => KeyEvent::CapH,
518        KeyCode::Char('I') => KeyEvent::CapI,
519        KeyCode::Char('J') => KeyEvent::CapJ,
520        KeyCode::Char('K') => KeyEvent::CapK,
521        KeyCode::Char('L') => KeyEvent::CapL,
522        KeyCode::Char('O') => KeyEvent::CapO,
523        KeyCode::Char('S') => KeyEvent::CapS,
524        KeyCode::Char('U') => KeyEvent::CapU,
525        KeyCode::Char('X') => KeyEvent::CapX,
526        KeyCode::Char('1') => NumKey::One.into(),
527        KeyCode::Char('2') => NumKey::Two.into(),
528        KeyCode::Char('3') => NumKey::Three.into(),
529        KeyCode::Char('4') => NumKey::Four.into(),
530        KeyCode::Char('5') => NumKey::Five.into(),
531        KeyCode::Char('6') => NumKey::Six.into(),
532        KeyCode::Char('7') => NumKey::Seven.into(),
533        KeyCode::Char('8') => NumKey::Eight.into(),
534        KeyCode::Char('9') => NumKey::Nine.into(),
535        KeyCode::Char('0') => NumKey::Zero.into(),
536        KeyCode::Char('$') => KeyEvent::DollarSign,
537        KeyCode::Char('^') => KeyEvent::Caret,
538        KeyCode::Char('~') => KeyEvent::Tilde,
539        KeyCode::Char('?') => KeyEvent::QuestionMark,
540        KeyCode::Char('<') => KeyEvent::AngleBracketOpen,
541        KeyCode::Char('>') => KeyEvent::AngleBracketClose,
542        KeyCode::Char('.') => KeyEvent::Dot,
543        KeyCode::Char('-') => KeyEvent::Dash,
544        KeyCode::Char(' ') => KeyEvent::Space,
545        KeyCode::Left => KeyEvent::Left,
546        KeyCode::Right => KeyEvent::Right,
547        KeyCode::Up => KeyEvent::Up,
548        KeyCode::Down => KeyEvent::Down,
549        KeyCode::Enter => KeyEvent::Enter,
550        KeyCode::Tab => KeyEvent::Tab,
551        KeyCode::Esc => KeyEvent::Esc,
552        _ => return None,
553    };
554
555    Some(event)
556}