1use crate::app::Cli;
2use crate::commands::Command;
3use crate::common::{
4 BOLD, DIM, GREEN, ICONS, colored, fmt_deadline, fmt_task_line, fmt_task_with_note,
5};
6use anyhow::Result;
7use clap::Args;
8use std::collections::BTreeMap;
9
10#[derive(Debug, Args)]
11#[command(about = "Show all tasks in a project")]
12pub struct ProjectArgs {
13 pub project_id: String,
15 #[arg(long)]
17 pub detailed: bool,
18}
19
20impl Command for ProjectArgs {
21 fn run_with_ctx(
22 &self,
23 cli: &Cli,
24 out: &mut dyn std::io::Write,
25 ctx: &mut dyn crate::cmd_ctx::CmdCtx,
26 ) -> Result<()> {
27 let store = cli.load_store()?;
28 let today = ctx.today();
29 let (task_opt, err, ambiguous) = store.resolve_mark_identifier(&self.project_id);
30 let Some(project) = task_opt else {
31 eprintln!("{err}");
32 for match_task in ambiguous {
33 eprintln!(" {}", match_task.title);
34 }
35 return Ok(());
36 };
37
38 if !project.is_project() {
39 eprintln!("Not a project: {}", project.title);
40 return Ok(());
41 }
42
43 let children = store
44 .tasks(None, Some(false), None)
45 .into_iter()
46 .filter(|t| store.effective_project_uuid(t).as_ref() == Some(&project.uuid))
47 .collect::<Vec<_>>();
48
49 let headings = store
50 .tasks_by_uuid
51 .values()
52 .filter(|t| {
53 t.is_heading() && !t.trashed && t.project.as_ref() == Some(&project.uuid)
54 })
55 .cloned()
56 .map(|h| (h.uuid.clone(), h))
57 .collect::<BTreeMap<_, _>>();
58
59 let mut ungrouped = Vec::new();
60 let mut by_heading: BTreeMap<_, Vec<_>> = BTreeMap::new();
61 for t in children.clone() {
62 if let Some(heading_uuid) = &t.action_group
63 && headings.contains_key(heading_uuid)
64 {
65 by_heading.entry(heading_uuid.clone()).or_default().push(t);
66 continue;
67 }
68 ungrouped.push(t);
69 }
70
71 let mut sorted_heading_uuids = by_heading.keys().cloned().collect::<Vec<_>>();
72 sorted_heading_uuids.sort_by_key(|u| headings.get(u).map(|h| h.index).unwrap_or(0));
73 ungrouped.sort_by_key(|t| t.index);
74 for items in by_heading.values_mut() {
75 items.sort_by_key(|t| t.index);
76 }
77
78 let total = children.len();
79 let progress = store.project_progress(&project.uuid);
80 let done_count = progress.done;
81
82 let tags = if project.tags.is_empty() {
83 String::new()
84 } else {
85 let tag_names = project
86 .tags
87 .iter()
88 .map(|t| store.resolve_tag_title(t))
89 .collect::<Vec<_>>()
90 .join(", ");
91 colored(&format!(" [{tag_names}]"), &[DIM], cli.no_color)
92 };
93 writeln!(
94 out,
95 "{}{}{}",
96 colored(
97 &format!(
98 "{} {} ({}/{})",
99 ICONS.project,
100 project.title,
101 done_count,
102 done_count + total as i32
103 ),
104 &[BOLD, GREEN],
105 cli.no_color,
106 ),
107 fmt_deadline(project.deadline, &today, cli.no_color),
108 tags
109 )?;
110
111 if let Some(notes) = &project.notes {
112 let lines = notes.lines().collect::<Vec<_>>();
113 for note in lines.iter().take(lines.len().saturating_sub(1)) {
114 writeln!(
115 out,
116 "{} {}",
117 colored(" │", &[DIM], cli.no_color),
118 colored(note, &[DIM], cli.no_color)
119 )?;
120 }
121 if let Some(last) = lines.last() {
122 writeln!(
123 out,
124 "{} {}",
125 colored(" └", &[DIM], cli.no_color),
126 colored(last, &[DIM], cli.no_color)
127 )?;
128 }
129 }
130
131 let mut all_uuids = vec![project.uuid.clone()];
132 all_uuids.extend(children.iter().map(|t| t.uuid.clone()));
133 let id_prefix_len = store.unique_prefix_length(&all_uuids);
134
135 if children.is_empty() {
136 writeln!(out, "{}", colored(" No tasks.", &[DIM], cli.no_color))?;
137 return Ok(());
138 }
139
140 if !ungrouped.is_empty() {
141 writeln!(out)?;
142 for t in ungrouped {
143 let line =
144 fmt_task_line(
145 &t,
146 &store,
147 false,
148 true,
149 false,
150 Some(id_prefix_len),
151 &today,
152 cli.no_color,
153 );
154 writeln!(
155 out,
156 "{}",
157 fmt_task_with_note(
158 line,
159 &t,
160 " ",
161 Some(id_prefix_len),
162 self.detailed,
163 cli.no_color,
164 )
165 )?;
166 }
167 }
168
169 for heading_uuid in sorted_heading_uuids {
170 if let Some(heading) = headings.get(&heading_uuid) {
171 writeln!(out)?;
172 writeln!(
173 out,
174 "{}",
175 colored(&format!(" {}", heading.title), &[BOLD], cli.no_color)
176 )?;
177 if let Some(tasks) = by_heading.get(&heading_uuid) {
178 for t in tasks {
179 let line = fmt_task_line(
180 t,
181 &store,
182 false,
183 true,
184 false,
185 Some(id_prefix_len),
186 &today,
187 cli.no_color,
188 );
189 writeln!(
190 out,
191 "{}",
192 fmt_task_with_note(
193 line,
194 t,
195 " ",
196 Some(id_prefix_len),
197 self.detailed,
198 cli.no_color,
199 )
200 )?;
201 }
202 }
203 }
204 }
205
206 Ok(())
207 }
208}