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