things-api 0.9.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 +30 -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 +26 -3
- package/dist/cli/commands/area.js +125 -70
- 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 +18 -1
- package/dist/cli/commands/project.js +69 -31
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +423 -157
- 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.js +47 -24
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.d.ts +1 -1
- package/dist/cli/commands/todo.js +7 -6
- 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 +1 -1
- 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 +39 -2
- package/dist/cli/glyphs.js +91 -21
- 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 +32 -25
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +7 -0
- package/dist/cli/period.js +12 -0
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.d.ts +26 -19
- package/dist/cli/read-driver.js +67 -33
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +92 -28
- package/dist/cli/render.js +318 -97
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +42 -4
- package/dist/cli/resolve-invocation.js +97 -14
- package/dist/cli/resolve-invocation.js.map +1 -1
- 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 +137 -24
- package/dist/client.js +105 -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 +87 -17
- 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 +691 -133
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +31 -7
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +7 -0
- package/dist/model/mappers.js +7 -2
- package/dist/model/mappers.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/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/predicates.d.ts +30 -0
- package/dist/read/predicates.js +30 -0
- package/dist/read/predicates.js.map +1 -1
- 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 +149 -6
- package/dist/read/queries.js +250 -31
- package/dist/read/queries.js.map +1 -1
- 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/sidebar-order.js +2 -1
- package/dist/read/sidebar-order.js.map +1 -1
- 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/{pagination.js → truncation.js} +103 -94
- package/dist/read/truncation.js.map +1 -0
- package/dist/read/views.d.ts +140 -16
- package/dist/read/views.js +264 -67
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +19 -0
- package/dist/surface-copy.js +32 -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 -6
- 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 +493 -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
- package/dist/read/pagination.d.ts +0 -104
- package/dist/read/pagination.js.map +0 -1
package/dist/cli/render.js
CHANGED
|
@@ -7,13 +7,11 @@
|
|
|
7
7
|
* (agents and humans both need stable references); colors engage on a TTY only
|
|
8
8
|
* (../cli/style.ts).
|
|
9
9
|
*/
|
|
10
|
-
import { isTodayMember, } from "../
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { blue, bold, dim, strike, underline } from "./style.js";
|
|
14
|
-
import { areaMark, CHECKLIST_MARK, countChip, dateChip, deadlineToken, eveningMoon, LEGEND, LEGEND_GROUPS, loggedDate, NOTES_MARK, projectCircle, REMINDER_MARK, todayStar, todoBox, } from "./glyphs.js";
|
|
15
|
-
import { partitionSomedaySection, splitSectionBlocks, } from "../read/pagination.js";
|
|
10
|
+
import { isTodayMember, localToday, partitionSomedaySection, splitSectionBlocks, templateStatus, } from "../index.js";
|
|
11
|
+
import { bold, dim, strike, underline } from "./style.js";
|
|
12
|
+
import { areaMark, CHECKLIST_MARK, countChip, dateChip, deadlineToken, eveningMoon, LEGEND, LEGEND_GROUPS, loggedDate, NOTES_MARK, projectCircle, projectTitleAccent, REMINDER_MARK, todayStar, todoBox, } from "./glyphs.js";
|
|
16
13
|
import { FULL_MONTHS, upcomingBucket } from "./period.js";
|
|
14
|
+
import { fitRow, getFitWidth, resolveFit, stripSgr, TITLE_MIN, visibleWidth, } from "./width.js";
|
|
17
15
|
/**
|
|
18
16
|
* A view's TTY-only title preamble: bold view name + its dim Things deep link,
|
|
19
17
|
* then a blank line — e.g. `Anytime (things:///show?id=anytime)`. The id is the
|
|
@@ -25,9 +23,8 @@ export function viewHeaderLines(view) {
|
|
|
25
23
|
const title = view.charAt(0).toUpperCase() + view.slice(1);
|
|
26
24
|
return [`${bold(title)} ${dim(`(things:///show?id=${view})`)}`, ""];
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
export const stripAnsi =
|
|
30
|
-
const visibleWidth = (s) => [...stripAnsi(s)].length;
|
|
26
|
+
/** Re-export the canonical ANSI-stripper (impl in ./width.ts) — the legend and `--json` legend path use it. */
|
|
27
|
+
export const stripAnsi = stripSgr;
|
|
31
28
|
/** Left-column width for the legend's glyph samples (short marks align; long textual samples overflow). */
|
|
32
29
|
const LEGEND_GUTTER = 10;
|
|
33
30
|
/**
|
|
@@ -52,15 +49,20 @@ export function renderLegend() {
|
|
|
52
49
|
}
|
|
53
50
|
return lines;
|
|
54
51
|
}
|
|
52
|
+
/** Wraps a `#a #b` tag form in the row's dim styling (incl. its leading space). */
|
|
53
|
+
const styleTags = (form) => ` ${dim(form)}`;
|
|
55
54
|
/**
|
|
56
55
|
* One item line:
|
|
57
56
|
* `<uuid-prefix> <box> [★|⏾] [logged-date] [‹chip›] <title> [‹n›] [⍾] [≡] [≔] (container) #tags [⚑ deadline]`.
|
|
58
57
|
* Repeating templates seat ↻ INSIDE the box (`[↻]`/`(↻)`) rather than as a
|
|
59
|
-
* separate mark; open project
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
58
|
+
* separate mark; open project circles render blue, and project TITLES render
|
|
59
|
+
* bold + default-colored in every state (projectTitleAccent — the render-
|
|
60
|
+
* language law). The box is the glyph-language state carrier (../glyphs.ts):
|
|
61
|
+
* `[ ]`-family for to-dos, `( )`-family for projects — state survives with
|
|
62
|
+
* color stripped. Completed titles dim; canceled titles dim+strike (the `[×]`
|
|
63
|
+
* mark keeps the state when strike/ANSI is unavailable) — except where a view
|
|
64
|
+
* declares resolved its norm (Logbook, `resolvedNormal`), where completed is
|
|
65
|
+
* plain and canceled keeps only its strike. Human output shows
|
|
64
66
|
* a SHORTENED uuid prefix (every command accepts unique prefixes >= 6
|
|
65
67
|
* chars); `uuidWidth` is the display length from uuidDisplayWidth — never
|
|
66
68
|
* below 8 so a copied prefix stays unique across the whole database, not
|
|
@@ -78,8 +80,6 @@ export function formatItem(item, uuidWidth = 0, opts = {}) {
|
|
|
78
80
|
if (opts.mark != null)
|
|
79
81
|
meta.push(opts.mark);
|
|
80
82
|
// ↻ now lives INSIDE the box for templates (glyphs.ts) — no separate mark.
|
|
81
|
-
if (opts.statusWord !== undefined)
|
|
82
|
-
meta.push(dim(opts.statusWord));
|
|
83
83
|
if (item.status !== "open" && item.stopped !== null)
|
|
84
84
|
meta.push(loggedDate(item.stopped, todayIso));
|
|
85
85
|
if (opts.hideDateChip === true) {
|
|
@@ -90,6 +90,12 @@ export function formatItem(item, uuidWidth = 0, opts = {}) {
|
|
|
90
90
|
// Repeating templates chip their app-materialized next occurrence.
|
|
91
91
|
else if (item.repeating.isTemplate && item.repeating.nextOccurrence != null)
|
|
92
92
|
meta.push(dateChip(item.repeating.nextOccurrence, todayIso));
|
|
93
|
+
// A template with NO next date chips its scheduling word instead
|
|
94
|
+
// (waiting/paused/ended), in the same ‹chevron› form — derived HERE so every
|
|
95
|
+
// list view inherits it, not just upcoming's Repeating To-Dos section. (The
|
|
96
|
+
// detail card's prose repeat state is NOT chipped.)
|
|
97
|
+
else if (item.repeating.isTemplate)
|
|
98
|
+
meta.push(dim(`‹${templateStatus(item.repeating, todayIso)}›`));
|
|
93
99
|
// List rows mute their tags (the GUI's gray pills); tags go green only on
|
|
94
100
|
// the opened resource (todo show / the project|area header row).
|
|
95
101
|
const tags = item.tags.length > 0 ? ` ${dim(`#${item.tags.map((t) => t.title).join(" #")}`)}` : "";
|
|
@@ -98,9 +104,16 @@ export function formatItem(item, uuidWidth = 0, opts = {}) {
|
|
|
98
104
|
// template rows drop it via the sentinel guard: their deadline column
|
|
99
105
|
// carries app-internal 4001-01-01 sentinels (upcoming's synthesized
|
|
100
106
|
// occurrences carry REAL rule-derived deadlines and must keep the flag).
|
|
107
|
+
// The full-vs-compact deadline form is decided ONCE per view from the effective
|
|
108
|
+
// width against the two derived floors (width.ts resolveFit) — never per row —
|
|
109
|
+
// so the right-pinned gutter never mixes `Aug 12` and `8/12`. The null path
|
|
110
|
+
// (pipes/grep/--json/non-TTY) is always full-form (byte-identical contract).
|
|
111
|
+
const rawFitWidth = getFitWidth();
|
|
112
|
+
const fit = rawFitWidth === null ? null : resolveFit(rawFitWidth, FULL_FIT_FLOOR, COMPACT_FIT_FLOOR);
|
|
113
|
+
const compact = fit?.compact === true;
|
|
101
114
|
const rule = item.repeating.rule;
|
|
102
115
|
const deadline = item.status === "open" && item.deadline !== null && item.deadline < "4000"
|
|
103
|
-
? ` ${deadlineToken(item.deadline, todayIso)}`
|
|
116
|
+
? ` ${deadlineToken(item.deadline, todayIso, compact)}`
|
|
104
117
|
: // The GUI's bare flag on no-date repeating templates: the rule WILL
|
|
105
118
|
// assign each occurrence a deadline (fixed rules always do; after-
|
|
106
119
|
// completion only with a start offset), date unknown until spawned.
|
|
@@ -121,17 +134,32 @@ export function formatItem(item, uuidWidth = 0, opts = {}) {
|
|
|
121
134
|
: "";
|
|
122
135
|
// width 0 (the default) means "no column" — show the full uuid untouched.
|
|
123
136
|
const shownUuid = uuidWidth > 0 ? uuidCol(item.uuid, uuidWidth) : item.uuid;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
// Title styling composes four independent channels (docs/design/render-
|
|
138
|
+
// language.md) — a row marks only its deviations from the container's normal:
|
|
139
|
+
// strike — a canceled item (kept in EVERY context, GUI parity)
|
|
140
|
+
// dim — a resolved row DEVIATING from its list's norm (completed/
|
|
141
|
+
// canceled among open rows); dropped where resolved IS the norm
|
|
142
|
+
// (the Logbook passes resolvedNormal)
|
|
143
|
+
// bold — the project TYPE weight, every project row and every state,
|
|
144
|
+
// routed through the single law projectTitleAccent (glyphs.ts)
|
|
145
|
+
// underline — heading ROLE only (asTitle): a project that heads its own
|
|
146
|
+
// to-do group, never a plain project row
|
|
147
|
+
// Title styling is a closure over the raw text so the width fitter can
|
|
148
|
+
// truncate the PLAIN title and re-apply the same wraps — the ellipsis/clip
|
|
149
|
+
// boundary always lands outside the SGR runs (a cut never splits an escape).
|
|
150
|
+
const resolved = item.status === "completed" || item.status === "canceled";
|
|
151
|
+
const styleTitle = (text) => {
|
|
152
|
+
let t = text;
|
|
153
|
+
if (item.status === "canceled")
|
|
154
|
+
t = strike(t);
|
|
155
|
+
if (resolved && opts.resolvedNormal !== true)
|
|
156
|
+
t = dim(t);
|
|
157
|
+
if (item.type === "project")
|
|
158
|
+
t = projectTitleAccent(t);
|
|
159
|
+
if (asTitle)
|
|
160
|
+
t = underline(t);
|
|
161
|
+
return t;
|
|
162
|
+
};
|
|
135
163
|
// GUI indicator order after the title: bell, document, checklist.
|
|
136
164
|
const tail = [
|
|
137
165
|
...(item.type === "project" ? [countChip(item)] : []),
|
|
@@ -139,12 +167,34 @@ export function formatItem(item, uuidWidth = 0, opts = {}) {
|
|
|
139
167
|
...(item.notes !== "" ? [dim(NOTES_MARK)] : []),
|
|
140
168
|
...(item.type === "to-do" && item.checklistItemsCount > 0 ? [dim(CHECKLIST_MARK)] : []),
|
|
141
169
|
];
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
// Named segments (docs/design/width-aware-tty.md): the fixed left run (uuid +
|
|
171
|
+
// box + meta chips), the fixed tail markers, the collapsible tags, the CLI-
|
|
172
|
+
// only container, and the full deadline. `styleTags` wraps a `#a #b` form in
|
|
173
|
+
// the row's dim styling (incl. its leading space); the fitter folds the list.
|
|
174
|
+
const left = `${dim(shownUuid)} ${box}${meta.length > 0 ? ` ${meta.join(" ")}` : ""}`;
|
|
175
|
+
const tailStr = tail.length > 0 ? ` ${tail.join(" ")}` : "";
|
|
176
|
+
const ctxStr = context === "" ? "" : dim(context);
|
|
177
|
+
const tagNames = item.tags.map((t) => t.title);
|
|
178
|
+
const full = `${left} ${styleTitle(item.title)}${tailStr}${tags}${ctxStr}${deadline}`;
|
|
179
|
+
// width null (the default — pipes/grep/--json/non-TTY) means NO fitting:
|
|
180
|
+
// return the fully-composed row, byte-identical to before this feature.
|
|
181
|
+
if (fit === null)
|
|
182
|
+
return full;
|
|
183
|
+
const seg = {
|
|
184
|
+
left,
|
|
185
|
+
rawTitle: item.title,
|
|
186
|
+
styleTitle,
|
|
187
|
+
tail: tailStr,
|
|
188
|
+
tagNames,
|
|
189
|
+
styleTags,
|
|
190
|
+
context: ctxStr,
|
|
191
|
+
deadline,
|
|
192
|
+
full,
|
|
193
|
+
};
|
|
194
|
+
// Fit to the effective width: never below the compact floor (a sub-floor
|
|
195
|
+
// terminal renders at COMPACT_FIT_FLOOR and wraps, rather than clipping
|
|
196
|
+
// metadata). resolveFit already clamped fit.width up to the compact floor.
|
|
197
|
+
return fitRow(seg, fit.width);
|
|
148
198
|
}
|
|
149
199
|
/**
|
|
150
200
|
* Row prefix in today-aware views: yellow ★ for Today members, cyan ⏾ for
|
|
@@ -158,7 +208,75 @@ export function todayMark(item, now) {
|
|
|
158
208
|
return evening ? eveningMoon() : todayStar();
|
|
159
209
|
}
|
|
160
210
|
/** Minimum displayed-prefix length: shorter prefixes collide across the DB. */
|
|
161
|
-
const UUID_DISPLAY_MIN = 8;
|
|
211
|
+
export const UUID_DISPLAY_MIN = 8;
|
|
212
|
+
/**
|
|
213
|
+
* The two DERIVED width floors (docs/design/width-aware-tty.md § Compact
|
|
214
|
+
* deadline forms). Not chosen: computed from the actual glyph inventory as the
|
|
215
|
+
* worst-case FIXED furniture a row can carry plus a {@link TITLE_MIN}-column
|
|
216
|
+
* title. The only part that differs between the floors is the deadline token —
|
|
217
|
+
* the FULL floor budgets the full worst-case form (`⚑ 14 days left`), the
|
|
218
|
+
* COMPACT floor the narrow iOS-oracle worst case (`⚑ 14d left`; year-bearing
|
|
219
|
+
* far dates collapse to the bare year in compact mode, so they no longer
|
|
220
|
+
* compete — Mike's ruling 2026-07-14). Everything else
|
|
221
|
+
* is shared, so this returns both from one enumeration. Enumerated parts (any
|
|
222
|
+
* glyph change re-derives these; `width.test.ts` recomputes them independently
|
|
223
|
+
* so they cannot silently drift):
|
|
224
|
+
* - the id column (never shrinks) + its two-space separator
|
|
225
|
+
* - the checkbox glyph
|
|
226
|
+
* - a space + the widest meta chip (a future ‹date› carrying a year)
|
|
227
|
+
* - a space + the tail: project count chip + all three marks ◷ ≡ ≔
|
|
228
|
+
* (a conservative superset — the count chip and checklist never truly
|
|
229
|
+
* co-occur on one row, but budgeting both keeps the floor safe)
|
|
230
|
+
* - a space + the bare `#…` marker (tags never fully vanish)
|
|
231
|
+
* - a space + the longest deadline token (full or compact worst case)
|
|
232
|
+
* - a space + a TITLE_MIN-column title
|
|
233
|
+
*
|
|
234
|
+
* The driver fits every row to at least the COMPACT floor
|
|
235
|
+
* (`max(terminalWidth, COMPACT_FIT_FLOOR)`); a width in `[compact, full)` renders
|
|
236
|
+
* compact deadlines and every wider width the full form. So the worst-furniture
|
|
237
|
+
* row's title lands at exactly TITLE_MIN at whichever floor is active, every
|
|
238
|
+
* lighter row's title is wider (no per-row raggedness), and below the compact
|
|
239
|
+
* floor the terminal wraps (nothing is clipped) — mirroring the GUI, whose
|
|
240
|
+
* minimum WINDOW width is the same worst-case derivation.
|
|
241
|
+
*/
|
|
242
|
+
function computeFitFloors() {
|
|
243
|
+
const todayIso = "2000-01-01";
|
|
244
|
+
const box = "[ ]"; // every box form is 3 cells
|
|
245
|
+
const metaChip = dateChip("2027-09-22", todayIso); // widest ‹date› (with year)
|
|
246
|
+
const countChipWorst = countChip({
|
|
247
|
+
untrashedLeafActionsCount: 999,
|
|
248
|
+
openUntrashedLeafActionsCount: 999,
|
|
249
|
+
});
|
|
250
|
+
const tail = [countChipWorst, REMINDER_MARK, NOTES_MARK, CHECKLIST_MARK].join(" ");
|
|
251
|
+
const fullDeadline = deadlineToken("2000-01-15", todayIso); // ⚑ 14 days left — longest full form
|
|
252
|
+
// Compact worst case: the widest token that can appear in compact mode. Far
|
|
253
|
+
// dates collapse to the bare year (`⚑ 2001`, 6 cells), so the narrow
|
|
254
|
+
// relative `⚑ 14d left` (10 cells) governs; keep the max() so the floor
|
|
255
|
+
// stays safe if the compact vocabulary ever shifts.
|
|
256
|
+
const compactDeadline = Math.max(visibleWidth(deadlineToken("2000-01-15", todayIso, true)), // ⚑ 14d left
|
|
257
|
+
visibleWidth(deadlineToken("2001-02-10", todayIso, true)));
|
|
258
|
+
const furniture = UUID_DISPLAY_MIN +
|
|
259
|
+
2 + // the two spaces after the id column
|
|
260
|
+
visibleWidth(box) +
|
|
261
|
+
1 +
|
|
262
|
+
visibleWidth(metaChip) + // space + widest meta chip
|
|
263
|
+
1 + // the space before the title
|
|
264
|
+
TITLE_MIN +
|
|
265
|
+
1 +
|
|
266
|
+
visibleWidth(tail) + // space + count chip + ◷ ≡ ≔
|
|
267
|
+
1 +
|
|
268
|
+
visibleWidth("#…") + // space + the bare tag marker
|
|
269
|
+
1; // the space before the deadline token
|
|
270
|
+
return {
|
|
271
|
+
full: furniture + visibleWidth(fullDeadline),
|
|
272
|
+
compact: furniture + compactDeadline,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const FIT_FLOORS = computeFitFloors();
|
|
276
|
+
/** The full-form TTY row floor — worst-case furniture + full deadline. See {@link computeFitFloors}. */
|
|
277
|
+
export const FULL_FIT_FLOOR = FIT_FLOORS.full;
|
|
278
|
+
/** The compact-form TTY row floor — worst-case furniture + compact deadline. See {@link computeFitFloors}. */
|
|
279
|
+
export const COMPACT_FIT_FLOOR = FIT_FLOORS.compact;
|
|
162
280
|
/**
|
|
163
281
|
* Display width for a list's uuid column: the shortest prefix that is
|
|
164
282
|
* unique WITHIN the list, floored at UUID_DISPLAY_MIN (list-local
|
|
@@ -200,31 +318,66 @@ export function renderList(items) {
|
|
|
200
318
|
* the per-row ★/⏾, where the marker still carries information.
|
|
201
319
|
*
|
|
202
320
|
* This Evening mirrors the GUI: it renders ONLY when evening items exist —
|
|
203
|
-
* a truly-empty evening has no header at all. `
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
321
|
+
* a truly-empty evening has no header at all. `view` is the rows that survived
|
|
322
|
+
* the global `--limit`; `sections` is the metadata's pre-cap per-section counts
|
|
323
|
+
* ({@link SectionCount}) — the Today/This-Evening totals the hint math needs, so
|
|
324
|
+
* the section stays honest under truncation without a pre-cap copy of the view.
|
|
325
|
+
* When the cap hid some or all evening rows, an honest muted hint counts the
|
|
326
|
+
* hidden ones and points at the isolated `--evening` view — never the misleading
|
|
327
|
+
* `(empty)` a truncated evening used to show. (In `--evening` mode that pointer
|
|
328
|
+
* is redundant, so the hint keeps the `--limit`/`--all` levers instead.) `base`
|
|
329
|
+
* is the user's own invocation (flags echoed). The global footer (row driver)
|
|
330
|
+
* still reports the whole-view remainder separately.
|
|
331
|
+
*
|
|
332
|
+
* `options.eveningOnly` (the `--evening` section filter) renders ONLY the This
|
|
333
|
+
* Evening block — the Today header and its `(empty)` placeholder are suppressed
|
|
334
|
+
* because that section is deliberately filtered out, not merely empty.
|
|
210
335
|
*/
|
|
211
|
-
export function renderToday(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
];
|
|
217
|
-
|
|
336
|
+
export function renderToday(view, sections, base, options) {
|
|
337
|
+
// Pre-cap totals from the truncation metadata; fall back to the shown view's
|
|
338
|
+
// own lengths when a caller hands an unbounded view with no section counts.
|
|
339
|
+
const todayTotal = sections?.find((s) => s.key === "today")?.total ?? view.today.length;
|
|
340
|
+
const eveningTotal = sections?.find((s) => s.key === "evening")?.total ?? view.evening.length;
|
|
341
|
+
const w = uuidDisplayWidth([...view.today, ...view.evening]);
|
|
342
|
+
const eveningOnly = options?.eveningOnly === true;
|
|
343
|
+
const lines = eveningOnly
|
|
344
|
+
? []
|
|
345
|
+
: [
|
|
346
|
+
`${bold("──")} ${todayStar()} ${bold(`Today (badge: ${view.badge.dueOrOverdue} due/overdue · ${view.badge.other} other) ──`)}`,
|
|
347
|
+
...(view.today.length === 0 ? ["(empty)"] : view.today.map((i) => formatItem(i, w))),
|
|
348
|
+
];
|
|
349
|
+
if (eveningTotal > 0) {
|
|
350
|
+
// A blank line before the header matches every other grouped renderer's
|
|
351
|
+
// section spacing — but only when the Today section rendered above it. In
|
|
352
|
+
// --evening mode the header is the first line, so no leading blank.
|
|
353
|
+
if (!eveningOnly)
|
|
354
|
+
lines.push("");
|
|
218
355
|
lines.push(`${bold("──")} ${eveningMoon()} ${bold("This Evening ──")}`);
|
|
219
|
-
for (const i of
|
|
356
|
+
for (const i of view.evening)
|
|
220
357
|
lines.push(formatItem(i, w));
|
|
221
|
-
const hidden =
|
|
358
|
+
const hidden = eveningTotal - view.evening.length;
|
|
222
359
|
if (hidden > 0) {
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
360
|
+
const more = view.evening.length > 0 ? "more " : "";
|
|
361
|
+
const count = `${hidden} ${more}evening item${hidden === 1 ? "" : "s"}`;
|
|
362
|
+
// Normal Today view: the global truncation footer already carries the
|
|
363
|
+
// quantity levers (a bigger --limit / --all), so this hint is a pure
|
|
364
|
+
// pointer to the isolated This Evening view. In --evening mode that
|
|
365
|
+
// pointer is redundant (--evening is already active), so keep offering
|
|
366
|
+
// the levers that actually reveal more rows.
|
|
367
|
+
if (eveningOnly) {
|
|
368
|
+
const total = todayTotal + eveningTotal;
|
|
369
|
+
lines.push(dim(`… ${count} — \`${base} --limit ${total}\` · \`${base} --all\``));
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
lines.push(dim(`… ${count} — \`${base} --evening\``));
|
|
373
|
+
}
|
|
226
374
|
}
|
|
227
375
|
}
|
|
376
|
+
else if (eveningOnly) {
|
|
377
|
+
// Evening filter with no evening members: the section is genuinely empty
|
|
378
|
+
// (nothing was filtered out here), so an honest `(empty)` is correct.
|
|
379
|
+
lines.push("(empty)");
|
|
380
|
+
}
|
|
228
381
|
return lines;
|
|
229
382
|
}
|
|
230
383
|
/**
|
|
@@ -284,20 +437,31 @@ export function renderProjectsSidebar(items, hints) {
|
|
|
284
437
|
lines.push(`${bold("──")} ${areaMark()} ${bold(`${group.area.title} ──`)}`);
|
|
285
438
|
}
|
|
286
439
|
lines.push(...rows.map((item) => formatItem(item, w, { suppressArea: group.area?.uuid ?? null })));
|
|
440
|
+
// A per-group locator count (not a standalone disclosure): it marks WHERE
|
|
441
|
+
// the hidden later projects sit; the single reveal command rides the
|
|
442
|
+
// whole-view placeholder below.
|
|
287
443
|
if (group.hidden > 0)
|
|
288
|
-
lines.push(dim(
|
|
444
|
+
lines.push(dim(`… ${group.hidden} later project${group.hidden === 1 ? "" : "s"}`));
|
|
289
445
|
else if (group.area !== null && rows.length === 0)
|
|
290
446
|
lines.push(dim("(no projects)"));
|
|
291
447
|
}
|
|
292
448
|
if (total > 0)
|
|
293
|
-
lines.push("",
|
|
449
|
+
lines.push("", disclosureHint(total, "later project", [{ command: "things projects --show-later" }]));
|
|
294
450
|
return lines;
|
|
295
451
|
}
|
|
452
|
+
/** Upcoming rows group under startDate, or under deadline for non-template rows. */
|
|
453
|
+
const groupDate = (i) => i.startDate ?? (i.repeating.isTemplate ? null : i.deadline);
|
|
296
454
|
/**
|
|
297
455
|
* Upcoming rows under GUI-style date headers (empty periods are simply
|
|
298
456
|
* absent), with the trailing Repeating To-Dos section: templates with no
|
|
299
457
|
* set next occurrence, carrying their waiting/paused/ended status word and
|
|
300
458
|
* the bare ⚑ when the rule will assign a deadline per occurrence.
|
|
459
|
+
*
|
|
460
|
+
* Deadline-forecast rows (UPC1) keep startDate=null but carry a real future
|
|
461
|
+
* deadline; they group under their DEADLINE date via COALESCE(startDate,
|
|
462
|
+
* deadline) — no when-date pill (the header carries the date; formatItem's
|
|
463
|
+
* chip keys on startDate, so a null-startDate row shows only the ⚑ deadline
|
|
464
|
+
* flag, mirroring the GUI's bare-flag anatomy).
|
|
301
465
|
*/
|
|
302
466
|
export function renderUpcoming(items, now) {
|
|
303
467
|
if (items.length === 0)
|
|
@@ -305,12 +469,12 @@ export function renderUpcoming(items, now) {
|
|
|
305
469
|
const todayIso = localToday(now);
|
|
306
470
|
const w = uuidDisplayWidth(items);
|
|
307
471
|
const fmtOpts = now === undefined ? {} : { now };
|
|
308
|
-
const dated = items.filter((i) => i
|
|
472
|
+
const dated = items.filter((i) => groupDate(i) !== null);
|
|
309
473
|
const resting = items.filter((i) => i.startDate === null && i.repeating.isTemplate);
|
|
310
474
|
const lines = [];
|
|
311
475
|
let openHeader = null;
|
|
312
476
|
for (const item of dated) {
|
|
313
|
-
const bucket = upcomingBucket(item
|
|
477
|
+
const bucket = upcomingBucket(groupDate(item) ?? "", todayIso);
|
|
314
478
|
if (bucket.label !== openHeader) {
|
|
315
479
|
if (lines.length > 0)
|
|
316
480
|
lines.push("");
|
|
@@ -323,9 +487,10 @@ export function renderUpcoming(items, now) {
|
|
|
323
487
|
if (lines.length > 0)
|
|
324
488
|
lines.push("");
|
|
325
489
|
lines.push(bold("── Repeating To-Dos ──"));
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
490
|
+
// The ‹waiting›/‹paused›/‹ended› chip is derived inside formatItem
|
|
491
|
+
// (a template with no next date), so resting rows need no special opts.
|
|
492
|
+
for (const item of resting)
|
|
493
|
+
lines.push(formatItem(item, w, fmtOpts));
|
|
329
494
|
}
|
|
330
495
|
return lines;
|
|
331
496
|
}
|
|
@@ -334,6 +499,9 @@ export function renderUpcoming(items, now) {
|
|
|
334
499
|
* — `── July ──` within the current year, `── March 2025 ──` beyond (finer
|
|
335
500
|
* than the GUI's bare per-year buckets, deliberately). Truncation past the
|
|
336
501
|
* row limit is reported by the shared hint the command appends, not here.
|
|
502
|
+
* Resolved is the Logbook's NORM, so rows pass `resolvedNormal`: completed
|
|
503
|
+
* titles render plain and canceled titles keep their strikethrough but drop
|
|
504
|
+
* the dim (the blue `[✓]`/`[×]` marks and logged date are unchanged).
|
|
337
505
|
*/
|
|
338
506
|
export function renderLogbook(items, now) {
|
|
339
507
|
if (items.length === 0)
|
|
@@ -353,7 +521,9 @@ export function renderLogbook(items, now) {
|
|
|
353
521
|
lines.push(bold(`── ${heading} ──`));
|
|
354
522
|
openHeading = heading;
|
|
355
523
|
}
|
|
356
|
-
|
|
524
|
+
// The Logbook's normal IS the resolved state — rows render plain
|
|
525
|
+
// (completed) / strike-only (canceled), never dim (render-language delta 6).
|
|
526
|
+
lines.push(formatItem(item, w, { resolvedNormal: true, ...(now === undefined ? {} : { now }) }));
|
|
357
527
|
}
|
|
358
528
|
return lines;
|
|
359
529
|
}
|
|
@@ -409,10 +579,34 @@ export function renderSections(sections, star = false) {
|
|
|
409
579
|
return lines;
|
|
410
580
|
}
|
|
411
581
|
/** Single-quote a title for a copy-pasteable drill-down command. */
|
|
412
|
-
function quoteTitle(title) {
|
|
582
|
+
export function quoteTitle(title) {
|
|
413
583
|
return `'${title.replace(/'/g, "'\\''")}'`;
|
|
414
584
|
}
|
|
415
|
-
|
|
585
|
+
/**
|
|
586
|
+
* The one disclosure-hint grammar (docs/design/render-language.md § Disclosure
|
|
587
|
+
* hints): a muted `… <count> <noun> — [label:] `command` [· [label:] `command`]`.
|
|
588
|
+
* Every muted hint that discloses more content routes through here so the
|
|
589
|
+
* grammar lives in ONE place (the same one-law pattern as projectTitleAccent).
|
|
590
|
+
*
|
|
591
|
+
* `count` pluralizes `noun` (pass a `{ one, many }` pair when the plural is not
|
|
592
|
+
* a trailing `s`); each action is a FULL command echoing the user's own
|
|
593
|
+
* invocation, and a `label` prefixes an action only where it adds semantics the
|
|
594
|
+
* command text doesn't carry (e.g. `recent:` before a capped `--show-logged`).
|
|
595
|
+
* `indent` marks a TRUNCATION FOOTER — two spaces under its partially-shown
|
|
596
|
+
* section — whereas a HIDDEN-SECTION placeholder stays flush at the position
|
|
597
|
+
* its unrendered section would occupy.
|
|
598
|
+
*/
|
|
599
|
+
export function disclosureHint(count, noun, actions, opts = {}) {
|
|
600
|
+
const phrase = typeof noun === "string"
|
|
601
|
+
? `${noun}${count === 1 ? "" : "s"}`
|
|
602
|
+
: count === 1
|
|
603
|
+
? noun.one
|
|
604
|
+
: noun.many;
|
|
605
|
+
const acts = actions
|
|
606
|
+
.map((a) => `${a.label === undefined ? "" : `${a.label}: `}\`${a.command}\``)
|
|
607
|
+
.join(" · ");
|
|
608
|
+
return dim(`${opts.indent === true ? " " : ""}… ${count} ${phrase} — ${acts}`);
|
|
609
|
+
}
|
|
416
610
|
/** Muted per-block truncation line: `… N more — \`drill-down\``. */
|
|
417
611
|
function blockMoreLine(total, shown, drill) {
|
|
418
612
|
return dim(` … ${total - shown} more${drill === null ? "" : ` — \`${drill}\``}`);
|
|
@@ -421,36 +615,51 @@ function blockMoreLine(total, shown, drill) {
|
|
|
421
615
|
* Type-aware variant for blocks that mix project rows and to-dos (someday's
|
|
422
616
|
* loose/area blocks): the hidden remainder is split by type — `… 5 more
|
|
423
617
|
* projects, 14 more to-dos` — omitting a type with nothing hidden and
|
|
424
|
-
* pluralizing per count.
|
|
618
|
+
* pluralizing per count. The counts come from the block metadata (projects
|
|
619
|
+
* always list first, so the hidden split is derivable from `totalProjects`).
|
|
425
620
|
*/
|
|
426
|
-
function mixedMoreLine(
|
|
427
|
-
const projects = hidden.filter((i) => i.type === "project").length;
|
|
428
|
-
const todos = hidden.length - projects;
|
|
621
|
+
function mixedMoreLine(projects, todos, drill) {
|
|
429
622
|
const parts = [
|
|
430
623
|
...(projects > 0 ? [`${projects} more project${projects === 1 ? "" : "s"}`] : []),
|
|
431
624
|
...(todos > 0 ? [`${todos} more to-do${todos === 1 ? "" : "s"}`] : []),
|
|
432
625
|
];
|
|
433
626
|
return dim(` … ${parts.join(", ")}${drill === null ? "" : ` — \`${drill}\``}`);
|
|
434
627
|
}
|
|
628
|
+
/** The area/loose block for a section, matched by identity in the grouped metadata. */
|
|
629
|
+
function areaBlockFor(grouped, area) {
|
|
630
|
+
const kind = area === null ? "loose" : "area";
|
|
631
|
+
const ref = area?.uuid ?? null;
|
|
632
|
+
return grouped.blocks.find((b) => b.kind === kind && b.ref === ref);
|
|
633
|
+
}
|
|
634
|
+
/** A nested project block anywhere in the grouped metadata, matched by project uuid. */
|
|
635
|
+
function projectBlockFor(grouped, projectUuid) {
|
|
636
|
+
for (const b of grouped.blocks) {
|
|
637
|
+
const hit = b.children?.find((c) => c.kind === "project" && c.ref === projectUuid);
|
|
638
|
+
if (hit !== undefined)
|
|
639
|
+
return hit;
|
|
640
|
+
}
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
435
643
|
/**
|
|
436
644
|
* Muted bottom line for a truncated grouped view: `── more per group — see
|
|
437
|
-
* more: \`<base> <bigger flags>\` ·
|
|
438
|
-
*
|
|
645
|
+
* more: \`<base> <bigger flags>\` · \`<base> --all\` ──`, where the bigger-flags
|
|
646
|
+
* command doubles exactly the caps that actually truncated.
|
|
439
647
|
*/
|
|
440
648
|
function groupedBottomLine(base, escalations, allBase = base) {
|
|
441
649
|
const seeMore = escalations.length > 0 ? `see more: \`${base} ${escalations.join(" ")}\` · ` : "";
|
|
442
|
-
return dim(`── more per group — ${seeMore}
|
|
650
|
+
return dim(`── more per group — ${seeMore}\`${allBase} --all\` ──`);
|
|
443
651
|
}
|
|
444
652
|
/**
|
|
445
653
|
* The anytime preview: the FULL block skeleton — every area header and every
|
|
446
|
-
* project row — always renders; `
|
|
447
|
-
*
|
|
448
|
-
* block trails a muted `… N more — \`things (project|area) show
|
|
449
|
-
* drill-down (the loose block has no container, so it shows only the
|
|
450
|
-
* and the view ends with one line escalating the caps that hit
|
|
451
|
-
*
|
|
654
|
+
* project row — always renders; the `sections` are the already-bounded view
|
|
655
|
+
* and `grouped` the per-block metadata that carries each block's pre-cap total,
|
|
656
|
+
* so a truncated block trails a muted `… N more — \`things (project|area) show
|
|
657
|
+
* '…'\`` drill-down (the loose block has no container, so it shows only the
|
|
658
|
+
* count), and the view ends with one line escalating the caps that hit
|
|
659
|
+
* (`limits` supplies the doubling). Today members are starred. Mirrors
|
|
660
|
+
* renderSections' layout exactly.
|
|
452
661
|
*/
|
|
453
|
-
export function renderAnytimePreview(sections, limits, base) {
|
|
662
|
+
export function renderAnytimePreview(sections, grouped, limits, base) {
|
|
454
663
|
const all = sections.flatMap((s) => s.items);
|
|
455
664
|
if (all.length === 0)
|
|
456
665
|
return ["(empty)"];
|
|
@@ -467,31 +676,33 @@ export function renderAnytimePreview(sections, limits, base) {
|
|
|
467
676
|
blank();
|
|
468
677
|
lines.push(`${bold("──")} ${areaMark()} ${bold(`${section.area.title} ──`)}`);
|
|
469
678
|
}
|
|
679
|
+
// The section is already bounded, so its direct/child lists ARE the shown
|
|
680
|
+
// rows; the pre-cap totals come from the block metadata.
|
|
470
681
|
const { direct, projects } = splitSectionBlocks(section);
|
|
471
682
|
const suppressArea = section.area?.uuid ?? null;
|
|
472
|
-
const
|
|
473
|
-
for (const item of
|
|
683
|
+
const areaBlock = areaBlockFor(grouped, section.area);
|
|
684
|
+
for (const item of direct) {
|
|
474
685
|
lines.push(formatItem(item, w, { suppressArea, mark: todayMark(item) }));
|
|
475
686
|
}
|
|
476
|
-
if (
|
|
687
|
+
if (areaBlock !== undefined && areaBlock.total > areaBlock.shown) {
|
|
477
688
|
areaHit = true;
|
|
478
689
|
const drill = section.area === null ? null : `things area show ${quoteTitle(section.area.title)}`;
|
|
479
|
-
lines.push(blockMoreLine(
|
|
690
|
+
lines.push(blockMoreLine(areaBlock.total, areaBlock.shown, drill));
|
|
480
691
|
}
|
|
481
692
|
for (const { project, items: children } of projects) {
|
|
482
693
|
blank();
|
|
483
694
|
lines.push(formatItem(project, w, { projectTitle: true, suppressArea, mark: todayMark(project) }));
|
|
484
|
-
const
|
|
485
|
-
for (const item of shownChildren) {
|
|
695
|
+
for (const item of children) {
|
|
486
696
|
lines.push(formatItem(item, w, {
|
|
487
697
|
suppressProject: project.uuid,
|
|
488
698
|
suppressArea,
|
|
489
699
|
mark: todayMark(item),
|
|
490
700
|
}));
|
|
491
701
|
}
|
|
492
|
-
|
|
702
|
+
const projectBlock = areaBlock?.children?.find((c) => c.kind === "project" && c.ref === project.uuid);
|
|
703
|
+
if (projectBlock !== undefined && projectBlock.total > projectBlock.shown) {
|
|
493
704
|
projectHit = true;
|
|
494
|
-
lines.push(blockMoreLine(
|
|
705
|
+
lines.push(blockMoreLine(projectBlock.total, projectBlock.shown, `things project show ${quoteTitle(project.title)}`));
|
|
495
706
|
}
|
|
496
707
|
}
|
|
497
708
|
}
|
|
@@ -516,7 +727,7 @@ export function renderAnytimePreview(sections, limits, base) {
|
|
|
516
727
|
* When the toggle is off and such items exist, a muted bottom hint counts
|
|
517
728
|
* them and names the flag.
|
|
518
729
|
*/
|
|
519
|
-
export function renderSomedayPreview(sections, limits, base, showActive, hiddenActiveItems) {
|
|
730
|
+
export function renderSomedayPreview(sections, grouped, limits, base, showActive, hiddenActiveItems) {
|
|
520
731
|
const all = sections.flatMap((s) => s.items);
|
|
521
732
|
const lines = [];
|
|
522
733
|
let areaHit = false;
|
|
@@ -532,6 +743,8 @@ export function renderSomedayPreview(sections, limits, base, showActive, hiddenA
|
|
|
532
743
|
const w = uuidDisplayWidth(all);
|
|
533
744
|
const trailing = [];
|
|
534
745
|
for (const section of sections) {
|
|
746
|
+
// The section is already bounded; re-partitioning it yields the SHOWN own
|
|
747
|
+
// items and child groups, with pre-cap totals read from the metadata.
|
|
535
748
|
const { own, children } = partitionSomedaySection(section);
|
|
536
749
|
trailing.push(...children);
|
|
537
750
|
if (section.area !== null) {
|
|
@@ -539,13 +752,18 @@ export function renderSomedayPreview(sections, limits, base, showActive, hiddenA
|
|
|
539
752
|
lines.push(`${bold("──")} ${areaMark()} ${bold(`${section.area.title} ──`)}`);
|
|
540
753
|
}
|
|
541
754
|
const suppressArea = section.area?.uuid ?? null;
|
|
542
|
-
const
|
|
543
|
-
for (const item of shownOwn)
|
|
755
|
+
for (const item of own)
|
|
544
756
|
lines.push(formatItem(item, w, { suppressArea }));
|
|
545
|
-
|
|
757
|
+
const ownBlock = areaBlockFor(grouped, section.area);
|
|
758
|
+
if (ownBlock !== undefined && ownBlock.total > ownBlock.shown) {
|
|
546
759
|
areaHit = true;
|
|
547
760
|
const drill = section.area === null ? null : `things area show ${quoteTitle(section.area.title)}`;
|
|
548
|
-
|
|
761
|
+
// Projects list first, so the hidden split is derivable from the block's
|
|
762
|
+
// totalProjects and how many of them were shown.
|
|
763
|
+
const shownProjects = Math.min(ownBlock.shown, ownBlock.totalProjects ?? 0);
|
|
764
|
+
const hiddenProjects = (ownBlock.totalProjects ?? 0) - shownProjects;
|
|
765
|
+
const hiddenTodos = ownBlock.total - ownBlock.shown - hiddenProjects;
|
|
766
|
+
lines.push(mixedMoreLine(hiddenProjects, hiddenTodos, drill));
|
|
549
767
|
}
|
|
550
768
|
}
|
|
551
769
|
if (trailing.length > 0) {
|
|
@@ -553,14 +771,14 @@ export function renderSomedayPreview(sections, limits, base, showActive, hiddenA
|
|
|
553
771
|
lines.push(bold("── From active projects ──"));
|
|
554
772
|
for (const group of trailing) {
|
|
555
773
|
blank();
|
|
556
|
-
lines.push(`${dim(uuidCol(group.project.uuid, w))} ${
|
|
557
|
-
const
|
|
558
|
-
for (const item of shown) {
|
|
774
|
+
lines.push(`${dim(uuidCol(group.project.uuid, w))} ${underline(projectTitleAccent(group.project.title))}`);
|
|
775
|
+
for (const item of group.items) {
|
|
559
776
|
lines.push(formatItem(item, w, { suppressProject: group.project.uuid }));
|
|
560
777
|
}
|
|
561
|
-
|
|
778
|
+
const projectBlock = projectBlockFor(grouped, group.project.uuid);
|
|
779
|
+
if (projectBlock !== undefined && projectBlock.total > projectBlock.shown) {
|
|
562
780
|
projectHit = true;
|
|
563
|
-
lines.push(blockMoreLine(
|
|
781
|
+
lines.push(blockMoreLine(projectBlock.total, projectBlock.shown, `things project show ${quoteTitle(group.project.title)}`));
|
|
564
782
|
}
|
|
565
783
|
}
|
|
566
784
|
}
|
|
@@ -578,7 +796,10 @@ export function renderSomedayPreview(sections, limits, base, showActive, hiddenA
|
|
|
578
796
|
}
|
|
579
797
|
if (!showActive && hiddenActiveItems > 0) {
|
|
580
798
|
blank();
|
|
581
|
-
lines.push(
|
|
799
|
+
lines.push(disclosureHint(hiddenActiveItems, {
|
|
800
|
+
one: "someday to-do inside active projects",
|
|
801
|
+
many: "someday to-dos inside active projects",
|
|
802
|
+
}, [{ command: `${base} --show-active-project-items` }]));
|
|
582
803
|
}
|
|
583
804
|
return lines;
|
|
584
805
|
}
|