things3_cloud/commands/
project.rs1use std::{collections::BTreeMap, sync::Arc};
2
3use anyhow::Result;
4use clap::Args;
5use iocraft::prelude::*;
6
7use crate::{
8 app::Cli,
9 commands::{Command, detailed_json_conflict, write_json},
10 ui::{
11 render_element_to_string,
12 views::{
13 json::common::build_tasks_json,
14 project::{ProjectHeadingGroup, ProjectView},
15 },
16 },
17};
18
19#[derive(Debug, Args)]
20#[command(about = "Show all tasks in a project")]
21pub struct ProjectArgs {
22 pub project_id: String,
24 #[arg(long, short = 'd')]
26 pub detailed: bool,
27}
28
29impl Command for ProjectArgs {
30 fn run_with_ctx(
31 &self,
32 cli: &Cli,
33 out: &mut dyn std::io::Write,
34 ctx: &mut dyn crate::cmd_ctx::CmdCtx,
35 ) -> Result<()> {
36 let store = Arc::new(cli.load_store()?);
37 let today = ctx.today();
38 let (task_opt, err, ambiguous) = store.resolve_mark_identifier(&self.project_id);
39 let Some(project) = task_opt else {
40 eprintln!("{err}");
41 for match_task in ambiguous {
42 eprintln!(" {}", match_task.title);
43 }
44 return Ok(());
45 };
46
47 if !project.is_project() {
48 eprintln!("Not a project: {}", project.title);
49 return Ok(());
50 }
51
52 let children = store
53 .tasks(None, Some(false), None)
54 .into_iter()
55 .filter(|t| store.effective_project_uuid(t).as_ref() == Some(&project.uuid))
56 .collect::<Vec<_>>();
57
58 let json = cli.json;
59 if json {
60 if detailed_json_conflict(json, self.detailed) {
61 return Ok(());
62 }
63
64 let mut sorted_children = children.clone();
65 sorted_children.sort_by_key(|t| t.index);
66 write_json(out, &build_tasks_json(&sorted_children, &store, &today))?;
67 return Ok(());
68 }
69
70 let headings = store
71 .tasks_by_uuid
72 .values()
73 .filter(|t| t.is_heading() && !t.trashed && t.project.as_ref() == Some(&project.uuid))
74 .cloned()
75 .map(|h| (h.uuid.clone(), h))
76 .collect::<BTreeMap<_, _>>();
77
78 let mut ungrouped = Vec::new();
79 let mut by_heading: BTreeMap<_, Vec<_>> = BTreeMap::new();
80 for t in children.clone() {
81 if let Some(heading_uuid) = &t.action_group
82 && headings.contains_key(heading_uuid)
83 {
84 by_heading.entry(heading_uuid.clone()).or_default().push(t);
85 continue;
86 }
87 ungrouped.push(t);
88 }
89
90 let mut sorted_heading_uuids = by_heading.keys().cloned().collect::<Vec<_>>();
91 sorted_heading_uuids.sort_by_key(|u| headings.get(u).map(|h| h.index).unwrap_or(0));
92 ungrouped.sort_by_key(|t| t.index);
93 for items in by_heading.values_mut() {
94 items.sort_by_key(|t| t.index);
95 }
96
97 let heading_groups = sorted_heading_uuids
98 .iter()
99 .filter_map(|heading_uuid| {
100 let heading = headings.get(heading_uuid)?;
101 let tasks = by_heading.get(heading_uuid)?;
102 Some(ProjectHeadingGroup {
103 title: heading.title.clone(),
104 items: tasks.iter().collect::<Vec<_>>(),
105 })
106 })
107 .collect::<Vec<_>>();
108
109 let mut ui = element! {
110 ContextProvider(value: Context::owned(store.clone())) {
111 ContextProvider(value: Context::owned(today)) {
112 ProjectView(
113 project: &project,
114 ungrouped: ungrouped.iter().collect::<Vec<_>>(),
115 heading_groups,
116 detailed: self.detailed,
117 no_color: cli.no_color,
118 )
119 }
120 }
121 };
122 let rendered = render_element_to_string(&mut ui, cli.no_color);
123 writeln!(out, "{}", rendered)?;
124
125 Ok(())
126 }
127}