Refactor priority and effort to text labels at CLI boundary

Amolith created

Change summary

src/cli.rs             | 28 ++++++++++++++++++----------
src/cmd/list.rs        |  8 +++++++-
src/cmd/mod.rs         | 16 ++++++++++++----
src/cmd/ready.rs       | 13 ++++++++++---
src/cmd/show.rs        | 14 ++++++++++++--
src/cmd/update.rs      |  6 ++++++
src/db.rs              | 44 ++++++++++++++++++++++++++++++++++++++++++++
tests/cli_create.rs    |  2 +-
tests/cli_list_show.rs |  6 +++---
tests/cli_update.rs    |  4 ++--
10 files changed, 115 insertions(+), 26 deletions(-)

Detailed changes

src/cli.rs 🔗

@@ -26,13 +26,13 @@ pub enum Command {
         /// Task title
         title: Option<String>,
 
-        /// Priority level (1=high, 2=medium, 3=low)
-        #[arg(short, long, default_value_t = 2)]
-        priority: i32,
+        /// Priority (low, medium, high)
+        #[arg(short, long, default_value = "medium")]
+        priority: String,
 
-        /// Effort level (1=low, 2=medium, 3=high)
-        #[arg(short, long, default_value_t = 2)]
-        effort: i32,
+        /// Effort (low, medium, high)
+        #[arg(short, long, default_value = "medium")]
+        effort: String,
 
         /// Task type
         #[arg(short = 't', long = "type", default_value = "task")]
@@ -58,9 +58,13 @@ pub enum Command {
         #[arg(short, long)]
         status: Option<String>,
 
-        /// Filter by priority
+        /// Filter by priority (low, medium, high)
         #[arg(short, long)]
-        priority: Option<i32>,
+        priority: Option<String>,
+
+        /// Filter by effort (low, medium, high)
+        #[arg(short, long)]
+        effort: Option<String>,
 
         /// Filter by label
         #[arg(short, long)]
@@ -82,9 +86,13 @@ pub enum Command {
         #[arg(short, long)]
         status: Option<String>,
 
-        /// Set priority
+        /// Set priority (low, medium, high)
+        #[arg(short, long)]
+        priority: Option<String>,
+
+        /// Set effort (low, medium, high)
         #[arg(short, long)]
-        priority: Option<i32>,
+        effort: Option<String>,
 
         /// Set title
         #[arg(short = 't', long)]

src/cmd/list.rs 🔗

@@ -7,6 +7,7 @@ pub fn run(
     root: &Path,
     status: Option<&str>,
     priority: Option<i32>,
+    effort: Option<i32>,
     label: Option<&str>,
     json: bool,
 ) -> Result<()> {
@@ -29,6 +30,11 @@ pub fn run(
         params.push(Box::new(p));
         idx += 1;
     }
+    if let Some(e) = effort {
+        sql.push_str(&format!(" AND effort = ?{idx}"));
+        params.push(Box::new(e));
+        idx += 1;
+    }
     if let Some(l) = label {
         sql.push_str(&format!(
             " AND id IN (SELECT task_id FROM labels WHERE label = ?{idx})"
@@ -70,7 +76,7 @@ pub fn run(
                 format!("[{}]", t.status),
                 c.reset,
                 c.red,
-                format!("P{}", t.priority),
+                db::priority_label(t.priority),
                 c.reset,
                 t.title,
             );

src/cmd/mod.rs 🔗

@@ -43,8 +43,8 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
                 &root,
                 create::Opts {
                     title: title.as_deref(),
-                    priority: *priority,
-                    effort: *effort,
+                    priority: db::parse_priority(priority)?,
+                    effort: db::parse_effort(effort)?,
                     task_type,
                     desc: desc.as_deref(),
                     parent: parent.as_deref(),
@@ -56,13 +56,17 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
         Command::List {
             status,
             priority,
+            effort,
             label,
         } => {
             let root = require_root()?;
+            let pri = priority.as_deref().map(db::parse_priority).transpose()?;
+            let eff = effort.as_deref().map(db::parse_effort).transpose()?;
             list::run(
                 &root,
                 status.as_deref(),
-                *priority,
+                pri,
+                eff,
                 label.as_deref(),
                 cli.json,
             )
@@ -75,16 +79,20 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
             id,
             status,
             priority,
+            effort,
             title,
             desc,
         } => {
             let root = require_root()?;
+            let pri = priority.as_deref().map(db::parse_priority).transpose()?;
+            let eff = effort.as_deref().map(db::parse_effort).transpose()?;
             update::run(
                 &root,
                 id,
                 update::Opts {
                     status: status.as_deref(),
-                    priority: *priority,
+                    priority: pri,
+                    effort: eff,
                     title: title.as_deref(),
                     desc: desc.as_deref(),
                     json: cli.json,

src/cmd/ready.rs 🔗

@@ -29,7 +29,8 @@ pub fn run(root: &Path, json: bool) -> Result<()> {
                 serde_json::json!({
                     "id": t.id,
                     "title": t.title,
-                    "priority": t.priority,
+                    "priority": db::priority_label(t.priority),
+                    "effort": db::effort_label(t.effort),
                 })
             })
             .collect();
@@ -38,8 +39,14 @@ pub fn run(root: &Path, json: bool) -> Result<()> {
         let c = crate::color::stdout_theme();
         for t in &tasks {
             println!(
-                "{}{:<12}{} {}P{:<3}{} {}",
-                c.green, t.id, c.reset, c.red, t.priority, c.reset, t.title
+                "{}{:<12}{} {}{:<8}{} {}",
+                c.green,
+                t.id,
+                c.reset,
+                c.red,
+                db::priority_label(t.priority),
+                c.reset,
+                t.title
             );
         }
     }

src/cmd/show.rs 🔗

@@ -24,8 +24,18 @@ pub fn run(root: &Path, id: &str, json: bool) -> Result<()> {
         println!("{}          id{} = {}", c.bold, c.reset, t.id);
         println!("{}       title{} = {}", c.bold, c.reset, t.title);
         println!("{}      status{} = {}", c.bold, c.reset, t.status);
-        println!("{}    priority{} = {}", c.bold, c.reset, t.priority);
-        println!("{}      effort{} = {}", c.bold, c.reset, t.effort);
+        println!(
+            "{}    priority{} = {}",
+            c.bold,
+            c.reset,
+            db::priority_label(t.priority)
+        );
+        println!(
+            "{}      effort{} = {}",
+            c.bold,
+            c.reset,
+            db::effort_label(t.effort)
+        );
         println!("{}        type{} = {}", c.bold, c.reset, t.task_type);
         if !t.description.is_empty() {
             println!("{} description{} = {}", c.bold, c.reset, t.description);

src/cmd/update.rs 🔗

@@ -6,6 +6,7 @@ use crate::db;
 pub struct Opts<'a> {
     pub status: Option<&'a str>,
     pub priority: Option<i32>,
+    pub effort: Option<i32>,
     pub title: Option<&'a str>,
     pub desc: Option<&'a str>,
     pub json: bool,
@@ -29,6 +30,11 @@ pub fn run(root: &Path, id: &str, opts: Opts) -> Result<()> {
         params.push(Box::new(p));
         idx += 1;
     }
+    if let Some(e) = opts.effort {
+        sets.push(format!("effort = ?{idx}"));
+        params.push(Box::new(e));
+        idx += 1;
+    }
     if let Some(t) = opts.title {
         sets.push(format!("title = ?{idx}"));
         params.push(Box::new(t.to_string()));

src/db.rs 🔗

@@ -49,6 +49,50 @@ pub struct TaskDetail {
     pub blockers: Vec<String>,
 }
 
+/// Parse a priority label to its integer value.
+///
+/// Accepts "low" (3), "medium" (2), or "high" (1).
+pub fn parse_priority(s: &str) -> anyhow::Result<i32> {
+    match s {
+        "high" => Ok(1),
+        "medium" => Ok(2),
+        "low" => Ok(3),
+        _ => bail!("invalid priority '{s}': expected low, medium, or high"),
+    }
+}
+
+/// Convert a priority integer back to its label.
+pub fn priority_label(val: i32) -> &'static str {
+    match val {
+        1 => "high",
+        2 => "medium",
+        3 => "low",
+        _ => "unknown",
+    }
+}
+
+/// Parse an effort label to its integer value.
+///
+/// Accepts "low" (1), "medium" (2), or "high" (3).
+pub fn parse_effort(s: &str) -> anyhow::Result<i32> {
+    match s {
+        "low" => Ok(1),
+        "medium" => Ok(2),
+        "high" => Ok(3),
+        _ => bail!("invalid effort '{s}': expected low, medium, or high"),
+    }
+}
+
+/// Convert an effort integer back to its label.
+pub fn effort_label(val: i32) -> &'static str {
+    match val {
+        1 => "low",
+        2 => "medium",
+        3 => "high",
+        _ => "unknown",
+    }
+}
+
 /// Current UTC time in ISO 8601 format.
 pub fn now_utc() -> String {
     chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()

tests/cli_create.rs 🔗

@@ -41,7 +41,7 @@ fn create_json_returns_task_object() {
 fn create_with_priority_and_type() {
     let tmp = init_tmp();
 
-    td().args(["--json", "create", "Urgent bug", "-p", "1", "-t", "bug"])
+    td().args(["--json", "create", "Urgent bug", "-p", "high", "-t", "bug"])
         .current_dir(&tmp)
         .assert()
         .success()

tests/cli_list_show.rs 🔗

@@ -74,17 +74,17 @@ fn list_filter_by_status() {
 fn list_filter_by_priority() {
     let tmp = init_tmp();
 
-    td().args(["create", "Low prio", "-p", "3"])
+    td().args(["create", "Low prio", "-p", "low"])
         .current_dir(&tmp)
         .assert()
         .success();
-    td().args(["create", "High prio", "-p", "1"])
+    td().args(["create", "High prio", "-p", "high"])
         .current_dir(&tmp)
         .assert()
         .success();
 
     let out = td()
-        .args(["--json", "list", "-p", "1"])
+        .args(["--json", "list", "-p", "high"])
         .current_dir(&tmp)
         .output()
         .unwrap();

tests/cli_update.rs 🔗

@@ -53,7 +53,7 @@ fn update_changes_priority() {
     let tmp = init_tmp();
     let id = create_task(&tmp, "Reprioritise");
 
-    td().args(["update", &id, "-p", "1"])
+    td().args(["update", &id, "-p", "high"])
         .current_dir(&tmp)
         .assert()
         .success();
@@ -96,7 +96,7 @@ fn update_json_returns_task() {
     let id = create_task(&tmp, "JSON update");
 
     let out = td()
-        .args(["--json", "update", &id, "-p", "1"])
+        .args(["--json", "update", &id, "-p", "high"])
         .current_dir(&tmp)
         .output()
         .unwrap();