Skip to main content

things3_cloud/commands/
project.rs

1use 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    /// Project UUID (or unique UUID prefix)
23    pub project_id: String,
24    /// Show notes beneath each task
25    #[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}