Choose a file from the tree.
import * as todo from "./todo.ts";
const [command, ...rest] = process.argv.slice(2);
try {
switch (command) {
case "add":
add(rest);
break;
case "list":
list(rest);
break;
case "done":
done(rest);
break;
case "remove":
remove(rest);
break;
default:
usage();
}
} catch (error) {
console.error((error as Error).message);
process.exitCode = 1;
}
function add(args: string[]) {
const words: string[] = [];
const needs: string[] = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === "--needs") {
i = i + 1;
needs.push(...(args[i] ?? "").split(",").filter((id) => id !== ""));
} else {
words.push(args[i]);
}
}
const added = todo.add(words.join(" "), needs);
console.log(`added ${added.id} ${added.title}`);
}
function list(args: string[]) {
const asked = args[0] ?? "all";
const filter = todo.filters.find((candidate) => candidate === asked);
if (filter === undefined) throw new Error(`usage: list <${todo.filters.join("|")}>`);
const todos = todo.all();
const shown = todo.select(todos, filter);
if (shown.length === 0) {
console.log("nothing to do");
return;
}
const rows = [["id", "status", "title", "needs"]];
for (const item of shown) {
const waiting = todo.waitingFor(item, todos);
rows.push([item.id, todo.statusOf(item, todos), item.title, waiting.map((other) => other.id).join(" ")]);
}
table(rows);
}
function done(args: string[]) {
const finished = todo.done(args[0]);
console.log(`done ${finished.id} ${finished.title}`);
}
function remove(args: string[]) {
const removed = todo.remove(args[0]);
console.log(`removed ${removed.id}`);
}
function table(rows: string[][]) {
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
for (const row of rows) {
console.log(
row
.map((cell, column) => cell.padEnd(widths[column]))
.join(" ")
.trimEnd(),
);
}
}
function usage() {
console.log("usage: todo <add|list|done|remove>");
console.log(" add <title> [--needs <id>,<id>]");
console.log(` list [${todo.filters.join("|")}]`);
console.log(" done <id>");
console.log(" remove <id>");
}
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
export type Todo = {
id: string;
title: string;
done: boolean;
needs: string[];
createdAt: string;
};
export type Status = "done" | "ready" | "waiting";
export type Filter = "all" | "open" | "done" | "ready";
export const filters: Filter[] = ["all", "open", "done", "ready"];
const dir = join(homedir(), ".todos");
if (!existsSync(dir)) mkdirSync(dir);
export function add(title: string, needs: string[]): Todo {
const todos = all();
if (title.trim() === "") throw new Error("a todo needs a title");
for (const id of needs) find(todos, id);
const todo: Todo = {
id: Date.now().toString(36),
title: title.trim(),
done: false,
needs,
createdAt: new Date().toISOString(),
};
write(todo);
return todo;
}
export function done(id: string): Todo {
const todos = all();
const todo = find(todos, id);
const waiting = waitingFor(todo, todos);
if (waiting.length > 0) {
throw new Error(`${todo.id} still waits for ${waiting.map((other) => other.id).join(" ")}`);
}
todo.done = true;
write(todo);
return todo;
}
export function remove(id: string): Todo {
const todo = find(all(), id);
rmSync(join(dir, `${todo.id}.json`));
return todo;
}
export function all(): Todo[] {
const todos: Todo[] = readdirSync(dir)
.filter((file) => file.endsWith(".json"))
.map((file) => JSON.parse(readFileSync(join(dir, file), "utf8")))
.map((todo) => ({ ...todo, needs: todo.needs ?? [] }));
todos.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
return todos;
}
export function select(todos: Todo[], filter: Filter): Todo[] {
return todos.filter((todo) => {
if (filter === "open") return !todo.done;
if (filter === "done") return todo.done;
if (filter === "ready") return statusOf(todo, todos) === "ready";
return true;
});
}
export function statusOf(todo: Todo, todos: Todo[]): Status {
if (todo.done) return "done";
if (waitingFor(todo, todos).length === 0) return "ready";
return "waiting";
}
export function waitingFor(todo: Todo, todos: Todo[]): Todo[] {
const waiting: Todo[] = [];
for (const id of todo.needs) {
const other = todos.find((candidate) => candidate.id === id);
if (other !== undefined && !other.done) waiting.push(other);
}
return waiting;
}
function find(todos: Todo[], id: string): Todo {
const todo = todos.find((candidate) => candidate.id === id);
if (todo === undefined) throw new Error(`there is no todo with id ${id}`);
return todo;
}
function write(todo: Todo) {
writeFileSync(join(dir, `${todo.id}.json`), JSON.stringify(todo, null, 2) + "\n");
}