Skip to main content

void/app/
task_ops.rs

1use super::*;
2use crate::storage;
3
4impl App {
5    pub fn pending_task_ids(&self) -> Vec<u64> {
6        storage::sorted_pending_tasks(&self.data)
7            .into_iter()
8            .map(|t| t.id)
9            .collect()
10    }
11
12    pub fn dashboard_selected_task_id(&self) -> Option<u64> {
13        let pending = self.dashboard_tasks();
14        if pending.is_empty() {
15            None
16        } else {
17            let idx = self.dashboard_task_selected.min(pending.len() - 1);
18            Some(pending[idx].id)
19        }
20    }
21
22    pub(crate) fn clamp_dashboard_task_selection(&mut self) {
23        let n = self.dashboard_tasks().len();
24        if n == 0 {
25            self.dashboard_task_selected = 0;
26        } else if self.dashboard_task_selected >= n {
27            self.dashboard_task_selected = n - 1;
28        }
29    }
30
31    pub(crate) fn clamp_task_selection_after_mutation(&mut self) {
32        let len = self.filtered_task_indices().len();
33        if len == 0 {
34            self.task_state.select(None);
35        } else {
36            let sel = self.task_state.selected().unwrap_or(0).min(len - 1);
37            self.task_state.select(Some(sel));
38        }
39    }
40
41    pub(crate) fn move_dashboard_task_selection(&mut self, delta: i32) {
42        let count = self.dashboard_tasks().len();
43        if count == 0 {
44            return;
45        }
46        let cur = self.dashboard_task_selected as i32;
47        let next = (cur + delta).rem_euclid(count as i32) as usize;
48        self.dashboard_task_selected = next;
49    }
50
51    pub fn pending_task_count(&self) -> u32 {
52        storage::pending_tasks(&self.data).count() as u32
53    }
54
55    pub fn active_task_pending_index(&self) -> Option<u32> {
56        let id = self.active_task?;
57        storage::sorted_pending_tasks(&self.data)
58            .iter()
59            .position(|t| t.id == id)
60            .map(|i| i as u32)
61    }
62
63    pub fn active_task_progress(&self) -> Option<f64> {
64        let id = self.active_task?;
65        let task = self.data.tasks.iter().find(|t| t.id == id)?;
66        Some(task.progress_ratio())
67    }
68
69    pub(crate) fn matches_filter(&self, t: &crate::model::Task) -> bool {
70        if !self.task_search.is_empty() {
71            let q = self.task_search.to_lowercase();
72            let title_match = t.title.to_lowercase().contains(&q);
73            let tags_match = t.tags.iter().any(|tag| tag.to_lowercase().contains(&q));
74            if !title_match && !tags_match {
75                return false;
76            }
77        }
78        if let Some(ref tag) = self.active_tag_filter {
79            if !t.tags.iter().any(|t| t == tag) {
80                return false;
81            }
82        }
83        match self.task_filter {
84            TaskFilter::All => !t.archived,
85            TaskFilter::Pending => t.status != crate::model::TaskStatus::Done && !t.archived,
86            TaskFilter::Done => t.status == crate::model::TaskStatus::Done && !t.archived,
87            TaskFilter::Today => {
88                t.today && t.status != crate::model::TaskStatus::Done && !t.archived
89            }
90            TaskFilter::Archived => t.archived,
91        }
92    }
93
94    pub fn filtered_task_indices(&self) -> Vec<usize> {
95        self.data
96            .tasks
97            .iter()
98            .enumerate()
99            .filter(|(_, t)| self.matches_filter(t))
100            .map(|(i, _)| i)
101            .collect()
102    }
103
104    pub fn dashboard_tasks(&self) -> Vec<&crate::model::Task> {
105        let mut tasks: Vec<_> = self
106            .filtered_task_indices()
107            .into_iter()
108            .map(|i| &self.data.tasks[i])
109            .collect();
110        tasks.sort_by(|a, b| {
111            b.priority
112                .rank()
113                .cmp(&a.priority.rank())
114                .then(b.today.cmp(&a.today))
115                .then(a.sort_order.cmp(&b.sort_order))
116        });
117        tasks
118    }
119
120    pub fn set_active_task(&mut self, id: Option<u64>) {
121        if let Some(id) = id {
122            if self
123                .data
124                .tasks
125                .iter()
126                .find(|t| t.id == id)
127                .is_some_and(|t| t.status == crate::model::TaskStatus::Done)
128            {
129                self.set_status("That task is done — pick another.", true);
130                return;
131            }
132            if self
133                .data
134                .tasks
135                .iter()
136                .find(|t| t.id == id)
137                .is_some_and(|t| t.is_blocked(&self.data.tasks))
138            {
139                self.set_status("Task is blocked — complete dependencies first.", true);
140                return;
141            }
142            self.persist_data(|db, data| storage::promote_task_on_activate(db, data, id));
143        }
144        self.active_task = id;
145        self.data.active_task_id = id;
146        self.persist(|db| db.persist_active_task(id));
147    }
148
149    pub fn cycle_active_task_status(&mut self) {
150        let Some(id) = self.active_task else {
151            self.set_status("No active task — set one on Tasks (Space).", true);
152            return;
153        };
154        self.cycle_task_status_for(id, false);
155    }
156
157    pub fn cycle_task_status_for(&mut self, id: u64, set_active: bool) {
158        if set_active {
159            self.set_active_task(Some(id));
160        }
161        self.persist_data(|db, data| storage::cycle_task_status(db, data, id));
162        let status = self
163            .data
164            .tasks
165            .iter()
166            .find(|t| t.id == id)
167            .map(|t| t.status);
168        let Some(status) = status else {
169            return;
170        };
171        if status == crate::model::TaskStatus::Done {
172            if self.active_task == Some(id) {
173                self.active_task = None;
174                self.data.active_task_id = None;
175                self.persist(|db| db.persist_active_task(None));
176            }
177            self.maybe_advance_task();
178        }
179        self.bump_data();
180        self.set_status(format!("Task status: {}", status.label()), false);
181        if status == crate::model::TaskStatus::Done {
182            self.check_queue_empty();
183        }
184    }
185
186    pub fn mark_active_task_done(&mut self) {
187        let Some(id) = self.active_task else {
188            self.set_status("No active task — set one on Tasks (Space).", true);
189            return;
190        };
191        self.persist_data(|db, data| storage::mark_task_done(db, data, id));
192        self.active_task = None;
193        self.data.active_task_id = None;
194        self.persist(|db| db.persist_active_task(None));
195        self.bump_data();
196        self.maybe_advance_task();
197        self.set_status("Task marked done.", false);
198        self.check_queue_empty();
199    }
200
201    pub(crate) fn auto_pick_task_if_needed(&mut self) {
202        if self.active_task.is_some() || !self.data.auto_pick_task {
203            return;
204        }
205        if let Some(id) = storage::pick_best_task(&self.data) {
206            self.set_active_task(Some(id));
207        }
208    }
209
210    pub(crate) fn maybe_advance_task(&mut self) {
211        if !self.data.auto_advance_task {
212            return;
213        }
214        let next = storage::advance_to_next_task(&self.data, self.active_task);
215        self.set_active_task(next);
216        if let Some(id) = next {
217            if let Some(t) = self.data.tasks.iter().find(|t| t.id == id) {
218                self.set_status(format!("Next task: {}", t.title), false);
219            }
220        }
221    }
222
223    pub fn start_focus_on_task(&mut self, id: u64) {
224        if self
225            .data
226            .tasks
227            .iter()
228            .find(|t| t.id == id)
229            .is_some_and(|t| t.status == crate::model::TaskStatus::Done)
230        {
231            self.set_status("That task is done — pick another.", true);
232            return;
233        }
234        self.set_active_task(Some(id));
235        self.tab = FocusTab::Dashboard;
236        if self.timer.mode != TimerMode::Focus {
237            self.timer.configure(TimerMode::Focus);
238        }
239        self.start_timer();
240    }
241
242    pub fn cycle_task_filter(&mut self) {
243        self.task_filter = self.task_filter.next();
244        self.task_state.select(Some(0));
245        self.set_status(format!("Filter: {}", self.task_filter.label()), false);
246    }
247
248    pub fn toggle_bulk_mode(&mut self) {
249        self.bulk_mode = !self.bulk_mode;
250        self.bulk_selected.clear();
251        if self.bulk_mode {
252            if let Some(id) = self.selected_task_id() {
253                self.bulk_selected.insert(id);
254            }
255        }
256        self.set_status(
257            format!(
258                "Bulk select {} — [v]/Enter toggle row, [V] done, [D] delete, [q] exit",
259                if self.bulk_mode { "on" } else { "off" }
260            ),
261            false,
262        );
263    }
264
265    pub fn toggle_bulk_item(&mut self) {
266        let Some(id) = self.selected_task_id() else {
267            return;
268        };
269        if self.bulk_selected.contains(&id) {
270            self.bulk_selected.remove(&id);
271        } else {
272            self.bulk_selected.insert(id);
273        }
274        self.set_status(
275            format!("{} task(s) selected", self.bulk_selected.len()),
276            false,
277        );
278    }
279
280    pub fn clamp_subtask_selection(&mut self) {
281        let Some(id) = self.selected_task_id() else {
282            self.subtask_selected = 0;
283            self.subtask_state.select(None);
284            return;
285        };
286        let n = self
287            .data
288            .tasks
289            .iter()
290            .find(|t| t.id == id)
291            .map(|t| t.subtasks.len())
292            .unwrap_or(0);
293        if n == 0 {
294            self.subtask_selected = 0;
295            self.subtask_focus = false;
296            self.subtask_state.select(None);
297        } else if self.subtask_selected >= n {
298            self.subtask_selected = n - 1;
299            self.subtask_state.select(Some(self.subtask_selected));
300        } else {
301            self.subtask_state.select(Some(self.subtask_selected));
302        }
303    }
304
305    pub fn sync_subtask_list(&mut self) {
306        self.clamp_subtask_selection();
307    }
308
309    pub fn selected_subtask_count(&self) -> usize {
310        self.selected_task_id()
311            .and_then(|id| {
312                self.data
313                    .tasks
314                    .iter()
315                    .find(|t| t.id == id)
316                    .map(|t| t.subtasks.len())
317            })
318            .unwrap_or(0)
319    }
320
321    pub fn toggle_subtask_focus(&mut self) {
322        if self.selected_subtask_count() == 0 {
323            self.set_status("No subtasks — press [c] to add.", true);
324            return;
325        }
326        self.subtask_focus = !self.subtask_focus;
327        if self.subtask_focus {
328            self.reset_subtask_selection();
329            self.sync_subtask_list();
330            self.set_status(
331                "Subtask focus — j/k or ↑/↓ navigate · x/Enter toggle · - remove · q back",
332                false,
333            );
334        } else {
335            self.set_status("Task list focus", false);
336        }
337    }
338
339    pub fn move_subtask_selection(&mut self, delta: i32) {
340        let Some(id) = self.selected_task_id() else {
341            return;
342        };
343        let n = self
344            .data
345            .tasks
346            .iter()
347            .find(|t| t.id == id)
348            .map(|t| t.subtasks.len())
349            .unwrap_or(0);
350        if n == 0 {
351            self.set_status("No subtasks — press [c] to add.", true);
352            return;
353        }
354        let cur = self.subtask_selected as i32;
355        self.subtask_selected = (cur + delta).rem_euclid(n as i32) as usize;
356        self.subtask_state.select(Some(self.subtask_selected));
357        if let Some(s) = self
358            .data
359            .tasks
360            .iter()
361            .find(|t| t.id == id)
362            .and_then(|t| t.subtasks.get(self.subtask_selected))
363        {
364            self.set_status(format!("Subtask: {}", s.title), false);
365        }
366    }
367
368    pub fn reset_subtask_selection(&mut self) {
369        let Some(id) = self.selected_task_id() else {
370            self.subtask_selected = 0;
371            return;
372        };
373        let Some(t) = self.data.tasks.iter().find(|t| t.id == id) else {
374            self.subtask_selected = 0;
375            return;
376        };
377        if t.subtasks.is_empty() {
378            self.subtask_selected = 0;
379            self.subtask_state.select(None);
380        } else {
381            self.subtask_selected = t.subtasks.iter().position(|s| !s.done).unwrap_or(0);
382            self.subtask_state.select(Some(self.subtask_selected));
383        }
384    }
385
386    pub fn delete_subtask_on_selected(&mut self) {
387        let Some(id) = self.selected_task_id() else {
388            return;
389        };
390        let sub_id = self
391            .data
392            .tasks
393            .iter()
394            .find(|t| t.id == id)
395            .and_then(|t| t.subtasks.get(self.subtask_selected))
396            .map(|s| s.id);
397        let Some(sub_id) = sub_id else {
398            self.set_status("No subtasks to remove.", true);
399            return;
400        };
401        let title = self
402            .data
403            .tasks
404            .iter()
405            .find(|t| t.id == id)
406            .and_then(|t| t.subtasks.iter().find(|s| s.id == sub_id))
407            .map(|s| s.title.clone())
408            .unwrap_or_default();
409        self.persist_data(|db, data| storage::delete_subtask(db, data, id, sub_id));
410        self.bump_data();
411        self.reset_subtask_selection();
412        self.set_status(format!("Removed subtask \"{title}\""), false);
413    }
414
415    pub fn open_add_subtask(&mut self) {
416        let Some(id) = self.selected_task_id() else {
417            self.set_status("No task selected.", true);
418            return;
419        };
420        self.input_buffer.clear();
421        self.popup = Some(Popup::AddSubtask(id));
422        self.input_mode = InputMode::Editing;
423    }
424
425    pub fn toggle_subtask_on_selected(&mut self) {
426        let Some(id) = self.selected_task_id() else {
427            return;
428        };
429        let sub_id = self
430            .data
431            .tasks
432            .iter()
433            .find(|t| t.id == id)
434            .and_then(|t| t.subtasks.get(self.subtask_selected))
435            .map(|s| s.id);
436        let Some(sub_id) = sub_id else {
437            self.set_status("No subtasks — press [c] to add.", true);
438            return;
439        };
440        self.persist_data(|db, data| storage::toggle_subtask(db, data, id, sub_id));
441        self.bump_data();
442        self.clamp_subtask_selection();
443        if let Some(t) = self.data.tasks.iter().find(|t| t.id == id) {
444            if let Some(s) = t.subtasks.iter().find(|s| s.id == sub_id) {
445                let state = if s.done { "done" } else { "open" };
446                self.set_status(format!("Subtask \"{}\" marked {state}", s.title), false);
447            }
448        }
449    }
450
451    pub fn archive_selected_task(&mut self) {
452        let Some(id) = self.selected_task_id() else {
453            self.set_status("No task selected.", true);
454            return;
455        };
456        self.persist_data(|db, data| storage::archive_task(db, data, id));
457        self.bump_data();
458        self.clamp_task_selection_after_mutation();
459        self.set_status("Task archived.", false);
460    }
461}