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 settings.yank_combos(),
244 );
245 let tags = crate::components::drawer_views::TagsPanel::new(
246 vault.clone(),
247 settings.icons(),
248 settings.yank_combos(),
249 );
250 let links = crate::components::drawer_views::LinksPanel::new(
251 vault.clone(),
252 settings.icons(),
253 settings.yank_combos(),
254 );
255 let outline = crate::components::drawer_views::OutlinePanel::new(
256 vault.clone(),
257 settings.icons(),
258 settings.yank_combos(),
259 );
260 DrawerHost::new(
261 vault,
262 &settings.key_bindings,
263 sidebar,
264 query,
265 semantic,
266 tags,
267 links,
268 outline,
269 )
270 }
271
272 fn key(code: KeyCode) -> InputEvent {
273 InputEvent::Key(KeyEvent::new(code, KeyModifiers::NONE))
274 }
275
276 #[tokio::test]
279 async fn find_view_delivers_keys_only() {
280 let mut host = make_host().await;
281 let (tx, _rx) = unbounded_channel();
282 host.set_view(DrawerView::Find);
283 assert_eq!(
284 host.handle_input(&InputEvent::Paste("hi".into()), &tx),
285 EventState::NotConsumed,
286 "a paste is not a key; FIND must not consume it"
287 );
288 }
289
290 #[tokio::test]
294 async fn config_view_routes_to_the_config_panel() {
295 let mut host = make_host().await;
296 let (tx, mut rx) = unbounded_channel();
297 host.set_view(DrawerView::Config);
298 assert_eq!(
299 host.handle_input(&key(KeyCode::Char('t')), &tx),
300 EventState::Consumed
301 );
302 assert!(matches!(
303 rx.try_recv(),
304 Ok(AppEvent::ExecuteLeaderAction(
305 crate::keys::leader::LeaderAction::VaultTheme
306 ))
307 ));
308 assert_eq!(
309 host.handle_input(&key(KeyCode::Char('x')), &tx),
310 EventState::NotConsumed
311 );
312 }
313
314 #[tokio::test]
316 async fn hints_follow_the_active_view() {
317 let mut host = make_host().await;
318 host.set_view(DrawerView::Config);
319 let cfg_hints = host.hint_shortcuts();
320 assert!(cfg_hints.iter().any(|(_, h)| h == "Theme picker"));
321 host.set_view(DrawerView::Files);
322 assert_ne!(host.hint_shortcuts(), cfg_hints);
323 }
324
325 #[tokio::test]
328 async fn text_input_views_are_find_and_semantic() {
329 let mut host = make_host().await;
330 for (view, expect) in [
331 (DrawerView::Files, false),
332 (DrawerView::Find, true),
333 (DrawerView::Semantic, true),
334 (DrawerView::Ask, false),
335 (DrawerView::Tags, false),
336 (DrawerView::Links, false),
337 (DrawerView::Outline, false),
338 (DrawerView::Config, false),
339 ] {
340 host.set_view(view);
341 assert_eq!(host.is_text_input(), expect, "{view:?}");
342 }
343 }
344}