Skip to main content

kimun_notes/components/preferences/
display_section.rs

1use 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
11/// Number of selectable rows in this section.
12const ROW_COUNT: usize = 2;
13
14pub struct DisplaySection {
15    pub use_nerd_fonts: bool,
16    /// Whether kimün checks GitHub for a newer release on startup.
17    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    /// Toggle the currently selected row.
33    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}