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}