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