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