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