Skip to main content

rstask_core/
display.rs

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    /// Returns the row style for this task
12    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        // Determine foreground color based on priority and due date
24        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            // Overdue tasks get high priority color
28            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        // Determine background color
38        if active {
39            style.bg = BG_ACTIVE;
40        } else if paused {
41            style.bg = BG_PAUSED;
42        }
43
44        style
45    }
46
47    /// Displays a single task in detail
48    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    /// Displays tasks in "next" view (by priority and creation date)
105    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            // Count critical tasks
114            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    /// Renders tasks as JSON
143    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    /// Renders tasks as a table
151    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        // Multiple tasks - show as table
175        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    /// Displays tasks grouped by week (for show-resolved)
223    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    /// Displays projects
293    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}