Skip to main content

kimun_notes/components/
events.rs

1use std::num::NonZeroU64;
2use std::sync::Arc;
3use std::time::Duration;
4
5use ratatui::crossterm::event::{KeyEvent, MouseEvent};
6use tokio::sync::mpsc::UnboundedSender;
7
8use kimun_core::{NoteVault, nfs::VaultPath};
9
10/// All events that flow through the system — both input events (from crossterm)
11/// and app-level messages sent by components / screens to the main loop.
12#[derive(Debug, Clone)]
13pub enum AppEvent {
14    Input(InputEvent),
15    OpenScreen(ScreenEvent),
16
17    // ── App-level messages ───────────────────────────────────────────────────
18    Quit,
19    Redraw,
20    Autosave,
21    /// Background autosave task finished. `saved_revision` carries the
22    /// editor's `content_revision` at the moment the save was *issued*
23    /// on success, `None` if the write failed. The editor screen uses
24    /// `path` to ignore stale completions for notes the user has
25    /// already navigated away from, and `saved_revision` to clear the
26    /// dirty flag iff the buffer is still at that revision (i.e. no
27    /// edits during the save). `NonZeroU64` because the editor's
28    /// `content_revision` is never zero.
29    AutosaveCompleted {
30        path: VaultPath,
31        saved_revision: Option<NonZeroU64>,
32    },
33    OpenPath(VaultPath),
34    FocusEditor,
35    FocusSidebar,
36    /// Sent by SettingsScreen when user confirms Save. The shared settings
37    /// reference already contains the updated values.
38    SettingsSaved,
39    /// Sent by SettingsScreen when user discards or closes unchanged.
40    CloseSettings,
41    /// Sent by VaultSection; SettingsScreen::handle_app_message intercepts.
42    OpenFileBrowser,
43    /// Sent by IndexingSection; SettingsScreen intercepts.
44    TriggerFastReindex,
45    TriggerFullReindex,
46    /// Sent by indexing tokio task on completion.
47    IndexingDone(Result<Duration, String>),
48    /// Open (or create) today's journal entry and switch to it in the editor.
49    OpenJournal,
50    /// Sent by NoteBrowserModal on Esc or after Enter+open.
51    CloseNoteBrowser,
52    /// Follow the link under the editor cursor: note name/path or external URL.
53    FollowLink(String),
54    /// Open the search modal pre-filled with `#<name>` to browse notes by label.
55    FollowLabel(String),
56    /// Insert raw text at the editor's cursor (replacing any active selection).
57    /// Used by the screen layer to deliver async results back to the editor —
58    /// e.g. the markdown link generated after a clipboard image is saved as an attachment.
59    InsertAtCursor(String),
60
61    // ── File-operation dialog messages ───────────────────────────────────────
62    /// Request to show the file-operations menu (delete / rename / move).
63    ShowFileOpsMenu(VaultPath),
64    /// Request to show the delete confirmation dialog for the given entry.
65    ShowDeleteDialog(VaultPath),
66    /// Request to show the rename dialog for the given entry.
67    ShowRenameDialog(VaultPath),
68    /// Request to show the move dialog for the given entry.
69    ShowMoveDialog(VaultPath),
70    /// Confirmation that the given entry was successfully deleted.
71    EntryDeleted(VaultPath),
72    /// Confirmation that an entry was successfully renamed.
73    EntryRenamed {
74        from: VaultPath,
75        to: VaultPath,
76    },
77    /// Confirmation that an entry was successfully moved.
78    EntryMoved {
79        from: VaultPath,
80        to: VaultPath,
81    },
82    /// A new note was just created and should be opened; sidebar should reflect it.
83    EntryCreated(VaultPath),
84    /// A dialog operation failed; carries a human-readable error message.
85    DialogError(String),
86    /// Dismiss the currently visible dialog without taking action.
87    CloseDialog,
88
89    /// A vault was found to be structurally unusable (conflicts, invalid layout, etc.).
90    /// Carries a formatted, human-readable error message.
91    ///
92    /// Handled by `handle_app_message` in `main.rs`, which clears the workspace,
93    /// saves settings, and opens the settings screen with an error overlay.
94    /// To add a new conflict source: emit this event from the detection site; no
95    /// other files need to change.
96    VaultConflict(String),
97
98    // ── Dialog async result messages ─────────────────────────────────────────
99    /// Rename dialog: name availability check result.
100    RenameValidation {
101        available: bool,
102    },
103    /// Move dialog: directory list has loaded.
104    MoveDirectoriesLoaded(Vec<VaultPath>),
105    /// Move dialog: fuzzy filter results are ready.
106    MoveFilterResults(Vec<VaultPath>),
107    /// Move dialog: destination existence check result.
108    MoveDestValidation {
109        available: bool,
110    },
111
112    // ── Backlinks panel messages ────────────────────────────────────────────
113    /// Backlinks for the current note have been loaded.
114    BacklinksLoaded(Vec<crate::components::backlinks_panel::BacklinkEntry>),
115
116    // ── Workspace messages ──────────────────────────────────────────────
117    /// User switched to a different workspace. Carries the workspace name.
118    /// Handled by main.rs to rebuild the vault and navigate to StartScreen.
119    WorkspaceSwitched(String),
120}
121
122impl AppEvent {
123    pub fn send_input(event: InputEvent) -> Self {
124        AppEvent::Input(event)
125    }
126}
127
128// ── Input events ────────────────────────────────────────────────────────
129#[derive(Debug, Clone)]
130pub enum InputEvent {
131    Key(KeyEvent),
132    Mouse(MouseEvent),
133    /// Bracketed-paste payload from the terminal. On macOS this is what
134    /// Cmd+V delivers, since the terminal intercepts Cmd combos before they
135    /// reach the TUI. The string may be empty when the clipboard holds only
136    /// non-text content (e.g. an image).
137    Paste(String),
138}
139
140// ── Screen events ────────────────────────────────────────────────────────
141#[derive(Debug, Clone)]
142pub enum ScreenEvent {
143    Start,
144    OpenSettings,
145    /// Open the settings screen with an error overlay already shown.
146    OpenSettingsWithError(String),
147    /// Navigate to the editor for the given vault root path.
148    OpenEditor(Arc<NoteVault>, VaultPath),
149    /// Navigate to the browse screen for the given vault root and directory path.
150    OpenBrowse(Arc<NoteVault>, VaultPath),
151}
152
153/// Convenience alias used throughout the codebase.
154pub type AppTx = UnboundedSender<AppEvent>;
155
156/// Build a `Send + Sync` callback that fires `AppEvent::Redraw` on the
157/// app event bus. Used by long-lived components (autocomplete query
158/// task, etc.) that need to wake the render loop from a background
159/// thread but should not be aware of `AppEvent` themselves.
160pub fn redraw_callback(tx: AppTx) -> Arc<dyn Fn() + Send + Sync + 'static> {
161    Arc::new(move || {
162        let _ = tx.send(AppEvent::Redraw);
163    })
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    fn _assert_new_variants_exist(e: AppEvent) {
171        match e {
172            AppEvent::ShowDeleteDialog(_) => {}
173            AppEvent::ShowRenameDialog(_) => {}
174            AppEvent::ShowMoveDialog(_) => {}
175            AppEvent::EntryDeleted(_) => {}
176            AppEvent::EntryRenamed { from: _, to: _ } => {}
177            AppEvent::EntryMoved { from: _, to: _ } => {}
178            AppEvent::DialogError(_) => {}
179            AppEvent::CloseDialog => {}
180            _ => {}
181        }
182    }
183}