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}