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}