From 8302c8839e513b55c262ce3c0a1e13b750eb1dfa Mon Sep 17 00:00:00 2001 From: Dheepak Krishnamurthy Date: Wed, 27 Sep 2023 03:23:24 -0400 Subject: [PATCH] WIP --- Cargo.lock | 1 + Cargo.toml | 2 +- src/app.rs | 3 +- src/components/task_report.rs | 40 ++++++- src/config.rs | 210 +++++++++++++++++++++++++++++++++- 5 files changed, 250 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a9a0b79..db94e89f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1285,6 +1285,7 @@ dependencies = [ "indoc", "itertools", "paste", + "serde", "strum", "time 0.3.28", "unicode-segmentation", diff --git a/Cargo.toml b/Cargo.toml index 3c8b09af..e26c93b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ log = "0.4.20" path-clean = "1.0.1" pretty_assertions = "1.4.0" rand = "0.8.5" -ratatui = { version = "0.23.0", features = ["all-widgets"] } +ratatui = { version = "0.23.0", features = ["serde", "all-widgets"] } regex = "1.9.5" rustyline = { version = "12.0.0", features = ["with-file-history", "derive"] } serde = { version = "1.0.188", features = ["derive"] } diff --git a/src/app.rs b/src/app.rs index 63cd873b..5e02af0b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,7 +33,8 @@ pub struct App { impl App { pub fn new(tick_rate: f64, frame_rate: f64, report: &str) -> Result { let app = TaskReport::new().report(report.into()); - let config = Config::new().unwrap(); + let mut config = Config::new()?; + config.taskwarrior_config()?; let mode = Mode::TaskReport; Ok(Self { tick_rate, diff --git a/src/components/task_report.rs b/src/components/task_report.rs index 4ac08b69..ba60131e 100644 --- a/src/components/task_report.rs +++ b/src/components/task_report.rs @@ -504,6 +504,38 @@ impl TaskReport { } widths } + + fn style_for_task(&self, task: &Task) -> Style { + let virtual_tag_names_in_precedence = &self.config.taskwarrior.rule_precedence_color; + + let mut style = Style::default(); + + for tag_name in virtual_tag_names_in_precedence.iter().rev() { + if tag_name == "uda." || tag_name == "priority" { + if let Some(p) = task.priority() { + let s = self.config.taskwarrior.color.uda_priority.get(p).copied().unwrap_or_default(); + style = style.patch(s); + } + } else if tag_name == "tag." { + if let Some(tags) = task.tags() { + for t in tags { + let s = self.config.taskwarrior.color.tag.get(t).copied().unwrap_or_default(); + style = style.patch(s); + } + } + } else if tag_name == "project." { + if let Some(p) = task.project() { + let s = self.config.taskwarrior.color.project.get(p).copied().unwrap_or_default(); + style = style.patch(s); + } + } else if task.tags().unwrap_or(&vec![]).contains(&tag_name.to_string().replace('.', "").to_uppercase()) { + let s = self.config.taskwarrior.color.tag.get(tag_name).copied().unwrap_or_default(); + style = style.patch(s); + } + } + + style + } } impl Component for TaskReport { @@ -539,15 +571,17 @@ impl Component for TaskReport { } let widths = self.calculate_widths(rect.width); let constraints: Vec = widths.iter().map(|i| Constraint::Min(*i as u16)).collect(); - let rows = self.rows.iter().map(|row| Row::new(row.clone())); + let rows = self.rows.iter().enumerate().map(|(i, row)| { + let style = self.style_for_task(&self.tasks[i]); + Row::new(row.clone()) + }); let table = Table::new(rows) - .header(Row::new(self.labels.clone())) + .header(Row::new(self.labels.iter().map(|l| Cell::from(l.clone()).underlined()))) .widths(&constraints) .highlight_symbol(&self.config.task_report.selection_indicator) .highlight_spacing(HighlightSpacing::Always) .column_spacing(column_spacing); f.render_stateful_widget(table, rect, &mut self.state); - Ok(()) } } diff --git a/src/config.rs b/src/config.rs index eb4e216f..68bcff0a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,75 @@ use crate::{action::Action, app::Mode}; const CONFIG: &str = include_str!("../.config/config.json5"); +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TaskwarriorColorConfig { + pub active: Style, + pub alternate: Style, + pub blocked: Style, + pub blocking: Style, + pub burndown_done: Style, + pub burndown_pending: Style, + pub burndown_started: Style, + pub calendar_due: Style, + pub calendar_due_today: Style, + pub calendar_holiday: Style, + pub calendar_overdue: Style, + pub calendar_scheduled: Style, + pub calendar_today: Style, + pub calendar_weekend: Style, + pub calendar_weeknumber: Style, + pub completed: Style, + pub debug: Style, + pub deleted: Style, + pub due: Style, + pub due_today: Style, + pub error: Style, + pub footnote: Style, + pub header: Style, + pub history_add: Style, + pub history_delete: Style, + pub history_done: Style, + pub label: Style, + pub label_sort: Style, + pub overdue: Style, + pub project_basics: Style, + pub project_none: Style, + pub project_wth: Style, + pub recurring: Style, + pub scheduled: Style, + pub summary_background: Style, + pub summary_bar: Style, + pub sync_added: Style, + pub sync_changed: Style, + pub sync_rejected: Style, + pub tag_next: Style, + pub tag_none: Style, + pub tagged: Style, + pub uda_priority: HashMap, + pub tag: HashMap, + pub project: HashMap, + pub undo_after: Style, + pub undo_before: Style, + pub until: Style, + pub warning: Style, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TaskwarriorConfig { + #[serde(default)] + pub rule_precedence_color: Vec, + #[serde(default)] + pub uda_priority_values: Vec, + #[serde(default)] + pub weekstart: bool, + #[serde(default)] + pub due: usize, + #[serde(default)] + pub color: TaskwarriorColorConfig, + #[serde(default)] + pub data_location: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct TaskReportConfig { #[serde(default)] @@ -64,8 +133,10 @@ pub struct AppConfig { #[derive(Clone, Debug, Default, Deserialize)] pub struct Config { #[serde(default)] - pub task_report: TaskReportConfig, + pub taskwarrior: TaskwarriorConfig, #[serde(default)] + pub task_report: TaskReportConfig, + #[serde(default, flatten)] pub config: AppConfig, #[serde(default)] pub keybindings: KeyBindings, @@ -113,6 +184,143 @@ impl Config { Ok(cfg) } + + pub fn taskwarrior_config(&mut self) -> Result<()> { + let output = std::process::Command::new("task") + .arg("rc.color=off") + .arg("rc._forcecolor=off") + .arg("rc.defaultwidth=0") + .arg("show") + .output()?; + + if !output.status.success() { + let output = std::process::Command::new("task").arg("diagnostics").output()?; + return Err(color_eyre::eyre::eyre!( + "Unable to run `task show`.\n{}\n{}\nPlease check your configuration or open a issue on github.", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + + let data = String::from_utf8_lossy(&output.stdout); + + self.rule_precedence_color(&data); + self.uda_priority_values(&data); + self.weekstart(&data); + self.due(&data); + self.data_location(&data); + Ok(()) + } + + fn color(&mut self, data: &str) -> Result<()> { + self.taskwarrior.color.active = parse_style(&get_config("color.active", data)?); + self.taskwarrior.color.alternate = parse_style(&get_config("color.alternate", data)?); + self.taskwarrior.color.blocked = parse_style(&get_config("color.blocked", data)?); + self.taskwarrior.color.blocking = parse_style(&get_config("color.blocking", data)?); + self.taskwarrior.color.burndown_done = parse_style(&get_config("color.burndown.done", data)?); + self.taskwarrior.color.burndown_pending = parse_style(&get_config("color.burndown.pending", data)?); + self.taskwarrior.color.burndown_started = parse_style(&get_config("color.burndown.started", data)?); + self.taskwarrior.color.calendar_due = parse_style(&get_config("color.calendar.due", data)?); + self.taskwarrior.color.calendar_due_today = parse_style(&get_config("color.calendar.due.today", data)?); + self.taskwarrior.color.calendar_holiday = parse_style(&get_config("color.calendar.holiday", data)?); + self.taskwarrior.color.calendar_overdue = parse_style(&get_config("color.calendar.overdue", data)?); + self.taskwarrior.color.calendar_scheduled = parse_style(&get_config("color.calendar.scheduled", data)?); + self.taskwarrior.color.calendar_today = parse_style(&get_config("color.calendar.today", data)?); + self.taskwarrior.color.calendar_weekend = parse_style(&get_config("color.calendar.weekend", data)?); + self.taskwarrior.color.calendar_weeknumber = parse_style(&get_config("color.calendar.weeknumber", data)?); + self.taskwarrior.color.completed = parse_style(&get_config("color.completed", data)?); + self.taskwarrior.color.debug = parse_style(&get_config("color.debug", data)?); + self.taskwarrior.color.deleted = parse_style(&get_config("color.deleted", data)?); + self.taskwarrior.color.due = parse_style(&get_config("color.due", data)?); + self.taskwarrior.color.due_today = parse_style(&get_config("color.due.today", data)?); + self.taskwarrior.color.error = parse_style(&get_config("color.error", data)?); + self.taskwarrior.color.footnote = parse_style(&get_config("color.footnote", data)?); + self.taskwarrior.color.header = parse_style(&get_config("color.header", data)?); + self.taskwarrior.color.history_add = parse_style(&get_config("color.history.add", data)?); + self.taskwarrior.color.history_delete = parse_style(&get_config("color.history.delete", data)?); + self.taskwarrior.color.history_done = parse_style(&get_config("color.history.done", data)?); + self.taskwarrior.color.label = parse_style(&get_config("color.label", data)?); + self.taskwarrior.color.label_sort = parse_style(&get_config("color.label.sort", data)?); + self.taskwarrior.color.overdue = parse_style(&get_config("color.overdue", data)?); + self.taskwarrior.color.project_basics = parse_style(&get_config("color.project.basics", data)?); + self.taskwarrior.color.project_none = parse_style(&get_config("color.project.none", data)?); + self.taskwarrior.color.project_wth = parse_style(&get_config("color.project.wth", data)?); + self.taskwarrior.color.recurring = parse_style(&get_config("color.recurring", data)?); + self.taskwarrior.color.scheduled = parse_style(&get_config("color.scheduled", data)?); + self.taskwarrior.color.summary_background = parse_style(&get_config("color.summary.background", data)?); + self.taskwarrior.color.summary_bar = parse_style(&get_config("color.summary.bar", data)?); + self.taskwarrior.color.sync_added = parse_style(&get_config("color.sync.added", data)?); + self.taskwarrior.color.sync_changed = parse_style(&get_config("color.sync.changed", data)?); + self.taskwarrior.color.sync_rejected = parse_style(&get_config("color.sync.rejected", data)?); + self.taskwarrior.color.tag_next = parse_style(&get_config("color.tag.next", data)?); + self.taskwarrior.color.tag_none = parse_style(&get_config("color.tag.none", data)?); + self.taskwarrior.color.tagged = parse_style(&get_config("color.tagged", data)?); + self.taskwarrior.color.uda_priority.insert("H".into(), parse_style(&get_config("color.uda.priority.H", data)?)); + self.taskwarrior.color.uda_priority.insert("L".into(), parse_style(&get_config("color.uda.priority.L", data)?)); + self.taskwarrior.color.uda_priority.insert("M".into(), parse_style(&get_config("color.uda.priority.M", data)?)); + self.taskwarrior.color.uda_priority.insert("U".into(), parse_style(&get_config("color.uda.priority.U", data)?)); + self.taskwarrior.color.undo_after = parse_style(&get_config("color.undo.after", data)?); + self.taskwarrior.color.undo_before = parse_style(&get_config("color.undo.before", data)?); + self.taskwarrior.color.until = parse_style(&get_config("color.until", data)?); + self.taskwarrior.color.warning = parse_style(&get_config("color.warning", data)?); + Ok(()) + } + + fn data_location(&mut self, data: &str) { + self.taskwarrior.data_location = get_config("data.location", data).unwrap(); + } + + fn rule_precedence_color(&mut self, data: &str) { + let data = get_config("rule.precedence.color", data).unwrap(); + self.taskwarrior.rule_precedence_color = data.split(',').map(ToString::to_string).collect::>(); + } + + fn weekstart(&mut self, data: &str) { + let data = try_get_config("weekstart", data).unwrap_or_default(); + self.taskwarrior.weekstart = data.eq_ignore_ascii_case("Monday"); + } + + fn due(&mut self, data: &str) { + self.taskwarrior.due = try_get_config("due", data).unwrap_or_default().parse::().unwrap_or(7) + } + + fn uda_priority_values(&mut self, data: &str) { + let data = get_config("uda.priority.values", data).unwrap(); + self.taskwarrior.uda_priority_values = data.split(',').map(ToString::to_string).collect::>(); + } +} + +fn get_config(config: &str, data: &str) -> Result { + try_get_config(config, data).ok_or(color_eyre::eyre::eyre!("Unable to parse `task show {config}`")) +} + +fn try_get_config(config: &str, data: &str) -> Option { + let mut config_lines = Vec::new(); + + for line in data.split('\n') { + if config_lines.is_empty() { + if line.starts_with(config) { + config_lines.push(line.trim_start_matches(config).trim_start().trim_end().to_string()); + } else { + let config = &config.replace('-', "_"); + if line.starts_with(config) { + config_lines.push(line.trim_start_matches(config).trim_start().trim_end().to_string()); + } + } + } else { + if !line.starts_with(" ") { + return Some(config_lines.join(" ")); + } + + config_lines.push(line.trim_start().trim_end().to_string()); + } + } + + if !config_lines.is_empty() { + return Some(config_lines.join(" ")); + } + + None } #[derive(Clone, Debug, Default, Deref, DerefMut)]