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 notes_match = t.notes.to_lowercase().contains(&q);
74            let tags_match = t.tags.iter().any(|tag| tag.to_lowercase().contains(&q));
75            if !title_match && !notes_match && !tags_match {
76                return false;
77            }
78        }
79        if let Some(ref tag) = self.active_tag_filter {
80            if !t.tags.iter().any(|t| t == tag) {
81                return false;
82            }
83        }
84        match self.task_filter {
85            TaskFilter::All => true,
86            TaskFilter::Pending => t.status != crate::model::TaskStatus::Done,
87            TaskFilter::Done => t.status == crate::model::TaskStatus::Done,
88            TaskFilter::Today => t.today && t.status != crate::model::TaskStatus::Done,
89        }
90    }
91
92    pub fn filtered_task_indices(&self) -> Vec<usize> {
93        self.data
94            .tasks
95            .iter()
96            .enumerate()
97            .filter(|(_, t)| self.matches_filter(t))
98            .map(|(i, _)| i)
99            .collect()
100    }
101
102    pub fn dashboard_tasks(&self) -> Vec<&crate::model::Task> {
103        let mut tasks: Vec<_> = self
104            .filtered_task_indices()
105            .into_iter()
106            .map(|i| &self.data.tasks[i])
107            .collect();
108        tasks.sort_by(|a, b| {
109            b.priority
110                .rank()
111                .cmp(&a.priority.rank())
112                .then(b.today.cmp(&a.today))
113                .then(a.sort_order.cmp(&b.sort_order))
114        });
115        tasks
116    }
117
118    pub fn set_active_task(&mut self, id: Option<u64>) {
119        if let Some(id) = id {
120            if self
121                .data
122                .tasks
123                .iter()
124                .find(|t| t.id == id)
125                .is_some_and(|t| t.status == crate::model::TaskStatus::Done)
126            {
127                self.set_status("That task is done — pick another.", true);
128                return;
129            }
130            self.persist_data(|db, data| storage::promote_task_on_activate(db, data, id));
131        }
132        self.active_task = id;
133        self.data.active_task_id = id;
134        self.persist(|db| db.persist_active_task(id));
135    }
136
137    pub fn cycle_active_task_status(&mut self) {
138        let Some(id) = self.active_task else {
139            self.set_status("No active task — set one on Tasks (Space).", true);
140            return;
141        };
142        self.persist_data(|db, data| storage::cycle_task_status(db, data, id));
143        let status = self
144            .data
145            .tasks
146            .iter()
147            .find(|t| t.id == id)
148            .map(|t| t.status);
149        let Some(status) = status else {
150            return;
151        };
152        if status == crate::model::TaskStatus::Done {
153            self.active_task = None;
154            self.data.active_task_id = None;
155            self.persist(|db| db.persist_active_task(None));
156            self.maybe_advance_task();
157        }
158        self.bump_data();
159        self.set_status(format!("Task status: {}", status.label()), false);
160        if status == crate::model::TaskStatus::Done {
161            self.check_queue_empty();
162        }
163    }
164
165    pub fn mark_active_task_done(&mut self) {
166        let Some(id) = self.active_task else {
167            self.set_status("No active task — set one on Tasks (Space).", true);
168            return;
169        };
170        self.persist_data(|db, data| storage::mark_task_done(db, data, id));
171        self.active_task = None;
172        self.data.active_task_id = None;
173        self.persist(|db| db.persist_active_task(None));
174        self.bump_data();
175        self.maybe_advance_task();
176        self.set_status("Task marked done.", false);
177        self.check_queue_empty();
178    }
179
180    pub(crate) fn auto_pick_task_if_needed(&mut self) {
181        if self.active_task.is_some() || !self.data.auto_pick_task {
182            return;
183        }
184        if let Some(id) = storage::pick_best_task(&self.data) {
185            self.set_active_task(Some(id));
186        }
187    }
188
189    pub(crate) fn maybe_advance_task(&mut self) {
190        if !self.data.auto_advance_task {
191            return;
192        }
193        let next = storage::advance_to_next_task(&self.data, self.active_task);
194        self.set_active_task(next);
195        if let Some(id) = next {
196            if let Some(t) = self.data.tasks.iter().find(|t| t.id == id) {
197                self.set_status(format!("Next task: {}", t.title), false);
198            }
199        }
200    }
201
202    pub fn start_focus_on_task(&mut self, id: u64) {
203        if self
204            .data
205            .tasks
206            .iter()
207            .find(|t| t.id == id)
208            .is_some_and(|t| t.status == crate::model::TaskStatus::Done)
209        {
210            self.set_status("That task is done — pick another.", true);
211            return;
212        }
213        self.set_active_task(Some(id));
214        self.tab = FocusTab::Dashboard;
215        if self.timer.mode != TimerMode::Focus {
216            self.timer.configure(TimerMode::Focus);
217        }
218        self.start_timer();
219    }
220
221    pub fn cycle_task_filter(&mut self) {
222        self.task_filter = self.task_filter.next();
223        self.task_state.select(Some(0));
224        self.set_status(format!("Filter: {}", self.task_filter.label()), false);
225    }
226}