next.rs

  1use anyhow::{bail, Result};
  2use comfy_table::presets::NOTHING;
  3use comfy_table::{Cell, Table};
  4use std::collections::HashSet;
  5use std::path::Path;
  6
  7use crate::color::{cell_bold, stdout_use_color};
  8use crate::db;
  9use crate::score::{self, Mode};
 10
 11fn parse_mode(s: &str) -> Result<Mode> {
 12    match s {
 13        "impact" => Ok(Mode::Impact),
 14        "effort" => Ok(Mode::Effort),
 15        _ => bail!("invalid mode '{s}': expected impact or effort"),
 16    }
 17}
 18
 19pub fn run(root: &Path, mode_str: &str, verbose: bool, limit: usize, json: bool) -> Result<()> {
 20    let mode = parse_mode(mode_str)?;
 21    let store = db::open(root)?;
 22    let all = store.list_tasks()?;
 23
 24    let open_tasks: Vec<score::TaskInput> = all
 25        .iter()
 26        .filter(|t| t.status == db::Status::Open)
 27        .map(|t| score::TaskInput {
 28            id: t.id.as_str().to_string(),
 29            title: t.title.clone(),
 30            priority_score: t.priority.score(),
 31            effort_score: t.effort.score(),
 32            priority_label: db::priority_label(t.priority).to_string(),
 33            effort_label: db::effort_label(t.effort).to_string(),
 34        })
 35        .collect();
 36
 37    let edges: Vec<(String, String)> = all
 38        .iter()
 39        .filter(|t| t.status == db::Status::Open)
 40        .flat_map(|t| {
 41            t.blockers
 42                .iter()
 43                .map(|b| (t.id.as_str().to_string(), b.as_str().to_string()))
 44                .collect::<Vec<_>>()
 45        })
 46        .collect();
 47
 48    let parents_with_open_children: HashSet<String> = all
 49        .iter()
 50        .filter(|t| t.status == db::Status::Open)
 51        .filter_map(|t| t.parent.as_ref().map(|p| p.as_str().to_string()))
 52        .collect();
 53
 54    let scored = score::rank(
 55        &open_tasks,
 56        &edges,
 57        &parents_with_open_children,
 58        mode,
 59        limit,
 60    );
 61
 62    if scored.is_empty() {
 63        if json {
 64            println!("[]");
 65        } else {
 66            println!("No open tasks.");
 67        }
 68        return Ok(());
 69    }
 70
 71    if json {
 72        let out: Vec<_> = scored
 73            .iter()
 74            .enumerate()
 75            .map(|(i, s)| {
 76                serde_json::json!({
 77                    "rank": i + 1,
 78                    "id": s.id,
 79                    "title": s.title,
 80                    "score": s.score,
 81                    "priority": s.priority,
 82                    "effort": s.effort,
 83                    "downstream_score": s.downstream_score,
 84                    "priority_weight": s.priority_weight,
 85                    "effort_weight": s.effort_weight,
 86                    "total_unblocked": s.total_unblocked,
 87                    "direct_unblocked": s.direct_unblocked,
 88                })
 89            })
 90            .collect();
 91        println!("{}", serde_json::to_string(&out)?);
 92    } else {
 93        let use_color = stdout_use_color();
 94        let mut table = Table::new();
 95        table.load_preset(NOTHING);
 96        table.set_header(vec!["#", "ID", "SCORE", "TITLE"]);
 97
 98        for (i, s) in scored.iter().enumerate() {
 99            let short = db::TaskId::display_id(&s.id);
100            table.add_row(vec![
101                Cell::new(i + 1),
102                cell_bold(&short, use_color),
103                Cell::new(format!("{:.2}", s.score)),
104                Cell::new(&s.title),
105            ]);
106        }
107        println!("{table}");
108
109        if verbose {
110            let mode_label = match mode {
111                Mode::Impact => "impact",
112                Mode::Effort => "effort",
113            };
114            println!("\nmode: {mode_label}");
115            for (i, s) in scored.iter().enumerate() {
116                let short = db::TaskId::display_id(&s.id);
117                let formula = match mode {
118                    Mode::Impact => format!(
119                        "({:.2} + 1.00) × {:.2} / {:.2}^0.25 = {:.2}",
120                        s.downstream_score, s.priority_weight, s.effort_weight, s.score
121                    ),
122                    Mode::Effort => format!(
123                        "({:.2} × 0.25 + 1.00) × {:.2} / {:.2}² = {:.2}",
124                        s.downstream_score, s.priority_weight, s.effort_weight, s.score
125                    ),
126                };
127                let task_word = if s.total_unblocked == 1 {
128                    "task"
129                } else {
130                    "tasks"
131                };
132                println!(
133                    "\n{}. {}\n   {}\n   Unblocks: {} {} ({} directly)",
134                    i + 1,
135                    short,
136                    formula,
137                    s.total_unblocked,
138                    task_word,
139                    s.direct_unblocked
140                );
141            }
142        }
143    }
144
145    Ok(())
146}