Skip to main content

kimun_notes/components/preferences/
indexing_section.rs

1use ratatui::Frame;
2use ratatui::layout::{Constraint, Direction, Layout, Rect};
3use ratatui::style::{Modifier, Style};
4use ratatui::widgets::{Block, Borders, Paragraph};
5
6use crate::components::Component;
7use crate::components::event_state::EventState;
8use crate::components::events::{AppEvent, AppTx, InputEvent};
9use crate::settings::themes::Theme;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum IndexAction {
13    Fast,
14    Full,
15}
16
17pub struct IndexingSection {
18    pub selected: IndexAction,
19    vault_available: bool,
20}
21
22impl IndexingSection {
23    pub fn new(vault_available: bool) -> Self {
24        Self {
25            selected: IndexAction::Fast,
26            vault_available,
27        }
28    }
29
30    pub fn set_vault_available(&mut self, available: bool) {
31        self.vault_available = available;
32    }
33}
34
35impl Component for IndexingSection {
36    fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
37        if !self.vault_available {
38            return EventState::NotConsumed;
39        }
40        let InputEvent::Key(key) = event else {
41            return EventState::NotConsumed;
42        };
43        match key.code {
44            ratatui::crossterm::event::KeyCode::Right
45            | ratatui::crossterm::event::KeyCode::Char('l') => {
46                self.selected = IndexAction::Full;
47                EventState::Consumed
48            }
49            ratatui::crossterm::event::KeyCode::Left
50            | ratatui::crossterm::event::KeyCode::Char('h') => {
51                self.selected = IndexAction::Fast;
52                EventState::Consumed
53            }
54            ratatui::crossterm::event::KeyCode::Enter => {
55                let msg = match self.selected {
56                    IndexAction::Fast => AppEvent::TriggerFastReindex,
57                    IndexAction::Full => AppEvent::TriggerFullReindex,
58                };
59                tx.send(msg).ok();
60                EventState::Consumed
61            }
62            _ => EventState::NotConsumed,
63        }
64    }
65
66    fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
67        let border_style = theme.border_style(focused);
68        let block = Block::default()
69            .title("Reindex")
70            .borders(Borders::ALL)
71            .border_style(border_style)
72            .style(theme.base_style());
73        let inner = block.inner(rect);
74        f.render_widget(block, rect);
75
76        let fast_label = if self.selected == IndexAction::Fast {
77            "[ Fast Reindex ]"
78        } else {
79            "  Fast Reindex  "
80        };
81        let full_label = if self.selected == IndexAction::Full {
82            "[ Full Reindex ]"
83        } else {
84            "  Full Reindex  "
85        };
86        let dim = if self.vault_available {
87            Style::default()
88                .fg(theme.fg.to_ratatui())
89                .bg(theme.bg.to_ratatui())
90        } else {
91            Style::default()
92                .fg(theme.fg.to_ratatui())
93                .bg(theme.bg.to_ratatui())
94                .add_modifier(Modifier::DIM)
95        };
96
97        let cols = Layout::default()
98            .direction(Direction::Horizontal)
99            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
100            .split(inner);
101        f.render_widget(Paragraph::new(fast_label).style(dim), cols[0]);
102        f.render_widget(Paragraph::new(full_label).style(dim), cols[1]);
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::components::events::AppEvent;
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 not_consumed_when_vault_unavailable() {
123        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
124        let mut section = IndexingSection::new(false);
125        let enter_result = section.handle_input(&key(KeyCode::Enter), &tx);
126        assert!(matches!(
127            enter_result,
128            crate::components::event_state::EventState::NotConsumed
129        ));
130        let right_result = section.handle_input(&key(KeyCode::Right), &tx);
131        assert!(matches!(
132            right_result,
133            crate::components::event_state::EventState::NotConsumed
134        ));
135        let left_result = section.handle_input(&key(KeyCode::Left), &tx);
136        assert!(matches!(
137            left_result,
138            crate::components::event_state::EventState::NotConsumed
139        ));
140        let l_result = section.handle_input(&key(KeyCode::Char('l')), &tx);
141        assert!(matches!(
142            l_result,
143            crate::components::event_state::EventState::NotConsumed
144        ));
145        let h_result = section.handle_input(&key(KeyCode::Char('h')), &tx);
146        assert!(matches!(
147            h_result,
148            crate::components::event_state::EventState::NotConsumed
149        ));
150        assert!(
151            rx.try_recv().is_err(),
152            "No messages should be sent when vault_available == false"
153        );
154    }
155
156    #[test]
157    fn set_vault_available_enables_keys() {
158        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
159        let mut section = IndexingSection::new(false);
160        section.handle_input(&key(KeyCode::Enter), &tx);
161        assert!(
162            rx.try_recv().is_err(),
163            "Enter should be blocked when unavailable"
164        );
165        section.set_vault_available(true);
166        section.handle_input(&key(KeyCode::Enter), &tx);
167        let msg = rx
168            .try_recv()
169            .expect("Enter should send message after enabling");
170        assert!(matches!(msg, AppEvent::TriggerFastReindex));
171    }
172
173    #[test]
174    fn right_cycles_fast_to_full() {
175        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
176        let mut section = IndexingSection::new(true);
177        assert_eq!(section.selected, IndexAction::Fast);
178        section.handle_input(&key(KeyCode::Right), &tx);
179        assert_eq!(section.selected, IndexAction::Full);
180    }
181
182    #[test]
183    fn left_cycles_full_to_fast() {
184        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
185        let mut section = IndexingSection::new(true);
186        section.handle_input(&key(KeyCode::Right), &tx);
187        section.handle_input(&key(KeyCode::Left), &tx);
188        assert_eq!(section.selected, IndexAction::Fast);
189    }
190
191    #[test]
192    fn enter_on_fast_sends_trigger_fast_reindex() {
193        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
194        let mut section = IndexingSection::new(true);
195        section.handle_input(&key(KeyCode::Enter), &tx);
196        let msg = rx.try_recv().expect("message should be sent");
197        assert!(matches!(msg, AppEvent::TriggerFastReindex));
198    }
199
200    #[test]
201    fn enter_on_full_sends_trigger_full_reindex() {
202        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
203        let mut section = IndexingSection::new(true);
204        section.handle_input(&key(KeyCode::Right), &tx);
205        assert!(rx.try_recv().is_err(), "Right should not send any message");
206        section.handle_input(&key(KeyCode::Enter), &tx);
207        let msg = rx.try_recv().expect("message should be sent");
208        assert!(matches!(msg, AppEvent::TriggerFullReindex));
209    }
210
211    #[test]
212    fn right_is_idempotent_when_already_full() {
213        let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
214        let mut section = IndexingSection::new(true);
215        section.handle_input(&key(KeyCode::Right), &tx);
216        section.handle_input(&key(KeyCode::Right), &tx);
217        assert_eq!(section.selected, IndexAction::Full);
218    }
219}