Detailed changes
@@ -3,12 +3,13 @@ use std::path::Path;
use crate::db;
use crate::editor;
+use crate::model::{Effort, Priority};
use crate::ops;
pub struct Opts<'a> {
pub title: Option<&'a str>,
- pub priority: db::Priority,
- pub effort: db::Effort,
+ pub priority: Priority,
+ pub effort: Effort,
pub task_type: &'a str,
pub desc: Option<&'a str>,
pub parent: Option<&'a str>,
@@ -11,6 +11,7 @@ use anyhow::{anyhow, Result};
use serde::Serialize;
use crate::db;
+use crate::model::{now_utc, Status, Task, TaskId};
/// Categories of integrity issues that doctor can detect.
#[derive(Debug, Clone, Serialize)]
@@ -58,8 +59,8 @@ struct Report {
/// Repair action to apply when `--fix` is requested.
enum Repair {
- ClearParent(db::TaskId),
- RemoveBlocker(db::TaskId, db::TaskId),
+ ClearParent(TaskId),
+ RemoveBlocker(TaskId, TaskId),
}
pub fn run(root: &Path, fix: bool, json: bool) -> Result<()> {
@@ -70,7 +71,7 @@ pub fn run(root: &Path, fix: bool, json: bool) -> Result<()> {
let all_ids: HashSet<String> = tasks.iter().map(|t| t.id.as_str().to_string()).collect();
let open_ids: HashSet<String> = tasks
.iter()
- .filter(|t| t.status != db::Status::Closed && t.deleted_at.is_none())
+ .filter(|t| t.status != Status::Closed && t.deleted_at.is_none())
.map(|t| t.id.as_str().to_string())
.collect();
@@ -85,7 +86,7 @@ pub fn run(root: &Path, fix: bool, json: bool) -> Result<()> {
if fix && !repairs.is_empty() {
store.apply_and_persist(|doc| {
let tasks_map = doc.get_map("tasks");
- let ts = db::now_utc();
+ let ts = now_utc();
for repair in &repairs {
match repair {
@@ -130,12 +131,12 @@ pub fn run(root: &Path, fix: bool, json: bool) -> Result<()> {
///
/// Skips tombstoned tasks — their stale references are not actionable.
fn check_dangling_parents(
- tasks: &[db::Task],
+ tasks: &[Task],
all_ids: &HashSet<String>,
findings: &mut Vec<Finding>,
repairs: &mut Vec<Repair>,
) {
- let task_map: std::collections::HashMap<&str, &db::Task> =
+ let task_map: std::collections::HashMap<&str, &Task> =
tasks.iter().map(|t| (t.id.as_str(), t)).collect();
for task in tasks {
@@ -154,7 +155,7 @@ fn check_dangling_parents(
task: task.id.to_string(),
detail: format!(
"parent references missing task {}",
- db::TaskId::display_id(parent.as_str()),
+ TaskId::display_id(parent.as_str()),
),
active: true,
fixed: false,
@@ -171,7 +172,7 @@ fn check_dangling_parents(
task: task.id.to_string(),
detail: format!(
"parent references tombstoned task {}",
- db::TaskId::display_id(parent.as_str()),
+ TaskId::display_id(parent.as_str()),
),
active: true,
fixed: false,
@@ -186,7 +187,7 @@ fn check_dangling_parents(
///
/// Skips tombstoned tasks — their stale references are not actionable.
fn check_dangling_blockers(
- tasks: &[db::Task],
+ tasks: &[Task],
all_ids: &HashSet<String>,
findings: &mut Vec<Finding>,
repairs: &mut Vec<Repair>,
@@ -203,7 +204,7 @@ fn check_dangling_blockers(
task: task.id.to_string(),
detail: format!(
"blocker references missing task {}",
- db::TaskId::display_id(blocker.as_str()),
+ TaskId::display_id(blocker.as_str()),
),
active: true,
fixed: false,
@@ -225,7 +226,7 @@ fn check_dangling_blockers(
/// are "inert" — `partition_blockers` already resolves them at runtime.
/// Only active cycles are repaired by `--fix`.
fn check_blocker_cycles(
- tasks: &[db::Task],
+ tasks: &[Task],
all_ids: &HashSet<String>,
open_ids: &HashSet<String>,
findings: &mut Vec<Finding>,
@@ -267,12 +268,12 @@ fn check_blocker_cycles(
.all(|id| open_ids.contains(id));
// Build a human-readable cycle string using short IDs.
- let display: Vec<String> = cycle.iter().map(|id| db::TaskId::display_id(id)).collect();
+ let display: Vec<String> = cycle.iter().map(|id| TaskId::display_id(id)).collect();
let cycle_str = display.join(" → ");
findings.push(Finding {
kind: FindingKind::BlockerCycle,
- task: db::TaskId::display_id(&task_id),
+ task: TaskId::display_id(&task_id),
detail: cycle_str,
active,
fixed: false,
@@ -280,8 +281,8 @@ fn check_blocker_cycles(
if active {
repairs.push(Repair::RemoveBlocker(
- db::TaskId::parse(&task_id)?,
- db::TaskId::parse(&blocker_id)?,
+ TaskId::parse(&task_id)?,
+ TaskId::parse(&blocker_id)?,
));
}
@@ -301,7 +302,7 @@ fn check_blocker_cycles(
/// current path, we have a cycle. Repairs clear the parent field on the
/// task with the lexicographically lowest ULID in the cycle.
fn check_parent_cycles(
- tasks: &[db::Task],
+ tasks: &[Task],
all_ids: &HashSet<String>,
findings: &mut Vec<Finding>,
repairs: &mut Vec<Repair>,
@@ -338,8 +339,7 @@ fn check_parent_cycles(
let mut cycle: Vec<String> = path[pos..].to_vec();
cycle.push(node); // close the loop
- let display: Vec<String> =
- cycle.iter().map(|id| db::TaskId::display_id(id)).collect();
+ let display: Vec<String> = cycle.iter().map(|id| TaskId::display_id(id)).collect();
let cycle_str = display.join(" → ");
// The task with the lowest ULID gets its parent cleared.
@@ -351,12 +351,12 @@ fn check_parent_cycles(
findings.push(Finding {
kind: FindingKind::ParentCycle,
- task: db::TaskId::display_id(&lowest),
+ task: TaskId::display_id(&lowest),
detail: cycle_str,
active: true,
fixed: false,
});
- repairs.push(Repair::ClearParent(db::TaskId::parse(&lowest)?));
+ repairs.push(Repair::ClearParent(TaskId::parse(&lowest)?));
break;
}
if globally_visited.contains(&node) {
@@ -2,6 +2,7 @@ use anyhow::Result;
use std::path::Path;
use crate::db;
+use crate::model::Status;
use crate::ops;
pub fn run(root: &Path, ids: &[String], json: bool) -> Result<()> {
@@ -23,7 +24,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.status(db::Status::Closed), c.reset);
+ println!("{}closed{} {id}", c.status(Status::Closed), c.reset);
}
}
@@ -4,6 +4,7 @@ use std::io::BufRead;
use std::path::Path;
use crate::db;
+use crate::model::{Effort, Priority, Status, TaskId};
#[derive(Deserialize)]
struct ImportTask {
@@ -68,10 +69,10 @@ pub fn run(root: &Path, file: &str) -> Result<()> {
continue;
}
let t: ImportTask = serde_json::from_str(&line)?;
- let id = db::TaskId::parse(&t.id)?;
- db::parse_priority(&t.priority)?;
- db::parse_status(&t.status)?;
- db::parse_effort(&t.effort)?;
+ let id = TaskId::parse(&t.id)?;
+ Priority::parse(&t.priority)?;
+ Status::parse(&t.status)?;
+ Effort::parse(&t.effort)?;
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
@@ -99,12 +100,12 @@ pub fn run(root: &Path, file: &str) -> Result<()> {
let blockers = db::get_or_create_child_map(&task, "blockers")?;
for blk in &t.blockers {
let parsed =
- db::TaskId::parse(blk).map_err(|_| anyhow!("invalid blocker id '{blk}'"))?;
+ TaskId::parse(blk).map_err(|_| anyhow!("invalid blocker id '{blk}'"))?;
blockers.insert(parsed.as_str(), true)?;
}
let logs = db::get_or_create_child_map(&task, "logs")?;
for entry in &t.logs {
- let log_id = db::TaskId::parse(&entry.id)
+ let log_id = TaskId::parse(&entry.id)
.map_err(|_| anyhow!("invalid log id '{}'", entry.id))?;
let record = logs.get_or_create_container(log_id.as_str(), loro::LoroMap::new())?;
record.insert("timestamp", entry.timestamp.clone())?;
@@ -5,12 +5,13 @@ use std::path::Path;
use crate::color::{cell_bold, cell_effort, cell_priority, cell_status, stdout_use_color};
use crate::db;
+use crate::model::{Effort, Priority, Status};
pub fn run(
root: &Path,
status: Option<&str>,
- priority: Option<db::Priority>,
- effort: Option<db::Effort>,
+ priority: Option<Priority>,
+ effort: Option<Effort>,
label: Option<&str>,
json: bool,
) -> Result<()> {
@@ -18,7 +19,7 @@ pub fn run(
let mut tasks = store.list_tasks()?;
if let Some(s) = status {
- let parsed = db::parse_status(s)?;
+ let parsed = Status::parse(s)?;
tasks.retain(|t| t.status == parsed);
}
if let Some(p) = priority {
@@ -52,13 +53,9 @@ pub fn run(
for t in &tasks {
table.add_row(vec![
cell_bold(&t.id, use_color),
- cell_status(
- format!("[{}]", db::status_label(t.status)),
- t.status,
- 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_status(format!("[{}]", t.status.as_str()), t.status, use_color),
+ cell_priority(t.priority.as_str(), t.priority, use_color),
+ cell_effort(t.effort.as_str(), t.effort, use_color),
Cell::new(&t.title),
]);
}
@@ -23,6 +23,7 @@ mod webui;
use crate::cli::{Cli, Command};
use crate::db;
+use crate::model::{Effort, Priority};
use anyhow::Result;
fn require_root() -> Result<std::path::PathBuf> {
@@ -50,8 +51,8 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
&root,
create::Opts {
title: title.as_deref(),
- priority: db::parse_priority(priority)?,
- effort: db::parse_effort(effort)?,
+ priority: Priority::parse(priority)?,
+ effort: Effort::parse(effort)?,
task_type,
desc: desc.as_deref(),
parent: parent.as_deref(),
@@ -67,8 +68,8 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
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()?;
+ let pri = priority.as_deref().map(Priority::parse).transpose()?;
+ let eff = effort.as_deref().map(Effort::parse).transpose()?;
list::run(
&root,
status.as_deref(),
@@ -95,8 +96,8 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
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()?;
+ let pri = priority.as_deref().map(Priority::parse).transpose()?;
+ let eff = effort.as_deref().map(Effort::parse).transpose()?;
update::run(
&root,
id,
@@ -6,6 +6,7 @@ use std::path::Path;
use crate::color::{cell_bold, stdout_use_color};
use crate::db;
+use crate::model::{Status, TaskId};
use crate::score::{self, Mode};
fn parse_mode(s: &str) -> Result<Mode> {
@@ -23,20 +24,20 @@ pub fn run(root: &Path, mode_str: &str, verbose: bool, limit: usize, json: bool)
let open_tasks: Vec<score::TaskInput> = all
.iter()
- .filter(|t| t.status == db::Status::Open)
+ .filter(|t| t.status == Status::Open)
.map(|t| score::TaskInput {
id: t.id.to_string(),
title: t.title.clone(),
priority_score: t.priority.score(),
effort_score: t.effort.score(),
- priority_label: db::priority_label(t.priority).to_string(),
- effort_label: db::effort_label(t.effort).to_string(),
+ priority_label: t.priority.as_str().to_string(),
+ effort_label: t.effort.as_str().to_string(),
})
.collect();
let edges: Vec<(String, String)> = all
.iter()
- .filter(|t| t.status == db::Status::Open)
+ .filter(|t| t.status == Status::Open)
.flat_map(|t| {
t.blockers
.iter()
@@ -47,7 +48,7 @@ pub fn run(root: &Path, mode_str: &str, verbose: bool, limit: usize, json: bool)
let parents_with_open_children: HashSet<String> = all
.iter()
- .filter(|t| t.status == db::Status::Open)
+ .filter(|t| t.status == Status::Open)
.filter_map(|t| t.parent.as_ref().map(ToString::to_string))
.collect();
@@ -96,7 +97,7 @@ pub fn run(root: &Path, mode_str: &str, verbose: bool, limit: usize, json: bool)
table.set_header(vec!["#", "ID", "SCORE", "TITLE"]);
for (i, s) in scored.iter().enumerate() {
- let short = db::TaskId::display_id(&s.id);
+ let short = TaskId::display_id(&s.id);
table.add_row(vec![
Cell::new(i + 1),
cell_bold(&short, use_color),
@@ -113,7 +114,7 @@ pub fn run(root: &Path, mode_str: &str, verbose: bool, limit: usize, json: bool)
};
println!("\nmode: {mode_label}");
for (i, s) in scored.iter().enumerate() {
- let short = db::TaskId::display_id(&s.id);
+ let short = TaskId::display_id(&s.id);
let formula = match mode {
Mode::Impact => format!(
"({:.2} + 1.00) × {:.2} / {:.2}^0.25 = {:.2}",
@@ -5,13 +5,14 @@ use std::path::Path;
use crate::color::{cell_bold, cell_effort, cell_priority, stdout_use_color};
use crate::db;
+use crate::model::Status;
pub fn run(root: &Path, json: bool) -> Result<()> {
let store = db::open(root)?;
let mut tasks = Vec::new();
for task in store.list_tasks()? {
- if task.status != db::Status::Open {
+ if task.status != Status::Open {
continue;
}
let blockers = db::partition_blockers(&store, &task.blockers)?;
@@ -32,8 +33,8 @@ pub fn run(root: &Path, json: bool) -> Result<()> {
for t in &tasks {
table.add_row(vec![
cell_bold(&t.id, 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_priority(t.priority.as_str(), t.priority, use_color),
+ cell_effort(t.effort.as_str(), t.effort, use_color),
Cell::new(&t.title),
]);
}
@@ -2,6 +2,7 @@ use anyhow::Result;
use std::path::Path;
use crate::db;
+use crate::model::Status;
use crate::ops;
pub fn run(root: &Path, ids: &[String], json: bool) -> Result<()> {
@@ -23,7 +24,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.status(db::Status::Open), c.reset);
+ println!("{}reopened{} {id}", c.status(Status::Open), c.reset);
}
}
@@ -3,6 +3,7 @@ use serde::Serialize;
use std::path::Path;
use crate::db;
+use crate::model::TaskId;
use crate::ops;
#[derive(Serialize)]
@@ -15,7 +16,7 @@ struct RmResult {
pub fn run(root: &Path, ids: &[String], recursive: bool, force: bool, json: bool) -> Result<()> {
let store = db::open(root)?;
- let resolved: Vec<db::TaskId> = ids
+ let resolved: Vec<TaskId> = ids
.iter()
.map(|raw| db::resolve_task_id(&store, raw, false))
.collect::<Result<_>>()?;
@@ -5,12 +5,13 @@ use std::path::Path;
use crate::color::{cell_bold, stdout_use_color};
use crate::db;
+use crate::model::Task;
pub fn run(root: &Path, query: &str, json: bool) -> Result<()> {
let store = db::open(root)?;
let q = query.to_lowercase();
- let tasks: Vec<db::Task> = store
+ let tasks: Vec<Task> = store
.list_tasks()?
.into_iter()
.filter(|t| {
@@ -23,7 +23,7 @@ pub fn run(root: &Path, id: &str, json: bool) -> Result<()> {
task.title,
c.reset,
c.status(task.status),
- db::status_label(task.status),
+ task.status.as_str(),
c.reset
);
@@ -40,10 +40,10 @@ pub fn run(root: &Path, id: &str, json: bool) -> Result<()> {
c.reset,
task.task_type,
c.priority(task.priority),
- db::priority_label(task.priority),
+ task.priority.as_str(),
c.reset,
c.effort(task.effort),
- db::effort_label(task.effort),
+ task.effort.as_str(),
c.reset,
);
@@ -2,24 +2,19 @@ use anyhow::Result;
use std::path::Path;
use crate::db;
+use crate::model::Status;
pub fn run(root: &Path) -> Result<()> {
let store = db::open(root)?;
let tasks = store.list_tasks()?;
let total = tasks.len();
- let open = tasks
- .iter()
- .filter(|t| t.status == db::Status::Open)
- .count();
+ let open = tasks.iter().filter(|t| t.status == Status::Open).count();
let in_progress = tasks
.iter()
- .filter(|t| t.status == db::Status::InProgress)
- .count();
- let closed = tasks
- .iter()
- .filter(|t| t.status == db::Status::Closed)
+ .filter(|t| t.status == Status::InProgress)
.count();
+ let closed = tasks.iter().filter(|t| t.status == Status::Closed).count();
println!(
"{}",
@@ -3,12 +3,13 @@ use std::path::Path;
use crate::db;
use crate::editor;
+use crate::model::{Effort, Priority, Status};
use crate::ops;
pub struct Opts<'a> {
pub status: Option<&'a str>,
- pub priority: Option<db::Priority>,
- pub effort: Option<db::Effort>,
+ pub priority: Option<Priority>,
+ pub effort: Option<Effort>,
pub title: Option<&'a str>,
pub desc: Option<&'a str>,
pub json: bool,
@@ -18,7 +19,7 @@ pub fn run(root: &Path, id: &str, opts: Opts) -> Result<()> {
let store = db::open(root)?;
let task_id = db::resolve_task_id(&store, id, false)?;
- let parsed_status = opts.status.map(db::parse_status).transpose()?;
+ let parsed_status = opts.status.map(Status::parse).transpose()?;
// If no fields were supplied, open the editor so the user can revise the
// task's title and description interactively.
@@ -4,7 +4,8 @@ use anyhow::Result;
use axum::extract::{Path as AxumPath, Query, State};
use axum::response::Response;
-use crate::db::{self, Store, TaskId};
+use crate::db::{self, Store};
+use crate::model::{Effort, Priority, Status, Task, TaskId};
use crate::score;
use super::helpers::{
@@ -29,18 +30,12 @@ pub(super) async fn index_handler(State(state): State<AppState>) -> Response {
match Store::open(&root, name) {
Ok(store) => {
let tasks = store.list_tasks()?;
- let open = tasks
- .iter()
- .filter(|t| t.status == db::Status::Open)
- .count();
+ let open = tasks.iter().filter(|t| t.status == Status::Open).count();
let in_progress = tasks
.iter()
- .filter(|t| t.status == db::Status::InProgress)
- .count();
- let closed = tasks
- .iter()
- .filter(|t| t.status == db::Status::Closed)
+ .filter(|t| t.status == Status::InProgress)
.count();
+ let closed = tasks.iter().filter(|t| t.status == Status::Closed).count();
cards.push(ProjectCard::Ok {
name: name.clone(),
open,
@@ -101,18 +96,12 @@ pub(super) async fn project_handler(
let tasks = store.list_tasks()?;
// Stats from the full unfiltered set.
- let stats_open = tasks
- .iter()
- .filter(|t| t.status == db::Status::Open)
- .count();
+ let stats_open = tasks.iter().filter(|t| t.status == Status::Open).count();
let stats_in_progress = tasks
.iter()
- .filter(|t| t.status == db::Status::InProgress)
- .count();
- let stats_closed = tasks
- .iter()
- .filter(|t| t.status == db::Status::Closed)
+ .filter(|t| t.status == Status::InProgress)
.count();
+ let stats_closed = tasks.iter().filter(|t| t.status == Status::Closed).count();
// Collect distinct labels for the filter dropdown.
let mut label_set: HashSet<String> = HashSet::new();
@@ -127,20 +116,20 @@ pub(super) async fn project_handler(
// Next-up scoring (top 5 open tasks).
let open_tasks: Vec<score::TaskInput> = tasks
.iter()
- .filter(|t| t.status == db::Status::Open)
+ .filter(|t| t.status == Status::Open)
.map(|t| score::TaskInput {
id: t.id.as_str().to_string(),
title: t.title.clone(),
priority_score: t.priority.score(),
effort_score: t.effort.score(),
- priority_label: db::priority_label(t.priority).to_string(),
- effort_label: db::effort_label(t.effort).to_string(),
+ priority_label: t.priority.as_str().to_string(),
+ effort_label: t.effort.as_str().to_string(),
})
.collect();
let edges: Vec<(String, String)> = tasks
.iter()
- .filter(|t| t.status == db::Status::Open)
+ .filter(|t| t.status == Status::Open)
.flat_map(|t| {
t.blockers
.iter()
@@ -151,7 +140,7 @@ pub(super) async fn project_handler(
let parents_with_open_children: HashSet<String> = tasks
.iter()
- .filter(|t| t.status == db::Status::Open)
+ .filter(|t| t.status == Status::Open)
.filter_map(|t| t.parent.as_ref().map(|p| p.as_str().to_string()))
.collect();
@@ -176,25 +165,25 @@ pub(super) async fn project_handler(
.collect();
// Apply filters.
- let mut filtered: Vec<&db::Task> = tasks.iter().collect();
+ let mut filtered: Vec<&Task> = tasks.iter().collect();
if let Some(ref s) = query.status {
if !s.is_empty() {
- if let Ok(parsed) = db::parse_status(s) {
+ if let Ok(parsed) = Status::parse(s) {
filtered.retain(|t| t.status == parsed);
}
}
}
if let Some(ref p) = query.priority {
if !p.is_empty() {
- if let Ok(parsed) = db::parse_priority(p) {
+ if let Ok(parsed) = Priority::parse(p) {
filtered.retain(|t| t.priority == parsed);
}
}
}
if let Some(ref e) = query.effort {
if !e.is_empty() {
- if let Ok(parsed) = db::parse_effort(e) {
+ if let Ok(parsed) = Effort::parse(e) {
filtered.retain(|t| t.effort == parsed);
}
}
@@ -237,14 +226,14 @@ pub(super) async fn project_handler(
let page_tasks: Vec<TaskRow> = filtered[start..end]
.iter()
.map(|t| {
- let status = db::status_label(t.status).to_string();
+ let status = t.status.as_str().to_string();
TaskRow {
full_id: t.id.as_str().to_string(),
short_id: t.id.short(),
status_display: friendly_status(&status),
status,
- priority: db::priority_label(t.priority).to_string(),
- effort: db::effort_label(t.effort).to_string(),
+ priority: t.priority.as_str().to_string(),
+ effort: t.effort.as_str().to_string(),
title: t.title.clone(),
created_at_display: friendly_date(&t.created_at),
created_at: t.created_at.clone(),
@@ -350,14 +339,14 @@ pub(super) async fn task_handler(
.iter()
.filter(|t| t.parent.as_ref() == Some(&task_id))
.map(|t| {
- let status = db::status_label(t.status).to_string();
+ let status = t.status.as_str().to_string();
TaskRow {
full_id: t.id.as_str().to_string(),
short_id: t.id.short(),
status_display: friendly_status(&status),
status,
- priority: db::priority_label(t.priority).to_string(),
- effort: db::effort_label(t.effort).to_string(),
+ priority: t.priority.as_str().to_string(),
+ effort: t.effort.as_str().to_string(),
title: t.title.clone(),
created_at_display: friendly_date(&t.created_at),
created_at: t.created_at.clone(),
@@ -371,9 +360,9 @@ pub(super) async fn task_handler(
title: task.title.clone(),
description: render_markdown(&task.description),
task_type: task.task_type.clone(),
- status: db::status_label(task.status).to_string(),
- priority: db::priority_label(task.priority).to_string(),
- effort: db::effort_label(task.effort).to_string(),
+ status: task.status.as_str().to_string(),
+ priority: task.priority.as_str().to_string(),
+ effort: task.effort.as_str().to_string(),
created_at_display: friendly_date(&task.created_at),
created_at: task.created_at.clone(),
updated_at_display: friendly_date(&task.updated_at),
@@ -4,6 +4,7 @@ use axum::response::{Html, IntoResponse, Redirect, Response};
use axum::Json;
use crate::db;
+use crate::model::{Status, Task};
use super::views::ErrorTemplate;
@@ -79,16 +80,16 @@ impl SortOrder {
/// Map a `Status` to a numeric value for semantic sorting.
/// Lower values sort first in ascending order: open → in_progress → closed.
-fn status_sort_key(s: db::Status) -> i32 {
+fn status_sort_key(s: Status) -> i32 {
match s {
- db::Status::Open => 1,
- db::Status::InProgress => 2,
- db::Status::Closed => 3,
+ Status::Open => 1,
+ Status::InProgress => 2,
+ Status::Closed => 3,
}
}
/// Apply the chosen sort field and direction to a filtered task list.
-pub(super) fn sort_tasks(tasks: &mut [&db::Task], field: SortField, order: SortOrder) {
+pub(super) fn sort_tasks(tasks: &mut [&Task], field: SortField, order: SortOrder) {
tasks.sort_by(|a, b| {
let cmp = match field {
SortField::Id => a.id.as_str().cmp(b.id.as_str()),
@@ -4,7 +4,8 @@ use axum::http::HeaderMap;
use axum::response::Response;
use axum::Form;
-use crate::db::{self, Store, TaskId};
+use crate::db::{self, Store};
+use crate::model::{Effort, LogEntry, Priority, Status, Task, TaskId};
use crate::ops;
use super::helpers::{mutation_error, mutation_response};
@@ -115,7 +116,7 @@ pub(super) async fn create_handler(
Form(form): Form<CreateForm>,
) -> Response {
let root = state.data_root.clone();
- let result = tokio::task::spawn_blocking(move || -> Result<(db::Task, String)> {
+ let result = tokio::task::spawn_blocking(move || -> Result<(Task, String)> {
let store = Store::open(&root, &name)?;
let parent = if form.parent.is_empty() {
@@ -138,8 +139,8 @@ pub(super) async fn create_handler(
title: form.title,
description: form.description,
task_type: form.task_type,
- priority: db::parse_priority(&form.priority)?,
- effort: db::parse_effort(&form.effort)?,
+ priority: Priority::parse(&form.priority)?,
+ effort: Effort::parse(&form.effort)?,
parent,
labels,
},
@@ -168,7 +169,7 @@ pub(super) async fn update_handler(
Form(form): Form<UpdateForm>,
) -> Response {
let root = state.data_root.clone();
- let result = tokio::task::spawn_blocking(move || -> Result<(db::Task, String)> {
+ let result = tokio::task::spawn_blocking(move || -> Result<(Task, String)> {
let store = Store::open(&root, &name)?;
let task_id = db::resolve_task_id(&store, &id, false)?;
@@ -180,19 +181,19 @@ pub(super) async fn update_handler(
.status
.as_deref()
.filter(|s| !s.is_empty())
- .map(db::parse_status)
+ .map(Status::parse)
.transpose()?,
priority: form
.priority
.as_deref()
.filter(|s| !s.is_empty())
- .map(db::parse_priority)
+ .map(Priority::parse)
.transpose()?,
effort: form
.effort
.as_deref()
.filter(|s| !s.is_empty())
- .map(db::parse_effort)
+ .map(Effort::parse)
.transpose()?,
title: form.title.filter(|s| !s.is_empty()),
description: form.description,
@@ -225,7 +226,7 @@ pub(super) async fn log_handler(
Form(form): Form<LogForm>,
) -> Response {
let root = state.data_root.clone();
- let result = tokio::task::spawn_blocking(move || -> Result<(db::LogEntry, String)> {
+ let result = tokio::task::spawn_blocking(move || -> Result<(LogEntry, String)> {
let store = Store::open(&root, &name)?;
let task_id = db::resolve_task_id(&store, &id, false)?;
let entry = ops::add_log(&store, &task_id, &form.message)?;
@@ -1,7 +1,7 @@
use comfy_table::{Attribute, Cell, Color};
use std::io::IsTerminal;
-use crate::db;
+use crate::model::{Effort, Priority, Status};
pub struct Theme {
pub red: &'static str,
@@ -88,68 +88,68 @@ pub fn cell_fg(text: impl ToString, color: Color, use_color: bool) -> Cell {
impl Theme {
/// ANSI escape for a task status.
- pub fn status(&self, s: db::Status) -> &str {
+ pub fn status(&self, s: Status) -> &str {
match s {
- db::Status::Open => self.green,
- db::Status::InProgress => self.bold_yellow,
- db::Status::Closed => "",
+ Status::Open => self.green,
+ Status::InProgress => self.bold_yellow,
+ Status::Closed => "",
}
}
/// ANSI escape for a priority level.
- pub fn priority(&self, p: db::Priority) -> &str {
+ pub fn priority(&self, p: Priority) -> &str {
match p {
- db::Priority::High => self.bold_red,
- db::Priority::Medium => "",
- db::Priority::Low => self.cyan,
+ Priority::High => self.bold_red,
+ Priority::Medium => "",
+ Priority::Low => self.cyan,
}
}
/// ANSI escape for an effort level.
- pub fn effort(&self, e: db::Effort) -> &str {
+ pub fn effort(&self, e: Effort) -> &str {
match e {
- db::Effort::High => self.bold_red,
- db::Effort::Medium => "",
- db::Effort::Low => self.cyan,
+ Effort::High => self.bold_red,
+ Effort::Medium => "",
+ 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 {
+pub fn cell_status(text: impl ToString, s: 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,
+ Status::Open => cell.fg(Color::Green),
+ Status::InProgress => cell.fg(Color::Yellow).add_attribute(Attribute::Bold),
+ 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 {
+pub fn cell_priority(text: impl ToString, p: 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),
+ Priority::High => cell.fg(Color::Red).add_attribute(Attribute::Bold),
+ Priority::Medium => cell,
+ 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 {
+pub fn cell_effort(text: impl ToString, e: 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),
+ Effort::High => cell.fg(Color::Red).add_attribute(Attribute::Bold),
+ Effort::Medium => cell,
+ Effort::Low => cell.fg(Color::Cyan),
}
}
@@ -3,7 +3,6 @@ use loro::{Container, ExportMode, LoroDoc, LoroMap, PeerID, ValueOrContainer};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
-use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -11,6 +10,7 @@ use std::path::{Path, PathBuf};
use fs2::FileExt;
use ulid::Ulid;
+use crate::model::{now_utc, BlockerPartition, Effort, LogEntry, Priority, Status, Task, TaskId};
pub const PROJECT_ENV: &str = "TD_PROJECT";
pub(crate) const PROJECTS_DIR: &str = "projects";
@@ -20,226 +20,6 @@ const BASE_FILE: &str = "base.loro";
const TMP_SUFFIX: &str = ".tmp";
use crate::migrate;
-/// Current UTC time in ISO 8601 format.
-pub fn now_utc() -> String {
- chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()
-}
-
-/// Lifecycle state for a task.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum Status {
- Open,
- InProgress,
- Closed,
-}
-
-impl Status {
- fn as_str(self) -> &'static str {
- match self {
- Status::Open => "open",
- Status::InProgress => "in_progress",
- Status::Closed => "closed",
- }
- }
-
- fn parse(raw: &str) -> Result<Self> {
- match raw {
- "open" => Ok(Self::Open),
- "in_progress" => Ok(Self::InProgress),
- "closed" => Ok(Self::Closed),
- _ => bail!("invalid status '{raw}'"),
- }
- }
-}
-
-/// Priority for task ordering.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum Priority {
- High,
- Medium,
- Low,
-}
-
-impl Priority {
- fn as_str(self) -> &'static str {
- match self {
- Priority::High => "high",
- Priority::Medium => "medium",
- Priority::Low => "low",
- }
- }
-
- fn parse(raw: &str) -> Result<Self> {
- match raw {
- "high" => Ok(Self::High),
- "medium" => Ok(Self::Medium),
- "low" => Ok(Self::Low),
- _ => bail!("invalid priority '{raw}'"),
- }
- }
-
- pub fn score(self) -> i32 {
- match self {
- Priority::High => 1,
- Priority::Medium => 2,
- Priority::Low => 3,
- }
- }
-}
-
-/// Estimated effort for a task.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum Effort {
- Low,
- Medium,
- High,
-}
-
-impl Effort {
- fn as_str(self) -> &'static str {
- match self {
- Effort::Low => "low",
- Effort::Medium => "medium",
- Effort::High => "high",
- }
- }
-
- fn parse(raw: &str) -> Result<Self> {
- match raw {
- "low" => Ok(Self::Low),
- "medium" => Ok(Self::Medium),
- "high" => Ok(Self::High),
- _ => bail!("invalid effort '{raw}'"),
- }
- }
-
- pub fn score(self) -> i32 {
- match self {
- Effort::Low => 1,
- Effort::Medium => 2,
- Effort::High => 3,
- }
- }
-}
-
-/// A stable task identifier backed by a ULID.
-///
-/// Serializes as the short display form (`td-XXXXXXX`) for user-facing
-/// JSON. Use [`TaskId::as_str`] when the full ULID is needed (e.g.
-/// for CRDT keys or export round-tripping).
-#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct TaskId(String);
-
-impl Serialize for TaskId {
- fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
- serializer.serialize_str(&self.short())
- }
-}
-
-impl TaskId {
- pub fn new(id: Ulid) -> Self {
- Self(id.to_string())
- }
-
- pub fn parse(raw: &str) -> Result<Self> {
- let id = Ulid::from_string(raw).with_context(|| format!("invalid task id '{raw}'"))?;
- Ok(Self::new(id))
- }
-
- pub fn as_str(&self) -> &str {
- &self.0
- }
-
- pub fn short(&self) -> String {
- format!("td-{}", &self.0[self.0.len() - 7..])
- }
-
- /// Return a display-friendly short ID from a raw ULID string.
- pub fn display_id(raw: &str) -> String {
- let n = raw.len();
- if n > 7 {
- format!("td-{}", &raw[n - 7..])
- } else {
- format!("td-{raw}")
- }
- }
-}
-
-impl fmt::Display for TaskId {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", self.short())
- }
-}
-
-/// A task log entry embedded in a task record.
-#[derive(Debug, Clone, Serialize)]
-pub struct LogEntry {
- pub id: TaskId,
- pub timestamp: String,
- pub message: String,
-}
-
-/// Hydrated task data from the CRDT document.
-#[derive(Debug, Clone, Serialize)]
-pub struct Task {
- pub id: TaskId,
- pub title: String,
- pub description: String,
- #[serde(rename = "type")]
- pub task_type: String,
- pub priority: Priority,
- pub status: Status,
- pub effort: Effort,
- pub parent: Option<TaskId>,
- pub created_at: String,
- pub updated_at: String,
- pub deleted_at: Option<String>,
- pub labels: Vec<String>,
- pub blockers: Vec<TaskId>,
- pub logs: Vec<LogEntry>,
-}
-
-impl Task {
- /// Serialize this task with full ULIDs instead of short display IDs.
- ///
- /// Used by `export` so that `import` can round-trip data losslessly —
- /// `import` needs the full ULID to recreate exact CRDT keys.
- pub fn to_export_value(&self) -> serde_json::Value {
- serde_json::json!({
- "id": self.id.as_str(),
- "title": self.title,
- "description": self.description,
- "type": self.task_type,
- "priority": self.priority,
- "status": self.status,
- "effort": self.effort,
- "parent": self.parent.as_ref().map(|p| p.as_str()),
- "created_at": self.created_at,
- "updated_at": self.updated_at,
- "deleted_at": self.deleted_at,
- "labels": self.labels,
- "blockers": self.blockers.iter().map(|b| b.as_str()).collect::<Vec<_>>(),
- "logs": self.logs.iter().map(|l| {
- serde_json::json!({
- "id": l.id.as_str(),
- "timestamp": l.timestamp,
- "message": l.message,
- })
- }).collect::<Vec<_>>(),
- })
- }
-}
-
-/// Result type for partitioning blockers by task state.
-#[derive(Debug, Default, Clone, Serialize)]
-pub struct BlockerPartition {
- pub open: Vec<TaskId>,
- pub resolved: Vec<TaskId>,
-}
-
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct BindingsFile {
#[serde(default)]
@@ -572,35 +352,6 @@ impl Store {
}
}
-/// Generate a new task ULID.
-pub fn gen_id() -> TaskId {
- TaskId::new(Ulid::new())
-}
-
-pub fn parse_status(s: &str) -> Result<Status> {
- Status::parse(s)
-}
-
-pub fn parse_priority(s: &str) -> Result<Priority> {
- Priority::parse(s)
-}
-
-pub fn parse_effort(s: &str) -> Result<Effort> {
- Effort::parse(s)
-}
-
-pub fn status_label(s: Status) -> &'static str {
- s.as_str()
-}
-
-pub fn priority_label(p: Priority) -> &'static str {
- p.as_str()
-}
-
-pub fn effort_label(e: Effort) -> &'static str {
- e.as_str()
-}
-
pub fn data_root() -> Result<PathBuf> {
let home = std::env::var("HOME").context("HOME is not set")?;
Ok(PathBuf::from(home).join(".local").join("share").join("td"))
@@ -4,6 +4,7 @@ pub mod color;
pub mod db;
pub mod editor;
pub mod migrate;
+pub mod model;
pub mod ops;
pub mod score;
@@ -0,0 +1,233 @@
+use anyhow::{bail, Context, Result};
+use serde::Serialize;
+use std::fmt;
+use ulid::Ulid;
+
+/// Current UTC time in ISO 8601 format.
+pub fn now_utc() -> String {
+ chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()
+}
+
+/// Lifecycle state for a task.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Status {
+ Open,
+ InProgress,
+ Closed,
+}
+
+impl Status {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Status::Open => "open",
+ Status::InProgress => "in_progress",
+ Status::Closed => "closed",
+ }
+ }
+
+ pub fn parse(raw: &str) -> Result<Self> {
+ match raw {
+ "open" => Ok(Self::Open),
+ "in_progress" => Ok(Self::InProgress),
+ "closed" => Ok(Self::Closed),
+ _ => bail!("invalid status '{raw}'"),
+ }
+ }
+}
+
+/// Priority for task ordering.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Priority {
+ High,
+ Medium,
+ Low,
+}
+
+impl Priority {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Priority::High => "high",
+ Priority::Medium => "medium",
+ Priority::Low => "low",
+ }
+ }
+
+ pub fn parse(raw: &str) -> Result<Self> {
+ match raw {
+ "high" => Ok(Self::High),
+ "medium" => Ok(Self::Medium),
+ "low" => Ok(Self::Low),
+ _ => bail!("invalid priority '{raw}'"),
+ }
+ }
+
+ pub fn score(self) -> i32 {
+ match self {
+ Priority::High => 1,
+ Priority::Medium => 2,
+ Priority::Low => 3,
+ }
+ }
+}
+
+/// Estimated effort for a task.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Effort {
+ Low,
+ Medium,
+ High,
+}
+
+impl Effort {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Effort::Low => "low",
+ Effort::Medium => "medium",
+ Effort::High => "high",
+ }
+ }
+
+ pub fn parse(raw: &str) -> Result<Self> {
+ match raw {
+ "low" => Ok(Self::Low),
+ "medium" => Ok(Self::Medium),
+ "high" => Ok(Self::High),
+ _ => bail!("invalid effort '{raw}'"),
+ }
+ }
+
+ pub fn score(self) -> i32 {
+ match self {
+ Effort::Low => 1,
+ Effort::Medium => 2,
+ Effort::High => 3,
+ }
+ }
+}
+
+/// A stable task identifier backed by a ULID.
+///
+/// Serializes as the short display form (`td-XXXXXXX`) for user-facing
+/// JSON. Use [`TaskId::as_str`] when the full ULID is needed (e.g.
+/// for CRDT keys or export round-tripping).
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct TaskId(String);
+
+impl Serialize for TaskId {
+ fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+ serializer.serialize_str(&self.short())
+ }
+}
+
+impl TaskId {
+ pub fn new(id: Ulid) -> Self {
+ Self(id.to_string())
+ }
+
+ pub fn parse(raw: &str) -> Result<Self> {
+ let id = Ulid::from_string(raw).with_context(|| format!("invalid task id '{raw}'"))?;
+ Ok(Self::new(id))
+ }
+
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+
+ pub fn short(&self) -> String {
+ format!("td-{}", &self.0[self.0.len() - 7..])
+ }
+
+ /// Return a display-friendly short ID from a raw ULID string.
+ pub fn display_id(raw: &str) -> String {
+ let n = raw.len();
+ if n > 7 {
+ format!("td-{}", &raw[n - 7..])
+ } else {
+ format!("td-{raw}")
+ }
+ }
+}
+
+impl fmt::Display for TaskId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.short())
+ }
+}
+
+/// A task log entry embedded in a task record.
+#[derive(Debug, Clone, Serialize)]
+pub struct LogEntry {
+ pub id: TaskId,
+ pub timestamp: String,
+ pub message: String,
+}
+
+/// Hydrated task data from the CRDT document.
+#[derive(Debug, Clone, Serialize)]
+pub struct Task {
+ pub id: TaskId,
+ pub title: String,
+ pub description: String,
+ #[serde(rename = "type")]
+ pub task_type: String,
+ pub priority: Priority,
+ pub status: Status,
+ pub effort: Effort,
+ pub parent: Option<TaskId>,
+ pub created_at: String,
+ pub updated_at: String,
+ pub deleted_at: Option<String>,
+ pub labels: Vec<String>,
+ pub blockers: Vec<TaskId>,
+ pub logs: Vec<LogEntry>,
+}
+
+impl Task {
+ /// Serialize this task with full ULIDs instead of short display IDs.
+ ///
+ /// Used by `export` so that `import` can round-trip data losslessly —
+ /// `import` needs the full ULID to recreate exact CRDT keys.
+ pub fn to_export_value(&self) -> serde_json::Value {
+ serde_json::json!({
+ "id": self.id.as_str(),
+ "title": self.title,
+ "description": self.description,
+ "type": self.task_type,
+ "priority": self.priority,
+ "status": self.status,
+ "effort": self.effort,
+ "parent": self.parent.as_ref().map(|p| p.as_str()),
+ "created_at": self.created_at,
+ "updated_at": self.updated_at,
+ "deleted_at": self.deleted_at,
+ "labels": self.labels,
+ "blockers": self.blockers.iter().map(|b| b.as_str()).collect::<Vec<_>>(),
+ "logs": self
+ .logs
+ .iter()
+ .map(|l| {
+ serde_json::json!({
+ "id": l.id.as_str(),
+ "timestamp": l.timestamp,
+ "message": l.message,
+ })
+ })
+ .collect::<Vec<_>>(),
+ })
+ }
+}
+
+/// Result type for partitioning blockers by task state.
+#[derive(Debug, Default, Clone, Serialize)]
+pub struct BlockerPartition {
+ pub open: Vec<TaskId>,
+ pub resolved: Vec<TaskId>,
+}
+
+/// Generate a new task ULID.
+pub fn gen_id() -> TaskId {
+ TaskId::new(Ulid::new())
+}
@@ -10,7 +10,8 @@ use std::path::Path;
use anyhow::{anyhow, bail, Result};
use loro::LoroMap;
-use crate::db::{self, LogEntry, Store, Task, TaskId};
+use crate::db::{self, Store};
+use crate::model::{gen_id, now_utc, Effort, LogEntry, Priority, Status, Task, TaskId};
/// Create a new project and optionally bind a directory path to it.
pub fn init_project(root: &Path, name: &str, bind_path: Option<&Path>) -> Result<()> {
@@ -27,16 +28,16 @@ pub struct CreateOpts {
pub title: String,
pub description: String,
pub task_type: String,
- pub priority: db::Priority,
- pub effort: db::Effort,
+ pub priority: Priority,
+ pub effort: Effort,
pub parent: Option<TaskId>,
pub labels: Vec<String>,
}
/// Create a task and return the hydrated result.
pub fn create_task(store: &Store, opts: CreateOpts) -> Result<Task> {
- let ts = db::now_utc();
- let id = db::gen_id();
+ let ts = now_utc();
+ let id = gen_id();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
@@ -45,9 +46,9 @@ pub fn create_task(store: &Store, opts: CreateOpts) -> Result<Task> {
task.insert("title", opts.title.as_str())?;
task.insert("description", opts.description.as_str())?;
task.insert("type", opts.task_type.as_str())?;
- task.insert("priority", db::priority_label(opts.priority))?;
- task.insert("status", db::status_label(db::Status::Open))?;
- task.insert("effort", db::effort_label(opts.effort))?;
+ task.insert("priority", opts.priority.as_str())?;
+ task.insert("status", Status::Open.as_str())?;
+ task.insert("effort", opts.effort.as_str())?;
task.insert(
"parent",
opts.parent.as_ref().map(|p| p.as_str()).unwrap_or(""),
@@ -77,29 +78,29 @@ pub fn create_task(store: &Store, opts: CreateOpts) -> Result<Task> {
/// Input for updating an existing task. All fields are optional; only
/// populated fields are written.
pub struct UpdateOpts {
- pub status: Option<db::Status>,
- pub priority: Option<db::Priority>,
- pub effort: Option<db::Effort>,
+ pub status: Option<Status>,
+ pub priority: Option<Priority>,
+ pub effort: Option<Effort>,
pub title: Option<String>,
pub description: Option<String>,
}
/// Update task fields and return the refreshed task.
pub fn update_task(store: &Store, task_id: &TaskId, opts: UpdateOpts) -> Result<Task> {
- let ts = db::now_utc();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
let task = db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
if let Some(s) = opts.status {
- task.insert("status", db::status_label(s))?;
+ task.insert("status", s.as_str())?;
}
if let Some(p) = opts.priority {
- task.insert("priority", db::priority_label(p))?;
+ task.insert("priority", p.as_str())?;
}
if let Some(e) = opts.effort {
- task.insert("effort", db::effort_label(e))?;
+ task.insert("effort", e.as_str())?;
}
if let Some(ref t) = opts.title {
task.insert("title", t.as_str())?;
@@ -118,11 +119,11 @@ pub fn update_task(store: &Store, task_id: &TaskId, opts: UpdateOpts) -> Result<
/// Mark a single task as closed.
pub fn mark_done(store: &Store, task_id: &TaskId) -> Result<()> {
- let ts = db::now_utc();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
if let Some(task) = db::get_task_map(&tasks, task_id)? {
- task.insert("status", db::status_label(db::Status::Closed))?;
+ task.insert("status", Status::Closed.as_str())?;
task.insert("updated_at", ts.clone())?;
}
Ok(())
@@ -132,11 +133,11 @@ pub fn mark_done(store: &Store, task_id: &TaskId) -> Result<()> {
/// Reopen a single closed task.
pub fn reopen_task(store: &Store, task_id: &TaskId) -> Result<()> {
- let ts = db::now_utc();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
if let Some(task) = db::get_task_map(&tasks, task_id)? {
- task.insert("status", db::status_label(db::Status::Open))?;
+ task.insert("status", Status::Open.as_str())?;
task.insert("updated_at", ts.clone())?;
}
Ok(())
@@ -146,8 +147,8 @@ pub fn reopen_task(store: &Store, task_id: &TaskId) -> Result<()> {
/// Append a log entry to a task and return the entry.
pub fn add_log(store: &Store, task_id: &TaskId, message: &str) -> Result<LogEntry> {
- let log_id = db::gen_id();
- let ts = db::now_utc();
+ let log_id = gen_id();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
@@ -169,7 +170,7 @@ pub fn add_log(store: &Store, task_id: &TaskId, message: &str) -> Result<LogEntr
/// Add a label to a task.
pub fn add_label(store: &Store, task_id: &TaskId, label: &str) -> Result<()> {
- let ts = db::now_utc();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
let task = db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
@@ -183,7 +184,7 @@ pub fn add_label(store: &Store, task_id: &TaskId, label: &str) -> Result<()> {
/// Remove a label from a task.
pub fn remove_label(store: &Store, task_id: &TaskId, label: &str) -> Result<()> {
- let ts = db::now_utc();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
let task = db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
@@ -206,7 +207,7 @@ pub fn add_dep(store: &Store, child_id: &TaskId, blocker_id: &TaskId) -> Result<
bail!("adding dependency would create a cycle");
}
- let ts = db::now_utc();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
let child_task =
@@ -221,7 +222,7 @@ pub fn add_dep(store: &Store, child_id: &TaskId, blocker_id: &TaskId) -> Result<
/// Remove a blocker dependency.
pub fn remove_dep(store: &Store, child_id: &TaskId, blocker_id: &TaskId) -> Result<()> {
- let ts = db::now_utc();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
let child_task =
@@ -279,7 +280,7 @@ pub fn soft_delete(store: &Store, ids: &[TaskId], recursive: bool) -> Result<Del
.map(|t| t.id.clone())
.collect();
- let ts = db::now_utc();
+ let ts = now_utc();
store.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
@@ -288,7 +289,7 @@ pub fn soft_delete(store: &Store, ids: &[TaskId], recursive: bool) -> Result<Del
db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
task.insert("deleted_at", ts.clone())?;
task.insert("updated_at", ts.clone())?;
- task.insert("status", db::status_label(db::Status::Closed))?;
+ task.insert("status", Status::Closed.as_str())?;
}
for task in store.list_tasks_unfiltered()? {
@@ -39,6 +39,7 @@ fn sync_invalid_code_format_fails() {
fn sync_exchanges_tasks_between_peers() {
use std::fs;
use yatd::db;
+ use yatd::model;
let home_a = tempfile::tempdir().unwrap();
let cwd_a = tempfile::tempdir().unwrap();
@@ -48,7 +49,7 @@ fn sync_exchanges_tasks_between_peers() {
// --- Set up peer A: init a project and create a task ---
std::env::set_var("HOME", home_a.path());
let store_a = db::init(cwd_a.path(), "shared").unwrap();
- let id_a = db::gen_id();
+ let id_a = model::gen_id();
store_a
.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
@@ -60,8 +61,8 @@ fn sync_exchanges_tasks_between_peers() {
task.insert("status", "open")?;
task.insert("effort", "medium")?;
task.insert("parent", "")?;
- task.insert("created_at", db::now_utc())?;
- task.insert("updated_at", db::now_utc())?;
+ task.insert("created_at", model::now_utc())?;
+ task.insert("updated_at", model::now_utc())?;
task.insert("deleted_at", "")?;
task.insert_container("labels", loro::LoroMap::new())?;
task.insert_container("blockers", loro::LoroMap::new())?;
@@ -97,7 +98,7 @@ fn sync_exchanges_tasks_between_peers() {
std::env::set_var("HOME", home_b.path());
let store_b = db::open(cwd_b.path()).unwrap();
- let id_b = db::gen_id();
+ let id_b = model::gen_id();
store_b
.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
@@ -109,8 +110,8 @@ fn sync_exchanges_tasks_between_peers() {
task.insert("status", "open")?;
task.insert("effort", "low")?;
task.insert("parent", "")?;
- task.insert("created_at", db::now_utc())?;
- task.insert("updated_at", db::now_utc())?;
+ task.insert("created_at", model::now_utc())?;
+ task.insert("updated_at", model::now_utc())?;
task.insert("deleted_at", "")?;
task.insert_container("labels", loro::LoroMap::new())?;
task.insert_container("blockers", loro::LoroMap::new())?;
@@ -194,13 +195,14 @@ fn try_open_returns_none_without_binding() {
#[test]
fn bootstrap_from_peer_creates_openable_store() {
use yatd::db;
+ use yatd::model;
let home_a = tempfile::tempdir().unwrap();
let cwd_a = tempfile::tempdir().unwrap();
std::env::set_var("HOME", home_a.path());
let source = db::init(cwd_a.path(), "shared").unwrap();
- let id = db::gen_id();
+ let id = model::gen_id();
source
.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
@@ -212,8 +214,8 @@ fn bootstrap_from_peer_creates_openable_store() {
task.insert("status", "open")?;
task.insert("effort", "medium")?;
task.insert("parent", "")?;
- task.insert("created_at", db::now_utc())?;
- task.insert("updated_at", db::now_utc())?;
+ task.insert("created_at", model::now_utc())?;
+ task.insert("updated_at", model::now_utc())?;
task.insert("deleted_at", "")?;
task.insert_container("labels", loro::LoroMap::new())?;
task.insert_container("blockers", loro::LoroMap::new())?;
@@ -274,7 +276,7 @@ fn bootstrap_from_peer_rejects_missing_project_id() {
/// Helper: insert a minimal valid task into a doc via apply_and_persist.
fn insert_task(store: &yatd::db::Store, title: &str) {
- let id = yatd::db::gen_id();
+ let id = yatd::model::gen_id();
store
.apply_and_persist(|doc| {
let tasks = doc.get_map("tasks");
@@ -286,8 +288,8 @@ fn insert_task(store: &yatd::db::Store, title: &str) {
task.insert("status", "open")?;
task.insert("effort", "medium")?;
task.insert("parent", "")?;
- task.insert("created_at", yatd::db::now_utc())?;
- task.insert("updated_at", yatd::db::now_utc())?;
+ task.insert("created_at", yatd::model::now_utc())?;
+ task.insert("updated_at", yatd::model::now_utc())?;
task.insert("deleted_at", "")?;
task.insert_container("labels", loro::LoroMap::new())?;
task.insert_container("blockers", loro::LoroMap::new())?;