Skip to main content

void/app/
keys.rs

1use super::*;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3
4impl App {
5    pub fn handle_key(&mut self, key: KeyEvent) {
6        self.last_activity = Instant::now();
7        if self.searching {
8            self.handle_search_key(key);
9            return;
10        }
11        if self.popup.is_some() {
12            self.handle_popup_key(key);
13            return;
14        }
15        if key.code == KeyCode::Esc && self.bulk_mode && self.tab == FocusTab::Tasks {
16            self.toggle_bulk_mode();
17            return;
18        }
19        if key.code == KeyCode::Esc && self.bulk_mode && self.tab == FocusTab::Tasks {
20            self.toggle_bulk_mode();
21            return;
22        }
23        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
24        match key.code {
25            KeyCode::Char('q') if self.subtask_focus && self.tab == FocusTab::Tasks => {
26                self.subtask_focus = false;
27                self.set_status("Task list focus", false);
28            }
29            KeyCode::Char('q') if self.bulk_mode && self.tab == FocusTab::Tasks => {
30                self.toggle_bulk_mode();
31            }
32            KeyCode::Char('q') => self.should_quit = true,
33            KeyCode::Esc => self.should_quit = true,
34            KeyCode::Char('c') if ctrl => self.should_quit = true,
35            KeyCode::Char('s') if ctrl => self.export_backup(),
36            KeyCode::Char('1') => self.tab = FocusTab::Dashboard,
37            KeyCode::Char('2') => self.tab = FocusTab::Tasks,
38            KeyCode::Char('3') => self.tab = FocusTab::Stats,
39            KeyCode::Char('4') => self.tab = FocusTab::Settings,
40            KeyCode::Char('5') | KeyCode::Char('h') => self.tab = FocusTab::Help,
41            KeyCode::Tab if self.tab == FocusTab::Tasks && self.selected_subtask_count() > 0 => {
42                self.toggle_subtask_focus();
43            }
44            KeyCode::Tab => self.next_tab(),
45            KeyCode::BackTab if self.tab == FocusTab::Tasks && self.subtask_focus => {
46                self.subtask_focus = false;
47                self.set_status("Task list focus", false);
48            }
49            KeyCode::BackTab => self.prev_tab(),
50            _ => match self.tab {
51                FocusTab::Dashboard => self.handle_dashboard_key(key),
52                FocusTab::Tasks => self.handle_tasks_key(key),
53                FocusTab::Stats => self.handle_stats_key(key),
54                FocusTab::Settings => self.handle_settings_key(key),
55                FocusTab::Help => {}
56            },
57        }
58    }
59
60    pub(crate) fn next_tab(&mut self) {
61        let cur = FocusTab::all()
62            .iter()
63            .position(|t| *t == self.tab)
64            .unwrap_or(0);
65        self.tab = FocusTab::all()[(cur + 1) % FocusTab::all().len()];
66    }
67
68    pub(crate) fn prev_tab(&mut self) {
69        let cur = FocusTab::all()
70            .iter()
71            .position(|t| *t == self.tab)
72            .unwrap_or(0);
73        let n = FocusTab::all().len();
74        self.tab = FocusTab::all()[(cur + n - 1) % n];
75    }
76
77    pub(crate) fn handle_dashboard_key(&mut self, key: KeyEvent) {
78        match key.code {
79            KeyCode::Char('s') | KeyCode::Char(' ') => self.toggle_timer(),
80            KeyCode::Char('p') => self.pause_timer(),
81            KeyCode::Char('r') => self.reset_timer(),
82            KeyCode::Char('n') => {
83                self.timer.skip();
84                self.on_timer_finished(true);
85            }
86            KeyCode::Char('m') => self.cycle_mode(),
87            KeyCode::Char('P') => self.cycle_timer_preset(),
88            KeyCode::Char('+') | KeyCode::Char('=') => self.adjust_minutes(1),
89            KeyCode::Char('-') | KeyCode::Char('_') => self.adjust_minutes(-1),
90            KeyCode::Char('a') => self.open_add_task(),
91            KeyCode::Char('f') => {
92                if let Some(id) = self.dashboard_selected_task_id() {
93                    self.set_active_task(Some(id));
94                    self.set_status("Task set as active.", false);
95                }
96            }
97            KeyCode::Char('g') => {
98                self.cycle_task_filter();
99            }
100            KeyCode::Char('t') => {
101                self.cycle_tag_filter();
102            }
103            KeyCode::Char('z') => {
104                self.zen_mode = !self.zen_mode;
105                self.set_status(
106                    format!("Zen mode {}.", if self.zen_mode { "on" } else { "off" }),
107                    false,
108                );
109            }
110            KeyCode::Down | KeyCode::Char('j') => self.move_dashboard_task_selection(1),
111            KeyCode::Up | KeyCode::Char('k') => self.move_dashboard_task_selection(-1),
112            KeyCode::Enter => {
113                if let Some(id) = self.dashboard_selected_task_id() {
114                    self.cycle_task_status_for(id, true);
115                    self.clamp_dashboard_task_selection();
116                } else {
117                    self.cycle_active_task_status();
118                }
119            }
120            KeyCode::Char('x') => {
121                if let Some(id) = self.dashboard_selected_task_id() {
122                    self.persist_data(|db, data| storage::mark_task_done(db, data, id));
123                    if self.active_task == Some(id) {
124                        self.active_task = None;
125                        self.data.active_task_id = None;
126                        self.persist(|db| db.persist_active_task(None));
127                        self.maybe_advance_task();
128                    }
129                    self.bump_data();
130                    self.clamp_dashboard_task_selection();
131                    self.check_queue_empty();
132                    self.set_status("Task marked done.", false);
133                } else {
134                    self.mark_active_task_done();
135                }
136            }
137            KeyCode::Char('e') | KeyCode::Char('E') => self.end_session(),
138            _ => {}
139        }
140    }
141
142    pub(crate) fn handle_stats_key(&mut self, key: KeyEvent) {
143        if self.recent_sessions.is_empty() {
144            if matches!(key.code, KeyCode::Char('e') | KeyCode::Char('E')) {
145                self.end_session();
146            }
147            return;
148        }
149        let n = self.recent_sessions.len();
150        match key.code {
151            KeyCode::Down | KeyCode::Char('j') => {
152                self.stats_session_selected = (self.stats_session_selected + 1) % n;
153            }
154            KeyCode::Up | KeyCode::Char('k') => {
155                self.stats_session_selected = if self.stats_session_selected == 0 {
156                    n - 1
157                } else {
158                    self.stats_session_selected - 1
159                };
160            }
161            KeyCode::Char('d') => {
162                let id = self.recent_sessions[self.stats_session_selected].id;
163                self.persist_data(|db, data| storage::delete_session(db, data, id));
164                self.bump_data();
165                self.set_status("Session deleted.", false);
166            }
167            KeyCode::Char('+') | KeyCode::Char('=') => {
168                let entry = &self.recent_sessions[self.stats_session_selected];
169                let new_mins = entry.record.minutes.saturating_add(5);
170                let id = entry.id;
171                self.persist_data(|db, data| {
172                    storage::adjust_session_minutes(db, data, id, new_mins)
173                });
174                self.bump_data();
175            }
176            KeyCode::Char('-') => {
177                let entry = &self.recent_sessions[self.stats_session_selected];
178                let new_mins = entry.record.minutes.saturating_sub(5).max(1);
179                let id = entry.id;
180                self.persist_data(|db, data| {
181                    storage::adjust_session_minutes(db, data, id, new_mins)
182                });
183                self.bump_data();
184            }
185            KeyCode::Char('e') | KeyCode::Char('E') => self.end_session(),
186            KeyCode::Char('[') if self.stats_session_page > 0 => {
187                self.stats_session_page -= 1;
188                self.stats_session_selected = 0;
189                self.refresh_recent_sessions();
190            }
191            KeyCode::Char(']') => {
192                let max_page = self.stats_session_total.saturating_sub(1) / App::SESSIONS_PER_PAGE;
193                if self.stats_session_page < max_page {
194                    self.stats_session_page += 1;
195                    self.stats_session_selected = 0;
196                    self.refresh_recent_sessions();
197                }
198            }
199            _ => {}
200        }
201    }
202
203    pub(crate) fn handle_search_key(&mut self, key: KeyEvent) {
204        match key.code {
205            KeyCode::Esc => {
206                self.searching = false;
207                self.task_search.clear();
208            }
209            KeyCode::Enter => {
210                self.searching = false;
211            }
212            KeyCode::Backspace => {
213                self.task_search.pop();
214            }
215            KeyCode::Char(c) => {
216                self.task_search.push(c);
217            }
218            _ => {}
219        }
220    }
221
222    pub(crate) fn handle_tasks_key(&mut self, key: KeyEvent) {
223        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
224        match key.code {
225            KeyCode::Char('f') => {
226                if let Some(id) = self.selected_task_id() {
227                    self.start_focus_on_task(id);
228                }
229            }
230            KeyCode::Char('g') => {
231                self.cycle_task_filter();
232            }
233            KeyCode::Char('/') => {
234                self.searching = true;
235                self.task_search.clear();
236            }
237            KeyCode::Char('t') => {
238                if let Some(id) = self.selected_task_id() {
239                    self.persist_data(|db, data| storage::toggle_today(db, data, id));
240                    self.bump_data();
241                }
242            }
243            KeyCode::Char('a') => self.open_add_task(),
244            KeyCode::Char('e') => self.open_edit_task(),
245            KeyCode::Char('d') => self.open_confirm_delete(),
246            KeyCode::Char('v') => {
247                if self.bulk_mode {
248                    self.toggle_bulk_item();
249                } else {
250                    self.toggle_bulk_mode();
251                }
252            }
253            KeyCode::Char('V') if self.bulk_mode => {
254                if self.bulk_selected.is_empty() {
255                    self.set_status("No tasks selected.", true);
256                } else {
257                    self.popup = Some(Popup::BulkConfirm(BulkAction::MarkDone));
258                }
259            }
260            KeyCode::Char('D') if self.bulk_mode => {
261                if self.bulk_selected.is_empty() {
262                    self.set_status("No tasks selected.", true);
263                } else {
264                    self.popup = Some(Popup::BulkConfirm(BulkAction::Delete));
265                }
266            }
267            KeyCode::Char('A') => self.archive_selected_task(),
268            KeyCode::Char('i') => {
269                if let Some(id) = self.selected_task_id() {
270                    let next = self
271                        .data
272                        .tasks
273                        .iter()
274                        .find(|t| t.id == id)
275                        .map(|t| t.recurrence.next())
276                        .unwrap_or(crate::model::TaskRecurrence::None);
277                    self.persist_data(|db, data| storage::set_task_recurrence(db, data, id, next));
278                    self.bump_data();
279                    self.set_status(format!("Recurrence: {}", next.label()), false);
280                }
281            }
282            KeyCode::Char('c') => self.open_add_subtask(),
283            KeyCode::Char('x') | KeyCode::Char('X') => self.toggle_subtask_on_selected(),
284            KeyCode::Char('-') | KeyCode::Char('_') => self.delete_subtask_on_selected(),
285            KeyCode::Enter if self.bulk_mode => self.toggle_bulk_item(),
286            KeyCode::Enter if self.subtask_focus => self.toggle_subtask_on_selected(),
287            KeyCode::Enter => {
288                if let Some(id) = self.selected_task_id() {
289                    self.cycle_task_status_for(id, false);
290                }
291            }
292            KeyCode::Char(' ') if !self.subtask_focus => {
293                if let Some(id) = self.selected_task_id() {
294                    self.set_active_task(Some(id));
295                    self.set_status("Task set as active for the timer.", false);
296                }
297            }
298            KeyCode::Char('1') => {
299                if let Some(id) = self.selected_task_id() {
300                    self.persist_data(|db, data| {
301                        storage::set_priority(db, data, id, Priority::Low)
302                    });
303                    self.bump_data();
304                }
305            }
306            KeyCode::Char('2') => {
307                if let Some(id) = self.selected_task_id() {
308                    self.persist_data(|db, data| {
309                        storage::set_priority(db, data, id, Priority::Medium)
310                    });
311                    self.bump_data();
312                }
313            }
314            KeyCode::Char('3') => {
315                if let Some(id) = self.selected_task_id() {
316                    self.persist_data(|db, data| {
317                        storage::set_priority(db, data, id, Priority::High)
318                    });
319                    self.bump_data();
320                }
321            }
322            KeyCode::Down | KeyCode::Char('j') if !ctrl && self.subtask_focus => {
323                self.move_subtask_selection(1);
324            }
325            KeyCode::Up | KeyCode::Char('k') if !ctrl && self.subtask_focus => {
326                self.move_subtask_selection(-1);
327            }
328            KeyCode::Down | KeyCode::Char('j') if !ctrl => self.move_task_selection(1),
329            KeyCode::Up | KeyCode::Char('k') if !ctrl => self.move_task_selection(-1),
330            KeyCode::Down | KeyCode::Char('j') if ctrl => {
331                if let Some(id) = self.selected_task_id() {
332                    self.reordering_task = Some(id);
333                    self.persist_data(|db, data| storage::move_task(db, data, id, 1));
334                    self.bump_data();
335                    self.reordering_task = None;
336                }
337            }
338            KeyCode::Up | KeyCode::Char('k') if ctrl => {
339                if let Some(id) = self.selected_task_id() {
340                    self.reordering_task = Some(id);
341                    self.persist_data(|db, data| storage::move_task(db, data, id, -1));
342                    self.bump_data();
343                    self.reordering_task = None;
344                }
345            }
346            KeyCode::PageDown => self.move_task_selection(8),
347            KeyCode::PageUp => self.move_task_selection(-8),
348            KeyCode::Home => {
349                let len = self.filtered_task_indices().len();
350                if len > 0 {
351                    self.task_state.select(Some(0));
352                }
353            }
354            KeyCode::End => {
355                let len = self.filtered_task_indices().len();
356                if len > 0 {
357                    self.task_state.select(Some(len - 1));
358                }
359            }
360            _ => {}
361        }
362    }
363
364    pub(crate) fn move_task_selection(&mut self, delta: i32) {
365        let len = self.filtered_task_indices().len();
366        if len == 0 {
367            return;
368        }
369        let cur = self.task_state.selected().unwrap_or(0) as i32;
370        let new = (cur + delta).clamp(0, len as i32 - 1) as usize;
371        self.task_state.select(Some(new));
372        self.subtask_focus = false;
373        self.reset_subtask_selection();
374    }
375}