A command-line journaling application
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}