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 "note_editor_experimental_set_edit_mode" => {
207 Some(Command::NoteEditorExperimentalSetEditView)
208 }
209 "note_editor_experimental_set_read_mode" => {
211 Some(Command::NoteEditorExperimentalSetReadView)
212 }
213 "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 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 .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 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 _ = process::Command::new(command)
466 .arg(args.join(" "))
467 .spawn()
468 .ok();
469 None
470 },
471 )
472}