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#[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}