1use ratatui::Frame;
7use ratatui::layout::Rect;
8
9use crate::components::Component;
10use crate::components::ask_sources::SourcesPanel;
11use crate::components::config_panel::ConfigPanel;
12use crate::components::drawer_views::{LinksPanel, OutlinePanel, TagsPanel};
13use crate::components::event_state::EventState;
14use crate::components::events::{AppTx, InputEvent};
15use crate::components::query_panel::QueryPanel;
16use crate::components::semantic_search::SemanticPanel;
17use crate::components::sidebar::SidebarComponent;
18use crate::settings::themes::Theme;
19use kimun_core::NoteVault;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum DrawerView {
25 Files,
26 Find,
27 Semantic,
28 Ask,
29 Tags,
30 Links,
31 Outline,
32 Config,
33}
34
35impl DrawerView {
36 pub fn label(&self) -> &'static str {
38 match self {
39 DrawerView::Files => "FILES",
40 DrawerView::Find => "FIND",
41 DrawerView::Semantic => "SEMANTIC",
42 DrawerView::Ask => "ASK",
43 DrawerView::Tags => "TAGS",
44 DrawerView::Links => "LINKS",
45 DrawerView::Outline => "OUTLINE",
46 DrawerView::Config => "CFG",
47 }
48 }
49}
50
51pub use crate::components::config_panel::ConfigInfo;
52
53pub struct DrawerHost {
57 active: DrawerView,
58 sidebar: SidebarComponent,
59 query: QueryPanel,
60 semantic: SemanticPanel,
61 ask_sources: SourcesPanel,
65 tags: TagsPanel,
66 links: LinksPanel,
67 outline: OutlinePanel,
68 config: ConfigPanel,
69}
70
71impl DrawerHost {
72 #[allow(clippy::too_many_arguments)]
73 pub fn new(
74 vault: std::sync::Arc<NoteVault>,
75 key_bindings: &crate::keys::KeyBindings,
76 sidebar: SidebarComponent,
77 query: QueryPanel,
78 semantic: SemanticPanel,
79 tags: TagsPanel,
80 links: LinksPanel,
81 outline: OutlinePanel,
82 ) -> Self {
83 Self {
84 active: DrawerView::Files,
85 sidebar,
86 query,
87 semantic,
88 ask_sources: SourcesPanel::new(vault, key_bindings),
91 tags,
92 links,
93 outline,
94 config: ConfigPanel::default(),
95 }
96 }
97
98 pub fn set_config_info(&mut self, info: ConfigInfo) {
100 self.config.set_info(info);
101 }
102
103 pub fn active_view(&self) -> DrawerView {
104 self.active
105 }
106
107 pub fn is_text_input(&self) -> bool {
112 matches!(self.active, DrawerView::Find | DrawerView::Semantic)
115 }
116
117 pub fn set_view(&mut self, view: DrawerView) {
118 self.active = view;
119 }
120
121 pub fn sidebar(&self) -> &SidebarComponent {
124 &self.sidebar
125 }
126 pub fn sidebar_mut(&mut self) -> &mut SidebarComponent {
127 &mut self.sidebar
128 }
129 pub fn query(&self) -> &QueryPanel {
130 &self.query
131 }
132 pub fn query_mut(&mut self) -> &mut QueryPanel {
133 &mut self.query
134 }
135 pub fn semantic_mut(&mut self) -> &mut SemanticPanel {
136 &mut self.semantic
137 }
138 pub fn ask_sources_mut(&mut self) -> &mut SourcesPanel {
139 &mut self.ask_sources
140 }
141 pub fn tags_mut(&mut self) -> &mut TagsPanel {
142 &mut self.tags
143 }
144 pub fn links_mut(&mut self) -> &mut LinksPanel {
145 &mut self.links
146 }
147 pub fn outline_mut(&mut self) -> &mut OutlinePanel {
148 &mut self.outline
149 }
150
151 pub fn hint_shortcuts(&self) -> Vec<(String, String)> {
152 match self.active {
153 DrawerView::Files => self.sidebar.hint_shortcuts(),
154 DrawerView::Find => self.query.hint_shortcuts(),
155 DrawerView::Semantic => self.semantic.hint_shortcuts(),
156 DrawerView::Ask => self.ask_sources.hint_shortcuts(),
157 DrawerView::Tags => self.tags.hint_shortcuts(),
158 DrawerView::Links => self.links.hint_shortcuts(),
159 DrawerView::Outline => self.outline.hint_shortcuts(),
160 DrawerView::Config => self.config.hint_shortcuts(),
161 }
162 }
163
164 pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
165 match self.active {
166 DrawerView::Files => self.sidebar.handle_input(event, tx),
167 DrawerView::Find => {
168 if let InputEvent::Key(key) = event {
171 self.query.handle_key(key, tx)
172 } else {
173 EventState::NotConsumed
174 }
175 }
176 DrawerView::Semantic => self.semantic.handle_input(event, tx),
177 DrawerView::Ask => self.ask_sources.handle_input(event, tx),
178 DrawerView::Tags => self.tags.handle_input(event, tx),
179 DrawerView::Links => self.links.handle_input(event, tx),
180 DrawerView::Outline => self.outline.handle_input(event, tx),
181 DrawerView::Config => self.config.handle_input(event, tx),
182 }
183 }
184
185 pub fn handle_mouse(&mut self, event: &InputEvent, tx: &AppTx) {
186 let InputEvent::Mouse(mouse) = event else {
187 return;
188 };
189 match self.active {
190 DrawerView::Find => {
193 self.query.handle_mouse(mouse, tx);
194 }
195 _ => {
196 self.handle_input(event, tx);
197 }
198 }
199 }
200
201 pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
202 match self.active {
203 DrawerView::Files => self.sidebar.render(f, rect, theme, focused),
204 DrawerView::Find => self.query.render(f, rect, theme, focused),
205 DrawerView::Semantic => self.semantic.render(f, rect, theme, focused),
206 DrawerView::Ask => self.ask_sources.render(f, rect, theme, focused),
207 DrawerView::Tags => self.tags.render(f, rect, theme, focused),
208 DrawerView::Links => self.links.render(f, rect, theme, focused),
209 DrawerView::Outline => self.outline.render(f, rect, theme, focused),
210 DrawerView::Config => self.config.render(f, rect, theme, focused),
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::components::events::AppEvent;
219 use crate::settings::AppSettings;
220 use crate::test_support::temp_vault;
221 use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
222 use tokio::sync::mpsc::unbounded_channel;
223
224 async fn make_host() -> DrawerHost {
225 let vault = temp_vault("drawerhost").await;
226 vault.validate_and_init().await.unwrap();
227 let settings = AppSettings::default();
228 let sidebar = crate::components::sidebar::SidebarComponent::new(
229 settings.key_bindings.clone(),
230 vault.clone(),
231 settings.icons(),
232 &settings,
233 );
234 let query = crate::components::query_panel::QueryPanel::new(
235 vault.clone(),
236 settings.key_bindings.clone(),
237 settings.icons(),
238 );
239 let semantic = crate::components::semantic_search::SemanticPanel::new(
240 vault.clone(),
241 std::sync::Arc::new(std::sync::RwLock::new(settings.clone())),
242 settings.icons(),
243 );
244 let tags = crate::components::drawer_views::TagsPanel::new(vault.clone(), settings.icons());
245 let links =
246 crate::components::drawer_views::LinksPanel::new(vault.clone(), settings.icons());
247 let outline =
248 crate::components::drawer_views::OutlinePanel::new(vault.clone(), settings.icons());
249 DrawerHost::new(
250 vault,
251 &settings.key_bindings,
252 sidebar,
253 query,
254 semantic,
255 tags,
256 links,
257 outline,
258 )
259 }
260
261 fn key(code: KeyCode) -> InputEvent {
262 InputEvent::Key(KeyEvent::new(code, KeyModifiers::NONE))
263 }
264
265 #[tokio::test]
268 async fn find_view_delivers_keys_only() {
269 let mut host = make_host().await;
270 let (tx, _rx) = unbounded_channel();
271 host.set_view(DrawerView::Find);
272 assert_eq!(
273 host.handle_input(&InputEvent::Paste("hi".into()), &tx),
274 EventState::NotConsumed,
275 "a paste is not a key; FIND must not consume it"
276 );
277 }
278
279 #[tokio::test]
283 async fn config_view_routes_to_the_config_panel() {
284 let mut host = make_host().await;
285 let (tx, mut rx) = unbounded_channel();
286 host.set_view(DrawerView::Config);
287 assert_eq!(
288 host.handle_input(&key(KeyCode::Char('t')), &tx),
289 EventState::Consumed
290 );
291 assert!(matches!(
292 rx.try_recv(),
293 Ok(AppEvent::ExecuteLeaderAction(
294 crate::keys::leader::LeaderAction::VaultTheme
295 ))
296 ));
297 assert_eq!(
298 host.handle_input(&key(KeyCode::Char('x')), &tx),
299 EventState::NotConsumed
300 );
301 }
302
303 #[tokio::test]
305 async fn hints_follow_the_active_view() {
306 let mut host = make_host().await;
307 host.set_view(DrawerView::Config);
308 let cfg_hints = host.hint_shortcuts();
309 assert!(cfg_hints.iter().any(|(_, h)| h == "Theme picker"));
310 host.set_view(DrawerView::Files);
311 assert_ne!(host.hint_shortcuts(), cfg_hints);
312 }
313
314 #[tokio::test]
317 async fn text_input_views_are_find_and_semantic() {
318 let mut host = make_host().await;
319 for (view, expect) in [
320 (DrawerView::Files, false),
321 (DrawerView::Find, true),
322 (DrawerView::Semantic, true),
323 (DrawerView::Ask, false),
324 (DrawerView::Tags, false),
325 (DrawerView::Links, false),
326 (DrawerView::Outline, false),
327 (DrawerView::Config, false),
328 ] {
329 host.set_view(view);
330 assert_eq!(host.is_text_input(), expect, "{view:?}");
331 }
332 }
333}