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