use std::io::Write;
use clap::Parser;
use crate::cli::Cli;
use crate::error::Result;
use crate::model::Status;
use crate::storage;
use super::task::active_library_root;
const NESTED: [&str; 2] = ["step", "body"];
const HELP: &str = "\
Commands (prefix with '/'):
/add <title> create a task (alias of /new)
/list [filters] list tasks, e.g. /list --status todo --tag backend
/show [id] task details
/start /done /cancel [id]
/status [id] <status>
/set [id] --title ... --description ...
/step get|add|done|remove [id] ...
/body append|replace|delete [id] ...
/edit [id] open in $EDITOR
/delete [id] asks for confirmation
/board /stats /remind /overdue /search <text>
/archive /batch /tag /template /lib /fix-names
Workbench:
/use <id> select a task; later commands may omit the id
/unuse clear the selection
/refresh redraw the dashboard
/help this help
/quit leave (also: /exit, Ctrl-D)
";
pub fn run() -> Result<()> {
let mut current: Option<u64> = None;
println!("tasks workbench — /help for commands, /quit to leave");
dashboard(current)?;
let stdin = std::io::stdin();
loop {
print!("{}> ", current.map(|s| format!("[#{s}] ")).unwrap_or_default());
std::io::stdout().flush()?;
let mut line = String::new();
if stdin.read_line(&mut line)? == 0 {
println!();
return Ok(());
}
let line = line.trim();
if line.is_empty() {
continue;
}
let Some(rest) = line.strip_prefix('/') else {
eprintln!("commands must start with '/' — try /help");
continue;
};
let words: Vec<String> = rest.split_whitespace().map(String::from).collect();
if words.is_empty() {
continue;
}
match words[0].as_str() {
"quit" | "exit" | "q" => return Ok(()),
"help" | "h" => {
print!("{HELP}");
continue;
}
"refresh" => {
dashboard(current)?;
continue;
}
"unuse" => {
current = None;
continue;
}
"use" => {
match select(words.get(1).map(String::as_str)) {
Ok(seq) => current = Some(seq),
Err(err) => eprintln!("error: {err}"),
}
continue;
}
_ => {}
}
if let Err(err) = execute(&words, current) {
eprintln!("error: {err}");
continue;
}
if let Some(seq) = current
&& select(Some(&seq.to_string())).is_err()
{
current = None;
}
dashboard(current)?;
}
}
fn select(arg: Option<&str>) -> Result<u64> {
let id = arg.ok_or_else(|| {
crate::error::Error::InvalidTaskFile("usage: /use <id>".into())
})?;
let root = active_library_root()?;
Ok(storage::resolve_task(&root, id)?.task.meta.seq)
}
fn execute(words: &[String], current: Option<u64>) -> Result<()> {
let build = |args: &[String]| -> Vec<String> {
let mut v = vec!["tasks".to_string()];
v.extend(args.iter().cloned());
v
};
let parsed = Cli::try_parse_from(build(words));
let cli = match (parsed, current) {
(Ok(cli), _) => cli,
(Err(first_err), Some(seq)) => {
let at = if NESTED.contains(&words[0].as_str()) { 2 } else { 1 };
if at > words.len() {
return Err(crate::error::Error::InvalidTaskFile(first_err.to_string()));
}
let mut with_id = words.to_vec();
with_id.insert(at, seq.to_string());
Cli::try_parse_from(build(&with_id))
.map_err(|_| crate::error::Error::InvalidTaskFile(first_err.to_string()))?
}
(Err(err), None) => {
return Err(crate::error::Error::InvalidTaskFile(err.to_string()));
}
};
if matches!(cli.command, crate::cli::Command::Repl) {
return Err(crate::error::Error::InvalidTaskFile(
"already in the workbench".into(),
));
}
crate::dispatch(cli)
}
fn dashboard(current: Option<u64>) -> Result<()> {
let root = active_library_root()?;
let tasks = storage::load_library(&root)?.tasks;
println!("\n=== in progress ===");
let active: Vec<_> = tasks
.iter()
.filter(|t| t.task.meta.status == Status::InProgress)
.collect();
if active.is_empty() {
println!("(none)");
}
for st in &active {
let m = &st.task.meta;
let marker = if current == Some(m.seq) { "*" } else { " " };
println!("{marker} #{} [{}] {}", m.seq, m.priority, m.title);
if let Some(d) = &m.description {
println!(" {d}");
}
if let Some(due) = m.due_date {
println!(" due {}", due.date_naive());
}
if !st.task.steps.is_empty() {
let done = st.task.steps.iter().filter(|s| s.done).count();
println!(" steps {done}/{}", st.task.steps.len());
for (i, step) in st.task.steps.iter().enumerate() {
let mark = if step.done { "x" } else { " " };
println!(" s{} [{mark}] {}", i + 1, step.title);
}
}
}
let count = |s: Status| tasks.iter().filter(|t| t.task.meta.status == s).count();
let now = chrono::Utc::now();
let overdue = tasks
.iter()
.filter(|t| {
!matches!(t.task.meta.status, Status::Done | Status::Cancelled)
&& t.task.meta.due_date.is_some_and(|d| d < now)
})
.count();
println!(
"=== todo {} | blocked {} | in_review {} | done {} | cancelled {} | overdue {} ===",
count(Status::Todo),
count(Status::Blocked),
count(Status::InReview),
count(Status::Done),
count(Status::Cancelled),
overdue
);
Ok(())
}