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 "note_editor_experimental_set_edit_mode" => {
169 Some(Command::NoteEditorExperimentalSetEditView)
170 }
171 "note_editor_experimental_set_read_mode" => {
173 Some(Command::NoteEditorExperimentalSetReadView)
174 }
175 "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 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 .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 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 _ = process::Command::new(command)
401 .arg(args.join(" "))
402 .spawn()
403 .ok();
404 None
405 },
406 )
407}