basalt-tui 0.12.6

Basalt TUI application for Obsidian notes.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
use ratatui::{
    crossterm::{
        terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
        ExecutableCommand,
    },
    DefaultTerminal,
};
use serde::{Deserialize, Deserializer};
use std::{io::stdout, process};
use tracing::error;

use crate::{
    app::{Message, ScrollAmount},
    debug_log, explorer, help_modal, input, note_editor, outline, splash_modal,
    vault_selector_modal,
};

trait ReplaceVar {
    fn replace_var(&self, variable: &str, content: &str) -> Self;
}

impl ReplaceVar for String {
    fn replace_var(&self, variable: &str, content: &str) -> Self {
        self.replace(variable, content)
    }
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Command {
    Quit,

    SplashUp,
    SplashDown,
    SplashOpen,

    ExplorerUp,
    ExplorerDown,
    ExplorerOpen,
    ExplorerSort,
    ExplorerToggle,
    ExplorerNewUntitledNote,
    ExplorerNewUntitledFolder,
    ExplorerToggleInputRename,
    ExplorerToggleOutline,
    ExplorerSwitchPaneNext,
    ExplorerSwitchPanePrevious,
    ExplorerHidePane,
    ExplorerExpandPane,
    ExplorerScrollUpOne,
    ExplorerScrollDownOne,
    ExplorerScrollUpHalfPage,
    ExplorerScrollDownHalfPage,

    OutlineUp,
    OutlineDown,
    OutlineSelect,
    OutlineExpand,
    OutlineToggle,
    OutlineToggleExplorer,
    OutlineSwitchPaneNext,
    OutlineSwitchPanePrevious,

    HelpModalScrollUpOne,
    HelpModalScrollDownOne,
    HelpModalScrollUpHalfPage,
    HelpModalScrollDownHalfPage,
    HelpModalToggle,
    HelpModalClose,

    NoteEditorScrollUpOne,
    NoteEditorScrollDownOne,
    NoteEditorScrollUpHalfPage,
    NoteEditorScrollDownHalfPage,
    NoteEditorSwitchPaneNext,
    NoteEditorSwitchPanePrevious,
    NoteEditorToggleExplorer,
    NoteEditorToggleOutline,
    NoteEditorCursorUp,
    NoteEditorCursorDown,
    NoteEditorScrollToTop,
    NoteEditorScrollToBottom,

    ExplorerScrollToTop,
    ExplorerScrollToBottom,

    NoteEditorExperimentalCursorWordForward,
    NoteEditorExperimentalCursorWordBackward,
    NoteEditorExperimentalToggleView,
    NoteEditorExperimentalSetEditView,
    NoteEditorExperimentalSetReadView,
    NoteEditorExperimentalSave,
    NoteEditorExperimentalExit,
    NoteEditorExperimentalCursorLeft,
    NoteEditorExperimentalCursorRight,
    NoteEditorInsertMode,

    VaultSelectorModalUp,
    VaultSelectorModalDown,
    VaultSelectorModalClose,
    VaultSelectorModalOpen,
    VaultSelectorModalToggle,

    DebugLogToggle,
    DebugLogClose,
    DebugLogClear,
    DebugLogCycleLevel,
    DebugLogScrollUpOne,
    DebugLogScrollDownOne,
    DebugLogScrollUpHalfPage,
    DebugLogScrollDownHalfPage,

    InputModalWordForward,
    InputModalWordBackward,
    InputModalLeft,
    InputModalRight,
    InputModalCancel,
    InputModalAccept,
    InputModalEditMode,

    Exec(String),
    Spawn(String),
}

fn str_to_command(s: &str) -> Option<Command> {
    match s {
        "quit" => Some(Command::Quit),

        "splash_up" => Some(Command::SplashUp),
        "splash_down" => Some(Command::SplashDown),
        "splash_open" => Some(Command::SplashOpen),

        "explorer_up" => Some(Command::ExplorerUp),
        "explorer_down" => Some(Command::ExplorerDown),
        "explorer_open" => Some(Command::ExplorerOpen),
        "explorer_sort" => Some(Command::ExplorerSort),
        "explorer_toggle" => Some(Command::ExplorerToggle),
        "explorer_new_untitled_note" => Some(Command::ExplorerNewUntitledNote),
        "explorer_new_untitled_folder" => Some(Command::ExplorerNewUntitledFolder),
        "explorer_toggle_outline" => Some(Command::ExplorerToggleOutline),
        "explorer_toggle_input_rename" => Some(Command::ExplorerToggleInputRename),
        "explorer_switch_pane_next" => Some(Command::ExplorerSwitchPaneNext),
        "explorer_hide_pane" => Some(Command::ExplorerHidePane),
        "explorer_expand_pane" => Some(Command::ExplorerExpandPane),
        "explorer_switch_pane_previous" => Some(Command::ExplorerSwitchPanePrevious),
        "explorer_scroll_up_one" => Some(Command::ExplorerScrollUpOne),
        "explorer_scroll_down_one" => Some(Command::ExplorerScrollDownOne),
        "explorer_scroll_up_half_page" => Some(Command::ExplorerScrollUpHalfPage),
        "explorer_scroll_down_half_page" => Some(Command::ExplorerScrollDownHalfPage),

        "input_modal_word_forward" => Some(Command::InputModalWordForward),
        "input_modal_word_backward" => Some(Command::InputModalWordBackward),
        "input_modal_left" => Some(Command::InputModalLeft),
        "input_modal_right" => Some(Command::InputModalRight),
        "input_modal_cancel" => Some(Command::InputModalCancel),
        "input_modal_accept" => Some(Command::InputModalAccept),
        "input_modal_edit_mode" => Some(Command::InputModalEditMode),

        "outline_up" => Some(Command::OutlineUp),
        "outline_down" => Some(Command::OutlineDown),
        "outline_select" => Some(Command::OutlineSelect),
        "outline_expand" => Some(Command::OutlineExpand),
        "outline_toggle" => Some(Command::OutlineToggle),
        "outline_toggle_explorer" => Some(Command::OutlineToggleExplorer),
        "outline_switch_pane_next" => Some(Command::OutlineSwitchPaneNext),
        "outline_switch_pane_previous" => Some(Command::OutlineSwitchPanePrevious),

        "help_modal_scroll_up_one" => Some(Command::HelpModalScrollUpOne),
        "help_modal_scroll_down_one" => Some(Command::HelpModalScrollDownOne),
        "help_modal_scroll_up_half_page" => Some(Command::HelpModalScrollUpHalfPage),
        "help_modal_scroll_down_half_page" => Some(Command::HelpModalScrollDownHalfPage),
        "help_modal_toggle" => Some(Command::HelpModalToggle),
        "help_modal_close" => Some(Command::HelpModalClose),

        "note_editor_scroll_up_one" => Some(Command::NoteEditorScrollUpOne),
        "note_editor_scroll_down_one" => Some(Command::NoteEditorScrollDownOne),
        "note_editor_scroll_up_half_page" => Some(Command::NoteEditorScrollUpHalfPage),
        "note_editor_scroll_down_half_page" => Some(Command::NoteEditorScrollDownHalfPage),
        "note_editor_switch_pane_next" => Some(Command::NoteEditorSwitchPaneNext),
        "note_editor_switch_pane_previous" => Some(Command::NoteEditorSwitchPanePrevious),
        "note_editor_toggle_explorer" => Some(Command::NoteEditorToggleExplorer),
        "note_editor_toggle_outline" => Some(Command::NoteEditorToggleOutline),
        "note_editor_cursor_up" => Some(Command::NoteEditorCursorUp),
        "note_editor_cursor_down" => Some(Command::NoteEditorCursorDown),
        "note_editor_scroll_to_top" => Some(Command::NoteEditorScrollToTop),
        "note_editor_scroll_to_bottom" => Some(Command::NoteEditorScrollToBottom),

        "explorer_scroll_to_top" => Some(Command::ExplorerScrollToTop),
        "explorer_scroll_to_bottom" => Some(Command::ExplorerScrollToBottom),

        "note_editor_experimental_cursor_word_forward" => {
            Some(Command::NoteEditorExperimentalCursorWordForward)
        }
        "note_editor_experimental_cursor_word_backward" => {
            Some(Command::NoteEditorExperimentalCursorWordBackward)
        }
        "note_editor_experimental_set_edit_view" => {
            Some(Command::NoteEditorExperimentalSetEditView)
        }
        "note_editor_experimental_toggle_view" => Some(Command::NoteEditorExperimentalToggleView),
        "note_editor_experimental_set_read_view" => {
            Some(Command::NoteEditorExperimentalSetReadView)
        }
        "note_editor_experimental_save" => Some(Command::NoteEditorExperimentalSave),
        "note_editor_experimental_exit" => Some(Command::NoteEditorExperimentalExit),
        "note_editor_experimental_cursor_left" => Some(Command::NoteEditorExperimentalCursorLeft),
        "note_editor_experimental_cursor_right" => Some(Command::NoteEditorExperimentalCursorRight),
        "note_editor_insert_mode" => Some(Command::NoteEditorInsertMode),

        "vault_selector_modal_up" => Some(Command::VaultSelectorModalUp),
        "vault_selector_modal_down" => Some(Command::VaultSelectorModalDown),
        "vault_selector_modal_close" => Some(Command::VaultSelectorModalClose),
        "vault_selector_modal_open" => Some(Command::VaultSelectorModalOpen),
        "vault_selector_modal_toggle" => Some(Command::VaultSelectorModalToggle),

        "debug_log_toggle" => Some(Command::DebugLogToggle),
        "debug_log_close" => Some(Command::DebugLogClose),
        "debug_log_clear" => Some(Command::DebugLogClear),
        "debug_log_cycle_level" => Some(Command::DebugLogCycleLevel),
        "debug_log_scroll_up_one" => Some(Command::DebugLogScrollUpOne),
        "debug_log_scroll_down_one" => Some(Command::DebugLogScrollDownOne),
        "debug_log_scroll_up_half_page" => Some(Command::DebugLogScrollUpHalfPage),
        "debug_log_scroll_down_half_page" => Some(Command::DebugLogScrollDownHalfPage),

        // TODO: Remove deprecations in the next major version
        // Deprecated
        "note_editor_experimental_set_edit_mode" => {
            Some(Command::NoteEditorExperimentalSetEditView)
        }
        // Deprecated
        "note_editor_experimental_set_read_mode" => {
            Some(Command::NoteEditorExperimentalSetReadView)
        }
        // Deprecated
        "note_editor_experimental_exit_mode" => Some(Command::NoteEditorExperimentalExit),
        _ => None,
    }
}

impl<'de> Deserialize<'de> for Command {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;

        if let Some(command) = s
            .strip_prefix("exec:")
            .map(|command| Command::Exec(command.to_string()))
            .or(s
                .strip_prefix("spawn:")
                .map(|command| Command::Spawn(command.to_string())))
        {
            return Ok(command);
        }

        str_to_command(&s).ok_or(serde::de::Error::custom(format!(
            "{s} is not a valid command"
        )))
    }
}

impl From<Command> for Message<'_> {
    fn from(value: Command) -> Self {
        match value {
            Command::Quit => Message::Quit,

            Command::SplashUp => Message::Splash(splash_modal::Message::Up),
            Command::SplashDown => Message::Splash(splash_modal::Message::Down),
            Command::SplashOpen => Message::Splash(splash_modal::Message::Open),

            Command::ExplorerUp => Message::Explorer(explorer::Message::Up),
            Command::ExplorerDown => Message::Explorer(explorer::Message::Down),
            Command::ExplorerOpen => Message::Explorer(explorer::Message::Select),
            Command::ExplorerSort => Message::Explorer(explorer::Message::Sort),
            Command::ExplorerToggle => Message::Explorer(explorer::Message::Toggle),
            Command::ExplorerToggleOutline => Message::Explorer(explorer::Message::ToggleOutline),
            Command::ExplorerToggleInputRename => {
                Message::Explorer(explorer::Message::ToggleInputRename)
            }
            Command::ExplorerHidePane => Message::Explorer(explorer::Message::HidePane),
            Command::ExplorerExpandPane => Message::Explorer(explorer::Message::ExpandPane),
            Command::ExplorerSwitchPaneNext => Message::Explorer(explorer::Message::SwitchPaneNext),
            Command::ExplorerSwitchPanePrevious => {
                Message::Explorer(explorer::Message::SwitchPanePrevious)
            }
            Command::ExplorerScrollUpOne => {
                Message::Explorer(explorer::Message::ScrollUp(ScrollAmount::One))
            }
            Command::ExplorerScrollDownOne => {
                Message::Explorer(explorer::Message::ScrollDown(ScrollAmount::One))
            }
            Command::ExplorerScrollUpHalfPage => {
                Message::Explorer(explorer::Message::ScrollUp(ScrollAmount::HalfPage))
            }
            Command::ExplorerScrollDownHalfPage => {
                Message::Explorer(explorer::Message::ScrollDown(ScrollAmount::HalfPage))
            }
            Command::ExplorerNewUntitledNote => Message::CreateUntitledNote,
            Command::ExplorerNewUntitledFolder => Message::CreateUntitledFolder,

            Command::InputModalEditMode => Message::Input(input::Message::EditMode),
            Command::InputModalAccept => Message::Input(input::Message::Accept),
            Command::InputModalCancel => Message::Input(input::Message::Cancel),
            Command::InputModalLeft => Message::Input(input::Message::CursorLeft),
            Command::InputModalRight => Message::Input(input::Message::CursorRight),
            Command::InputModalWordForward => Message::Input(input::Message::CursorWordForward),
            Command::InputModalWordBackward => Message::Input(input::Message::CursorWordBackward),

            Command::OutlineUp => Message::Outline(outline::Message::Up),
            Command::OutlineDown => Message::Outline(outline::Message::Down),
            Command::OutlineSelect => Message::Outline(outline::Message::Select),
            Command::OutlineExpand => Message::Outline(outline::Message::Expand),
            Command::OutlineToggle => Message::Outline(outline::Message::Toggle),
            Command::OutlineToggleExplorer => Message::Outline(outline::Message::ToggleExplorer),
            Command::OutlineSwitchPaneNext => Message::Outline(outline::Message::SwitchPaneNext),
            Command::OutlineSwitchPanePrevious => {
                Message::Outline(outline::Message::SwitchPanePrevious)
            }

            Command::HelpModalScrollUpOne => {
                Message::HelpModal(help_modal::Message::ScrollUp(ScrollAmount::One))
            }
            Command::HelpModalScrollDownOne => {
                Message::HelpModal(help_modal::Message::ScrollDown(ScrollAmount::One))
            }
            Command::HelpModalScrollUpHalfPage => {
                Message::HelpModal(help_modal::Message::ScrollUp(ScrollAmount::HalfPage))
            }
            Command::HelpModalScrollDownHalfPage => {
                Message::HelpModal(help_modal::Message::ScrollDown(ScrollAmount::HalfPage))
            }
            Command::HelpModalToggle => Message::HelpModal(help_modal::Message::Toggle),
            Command::HelpModalClose => Message::HelpModal(help_modal::Message::Close),

            Command::NoteEditorScrollUpOne => {
                Message::NoteEditor(note_editor::Message::ScrollUp(ScrollAmount::One))
            }
            Command::NoteEditorScrollDownOne => {
                Message::NoteEditor(note_editor::Message::ScrollDown(ScrollAmount::One))
            }
            Command::NoteEditorScrollUpHalfPage => {
                Message::NoteEditor(note_editor::Message::ScrollUp(ScrollAmount::HalfPage))
            }
            Command::NoteEditorScrollDownHalfPage => {
                Message::NoteEditor(note_editor::Message::ScrollDown(ScrollAmount::HalfPage))
            }
            Command::NoteEditorSwitchPaneNext => {
                Message::NoteEditor(note_editor::Message::SwitchPaneNext)
            }
            Command::NoteEditorSwitchPanePrevious => {
                Message::NoteEditor(note_editor::Message::SwitchPanePrevious)
            }
            Command::NoteEditorCursorUp => Message::NoteEditor(note_editor::Message::CursorUp),
            Command::NoteEditorCursorDown => Message::NoteEditor(note_editor::Message::CursorDown),
            Command::NoteEditorScrollToTop => {
                Message::NoteEditor(note_editor::Message::ScrollToTop)
            }
            Command::NoteEditorScrollToBottom => {
                Message::NoteEditor(note_editor::Message::ScrollToBottom)
            }
            Command::ExplorerScrollToTop => Message::Explorer(explorer::Message::ScrollToTop),
            Command::ExplorerScrollToBottom => Message::Explorer(explorer::Message::ScrollToBottom),
            Command::NoteEditorToggleExplorer => {
                Message::NoteEditor(note_editor::Message::ToggleExplorer)
            }
            Command::NoteEditorToggleOutline => {
                Message::NoteEditor(note_editor::Message::ToggleOutline)
            }

            // Experimental
            Command::NoteEditorExperimentalToggleView => {
                Message::NoteEditor(note_editor::Message::ToggleView)
            }
            Command::NoteEditorExperimentalSetEditView => {
                Message::NoteEditor(note_editor::Message::EditView)
            }
            Command::NoteEditorExperimentalSetReadView => {
                Message::NoteEditor(note_editor::Message::ReadView)
            }
            Command::NoteEditorExperimentalSave => Message::NoteEditor(note_editor::Message::Save),
            Command::NoteEditorExperimentalExit => Message::NoteEditor(note_editor::Message::Exit),
            Command::NoteEditorExperimentalCursorWordForward => {
                Message::NoteEditor(note_editor::Message::CursorWordForward)
            }
            Command::NoteEditorExperimentalCursorWordBackward => {
                Message::NoteEditor(note_editor::Message::CursorWordBackward)
            }
            Command::NoteEditorExperimentalCursorLeft => {
                Message::NoteEditor(note_editor::Message::CursorLeft)
            }
            Command::NoteEditorExperimentalCursorRight => {
                Message::NoteEditor(note_editor::Message::CursorRight)
            }
            Command::NoteEditorInsertMode => Message::NoteEditor(note_editor::Message::InsertMode),

            Command::VaultSelectorModalClose => {
                Message::VaultSelectorModal(vault_selector_modal::Message::Close)
            }
            Command::VaultSelectorModalToggle => {
                Message::VaultSelectorModal(vault_selector_modal::Message::Toggle)
            }
            Command::VaultSelectorModalUp => {
                Message::VaultSelectorModal(vault_selector_modal::Message::Up)
            }
            Command::VaultSelectorModalDown => {
                Message::VaultSelectorModal(vault_selector_modal::Message::Down)
            }
            Command::VaultSelectorModalOpen => {
                Message::VaultSelectorModal(vault_selector_modal::Message::Select)
            }

            Command::DebugLogToggle => Message::DebugLog(debug_log::Message::Toggle),
            Command::DebugLogClose => Message::DebugLog(debug_log::Message::Close),
            Command::DebugLogClear => Message::DebugLog(debug_log::Message::Clear),
            Command::DebugLogCycleLevel => Message::DebugLog(debug_log::Message::CycleLevel),
            Command::DebugLogScrollUpOne => {
                Message::DebugLog(debug_log::Message::ScrollUp(ScrollAmount::One))
            }
            Command::DebugLogScrollDownOne => {
                Message::DebugLog(debug_log::Message::ScrollDown(ScrollAmount::One))
            }
            Command::DebugLogScrollUpHalfPage => {
                Message::DebugLog(debug_log::Message::ScrollUp(ScrollAmount::HalfPage))
            }
            Command::DebugLogScrollDownHalfPage => {
                Message::DebugLog(debug_log::Message::ScrollDown(ScrollAmount::HalfPage))
            }

            Command::Exec(command) => Message::Exec(command),
            Command::Spawn(command) => Message::Spawn(command),
        }
    }
}

pub fn run_command<'a>(
    command: String,
    vault_name: &str,
    note_name: &str,
    note_path: &str,
    mut callback: impl FnMut(&str, &[&str]) -> Option<Message<'a>>,
) -> Option<Message<'a>> {
    let expanded = command
        .replace_var("%vault", vault_name)
        // Order matters, otherwise all mentions of %note_path would be replaced with %note value
        .replace_var("%note_path", note_path)
        .replace_var("%note", note_name);

    let args = expanded.split_whitespace().collect::<Vec<_>>();

    match args.as_slice() {
        [command, args @ ..] => callback(command, args),
        [] => None,
    }
}

pub fn sync_command<'a>(
    terminal: &mut DefaultTerminal,
    command: String,
    vault_name: &str,
    note_name: &str,
    note_path: &str,
) -> Option<Message<'a>> {
    fn enter_alternate_screen(terminal: &mut DefaultTerminal) -> Result<(), std::io::Error> {
        disable_raw_mode()?;
        stdout().execute(LeaveAlternateScreen)?;
        stdout().execute(EnterAlternateScreen)?;
        enable_raw_mode()?;
        terminal.clear()
    }

    run_command(
        command,
        vault_name,
        note_name,
        note_path,
        |command, args| {
            if let Err(error) = process::Command::new(command).arg(args.join(" ")).status() {
                error!(?error, command, "exec command failed");
                return None;
            }
            enter_alternate_screen(terminal)
                .map(|_| Message::Explorer(explorer::Message::Select))
                .ok()
        },
    )
}

pub fn spawn_command<'a>(
    command: String,
    vault_name: &str,
    note_name: &str,
    note_path: &str,
) -> Option<Message<'a>> {
    run_command(
        command,
        vault_name,
        note_name,
        note_path,
        |command, args| {
            if let Err(error) = process::Command::new(command).arg(args.join(" ")).spawn() {
                error!(?error, command, "spawn command failed");
            }
            None
        },
    )
}