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<(String, String, i32, i32)> = all
25 .iter()
26 .filter(|t| t.status == db::Status::Open)
27 .map(|t| {
28 (
29 t.id.as_str().to_string(),
30 t.title.clone(),
31 t.priority.score(),
32 t.effort.score(),
33 )
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 println!();
111 for (i, s) in scored.iter().enumerate() {
112 let short = db::TaskId::display_id(&s.id);
113 println!("{}. {} — score: {:.2}", i + 1, short, s.score);
114 }
115 }
116 }
117
118 Ok(())
119}