void-focus 0.3.0-alpha.3

A feature-rich terminal focus timer with task tracking
Documentation
use super::*;
use crate::storage;

impl App {
    pub fn pending_task_ids(&self) -> Vec<u64> {
        storage::sorted_pending_tasks(&self.data)
            .into_iter()
            .map(|t| t.id)
            .collect()
    }

    pub fn dashboard_selected_task_id(&self) -> Option<u64> {
        let pending = self.dashboard_tasks();
        if pending.is_empty() {
            None
        } else {
            let idx = self.dashboard_task_selected.min(pending.len() - 1);
            Some(pending[idx].id)
        }
    }

    pub(crate) fn clamp_dashboard_task_selection(&mut self) {
        let n = self.dashboard_tasks().len();
        if n == 0 {
            self.dashboard_task_selected = 0;
        } else if self.dashboard_task_selected >= n {
            self.dashboard_task_selected = n - 1;
        }
    }

    pub(crate) fn clamp_task_selection_after_mutation(&mut self) {
        let len = self.filtered_task_indices().len();
        if len == 0 {
            self.task_state.select(None);
        } else {
            let sel = self.task_state.selected().unwrap_or(0).min(len - 1);
            self.task_state.select(Some(sel));
        }
    }

    pub(crate) fn move_dashboard_task_selection(&mut self, delta: i32) {
        let count = self.dashboard_tasks().len();
        if count == 0 {
            return;
        }
        let cur = self.dashboard_task_selected as i32;
        let next = (cur + delta).rem_euclid(count as i32) as usize;
        self.dashboard_task_selected = next;
    }

    pub fn pending_task_count(&self) -> u32 {
        storage::pending_tasks(&self.data).count() as u32
    }

    pub fn active_task_pending_index(&self) -> Option<u32> {
        let id = self.active_task?;
        storage::sorted_pending_tasks(&self.data)
            .iter()
            .position(|t| t.id == id)
            .map(|i| i as u32)
    }

    pub fn active_task_progress(&self) -> Option<f64> {
        let id = self.active_task?;
        let task = self.data.tasks.iter().find(|t| t.id == id)?;
        Some(task.progress_ratio())
    }

    pub(crate) fn matches_filter(&self, t: &crate::model::Task) -> bool {
        if !self.task_search.is_empty() {
            let q = self.task_search.to_lowercase();
            let title_match = t.title.to_lowercase().contains(&q);
            let notes_match = t.notes.to_lowercase().contains(&q);
            let tags_match = t.tags.iter().any(|tag| tag.to_lowercase().contains(&q));
            if !title_match && !notes_match && !tags_match {
                return false;
            }
        }
        if let Some(ref tag) = self.active_tag_filter {
            if !t.tags.iter().any(|t| t == tag) {
                return false;
            }
        }
        match self.task_filter {
            TaskFilter::All => true,
            TaskFilter::Pending => t.status != crate::model::TaskStatus::Done,
            TaskFilter::Done => t.status == crate::model::TaskStatus::Done,
            TaskFilter::Today => t.today && t.status != crate::model::TaskStatus::Done,
        }
    }

    pub fn filtered_task_indices(&self) -> Vec<usize> {
        self.data
            .tasks
            .iter()
            .enumerate()
            .filter(|(_, t)| self.matches_filter(t))
            .map(|(i, _)| i)
            .collect()
    }

    pub fn dashboard_tasks(&self) -> Vec<&crate::model::Task> {
        let mut tasks: Vec<_> = self
            .filtered_task_indices()
            .into_iter()
            .map(|i| &self.data.tasks[i])
            .collect();
        tasks.sort_by(|a, b| {
            b.priority
                .rank()
                .cmp(&a.priority.rank())
                .then(b.today.cmp(&a.today))
                .then(a.sort_order.cmp(&b.sort_order))
        });
        tasks
    }

    pub fn set_active_task(&mut self, id: Option<u64>) {
        if let Some(id) = id {
            if self
                .data
                .tasks
                .iter()
                .find(|t| t.id == id)
                .is_some_and(|t| t.status == crate::model::TaskStatus::Done)
            {
                self.set_status("That task is done — pick another.", true);
                return;
            }
            self.persist_data(|db, data| storage::promote_task_on_activate(db, data, id));
        }
        self.active_task = id;
        self.data.active_task_id = id;
        self.persist(|db| db.persist_active_task(id));
    }

    pub fn cycle_active_task_status(&mut self) {
        let Some(id) = self.active_task else {
            self.set_status("No active task — set one on Tasks (Space).", true);
            return;
        };
        self.persist_data(|db, data| storage::cycle_task_status(db, data, id));
        let status = self
            .data
            .tasks
            .iter()
            .find(|t| t.id == id)
            .map(|t| t.status);
        let Some(status) = status else {
            return;
        };
        if status == crate::model::TaskStatus::Done {
            self.active_task = None;
            self.data.active_task_id = None;
            self.persist(|db| db.persist_active_task(None));
            self.maybe_advance_task();
        }
        self.bump_data();
        self.set_status(format!("Task status: {}", status.label()), false);
        if status == crate::model::TaskStatus::Done {
            self.check_queue_empty();
        }
    }

    pub fn mark_active_task_done(&mut self) {
        let Some(id) = self.active_task else {
            self.set_status("No active task — set one on Tasks (Space).", true);
            return;
        };
        self.persist_data(|db, data| storage::mark_task_done(db, data, id));
        self.active_task = None;
        self.data.active_task_id = None;
        self.persist(|db| db.persist_active_task(None));
        self.bump_data();
        self.maybe_advance_task();
        self.set_status("Task marked done.", false);
        self.check_queue_empty();
    }

    pub(crate) fn auto_pick_task_if_needed(&mut self) {
        if self.active_task.is_some() || !self.data.auto_pick_task {
            return;
        }
        if let Some(id) = storage::pick_best_task(&self.data) {
            self.set_active_task(Some(id));
        }
    }

    pub(crate) fn maybe_advance_task(&mut self) {
        if !self.data.auto_advance_task {
            return;
        }
        let next = storage::advance_to_next_task(&self.data, self.active_task);
        self.set_active_task(next);
        if let Some(id) = next {
            if let Some(t) = self.data.tasks.iter().find(|t| t.id == id) {
                self.set_status(format!("Next task: {}", t.title), false);
            }
        }
    }

    pub fn start_focus_on_task(&mut self, id: u64) {
        if self
            .data
            .tasks
            .iter()
            .find(|t| t.id == id)
            .is_some_and(|t| t.status == crate::model::TaskStatus::Done)
        {
            self.set_status("That task is done — pick another.", true);
            return;
        }
        self.set_active_task(Some(id));
        self.tab = FocusTab::Dashboard;
        if self.timer.mode != TimerMode::Focus {
            self.timer.configure(TimerMode::Focus);
        }
        self.start_timer();
    }

    pub fn cycle_task_filter(&mut self) {
        self.task_filter = self.task_filter.next();
        self.task_state.select(Some(0));
        self.set_status(format!("Filter: {}", self.task_filter.label()), false);
    }
}