Skip to main content

basalt_tui/
command.rs

1use ratatui::{
2    crossterm::{
3        terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
4        ExecutableCommand,
5    },
6    DefaultTerminal,
7};
8use serde::{Deserialize, Deserializer};
9use std::{io::stdout, process};
10
11use crate::{
12    app::{Message, ScrollAmount},
13    explorer, help_modal, input, note_editor, outline, splash_modal, vault_selector_modal,
14};
15
16trait ReplaceVar {
17    fn replace_var(&self, variable: &str, content: &str) -> Self;
18}
19
20impl ReplaceVar for String {
21    fn replace_var(&self, variable: &str, content: &str) -> Self {
22        self.replace(variable, content)
23    }
24}
25
26#[derive(Clone, Debug, PartialEq)]
27pub(crate) enum Command {
28    Quit,
29
30    SplashUp,
31    SplashDown,
32    SplashOpen,
33
34    ExplorerUp,
35    ExplorerDown,
36    ExplorerOpen,
37    ExplorerSort,
38    ExplorerToggle,
39    ExplorerNewUntitledNote,
40    ExplorerNewUntitledFolder,
41    ExplorerToggleInputRename,
42    ExplorerToggleOutline,
43    ExplorerSwitchPaneNext,
44    ExplorerSwitchPanePrevious,
45    ExplorerHidePane,
46    ExplorerExpandPane,
47    ExplorerScrollUpOne,
48    ExplorerScrollDownOne,
49    ExplorerScrollUpHalfPage,
50    ExplorerScrollDownHalfPage,
51
52    OutlineUp,
53    OutlineDown,
54    OutlineSelect,
55    OutlineExpand,
56    OutlineToggle,
57    OutlineToggleExplorer,
58    OutlineSwitchPaneNext,
59    OutlineSwitchPanePrevious,
60
61    HelpModalScrollUpOne,
62    HelpModalScrollDownOne,
63    HelpModalScrollUpHalfPage,
64    HelpModalScrollDownHalfPage,
65    HelpModalToggle,
66    HelpModalClose,
67
68    NoteEditorScrollUpOne,
69    NoteEditorScrollDownOne,
70    NoteEditorScrollUpHalfPage,
71    NoteEditorScrollDownHalfPage,
72    NoteEditorSwitchPaneNext,
73    NoteEditorSwitchPanePrevious,
74    NoteEditorToggleExplorer,
75    NoteEditorToggleOutline,
76    NoteEditorCursorUp,
77    NoteEditorCursorDown,
78    NoteEditorScrollToTop,
79    NoteEditorScrollToBottom,
80
81    ExplorerScrollToTop,
82    ExplorerScrollToBottom,
83
84    NoteEditorExperimentalCursorWordForward,
85    NoteEditorExperimentalCursorWordBackward,
86    NoteEditorExperimentalToggleView,
87    NoteEditorExperimentalSetEditView,
88    NoteEditorExperimentalSetReadView,
89    NoteEditorExperimentalSave,
90    NoteEditorExperimentalExit,
91    NoteEditorExperimentalCursorLeft,
92    NoteEditorExperimentalCursorRight,
93    NoteEditorInsertMode,
94
95    VaultSelectorModalUp,
96    VaultSelectorModalDown,
97    VaultSelectorModalClose,
98    VaultSelectorModalOpen,
99    VaultSelectorModalToggle,
100
101    InputModalWordForward,
102    InputModalWordBackward,
103    InputModalLeft,
104    InputModalRight,
105    InputModalCancel,
106    InputModalAccept,
107    InputModalEditMode,
108
109    Exec(String),
110    Spawn(String),
111}
112
113fn str_to_command(s: &str) -> Option<Command> {
114    match s {
115        "quit" => Some(Command::Quit),
116
117        "splash_up" => Some(Command::SplashUp),
118        "splash_down" => Some(Command::SplashDown),
119        "splash_open" => Some(Command::SplashOpen),
120
121        "explorer_up" => Some(Command::ExplorerUp),
122        "explorer_down" => Some(Command::ExplorerDown),
123        "explorer_open" => Some(Command::ExplorerOpen),
124        "explorer_sort" => Some(Command::ExplorerSort),
125        "explorer_toggle" => Some(Command::ExplorerToggle),
126        "explorer_new_untitled_note" => Some(Command::ExplorerNewUntitledNote),
127        "explorer_new_untitled_folder" => Some(Command::ExplorerNewUntitledFolder),
128        "explorer_toggle_outline" => Some(Command::ExplorerToggleOutline),
129        "explorer_toggle_input_rename" => Some(Command::ExplorerToggleInputRename),
130        "explorer_switch_pane_next" => Some(Command::ExplorerSwitchPaneNext),
131        "explorer_hide_pane" => Some(Command::ExplorerHidePane),
132        "explorer_expand_pane" => Some(Command::ExplorerExpandPane),
133        "explorer_switch_pane_previous" => Some(Command::ExplorerSwitchPanePrevious),
134        "explorer_scroll_up_one" => Some(Command::ExplorerScrollUpOne),
135        "explorer_scroll_down_one" => Some(Command::ExplorerScrollDownOne),
136        "explorer_scroll_up_half_page" => Some(Command::ExplorerScrollUpHalfPage),
137        "explorer_scroll_down_half_page" => Some(Command::ExplorerScrollDownHalfPage),
138
139        "input_modal_word_forward" => Some(Command::InputModalWordForward),
140        "input_modal_word_backward" => Some(Command::InputModalWordBackward),
141        "input_modal_left" => Some(Command::InputModalLeft),
142        "input_modal_right" => Some(Command::InputModalRight),
143        "input_modal_cancel" => Some(Command::InputModalCancel),
144        "input_modal_accept" => Some(Command::InputModalAccept),
145        "input_modal_edit_mode" => Some(Command::InputModalEditMode),
146
147        "outline_up" => Some(Command::OutlineUp),
148        "outline_down" => Some(Command::OutlineDown),
149        "outline_select" => Some(Command::OutlineSelect),
150        "outline_expand" => Some(Command::OutlineExpand),
151        "outline_toggle" => Some(Command::OutlineToggle),
152        "outline_toggle_explorer" => Some(Command::OutlineToggleExplorer),
153        "outline_switch_pane_next" => Some(Command::OutlineSwitchPaneNext),
154        "outline_switch_pane_previous" => Some(Command::OutlineSwitchPanePrevious),
155
156        "help_modal_scroll_up_one" => Some(Command::HelpModalScrollUpOne),
157        "help_modal_scroll_down_one" => Some(Command::HelpModalScrollDownOne),
158        "help_modal_scroll_up_half_page" => Some(Command::HelpModalScrollUpHalfPage),
159        "help_modal_scroll_down_half_page" => Some(Command::HelpModalScrollDownHalfPage),
160        "help_modal_toggle" => Some(Command::HelpModalToggle),
161        "help_modal_close" => Some(Command::HelpModalClose),
162
163        "note_editor_scroll_up_one" => Some(Command::NoteEditorScrollUpOne),
164        "note_editor_scroll_down_one" => Some(Command::NoteEditorScrollDownOne),
165        "note_editor_scroll_up_half_page" => Some(Command::NoteEditorScrollUpHalfPage),
166        "note_editor_scroll_down_half_page" => Some(Command::NoteEditorScrollDownHalfPage),
167        "note_editor_switch_pane_next" => Some(Command::NoteEditorSwitchPaneNext),
168        "note_editor_switch_pane_previous" => Some(Command::NoteEditorSwitchPanePrevious),
169        "note_editor_toggle_explorer" => Some(Command::NoteEditorToggleExplorer),
170        "note_editor_toggle_outline" => Some(Command::NoteEditorToggleOutline),
171        "note_editor_cursor_up" => Some(Command::NoteEditorCursorUp),
172        "note_editor_cursor_down" => Some(Command::NoteEditorCursorDown),
173        "note_editor_scroll_to_top" => Some(Command::NoteEditorScrollToTop),
174        "note_editor_scroll_to_bottom" => Some(Command::NoteEditorScrollToBottom),
175
176        "explorer_scroll_to_top" => Some(Command::ExplorerScrollToTop),
177        "explorer_scroll_to_bottom" => Some(Command::ExplorerScrollToBottom),
178
179        "note_editor_experimental_cursor_word_forward" => {
180            Some(Command::NoteEditorExperimentalCursorWordForward)
181        }
182        "note_editor_experimental_cursor_word_backward" => {
183            Some(Command::NoteEditorExperimentalCursorWordBackward)
184        }
185        "note_editor_experimental_set_edit_view" => {
186            Some(Command::NoteEditorExperimentalSetEditView)
187        }
188        "note_editor_experimental_toggle_view" => Some(Command::NoteEditorExperimentalToggleView),
189        "note_editor_experimental_set_read_view" => {
190            Some(Command::NoteEditorExperimentalSetReadView)
191        }
192        "note_editor_experimental_save" => Some(Command::NoteEditorExperimentalSave),
193        "note_editor_experimental_exit" => Some(Command::NoteEditorExperimentalExit),
194        "note_editor_experimental_cursor_left" => Some(Command::NoteEditorExperimentalCursorLeft),
195        "note_editor_experimental_cursor_right" => Some(Command::NoteEditorExperimentalCursorRight),
196        "note_editor_insert_mode" => Some(Command::NoteEditorInsertMode),
197
198        "vault_selector_modal_up" => Some(Command::VaultSelectorModalUp),
199        "vault_selector_modal_down" => Some(Command::VaultSelectorModalDown),
200        "vault_selector_modal_close" => Some(Command::VaultSelectorModalClose),
201        "vault_selector_modal_open" => Some(Command::VaultSelectorModalOpen),
202        "vault_selector_modal_toggle" => Some(Command::VaultSelectorModalToggle),
203
204        // TODO: Remove deprecations in the next major version
205        // Deprecated
206        "note_editor_experimental_set_edit_mode" => {
207            Some(Command::NoteEditorExperimentalSetEditView)
208        }
209        // Deprecated
210        "note_editor_experimental_set_read_mode" => {
211            Some(Command::NoteEditorExperimentalSetReadView)
212        }
213        // Deprecated
214        "note_editor_experimental_exit_mode" => Some(Command::NoteEditorExperimentalExit),
215        _ => None,
216    }
217}
218
219impl<'de> Deserialize<'de> for Command {
220    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
221    where
222        D: Deserializer<'de>,
223    {
224        let s = String::deserialize(deserializer)?;
225
226        if let Some(command) = s
227            .strip_prefix("exec:")
228            .map(|command| Command::Exec(command.to_string()))
229            .or(s
230                .strip_prefix("spawn:")
231                .map(|command| Command::Spawn(command.to_string())))
232        {
233            return Ok(command);
234        }
235
236        str_to_command(&s).ok_or(serde::de::Error::custom(format!(
237            "{s} is not a valid command"
238        )))
239    }
240}
241
242impl From<Command> for Message<'_> {
243    fn from(value: Command) -> Self {
244        match value {
245            Command::Quit => Message::Quit,
246
247            Command::SplashUp => Message::Splash(splash_modal::Message::Up),
248            Command::SplashDown => Message::Splash(splash_modal::Message::Down),
249            Command::SplashOpen => Message::Splash(splash_modal::Message::Open),
250
251            Command::ExplorerUp => Message::Explorer(explorer::Message::Up),
252            Command::ExplorerDown => Message::Explorer(explorer::Message::Down),
253            Command::ExplorerOpen => Message::Explorer(explorer::Message::Open),
254            Command::ExplorerSort => Message::Explorer(explorer::Message::Sort),
255            Command::ExplorerToggle => Message::Explorer(explorer::Message::Toggle),
256            Command::ExplorerToggleOutline => Message::Explorer(explorer::Message::ToggleOutline),
257            Command::ExplorerToggleInputRename => {
258                Message::Explorer(explorer::Message::ToggleInputRename)
259            }
260            Command::ExplorerHidePane => Message::Explorer(explorer::Message::HidePane),
261            Command::ExplorerExpandPane => Message::Explorer(explorer::Message::ExpandPane),
262            Command::ExplorerSwitchPaneNext => Message::Explorer(explorer::Message::SwitchPaneNext),
263            Command::ExplorerSwitchPanePrevious => {
264                Message::Explorer(explorer::Message::SwitchPanePrevious)
265            }
266            Command::ExplorerScrollUpOne => {
267                Message::Explorer(explorer::Message::ScrollUp(ScrollAmount::One))
268            }
269            Command::ExplorerScrollDownOne => {
270                Message::Explorer(explorer::Message::ScrollDown(ScrollAmount::One))
271            }
272            Command::ExplorerScrollUpHalfPage => {
273                Message::Explorer(explorer::Message::ScrollUp(ScrollAmount::HalfPage))
274            }
275            Command::ExplorerScrollDownHalfPage => {
276                Message::Explorer(explorer::Message::ScrollDown(ScrollAmount::HalfPage))
277            }
278            Command::ExplorerNewUntitledNote => Message::CreateUntitledNote,
279            Command::ExplorerNewUntitledFolder => Message::CreateUntitledFolder,
280
281            Command::InputModalEditMode => Message::Input(input::Message::EditMode),
282            Command::InputModalAccept => Message::Input(input::Message::Accept),
283            Command::InputModalCancel => Message::Input(input::Message::Cancel),
284            Command::InputModalLeft => Message::Input(input::Message::CursorLeft),
285            Command::InputModalRight => Message::Input(input::Message::CursorRight),
286            Command::InputModalWordForward => Message::Input(input::Message::CursorWordForward),
287            Command::InputModalWordBackward => Message::Input(input::Message::CursorWordBackward),
288
289            Command::OutlineUp => Message::Outline(outline::Message::Up),
290            Command::OutlineDown => Message::Outline(outline::Message::Down),
291            Command::OutlineSelect => Message::Outline(outline::Message::Select),
292            Command::OutlineExpand => Message::Outline(outline::Message::Expand),
293            Command::OutlineToggle => Message::Outline(outline::Message::Toggle),
294            Command::OutlineToggleExplorer => Message::Outline(outline::Message::ToggleExplorer),
295            Command::OutlineSwitchPaneNext => Message::Outline(outline::Message::SwitchPaneNext),
296            Command::OutlineSwitchPanePrevious => {
297                Message::Outline(outline::Message::SwitchPanePrevious)
298            }
299
300            Command::HelpModalScrollUpOne => {
301                Message::HelpModal(help_modal::Message::ScrollUp(ScrollAmount::One))
302            }
303            Command::HelpModalScrollDownOne => {
304                Message::HelpModal(help_modal::Message::ScrollDown(ScrollAmount::One))
305            }
306            Command::HelpModalScrollUpHalfPage => {
307                Message::HelpModal(help_modal::Message::ScrollUp(ScrollAmount::HalfPage))
308            }
309            Command::HelpModalScrollDownHalfPage => {
310                Message::HelpModal(help_modal::Message::ScrollDown(ScrollAmount::HalfPage))
311            }
312            Command::HelpModalToggle => Message::HelpModal(help_modal::Message::Toggle),
313            Command::HelpModalClose => Message::HelpModal(help_modal::Message::Close),
314
315            Command::NoteEditorScrollUpOne => {
316                Message::NoteEditor(note_editor::Message::ScrollUp(ScrollAmount::One))
317            }
318            Command::NoteEditorScrollDownOne => {
319                Message::NoteEditor(note_editor::Message::ScrollDown(ScrollAmount::One))
320            }
321            Command::NoteEditorScrollUpHalfPage => {
322                Message::NoteEditor(note_editor::Message::ScrollUp(ScrollAmount::HalfPage))
323            }
324            Command::NoteEditorScrollDownHalfPage => {
325                Message::NoteEditor(note_editor::Message::ScrollDown(ScrollAmount::HalfPage))
326            }
327            Command::NoteEditorSwitchPaneNext => {
328                Message::NoteEditor(note_editor::Message::SwitchPaneNext)
329            }
330            Command::NoteEditorSwitchPanePrevious => {
331                Message::NoteEditor(note_editor::Message::SwitchPanePrevious)
332            }
333            Command::NoteEditorCursorUp => Message::NoteEditor(note_editor::Message::CursorUp),
334            Command::NoteEditorCursorDown => Message::NoteEditor(note_editor::Message::CursorDown),
335            Command::NoteEditorScrollToTop => {
336                Message::NoteEditor(note_editor::Message::ScrollToTop)
337            }
338            Command::NoteEditorScrollToBottom => {
339                Message::NoteEditor(note_editor::Message::ScrollToBottom)
340            }
341            Command::ExplorerScrollToTop => Message::Explorer(explorer::Message::ScrollToTop),
342            Command::ExplorerScrollToBottom => Message::Explorer(explorer::Message::ScrollToBottom),
343            Command::NoteEditorToggleExplorer => {
344                Message::NoteEditor(note_editor::Message::ToggleExplorer)
345            }
346            Command::NoteEditorToggleOutline => {
347                Message::NoteEditor(note_editor::Message::ToggleOutline)
348            }
349
350            // Experimental
351            Command::NoteEditorExperimentalToggleView => {
352                Message::NoteEditor(note_editor::Message::ToggleView)
353            }
354            Command::NoteEditorExperimentalSetEditView => {
355                Message::NoteEditor(note_editor::Message::EditView)
356            }
357            Command::NoteEditorExperimentalSetReadView => {
358                Message::NoteEditor(note_editor::Message::ReadView)
359            }
360            Command::NoteEditorExperimentalSave => Message::NoteEditor(note_editor::Message::Save),
361            Command::NoteEditorExperimentalExit => Message::NoteEditor(note_editor::Message::Exit),
362            Command::NoteEditorExperimentalCursorWordForward => {
363                Message::NoteEditor(note_editor::Message::CursorWordForward)
364            }
365            Command::NoteEditorExperimentalCursorWordBackward => {
366                Message::NoteEditor(note_editor::Message::CursorWordBackward)
367            }
368            Command::NoteEditorExperimentalCursorLeft => {
369                Message::NoteEditor(note_editor::Message::CursorLeft)
370            }
371            Command::NoteEditorExperimentalCursorRight => {
372                Message::NoteEditor(note_editor::Message::CursorRight)
373            }
374            Command::NoteEditorInsertMode => Message::NoteEditor(note_editor::Message::InsertMode),
375
376            Command::VaultSelectorModalClose => {
377                Message::VaultSelectorModal(vault_selector_modal::Message::Close)
378            }
379            Command::VaultSelectorModalToggle => {
380                Message::VaultSelectorModal(vault_selector_modal::Message::Toggle)
381            }
382            Command::VaultSelectorModalUp => {
383                Message::VaultSelectorModal(vault_selector_modal::Message::Up)
384            }
385            Command::VaultSelectorModalDown => {
386                Message::VaultSelectorModal(vault_selector_modal::Message::Down)
387            }
388            Command::VaultSelectorModalOpen => {
389                Message::VaultSelectorModal(vault_selector_modal::Message::Select)
390            }
391
392            Command::Exec(command) => Message::Exec(command),
393            Command::Spawn(command) => Message::Spawn(command),
394        }
395    }
396}
397
398pub fn run_command<'a>(
399    command: String,
400    vault_name: &str,
401    note_name: &str,
402    note_path: &str,
403    mut callback: impl FnMut(&str, &[&str]) -> Option<Message<'a>>,
404) -> Option<Message<'a>> {
405    let expanded = command
406        .replace_var("%vault", vault_name)
407        // Order matters, otherwise all mentions of %note_path would be replaced with %note value
408        .replace_var("%note_path", note_path)
409        .replace_var("%note", note_name);
410
411    let args = expanded.split_whitespace().collect::<Vec<_>>();
412
413    match args.as_slice() {
414        [command, args @ ..] => callback(command, args),
415        [] => None,
416    }
417}
418
419pub fn sync_command<'a>(
420    terminal: &mut DefaultTerminal,
421    command: String,
422    vault_name: &str,
423    note_name: &str,
424    note_path: &str,
425) -> Option<Message<'a>> {
426    fn enter_alternate_screen(terminal: &mut DefaultTerminal) -> Result<(), std::io::Error> {
427        disable_raw_mode()?;
428        stdout().execute(LeaveAlternateScreen)?;
429        stdout().execute(EnterAlternateScreen)?;
430        enable_raw_mode()?;
431        terminal.clear()
432    }
433
434    run_command(
435        command,
436        vault_name,
437        note_name,
438        note_path,
439        |command, args| {
440            // TODO:Error handling
441            process::Command::new(command)
442                .arg(args.join(" "))
443                .status()
444                .ok()?;
445            enter_alternate_screen(terminal)
446                .map(|_| Message::Explorer(explorer::Message::Open))
447                .ok()
448        },
449    )
450}
451
452pub fn spawn_command<'a>(
453    command: String,
454    vault_name: &str,
455    note_name: &str,
456    note_path: &str,
457) -> Option<Message<'a>> {
458    run_command(
459        command,
460        vault_name,
461        note_name,
462        note_path,
463        |command, args| {
464            // TODO:Error handling
465            _ = process::Command::new(command)
466                .arg(args.join(" "))
467                .spawn()
468                .ok();
469            None
470        },
471    )
472}