Skip to main content

kimun_notes/components/
drawer.rs

1//! The **Drawer** — the single panel between the activity rail and the
2//! editor. It renders whichever rail view is active: the file browser
3//! (FILES), the Query panel (FIND), or a placeholder for the views that land
4//! in later phases (TAGS, LINKS, OUTLINE, CFG).
5
6use ratatui::Frame;
7use ratatui::layout::Rect;
8use ratatui::style::Style;
9use ratatui::widgets::Paragraph;
10
11use crate::components::Component;
12use crate::components::backlinks_panel::QueryPanel;
13use crate::components::drawer_views::{LinksPanel, OutlinePanel, TagsPanel};
14use crate::components::event_state::EventState;
15use crate::components::events::{AppTx, InputEvent};
16use crate::components::panel::panel_block;
17use crate::components::sidebar::SidebarComponent;
18use crate::settings::themes::Theme;
19
20/// The views the activity rail can put in the drawer. Closed set, mirrors
21/// the rail items top to bottom.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum DrawerView {
24    Files,
25    Find,
26    Tags,
27    Links,
28    Outline,
29    Config,
30}
31
32impl DrawerView {
33    /// Status-bar label when the drawer shows this view.
34    pub fn label(&self) -> &'static str {
35        match self {
36            DrawerView::Files => "FILES",
37            DrawerView::Find => "FIND",
38            DrawerView::Tags => "TAGS",
39            DrawerView::Links => "LINKS",
40            DrawerView::Outline => "OUTLINE",
41            DrawerView::Config => "CFG",
42        }
43    }
44}
45
46/// What the CFG drawer view displays — resolved by the host screen when the
47/// view opens (the drawer itself holds no settings handle).
48#[derive(Default, Clone)]
49pub struct ConfigInfo {
50    pub theme_name: String,
51    pub leader_key: String,
52    pub preferences_key: String,
53    pub leader_timeout_ms: u64,
54    pub config_path: String,
55}
56
57/// Hosts the drawer views. FILES and FIND are the ported existing panels
58/// (file browser and Query panel); TAGS, LINKS, and OUTLINE are the
59/// phase-03 panels; CFG is a placeholder until the settings drawer lands.
60pub struct DrawerHost {
61    active: DrawerView,
62    sidebar: SidebarComponent,
63    query: QueryPanel,
64    tags: TagsPanel,
65    links: LinksPanel,
66    outline: OutlinePanel,
67    /// CFG view contents, refreshed by the host when the view opens.
68    config_info: ConfigInfo,
69}
70
71impl DrawerHost {
72    pub fn new(
73        sidebar: SidebarComponent,
74        query: QueryPanel,
75        tags: TagsPanel,
76        links: LinksPanel,
77        outline: OutlinePanel,
78    ) -> Self {
79        Self {
80            active: DrawerView::Files,
81            sidebar,
82            query,
83            tags,
84            links,
85            outline,
86            config_info: ConfigInfo::default(),
87        }
88    }
89
90    /// Refresh what the CFG view shows (called when the view opens).
91    pub fn set_config_info(&mut self, info: ConfigInfo) {
92        self.config_info = info;
93    }
94
95    pub fn active_view(&self) -> DrawerView {
96        self.active
97    }
98
99    /// Whether the active view is a text-input context (drives the status
100    /// bar's ⌨/≣ indicator). The surface owns this knowledge: FIND hosts a
101    /// query input; the list views are filter-as-you-type lists, which read
102    /// as lists (spec mockup shows them with ≣).
103    pub fn is_text_input(&self) -> bool {
104        matches!(self.active, DrawerView::Find)
105    }
106
107    pub fn set_view(&mut self, view: DrawerView) {
108        self.active = view;
109    }
110
111    // ── Typed accessors for view-specific calls from the host screen ───────
112
113    pub fn sidebar(&self) -> &SidebarComponent {
114        &self.sidebar
115    }
116    pub fn sidebar_mut(&mut self) -> &mut SidebarComponent {
117        &mut self.sidebar
118    }
119    pub fn query(&self) -> &QueryPanel {
120        &self.query
121    }
122    pub fn query_mut(&mut self) -> &mut QueryPanel {
123        &mut self.query
124    }
125    pub fn tags_mut(&mut self) -> &mut TagsPanel {
126        &mut self.tags
127    }
128    pub fn links_mut(&mut self) -> &mut LinksPanel {
129        &mut self.links
130    }
131    pub fn outline_mut(&mut self) -> &mut OutlinePanel {
132        &mut self.outline
133    }
134
135    pub fn hint_shortcuts(&self) -> Vec<(String, String)> {
136        match self.active {
137            DrawerView::Files => self.sidebar.hint_shortcuts(),
138            DrawerView::Find => self.query.hint_shortcuts(),
139            DrawerView::Tags => self.tags.hint_shortcuts(),
140            DrawerView::Links => self.links.hint_shortcuts(),
141            DrawerView::Outline => self.outline.hint_shortcuts(),
142            DrawerView::Config => vec![
143                ("t/⏎".into(), "Theme picker".into()),
144                ("p".into(), "Preferences".into()),
145            ],
146        }
147    }
148
149    pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
150        match self.active {
151            DrawerView::Files => self.sidebar.handle_input(event, tx),
152            DrawerView::Find => {
153                // The Query panel speaks `handle_key`; non-key events are not
154                // delivered to it.
155                if let InputEvent::Key(key) = event {
156                    self.query.handle_key(key, tx)
157                } else {
158                    EventState::NotConsumed
159                }
160            }
161            DrawerView::Tags => self.tags.handle_input(event, tx),
162            DrawerView::Links => self.links.handle_input(event, tx),
163            DrawerView::Outline => self.outline.handle_input(event, tx),
164            DrawerView::Config => {
165                if let InputEvent::Key(key) = event {
166                    use ratatui::crossterm::event::KeyCode;
167                    match key.code {
168                        KeyCode::Char('t') | KeyCode::Enter => {
169                            tx.send(crate::components::events::AppEvent::ExecuteLeaderAction(
170                                crate::keys::leader::LeaderAction::VaultTheme,
171                            ))
172                            .ok();
173                            return EventState::Consumed;
174                        }
175                        KeyCode::Char('p') => {
176                            tx.send(crate::components::events::AppEvent::OpenScreen(
177                                crate::components::events::ScreenEvent::OpenPreferences,
178                            ))
179                            .ok();
180                            return EventState::Consumed;
181                        }
182                        _ => {}
183                    }
184                }
185                EventState::NotConsumed
186            }
187        }
188    }
189
190    pub fn handle_mouse(&mut self, event: &InputEvent, tx: &AppTx) {
191        let InputEvent::Mouse(mouse) = event else {
192            return;
193        };
194        match self.active {
195            // The Query panel has a dedicated mouse entry point; every other
196            // view takes mouse events through its regular input path.
197            DrawerView::Find => {
198                self.query.handle_mouse(mouse, tx);
199            }
200            _ => {
201                self.handle_input(event, tx);
202            }
203        }
204    }
205
206    pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
207        match self.active {
208            DrawerView::Files => self.sidebar.render(f, rect, theme, focused),
209            DrawerView::Find => self.query.render(f, rect, theme, focused),
210            DrawerView::Tags => self.tags.render(f, rect, theme, focused),
211            DrawerView::Links => self.links.render(f, rect, theme, focused),
212            DrawerView::Outline => self.outline.render(f, rect, theme, focused),
213            DrawerView::Config => {
214                let block = panel_block("Config", theme, focused);
215                let inner = block.inner(rect);
216                f.render_widget(block, rect);
217                let info = &self.config_info;
218                let label = Style::default().fg(theme.gray.to_ratatui());
219                let value = Style::default().fg(theme.fg.to_ratatui());
220                let keycap = Style::default().fg(theme.yellow.to_ratatui());
221                let lines = vec![
222                    ratatui::text::Line::from(vec![
223                        ratatui::text::Span::styled(" theme    ", label),
224                        ratatui::text::Span::styled(info.theme_name.clone(), value),
225                    ]),
226                    ratatui::text::Line::from(vec![
227                        ratatui::text::Span::styled(" leader   ", label),
228                        ratatui::text::Span::styled(info.leader_key.clone(), value),
229                    ]),
230                    ratatui::text::Line::from(vec![
231                        ratatui::text::Span::styled(" prefs    ", label),
232                        ratatui::text::Span::styled(info.preferences_key.clone(), value),
233                    ]),
234                    ratatui::text::Line::from(vec![
235                        ratatui::text::Span::styled(" timeout  ", label),
236                        ratatui::text::Span::styled(
237                            format!("{} ms (which-key reveal)", info.leader_timeout_ms),
238                            value,
239                        ),
240                    ]),
241                    ratatui::text::Line::from(vec![
242                        ratatui::text::Span::styled(" config   ", label),
243                        ratatui::text::Span::styled(info.config_path.clone(), value),
244                    ]),
245                    ratatui::text::Line::default(),
246                    ratatui::text::Line::from(vec![
247                        ratatui::text::Span::styled(" t ", keycap),
248                        ratatui::text::Span::styled("theme picker", label),
249                    ]),
250                    ratatui::text::Line::from(vec![
251                        ratatui::text::Span::styled(" p ", keycap),
252                        ratatui::text::Span::styled("preferences", label),
253                    ]),
254                ];
255                f.render_widget(Paragraph::new(lines), inner);
256            }
257        }
258    }
259}