sara-tasks 2.4.0

Sara — folder-aware task manager
use anyhow::Result;
use rusqlite::Connection;

use chrono::{DateTime, Utc};

use crate::infrastructure::config::Config;
use crate::infrastructure::db;

mod render;

pub(super) enum ProjectAction {
    Quit,
    Open(String),
}

pub(super) struct ProjectRow {
    pub(super) name: String,
    pub(super) goal: Option<String>,
    pub(super) stack: Option<String>,
    pub(super) pending: u32,
    pub(super) done: u32,
    pub(super) last_activity: Option<DateTime<Utc>>,
}

pub(super) struct ProjectListState {
    pub(super) rows: Vec<ProjectRow>,
    pub(super) selected: usize,
    pub(super) scroll: u16,
}

pub fn run(conn: &Connection, cfg: &Config) -> Result<()> {
    let mut selected = 0usize;
    let mut scroll = 0u16;

    loop {
        let rows = build_rows(conn)?;
        if rows.is_empty() {
            println!("No projects yet. Run `sara init` in a repository to register one.");
            return Ok(());
        }

        let mut st = ProjectListState {
            selected: selected.min(rows.len() - 1),
            scroll,
            rows,
        };

        let action = crate::infrastructure::tui::with_terminal(|t| render::list_loop(t, &mut st))?;

        selected = st.selected;
        scroll = st.scroll;

        match action {
            ProjectAction::Quit => break,
            ProjectAction::Open(name) => {
                crate::commands::board::run(conn, cfg, Some(&name), false)?;
            }
        }
    }
    Ok(())
}

fn build_rows(conn: &Connection) -> Result<Vec<ProjectRow>> {
    let names = db::project_names(conn)?;
    let mut rows = Vec::with_capacity(names.len());
    for name in names {
        let profile = db::get_project(conn, &name)?;
        let stats = db::project_stats(conn, &name)?;
        let last_activity = db::project_last_activity(conn, &name)?;
        rows.push(ProjectRow {
            goal: profile.as_ref().and_then(|p| p.goal.clone()),
            stack: profile.as_ref().and_then(|p| p.stack.clone()),
            pending: stats.pending,
            done: stats.completed_total,
            last_activity,
            name,
        });
    }
    sort_rows(&mut rows);
    Ok(rows)
}

fn sort_rows(rows: &mut [ProjectRow]) {
    rows.sort_by(|a, b| {
        b.last_activity
            .cmp(&a.last_activity)
            .then_with(|| a.name.cmp(&b.name))
    });
}

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