Skip to main content

kimun_notes/components/settings/
editor_section.rs

1use ratatui::Frame;
2use ratatui::layout::{Constraint, Direction, Layout, Rect};
3use ratatui::widgets::{Block, Borders, Paragraph};
4
5use crate::components::Component;
6use crate::components::event_state::EventState;
7use crate::components::events::{AppTx, InputEvent};
8use crate::settings::themes::Theme;
9
10const MIN_AUTOSAVE_SECS: u64 = 5;
11const MAX_AUTOSAVE_SECS: u64 = 300;
12const STEP: u64 = 5;
13
14pub struct EditorSection {
15    pub autosave_interval_secs: u64,
16}
17
18impl EditorSection {
19    pub fn new(autosave_interval_secs: u64) -> Self {
20        Self {
21            autosave_interval_secs,
22        }
23    }
24}
25
26impl Component for EditorSection {
27    fn handle_input(&mut self, event: &InputEvent, _tx: &AppTx) -> EventState {
28        let InputEvent::Key(key) = event else {
29            return EventState::NotConsumed;
30        };
31        match key.code {
32            ratatui::crossterm::event::KeyCode::Left
33            | ratatui::crossterm::event::KeyCode::Char('h') => {
34                self.autosave_interval_secs = self
35                    .autosave_interval_secs
36                    .saturating_sub(STEP)
37                    .max(MIN_AUTOSAVE_SECS);
38                EventState::Consumed
39            }
40            ratatui::crossterm::event::KeyCode::Right
41            | ratatui::crossterm::event::KeyCode::Char('l') => {
42                self.autosave_interval_secs =
43                    (self.autosave_interval_secs + STEP).min(MAX_AUTOSAVE_SECS);
44                EventState::Consumed
45            }
46            _ => EventState::NotConsumed,
47        }
48    }
49
50    fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
51        let border_style = theme.border_style(focused);
52        let block = Block::default()
53            .title("Editor")
54            .borders(Borders::ALL)
55            .border_style(border_style)
56            .style(theme.base_style());
57        let inner = block.inner(rect);
58        f.render_widget(block, rect);
59
60        let rows = Layout::default()
61            .direction(Direction::Vertical)
62            .constraints([
63                Constraint::Length(1),
64                Constraint::Length(1),
65                Constraint::Min(0),
66            ])
67            .split(inner);
68
69        let label = Paragraph::new("Autosave Interval").style(theme.base_style());
70        f.render_widget(label, rows[0]);
71
72        let value = format!("  ◀  {}s  ▶   (←/→ to change)", self.autosave_interval_secs);
73        let value_style = if focused {
74            ratatui::style::Style::default()
75                .fg(theme.accent.to_ratatui())
76                .bg(theme.bg.to_ratatui())
77        } else {
78            ratatui::style::Style::default()
79                .fg(theme.fg.to_ratatui())
80                .bg(theme.bg.to_ratatui())
81        };
82        f.render_widget(Paragraph::new(value).style(value_style), rows[1]);
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
90
91    fn key(code: KeyCode) -> InputEvent {
92        InputEvent::Key(KeyEvent {
93            code,
94            modifiers: KeyModifiers::NONE,
95            kind: KeyEventKind::Press,
96            state: KeyEventState::NONE,
97        })
98    }
99
100    #[test]
101    fn right_increases_interval_by_step() {
102        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
103        let mut section = EditorSection::new(10);
104        section.handle_input(&key(KeyCode::Right), &tx);
105        assert_eq!(section.autosave_interval_secs, 15);
106    }
107
108    #[test]
109    fn left_decreases_interval_by_step() {
110        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
111        let mut section = EditorSection::new(10);
112        section.handle_input(&key(KeyCode::Left), &tx);
113        assert_eq!(section.autosave_interval_secs, 5);
114    }
115
116    #[test]
117    fn left_clamps_at_min() {
118        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
119        let mut section = EditorSection::new(5);
120        section.handle_input(&key(KeyCode::Left), &tx);
121        assert_eq!(section.autosave_interval_secs, MIN_AUTOSAVE_SECS);
122    }
123
124    #[test]
125    fn right_clamps_at_max() {
126        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
127        let mut section = EditorSection::new(298);
128        section.handle_input(&key(KeyCode::Right), &tx);
129        assert_eq!(section.autosave_interval_secs, MAX_AUTOSAVE_SECS);
130    }
131
132    #[test]
133    fn l_key_increases_interval() {
134        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
135        let mut section = EditorSection::new(10);
136        section.handle_input(&key(KeyCode::Char('l')), &tx);
137        assert_eq!(section.autosave_interval_secs, 15);
138    }
139
140    #[test]
141    fn h_key_decreases_interval() {
142        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
143        let mut section = EditorSection::new(10);
144        section.handle_input(&key(KeyCode::Char('h')), &tx);
145        assert_eq!(section.autosave_interval_secs, 5);
146    }
147}