Colour-code status, priority, and effort by level

Amolith created

Add per-value colour helpers to color.rs replacing the old hardcoded
single-colour-per-column approach.

CLI: open=green, in_progress=bold yellow, closed=default;
high=bold red, medium=default, low=cyan for both priority and effort.

Web UI: open=success, in_progress=warning, closed=default badge.
Project card badges are conditional on count (secondary when zero).
In-progress badge moves to front of row when nonzero.

Change summary

src/cmd/done.rs       |  2 
src/cmd/list.rs       | 12 +++---
src/cmd/ready.rs      |  8 ++--
src/cmd/reopen.rs     |  2 
src/cmd/show.rs       |  6 +-
src/color.rs          | 79 +++++++++++++++++++++++++++++++++++++++++++++
templates/index.html  | 11 ++++-
templates/macros.html |  4 +-
templates/task.html   |  2 
9 files changed, 105 insertions(+), 21 deletions(-)

Detailed changes

src/cmd/done.rs 🔗

@@ -23,7 +23,7 @@ pub fn run(root: &Path, ids: &[String], json: bool) -> Result<()> {
     } else {
         let c = crate::color::stdout_theme();
         for id in &closed {
-            println!("{}closed{} {id}", c.green, c.reset);
+            println!("{}closed{} {id}", c.status(db::Status::Closed), c.reset);
         }
     }
 

src/cmd/list.rs 🔗

@@ -1,9 +1,9 @@
 use anyhow::Result;
 use comfy_table::presets::NOTHING;
-use comfy_table::{Cell, Color, Table};
+use comfy_table::{Cell, Table};
 use std::path::Path;
 
-use crate::color::{cell_bold, cell_fg, stdout_use_color};
+use crate::color::{cell_bold, cell_effort, cell_priority, cell_status, stdout_use_color};
 use crate::db;
 
 pub fn run(
@@ -52,13 +52,13 @@ pub fn run(
         for t in &tasks {
             table.add_row(vec![
                 cell_bold(&t.id, use_color),
-                cell_fg(
+                cell_status(
                     format!("[{}]", db::status_label(t.status)),
-                    Color::Yellow,
+                    t.status,
                     use_color,
                 ),
-                cell_fg(db::priority_label(t.priority), Color::Red, use_color),
-                cell_fg(db::effort_label(t.effort), Color::Blue, use_color),
+                cell_priority(db::priority_label(t.priority), t.priority, use_color),
+                cell_effort(db::effort_label(t.effort), t.effort, use_color),
                 Cell::new(&t.title),
             ]);
         }

src/cmd/ready.rs 🔗

@@ -1,9 +1,9 @@
 use anyhow::Result;
 use comfy_table::presets::NOTHING;
-use comfy_table::{Cell, Color, Table};
+use comfy_table::{Cell, Table};
 use std::path::Path;
 
-use crate::color::{cell_bold, cell_fg, stdout_use_color};
+use crate::color::{cell_bold, cell_effort, cell_priority, stdout_use_color};
 use crate::db;
 
 pub fn run(root: &Path, json: bool) -> Result<()> {
@@ -32,8 +32,8 @@ pub fn run(root: &Path, json: bool) -> Result<()> {
         for t in &tasks {
             table.add_row(vec![
                 cell_bold(&t.id, use_color),
-                cell_fg(db::priority_label(t.priority), Color::Red, use_color),
-                cell_fg(db::effort_label(t.effort), Color::Blue, use_color),
+                cell_priority(db::priority_label(t.priority), t.priority, use_color),
+                cell_effort(db::effort_label(t.effort), t.effort, use_color),
                 Cell::new(&t.title),
             ]);
         }

src/cmd/reopen.rs 🔗

@@ -23,7 +23,7 @@ pub fn run(root: &Path, ids: &[String], json: bool) -> Result<()> {
     } else {
         let c = crate::color::stdout_theme();
         for id in &reopened {
-            println!("{}reopened{} {id}", c.green, c.reset);
+            println!("{}reopened{} {id}", c.status(db::Status::Open), c.reset);
         }
     }
 

src/cmd/show.rs 🔗

@@ -22,7 +22,7 @@ pub fn run(root: &Path, id: &str, json: bool) -> Result<()> {
         c.bold,
         task.title,
         c.reset,
-        c.yellow,
+        c.status(task.status),
         db::status_label(task.status),
         c.reset
     );
@@ -39,10 +39,10 @@ pub fn run(root: &Path, id: &str, json: bool) -> Result<()> {
         task.id,
         c.reset,
         task.task_type,
-        c.red,
+        c.priority(task.priority),
         db::priority_label(task.priority),
         c.reset,
-        c.blue,
+        c.effort(task.effort),
         db::effort_label(task.effort),
         c.reset,
     );

src/color.rs 🔗

@@ -1,12 +1,17 @@
 use comfy_table::{Attribute, Cell, Color};
 use std::io::IsTerminal;
 
+use crate::db;
+
 pub struct Theme {
     pub red: &'static str,
     pub green: &'static str,
     pub yellow: &'static str,
     pub blue: &'static str,
+    pub cyan: &'static str,
     pub bold: &'static str,
+    pub bold_red: &'static str,
+    pub bold_yellow: &'static str,
     pub reset: &'static str,
 }
 
@@ -15,7 +20,10 @@ const ON: Theme = Theme {
     green: "\x1b[32m",
     yellow: "\x1b[33m",
     blue: "\x1b[34m",
+    cyan: "\x1b[36m",
     bold: "\x1b[1m",
+    bold_red: "\x1b[1;31m",
+    bold_yellow: "\x1b[1;33m",
     reset: "\x1b[0m",
 };
 
@@ -24,7 +32,10 @@ const OFF: Theme = Theme {
     green: "",
     yellow: "",
     blue: "",
+    cyan: "",
     bold: "",
+    bold_red: "",
+    bold_yellow: "",
     reset: "",
 };
 
@@ -74,3 +85,71 @@ pub fn cell_fg(text: impl ToString, color: Color, use_color: bool) -> Cell {
         cell
     }
 }
+
+impl Theme {
+    /// ANSI escape for a task status.
+    pub fn status(&self, s: db::Status) -> &str {
+        match s {
+            db::Status::Open => self.green,
+            db::Status::InProgress => self.bold_yellow,
+            db::Status::Closed => "",
+        }
+    }
+
+    /// ANSI escape for a priority level.
+    pub fn priority(&self, p: db::Priority) -> &str {
+        match p {
+            db::Priority::High => self.bold_red,
+            db::Priority::Medium => "",
+            db::Priority::Low => self.cyan,
+        }
+    }
+
+    /// ANSI escape for an effort level.
+    pub fn effort(&self, e: db::Effort) -> &str {
+        match e {
+            db::Effort::High => self.bold_red,
+            db::Effort::Medium => "",
+            db::Effort::Low => self.cyan,
+        }
+    }
+}
+
+/// A table cell styled for a task status.
+pub fn cell_status(text: impl ToString, s: db::Status, use_color: bool) -> Cell {
+    let cell = Cell::new(text);
+    if !use_color {
+        return cell;
+    }
+    match s {
+        db::Status::Open => cell.fg(Color::Green),
+        db::Status::InProgress => cell.fg(Color::Yellow).add_attribute(Attribute::Bold),
+        db::Status::Closed => cell,
+    }
+}
+
+/// A table cell styled for a priority level.
+pub fn cell_priority(text: impl ToString, p: db::Priority, use_color: bool) -> Cell {
+    let cell = Cell::new(text);
+    if !use_color {
+        return cell;
+    }
+    match p {
+        db::Priority::High => cell.fg(Color::Red).add_attribute(Attribute::Bold),
+        db::Priority::Medium => cell,
+        db::Priority::Low => cell.fg(Color::Cyan),
+    }
+}
+
+/// A table cell styled for an effort level.
+pub fn cell_effort(text: impl ToString, e: db::Effort, use_color: bool) -> Cell {
+    let cell = Cell::new(text);
+    if !use_color {
+        return cell;
+    }
+    match e {
+        db::Effort::High => cell.fg(Color::Red).add_attribute(Attribute::Bold),
+        db::Effort::Medium => cell,
+        db::Effort::Low => cell.fg(Color::Cyan),
+    }
+}

templates/index.html 🔗

@@ -21,9 +21,14 @@
         <h2><a href="/projects/{{ name }}">{{ name }}</a></h2>
       </header>
       <div class="hstack gap-2">
-        <span class="badge">{{ open }} open</span>
-        <span class="badge secondary">{{ in_progress }} in progress</span>
-        <span class="badge success">{{ closed }} closed</span>
+        {% if *in_progress > 0 %}
+        <span class="badge warning">{{ in_progress }} in progress</span>
+        {% endif %}
+        <span class="badge{% if *open > 0 %} success{% else %} secondary{% endif %}">{{ open }} open</span>
+        {% if *in_progress == 0 %}
+        <span class="badge secondary">0 in progress</span>
+        {% endif %}
+        <span class="badge secondary">{{ closed }} closed</span>
       </div>
       {% if *total > 0 %}
       <progress value="{{ total - open - in_progress }}" max="{{ total }}" class="mt-2" aria-label="{{ closed }} of {{ total }} tasks closed in {{ name }}"></progress>

templates/macros.html 🔗

@@ -16,7 +16,7 @@
       {% for t in tasks %}
       <tr>
         <td><a href="/projects/{{ project_name }}/tasks/{{ t.full_id }}"><code>{{ t.short_id }}</code></a></td>
-        <td><span class="badge{% if t.status == "closed" %} success{% elif t.status == "in_progress" %} secondary{% endif %}">{{ t.status }}</span></td>
+        <td><span class="badge{% if t.status == "open" %} success{% elif t.status == "in_progress" %} warning{% endif %}">{{ t.status }}</span></td>
         <td>{{ t.priority }}</td>
         <td>{{ t.effort }}</td>
         <td>{{ t.title }}</td>
@@ -47,7 +47,7 @@
       {% for t in tasks %}
       <tr>
         <td><a href="/projects/{{ project_name }}/tasks/{{ t.full_id }}"><code>{{ t.short_id }}</code></a></td>
-        <td><span class="badge{% if t.status == "closed" %} success{% elif t.status == "in_progress" %} secondary{% endif %}">{{ t.status }}</span></td>
+        <td><span class="badge{% if t.status == "open" %} success{% elif t.status == "in_progress" %} warning{% endif %}">{{ t.status }}</span></td>
         <td>{{ t.priority }}</td>
         <td>{{ t.effort }}</td>
         <td>{{ t.title }}</td>

templates/task.html 🔗

@@ -20,7 +20,7 @@
 <article class="card mt-4">
   <header>
     <h1 class="task-title">{{ task.title }}</h1>
-    <span class="badge{% if task.status == "closed" %} success{% elif task.status == "in_progress" %} secondary{% endif %}">{{ task.status }}</span>
+    <span class="badge{% if task.status == "open" %} success{% elif task.status == "in_progress" %} warning{% endif %}">{{ task.status }}</span>
   </header>
 
   {% if !task.description.is_empty() %}