#!/usr/bin/env bun import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { parseArgs } from "node:util"; 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 partition(arr: T[], pred: (e: T) => boolean): [T[], T[]] { const yes: T[] = [], no: T[] = []; for (const e of arr) (pred(e) ? yes : no).push(e); return [yes, no]; } 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); } } function edit( body: string, editor: string[], ): string | undefined { const temp = join(tmpdir(), `hayom-${crypto.randomUUID()}.hayom`); try { writeFileSync(temp, body); const result = spawnSync(editor[0], [...editor.slice(1), temp], { stdio: "inherit", }); if (result.status === 0) { return readFileSync(temp, "utf-8"); } console.error("Aborted."); } finally { try { unlinkSync(temp); } catch {} } } function editFilteredEntries( entries: Entry[], filter: FilterParams, editor: string[], ): Entry[] | undefined { const [toEdit, toKeep] = partition(entries, (e) => matchesFilter(e, filter)); const result = 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 `); } function main(): void { const config = loadConfig(); const { values: opts, positionals } = parseArgs({ args: process.argv.slice(2), allowPositionals: true, options: { summary: { type: "boolean", short: "s" }, edit: { type: "boolean", short: "e" }, from: { type: "string", short: "f" }, journal: { type: "string", short: "j" }, count: { type: "string", short: "n" }, to: { type: "string", short: "t" }, on: { type: "string" }, help: { type: "boolean", short: "h" }, }, }); if (opts.help) { printHelp(); return; } const journal = opts.journal ?? config.default; const path = config.journals[journal].journal; if (!existsSync(path)) { writeFileSync(path, ""); } const entries = parseEntries(readFileSync(path, "utf-8").replace("\r", "")); if ( opts.from != null || opts.to != null || opts.on != null || opts.count != null || (positionals.length > 0 && positionals.every((e) => 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 = positionals.filter((arg) => arg.match(/^@./)); const filter: FilterParams = { from, to, tags, limit: opts.count ? parseInt(opts.count) : undefined, }; if (opts.edit) { const newEntries = editFilteredEntries(entries, filter, config.editor); if (newEntries != null) { saveEntries(path, newEntries); } } else { printFilteredEntries(entries, filter, opts.summary ?? false); } } else { const rawEntry = positionals.length > 0 ? positionals.join(" ") : edit("", config.editor); if (rawEntry && rawEntry.trim() !== "") { const entry = makeEntry(rawEntry); entries.push(entry); saveEntries(path, entries); } } } if (import.meta.main) { main(); }