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