Skip to main content

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}