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#[allow(clippy::double_must_use)]
38#[async_trait]
39pub trait AppScreen: Send {
40 /// Called once when the screen mounts. Send `AppEvent`s through `tx` to
41 /// trigger navigation (e.g. `StartScreen` checking whether a vault exists).
42 async fn on_enter(&mut self, _tx: &AppTx) {}
43
44 /// Handle an input event. Send events through `tx` for navigation or quit.
45 /// Returns whether this screen consumed the event.
46 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState;
47
48 fn render(&mut self, f: &mut Frame);
49
50 /// Handle an application-level event owned by this screen. Events the
51 /// screen does not recognize are silently ignored. The default
52 /// implementation handles nothing.
53 async fn handle_app_message(&mut self, _msg: AppEvent, _tx: &AppTx) {}
54
55 /// Try to open `path` within this screen (e.g. load a note into the
56 /// buffer, or navigate a sidebar). Return `Some(path)` if the screen
57 /// does not handle it, in which case the main loop switches to an
58 /// appropriate screen. Default: not handled.
59 /// `emphasis` carries the originating query's needles when the open came
60 /// from a query result; only the editor screen uses it (spec §5.1) — it
61 /// is dropped when the open reroutes to a screen switch.
62 async fn try_open_path(
63 &mut self,
64 path: VaultPath,
65 _emphasis: Option<Vec<String>>,
66 _tx: &AppTx,
67 ) -> Option<VaultPath> {
68 Some(path)
69 }
70
71 /// Try to open the attachment at `path` within this screen (the editor
72 /// screen shows it in its read-only attachment view). Return
73 /// `Some(path)` if the screen does not handle it, in which case the main
74 /// loop routes it to the editor screen. Default: not handled.
75 async fn try_open_attachment(&mut self, path: VaultPath, _tx: &AppTx) -> Option<VaultPath> {
76 Some(path)
77 }
78
79 fn get_kind(&self) -> ScreenKind;
80
81 /// Called once just before the screen is removed from the app (quit or screen transition).
82 /// Default implementation is a no-op.
83 async fn on_exit(&mut self, _tx: &AppTx) {}
84}
85
86#[cfg(test)]
87mod tests {
88 use tokio::sync::mpsc::unbounded_channel;
89
90 use std::sync::{Arc, RwLock};
91
92 use super::*;
93 use crate::app_screen::preferences::PreferencesScreen;
94 use crate::settings::AppSettings;
95
96 fn shared_defaults() -> crate::settings::SharedSettings {
97 Arc::new(RwLock::new(AppSettings::default()))
98 }
99
100 #[tokio::test]
101 async fn on_exit_default_is_noop() {
102 let (tx, _rx) = unbounded_channel::<AppEvent>();
103 let mut screen = PreferencesScreen::new(shared_defaults());
104 screen.on_exit(&tx).await; // must compile and not panic
105 }
106}