Skip to main content

void/app/
popups.rs

1use super::*;
2use crate::model::Priority;
3use crossterm::event::{KeyCode, KeyEvent};
4
5impl App {
6    pub fn close_popup(&mut self) {
7        self.popup = None;
8        self.input_mode = InputMode::Normal;
9        self.input_buffer.clear();
10        self.input_due_date.clear();
11        self.input_tags.clear();
12    }
13
14    fn preserved_task_notes(&self, id: u64) -> String {
15        self.data
16            .tasks
17            .iter()
18            .find(|t| t.id == id)
19            .map(|t| t.notes.clone())
20            .unwrap_or_default()
21    }
22
23    pub fn submit_popup(&mut self) {
24        match self.popup.clone() {
25            Some(Popup::AddTask) => {
26                let title = self.input_buffer.trim().to_string();
27                if title.is_empty() {
28                    self.set_status("Title cannot be empty.", true);
29                    return;
30                }
31                let due_date = match self.popup_due_date() {
32                    Ok(d) => d,
33                    Err(msg) => {
34                        self.set_status(msg, true);
35                        return;
36                    }
37                };
38                let tags = self.popup_tags();
39                if let Err(e) = storage::add_task_full(
40                    &self.db,
41                    &mut self.data,
42                    storage::TaskPayload {
43                        title,
44                        notes: String::new(),
45                        estimated_minutes: self.input_number,
46                        priority: self.input_priority,
47                        tags,
48                        due_date,
49                    },
50                ) {
51                    self.set_status(format!("Save error: {e}"), true);
52                    return;
53                }
54                self.bump_data();
55                let indices = self.filtered_task_indices();
56                let sel = indices.len().saturating_sub(1);
57                self.task_state
58                    .select(if indices.is_empty() { None } else { Some(sel) });
59                self.close_popup();
60                self.set_status("Task added.", false);
61            }
62            Some(Popup::EditTask(id)) => {
63                let title = self.input_buffer.trim().to_string();
64                if title.is_empty() {
65                    self.set_status("Title cannot be empty.", true);
66                    return;
67                }
68                let due_date = match self.popup_due_date() {
69                    Ok(d) => d,
70                    Err(msg) => {
71                        self.set_status(msg, true);
72                        return;
73                    }
74                };
75                let tags = self.popup_tags();
76                let estimate = self.input_number.clamp(1, 480);
77                let priority = self.input_priority;
78                let notes = self.preserved_task_notes(id);
79                if let Err(e) = storage::update_task(
80                    &self.db,
81                    &mut self.data,
82                    id,
83                    storage::TaskPayload {
84                        title,
85                        notes,
86                        estimated_minutes: estimate,
87                        priority,
88                        tags,
89                        due_date,
90                    },
91                ) {
92                    self.set_status(format!("Save error: {e}"), true);
93                    return;
94                }
95                self.bump_data();
96                self.close_popup();
97                self.set_status("Task updated.", false);
98            }
99            Some(Popup::ConfirmDelete(id)) => {
100                match storage::delete_task(&self.db, &mut self.data, id) {
101                    Ok(true) => {
102                        if self.active_task == Some(id) {
103                            self.set_active_task(None);
104                        }
105                        self.bump_data();
106                        self.clamp_task_selection_after_mutation();
107                        self.set_status("Task deleted.", false);
108                        self.check_queue_empty();
109                    }
110                    Ok(false) => {}
111                    Err(e) => self.set_status(format!("Delete error: {e}"), true),
112                }
113                self.close_popup();
114            }
115            Some(Popup::EmptyQueueChoice) => {}
116            Some(Popup::AddSubtask(id)) => {
117                let title = self.input_buffer.trim().to_string();
118                if title.is_empty() {
119                    self.set_status("Subtask title cannot be empty.", true);
120                    return;
121                }
122                if let Err(e) = storage::add_subtask(&self.db, &mut self.data, id, title.clone()) {
123                    self.set_status(format!("Save error: {e}"), true);
124                    return;
125                }
126                self.bump_data();
127                if let Some(t) = self.data.tasks.iter().find(|t| t.id == id) {
128                    self.subtask_selected = t.subtasks.len().saturating_sub(1);
129                }
130                self.input_buffer.clear();
131                self.subtask_focus = true;
132                self.sync_subtask_list();
133                self.set_status(
134                    format!("Added \"{title}\" — type another or q to close"),
135                    false,
136                );
137            }
138            Some(Popup::BulkConfirm(action)) => {
139                let ids: Vec<u64> = self.bulk_selected.iter().copied().collect();
140                let result = match action {
141                    BulkAction::MarkDone => storage::bulk_mark_done(&self.db, &mut self.data, &ids),
142                    BulkAction::Delete => storage::bulk_delete(&self.db, &mut self.data, &ids),
143                };
144                match result {
145                    Ok(n) => {
146                        self.bulk_selected.clear();
147                        self.bulk_mode = false;
148                        self.bump_data();
149                        self.clamp_task_selection_after_mutation();
150                        self.set_status(format!("Bulk action applied to {n} tasks."), false);
151                    }
152                    Err(e) => self.set_status(format!("Bulk error: {e}"), true),
153                }
154                self.close_popup();
155            }
156            None => {}
157        }
158    }
159
160    pub fn confirm_delete(&mut self) {
161        if let Some(Popup::ConfirmDelete(id)) = self.popup.clone() {
162            match storage::delete_task(&self.db, &mut self.data, id) {
163                Ok(true) => {
164                    if self.active_task == Some(id) {
165                        self.set_active_task(None);
166                    }
167                    self.bump_data();
168                    self.clamp_task_selection_after_mutation();
169                    self.set_status("Task deleted.", false);
170                    self.check_queue_empty();
171                }
172                Ok(false) => {}
173                Err(e) => self.set_status(format!("Delete error: {e}"), true),
174            }
175            self.close_popup();
176        }
177    }
178
179    pub(crate) fn handle_popup_key(&mut self, key: KeyEvent) {
180        if matches!(self.popup, Some(Popup::EmptyQueueChoice)) {
181            match key.code {
182                KeyCode::Esc => self.close_popup(),
183                KeyCode::Enter | KeyCode::Char('y') | KeyCode::Char('Y') => {
184                    self.data.empty_queue_behavior = EmptyQueueBehavior::FreeFocus;
185                    self.close_popup();
186                    self.set_status(
187                        "All tasks done — free focus. Sessions log as general focus.",
188                        false,
189                    );
190                }
191                KeyCode::Char('p') | KeyCode::Char('P') => {
192                    self.data.empty_queue_behavior = EmptyQueueBehavior::PauseTimer;
193                    self.close_popup();
194                    if self.timer.state == TimerState::Running {
195                        self.pause_timer();
196                    } else {
197                        self.timer.reset();
198                    }
199                    self.set_status("All tasks done — timer paused.", false);
200                }
201                KeyCode::Char('a') | KeyCode::Char('A') => {
202                    self.close_popup();
203                    self.open_add_task();
204                }
205                _ => {}
206            }
207            return;
208        }
209        if matches!(self.popup, Some(Popup::ConfirmDelete(_))) {
210            match key.code {
211                KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => {
212                    self.close_popup();
213                }
214                KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
215                    self.confirm_delete();
216                }
217                _ => {}
218            }
219            return;
220        }
221        if matches!(self.popup, Some(Popup::BulkConfirm(_))) {
222            match key.code {
223                KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => self.close_popup(),
224                KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => self.submit_popup(),
225                _ => {}
226            }
227            return;
228        }
229        if matches!(self.popup, Some(Popup::AddSubtask(_))) {
230            let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
231            match key.code {
232                KeyCode::Esc | KeyCode::Char('q') => self.close_popup(),
233                KeyCode::Enter => self.submit_popup(),
234                KeyCode::Backspace => {
235                    self.input_buffer.pop();
236                }
237                KeyCode::Char(c) if !ctrl => {
238                    self.input_buffer.push(c);
239                }
240                _ => {}
241            }
242            return;
243        }
244        let is_text_field = matches!(
245            self.input_field,
246            InputField::Title | InputField::DueDate | InputField::Tags
247        );
248        match key.code {
249            KeyCode::Esc => {
250                self.close_popup();
251            }
252            KeyCode::Tab | KeyCode::BackTab => {
253                let order = [
254                    InputField::Title,
255                    InputField::Estimate,
256                    InputField::Priority,
257                    InputField::DueDate,
258                    InputField::Tags,
259                ];
260                let idx = order
261                    .iter()
262                    .position(|f| *f == self.input_field)
263                    .unwrap_or(0);
264                let next = if key.code == KeyCode::Tab {
265                    (idx + 1) % order.len()
266                } else {
267                    (idx + order.len() - 1) % order.len()
268                };
269                self.input_field = order[next];
270            }
271            KeyCode::Enter => {
272                self.submit_popup();
273            }
274            _ => {
275                if is_text_field {
276                    self.handle_text_input(key);
277                } else {
278                    self.handle_field_input(key);
279                }
280            }
281        }
282    }
283
284    pub(crate) fn handle_text_input(&mut self, key: KeyEvent) {
285        if self.input_field == InputField::DueDate {
286            match key.code {
287                KeyCode::Left => {
288                    self.calendar_date -= chrono::Duration::days(1);
289                    self.input_due_date = self.calendar_date.format("%Y-%m-%d").to_string();
290                    return;
291                }
292                KeyCode::Right => {
293                    self.calendar_date += chrono::Duration::days(1);
294                    self.input_due_date = self.calendar_date.format("%Y-%m-%d").to_string();
295                    return;
296                }
297                KeyCode::Up => {
298                    self.calendar_date -= chrono::Duration::days(7);
299                    self.input_due_date = self.calendar_date.format("%Y-%m-%d").to_string();
300                    return;
301                }
302                KeyCode::Down => {
303                    self.calendar_date += chrono::Duration::days(7);
304                    self.input_due_date = self.calendar_date.format("%Y-%m-%d").to_string();
305                    return;
306                }
307                _ => {}
308            }
309        }
310        let buf = match self.input_field {
311            InputField::Title => &mut self.input_buffer,
312            InputField::DueDate => &mut self.input_due_date,
313            InputField::Tags => &mut self.input_tags,
314            _ => return,
315        };
316        match key.code {
317            KeyCode::Backspace => {
318                buf.pop();
319            }
320            KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
321                buf.push(c);
322            }
323            _ => {}
324        }
325    }
326
327    pub(crate) fn handle_field_input(&mut self, key: KeyEvent) {
328        match self.input_field {
329            InputField::Estimate => match key.code {
330                KeyCode::Char(c) if c.is_ascii_digit() => {
331                    let d = c.to_digit(10).unwrap_or(0);
332                    self.input_number = (self.input_number.saturating_mul(10) + d).min(480);
333                }
334                KeyCode::Backspace => {
335                    self.input_number /= 10;
336                    if self.input_number == 0 {
337                        self.input_number = 1;
338                    }
339                }
340                KeyCode::Up => self.input_number = (self.input_number + 5).min(480),
341                KeyCode::Down => self.input_number = self.input_number.saturating_sub(5).max(1),
342                _ => {}
343            },
344            InputField::Priority => {
345                let next = match key.code {
346                    KeyCode::Right | KeyCode::Up | KeyCode::Char(' ') => {
347                        match self.input_priority {
348                            Priority::Low => Priority::Medium,
349                            Priority::Medium => Priority::High,
350                            Priority::High => Priority::Low,
351                        }
352                    }
353                    KeyCode::Left | KeyCode::Down => match self.input_priority {
354                        Priority::Low => Priority::High,
355                        Priority::High => Priority::Medium,
356                        Priority::Medium => Priority::Low,
357                    },
358                    KeyCode::Char('1') => Priority::Low,
359                    KeyCode::Char('2') => Priority::Medium,
360                    KeyCode::Char('3') => Priority::High,
361                    _ => self.input_priority,
362                };
363                self.input_priority = next;
364            }
365            _ => {}
366        }
367    }
368}