1use crate::Result;
2use crate::constants::*;
3use crate::query::Query;
4use crate::table::{RowStyle, Table};
5use crate::task::Task;
6use crate::taskset::TaskSet;
7use crate::util::{get_term_size, stdout_is_tty};
8use chrono::{Datelike, Utc};
9
10impl Task {
11 pub fn style(&self) -> RowStyle {
13 let now = Utc::now();
14 let mut style = RowStyle::default();
15 let active = self.status == STATUS_ACTIVE;
16 let paused = self.status == STATUS_PAUSED;
17 let resolved = self.status == STATUS_RESOLVED;
18
19 let get_fg = |normal_color: u8, active_color: u8| -> u8 {
20 if active { active_color } else { normal_color }
21 };
22
23 if self.priority == PRIORITY_CRITICAL {
25 style.fg = get_fg(FG_PRIORITY_CRITICAL, FG_ACTIVE_PRIORITY_CRITICAL);
26 } else if self.due.is_some() && self.due.unwrap() < now && !resolved {
27 style.fg = get_fg(FG_PRIORITY_HIGH, FG_ACTIVE_PRIORITY_HIGH);
29 } else if self.priority == PRIORITY_HIGH {
30 style.fg = get_fg(FG_PRIORITY_HIGH, FG_ACTIVE_PRIORITY_HIGH);
31 } else if self.priority == PRIORITY_LOW {
32 style.fg = get_fg(FG_PRIORITY_LOW, FG_ACTIVE_PRIORITY_LOW);
33 } else {
34 style.fg = get_fg(FG_DEFAULT, FG_ACTIVE);
35 }
36
37 if active {
39 style.bg = BG_ACTIVE;
40 } else if paused {
41 style.bg = BG_PAUSED;
42 }
43
44 style
45 }
46
47 pub fn display(&self) {
49 let (w, _) = get_term_size();
50 let mut table = Table::new(w, vec!["Name".to_string(), "Value".to_string()]);
51
52 table.add_row(
53 vec!["ID".to_string(), self.id.to_string()],
54 RowStyle::default(),
55 );
56 table.add_row(
57 vec!["Priority".to_string(), self.priority.clone()],
58 RowStyle::default(),
59 );
60 table.add_row(
61 vec!["Summary".to_string(), self.summary.clone()],
62 RowStyle::default(),
63 );
64 table.add_row(
65 vec!["Status".to_string(), self.status.clone()],
66 RowStyle::default(),
67 );
68 table.add_row(
69 vec!["Project".to_string(), self.project.clone()],
70 RowStyle::default(),
71 );
72 table.add_row(
73 vec!["Tags".to_string(), self.tags.join(", ")],
74 RowStyle::default(),
75 );
76 table.add_row(
77 vec!["UUID".to_string(), self.uuid.clone()],
78 RowStyle::default(),
79 );
80 table.add_row(
81 vec!["Created".to_string(), self.created.to_string()],
82 RowStyle::default(),
83 );
84
85 if let Some(resolved) = self.resolved {
86 table.add_row(
87 vec!["Resolved".to_string(), resolved.to_string()],
88 RowStyle::default(),
89 );
90 }
91
92 if let Some(due) = self.due {
93 table.add_row(
94 vec!["Due".to_string(), due.to_string()],
95 RowStyle::default(),
96 );
97 }
98
99 table.render();
100 }
101}
102
103impl TaskSet {
104 pub fn display_by_next(&mut self, ctx: &Query, truncate: bool) -> Result<()> {
106 self.sort_by_created_ascending();
107 self.sort_by_priority_ascending();
108
109 if stdout_is_tty() {
110 ctx.print_context_description();
111 self.render_table(truncate)?;
112
113 let critical_in_view = self
115 .tasks()
116 .iter()
117 .filter(|t| t.priority == PRIORITY_CRITICAL)
118 .count();
119
120 let total_critical = self
121 .all_tasks()
122 .iter()
123 .filter(|t| {
124 t.priority == PRIORITY_CRITICAL && !HIDDEN_STATUSES.contains(&t.status.as_str())
125 })
126 .count();
127
128 if critical_in_view < total_critical {
129 println!(
130 "\x1b[38;5;{}m{} critical task(s) outside this context! Use `rstask -- P0` to see them.\x1b[0m",
131 FG_PRIORITY_CRITICAL,
132 total_critical - critical_in_view
133 );
134 }
135
136 Ok(())
137 } else {
138 self.render_json()
139 }
140 }
141
142 pub fn render_json(&self) -> Result<()> {
144 let tasks: Vec<_> = self.tasks().iter().map(|t| t.to_json()).collect();
145 let json = serde_json::to_string_pretty(&tasks)?;
146 println!("{}", json);
147 Ok(())
148 }
149
150 pub fn render_table(&self, truncate: bool) -> Result<()> {
152 let tasks = self.tasks();
153 let total = tasks.len();
154
155 if tasks.is_empty() {
156 println!("No tasks found. Run `rstask help` for instructions.");
157 return Ok(());
158 }
159
160 if tasks.len() == 1 {
161 let task = tasks[0];
162 task.display();
163
164 if !task.notes.is_empty() {
165 println!(
166 "\nNotes on task {}:\n\x1b[38;5;245m{}\x1b[0m\n",
167 task.id, task.notes
168 );
169 }
170
171 return Ok(());
172 }
173
174 let (w, h) = get_term_size();
176 let max_tasks = (h.saturating_sub(TERMINAL_HEIGHT_MARGIN)).max(MIN_TASKS_SHOWN);
177
178 let display_tasks = if truncate && max_tasks < tasks.len() {
179 &tasks[..max_tasks]
180 } else {
181 &tasks[..]
182 };
183
184 let mut table = Table::new(
185 w,
186 vec![
187 "ID".to_string(),
188 "Priority".to_string(),
189 "Tags".to_string(),
190 "Due".to_string(),
191 "Project".to_string(),
192 "Summary".to_string(),
193 ],
194 );
195
196 for task in display_tasks {
197 let style = task.style();
198 table.add_row(
199 vec![
200 format!("{:<2}", task.id),
201 task.priority.clone(),
202 task.tags.join(" "),
203 task.parse_due_date_to_str(),
204 task.project.clone(),
205 task.long_summary(),
206 ],
207 style,
208 );
209 }
210
211 table.render();
212
213 if truncate && max_tasks < total {
214 println!("\n{}/{} tasks shown.", max_tasks, total);
215 } else {
216 println!("\n{} tasks.", total);
217 }
218
219 Ok(())
220 }
221
222 pub fn display_by_week(&mut self) -> Result<()> {
224 self.sort_by_resolved_ascending();
225
226 if stdout_is_tty() {
227 let (w, _) = get_term_size();
228 let mut table: Option<Table> = None;
229 let mut last_week = 0;
230
231 let tasks = self.tasks();
232
233 for task in &tasks {
234 if let Some(resolved) = task.resolved {
235 let week = resolved.iso_week().week();
236
237 if week != last_week {
238 if let Some(t) = table
239 && !t.rows.is_empty()
240 {
241 t.render();
242 }
243
244 println!(
245 "\n\n> Week {}, starting {}\n",
246 week,
247 resolved.format("%a %-d %b %Y")
248 );
249
250 table = Some(Table::new(
251 w,
252 vec![
253 "Resolved".to_string(),
254 "Priority".to_string(),
255 "Tags".to_string(),
256 "Due".to_string(),
257 "Project".to_string(),
258 "Summary".to_string(),
259 ],
260 ));
261 }
262
263 if let Some(ref mut t) = table {
264 t.add_row(
265 vec![
266 resolved.format("%a %-d").to_string(),
267 task.priority.clone(),
268 task.tags.join(" "),
269 task.parse_due_date_to_str(),
270 task.project.clone(),
271 task.long_summary(),
272 ],
273 task.style(),
274 );
275 }
276
277 last_week = week;
278 }
279 }
280
281 if let Some(t) = table {
282 t.render();
283 }
284
285 println!("{} tasks.", tasks.len());
286 Ok(())
287 } else {
288 self.render_json()
289 }
290 }
291
292 pub fn display_projects(&self) -> Result<()> {
294 if stdout_is_tty() {
295 self.render_projects_table()
296 } else {
297 self.render_projects_json()
298 }
299 }
300
301 fn render_projects_json(&self) -> Result<()> {
302 let projects = self.get_projects();
303 let json = serde_json::to_string_pretty(&projects)?;
304 println!("{}", json);
305 Ok(())
306 }
307
308 fn render_projects_table(&self) -> Result<()> {
309 let projects = self.get_projects();
310 let (w, _) = get_term_size();
311 let mut table = Table::new(
312 w,
313 vec![
314 "Name".to_string(),
315 "Progress".to_string(),
316 "Created".to_string(),
317 ],
318 );
319
320 for project in projects {
321 if project.tasks_resolved < project.tasks {
322 table.add_row(
323 vec![
324 project.name.clone(),
325 format!("{}/{}", project.tasks_resolved, project.tasks),
326 project.created.format("%a %-d %b %Y").to_string(),
327 ],
328 project.style(),
329 );
330 }
331 }
332
333 table.render();
334 Ok(())
335 }
336}