use colored::Colorize;
use crate::models::{Project, Recurrence, Task};
use super::formatting::{get_due_colored, get_due_text, project_colored, project_name, truncate};
const ID_WIDTH: usize = 4;
const PRIORITY_WIDTH: usize = 1;
const STATUS_WIDTH: usize = 1;
const RECUR_WIDTH: usize = 1;
pub struct TableLayout<'a> {
id: usize,
priority: usize,
status: usize,
recur: usize,
task: usize,
project: usize,
tags: usize,
due: usize,
show_recur: bool,
show_project: bool,
show_tags: bool,
show_due: bool,
show_notes: bool,
show_resources: bool,
all_tasks: &'a [Task],
projects: &'a [Project],
notes: &'a [crate::models::Note],
resources: &'a [crate::models::Resource],
}
impl<'a> TableLayout<'a> {
pub fn new(
tasks: &[(usize, &Task)],
all_tasks: &'a [Task],
projects: &'a [Project],
notes: &'a [crate::models::Note],
resources: &'a [crate::models::Resource],
) -> Self {
let (task_w, project_w, tags_w, due_w) = calculate_column_widths(tasks, projects);
let show_recur = tasks.iter().any(|(_, t)| t.recurrence.is_some());
let show_project = tasks.iter().any(|(_, t)| {
t.project_id
.and_then(|pid| projects.iter().find(|p| p.uuid == pid && !p.is_deleted()))
.is_some()
});
let show_tags = tasks.iter().any(|(_, t)| !t.tags.is_empty());
let show_due = tasks.iter().any(|(_, t)| t.due_date.is_some());
let show_notes = tasks.iter().any(|(_, t)| {
notes
.iter()
.any(|n| !n.is_deleted() && n.task_id == Some(t.uuid))
});
let show_resources = tasks.iter().any(|(_, t)| {
notes.iter().any(|n| {
!n.is_deleted()
&& n.task_id == Some(t.uuid)
&& n.resource_ids
.iter()
.any(|rid| resources.iter().any(|r| !r.is_deleted() && r.uuid == *rid))
})
});
Self {
id: ID_WIDTH,
priority: PRIORITY_WIDTH,
status: STATUS_WIDTH,
recur: RECUR_WIDTH,
task: task_w,
project: project_w,
tags: tags_w,
due: due_w,
show_recur,
show_project,
show_tags,
show_due,
show_notes,
show_resources,
all_tasks,
projects,
notes,
resources,
}
}
pub fn total_width(&self) -> usize {
let mut width = self.id + 2 + self.priority + 2 + self.status + 2 + self.task;
if self.show_recur {
width += self.recur + 2;
}
if self.show_tags {
width += self.tags + 2;
}
if self.show_project {
width += self.project + 2;
}
if self.show_due {
width += self.due + 2;
}
if self.show_notes {
width += 5 + 2;
}
if self.show_resources {
width += 3 + 2;
}
width
}
pub fn display_header(&self) {
print!("{:>id_width$} ", "ID".dimmed(), id_width = self.id);
print!("{:<p$} ", "P".dimmed(), p = self.priority);
print!("{:<s$} ", "S".dimmed(), s = self.status);
if self.show_recur {
print!("{:<r$} ", "R".dimmed(), r = self.recur);
}
if self.show_tags {
print!("{:<t$} ", "Tags".dimmed(), t = self.tags);
}
if self.show_project {
print!("{:<p$} ", "Project".dimmed(), p = self.project);
}
if self.show_due {
print!("{:<d$} ", "Due".dimmed(), d = self.due);
}
print!("{:<t$}", "Task".dimmed(), t = self.task);
if self.show_notes {
print!(" {:^5}", "Notes".dimmed());
}
if self.show_resources {
print!(" {:^3}", "Res".dimmed());
}
println!();
}
pub fn display_separator(&self) {
println!("{}", "─".repeat(self.total_width()).dimmed());
}
pub fn display_task(&self, number: usize, task: &Task) {
let blocked = !task.completed && task.is_blocked(self.all_tasks);
let status_letter = if blocked {
"B".red()
} else if task.completed {
"D".green()
} else {
"P".yellow()
};
let letter = task.priority.letter();
let task_text = truncate(&task.text, self.task);
let name = project_name(task.project_id, self.projects);
let project_str = truncate(name, self.project);
let tags_str = if task.tags.is_empty() {
"—".to_string()
} else {
truncate(&task.tags.join(", "), self.tags)
};
let due_text = get_due_text(task);
let due_colored = get_due_colored(task, &due_text);
let recur_indicator = match task.recurrence {
Some(Recurrence::Daily) => "D".cyan(),
Some(Recurrence::Weekly) => "W".cyan(),
Some(Recurrence::Monthly) => "M".cyan(),
None => " ".normal(),
};
let (text_colored, tags_colored, proj_colored) = if task.completed {
(task_text.green(), tags_str.dimmed(), project_str.dimmed())
} else if blocked {
(
task_text.truecolor(150, 150, 150),
tags_str.dimmed(),
project_str.dimmed(),
)
} else {
let tags_c = if task.tags.is_empty() {
tags_str.dimmed()
} else {
tags_str.cyan()
};
(
task_text.bright_white(),
tags_c,
project_colored(&project_str),
)
};
print!(
"{:>id_width$} ",
format!("#{}", number).dimmed(),
id_width = self.id
);
print!("{:<p$} ", letter, p = self.priority);
print!("{:<s$} ", status_letter, s = self.status);
if self.show_recur {
print!("{:<r$} ", recur_indicator, r = self.recur);
}
if self.show_tags {
print!("{:<t$} ", tags_colored, t = self.tags);
}
if self.show_project {
print!("{:<p$} ", proj_colored, p = self.project);
}
if self.show_due {
print!("{:<d$} ", due_colored, d = self.due);
}
print!("{:<t$}", text_colored, t = self.task);
if self.show_notes {
let count = self
.notes
.iter()
.filter(|n| !n.is_deleted() && n.task_id == Some(task.uuid))
.count();
let notes_str = if count > 0 {
format!("{:^5}", count).dimmed().to_string()
} else {
format!("{:^5}", "—").dimmed().to_string()
};
print!(" {}", notes_str);
}
if self.show_resources {
let count = self
.notes
.iter()
.filter(|n| !n.is_deleted() && n.task_id == Some(task.uuid))
.flat_map(|n| n.resource_ids.iter())
.filter(|rid| {
self.resources
.iter()
.any(|r| !r.is_deleted() && r.uuid == **rid)
})
.count();
let res_str = if count > 0 {
format!("{:^3}", count).dimmed().to_string()
} else {
format!("{:^3}", "—").dimmed().to_string()
};
print!(" {}", res_str);
}
println!();
}
}
fn calculate_column_widths(
tasks: &[(usize, &Task)],
projects: &[Project],
) -> (usize, usize, usize, usize) {
let mut max_task = 10;
let mut max_project = 7;
let mut max_tags = 4;
let mut max_due = 3;
for (_, task) in tasks {
max_task = max_task.max(task.text.len());
if let Some(pid) = task.project_id
&& let Some(p) = projects.iter().find(|p| p.uuid == pid && !p.is_deleted())
{
max_project = max_project.max(p.name.len());
}
if !task.tags.is_empty() {
max_tags = max_tags.max(task.tags.join(", ").len());
}
let due_text = get_due_text(task);
if !due_text.is_empty() {
max_due = max_due.max(due_text.len());
}
}
(
max_task.min(40),
max_project.min(24),
max_tags.min(20),
max_due.min(20),
)
}
pub fn display_lists(
tasks: &[(usize, &Task)],
title: &str,
all_tasks: &[Task],
projects: &[Project],
notes: &[crate::models::Note],
resources: &[crate::models::Resource],
) {
println!("\n{}:\n", title);
let layout = TableLayout::new(tasks, all_tasks, projects, notes, resources);
layout.display_header();
layout.display_separator();
let mut completed = 0;
let total = tasks.len();
for (number, task) in tasks {
layout.display_task(*number, task);
if task.completed {
completed += 1;
}
}
layout.display_separator();
let percentage = if total > 0 {
(completed as f32 / total as f32 * 100.0) as u32
} else {
0
};
let stats = format!("{} of {} completed ({}%)", completed, total, percentage);
if percentage == 100 {
println!("{}", stats.green().bold());
} else if percentage >= 50 {
println!("{}", stats.yellow());
} else {
println!("{}", stats.red());
}
println!();
}