import { partition } from "@std/collections"; import * as cli from "@std/cli"; import { DateTime } from "luxon"; import * as chrono from "chrono-node"; import { loadConfig } from "./config.ts"; import { type Entry, filterEntries, type FilterParams, makeEntry, matchesFilter, parseEntries, printEntry, renderEntry, saveEntries, } from "./mod.ts"; function tryDate(d?: unknown): DateTime | undefined { if (typeof d !== "string") return undefined; const parsed = chrono.parseDate(d); return parsed ? DateTime.fromJSDate(parsed) : undefined; } function printFilteredEntries( entries: Entry[], params: FilterParams, summary: boolean, ): void { const filtered = filterEntries(entries, params); let first = true; for (const entry of filtered) { if (!first && !summary) { console.log(); } first = false; printEntry(entry, summary); } } async function edit( body: string, editor: string[], ): Promise { const temp = Deno.makeTempFileSync({ suffix: ".hayom" }); try { Deno.writeTextFileSync(temp, body); const command = new Deno.Command(editor[0], { args: [...editor.slice(1), temp], }); const proc = command.spawn(); if ((await proc.status).success) { return Deno.readTextFileSync(temp); } console.error("Aborted."); } finally { Deno.remove(temp); } } async function editFilteredEntries( entries: Entry[], filter: FilterParams, editor: string[], ): Promise { const [toEdit, toKeep] = partition(entries, (e) => matchesFilter(e, filter)); const result = await edit(toEdit.map(renderEntry).join("\n"), editor); if (result == null) return undefined; return [...toKeep, ...parseEntries(result.replace("\r", ""))]; } function printHelp(): void { console.log(` usage: hayom [-j journal] ... options: --count | -n: number of entries to print --edit | -e: edit entries --from | -f: from timestamp --journal | -j: journal to use --on: on timestamp --summary | -s: print summary line only --to | -t: to timestamp `); } async function main(): Promise { const args = [...Deno.args]; const config = loadConfig(); const opts = cli.parseArgs(args, { boolean: ["summary"], alias: { "e": ["edit"], "f": ["from"], "j": ["journal"], "n": ["count"], "s": ["summary"], "t": ["to"], }, }); if (opts.help) { printHelp(); return; } const journal = typeof opts.journal === "string" ? opts.journal : config.default; const path = config.journals[journal].journal; try { Deno.lstatSync(path); } catch (e) { if (e instanceof Deno.errors.NotFound) { Deno.createSync(path).close(); } else throw e; } const entries = parseEntries(Deno.readTextFileSync(path).replace("\r", "")); if ( ["from", "f", "to", "t", "on", "count", "n"].some((arg) => arg in opts) || (opts._.length > 0 && opts._.every((e) => typeof e === "string" && e[0] === "@")) ) { let from = tryDate(opts.from); let to = tryDate(opts.to); if (opts.on) { const on = tryDate(opts.on); if (typeof on !== "object") { throw new Error("Bad date"); } [from, to] = [on.startOf("day"), on.endOf("day")]; } const tags = opts._.filter((arg) => typeof arg === "string" && arg.match(/^@./) ) as string[]; const filter = { from, to, tags, limit: opts.count as number }; if (opts.edit) { const newEntries = await editFilteredEntries( entries, filter, config.editor, ); if (newEntries != null) { saveEntries(path, newEntries); } } else { printFilteredEntries(entries, filter, opts.summary); } } else { const rawEntry = opts._.length > 0 ? opts._.join(" ") : await edit("", config.editor); if (rawEntry && rawEntry.trim() !== "") { const entry = makeEntry(rawEntry); entries.push(entry); saveEntries(path, entries); } } } if (import.meta.main) { await main(); }