kimun_notes/components/settings/
indexing_section.rs1use 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}