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 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}