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 "note_editor_experimental_set_edit_mode" => {
173 Some(Command::NoteEditorExperimentalSetEditView)
174 }
175 "note_editor_experimental_set_read_mode" => {
177 Some(Command::NoteEditorExperimentalSetReadView)
178 }
179 "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 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 .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 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 _ = process::Command::new(command)
407 .arg(args.join(" "))
408 .spawn()
409 .ok();
410 None
411 },
412 )
413}