things-api 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -11
- package/dist/audit/log.js +56 -2
- package/dist/audit/log.js.map +1 -1
- package/dist/audit/schema.d.ts +26 -8
- package/dist/audit/schema.js +42 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/area.d.ts +34 -2
- package/dist/cli/commands/area.js +140 -28
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +44 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/mcp.js +23 -4
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +23 -1
- package/dist/cli/commands/project.js +82 -15
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +6 -125
- package/dist/cli/commands/reads.js +649 -657
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/repeat-flags.d.ts +20 -0
- package/dist/cli/commands/repeat-flags.js +76 -0
- package/dist/cli/commands/repeat-flags.js.map +1 -0
- package/dist/cli/commands/setup.js +2 -3
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.d.ts +1 -1
- package/dist/cli/commands/show.js +169 -18
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/snapshot.js +1 -1
- package/dist/cli/commands/snapshot.js.map +1 -1
- package/dist/cli/commands/todo.d.ts +1 -1
- package/dist/cli/commands/todo.js +21 -9
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +378 -148
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +29 -0
- package/dist/cli/did-you-mean.js +53 -0
- package/dist/cli/did-you-mean.js.map +1 -0
- package/dist/cli/excess-args.d.ts +15 -0
- package/dist/cli/excess-args.js +51 -0
- package/dist/cli/excess-args.js.map +1 -0
- package/dist/cli/glyphs.d.ts +67 -7
- package/dist/cli/glyphs.js +202 -19
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +54 -0
- package/dist/cli/help.js +355 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/main.js +36 -21
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +39 -0
- package/dist/cli/period.js +141 -0
- package/dist/cli/period.js.map +1 -0
- package/dist/cli/read-driver.d.ts +83 -0
- package/dist/cli/read-driver.js +215 -0
- package/dist/cli/read-driver.js.map +1 -0
- package/dist/cli/render.d.ts +246 -0
- package/dist/cli/render.js +806 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/resolve-invocation.d.ts +116 -0
- package/dist/cli/resolve-invocation.js +261 -0
- package/dist/cli/resolve-invocation.js.map +1 -0
- package/dist/cli/shell-quote.d.ts +9 -0
- package/dist/cli/shell-quote.js +12 -0
- package/dist/cli/shell-quote.js.map +1 -0
- package/dist/cli/tag-filters.d.ts +59 -0
- package/dist/cli/tag-filters.js +57 -0
- package/dist/cli/tag-filters.js.map +1 -0
- package/dist/cli/verb-hint.d.ts +25 -0
- package/dist/cli/verb-hint.js +138 -0
- package/dist/cli/verb-hint.js.map +1 -0
- package/dist/cli/width.d.ts +135 -0
- package/dist/cli/width.js +313 -0
- package/dist/cli/width.js.map +1 -0
- package/dist/client.d.ts +146 -24
- package/dist/client.js +106 -16
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +136 -1
- package/dist/contracts.js +44 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/fingerprint.d.ts +12 -0
- package/dist/db/fingerprint.js +15 -1
- package/dist/db/fingerprint.js.map +1 -1
- package/dist/db/locate.js +1 -1
- package/dist/db/locate.js.map +1 -1
- package/dist/diagnose.d.ts +63 -0
- package/dist/diagnose.js +38 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +30 -4
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -1
- package/dist/mcp/server.js +850 -128
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +40 -7
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +7 -0
- package/dist/model/mappers.js +20 -3
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/occurrences.d.ts +6 -1
- package/dist/model/occurrences.js +9 -5
- package/dist/model/occurrences.js.map +1 -1
- package/dist/model/recurrence.d.ts +17 -8
- package/dist/model/recurrence.js.map +1 -1
- package/dist/model/serialize.d.ts +28 -0
- package/dist/model/serialize.js +80 -0
- package/dist/model/serialize.js.map +1 -0
- package/dist/model/when-sugar.d.ts +47 -0
- package/dist/model/when-sugar.js +45 -0
- package/dist/model/when-sugar.js.map +1 -0
- package/dist/read/area-view.d.ts +8 -1
- package/dist/read/area-view.js +49 -13
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/caps.d.ts +14 -0
- package/dist/read/caps.js +21 -0
- package/dist/read/caps.js.map +1 -0
- package/dist/read/filter-contract.d.ts +117 -0
- package/dist/read/filter-contract.js +78 -0
- package/dist/read/filter-contract.js.map +1 -0
- package/dist/read/log-boundary.d.ts +9 -4
- package/dist/read/log-boundary.js.map +1 -1
- package/dist/read/predicates.d.ts +61 -0
- package/dist/read/predicates.js +78 -0
- package/dist/read/predicates.js.map +1 -0
- package/dist/read/project-view.d.ts +12 -1
- package/dist/read/project-view.js +39 -8
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +156 -8
- package/dist/read/queries.js +261 -38
- package/dist/read/queries.js.map +1 -1
- package/dist/read/search-rank.d.ts +36 -0
- package/dist/read/search-rank.js +31 -0
- package/dist/read/search-rank.js.map +1 -0
- package/dist/read/sections.d.ts +51 -0
- package/dist/read/sections.js +37 -0
- package/dist/read/sections.js.map +1 -0
- package/dist/read/show-target.d.ts +13 -4
- package/dist/read/show-target.js +11 -2
- package/dist/read/show-target.js.map +1 -1
- package/dist/read/sidebar-order.d.ts +19 -0
- package/dist/read/sidebar-order.js +93 -0
- package/dist/read/sidebar-order.js.map +1 -0
- package/dist/read/tags.d.ts +27 -3
- package/dist/read/tags.js +83 -12
- package/dist/read/tags.js.map +1 -1
- package/dist/read/truncation.d.ts +71 -0
- package/dist/read/truncation.js +217 -0
- package/dist/read/truncation.js.map +1 -0
- package/dist/read/views.d.ts +177 -19
- package/dist/read/views.js +390 -213
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +42 -0
- package/dist/surface-copy.js +55 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/sync-health.d.ts +78 -0
- package/dist/sync-health.js +312 -0
- package/dist/sync-health.js.map +1 -0
- package/dist/write/accessibility-probe.d.ts +12 -0
- package/dist/write/accessibility-probe.js +63 -0
- package/dist/write/accessibility-probe.js.map +1 -0
- package/dist/write/automation-probe.d.ts +7 -0
- package/dist/write/automation-probe.js +8 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.js +4 -2
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +8 -0
- package/dist/write/capabilities.js +13 -5
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/commands.d.ts +2 -0
- package/dist/write/commands.js +562 -48
- package/dist/write/commands.js.map +1 -1
- package/dist/write/edit-checklist.js +3 -2
- package/dist/write/edit-checklist.js.map +1 -1
- package/dist/write/guards.d.ts +2 -7
- package/dist/write/guards.js +64 -6
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.js +2 -0
- package/dist/write/heading.js.map +1 -1
- package/dist/write/lock.d.ts +40 -2
- package/dist/write/lock.js +91 -14
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +4 -0
- package/dist/write/make-repeating-project.js +253 -0
- package/dist/write/make-repeating-project.js.map +1 -0
- package/dist/write/operations.d.ts +145 -1
- package/dist/write/operations.js +48 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.d.ts +19 -0
- package/dist/write/pipeline.js +109 -26
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +68 -3
- package/dist/write/pre-state.js +102 -3
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reopen.js +3 -2
- package/dist/write/reopen.js.map +1 -1
- package/dist/write/reorder.js +27 -17
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +22 -0
- package/dist/write/repeat-rule.js +258 -0
- package/dist/write/repeat-rule.js.map +1 -0
- package/dist/write/reversibility.js +49 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/tag-refs.d.ts +47 -0
- package/dist/write/tag-refs.js +126 -0
- package/dist/write/tag-refs.js.map +1 -0
- package/dist/write/undo.d.ts +74 -5
- package/dist/write/undo.js +494 -80
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +17 -1
- package/dist/write/vectors/registry.js +18 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/types.d.ts +119 -3
- package/dist/write/vectors/ui-certification.d.ts +48 -0
- package/dist/write/vectors/ui-certification.js +46 -0
- package/dist/write/vectors/ui-certification.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +188 -0
- package/dist/write/vectors/ui-drag.js +1095 -0
- package/dist/write/vectors/ui-drag.js.map +1 -0
- package/dist/write/vectors/ui-recipes.d.ts +70 -0
- package/dist/write/vectors/ui-recipes.js +558 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -0
- package/dist/write/vectors/ui.d.ts +137 -0
- package/dist/write/vectors/ui.js +656 -0
- package/dist/write/vectors/ui.js.map +1 -0
- package/dist/write/verify/delta.d.ts +31 -3
- package/dist/write/verify/delta.js +22 -6
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.js +1 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/exit-codes.d.ts +0 -26
- package/dist/cli/exit-codes.js +0 -26
- package/dist/cli/exit-codes.js.map +0 -1
- package/dist/cli/output.d.ts +0 -42
- package/dist/cli/output.js +0 -16
- package/dist/cli/output.js.map +0 -1
|
@@ -1,162 +1,21 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { openThings } from "../../client.js";
|
|
3
|
-
import { ThingsDbNotFoundError } from "../../db/locate.js";
|
|
4
|
-
import { ThingsDbOpenError } from "../../db/connection.js";
|
|
5
|
-
import { isTodayMember } from "../../read/views.js";
|
|
6
|
-
import { localToday } from "../../model/dates.js";
|
|
7
|
-
import { templateStatus } from "../../model/recurrence.js";
|
|
8
|
-
import { blue, bold, dim, strike, underline } from "../style.js";
|
|
9
|
-
import { areaMark, CHECKLIST_MARK, countChip, dateChip, deadlineToken, eveningMoon, loggedDate, NOTES_MARK, projectCircle, REMINDER_MARK, shortDate, todayStar, todoBox, } from "../glyphs.js";
|
|
10
|
-
import { errorEnvelope, ExitCode, okEnvelope } from "../../contracts.js";
|
|
11
|
-
export function withClient(opts, kind, fn, render) {
|
|
12
|
-
const started = Date.now();
|
|
13
|
-
let client = null;
|
|
14
|
-
try {
|
|
15
|
-
client = openThings(opts.db ? { dbPath: opts.db } : {});
|
|
16
|
-
const fp = client.fingerprint();
|
|
17
|
-
const data = fn(client);
|
|
18
|
-
const meta = {
|
|
19
|
-
dbVersion: fp.observation.databaseVersion,
|
|
20
|
-
fingerprint: fp.kind === "ok" ? "ok" : fp.kind === "drift" ? "drift" : "unknown",
|
|
21
|
-
elapsedMs: Date.now() - started,
|
|
22
|
-
};
|
|
23
|
-
if (fp.kind !== "ok") {
|
|
24
|
-
process.stderr.write(`warning: schema fingerprint ${meta.fingerprint} — reads best-effort, writes disabled (run \`things doctor\`)\n`);
|
|
25
|
-
}
|
|
26
|
-
if (opts.json) {
|
|
27
|
-
process.stdout.write(`${JSON.stringify(okEnvelope(kind, data, meta))}\n`);
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
process.stdout.write(`${render(data).join("\n")}\n`);
|
|
31
|
-
}
|
|
32
|
-
process.exitCode = ExitCode.Ok;
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
const meta = {
|
|
36
|
-
dbVersion: null,
|
|
37
|
-
fingerprint: "unknown",
|
|
38
|
-
elapsedMs: Date.now() - started,
|
|
39
|
-
};
|
|
40
|
-
const isEnv = err instanceof ThingsDbNotFoundError || err instanceof ThingsDbOpenError;
|
|
41
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
-
if (opts.json) {
|
|
43
|
-
process.stdout.write(`${JSON.stringify(errorEnvelope({ code: isEnv ? "environment" : "unexpected", message }, meta))}\n`);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
process.stderr.write(`error: ${message}\n`);
|
|
47
|
-
}
|
|
48
|
-
process.exitCode = isEnv ? ExitCode.Environment : ExitCode.Unexpected;
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
client?.close();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
1
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* for to-dos, `( )`-family for projects — state survives with color
|
|
61
|
-
* stripped. Completed titles dim; canceled titles dim+strike (the `[×]`
|
|
62
|
-
* mark keeps the state when strike/ANSI is unavailable). Human output shows
|
|
63
|
-
* a SHORTENED uuid prefix (every command accepts unique prefixes >= 6
|
|
64
|
-
* chars); `uuidWidth` is the display length from uuidDisplayWidth — never
|
|
65
|
-
* below 8 so a copied prefix stays unique across the whole database, not
|
|
66
|
-
* just the rendered list. Tags follow the title (`#`-prefixed, green — GUI
|
|
67
|
-
* color), after the count chip (projects), notes marker, and container.
|
|
68
|
-
* Heading-nested to-dos label their parent PROJECT (via headingProject),
|
|
69
|
-
* never the heading — GUI behavior. Colors engage on a TTY only
|
|
70
|
-
* (../style.ts); `--json` always carries full uuids.
|
|
2
|
+
* Read-only list commands: option/description wiring per view, delegating to
|
|
3
|
+
* the read driver (../read-driver.ts) for envelope/output and to the pure
|
|
4
|
+
* renderers (../render.ts) for human lines. Each command renders a compact
|
|
5
|
+
* human table (UUIDs always shown — agents and humans both need stable
|
|
6
|
+
* references) or a --json envelope.
|
|
71
7
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
meta.push(loggedDate(item.stopped, todayIso));
|
|
84
|
-
if (opts.hideDateChip === true) {
|
|
85
|
-
// rows under a day header — the header carries the date
|
|
86
|
-
}
|
|
87
|
-
else if (item.status === "open" && item.startDate !== null && item.startDate > todayIso)
|
|
88
|
-
meta.push(dateChip(item.startDate, todayIso));
|
|
89
|
-
// Repeating templates chip their app-materialized next occurrence.
|
|
90
|
-
else if (item.repeating.isTemplate && item.repeating.nextOccurrence != null)
|
|
91
|
-
meta.push(dateChip(item.repeating.nextOccurrence, todayIso));
|
|
92
|
-
// List rows mute their tags (the GUI's gray pills); tags go green only on
|
|
93
|
-
// the opened resource (todo show / the project|area header row).
|
|
94
|
-
const tags = item.tags.length > 0 ? ` ${dim(`#${item.tags.map((t) => t.title).join(" #")}`)}` : "";
|
|
95
|
-
// Closed rows drop the deadline flag — a months-old red "n days ago" on a
|
|
96
|
-
// logged item is noise (the GUI doesn't flag logbook rows either). Raw
|
|
97
|
-
// template rows drop it via the sentinel guard: their deadline column
|
|
98
|
-
// carries app-internal 4001-01-01 sentinels (upcoming's synthesized
|
|
99
|
-
// occurrences carry REAL rule-derived deadlines and must keep the flag).
|
|
100
|
-
const rule = item.repeating.rule;
|
|
101
|
-
const deadline = item.status === "open" && item.deadline !== null && item.deadline < "4000"
|
|
102
|
-
? ` ${deadlineToken(item.deadline, todayIso)}`
|
|
103
|
-
: // The GUI's bare flag on no-date repeating templates: the rule WILL
|
|
104
|
-
// assign each occurrence a deadline (fixed rules always do; after-
|
|
105
|
-
// completion only with a start offset), date unknown until spawned.
|
|
106
|
-
item.repeating.isTemplate &&
|
|
107
|
-
rule !== undefined &&
|
|
108
|
-
(rule.type === "fixed" || rule.startOffsetDays < 0)
|
|
109
|
-
? ` ${bold(dim("⚑"))}`
|
|
110
|
-
: "";
|
|
111
|
-
const container = item.type === "to-do" ? (item.project ?? item.headingProject ?? null) : null;
|
|
112
|
-
const context = container !== null
|
|
113
|
-
? container.uuid === opts.suppressProject
|
|
114
|
-
? ""
|
|
115
|
-
: ` (${container.title})`
|
|
116
|
-
: item.area
|
|
117
|
-
? item.area.uuid === opts.suppressArea
|
|
118
|
-
? ""
|
|
119
|
-
: ` (${item.area.title})`
|
|
120
|
-
: "";
|
|
121
|
-
const shownUuid = uuidWidth > 0 && item.uuid.length > uuidWidth
|
|
122
|
-
? item.uuid.slice(0, uuidWidth)
|
|
123
|
-
: item.uuid.padEnd(uuidWidth);
|
|
124
|
-
let title = item.title;
|
|
125
|
-
if (asTitle)
|
|
126
|
-
title = bold(underline(title));
|
|
127
|
-
else if (item.status === "canceled")
|
|
128
|
-
title = dim(strike(title));
|
|
129
|
-
else if (item.status === "completed")
|
|
130
|
-
title = dim(title);
|
|
131
|
-
// Open project rows render their title blue — GUI parity (list accent) and
|
|
132
|
-
// a color cue reinforcing the round bracket. To-dos stay default-colored.
|
|
133
|
-
else if (item.type === "project")
|
|
134
|
-
title = blue(title);
|
|
135
|
-
// GUI indicator order after the title: bell, document, checklist.
|
|
136
|
-
const tail = [
|
|
137
|
-
...(item.type === "project" ? [countChip(item)] : []),
|
|
138
|
-
...(item.reminder !== null ? [dim(REMINDER_MARK)] : []),
|
|
139
|
-
...(item.notes !== "" ? [dim(NOTES_MARK)] : []),
|
|
140
|
-
...(item.type === "to-do" && item.checklistItemsCount > 0 ? [dim(CHECKLIST_MARK)] : []),
|
|
141
|
-
];
|
|
142
|
-
return [
|
|
143
|
-
`${dim(shownUuid)} `,
|
|
144
|
-
box,
|
|
145
|
-
...meta,
|
|
146
|
-
`${title}${tail.length > 0 ? ` ${tail.join(" ")}` : ""}${tags}${context === "" ? "" : dim(context)}${deadline}`,
|
|
147
|
-
].join(" ");
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Row prefix in today-aware views: yellow ★ for Today members, cyan ⏾ for
|
|
151
|
-
* effective This-Evening members (raw evening assignment counts only while
|
|
152
|
-
* startDate is exactly today — the UI's daily expiry), null otherwise.
|
|
153
|
-
*/
|
|
154
|
-
export function todayMark(item, now) {
|
|
155
|
-
if (!isTodayMember(item, now))
|
|
156
|
-
return null;
|
|
157
|
-
const evening = item.todaySection === "evening" && item.startDate === localToday(now);
|
|
158
|
-
return evening ? eveningMoon() : todayStar();
|
|
159
|
-
}
|
|
8
|
+
import { Option } from "commander";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { runAreaShow } from "./area.js";
|
|
11
|
+
import { runProjectShow } from "./project.js";
|
|
12
|
+
import { dim } from "../style.js";
|
|
13
|
+
import { areaMark, LEGEND, shortDate } from "../glyphs.js";
|
|
14
|
+
import { formatItem, disclosureHint, renderAnytimePreview, renderLegend, renderList, renderLogbook, renderProjectsSidebar, renderSearch, renderSections, renderSomedayPreview, renderToday, renderUpcoming, stripAnsi, uuidCol, uuidDisplayWidth, } from "../render.js";
|
|
15
|
+
import { invocation, parseCap, parseLimit, runRead, shellQuote, truncationHint, usageError, withClient, } from "../read-driver.js";
|
|
16
|
+
import { addTagFilterOptions, collectRef, CONTAINER_TAG_HINT, EXACT_TAG_DESC, hasTagPresence, TAG_DESC, UNTAGGED_DESC, tagFilterFields, tagFlagConflict, tagInvocationParts, } from "../tag-filters.js";
|
|
17
|
+
import { doublePeriod, parsePeriodEnd, parsePeriodStart } from "../period.js";
|
|
18
|
+
import { ALL_DESC, AREA_LIMIT_DESC, AREA_PREVIEW_LIMIT, ExitCode, FILTER_CONTRACT, GROUPED_ALL_DESC, LIMIT_DESC, localToday, okEnvelope, PERIOD_SINCE, PERIOD_UNTIL, PROJECT_LIMIT_DESC, PROJECT_PREVIEW_LIMIT, validateViewArgs, } from "../../index.js";
|
|
160
19
|
/**
|
|
161
20
|
* Foreground the Things app on a resource via its share URI. A GUI action
|
|
162
21
|
* on this Mac — NOT headless; the shared implementation behind every
|
|
@@ -167,389 +26,291 @@ export function openInThings(uuid) {
|
|
|
167
26
|
execFileSync("/usr/bin/open", [uri]);
|
|
168
27
|
return uri;
|
|
169
28
|
}
|
|
170
|
-
/**
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const w = uuidDisplayWidth(items);
|
|
194
|
-
return items.length === 0 ? ["(empty)"] : items.map((i) => formatItem(i, w));
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* The sidebar mirror for `things projects`: loose projects first (the GUI
|
|
198
|
-
* lists them above the areas), then a `── ⬡ Area ──` header per area with
|
|
199
|
-
* its projects beneath (the redundant `(Area)` suffix suppressed). Items
|
|
200
|
-
* arrive from projectsView already in sidebar order — this only inserts the
|
|
201
|
-
* headers. Denser than renderSections on purpose: no title styling and no
|
|
202
|
-
* blank line per project (every row here IS a project). With `hints`,
|
|
203
|
-
* default-hidden later projects are never silent: each group trails a muted
|
|
204
|
-
* `…n later projects` count (a later-only area still gets its header), and
|
|
205
|
-
* the output ends with the flag that reveals them.
|
|
206
|
-
*/
|
|
207
|
-
export function renderProjectsSidebar(items, hints) {
|
|
208
|
-
const total = hints?.groups.reduce((n, g) => n + g.hidden, 0) ?? 0;
|
|
209
|
-
if (items.length === 0 && total === 0 && (hints === undefined || hints.groups.length === 0))
|
|
210
|
-
return ["(empty)"];
|
|
211
|
-
const w = uuidDisplayWidth(items);
|
|
212
|
-
const byGroup = new Map();
|
|
213
|
-
for (const item of items) {
|
|
214
|
-
const key = item.area?.uuid ?? null;
|
|
215
|
-
byGroup.set(key, [...(byGroup.get(key) ?? []), item]);
|
|
216
|
-
}
|
|
217
|
-
// hints (when present) carry the full sidebar group order, including
|
|
218
|
-
// later-only groups the visible items can't reveal.
|
|
219
|
-
const groups = hints?.groups ??
|
|
220
|
-
[...byGroup.keys()].map((key) => ({
|
|
221
|
-
area: key === null ? null : (items.find((i) => i.area?.uuid === key)?.area ?? null),
|
|
222
|
-
hidden: 0,
|
|
223
|
-
}));
|
|
224
|
-
const lines = [];
|
|
225
|
-
for (const group of groups) {
|
|
226
|
-
const rows = byGroup.get(group.area?.uuid ?? null) ?? [];
|
|
227
|
-
// The loose block only exists when it has content; areas mirror the
|
|
228
|
-
// sidebar and render even when empty.
|
|
229
|
-
if (group.area === null && rows.length === 0 && group.hidden === 0)
|
|
230
|
-
continue;
|
|
231
|
-
if (group.area !== null) {
|
|
232
|
-
if (lines.length > 0)
|
|
233
|
-
lines.push("");
|
|
234
|
-
lines.push(`${bold("──")} ${areaMark()} ${bold(`${group.area.title} ──`)}`);
|
|
29
|
+
/** Help copy for the `--overdue` content scope (open items past their deadline). */
|
|
30
|
+
const OVERDUE_DESC = "only open items past their deadline (due today is not overdue)";
|
|
31
|
+
export function registerReadCommands(program) {
|
|
32
|
+
program
|
|
33
|
+
.command("legend")
|
|
34
|
+
.description("The symbols and colors the list views use, grouped and explained: to-do boxes and " +
|
|
35
|
+
"project circles, markers and chips, colors and styles, section dividers and hints. " +
|
|
36
|
+
"Human-readable by default; --json emits {glyph, meaning, group} rows.")
|
|
37
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
38
|
+
.action((opts) => {
|
|
39
|
+
const started = Date.now();
|
|
40
|
+
if (opts.json) {
|
|
41
|
+
const entries = LEGEND.map((e) => ({
|
|
42
|
+
glyph: stripAnsi(e.glyph),
|
|
43
|
+
meaning: e.meaning,
|
|
44
|
+
group: e.group,
|
|
45
|
+
}));
|
|
46
|
+
const meta = {
|
|
47
|
+
dbVersion: null,
|
|
48
|
+
fingerprint: "unknown",
|
|
49
|
+
elapsedMs: Date.now() - started,
|
|
50
|
+
};
|
|
51
|
+
process.stdout.write(`${JSON.stringify(okEnvelope("legend", entries, meta))}\n`);
|
|
235
52
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
lines.push(dim(`…${group.hidden} later project${group.hidden === 1 ? "" : "s"}`));
|
|
239
|
-
else if (group.area !== null && rows.length === 0)
|
|
240
|
-
lines.push(dim("(no projects)"));
|
|
241
|
-
}
|
|
242
|
-
if (total > 0)
|
|
243
|
-
lines.push("", dim(`(${total} later project${total === 1 ? "" : "s"} — visible with \`things projects --show-later\`)`));
|
|
244
|
-
return lines;
|
|
245
|
-
}
|
|
246
|
-
const FULL_MONTHS = [
|
|
247
|
-
"January",
|
|
248
|
-
"February",
|
|
249
|
-
"March",
|
|
250
|
-
"April",
|
|
251
|
-
"May",
|
|
252
|
-
"June",
|
|
253
|
-
"July",
|
|
254
|
-
"August",
|
|
255
|
-
"September",
|
|
256
|
-
"October",
|
|
257
|
-
"November",
|
|
258
|
-
"December",
|
|
259
|
-
];
|
|
260
|
-
/**
|
|
261
|
-
* Relative period: `3d`/`2w`/`1m`/`1y` (days/weeks/calendar months/years),
|
|
262
|
-
* counted from `now` — FORWARD for an until-bound, BACKWARD for a since-
|
|
263
|
-
* bound (each command's natural direction; the flag name carries it).
|
|
264
|
-
*/
|
|
265
|
-
const RELATIVE_PERIOD = /^(\d+)([dwmy])$/i;
|
|
266
|
-
function relativePeriodDate(m, now, sign) {
|
|
267
|
-
const n = sign * Number(m[1]);
|
|
268
|
-
const unit = (m[2] ?? "").toLowerCase();
|
|
269
|
-
const d = new Date(now);
|
|
270
|
-
if (unit === "d")
|
|
271
|
-
d.setDate(d.getDate() + n);
|
|
272
|
-
else if (unit === "w")
|
|
273
|
-
d.setDate(d.getDate() + 7 * n);
|
|
274
|
-
else if (unit === "m")
|
|
275
|
-
d.setMonth(d.getMonth() + n);
|
|
276
|
-
else
|
|
277
|
-
d.setFullYear(d.getFullYear() + n);
|
|
278
|
-
return d;
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* `--until` accepting whole periods: `2024` means through Dec 31 2024,
|
|
282
|
-
* `2024-03` through Mar 31, `2024-03-05` through end of that day; relative
|
|
283
|
-
* periods (`2w`, `1m`, `1y`) count FORWARD from now through the end of the
|
|
284
|
-
* landing day; anything else parses as an instant.
|
|
285
|
-
*/
|
|
286
|
-
export function parsePeriodEnd(s, now = new Date()) {
|
|
287
|
-
const rel = RELATIVE_PERIOD.exec(s.trim());
|
|
288
|
-
if (rel !== null) {
|
|
289
|
-
const d = relativePeriodDate(rel, now, 1);
|
|
290
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
|
|
291
|
-
}
|
|
292
|
-
const m = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(s.trim());
|
|
293
|
-
if (m === null)
|
|
294
|
-
return new Date(s);
|
|
295
|
-
const year = Number(m[1]);
|
|
296
|
-
// Day 0 of month n+1 = the last day of month n.
|
|
297
|
-
if (m[2] === undefined)
|
|
298
|
-
return new Date(year, 11, 31, 23, 59, 59, 999);
|
|
299
|
-
const month = Number(m[2]) - 1;
|
|
300
|
-
if (m[3] === undefined)
|
|
301
|
-
return new Date(year, month + 1, 0, 23, 59, 59, 999);
|
|
302
|
-
return new Date(year, month, Number(m[3]), 23, 59, 59, 999);
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* `--since` accepting the same vocabulary at the period's START: `2024` =
|
|
306
|
-
* Jan 1 2024 00:00, `2024-03` = Mar 1, `2024-03-05` = that midnight;
|
|
307
|
-
* relative periods (`2w`, `1m`) count BACKWARD from now to the landing
|
|
308
|
-
* day's midnight; anything else parses as an instant.
|
|
309
|
-
*/
|
|
310
|
-
export function parsePeriodStart(s, now = new Date()) {
|
|
311
|
-
const rel = RELATIVE_PERIOD.exec(s.trim());
|
|
312
|
-
if (rel !== null) {
|
|
313
|
-
const d = relativePeriodDate(rel, now, -1);
|
|
314
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
315
|
-
}
|
|
316
|
-
const m = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(s.trim());
|
|
317
|
-
if (m === null)
|
|
318
|
-
return new Date(s);
|
|
319
|
-
const year = Number(m[1]);
|
|
320
|
-
if (m[2] === undefined)
|
|
321
|
-
return new Date(year, 0, 1);
|
|
322
|
-
const month = Number(m[2]) - 1;
|
|
323
|
-
if (m[3] === undefined)
|
|
324
|
-
return new Date(year, month, 1);
|
|
325
|
-
return new Date(year, month, Number(m[3]));
|
|
326
|
-
}
|
|
327
|
-
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
328
|
-
const SHORT_MONTHS = [
|
|
329
|
-
"Jan",
|
|
330
|
-
"Feb",
|
|
331
|
-
"Mar",
|
|
332
|
-
"Apr",
|
|
333
|
-
"May",
|
|
334
|
-
"Jun",
|
|
335
|
-
"Jul",
|
|
336
|
-
"Aug",
|
|
337
|
-
"Sep",
|
|
338
|
-
"Oct",
|
|
339
|
-
"Nov",
|
|
340
|
-
"Dec",
|
|
341
|
-
];
|
|
342
|
-
/**
|
|
343
|
-
* GUI-style Upcoming bucket for a date, granularity decaying with distance:
|
|
344
|
-
* individual days for the next week ("Wed Jul 15"), the remainder of the
|
|
345
|
-
* current month ("Jul 19–31"), months through the end of NEXT year
|
|
346
|
-
* ("August", "January 2027"), then bare years ("2028").
|
|
347
|
-
*/
|
|
348
|
-
function upcomingBucket(iso, todayIso) {
|
|
349
|
-
const [y, m, d] = iso.split("-").map(Number);
|
|
350
|
-
const [y0, m0, d0] = todayIso.split("-").map(Number);
|
|
351
|
-
const diff = Math.round((Date.UTC(y ?? 0, (m ?? 1) - 1, d ?? 1) - Date.UTC(y0 ?? 0, (m0 ?? 1) - 1, d0 ?? 1)) /
|
|
352
|
-
86_400_000);
|
|
353
|
-
if (diff <= 7) {
|
|
354
|
-
const weekday = WEEKDAYS[new Date(Date.UTC(y ?? 0, (m ?? 1) - 1, d ?? 1)).getUTCDay()];
|
|
355
|
-
return { label: `${weekday} ${SHORT_MONTHS[(m ?? 1) - 1]} ${d}`, isDay: true };
|
|
356
|
-
}
|
|
357
|
-
if (y === y0 && m === m0) {
|
|
358
|
-
const lastDay = new Date(y ?? 0, m ?? 1, 0).getDate();
|
|
359
|
-
return { label: `${SHORT_MONTHS[(m ?? 1) - 1]} ${(d0 ?? 1) + 8}–${lastDay}`, isDay: false };
|
|
360
|
-
}
|
|
361
|
-
if ((y ?? 0) <= (y0 ?? 0) + 1) {
|
|
362
|
-
const month = FULL_MONTHS[(m ?? 1) - 1];
|
|
363
|
-
return { label: y === y0 ? `${month}` : `${month} ${y}`, isDay: false };
|
|
364
|
-
}
|
|
365
|
-
return { label: `${y}`, isDay: false };
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Upcoming rows under GUI-style date headers (empty periods are simply
|
|
369
|
-
* absent), with the trailing Repeating To-Dos section: templates with no
|
|
370
|
-
* set next occurrence, carrying their waiting/paused/ended status word and
|
|
371
|
-
* the bare ⚑ when the rule will assign a deadline per occurrence.
|
|
372
|
-
*/
|
|
373
|
-
export function renderUpcoming(items, now) {
|
|
374
|
-
if (items.length === 0)
|
|
375
|
-
return ["(empty)"];
|
|
376
|
-
const todayIso = localToday(now);
|
|
377
|
-
const w = uuidDisplayWidth(items);
|
|
378
|
-
const fmtOpts = now === undefined ? {} : { now };
|
|
379
|
-
const dated = items.filter((i) => i.startDate !== null);
|
|
380
|
-
const resting = items.filter((i) => i.startDate === null && i.repeating.isTemplate);
|
|
381
|
-
const lines = [];
|
|
382
|
-
let openHeader = null;
|
|
383
|
-
for (const item of dated) {
|
|
384
|
-
const bucket = upcomingBucket(item.startDate ?? "", todayIso);
|
|
385
|
-
if (bucket.label !== openHeader) {
|
|
386
|
-
if (lines.length > 0)
|
|
387
|
-
lines.push("");
|
|
388
|
-
lines.push(bold(`── ${bucket.label} ──`));
|
|
389
|
-
openHeader = bucket.label;
|
|
53
|
+
else {
|
|
54
|
+
process.stdout.write(`${renderLegend().join("\n")}\n`);
|
|
390
55
|
}
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
56
|
+
process.exitCode = ExitCode.Ok;
|
|
57
|
+
});
|
|
58
|
+
program
|
|
59
|
+
.command("today")
|
|
60
|
+
.description("The Today list, split into Today and This Evening (evening expires daily), with the sidebar badge split (red = deadline due/overdue)")
|
|
61
|
+
.option("--tag <ref>", TAG_DESC, collectRef, [])
|
|
62
|
+
.option("--exact-tag", EXACT_TAG_DESC)
|
|
63
|
+
.option("--untagged", UNTAGGED_DESC)
|
|
64
|
+
.option("--overdue", OVERDUE_DESC)
|
|
65
|
+
.option("--evening", "show only the This Evening section")
|
|
66
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
67
|
+
.option("--all", ALL_DESC)
|
|
68
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
69
|
+
.option("--db <path>", "explicit database path")
|
|
70
|
+
.action((opts) => {
|
|
71
|
+
if (tagFlagConflict(opts))
|
|
72
|
+
return;
|
|
73
|
+
const lim = parseLimit(opts);
|
|
74
|
+
if (!lim.ok)
|
|
75
|
+
return;
|
|
76
|
+
const eveningOnly = opts.evening === true;
|
|
77
|
+
const base = invocation("today", [
|
|
78
|
+
...tagInvocationParts(opts),
|
|
79
|
+
opts.overdue === true && "--overdue",
|
|
80
|
+
eveningOnly && "--evening",
|
|
81
|
+
]);
|
|
82
|
+
const filter = {
|
|
83
|
+
...tagFilterFields(opts),
|
|
84
|
+
...(opts.overdue === true && { overdue: true }),
|
|
85
|
+
...(eveningOnly && { eveningOnly: true }),
|
|
86
|
+
};
|
|
87
|
+
runRead(opts, "today", (c) => {
|
|
88
|
+
const { view, truncation } = c.read.today({ ...filter, limit: lim.limit });
|
|
89
|
+
// The renderer keeps This Evening honest under truncation from the
|
|
90
|
+
// metadata's per-section counts; the lines are precomputed here and
|
|
91
|
+
// the global footer (whole-view remainder) is appended by the driver.
|
|
92
|
+
return {
|
|
93
|
+
data: view,
|
|
94
|
+
truncation,
|
|
95
|
+
lines: renderToday(view, truncation.sections, base, { eveningOnly }),
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
// Type-correct fallback for the TodayView payload; never reached
|
|
99
|
+
// because `lines` is always precomputed above.
|
|
100
|
+
(data) => renderToday(data, undefined, base, { eveningOnly }), base, "today");
|
|
101
|
+
});
|
|
102
|
+
program
|
|
103
|
+
.command("inbox")
|
|
104
|
+
.description("Unprocessed captures (Inbox). Bound the CREATION date with --since/--until — an " +
|
|
105
|
+
"item's arrival into Things (a demoted item keeps its original creation date, so " +
|
|
106
|
+
"this is not strictly when it entered the Inbox).")
|
|
107
|
+
.option("--tag <ref>", TAG_DESC, collectRef, [])
|
|
108
|
+
.option("--exact-tag", EXACT_TAG_DESC)
|
|
109
|
+
.option("--untagged", UNTAGGED_DESC)
|
|
110
|
+
.option("--overdue", OVERDUE_DESC)
|
|
111
|
+
.option("--since <when>", `only captures created on/after this bound: ${PERIOD_SINCE}`)
|
|
112
|
+
.option("--until <when>", `only captures created on/before this bound: ${PERIOD_UNTIL}`)
|
|
113
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
114
|
+
.option("--all", ALL_DESC)
|
|
115
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
116
|
+
.option("--db <path>", "explicit database path")
|
|
117
|
+
.action((opts) => {
|
|
118
|
+
if (tagFlagConflict(opts))
|
|
119
|
+
return;
|
|
120
|
+
const lim = parseLimit(opts);
|
|
121
|
+
if (!lim.ok)
|
|
122
|
+
return;
|
|
123
|
+
const since = opts.since !== undefined ? parsePeriodStart(opts.since) : undefined;
|
|
124
|
+
const until = opts.until !== undefined ? parsePeriodEnd(opts.until) : undefined;
|
|
125
|
+
for (const [flag, value] of [
|
|
126
|
+
["--since", since],
|
|
127
|
+
["--until", until],
|
|
128
|
+
]) {
|
|
129
|
+
if (value !== undefined && Number.isNaN(value.getTime())) {
|
|
130
|
+
usageError(opts, `${flag} is not a parseable date`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
399
133
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
134
|
+
// Bounds-&-defaults lift rule (identical to logbook): an explicit range
|
|
135
|
+
// bound (--since/--until) drops the default row cap unless --limit is
|
|
136
|
+
// also stated — the creation window the user named IS the bound, not an
|
|
137
|
+
// arbitrary 50-row cut.
|
|
138
|
+
const boundGiven = opts.since !== undefined || opts.until !== undefined;
|
|
139
|
+
const effectiveLimit = opts.limit === undefined && opts.all !== true && boundGiven ? null : lim.limit;
|
|
140
|
+
const base = invocation("inbox", [
|
|
141
|
+
...tagInvocationParts(opts),
|
|
142
|
+
opts.overdue === true && "--overdue",
|
|
143
|
+
opts.since !== undefined && `--since ${shellQuote(opts.since)}`,
|
|
144
|
+
opts.until !== undefined && `--until ${shellQuote(opts.until)}`,
|
|
145
|
+
]);
|
|
146
|
+
runRead(opts, "inbox", (c) => {
|
|
147
|
+
const { items: data, truncation } = c.read.inbox({
|
|
148
|
+
...tagFilterFields(opts),
|
|
149
|
+
...(opts.overdue === true && { overdue: true }),
|
|
150
|
+
...(since !== undefined && { since }),
|
|
151
|
+
...(until !== undefined && { until }),
|
|
152
|
+
limit: effectiveLimit,
|
|
153
|
+
});
|
|
154
|
+
const lines = renderList(data);
|
|
155
|
+
// Standard row-truncation hint (bigger --limit / --all), as every
|
|
156
|
+
// flat view — handled here (not via the driver's hintBase) so the
|
|
157
|
+
// creation-window note can sit last, mirroring upcoming's window
|
|
158
|
+
// footer mechanics.
|
|
159
|
+
const hint = truncationHint(base, truncation);
|
|
160
|
+
if (hint !== null)
|
|
161
|
+
lines.push("", hint);
|
|
162
|
+
// Presentation order is unchanged (manual ORDER BY index); the date
|
|
163
|
+
// bound is an invisible axis in the rows, so name the effective
|
|
164
|
+
// window in a dim footer note. Human output only — the precomputed
|
|
165
|
+
// lines never ride --json.
|
|
166
|
+
if (boundGiven) {
|
|
167
|
+
const today = localToday();
|
|
168
|
+
const sinceLabel = since !== undefined ? shortDate(localToday(since), today) : null;
|
|
169
|
+
const untilLabel = until !== undefined ? shortDate(localToday(until), today) : null;
|
|
170
|
+
const note = sinceLabel !== null && untilLabel !== null
|
|
171
|
+
? `(created ${sinceLabel} – ${untilLabel})`
|
|
172
|
+
: sinceLabel !== null
|
|
173
|
+
? `(created since ${sinceLabel})`
|
|
174
|
+
: `(created through ${untilLabel})`;
|
|
175
|
+
lines.push("", dim(note));
|
|
176
|
+
}
|
|
177
|
+
return { data, truncation, lines };
|
|
178
|
+
}, renderList, undefined, "inbox");
|
|
179
|
+
});
|
|
180
|
+
program
|
|
181
|
+
.command("anytime")
|
|
182
|
+
.description("All active items in canonical order (area-less first, then per " +
|
|
183
|
+
"area: direct to-dos, then each project with its members). Today members are " +
|
|
184
|
+
"starred (★). Children of someday/future-scheduled projects are excluded — the " +
|
|
185
|
+
"project row represents them. Every group and project row is always shown; " +
|
|
186
|
+
"--area-limit caps each area's direct list, --project-limit each project's list, " +
|
|
187
|
+
"--all shows everything")
|
|
188
|
+
.option("--tag <ref>", TAG_DESC, collectRef, [])
|
|
189
|
+
.option("--exact-tag", EXACT_TAG_DESC)
|
|
190
|
+
.option("--untagged", UNTAGGED_DESC)
|
|
191
|
+
.option("--overdue", OVERDUE_DESC)
|
|
192
|
+
.option("--area-limit <n>", AREA_LIMIT_DESC)
|
|
193
|
+
.option("--project-limit <n>", PROJECT_LIMIT_DESC)
|
|
194
|
+
.option("--all", GROUPED_ALL_DESC)
|
|
195
|
+
.addOption(new Option("--limit <n>").hideHelp())
|
|
196
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
197
|
+
.option("--db <path>", "explicit database path")
|
|
198
|
+
.action((opts) => {
|
|
199
|
+
if (tagFlagConflict(opts))
|
|
200
|
+
return;
|
|
201
|
+
if (opts.limit !== undefined) {
|
|
202
|
+
usageError(opts, "--limit is not available on anytime — cap blocks with --area-limit / --project-limit, or pass --all");
|
|
203
|
+
return;
|
|
426
204
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
205
|
+
const area = parseCap("--area-limit", opts.areaLimit, AREA_PREVIEW_LIMIT, opts.all === true, opts.json === true);
|
|
206
|
+
if (!area.ok)
|
|
207
|
+
return;
|
|
208
|
+
const project = parseCap("--project-limit", opts.projectLimit, PROJECT_PREVIEW_LIMIT, opts.all === true, opts.json === true);
|
|
209
|
+
if (!project.ok)
|
|
210
|
+
return;
|
|
211
|
+
const limits = { area: area.limit, project: project.limit };
|
|
212
|
+
const base = invocation("anytime", [
|
|
213
|
+
...tagInvocationParts(opts),
|
|
214
|
+
opts.overdue === true && "--overdue",
|
|
215
|
+
]);
|
|
216
|
+
// Content scopes compose (AND): --overdue narrows a tagged/untagged
|
|
217
|
+
// set, so the filter is built additively rather than one-or-the-other.
|
|
218
|
+
const filter = {
|
|
219
|
+
...tagFilterFields(opts),
|
|
220
|
+
...(opts.overdue === true && { overdue: true }),
|
|
221
|
+
};
|
|
222
|
+
runRead(opts, "anytime", (c) => {
|
|
223
|
+
const { view, grouped } = c.read.anytime({
|
|
224
|
+
...filter,
|
|
225
|
+
areaLimit: area.limit,
|
|
226
|
+
projectLimit: project.limit,
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
data: view,
|
|
230
|
+
grouped,
|
|
231
|
+
lines: renderAnytimePreview(view, grouped, limits, base),
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
// Grouped views hand back precomputed `lines`; renderSections is the
|
|
235
|
+
// type-correct fallback for the SidebarSection[] payload but is never
|
|
236
|
+
// reached here.
|
|
237
|
+
renderSections, undefined, "anytime");
|
|
238
|
+
});
|
|
239
|
+
program
|
|
240
|
+
.command("someday")
|
|
241
|
+
.description("Someday items (incubated, undated) in canonical order — inside each group the " +
|
|
242
|
+
"someday projects list first, then the to-dos; project children are represented " +
|
|
243
|
+
"by their project row. --show-active-project-items [n] appends a trailing section " +
|
|
244
|
+
"of someday to-dos inside active projects, grouped under their project (the UI's " +
|
|
245
|
+
"'Show items from active projects' toggle; n caps each project's list). " +
|
|
246
|
+
"--area-limit caps each group, --all shows everything")
|
|
247
|
+
.option("--tag <ref>", TAG_DESC, collectRef, [])
|
|
248
|
+
.option("--exact-tag", EXACT_TAG_DESC)
|
|
249
|
+
.option("--untagged", UNTAGGED_DESC)
|
|
250
|
+
.option("--overdue", OVERDUE_DESC)
|
|
251
|
+
.option("--area-limit <n>", AREA_LIMIT_DESC)
|
|
252
|
+
.option("--show-active-project-items [n]", "append someday to-dos inside active projects, grouped under their project; " +
|
|
253
|
+
"n caps each project's list (bare flag: every item)")
|
|
254
|
+
.option("--all", GROUPED_ALL_DESC)
|
|
255
|
+
.addOption(new Option("--limit <n>").hideHelp())
|
|
256
|
+
.option("--json", "emit versioned JSON envelope on stdout")
|
|
257
|
+
.option("--db <path>", "explicit database path")
|
|
258
|
+
.action((opts) => {
|
|
259
|
+
if (tagFlagConflict(opts))
|
|
260
|
+
return;
|
|
261
|
+
if (opts.limit !== undefined) {
|
|
262
|
+
process.stderr.write("error: --limit is not available on someday — cap groups with --area-limit, or pass --all\n");
|
|
263
|
+
process.exitCode = ExitCode.Usage;
|
|
264
|
+
return;
|
|
458
265
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
blank();
|
|
467
|
-
lines.push(formatItem(item, w, {
|
|
468
|
-
projectTitle: true,
|
|
469
|
-
suppressArea: section.area?.uuid ?? null,
|
|
470
|
-
mark,
|
|
471
|
-
}));
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
lines.push(formatItem(item, w, {
|
|
475
|
-
suppressProject: openProject,
|
|
476
|
-
suppressArea: section.area?.uuid ?? null,
|
|
477
|
-
mark,
|
|
478
|
-
}));
|
|
479
|
-
}
|
|
266
|
+
const showActive = opts.showActiveProjectItems !== undefined;
|
|
267
|
+
let projectCap = null;
|
|
268
|
+
if (typeof opts.showActiveProjectItems === "string") {
|
|
269
|
+
const capped = parseCap("--show-active-project-items", opts.showActiveProjectItems, 0, opts.all === true, opts.json === true);
|
|
270
|
+
if (!capped.ok)
|
|
271
|
+
return;
|
|
272
|
+
projectCap = capped.limit;
|
|
480
273
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
name: "someday",
|
|
522
|
-
description: "Someday items (incubated, undated) in sidebar order. Project children are " +
|
|
523
|
-
"represented by their project row; --active-project-items also lists someday " +
|
|
524
|
-
"to-dos inside active projects (the UI's 'Show items from active projects' toggle)",
|
|
525
|
-
fetch: (c, tag, exactTag, activeProjectItems) => c.read.someday({
|
|
526
|
-
...(tag !== undefined && { tag }),
|
|
527
|
-
...(exactTag === true && { exactTag }),
|
|
528
|
-
...(activeProjectItems === true && { activeProjectItems }),
|
|
529
|
-
}),
|
|
530
|
-
render: (sections) => renderSections(sections),
|
|
531
|
-
extraOption: [
|
|
532
|
-
"--active-project-items",
|
|
533
|
-
"also list someday to-dos inside active projects",
|
|
534
|
-
"activeProjectItems",
|
|
535
|
-
],
|
|
536
|
-
},
|
|
537
|
-
];
|
|
538
|
-
for (const cmd of listCommands) {
|
|
539
|
-
const command = program
|
|
540
|
-
.command(cmd.name)
|
|
541
|
-
.description(cmd.description)
|
|
542
|
-
.option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
|
|
543
|
-
.option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
|
|
544
|
-
.option("--json", "emit versioned JSON envelope on stdout")
|
|
545
|
-
.option("--db <path>", "explicit database path");
|
|
546
|
-
if (cmd.extraOption)
|
|
547
|
-
command.option(cmd.extraOption[0], cmd.extraOption[1]);
|
|
548
|
-
command.action((opts) => {
|
|
549
|
-
const extra = cmd.extraOption ? opts[cmd.extraOption[2]] === true : undefined;
|
|
550
|
-
withClient(opts, cmd.name, (c) => cmd.fetch(c, opts.tag, opts.exactTag, extra), (cmd.render ?? renderList));
|
|
551
|
-
});
|
|
552
|
-
}
|
|
274
|
+
const area = parseCap("--area-limit", opts.areaLimit, AREA_PREVIEW_LIMIT, opts.all === true, opts.json === true);
|
|
275
|
+
if (!area.ok)
|
|
276
|
+
return;
|
|
277
|
+
const limits = { area: area.limit, project: projectCap };
|
|
278
|
+
const filter = {
|
|
279
|
+
...tagFilterFields(opts),
|
|
280
|
+
...(opts.overdue === true && { overdue: true }),
|
|
281
|
+
};
|
|
282
|
+
const base = invocation("someday", [
|
|
283
|
+
...tagInvocationParts(opts),
|
|
284
|
+
opts.overdue === true && "--overdue",
|
|
285
|
+
]);
|
|
286
|
+
runRead(opts, "someday", (c) => {
|
|
287
|
+
const { view, grouped } = c.read.someday({
|
|
288
|
+
...filter,
|
|
289
|
+
...(showActive && { activeProjectItems: true }),
|
|
290
|
+
areaLimit: area.limit,
|
|
291
|
+
projectLimit: projectCap,
|
|
292
|
+
});
|
|
293
|
+
// Hidden-items-never-silent: when the toggle is off, one extra
|
|
294
|
+
// query counts what it would reveal (someday to-dos inside active
|
|
295
|
+
// projects) — summing the active-project child blocks' totals from
|
|
296
|
+
// that query's grouped metadata (projectLimit defaults to every
|
|
297
|
+
// item there, so each block's total is the full group size).
|
|
298
|
+
const hiddenActiveItems = showActive
|
|
299
|
+
? 0
|
|
300
|
+
: c.read
|
|
301
|
+
.someday({ ...filter, activeProjectItems: true })
|
|
302
|
+
.grouped.blocks.reduce((n, b) => n +
|
|
303
|
+
(b.children?.reduce((m, child) => m + (child.kind === "project" ? child.total : 0), 0) ?? 0), 0);
|
|
304
|
+
return {
|
|
305
|
+
data: view,
|
|
306
|
+
grouped,
|
|
307
|
+
lines: renderSomedayPreview(view, grouped, limits, base, showActive, hiddenActiveItems),
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
// Precomputed lines above; renderSections is the type-correct
|
|
311
|
+
// SidebarSection[] fallback, never reached here.
|
|
312
|
+
renderSections, undefined, "someday");
|
|
313
|
+
});
|
|
553
314
|
program
|
|
554
315
|
.command("upcoming")
|
|
555
316
|
.description("Future-scheduled items in date order, INCLUDING each repeating item's next " +
|
|
@@ -559,58 +320,99 @@ export function registerReadCommands(program) {
|
|
|
559
320
|
"PROJECTS the following n-1 occurrences per repeating item from its decoded rule " +
|
|
560
321
|
"(fixed rules only, max 10) — projections are host math the app has not " +
|
|
561
322
|
"materialized yet.")
|
|
562
|
-
.option("--until <period>",
|
|
563
|
-
"
|
|
564
|
-
.option("--all", "no date bound
|
|
565
|
-
.option("--limit <n>",
|
|
566
|
-
.option("--tag <ref>",
|
|
567
|
-
.option("--exact-tag",
|
|
323
|
+
.option("--until <period>", `only items scheduled through this bound: ${PERIOD_UNTIL} (whole periods)`, "1m")
|
|
324
|
+
.option("--since <period>", `skip items scheduled before this bound: ${PERIOD_SINCE}`)
|
|
325
|
+
.option("--all", "no date bound and no row limit — the app's full Upcoming")
|
|
326
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
327
|
+
.option("--tag <ref>", TAG_DESC, collectRef, [])
|
|
328
|
+
.option("--exact-tag", EXACT_TAG_DESC)
|
|
329
|
+
.option("--untagged", UNTAGGED_DESC)
|
|
568
330
|
.option("--horizon <n>", "occurrences per repeating item (default 1 = UI parity)")
|
|
569
331
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
570
332
|
.option("--db <path>", "explicit database path")
|
|
571
333
|
.action((opts, command) => {
|
|
334
|
+
if (tagFlagConflict(opts))
|
|
335
|
+
return;
|
|
572
336
|
const untilGiven = command.getOptionValueSource("until") !== "default";
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
337
|
+
const sinceGiven = opts.since !== undefined;
|
|
338
|
+
const limitGiven = opts.limit !== undefined;
|
|
339
|
+
if (opts.all === true && (untilGiven || sinceGiven)) {
|
|
340
|
+
usageError(opts, "--all does not combine with --until/--since");
|
|
576
341
|
return;
|
|
577
342
|
}
|
|
578
|
-
const
|
|
343
|
+
const lim = parseLimit(opts);
|
|
344
|
+
if (!lim.ok)
|
|
345
|
+
return;
|
|
346
|
+
// Bounds-&-defaults rule: an explicit volume cap (--limit) or range
|
|
347
|
+
// bound (--until/--since) disables the OTHER class's default. So an
|
|
348
|
+
// explicit --limit drops the default window (the next N scheduled
|
|
349
|
+
// items), and an explicit window drops the default row cap — each
|
|
350
|
+
// stated bound takes over output sizing.
|
|
351
|
+
const dropWindowDefault = !untilGiven && (limitGiven || sinceGiven);
|
|
352
|
+
const dropLimitDefault = !limitGiven && opts.all !== true && (untilGiven || sinceGiven);
|
|
353
|
+
const effectiveLimit = dropLimitDefault ? null : lim.limit;
|
|
354
|
+
const untilDate = opts.all === true || dropWindowDefault ? undefined : parsePeriodEnd(opts.until);
|
|
579
355
|
if (untilDate !== undefined && Number.isNaN(untilDate.getTime())) {
|
|
580
|
-
|
|
581
|
-
process.exitCode = ExitCode.Usage;
|
|
356
|
+
usageError(opts, `--until is not a parseable period: ${opts.until}`);
|
|
582
357
|
return;
|
|
583
358
|
}
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
process.stderr.write(`error: --limit must be a positive integer\n`);
|
|
588
|
-
process.exitCode = ExitCode.Usage;
|
|
359
|
+
const sinceDate = sinceGiven ? parsePeriodStart(opts.since) : undefined;
|
|
360
|
+
if (sinceDate !== undefined && Number.isNaN(sinceDate.getTime())) {
|
|
361
|
+
usageError(opts, `--since is not a parseable period: ${opts.since}`);
|
|
589
362
|
return;
|
|
590
363
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
364
|
+
const until = untilDate === undefined ? undefined : localToday(untilDate);
|
|
365
|
+
const since = sinceDate === undefined ? undefined : localToday(sinceDate);
|
|
366
|
+
// The default window is in force only for a bare invocation (no
|
|
367
|
+
// explicit cap or bound); that is the one case whose footer names the
|
|
368
|
+
// window itself alongside the levers.
|
|
369
|
+
const defaultWindowActive = until !== undefined && !untilGiven;
|
|
370
|
+
const base = invocation("upcoming", [
|
|
371
|
+
untilGiven && `--until ${shellQuote(opts.until)}`,
|
|
372
|
+
sinceGiven && `--since ${shellQuote(opts.since)}`,
|
|
373
|
+
...tagInvocationParts(opts),
|
|
374
|
+
opts.horizon !== undefined && `--horizon ${shellQuote(opts.horizon)}`,
|
|
375
|
+
]);
|
|
376
|
+
runRead(opts, "upcoming", (c) => {
|
|
377
|
+
const { items: data, truncation } = c.read.upcoming({
|
|
594
378
|
...(until !== undefined && { until }),
|
|
595
|
-
...(
|
|
596
|
-
...(opts
|
|
379
|
+
...(since !== undefined && { since }),
|
|
380
|
+
...tagFilterFields(opts),
|
|
597
381
|
...(opts.horizon !== undefined && { horizon: Number(opts.horizon) }),
|
|
382
|
+
limit: effectiveLimit,
|
|
598
383
|
});
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
384
|
+
const lines = renderUpcoming(data);
|
|
385
|
+
if (defaultWindowActive && until !== undefined) {
|
|
386
|
+
const windowLabel = shortDate(until, localToday());
|
|
387
|
+
if (truncation.truncated && truncation.limit !== null) {
|
|
388
|
+
// Bare invocation, row cap biting inside the default window: one
|
|
389
|
+
// line names BOTH the window and the two levers, so neither the
|
|
390
|
+
// limit nor the horizon is a hidden second bound.
|
|
391
|
+
const more = truncation.total - truncation.shown;
|
|
392
|
+
lines.push("", dim(`── ${more} more item${more === 1 ? "" : "s"} through ${windowLabel} — ` +
|
|
393
|
+
`see more: \`${base} --limit ${truncation.limit * 2}\` · ` +
|
|
394
|
+
`\`${base} --all\` ──`));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Default window active, row cap NOT biting: the only useful
|
|
398
|
+
// lever is a wider window (or everything).
|
|
399
|
+
lines.push("", dim(`(through ${windowLabel} — wider: \`${base} --until ${doublePeriod(opts.until)}\`` +
|
|
400
|
+
` · \`${base} --all\`)`));
|
|
401
|
+
}
|
|
608
402
|
}
|
|
609
|
-
if (
|
|
610
|
-
|
|
403
|
+
else if (truncation.truncated && truncation.limit !== null) {
|
|
404
|
+
// The user stated a bound: no window line, only a row hint, and
|
|
405
|
+
// only when an explicit --limit truncated. A stated --until/--since
|
|
406
|
+
// makes --all a usage error (and would discard the very window
|
|
407
|
+
// they asked for), so a bounded run offers just a bigger cap.
|
|
408
|
+
const more = truncation.total - truncation.shown;
|
|
409
|
+
const bounded = untilGiven || sinceGiven;
|
|
410
|
+
const allLever = bounded ? "" : ` · \`${base} --all\``;
|
|
411
|
+
lines.push("", dim(`── ${more} more item${more === 1 ? "" : "s"} — ` +
|
|
412
|
+
`see more: \`${base} --limit ${truncation.limit * 2}\`${allLever} ──`));
|
|
611
413
|
}
|
|
612
|
-
return lines;
|
|
613
|
-
}));
|
|
414
|
+
return { data, truncation, lines };
|
|
415
|
+
}, (items) => renderUpcoming(items), undefined, "upcoming");
|
|
614
416
|
});
|
|
615
417
|
program
|
|
616
418
|
.command("logbook")
|
|
@@ -618,17 +420,23 @@ export function registerReadCommands(program) {
|
|
|
618
420
|
"(year appended beyond the current year). Scope with --area (direct items + its " +
|
|
619
421
|
"projects' children, heading-nested included) / --project (all children, " +
|
|
620
422
|
"heading-nested included) / --tag; bound the logged date with --since/--until.")
|
|
621
|
-
.option("--limit <n>",
|
|
423
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
424
|
+
.option("--all", ALL_DESC)
|
|
622
425
|
.option("--area <ref>", "restrict to an area: direct items plus its projects' children")
|
|
623
426
|
.option("--project <ref>", "restrict to one project's children (uuid or unique name)")
|
|
624
|
-
.option("--since <when>",
|
|
625
|
-
"
|
|
626
|
-
.option("--
|
|
627
|
-
.option("--tag
|
|
628
|
-
.option("--
|
|
427
|
+
.option("--since <when>", `only entries logged on/after this bound: ${PERIOD_SINCE}`)
|
|
428
|
+
.option("--until <when>", `only entries logged on/before this bound: ${PERIOD_UNTIL}`)
|
|
429
|
+
.option("--tag <ref>", TAG_DESC, collectRef, [])
|
|
430
|
+
.option("--exact-tag", EXACT_TAG_DESC)
|
|
431
|
+
.option("--untagged", UNTAGGED_DESC)
|
|
629
432
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
630
433
|
.option("--db <path>", "explicit database path")
|
|
631
434
|
.action((opts) => {
|
|
435
|
+
if (tagFlagConflict(opts))
|
|
436
|
+
return;
|
|
437
|
+
const lim = parseLimit(opts);
|
|
438
|
+
if (!lim.ok)
|
|
439
|
+
return;
|
|
632
440
|
const since = opts.since !== undefined ? parsePeriodStart(opts.since) : undefined;
|
|
633
441
|
const until = opts.until !== undefined ? parsePeriodEnd(opts.until) : undefined;
|
|
634
442
|
for (const [flag, value] of [
|
|
@@ -636,69 +444,107 @@ export function registerReadCommands(program) {
|
|
|
636
444
|
["--until", until],
|
|
637
445
|
]) {
|
|
638
446
|
if (value !== undefined && Number.isNaN(value.getTime())) {
|
|
639
|
-
|
|
640
|
-
process.exitCode = ExitCode.Usage;
|
|
447
|
+
usageError(opts, `${flag} is not a parseable date`);
|
|
641
448
|
return;
|
|
642
449
|
}
|
|
643
450
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
451
|
+
// Bounds-&-defaults rule: an explicit range bound (--since/--until)
|
|
452
|
+
// drops the default row cap unless --limit is also stated — the logged
|
|
453
|
+
// window the user named is the bound, not an arbitrary 50-row cut.
|
|
454
|
+
const boundGiven = opts.since !== undefined || opts.until !== undefined;
|
|
455
|
+
const effectiveLimit = opts.limit === undefined && opts.all !== true && boundGiven ? null : lim.limit;
|
|
456
|
+
const base = invocation("logbook", [
|
|
457
|
+
opts.area !== undefined && `--area ${shellQuote(opts.area)}`,
|
|
458
|
+
opts.project !== undefined && `--project ${shellQuote(opts.project)}`,
|
|
459
|
+
opts.since !== undefined && `--since ${shellQuote(opts.since)}`,
|
|
460
|
+
opts.until !== undefined && `--until ${shellQuote(opts.until)}`,
|
|
461
|
+
...tagInvocationParts(opts),
|
|
462
|
+
]);
|
|
463
|
+
runRead(opts, "logbook", (c) => {
|
|
464
|
+
const { items, truncation } = c.read.logbook({
|
|
465
|
+
...(opts.area !== undefined && { area: opts.area }),
|
|
466
|
+
...(opts.project !== undefined && { project: opts.project }),
|
|
467
|
+
...(since !== undefined && { since }),
|
|
468
|
+
...(until !== undefined && { until }),
|
|
469
|
+
...tagFilterFields(opts),
|
|
470
|
+
limit: effectiveLimit,
|
|
471
|
+
});
|
|
472
|
+
return { data: items, truncation };
|
|
473
|
+
}, (items) => renderLogbook(items), base, "logbook");
|
|
654
474
|
});
|
|
655
475
|
program
|
|
656
476
|
.command("trash")
|
|
657
477
|
.description("Trashed items (trashed=1 flag, any status), most recently modified first")
|
|
658
|
-
.option("--limit <n>",
|
|
478
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
479
|
+
.option("--all", ALL_DESC)
|
|
659
480
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
660
481
|
.option("--db <path>", "explicit database path")
|
|
661
482
|
.action((opts) => {
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const items = c.read.trash({ limit: limit
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
return items.slice(0, limit);
|
|
670
|
-
}
|
|
671
|
-
return items;
|
|
672
|
-
}, ((items) => {
|
|
673
|
-
const lines = renderList(items);
|
|
674
|
-
if (clipped)
|
|
675
|
-
lines.push("", dim(`(${limit} shown — --limit reached; raise --limit)`));
|
|
676
|
-
return lines;
|
|
677
|
-
}));
|
|
483
|
+
const lim = parseLimit(opts);
|
|
484
|
+
if (!lim.ok)
|
|
485
|
+
return;
|
|
486
|
+
runRead(opts, "trash", (c) => {
|
|
487
|
+
const { items, truncation } = c.read.trash({ limit: lim.limit });
|
|
488
|
+
return { data: items, truncation };
|
|
489
|
+
}, renderList, invocation("trash", []), "trash");
|
|
678
490
|
});
|
|
679
|
-
program
|
|
680
|
-
.command("projects")
|
|
681
|
-
.description("
|
|
682
|
-
"
|
|
683
|
-
"
|
|
684
|
-
"
|
|
491
|
+
const projects = program
|
|
492
|
+
.command("projects [ref]")
|
|
493
|
+
.description("List active projects in canonical order, or — given a ref — show that one " +
|
|
494
|
+
"project (exactly like `things project show <ref>`). List: loose projects " +
|
|
495
|
+
"first, then grouped under their area (optionally scoped to --area <ref>). " +
|
|
496
|
+
"Someday and future-scheduled projects are hidden — --show-later appends them " +
|
|
497
|
+
"after each group's active block (state carried by the (~) mark and ‹date› " +
|
|
498
|
+
"chip). Filter the listed projects by their own tags with --tag / --untagged — " +
|
|
499
|
+
"inheritance-inclusive (a project inherits its area's tags), the same as the flat " +
|
|
500
|
+
"views. --show-logged applies only when showing one project.")
|
|
685
501
|
.option("--area <ref>", "filter by area (uuid or unique name)")
|
|
686
502
|
.option("--show-later", "include someday/future-scheduled projects after each active block")
|
|
503
|
+
.option("--show-logged [n]", "showing one project: include logged items (bare = all)")
|
|
504
|
+
.option("--overdue", "only projects past their deadline (due today is not overdue)")
|
|
505
|
+
.option("--all", "include someday/future-scheduled projects (same as --show-later)")
|
|
687
506
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
688
|
-
.option("--db <path>", "explicit database path")
|
|
689
|
-
|
|
507
|
+
.option("--db <path>", "explicit database path");
|
|
508
|
+
addTagFilterOptions(projects).action((id, opts) => {
|
|
509
|
+
// Given a ref, this IS `things project show <ref>` — delegate to the same
|
|
510
|
+
// code path so the output is identical (a true synonym). --overdue rides
|
|
511
|
+
// along (it filters the shown project's children there).
|
|
512
|
+
if (id !== undefined) {
|
|
513
|
+
runProjectShow(id, opts);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (tagFlagConflict(opts))
|
|
517
|
+
return;
|
|
518
|
+
// --all lifts the sole default restriction here — the hidden later block
|
|
519
|
+
// — so it is exactly --show-later (the charter: --all removes every
|
|
520
|
+
// default restriction on the view's own content).
|
|
521
|
+
const showLater = opts.showLater === true || opts.all === true;
|
|
522
|
+
// OWN-DEADLINE UNIFORM content scope: keep only projects whose own
|
|
523
|
+
// deadline is overdue. Composes as AND with --area/--show-later; a
|
|
524
|
+
// content scope, it never lifts a limit. Threaded into the hidden-later
|
|
525
|
+
// counting queries too so the counts reflect the same filter.
|
|
526
|
+
const overdue = opts.overdue === true;
|
|
527
|
+
// Tag scope (§9a): the projects LIST is a FLAT view — each project ROW is
|
|
528
|
+
// filtered by its own tags, INHERITANCE-INCLUSIVE (a project inherits its
|
|
529
|
+
// area's tags), the same as `anytime`. NOT a single-container view.
|
|
530
|
+
// Threaded into the hidden-later counts too so they reflect the same filter.
|
|
531
|
+
const tagFilter = tagFilterFields(opts);
|
|
690
532
|
let hints;
|
|
691
533
|
withClient(opts, "projects", (c) => {
|
|
692
534
|
const scope = opts.area !== undefined ? { areaUuid: opts.area } : {};
|
|
693
535
|
const visible = c.read.projects({
|
|
694
536
|
...scope,
|
|
695
|
-
...(
|
|
537
|
+
...(showLater && { later: true }),
|
|
538
|
+
...(overdue && { overdue: true }),
|
|
539
|
+
...tagFilter,
|
|
696
540
|
});
|
|
697
541
|
if (opts.area === undefined) {
|
|
698
542
|
// Sidebar scaffold: every VISIBLE area renders (project-less
|
|
699
543
|
// ones say so), in sidebar order; the loose block leads. One
|
|
700
544
|
// extra projects query buys the hidden-later counts.
|
|
701
|
-
const full =
|
|
545
|
+
const full = showLater
|
|
546
|
+
? visible
|
|
547
|
+
: c.read.projects({ later: true, ...(overdue && { overdue: true }), ...tagFilter });
|
|
702
548
|
const shown = new Set(visible.map((i) => i.uuid));
|
|
703
549
|
const groups = [
|
|
704
550
|
{ area: null, hidden: 0 },
|
|
@@ -717,43 +563,114 @@ export function registerReadCommands(program) {
|
|
|
717
563
|
}
|
|
718
564
|
hints = { groups };
|
|
719
565
|
}
|
|
720
|
-
else if (
|
|
566
|
+
else if (!showLater) {
|
|
721
567
|
// --area scoped: only the bottom hint needs a count.
|
|
722
|
-
const full = c.read.projects({
|
|
568
|
+
const full = c.read.projects({
|
|
569
|
+
...scope,
|
|
570
|
+
later: true,
|
|
571
|
+
...(overdue && { overdue: true }),
|
|
572
|
+
...tagFilter,
|
|
573
|
+
});
|
|
723
574
|
hints = { groups: [{ area: null, hidden: full.length - visible.length }] };
|
|
724
575
|
}
|
|
725
576
|
return visible;
|
|
726
577
|
},
|
|
727
578
|
// Scoped to one area the list is flat (the scope names the group);
|
|
728
579
|
// unscoped it mirrors the sidebar with ⬡ area headers.
|
|
729
|
-
(
|
|
580
|
+
(items) => {
|
|
730
581
|
if (opts.area === undefined)
|
|
731
582
|
return renderProjectsSidebar(items, hints);
|
|
732
583
|
const lines = renderList(items);
|
|
733
584
|
const hidden = hints?.groups.reduce((n, g) => n + g.hidden, 0) ?? 0;
|
|
734
|
-
if (hidden > 0)
|
|
735
|
-
|
|
585
|
+
if (hidden > 0) {
|
|
586
|
+
// Hidden-section placeholder: the reveal command echoes the user's
|
|
587
|
+
// own scope (--area) plus the flag that surfaces the later block.
|
|
588
|
+
const reveal = invocation("projects", [
|
|
589
|
+
opts.area !== undefined && `--area ${shellQuote(opts.area)}`,
|
|
590
|
+
overdue && "--overdue",
|
|
591
|
+
...tagInvocationParts(opts),
|
|
592
|
+
"--show-later",
|
|
593
|
+
]);
|
|
594
|
+
lines.push("", disclosureHint(hidden, "later project", [{ command: reveal }]));
|
|
595
|
+
}
|
|
736
596
|
return lines;
|
|
737
|
-
})
|
|
597
|
+
});
|
|
738
598
|
});
|
|
739
|
-
program
|
|
740
|
-
.command("areas")
|
|
741
|
-
.description("
|
|
599
|
+
const areas = program
|
|
600
|
+
.command("areas [ref]")
|
|
601
|
+
.description("List all areas with their direct tags, or — given a ref — show that one area " +
|
|
602
|
+
"(exactly like `things area show <ref>`: its projects and direct to-dos). " +
|
|
603
|
+
"--show-later / --show-logged / --area-limit / --project-limit / the tag filters " +
|
|
604
|
+
"apply only when showing one area.")
|
|
605
|
+
.option("--all", "show every area (no default restriction applies)")
|
|
606
|
+
.option("--show-later", "showing one area: include its Upcoming and Someday sections")
|
|
607
|
+
.option("--show-logged [n]", "showing one area: include the n most recent logged items")
|
|
608
|
+
.option("--project-limit <n>", "showing one area: maximum project rows to show")
|
|
609
|
+
.option("--area-limit <n>", "showing one area: maximum direct to-dos to show")
|
|
610
|
+
.option("--overdue", "showing one area: only rows whose own deadline is past (due today is not overdue)")
|
|
742
611
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
743
|
-
.option("--db <path>", "explicit database path")
|
|
744
|
-
|
|
745
|
-
|
|
612
|
+
.option("--db <path>", "explicit database path");
|
|
613
|
+
addTagFilterOptions(areas).addHelpText("after", CONTAINER_TAG_HINT);
|
|
614
|
+
areas.action((id, opts) => {
|
|
615
|
+
// Given a ref, this IS `things area show <ref>` — delegate to the same code
|
|
616
|
+
// path so the output is identical (a true synonym); --overdue and the tag
|
|
617
|
+
// filters scope its rows there.
|
|
618
|
+
if (id !== undefined) {
|
|
619
|
+
runAreaShow(id, opts);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// The areas LIST has no deadline to compare against — an area is not a
|
|
623
|
+
// dated entity — so --overdue is vacuous here and rejected fail-closed
|
|
624
|
+
// (the same exclusion style #159 used for upcoming/logbook/trash). The
|
|
625
|
+
// decision is the contract's (areas: overdue = false).
|
|
626
|
+
if (!FILTER_CONTRACT.areas.overdue && opts.overdue === true) {
|
|
627
|
+
usageError(opts, "--overdue does not apply to the areas list — areas have no deadline; use it on `things areas <ref>` (that area's rows) or `things projects --overdue`");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
// The tag filters scope an area's ROWS, not the area LIST — the bare list
|
|
631
|
+
// shows every area with its direct tags. Rejected fail-closed here (same
|
|
632
|
+
// style as --overdue; contract: areas tag = "rejected"). Use
|
|
633
|
+
// `things areas <ref>` or `things projects --tag`.
|
|
634
|
+
if (FILTER_CONTRACT.areas.tag === "rejected" &&
|
|
635
|
+
(hasTagPresence(opts) || opts.untagged === true)) {
|
|
636
|
+
usageError(opts, "the tag filters (--tag/--untagged) do not apply to the areas list — use them on `things areas <ref>` (that area's rows) or `things projects --tag`");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
withClient(opts, "areas", (c) => c.read.areas(), (data) => {
|
|
746
640
|
const w = uuidDisplayWidth(data);
|
|
747
|
-
return data.map((a) => `${dim(a.uuid
|
|
748
|
-
})
|
|
641
|
+
return data.map((a) => `${dim(uuidCol(a.uuid, w))} ${areaMark()} ${a.title}${a.tags.length ? ` ${dim(`#${a.tags.map((t) => t.title).join(" #")}`)}` : ""}`);
|
|
642
|
+
});
|
|
749
643
|
});
|
|
750
644
|
program
|
|
751
645
|
.command("tags")
|
|
752
|
-
.description("Tag taxonomy
|
|
646
|
+
.description("Tag taxonomy — the parent → child hierarchy as an indented tree")
|
|
647
|
+
.option("--all", "show every tag (no default restriction applies)")
|
|
753
648
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
754
649
|
.option("--db <path>", "explicit database path")
|
|
755
650
|
.action((opts) => {
|
|
756
|
-
withClient(opts, "tags", (c) => c.read.tags(),
|
|
651
|
+
withClient(opts, "tags", (c) => c.read.tags(),
|
|
652
|
+
// An INDENTED TREE, leaf names only — nesting is conveyed by indentation
|
|
653
|
+
// (2 spaces per depth level), mirroring the Things GUI. Tag names are
|
|
654
|
+
// globally unique (TAGW1-c), so each displayed leaf name is directly
|
|
655
|
+
// usable as a tag ref — no path prefix or uuid needed. Rows arrive in
|
|
656
|
+
// canonical DFS order (index, uuid) from tagsView, children following
|
|
657
|
+
// their parent; the tree IS structure, so leaf names render plainly (not
|
|
658
|
+
// dim). Depth is the length of the parent-name chain (names are unique).
|
|
659
|
+
(data) => {
|
|
660
|
+
const parentOf = new Map(data.map((t) => [t.title, t.parent]));
|
|
661
|
+
const depthOf = (title) => {
|
|
662
|
+
let depth = 0;
|
|
663
|
+
const seen = new Set();
|
|
664
|
+
let cur = parentOf.get(title) ?? null;
|
|
665
|
+
while (cur !== null && !seen.has(cur)) {
|
|
666
|
+
seen.add(cur);
|
|
667
|
+
depth++;
|
|
668
|
+
cur = parentOf.get(cur) ?? null;
|
|
669
|
+
}
|
|
670
|
+
return depth;
|
|
671
|
+
};
|
|
672
|
+
return data.map((t) => `${" ".repeat(depthOf(t.title))}${t.title}`);
|
|
673
|
+
});
|
|
757
674
|
});
|
|
758
675
|
program
|
|
759
676
|
.command("changes")
|
|
@@ -761,55 +678,130 @@ export function registerReadCommands(program) {
|
|
|
761
678
|
"trashed, logged, and repeating-template rows so agents can sync state; check " +
|
|
762
679
|
"trashed/status/repeating on each item. Caveats: tag/area edits and checklist-item " +
|
|
763
680
|
"edits don't bump tasks and are invisible here.")
|
|
764
|
-
.requiredOption("--since <when>",
|
|
765
|
-
.option("--limit <n>",
|
|
681
|
+
.requiredOption("--since <when>", `ISO date/datetime (e.g. 2026-07-05T14:30:00), or ${PERIOD_SINCE}`)
|
|
682
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
683
|
+
.option("--all", ALL_DESC)
|
|
766
684
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
767
685
|
.option("--db <path>", "explicit database path")
|
|
768
686
|
.action((opts) => {
|
|
769
|
-
const
|
|
687
|
+
const lim = parseLimit(opts);
|
|
688
|
+
if (!lim.ok)
|
|
689
|
+
return;
|
|
690
|
+
const since = parsePeriodStart(opts.since);
|
|
770
691
|
if (Number.isNaN(since.getTime())) {
|
|
771
|
-
|
|
772
|
-
process.exitCode = ExitCode.Usage;
|
|
692
|
+
usageError(opts, `--since is not a parseable date: ${opts.since}`);
|
|
773
693
|
return;
|
|
774
694
|
}
|
|
775
|
-
|
|
695
|
+
const base = invocation("changes", [`--since ${shellQuote(opts.since)}`]);
|
|
696
|
+
runRead(opts, "changes", (c) => {
|
|
697
|
+
const { items, truncation } = c.read.changes({ since, limit: lim.limit });
|
|
698
|
+
return { data: items, truncation };
|
|
699
|
+
}, (items) => items.length === 0
|
|
776
700
|
? ["(no changes)"]
|
|
777
|
-
: items.map((i) => `${i.changeKind === "created" ? "+" : "~"} ${formatItem(i)}${i.trashed ? " [trashed]" : ""}`))
|
|
701
|
+
: items.map((i) => `${i.changeKind === "created" ? "+" : "~"} ${formatItem(i)}${i.trashed ? " [trashed]" : ""}`), base);
|
|
778
702
|
});
|
|
779
703
|
program
|
|
780
704
|
.command("search <query>")
|
|
781
|
-
.description("Title/notes substring search,
|
|
782
|
-
"
|
|
783
|
-
"
|
|
705
|
+
.description("Title/notes substring search, ranked: title matches first, then notes, then a " +
|
|
706
|
+
"project surfaced by a matching heading title (shown as its parent project, " +
|
|
707
|
+
"`via heading`); projects rank above to-dos, active above someday, ties broken by " +
|
|
708
|
+
"most-recently-modified. Default scope: OPEN + untrashed items only — widen with " +
|
|
709
|
+
"--logged / --trashed / --all. Scope with --project / --area / --tag (tag matches " +
|
|
710
|
+
"include hierarchy descendants) / --type / --overdue (open items past their deadline).")
|
|
784
711
|
.option("--project <ref>", "restrict to one project's children (uuid or unique name)")
|
|
785
712
|
.option("--area <ref>", "restrict to one area's direct members (uuid or unique name)")
|
|
786
|
-
.option("--tag <ref>",
|
|
787
|
-
.option("--exact-tag",
|
|
713
|
+
.option("--tag <ref>", TAG_DESC, collectRef, [])
|
|
714
|
+
.option("--exact-tag", EXACT_TAG_DESC)
|
|
715
|
+
.option("--untagged", UNTAGGED_DESC)
|
|
716
|
+
.option("--overdue", OVERDUE_DESC)
|
|
788
717
|
.option("--type <kind>", "todo | project")
|
|
789
718
|
.option("--logged", "include completed/canceled items")
|
|
790
719
|
.option("--trashed", "include trashed items")
|
|
791
|
-
.option("--all", "
|
|
792
|
-
.option("--limit <n>",
|
|
720
|
+
.option("--all", "everything, unbounded: open + logged + trashed, with no row limit (excludes --limit)")
|
|
721
|
+
.option("--limit <n>", LIMIT_DESC)
|
|
793
722
|
.option("--json", "emit versioned JSON envelope on stdout")
|
|
794
723
|
.option("--db <path>", "explicit database path")
|
|
795
724
|
.action((query, opts) => {
|
|
725
|
+
const json = opts["json"] === true;
|
|
796
726
|
const type = opts["type"];
|
|
797
727
|
if (type !== undefined && type !== "todo" && type !== "project") {
|
|
798
|
-
|
|
799
|
-
process.exitCode = 2;
|
|
728
|
+
usageError({ json }, "--type must be todo or project");
|
|
800
729
|
return;
|
|
801
730
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
731
|
+
const tagFlags = {
|
|
732
|
+
...(Array.isArray(opts["tag"]) && { tag: opts["tag"] }),
|
|
733
|
+
exactTag: opts["exactTag"] === true,
|
|
734
|
+
untagged: opts["untagged"] === true,
|
|
735
|
+
};
|
|
736
|
+
const overdue = opts["overdue"] === true;
|
|
737
|
+
const all = opts["all"] === true;
|
|
738
|
+
// Tag-conflict AND the --overdue/status-widening incompatibility both
|
|
739
|
+
// derive from the shared contract (search: statusWidening = true) —
|
|
740
|
+
// --overdue lists OPEN, past-deadline items, so combining it with the
|
|
741
|
+
// completed/canceled/trashed-widening flags is contradictory.
|
|
742
|
+
const validated = validateViewArgs("search", {
|
|
743
|
+
...tagFlags,
|
|
744
|
+
overdue,
|
|
745
|
+
logged: opts["logged"] === true,
|
|
746
|
+
trashed: opts["trashed"] === true,
|
|
747
|
+
all,
|
|
748
|
+
}, {
|
|
749
|
+
untaggedConflict: "--untagged does not combine with --tag/--exact-tag",
|
|
750
|
+
overdueRejected: "--overdue does not apply to search",
|
|
751
|
+
overdueStatusWiden: "--overdue does not combine with --logged/--trashed/--all",
|
|
752
|
+
});
|
|
753
|
+
if (!validated.ok) {
|
|
754
|
+
usageError({ json }, validated.message);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const limitOpt = opts["limit"];
|
|
758
|
+
// --all widens the scope AND lifts the row limit — combining it with an
|
|
759
|
+
// explicit --limit is contradictory (like every other view).
|
|
760
|
+
const lim = parseLimit({ all, json, ...(limitOpt !== undefined && { limit: limitOpt }) });
|
|
761
|
+
if (!lim.ok)
|
|
762
|
+
return;
|
|
763
|
+
const base = invocation("search", [
|
|
764
|
+
shellQuote(query),
|
|
765
|
+
opts["project"] !== undefined && `--project ${shellQuote(opts["project"])}`,
|
|
766
|
+
opts["area"] !== undefined && `--area ${shellQuote(opts["area"])}`,
|
|
767
|
+
...tagInvocationParts(tagFlags),
|
|
768
|
+
overdue && "--overdue",
|
|
769
|
+
type !== undefined && `--type ${type}`,
|
|
770
|
+
opts["logged"] === true && "--logged",
|
|
771
|
+
opts["trashed"] === true && "--trashed",
|
|
772
|
+
]);
|
|
773
|
+
runRead(opts, "search", (c) => {
|
|
774
|
+
const { items, truncation } = c.read.search(query, {
|
|
775
|
+
limit: lim.limit,
|
|
776
|
+
...(opts["project"] !== undefined && { project: opts["project"] }),
|
|
777
|
+
...(opts["area"] !== undefined && { area: opts["area"] }),
|
|
778
|
+
...validated.filter,
|
|
779
|
+
...(type !== undefined && { type: type === "todo" ? "to-do" : "project" }),
|
|
780
|
+
...(opts["logged"] === true && { logged: true }),
|
|
781
|
+
...(opts["trashed"] === true && { trashed: true }),
|
|
782
|
+
...(all && { all: true }),
|
|
783
|
+
});
|
|
784
|
+
return { data: items, truncation };
|
|
785
|
+
}, renderSearch, base);
|
|
813
786
|
});
|
|
787
|
+
// Every row-rendering view points at `things legend` for its glyph language.
|
|
788
|
+
const GLYPH_VIEWS = new Set([
|
|
789
|
+
"today",
|
|
790
|
+
"inbox",
|
|
791
|
+
"anytime",
|
|
792
|
+
"someday",
|
|
793
|
+
"upcoming",
|
|
794
|
+
"logbook",
|
|
795
|
+
"trash",
|
|
796
|
+
"projects",
|
|
797
|
+
"areas",
|
|
798
|
+
"changes",
|
|
799
|
+
"search",
|
|
800
|
+
]);
|
|
801
|
+
for (const c of program.commands) {
|
|
802
|
+
if (GLYPH_VIEWS.has(c.name())) {
|
|
803
|
+
c.addHelpText("after", "\nsymbols & colors: run `things legend`");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
814
806
|
}
|
|
815
807
|
//# sourceMappingURL=reads.js.map
|