Skip to main content

kimun_notes/components/
config_panel.rs

1//! The CFG **drawer view** as a component — the same shape as its sibling
2//! views (Tags/Links/Outline), so `DrawerHost`'s dispatch stays uniform: one
3//! arm per view, never inline view logic in the host.
4
5use ratatui::Frame;
6use ratatui::layout::Rect;
7use ratatui::style::Style;
8use ratatui::widgets::Paragraph;
9
10use crate::components::event_state::EventState;
11use crate::components::events::{AppEvent, AppTx, InputEvent, ScreenEvent};
12use crate::components::panel::panel_block;
13use crate::keys::leader::LeaderAction;
14use crate::settings::themes::Theme;
15
16/// What the CFG drawer view displays — resolved by the host screen when the
17/// view opens (the panel itself holds no settings handle).
18#[derive(Default, Clone)]
19pub struct ConfigInfo {
20    pub theme_name: String,
21    pub leader_key: String,
22    pub preferences_key: String,
23    pub leader_timeout_ms: u64,
24    pub config_path: String,
25}
26
27/// Read-only settings summary plus two launcher keys: `t`/Enter opens the
28/// theme picker, `p` opens Preferences.
29#[derive(Default)]
30pub struct ConfigPanel {
31    info: ConfigInfo,
32}
33
34impl ConfigPanel {
35    /// Refresh what the view shows (called by the host when the view opens).
36    pub fn set_info(&mut self, info: ConfigInfo) {
37        self.info = info;
38    }
39
40    pub fn hint_shortcuts(&self) -> Vec<(String, String)> {
41        vec![
42            ("t/⏎".into(), "Theme picker".into()),
43            ("p".into(), "Preferences".into()),
44        ]
45    }
46
47    pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
48        use ratatui::crossterm::event::KeyCode;
49        if let InputEvent::Key(key) = event {
50            match key.code {
51                KeyCode::Char('t') | KeyCode::Enter => {
52                    tx.send(AppEvent::ExecuteLeaderAction(LeaderAction::VaultTheme))
53                        .ok();
54                    return EventState::Consumed;
55                }
56                KeyCode::Char('p') => {
57                    tx.send(AppEvent::OpenScreen(ScreenEvent::OpenPreferences))
58                        .ok();
59                    return EventState::Consumed;
60                }
61                _ => {}
62            }
63        }
64        EventState::NotConsumed
65    }
66
67    pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
68        let block = panel_block("Config", theme, focused);
69        let inner = block.inner(rect);
70        f.render_widget(block, rect);
71        let info = &self.info;
72        let label = Style::default().fg(theme.gray.to_ratatui());
73        let value = Style::default().fg(theme.fg.to_ratatui());
74        let keycap = Style::default().fg(theme.yellow.to_ratatui());
75        let lines = vec![
76            ratatui::text::Line::from(vec![
77                ratatui::text::Span::styled(" theme    ", label),
78                ratatui::text::Span::styled(info.theme_name.clone(), value),
79            ]),
80            ratatui::text::Line::from(vec![
81                ratatui::text::Span::styled(" leader   ", label),
82                ratatui::text::Span::styled(info.leader_key.clone(), value),
83            ]),
84            ratatui::text::Line::from(vec![
85                ratatui::text::Span::styled(" prefs    ", label),
86                ratatui::text::Span::styled(info.preferences_key.clone(), value),
87            ]),
88            ratatui::text::Line::from(vec![
89                ratatui::text::Span::styled(" timeout  ", label),
90                ratatui::text::Span::styled(
91                    format!("{} ms (which-key reveal)", info.leader_timeout_ms),
92                    value,
93                ),
94            ]),
95            ratatui::text::Line::from(vec![
96                ratatui::text::Span::styled(" config   ", label),
97                ratatui::text::Span::styled(info.config_path.clone(), value),
98            ]),
99            ratatui::text::Line::default(),
100            ratatui::text::Line::from(vec![
101                ratatui::text::Span::styled(" t ", keycap),
102                ratatui::text::Span::styled("theme picker", label),
103            ]),
104            ratatui::text::Line::from(vec![
105                ratatui::text::Span::styled(" p ", keycap),
106                ratatui::text::Span::styled("preferences", label),
107            ]),
108        ];
109        f.render_widget(Paragraph::new(lines), inner);
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
117    use tokio::sync::mpsc::unbounded_channel;
118
119    fn key(code: KeyCode) -> InputEvent {
120        InputEvent::Key(KeyEvent::new(code, KeyModifiers::NONE))
121    }
122
123    #[test]
124    fn t_and_enter_open_the_theme_picker() {
125        let (tx, mut rx) = unbounded_channel();
126        let mut panel = ConfigPanel::default();
127        for code in [KeyCode::Char('t'), KeyCode::Enter] {
128            assert_eq!(panel.handle_input(&key(code), &tx), EventState::Consumed);
129            assert!(matches!(
130                rx.try_recv(),
131                Ok(AppEvent::ExecuteLeaderAction(LeaderAction::VaultTheme))
132            ));
133        }
134    }
135
136    #[test]
137    fn p_opens_preferences() {
138        let (tx, mut rx) = unbounded_channel();
139        let mut panel = ConfigPanel::default();
140        assert_eq!(
141            panel.handle_input(&key(KeyCode::Char('p')), &tx),
142            EventState::Consumed
143        );
144        assert!(matches!(
145            rx.try_recv(),
146            Ok(AppEvent::OpenScreen(ScreenEvent::OpenPreferences))
147        ));
148    }
149
150    #[test]
151    fn other_input_is_not_consumed() {
152        let (tx, mut rx) = unbounded_channel();
153        let mut panel = ConfigPanel::default();
154        assert_eq!(
155            panel.handle_input(&key(KeyCode::Char('x')), &tx),
156            EventState::NotConsumed
157        );
158        assert_eq!(
159            panel.handle_input(&InputEvent::Paste("hi".into()), &tx),
160            EventState::NotConsumed
161        );
162        assert!(rx.try_recv().is_err(), "no event may be emitted");
163    }
164}