kimun_notes/components/preferences/
display_section.rs1use ratatui::Frame;
2use ratatui::layout::Rect;
3use ratatui::style::Style;
4use ratatui::widgets::{Block, Borders, List, ListItem, ListState};
5
6use crate::components::Component;
7use crate::components::event_state::EventState;
8use crate::components::events::{AppTx, InputEvent};
9use crate::settings::themes::Theme;
10
11const ROW_COUNT: usize = 3;
13
14pub struct DisplaySection {
15 pub use_nerd_fonts: bool,
16 pub update_check: bool,
18 pub mouse: bool,
21 list_state: ListState,
22}
23
24impl DisplaySection {
25 pub fn new(use_nerd_fonts: bool, update_check: bool, mouse: bool) -> Self {
26 let mut list_state = ListState::default();
27 list_state.select(Some(0));
28 Self {
29 use_nerd_fonts,
30 update_check,
31 mouse,
32 list_state,
33 }
34 }
35
36 fn toggle_selected(&mut self) {
38 match self.list_state.selected() {
39 Some(0) => self.use_nerd_fonts = !self.use_nerd_fonts,
40 Some(1) => self.update_check = !self.update_check,
41 Some(2) => self.mouse = !self.mouse,
42 _ => {}
43 }
44 }
45
46 fn move_selection(&mut self, delta: isize) {
47 let current = self.list_state.selected().unwrap_or(0) as isize;
48 let next = (current + delta).rem_euclid(ROW_COUNT as isize);
49 self.list_state.select(Some(next as usize));
50 }
51}
52
53impl Component for DisplaySection {
54 fn handle_input(&mut self, event: &InputEvent, _tx: &AppTx) -> EventState {
55 let InputEvent::Key(key) = event else {
56 return EventState::NotConsumed;
57 };
58 use ratatui::crossterm::event::KeyCode;
59 match key.code {
60 KeyCode::Enter | KeyCode::Char(' ') => {
61 self.toggle_selected();
62 EventState::Consumed
63 }
64 KeyCode::Up | KeyCode::Char('k') => {
65 self.move_selection(-1);
66 EventState::Consumed
67 }
68 KeyCode::Down | KeyCode::Char('j') => {
69 self.move_selection(1);
70 EventState::Consumed
71 }
72 _ => EventState::NotConsumed,
73 }
74 }
75
76 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
77 let border_style = theme.border_style(focused);
78 let block = Block::default()
79 .title("Display")
80 .borders(Borders::ALL)
81 .border_style(border_style)
82 .style(theme.base_style());
83
84 let checkbox = |on: bool| if on { "[x]" } else { "[ ]" };
85 let fg = Style::default().fg(theme.fg.to_ratatui());
86 let items = vec![
87 ListItem::new(format!(
88 " Use Nerd Fonts {}",
89 checkbox(self.use_nerd_fonts)
90 ))
91 .style(fg),
92 ListItem::new(format!(
93 " Check for updates on startup {}",
94 checkbox(self.update_check)
95 ))
96 .style(fg),
97 ListItem::new(format!(
98 " Capture mouse (restart to apply) {}",
99 checkbox(self.mouse)
100 ))
101 .style(fg),
102 ];
103
104 let list = List::new(items)
105 .block(block)
106 .style(theme.base_style())
107 .highlight_style(
108 Style::default()
109 .fg(theme.selection_fg.to_ratatui())
110 .bg(theme.selection_bg.to_ratatui()),
111 );
112 f.render_stateful_widget(list, rect, &mut self.list_state);
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::components::events::InputEvent;
120 use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
121
122 fn key(code: KeyCode) -> InputEvent {
123 InputEvent::Key(KeyEvent {
124 code,
125 modifiers: KeyModifiers::NONE,
126 kind: KeyEventKind::Press,
127 state: KeyEventState::NONE,
128 })
129 }
130
131 #[test]
132 fn enter_toggles_nerd_fonts() {
133 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
134 let mut section = DisplaySection::new(true, true, true);
135 section.handle_input(&key(KeyCode::Enter), &tx);
136 assert!(!section.use_nerd_fonts);
137 section.handle_input(&key(KeyCode::Enter), &tx);
138 assert!(section.use_nerd_fonts);
139 }
140
141 #[test]
142 fn space_toggles_nerd_fonts() {
143 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
144 let mut section = DisplaySection::new(false, true, true);
145 section.handle_input(&key(KeyCode::Char(' ')), &tx);
146 assert!(section.use_nerd_fonts);
147 }
148
149 #[test]
150 fn down_then_toggle_flips_update_check_only() {
151 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
152 let mut section = DisplaySection::new(true, true, true);
153 section.handle_input(&key(KeyCode::Down), &tx);
154 section.handle_input(&key(KeyCode::Enter), &tx);
155 assert!(!section.update_check, "update_check should toggle off");
156 assert!(section.use_nerd_fonts, "nerd fonts should be untouched");
157 }
158
159 #[test]
160 fn down_twice_then_toggle_flips_mouse_only() {
161 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
162 let mut section = DisplaySection::new(true, true, true);
163 section.handle_input(&key(KeyCode::Down), &tx);
164 section.handle_input(&key(KeyCode::Down), &tx);
165 section.handle_input(&key(KeyCode::Enter), &tx);
166 assert!(!section.mouse, "mouse should toggle off");
167 assert!(section.use_nerd_fonts, "nerd fonts should be untouched");
168 assert!(section.update_check, "update_check should be untouched");
169 }
170
171 #[test]
172 fn renders_checked_when_enabled() {
173 use ratatui::Terminal;
174 use ratatui::backend::TestBackend;
175 let backend = TestBackend::new(40, 10);
176 let mut terminal = Terminal::new(backend).unwrap();
177 let mut section = DisplaySection::new(true, true, true);
178 let theme = Theme::gruvbox_dark();
179 terminal
180 .draw(|f| section.render(f, f.area(), &theme, false))
181 .unwrap();
182 let buf = terminal.backend().buffer().clone();
183 let flat: String = buf.content.iter().map(|c| c.symbol()).collect();
184 assert!(flat.contains("[x]"), "expected [x] when nerd fonts enabled");
185 }
186
187 #[test]
188 fn renders_unchecked_when_disabled() {
189 use ratatui::Terminal;
190 use ratatui::backend::TestBackend;
191 let backend = TestBackend::new(40, 10);
192 let mut terminal = Terminal::new(backend).unwrap();
193 let mut section = DisplaySection::new(false, true, true);
194 let theme = Theme::gruvbox_dark();
195 terminal
196 .draw(|f| section.render(f, f.area(), &theme, false))
197 .unwrap();
198 let buf = terminal.backend().buffer().clone();
199 let flat: String = buf.content.iter().map(|c| c.symbol()).collect();
200 assert!(
201 flat.contains("[ ]"),
202 "expected [ ] when nerd fonts disabled"
203 );
204 }
205}