Skip to main content

things3_cloud/commands/
project.rs

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