kimun_notes/components/
config_panel.rs1use 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#[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#[derive(Default)]
30pub struct ConfigPanel {
31 info: ConfigInfo,
32}
33
34impl ConfigPanel {
35 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}