kimun_notes/app_screen/mod.rs
1pub mod ask;
2pub mod browse;
3pub mod click_run;
4pub mod doc_meta;
5pub mod editor;
6pub mod editor_input;
7pub mod onboarding;
8pub mod overlay_host;
9pub mod panel_set;
10pub mod preferences;
11pub mod start;
12
13use async_trait::async_trait;
14use ratatui::Frame;
15
16use kimun_core::nfs::VaultPath;
17
18use crate::components::event_state::EventState;
19use crate::components::events::{AppEvent, AppTx, InputEvent};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ScreenKind {
23 Start,
24 Browse,
25 Editor,
26 Onboarding,
27 Preferences,
28}
29
30// The `Send` supertrait is what lets `async_trait` box these futures as `Send`,
31// which `Box<dyn AppScreen>` needs. Screens were `?Send` until the editor stopped
32// being a `ratatui-textarea` `TextArea` — its `Block` field is non-`Send` as of
33// `ratatui-widgets 0.3.1`, whose shadow effect holds an `Arc<dyn CellEffect>`.
34// Nothing in a screen is non-`Send` now, so the relaxation is gone rather than
35// merely unused. Reintroducing a non-`Send` field means reintroducing `?Send`
36// across every screen impl and `Overlay` too, so prefer not to.
37#[async_trait]
38pub trait AppScreen: Send {
39 /// Called once when the screen mounts. Send `AppEvent`s through `tx` to
40 /// trigger navigation (e.g. `StartScreen` checking whether a vault exists).
41 async fn on_enter(&mut self, _tx: &AppTx) {}
42
43 /// Handle an input event. Send events through `tx` for navigation or quit.
44 /// Returns whether this screen consumed the event.
45 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState;
46
47 fn render(&mut self, f: &mut Frame);
48
49 /// Handle an application-level event owned by this screen. Events the
50 /// screen does not recognize are silently ignored. The default
51 /// implementation handles nothing.
52 async fn handle_app_message(&mut self, _msg: AppEvent, _tx: &AppTx) {}
53
54 /// Try to open `path` within this screen (e.g. load a note into the
55 /// buffer, or navigate a sidebar). Return `Some(path)` if the screen
56 /// does not handle it, in which case the main loop switches to an
57 /// appropriate screen. Default: not handled.
58 /// `emphasis` carries the originating query's needles when the open came
59 /// from a query result; only the editor screen uses it (spec §5.1) — it
60 /// is dropped when the open reroutes to a screen switch.
61 async fn try_open_path(
62 &mut self,
63 path: VaultPath,
64 _emphasis: Option<Vec<String>>,
65 _tx: &AppTx,
66 ) -> Option<VaultPath> {
67 Some(path)
68 }
69
70 /// Try to open the attachment at `path` within this screen (the editor
71 /// screen shows it in its read-only attachment view). Return
72 /// `Some(path)` if the screen does not handle it, in which case the main
73 /// loop routes it to the editor screen. Default: not handled.
74 async fn try_open_attachment(&mut self, path: VaultPath, _tx: &AppTx) -> Option<VaultPath> {
75 Some(path)
76 }
77
78 fn get_kind(&self) -> ScreenKind;
79
80 /// Called once just before the screen is removed from the app (quit or screen transition).
81 /// Default implementation is a no-op.
82 async fn on_exit(&mut self, _tx: &AppTx) {}
83}
84
85#[cfg(test)]
86mod tests {
87 use tokio::sync::mpsc::unbounded_channel;
88
89 use std::sync::{Arc, RwLock};
90
91 use super::*;
92 use crate::app_screen::preferences::PreferencesScreen;
93 use crate::settings::AppSettings;
94
95 fn shared_defaults() -> crate::settings::SharedSettings {
96 Arc::new(RwLock::new(AppSettings::default()))
97 }
98
99 #[tokio::test]
100 async fn on_exit_default_is_noop() {
101 let (tx, _rx) = unbounded_channel::<AppEvent>();
102 let mut screen = PreferencesScreen::new(shared_defaults());
103 screen.on_exit(&tx).await; // must compile and not panic
104 }
105}