A command-line journaling application
at main 167 lines 4.1 kB view raw
1import { partition } from "@std/collections"; 2import * as cli from "@std/cli"; 3import { DateTime } from "luxon"; 4import * as chrono from "chrono-node"; 5import { loadConfig } from "./config.ts"; 6import { 7 type Entry, 8 filterEntries, 9 type FilterParams, 10 makeEntry, 11 matchesFilter, 12 parseEntries, 13 printEntry, 14 renderEntry, 15 saveEntries, 16} from "./mod.ts"; 17 18function tryDate(d?: unknown): DateTime | undefined { 19 if (typeof d !== "string") return undefined; 20 const parsed = chrono.parseDate(d); 21 return parsed ? DateTime.fromJSDate(parsed) : undefined; 22} 23 24function printFilteredEntries( 25 entries: Entry[], 26 params: FilterParams, 27 summary: boolean, 28): void { 29 const filtered = filterEntries(entries, params); 30 let first = true; 31 for (const entry of filtered) { 32 if (!first && !summary) { 33 console.log(); 34 } 35 first = false; 36 printEntry(entry, summary); 37 } 38} 39 40async function edit( 41 body: string, 42 editor: string[], 43): Promise<string | undefined> { 44 const temp = Deno.makeTempFileSync({ suffix: ".hayom" }); 45 try { 46 Deno.writeTextFileSync(temp, body); 47 const command = new Deno.Command(editor[0], { 48 args: [...editor.slice(1), temp], 49 }); 50 const proc = command.spawn(); 51 if ((await proc.status).success) { 52 return Deno.readTextFileSync(temp); 53 } 54 console.error("Aborted."); 55 } finally { 56 Deno.remove(temp); 57 } 58} 59 60async function editFilteredEntries( 61 entries: Entry[], 62 filter: FilterParams, 63 editor: string[], 64): Promise<Entry[] | undefined> { 65 const [toEdit, toKeep] = partition(entries, (e) => matchesFilter(e, filter)); 66 const result = await edit(toEdit.map(renderEntry).join("\n"), editor); 67 if (result == null) return undefined; 68 return [...toKeep, ...parseEntries(result.replace("\r", ""))]; 69} 70 71function printHelp(): void { 72 console.log(` 73usage: hayom [-j journal] ... 74options: 75 --count | -n: number of entries to print 76 --edit | -e: edit entries 77 --from | -f: from timestamp 78 --journal | -j: journal to use 79 --on: on timestamp 80 --summary | -s: print summary line only 81 --to | -t: to timestamp 82`); 83} 84 85async function main(): Promise<void> { 86 const args = [...Deno.args]; 87 const config = loadConfig(); 88 89 const opts = cli.parseArgs(args, { 90 boolean: ["summary"], 91 alias: { 92 "e": ["edit"], 93 "f": ["from"], 94 "j": ["journal"], 95 "n": ["count"], 96 "s": ["summary"], 97 "t": ["to"], 98 }, 99 }); 100 101 if (opts.help) { 102 printHelp(); 103 return; 104 } 105 106 const journal = typeof opts.journal === "string" 107 ? opts.journal 108 : config.default; 109 const path = config.journals[journal].journal; 110 111 try { 112 Deno.lstatSync(path); 113 } catch (e) { 114 if (e instanceof Deno.errors.NotFound) { 115 Deno.createSync(path).close(); 116 } else throw e; 117 } 118 119 const entries = parseEntries(Deno.readTextFileSync(path).replace("\r", "")); 120 121 if ( 122 ["from", "f", "to", "t", "on", "count", "n"].some((arg) => arg in opts) || 123 (opts._.length > 0 && 124 opts._.every((e) => typeof e === "string" && e[0] === "@")) 125 ) { 126 let from = tryDate(opts.from); 127 let to = tryDate(opts.to); 128 if (opts.on) { 129 const on = tryDate(opts.on); 130 if (typeof on !== "object") { 131 throw new Error("Bad date"); 132 } 133 [from, to] = [on.startOf("day"), on.endOf("day")]; 134 } 135 const tags = opts._.filter((arg) => 136 typeof arg === "string" && arg.match(/^@./) 137 ) as string[]; 138 139 const filter = { from, to, tags, limit: opts.count as number }; 140 141 if (opts.edit) { 142 const newEntries = await editFilteredEntries( 143 entries, 144 filter, 145 config.editor, 146 ); 147 if (newEntries != null) { 148 saveEntries(path, newEntries); 149 } 150 } else { 151 printFilteredEntries(entries, filter, opts.summary); 152 } 153 } else { 154 const rawEntry = opts._.length > 0 155 ? opts._.join(" ") 156 : await edit("", config.editor); 157 if (rawEntry && rawEntry.trim() !== "") { 158 const entry = makeEntry(rawEntry); 159 entries.push(entry); 160 saveEntries(path, entries); 161 } 162 } 163} 164 165if (import.meta.main) { 166 await main(); 167}