things3_cloud/commands/
area.rs1use crate::app::Cli;
2use crate::commands::Command;
3use crate::common::{
4 colored, fmt_project_with_note, fmt_task_line, fmt_task_with_note, BOLD, DIM, ICONS, MAGENTA,
5};
6use crate::wire::task::TaskStatus;
7use anyhow::Result;
8use clap::Args;
9
10#[derive(Debug, Args)]
11#[command(about = "Show projects and tasks in an area")]
12pub struct AreaArgs {
13 pub area_id: String,
15 #[arg(long)]
17 pub detailed: bool,
18 #[arg(long)]
20 pub all: bool,
21}
22
23impl Command for AreaArgs {
24 fn run_with_ctx(
25 &self,
26 cli: &Cli,
27 out: &mut dyn std::io::Write,
28 ctx: &mut dyn crate::cmd_ctx::CmdCtx,
29 ) -> Result<()> {
30 let store = cli.load_store()?;
31 let today = ctx.today();
32 let (area_opt, err, ambiguous) = store.resolve_area_identifier(&self.area_id);
33 let Some(area) = area_opt else {
34 eprintln!("{err}");
35 for match_area in ambiguous {
36 eprintln!(
37 " {} {} ({})",
38 ICONS.area, match_area.title, match_area.uuid
39 );
40 }
41 return Ok(());
42 };
43
44 let status_filter = if self.all {
45 None
46 } else {
47 Some(TaskStatus::Incomplete)
48 };
49 let mut projects = store
50 .projects(status_filter)
51 .into_iter()
52 .filter(|p| p.area.as_ref() == Some(&area.uuid))
53 .collect::<Vec<_>>();
54 projects.sort_by_key(|p| p.index);
55
56 let mut loose_tasks = store
57 .tasks(status_filter, Some(false), None)
58 .into_iter()
59 .filter(|t| {
60 t.area.as_ref() == Some(&area.uuid)
61 && !t.is_project()
62 && store.effective_project_uuid(t).is_none()
63 })
64 .collect::<Vec<_>>();
65 loose_tasks.sort_by_key(|t| t.index);
66
67 let project_count = projects.len();
68 let task_count = loose_tasks.len();
69
70 let tags = if area.tags.is_empty() {
71 String::new()
72 } else {
73 let names = area
74 .tags
75 .iter()
76 .map(|t| store.resolve_tag_title(t))
77 .collect::<Vec<_>>()
78 .join(", ");
79 colored(&format!(" [{names}]"), &[DIM], cli.no_color)
80 };
81
82 let mut parts = Vec::new();
83 if project_count > 0 {
84 parts.push(format!(
85 "{} project{}",
86 project_count,
87 if project_count == 1 { "" } else { "s" }
88 ));
89 }
90 if task_count > 0 {
91 parts.push(format!(
92 "{} task{}",
93 task_count,
94 if task_count == 1 { "" } else { "s" }
95 ));
96 }
97 let count_str = if parts.is_empty() {
98 String::new()
99 } else {
100 format!(" ({})", parts.join(", "))
101 };
102
103 writeln!(
104 out,
105 "{}{}",
106 colored(
107 &format!("{} {}{}", ICONS.area, area.title, count_str),
108 &[BOLD, MAGENTA],
109 cli.no_color,
110 ),
111 tags
112 )?;
113
114 let mut all_uuids = vec![area.uuid.clone()];
115 all_uuids.extend(projects.iter().map(|p| p.uuid.clone()));
116 all_uuids.extend(loose_tasks.iter().map(|t| t.uuid.clone()));
117 let id_prefix_len = store.unique_prefix_length(&all_uuids);
118
119 if !loose_tasks.is_empty() {
120 writeln!(out)?;
121 for task in loose_tasks {
122 let line = fmt_task_line(
123 &task,
124 &store,
125 false,
126 true,
127 false,
128 Some(id_prefix_len),
129 &today,
130 cli.no_color,
131 );
132 writeln!(
133 out,
134 "{}",
135 fmt_task_with_note(
136 line,
137 &task,
138 " ",
139 Some(id_prefix_len),
140 self.detailed,
141 cli.no_color,
142 )
143 )?;
144 }
145 }
146
147 if !projects.is_empty() {
148 writeln!(out)?;
149 for project in projects {
150 writeln!(
151 out,
152 "{}",
153 fmt_project_with_note(
154 &project,
155 &store,
156 " ",
157 Some(id_prefix_len),
158 true,
159 false,
160 self.detailed,
161 &today,
162 cli.no_color,
163 )
164 )?;
165 }
166 }
167
168 Ok(())
169 }
170}