use fmt; use dirs; use bufio; use memio; use os; use fs; use path; use io; use strings; use format::ini; use strconv; use encoding::utf8; use getopt; use sort; type task = struct { name: str, path: str, context: (str | void), tags: ([]str | void), priority: uint, content: str, }; def METADATASEP: str = "----"; type rtaskerror = !(fs::error | ini::error | strconv::error | io::error | utf8::invalid); fn listtasks(root: str = "tasks", context: str = "*") ([]task | rtaskerror | path::error) = { const dirents = os::readdir(root)?; defer fs::dirents_free(dirents); let tasks: []task = []; for (const dirent .. dirents) { if (context == "*" || dirent.name == context) { if (fs::isdir(dirent.ftype)) { let buf = path::init()?; const p = path::push(&buf, root, dirent.name)?; listtasks(p)?; } else { let buf = path::init()?; const p = path::push(&buf, root, dirent.name)?; let t = readtask(p)?; append(tasks, t); }; }; }; return tasks; }; fn readtask(taskpath: str) (task | rtaskerror) = { const f = os::open(taskpath)?; defer io::close(f)!; const sc = bufio::newscanner(f); defer bufio::finish(&sc); const content = memio::dynamic(); defer io::close(&content)!; const meta = memio::dynamic(); defer io::close(&meta)!; let currentst = &meta; for (let line => bufio::scan_line(&sc)?) { line = strings::trim(line); if (line == METADATASEP) { currentst = &content; continue; }; memio::concat(currentst, line, "\n")?; }; // seek to start of memio buffer io::seek(&meta, 0, io::whence::SET)?; const sc = ini::scan(&meta); defer ini::finish(&sc); let t = task { context = void, tags = void, ... }; for (let entry: ini::entry => ini::next(&sc)?) { switch (entry.1) { case "name" => t.name = strings::dup(strings::trim(entry.2)); case "priority" => t.priority = strconv::stou(entry.2)?; case => void; }; }; if (t.name == "") { t.name = strings::dup(path::basename(taskpath)); }; t.path = strings::dup(taskpath); t.content = strings::dup(strings::trim(memio::string(&content)?)); return t; }; fn freetask(t: *task) void = { free(t.name); free(t.path); free(t.content); if (t.context is str) { free(t.context as str); }; if (t.tags is []str) { strings::freeall(t.tags as []str); }; }; fn finishall(tasks: []task) void = { for (const task .. tasks) { freetask(&task); }; }; fn sortname(a: const *opaque, b: const *opaque) int = { const a = a: *task; const b = b: *task; return strings::compare(a.name, b.name); }; export fn main() void = { const cmd = getopt::parse(os::args, "tasklist", ("filter", ["filter tasks", "id"]: []getopt::help), ("f", ["filter tasks", "id"]: []getopt::help), ("show", ["show task details", "id"]: []getopt::help), ("s", ["show task details", "id"]: []getopt::help), ("write", ["write a task", "id"]: []getopt::help), ("w", ["write a task", "id"]: []getopt::help), ("done", ["delete a task", "id"]: []getopt::help), ("d", ["delete a task", "id"]: []getopt::help), ("csv", ["print csv of tasks"]: []getopt::help), ("c", ["print csv of tasks"]: []getopt::help), ); defer getopt::finish(&cmd); const tasks = match (listtasks()) { case let e: fs::error => fmt::fatal(fs::strerror(e)); case let e: path::error => fmt::fatal(path::strerror(e)); case let tasks: []task => yield tasks; }; sort::sort(tasks: []opaque, size(task), &sortname); defer finishall(tasks); const com: (str, *getopt::command) = match (cmd.subcmd) { case void => listall(tasks); return; case let subcmd: (str, *getopt::command) => yield (subcmd.0, subcmd.1); }; match (execcommand(com.0, tasks, com.1)) { case let e: error => fmt::fatal(strerror(e)); case => void; }; };