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; use fnmatch; use errors; use time; type task = struct { name: str, path: str, context: (str | void), tags: []str, priority: uint, content: str, lmd: time::instant, }; def METADATASEP: str = "----"; type rtaskerror = !(fs::error | ini::error | strconv::error | io::error | utf8::invalid | path::error); fn listtasks(root: str = "tasks", context: str = "*") ([]task | rtaskerror) = { const dirents = os::readdir(root)?; defer fs::dirents_free(dirents); let tasks: []task = []; for (const dirent .. dirents) { if (fs::isdir(dirent.ftype)) { if (context == "*" || fnmatch::fnmatch(context, dirent.name)) { let buf = path::init()?; const p = path::push(&buf, root, dirent.name)?; append(tasks, 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)?) { if (currentst != &content) { 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 isc = ini::scan(&meta); defer ini::finish(&isc); const stat = os::stat(taskpath)?; let t = task { context = void, tags = [], lmd = stat.mtime, ... }; for (let entry: ini::entry => ini::next(&isc)?) { switch (entry.1) { case "name" => t.name = strings::dup(strings::trim(entry.2)); case "priority" => t.priority = strconv::stou(entry.2)?; case "tags" => t.tags = strings::dupall(strings::split(entry.2, ",")); case => void; }; }; if (t.name == "") { t.name = strings::dup(path::basename(taskpath)); }; t.path = strings::dup(taskpath); t.content = strings::dup(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); }; strings::freeall(t.tags); }; fn finishall(tasks: []task) void = { for (const task .. tasks) { freetask(&task); }; }; fn sortpriority(a: const *opaque, b: const *opaque) int = { const a = a: *task; const b = b: *task; return (b.priority - a.priority): int; }; fn sortname(a: const *opaque, b: const *opaque) int = { const a = a: *task; const b = b: *task; return strings::compare(a.name, b.name); }; fn sortlmd(a: const *opaque, b: const *opaque) int = { const a = a: *task; const b = b: *task; return time::compare(b.lmd, a.lmd); }; fn strrtaskerror(e: rtaskerror) str = { return match (e) { case let e: fs::error => yield fs::strerror(e); case let e: ini::error => yield ini::strerror(e); case let e: strconv::error => yield strconv::strerror(e); case let e: io::error => yield io::strerror(e); case let e: utf8::invalid => yield utf8::strerror(e); case let e: path::error => yield path::strerror(e); }; }; fn filtertags(cfg: config, tasks: *[]task) void = { if (len(cfg.tags) == 0z) { return; }; for (let i = 0z; i < len(tasks); i += 1) { const t = tasks[i]; let found = false; for :tagloop (let tag .. t.tags) { for (let tagfilter .. cfg.tags) { if (tag == tagfilter) { found = true; break :tagloop; }; }; }; if (!found) { freetask(&tasks[i]); delete(tasks[i]); i -= 1; }; }; }; export fn main() void = { const cmd = getopt::parse(os::args, "tasklist", ('f', "path", "tasks directory"), ('c', "context", "context filter"), ('t', "tags", "tags filter"), ('p', "sort by priority"), ('m', "sort by last modification date"), ('d', "activate debug mode"), ("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), ("add", ["add a task", "id"]: []getopt::help), ("a", ["add a task", "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), ("tsv", ["print tsv of tasks"]: []getopt::help), ("t", ["print tsv of tasks"]: []getopt::help), ); defer getopt::finish(&cmd); const cfg: config = match (readconfig()) { case let cfg: config => yield cfg; case let e: fs::error => match (e) { case let e: errors::noentry => fmt::fatal("No config file found"); case => fmt::fatal(fs::strerror(e)); }; case let e: cerror => fmt::fatal(strcerror(e)); }; defer cfinish(&cfg); let sortfn: *sort::cmpfunc = &sortname; for (let opt .. cmd.opts) { switch (opt.0) { case 'f' => cfg.tasksdir = strings::dup(opt.1); case 'c' => cfg.context = strings::dup(opt.1); case 't' => cfg.tags = strings::dupall(strings::split(opt.1, ",")); case 'p' => sortfn = &sortpriority; case 'm' => sortfn = &sortlmd; case 'd' => cfg.debug = true; case => abort(); }; }; if (cfg.debug) { fmt::errorln("config:")!; printconfig(cfg); }; const tasks = match (listtasks(cfg.tasksdir, cfg.context)) { case let e: rtaskerror => fmt::fatal(strrtaskerror(e)); case let tasks: []task => yield tasks; }; defer finishall(tasks); sort::sort(tasks: []opaque, size(task), sortfn); filtertags(cfg, &tasks); const com: (str, *getopt::command) = match (cmd.subcmd) { case void => listall(cfg, tasks); return; case let subcmd: (str, *getopt::command) => yield (subcmd.0, subcmd.1); }; match (execcommand(cfg, com.0, tasks, com.1)) { case let e: error => fmt::fatal(strerror(e)); case => void; }; };