merge: initial cli setup
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -161,6 +161,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
@@ -187,6 +188,15 @@ dependencies = [
|
|||||||
"strsim",
|
"strsim",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_complete"
|
||||||
|
version = "4.5.66"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.55"
|
version = "4.5.55"
|
||||||
@@ -1532,8 +1542,11 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"clap_complete",
|
||||||
"colored",
|
"colored",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|||||||
11
Cargo.toml
11
Cargo.toml
@@ -5,9 +5,18 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
chrono = "0.4"
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
colored = "3"
|
colored = "3"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono", "migrate"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono", "migrate"] }
|
||||||
tokio = { version = "1", features = ["rt", "macros"] }
|
tokio = { version = "1", features = ["rt", "macros"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
clap_complete = "4"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
28
build.rs
Normal file
28
build.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#[allow(dead_code)]
|
||||||
|
#[path = "src/model.rs"]
|
||||||
|
mod model;
|
||||||
|
|
||||||
|
include!("src/cli.rs");
|
||||||
|
|
||||||
|
use clap::{CommandFactory, ValueEnum};
|
||||||
|
use clap_complete::{generate_to, Shell};
|
||||||
|
use std::{env, io::Error};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Error> {
|
||||||
|
let outdir = env::var_os("SHELL_COMPLETIONS_DIR")
|
||||||
|
.or_else(|| env::var_os("OUT_DIR"));
|
||||||
|
|
||||||
|
let Some(outdir) = outdir else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = Cli::command();
|
||||||
|
|
||||||
|
for shell in Shell::value_variants() {
|
||||||
|
let path = generate_to(*shell, &mut cmd, "todo", &outdir)?;
|
||||||
|
|
||||||
|
println!("cargo:warning=completion file is generated: {path:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
115
src/cli.rs
Normal file
115
src/cli.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
|
use crate::model::Priority;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "todo", about = "global thought capture", version)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
/// add a new todo
|
||||||
|
#[command(alias = "a")]
|
||||||
|
Add(AddArgs),
|
||||||
|
|
||||||
|
/// list todos
|
||||||
|
#[command(alias = "ls")]
|
||||||
|
List(ListArgs),
|
||||||
|
|
||||||
|
/// mark a todo as done
|
||||||
|
Done(DoneArgs),
|
||||||
|
|
||||||
|
/// edit a todo
|
||||||
|
Edit(EditArgs),
|
||||||
|
|
||||||
|
/// remove a todo permanently
|
||||||
|
#[command(alias = "rm")]
|
||||||
|
Remove(RemoveArgs),
|
||||||
|
|
||||||
|
/// list all tags in use
|
||||||
|
Tags,
|
||||||
|
|
||||||
|
/// remove all completed todos
|
||||||
|
Purge,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct AddArgs {
|
||||||
|
/// the todo text
|
||||||
|
pub text: String,
|
||||||
|
|
||||||
|
/// priority (critical/high/medium/low or c/h/m/l)
|
||||||
|
#[arg(short, long, default_value = "medium")]
|
||||||
|
pub priority: Priority,
|
||||||
|
|
||||||
|
/// tags (repeatable: -t foo -t bar)
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub tag: Vec<String>,
|
||||||
|
|
||||||
|
/// do not associate with current repo
|
||||||
|
#[arg(long)]
|
||||||
|
pub no_project: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ListArgs {
|
||||||
|
/// filter by tag
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub tag: Vec<String>,
|
||||||
|
|
||||||
|
/// filter by priority
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub priority: Option<Priority>,
|
||||||
|
|
||||||
|
/// search text (substring match)
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub search: Option<String>,
|
||||||
|
|
||||||
|
/// include completed todos
|
||||||
|
#[arg(short = 'a', long)]
|
||||||
|
pub all: bool,
|
||||||
|
|
||||||
|
/// only show todos for current repo
|
||||||
|
#[arg(long)]
|
||||||
|
pub here: bool,
|
||||||
|
|
||||||
|
/// output as json
|
||||||
|
#[arg(long)]
|
||||||
|
pub json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct DoneArgs {
|
||||||
|
/// todo id
|
||||||
|
pub id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct EditArgs {
|
||||||
|
/// todo id
|
||||||
|
pub id: i64,
|
||||||
|
|
||||||
|
/// new text
|
||||||
|
pub text: Option<String>,
|
||||||
|
|
||||||
|
/// new priority
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub priority: Option<Priority>,
|
||||||
|
|
||||||
|
/// add tags
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub tag: Vec<String>,
|
||||||
|
|
||||||
|
/// remove tags
|
||||||
|
#[arg(long)]
|
||||||
|
pub untag: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct RemoveArgs {
|
||||||
|
/// todo id
|
||||||
|
pub id: i64,
|
||||||
|
}
|
||||||
111
src/commands.rs
Normal file
111
src/commands.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use colored::Colorize;
|
||||||
|
|
||||||
|
use crate::cli::{AddArgs, Command, DoneArgs, EditArgs, ListArgs, RemoveArgs};
|
||||||
|
use crate::db::{self, ListFilters, Pool};
|
||||||
|
use crate::display::{print_todo, print_todo_list};
|
||||||
|
|
||||||
|
pub struct Ctx<'a> {
|
||||||
|
pub pool: &'a Pool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(cmd: Command, ctx: &Ctx<'_>) -> Result<()> {
|
||||||
|
match cmd {
|
||||||
|
Command::Add(args) => add(ctx, args).await,
|
||||||
|
Command::List(args) => list(ctx, args).await,
|
||||||
|
Command::Done(args) => done(ctx, args).await,
|
||||||
|
Command::Edit(args) => edit(ctx, args).await,
|
||||||
|
Command::Remove(args) => remove(ctx, args).await,
|
||||||
|
Command::Tags => tags(ctx).await,
|
||||||
|
Command::Purge => purge(ctx).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add(ctx: &Ctx<'_>, args: AddArgs) -> Result<()> {
|
||||||
|
let project = if args.no_project {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
db::detect_project()
|
||||||
|
};
|
||||||
|
|
||||||
|
let todo =
|
||||||
|
db::insert_todo(ctx.pool, &args.text, &args.priority, project.as_deref(), &args.tag)
|
||||||
|
.await?;
|
||||||
|
print!("{} ", "added:".green().bold());
|
||||||
|
print_todo(&todo);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(ctx: &Ctx<'_>, args: ListArgs) -> Result<()> {
|
||||||
|
let project = if args.here {
|
||||||
|
db::detect_project()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let todos = db::list_todos(
|
||||||
|
ctx.pool,
|
||||||
|
ListFilters {
|
||||||
|
tags: &args.tag,
|
||||||
|
priority: args.priority.as_ref(),
|
||||||
|
search: args.search.as_deref(),
|
||||||
|
project: project.as_deref(),
|
||||||
|
include_done: args.all,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if args.json {
|
||||||
|
println!("{}", serde_json::to_string(&todos)?);
|
||||||
|
} else {
|
||||||
|
print_todo_list(&todos);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn done(ctx: &Ctx<'_>, args: DoneArgs) -> Result<()> {
|
||||||
|
let todo = db::complete_todo(ctx.pool, args.id).await?;
|
||||||
|
print!("{} ", "done:".green().bold());
|
||||||
|
print_todo(&todo);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tags(ctx: &Ctx<'_>) -> Result<()> {
|
||||||
|
let tags = db::list_tags(ctx.pool).await?;
|
||||||
|
if tags.is_empty() {
|
||||||
|
println!("{}", "no tags".dimmed());
|
||||||
|
} else {
|
||||||
|
for tag in &tags {
|
||||||
|
println!(" {}", format!("#{tag}").blue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn edit(ctx: &Ctx<'_>, args: EditArgs) -> Result<()> {
|
||||||
|
let todo = db::update_todo(
|
||||||
|
ctx.pool,
|
||||||
|
args.id,
|
||||||
|
args.text.as_deref(),
|
||||||
|
args.priority.as_ref(),
|
||||||
|
&args.tag,
|
||||||
|
&args.untag,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
print!("{} ", "edited:".green().bold());
|
||||||
|
print_todo(&todo);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove(ctx: &Ctx<'_>, args: RemoveArgs) -> Result<()> {
|
||||||
|
let todo = db::delete_todo(ctx.pool, args.id).await?;
|
||||||
|
print!("{} ", "removed:".red().bold());
|
||||||
|
print_todo(&todo);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn purge(ctx: &Ctx<'_>) -> Result<()> {
|
||||||
|
let count = db::purge_completed(ctx.pool).await?;
|
||||||
|
println!("{}", format!("purged {count} completed todo(s)").dimmed());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
173
src/db.rs
173
src/db.rs
@@ -124,6 +124,179 @@ pub async fn fetch_todo(pool: &Pool, id: i64) -> Result<Todo> {
|
|||||||
Ok(row.into_todo(tags))
|
Ok(row.into_todo(tags))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filter options for list_todos
|
||||||
|
pub struct ListFilters<'a> {
|
||||||
|
pub tags: &'a [String],
|
||||||
|
pub priority: Option<&'a Priority>,
|
||||||
|
pub search: Option<&'a str>,
|
||||||
|
pub project: Option<&'a str>,
|
||||||
|
pub include_done: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_todos(pool: &Pool, filters: ListFilters<'_>) -> Result<Vec<Todo>> {
|
||||||
|
// build query dynamically based on filters
|
||||||
|
let mut sql = String::from(
|
||||||
|
"SELECT DISTINCT t.id FROM todos t LEFT JOIN todo_tags tt ON t.id = tt.todo_id WHERE 1=1",
|
||||||
|
);
|
||||||
|
let mut bind_idx = 1;
|
||||||
|
|
||||||
|
if !filters.include_done {
|
||||||
|
sql.push_str(" AND t.completed_at IS NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pri) = filters.priority {
|
||||||
|
sql.push_str(&format!(" AND t.priority = ?{bind_idx}"));
|
||||||
|
bind_idx += 1;
|
||||||
|
let _ = pri; // used below during bind
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.search.is_some() {
|
||||||
|
sql.push_str(&format!(" AND t.text LIKE ?{bind_idx}"));
|
||||||
|
bind_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.project.is_some() {
|
||||||
|
sql.push_str(&format!(" AND t.project = ?{bind_idx}"));
|
||||||
|
bind_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in filters.tags {
|
||||||
|
sql.push_str(&format!(" AND EXISTS (SELECT 1 FROM todo_tags WHERE todo_id = t.id AND tag = ?{bind_idx})"));
|
||||||
|
bind_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.push_str(" ORDER BY t.id");
|
||||||
|
|
||||||
|
let mut query = sqlx::query_scalar::<_, i64>(&sql);
|
||||||
|
|
||||||
|
if let Some(pri) = filters.priority {
|
||||||
|
query = query.bind(priority_to_str(pri));
|
||||||
|
}
|
||||||
|
if let Some(search) = filters.search {
|
||||||
|
query = query.bind(format!("%{search}%"));
|
||||||
|
}
|
||||||
|
if let Some(project) = filters.project {
|
||||||
|
query = query.bind(project);
|
||||||
|
}
|
||||||
|
for tag in filters.tags {
|
||||||
|
query = query.bind(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids = query.fetch_all(pool).await.context("cannot list todos")?;
|
||||||
|
|
||||||
|
let mut todos = Vec::with_capacity(ids.len());
|
||||||
|
for id in ids {
|
||||||
|
todos.push(fetch_todo(pool, id).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(todos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn complete_todo(pool: &Pool, id: i64) -> Result<Todo> {
|
||||||
|
let rows = sqlx::query(
|
||||||
|
"UPDATE todos SET completed_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?1 AND completed_at IS NULL",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot complete todo")?;
|
||||||
|
|
||||||
|
anyhow::ensure!(rows.rows_affected() > 0, "todo {id} not found or already done");
|
||||||
|
|
||||||
|
fetch_todo(pool, id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_tags(pool: &Pool) -> Result<Vec<String>> {
|
||||||
|
let tags = sqlx::query_scalar::<_, String>(
|
||||||
|
"SELECT DISTINCT tag FROM todo_tags tt
|
||||||
|
JOIN todos t ON tt.todo_id = t.id
|
||||||
|
WHERE t.completed_at IS NULL
|
||||||
|
ORDER BY tag",
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot list tags")?;
|
||||||
|
|
||||||
|
Ok(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_todo(
|
||||||
|
pool: &Pool,
|
||||||
|
id: i64,
|
||||||
|
text: Option<&str>,
|
||||||
|
priority: Option<&Priority>,
|
||||||
|
add_tags: &[String],
|
||||||
|
remove_tags: &[String],
|
||||||
|
) -> Result<Todo> {
|
||||||
|
// verify exists
|
||||||
|
let exists = sqlx::query_scalar::<_, i64>("SELECT id FROM todos WHERE id = ?1")
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot check todo")?;
|
||||||
|
|
||||||
|
anyhow::ensure!(exists.is_some(), "todo {id} not found");
|
||||||
|
|
||||||
|
if let Some(text) = text {
|
||||||
|
sqlx::query("UPDATE todos SET text = ?1 WHERE id = ?2")
|
||||||
|
.bind(text)
|
||||||
|
.bind(id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot update text")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pri) = priority {
|
||||||
|
sqlx::query("UPDATE todos SET priority = ?1 WHERE id = ?2")
|
||||||
|
.bind(priority_to_str(pri))
|
||||||
|
.bind(id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot update priority")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in add_tags {
|
||||||
|
sqlx::query("INSERT OR IGNORE INTO todo_tags (todo_id, tag) VALUES (?1, ?2)")
|
||||||
|
.bind(id)
|
||||||
|
.bind(tag)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot add tag")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in remove_tags {
|
||||||
|
sqlx::query("DELETE FROM todo_tags WHERE todo_id = ?1 AND tag = ?2")
|
||||||
|
.bind(id)
|
||||||
|
.bind(tag)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot remove tag")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_todo(pool, id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_todo(pool: &Pool, id: i64) -> Result<Todo> {
|
||||||
|
let todo = fetch_todo(pool, id).await?;
|
||||||
|
|
||||||
|
sqlx::query("DELETE FROM todos WHERE id = ?1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot delete todo")?;
|
||||||
|
|
||||||
|
Ok(todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn purge_completed(pool: &Pool) -> Result<usize> {
|
||||||
|
let result = sqlx::query("DELETE FROM todos WHERE completed_at IS NOT NULL")
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.context("cannot purge completed")?;
|
||||||
|
|
||||||
|
Ok(result.rows_affected() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
// internal row type for sqlx FromRow
|
// internal row type for sqlx FromRow
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct TodoRow {
|
struct TodoRow {
|
||||||
|
|||||||
55
src/display.rs
Normal file
55
src/display.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use colored::Colorize;
|
||||||
|
|
||||||
|
use crate::model::{Priority, Todo};
|
||||||
|
|
||||||
|
pub fn print_todo(todo: &Todo) {
|
||||||
|
let id = format!("{:>3}", todo.id).dimmed();
|
||||||
|
|
||||||
|
let priority = match todo.priority {
|
||||||
|
Priority::Critical => "CRIT".red().bold(),
|
||||||
|
Priority::High => "high".red(),
|
||||||
|
Priority::Medium => " med".yellow(),
|
||||||
|
Priority::Low => " low".dimmed(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let done_mark = if todo.is_done() { "[x]" } else { "[ ]" };
|
||||||
|
|
||||||
|
let text = if todo.is_done() {
|
||||||
|
todo.text.strikethrough().dimmed().to_string()
|
||||||
|
} else {
|
||||||
|
todo.text.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tags = if todo.tags.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
todo.tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| format!("#{t}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
.blue()
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = todo
|
||||||
|
.project_short()
|
||||||
|
.map(|p| format!("({})", p).dimmed().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
println!("{id} {done_mark} [{priority}] {text} {tags} {project}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_todo_list(todos: &[Todo]) {
|
||||||
|
if todos.is_empty() {
|
||||||
|
println!("{}", "no todos found".dimmed());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for todo in todos {
|
||||||
|
print_todo(todo);
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = todos.len();
|
||||||
|
println!("{}", format!("\n{count} item(s)").dimmed());
|
||||||
|
}
|
||||||
12
src/main.rs
12
src/main.rs
@@ -1,9 +1,15 @@
|
|||||||
|
mod cli;
|
||||||
|
mod commands;
|
||||||
mod db;
|
mod db;
|
||||||
|
mod display;
|
||||||
mod model;
|
mod model;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let _pool = db::connect().await?;
|
let cli = cli::Cli::parse();
|
||||||
println!("database ready at {}", db::db_path()?.display());
|
let pool = db::connect().await?;
|
||||||
Ok(())
|
let ctx = commands::Ctx { pool: &pool };
|
||||||
|
commands::run(cli.command, &ctx).await
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/model.rs
13
src/model.rs
@@ -1,19 +1,16 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Priority {
|
pub enum Priority {
|
||||||
Low,
|
Low,
|
||||||
|
#[default]
|
||||||
Medium,
|
Medium,
|
||||||
High,
|
High,
|
||||||
Critical,
|
Critical,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Priority {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Priority {
|
impl std::fmt::Display for Priority {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -39,7 +36,7 @@ impl std::str::FromStr for Priority {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Todo {
|
pub struct Todo {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user