argv-todo 0.1.0

A Vim-style terminal todo manager with nested tasks, priorities, search, and local SQLite storage
use ratatui::widgets::ListState;

use super::{App, TreeRow};
use crate::config::TaskView;

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TaskPane {
    #[default]
    Tree,
    Todo,
    Completed,
}

#[derive(Default)]
pub(crate) struct TaskPanes {
    focus: TaskPane,
    todo: ListState,
    completed: ListState,
}

impl App {
    pub(crate) fn task_view(&self) -> TaskView {
        self.config
            .as_ref()
            .map_or(TaskView::Normal, |config| config.task_view)
    }

    pub(crate) fn task_pane(&self) -> TaskPane {
        match (self.task_view(), self.panes.focus) {
            (TaskView::Normal, _) => TaskPane::Tree,
            (TaskView::Split, TaskPane::Completed) => TaskPane::Completed,
            (TaskView::Split, _) => TaskPane::Todo,
        }
    }

    pub(crate) fn pane_list(&self, pane: TaskPane) -> &ListState {
        match pane {
            TaskPane::Tree => &self.list,
            TaskPane::Todo => &self.panes.todo,
            TaskPane::Completed => &self.panes.completed,
        }
    }

    pub(crate) fn pane_list_mut(&mut self, pane: TaskPane) -> &mut ListState {
        match pane {
            TaskPane::Tree => &mut self.list,
            TaskPane::Todo => &mut self.panes.todo,
            TaskPane::Completed => &mut self.panes.completed,
        }
    }

    pub(super) fn active_list(&self) -> &ListState {
        self.pane_list(self.task_pane())
    }

    pub(super) fn active_list_mut(&mut self) -> &mut ListState {
        self.pane_list_mut(self.task_pane())
    }

    pub(crate) fn rows_for_pane(&self, pane: TaskPane) -> Vec<TreeRow> {
        let rows = self.visible_rows();
        let rows = if pane == TaskPane::Tree && self.show_completed() {
            rows
        } else {
            self.rows_by_completion(rows, pane == TaskPane::Completed)
        };
        self.expanded_rows(rows)
    }

    pub(super) fn switch_task_pane(&mut self) {
        if self.task_view() == TaskView::Normal {
            return;
        }
        self.normalize_selection();
        self.panes.focus = if self.task_pane() == TaskPane::Todo {
            TaskPane::Completed
        } else {
            TaskPane::Todo
        };
        self.message("");
    }

    pub(super) fn select_in_task_view(&mut self, id: i64) {
        let pane = if self.task_view() == TaskView::Split {
            if self.todos.iter().any(|todo| todo.id == id && todo.done) {
                TaskPane::Completed
            } else {
                TaskPane::Todo
            }
        } else {
            TaskPane::Tree
        };
        self.panes.focus = pane;
        let row = self
            .rows_for_pane(pane)
            .iter()
            .filter(|row| !row.ghost)
            .position(|row| self.todos[row.index].id == id);
        self.pane_list_mut(pane).select(row);
    }

    pub(super) fn reconcile_task_view(&mut self, selected_id: Option<i64>) {
        if let Some(id) = selected_id {
            self.select_id(id);
        }
        self.normalize_selection();
    }

    pub(super) fn normalize_panes(&mut self) {
        for pane in [TaskPane::Tree, TaskPane::Todo, TaskPane::Completed] {
            let len = self
                .rows_for_pane(pane)
                .iter()
                .filter(|row| !row.ghost)
                .count();
            normalize_list(self.pane_list_mut(pane), len);
        }
    }

    fn rows_by_completion(&self, rows: Vec<TreeRow>, completed: bool) -> Vec<TreeRow> {
        let mut included = vec![false; rows.len()];
        let mut ancestors = Vec::new();
        for (position, row) in rows.iter().enumerate() {
            ancestors.truncate(row.depth);
            if self.todos[row.index].done == completed {
                included[position] = true;
                if completed {
                    for &ancestor in &ancestors {
                        included[ancestor] = true;
                    }
                }
            }
            ancestors.push(position);
        }
        let mut ancestors = Vec::new();
        let mut filtered = Vec::new();
        for (row, included) in rows.into_iter().zip(included) {
            ancestors.truncate(row.depth);
            if included {
                filtered.push(TreeRow {
                    index: row.index,
                    depth: ancestors.iter().filter(|&&included| included).count(),
                    ghost: self.todos[row.index].done != completed,
                });
            }
            ancestors.push(included);
        }
        filtered
    }
}

fn normalize_list(list: &mut ListState, len: usize) {
    list.select(if len == 0 {
        None
    } else {
        Some(list.selected().unwrap_or(0).min(len - 1))
    });
}

#[cfg(test)]
#[path = "../../tests/unit/app/views.rs"]
mod tests;