@promptctl/cc-candybar 1.0.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/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/cc-candybar +6 -0
- package/dist/index.mjs +185 -0
- package/package.json +99 -0
- package/plugin/.claude-plugin/plugin.json +11 -0
- package/plugin/bin/preview.sh +305 -0
- package/plugin/commands/candybar.md +403 -0
- package/plugin/templates/config-essential.json +36 -0
- package/plugin/templates/config-full.json +55 -0
- package/plugin/templates/config-standard.json +39 -0
- package/plugin/templates/config-tui-compact.json +48 -0
- package/plugin/templates/config-tui-full.json +89 -0
- package/plugin/templates/config-tui-standard.json +56 -0
- package/plugin/templates/config-tui.json +18 -0
- package/plugin/templates/nerd-fonts-sample.txt +5 -0
- package/schema/cc-candybar.schema.json +1379 -0
- package/src/click/wire.ts +113 -0
- package/src/config/action.ts +91 -0
- package/src/config/cli.ts +170 -0
- package/src/config/default-dsl-config.ts +661 -0
- package/src/config/dsl-loader.ts +265 -0
- package/src/config/dsl-types.ts +425 -0
- package/src/config/loader/actions.ts +530 -0
- package/src/config/loader/cache.ts +206 -0
- package/src/config/loader/cross-ref.ts +326 -0
- package/src/config/loader/cycles.ts +148 -0
- package/src/config/loader/diagnostics.ts +99 -0
- package/src/config/loader/discovery.ts +182 -0
- package/src/config/loader/emit-schema.ts +63 -0
- package/src/config/loader/globals.ts +42 -0
- package/src/config/loader/helpers.ts +48 -0
- package/src/config/loader/layout.ts +688 -0
- package/src/config/loader/merge.ts +40 -0
- package/src/config/loader/refs.ts +96 -0
- package/src/config/loader/segments.ts +120 -0
- package/src/config/loader/validate-core.ts +674 -0
- package/src/config/loader/variables.ts +260 -0
- package/src/daemon/acquire.ts +411 -0
- package/src/daemon/cache/git.ts +553 -0
- package/src/daemon/cache/render.ts +449 -0
- package/src/daemon/cache/session-usage-store.ts +446 -0
- package/src/daemon/cache/watchers.ts +245 -0
- package/src/daemon/client-debug.ts +120 -0
- package/src/daemon/client-stats.ts +129 -0
- package/src/daemon/client-transport.ts +273 -0
- package/src/daemon/client.ts +75 -0
- package/src/daemon/debug-types.ts +91 -0
- package/src/daemon/debug.ts +264 -0
- package/src/daemon/limits.ts +154 -0
- package/src/daemon/log.ts +69 -0
- package/src/daemon/parent-watchdog.ts +80 -0
- package/src/daemon/paths.ts +127 -0
- package/src/daemon/protocol.ts +235 -0
- package/src/daemon/render-payload.ts +611 -0
- package/src/daemon/server.ts +1103 -0
- package/src/daemon/session-state-file.ts +108 -0
- package/src/daemon/session-state.ts +237 -0
- package/src/daemon/stats.ts +229 -0
- package/src/daemon/verbs/index.ts +458 -0
- package/src/daemon/verbs/state-validators.ts +708 -0
- package/src/demo/dsl.ts +117 -0
- package/src/demo/mock-data.ts +67 -0
- package/src/demo/statusline.json5 +92 -0
- package/src/dsl/node-registry.ts +281 -0
- package/src/dsl/render.ts +558 -0
- package/src/index.ts +206 -0
- package/src/install/index.ts +410 -0
- package/src/proc/launch.ts +451 -0
- package/src/proc/stats-handle.ts +13 -0
- package/src/render/action.ts +458 -0
- package/src/render/diagnostic-style.ts +23 -0
- package/src/render/diagnostic-text.ts +77 -0
- package/src/render/error-glyph.ts +53 -0
- package/src/render/outcome-plan.ts +45 -0
- package/src/render/picker.ts +231 -0
- package/src/render/split-lines.ts +51 -0
- package/src/render/strip.ts +103 -0
- package/src/segments/cache.ts +131 -0
- package/src/segments/context.ts +190 -0
- package/src/segments/git.ts +561 -0
- package/src/segments/metrics.ts +101 -0
- package/src/segments/pricing.ts +452 -0
- package/src/segments/session.ts +188 -0
- package/src/segments/tmux.ts +74 -0
- package/src/template-engine/cells.ts +90 -0
- package/src/template-engine/colors.ts +102 -0
- package/src/template-engine/engine.ts +108 -0
- package/src/template-engine/funcs.ts +216 -0
- package/src/template-engine/index.ts +11 -0
- package/src/template-engine/layout.ts +112 -0
- package/src/template-engine/scope.ts +62 -0
- package/src/themes/index.ts +19 -0
- package/src/themes/palette-resolvers.ts +86 -0
- package/src/themes/policy.ts +79 -0
- package/src/themes/session-random.ts +88 -0
- package/src/utils/cache.ts +206 -0
- package/src/utils/claude.ts +616 -0
- package/src/utils/color-support.ts +118 -0
- package/src/utils/formatters.ts +77 -0
- package/src/utils/logger.ts +5 -0
- package/src/utils/outcome.ts +33 -0
- package/src/utils/schema-validator.ts +126 -0
- package/src/utils/single-flight.ts +57 -0
- package/src/utils/terminal-width.ts +43 -0
- package/src/utils/terminal.ts +11 -0
- package/src/utils/transcript-fs.ts +162 -0
- package/src/var-system/index.ts +24 -0
- package/src/var-system/sources.ts +1038 -0
- package/src/var-system/store.ts +223 -0
- package/src/var-system/types.ts +57 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// [LAW:one-type-per-behavior] Three primitives, three concerns:
|
|
2
|
+
//
|
|
3
|
+
// parseDslConfig (text → RawDslConfig)
|
|
4
|
+
// JSON5 syntax + per-record structural validation. Rejects the removed
|
|
5
|
+
// `layout:` key and `kind:"cells"` node with migration-pointing errors.
|
|
6
|
+
// Throws ConfigError on syntax / structural problems.
|
|
7
|
+
//
|
|
8
|
+
// mergeWithDefault (RawDslConfig + DslConfig → DslConfig)
|
|
9
|
+
// Cascade: shallow merge globals fields, by-name merge variables and
|
|
10
|
+
// segments, wholesale root replacement when present. Pure function.
|
|
11
|
+
//
|
|
12
|
+
// validateConfig (DslConfig → ValidatedConfig)
|
|
13
|
+
// Cross-references + cycle detection on the merged shape. Sole producer
|
|
14
|
+
// of ValidatedConfig. Throws ConfigError on cross-ref / cycle problems.
|
|
15
|
+
//
|
|
16
|
+
// loadConfig (path|null → DslConfig) wires parse+merge for the daemon's
|
|
17
|
+
// production path. validateConfig finishes the chain.
|
|
18
|
+
//
|
|
19
|
+
// [LAW:dataflow-not-control-flow] Validation passes accumulate issues into
|
|
20
|
+
// a list; consumers see every problem at once (compiler-style).
|
|
21
|
+
//
|
|
22
|
+
// This file is the pipeline orchestrator + the public barrel. Each validation
|
|
23
|
+
// concern lives in its own `loader/` module (split by change-reason); the
|
|
24
|
+
// re-exports below keep the import surface stable for every consumer.
|
|
25
|
+
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import JSON5 from "json5";
|
|
28
|
+
import {
|
|
29
|
+
type DslConfig,
|
|
30
|
+
type RawDslConfig,
|
|
31
|
+
type ValidatedConfig,
|
|
32
|
+
} from "./dsl-types.js";
|
|
33
|
+
import { DEFAULT_DSL_CONFIG } from "./default-dsl-config.js";
|
|
34
|
+
import { listResolvablePaletteNames } from "../themes/policy.js";
|
|
35
|
+
import {
|
|
36
|
+
ConfigError,
|
|
37
|
+
findKeyLine,
|
|
38
|
+
type ConfigIssue,
|
|
39
|
+
} from "./loader/diagnostics.js";
|
|
40
|
+
import {
|
|
41
|
+
describeType,
|
|
42
|
+
isPlainObject,
|
|
43
|
+
type Mutable,
|
|
44
|
+
type ValidateCtx,
|
|
45
|
+
} from "./loader/validate-core.js";
|
|
46
|
+
import { mergeWithDefault } from "./loader/merge.js";
|
|
47
|
+
import { validateGlobals } from "./loader/globals.js";
|
|
48
|
+
import { validateVariables } from "./loader/variables.js";
|
|
49
|
+
import { validateSegments } from "./loader/segments.js";
|
|
50
|
+
import { synthesizeGroupDecls, validateRoot } from "./loader/layout.js";
|
|
51
|
+
import { validateActions } from "./loader/actions.js";
|
|
52
|
+
import { validateHelpers } from "./loader/helpers.js";
|
|
53
|
+
import { validateCrossReferences } from "./loader/cross-ref.js";
|
|
54
|
+
import { validateNoCycles } from "./loader/cycles.js";
|
|
55
|
+
|
|
56
|
+
// ─── Public barrel ───────────────────────────────────────────────────────────
|
|
57
|
+
// [LAW:locality-or-seam] Consumers import from `dsl-loader`; the internal split
|
|
58
|
+
// is invisible to them. Moving a symbol between loader/ modules never touches a
|
|
59
|
+
// callsite as long as it stays re-exported here.
|
|
60
|
+
|
|
61
|
+
export { ConfigError, findKeyLine } from "./loader/diagnostics.js";
|
|
62
|
+
export type { ConfigIssue } from "./loader/diagnostics.js";
|
|
63
|
+
export {
|
|
64
|
+
expandHome,
|
|
65
|
+
dslConfigCandidatePaths,
|
|
66
|
+
resolveDslConfigPath,
|
|
67
|
+
detectConfigCollisions,
|
|
68
|
+
} from "./loader/discovery.js";
|
|
69
|
+
export { mergeWithDefault } from "./loader/merge.js";
|
|
70
|
+
export {
|
|
71
|
+
extractTemplateRefs,
|
|
72
|
+
extractActionRefs,
|
|
73
|
+
extractPickerRefs,
|
|
74
|
+
} from "./loader/refs.js";
|
|
75
|
+
|
|
76
|
+
// ─── Three-stage pipeline ────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load a JSON5 DSL config file from disk and merge it with the bundled
|
|
80
|
+
* default. Returns the effective DslConfig AND the raw source text.
|
|
81
|
+
*
|
|
82
|
+
* `path = null` means "no user file exists" — returns the default unchanged
|
|
83
|
+
* (uniform merge against an empty raw, which is deep-equal to the default) and
|
|
84
|
+
* an empty source. No consumer branches on file presence; that branch lives
|
|
85
|
+
* inside loadConfig exactly once.
|
|
86
|
+
*
|
|
87
|
+
* [LAW:one-source-of-truth] The source is returned alongside the config so the
|
|
88
|
+
* caller can hand it to validateConfig — cross-ref diagnostics (line numbers,
|
|
89
|
+
* the authored-surface discriminator) are derived from it, and the file is read
|
|
90
|
+
* exactly once here rather than re-read downstream.
|
|
91
|
+
*
|
|
92
|
+
* Throws ConfigError on JSON5 syntax / structural / per-record validation
|
|
93
|
+
* failures. Cross-references and cycles are validateConfig()'s job.
|
|
94
|
+
*
|
|
95
|
+
* [LAW:dataflow-not-control-flow] One function, one branch, same operations
|
|
96
|
+
* each call.
|
|
97
|
+
*/
|
|
98
|
+
export function loadConfig(
|
|
99
|
+
path: string | null,
|
|
100
|
+
dflt: DslConfig = DEFAULT_DSL_CONFIG,
|
|
101
|
+
allowedPalettes?: ReadonlySet<string>,
|
|
102
|
+
): { config: DslConfig; source: string } {
|
|
103
|
+
const source = path === null ? "" : fs.readFileSync(path, "utf-8");
|
|
104
|
+
const raw: RawDslConfig =
|
|
105
|
+
path === null ? {} : parseDslConfig(path, source, allowedPalettes);
|
|
106
|
+
return { config: mergeWithDefault(raw, dflt), source };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Promote a merged DslConfig to a ValidatedConfig by running cross-references
|
|
111
|
+
* and cycle detection. Sole producer of ValidatedConfig in the codebase — the
|
|
112
|
+
* phantom brand makes "the renderer never receives an unvalidated config" a
|
|
113
|
+
* compile-time invariant, not a runtime convention.
|
|
114
|
+
*
|
|
115
|
+
* Throws ConfigError aggregating every issue.
|
|
116
|
+
*
|
|
117
|
+
* [LAW:single-enforcer] One cast site, here, exclusive.
|
|
118
|
+
*/
|
|
119
|
+
export function validateConfig(
|
|
120
|
+
config: DslConfig,
|
|
121
|
+
filePath = "<config>",
|
|
122
|
+
source = "",
|
|
123
|
+
allowedPalettes: ReadonlySet<string> = new Set(listResolvablePaletteNames()),
|
|
124
|
+
): ValidatedConfig {
|
|
125
|
+
const issues: ConfigIssue[] = [];
|
|
126
|
+
const ctx: ValidateCtx = { source, issues, allowedPalettes, groups: [] };
|
|
127
|
+
validateCrossReferences(ctx, config);
|
|
128
|
+
validateNoCycles(ctx, config);
|
|
129
|
+
if (issues.length > 0) {
|
|
130
|
+
throw new ConfigError(filePath, issues);
|
|
131
|
+
}
|
|
132
|
+
return config as ValidatedConfig;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse a JSON5 DSL config source into a RawDslConfig. JSON5 syntax + per-
|
|
137
|
+
* record structural validation. Cross-references and cycles are NOT checked
|
|
138
|
+
* here — they belong to validateConfig, which runs on the merged shape.
|
|
139
|
+
*
|
|
140
|
+
* Returned shape preserves absence: top-level keys are optional in RawDslConfig.
|
|
141
|
+
*
|
|
142
|
+
* `allowedPalettes` is the set of palette names a `palette:` field may name.
|
|
143
|
+
* It defaults to every name that resolves to a concrete Palette, so production
|
|
144
|
+
* always validates loudly against the real registry. Tests inject a custom set
|
|
145
|
+
* to exercise validation without depending on registry contents.
|
|
146
|
+
*/
|
|
147
|
+
export function parseDslConfig(
|
|
148
|
+
filePath: string,
|
|
149
|
+
source: string,
|
|
150
|
+
allowedPalettes: ReadonlySet<string> = new Set(listResolvablePaletteNames()),
|
|
151
|
+
): RawDslConfig {
|
|
152
|
+
// ── Stage 1: JSON5 syntax. A parse error here is single, immediate, and
|
|
153
|
+
// carries line/col from the json5 package — no point continuing to other
|
|
154
|
+
// passes that need a parsed structure to inspect.
|
|
155
|
+
const raw = parseJson5OrThrow(filePath, source);
|
|
156
|
+
|
|
157
|
+
const issues: ConfigIssue[] = [];
|
|
158
|
+
const ctx: ValidateCtx = { source, issues, allowedPalettes, groups: [] };
|
|
159
|
+
|
|
160
|
+
// ── Stage 2: top-level shape + per-record shape. Absence survives as
|
|
161
|
+
// `undefined` in the returned RawDslConfig.
|
|
162
|
+
if (!isPlainObject(raw)) {
|
|
163
|
+
throw new ConfigError(filePath, [
|
|
164
|
+
{
|
|
165
|
+
path: "",
|
|
166
|
+
message: `Config root must be an object, got ${describeType(raw)}`,
|
|
167
|
+
},
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const topLevel = validateTopLevel(ctx, raw);
|
|
172
|
+
|
|
173
|
+
if (issues.length > 0) {
|
|
174
|
+
throw new ConfigError(filePath, issues);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return topLevel;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Internals ───────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
interface Json5Error extends Error {
|
|
183
|
+
lineNumber?: number;
|
|
184
|
+
columnNumber?: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseJson5OrThrow(filePath: string, source: string): unknown {
|
|
188
|
+
try {
|
|
189
|
+
return JSON5.parse(source);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const e = err as Json5Error;
|
|
192
|
+
throw new ConfigError(filePath, [
|
|
193
|
+
{
|
|
194
|
+
path: "",
|
|
195
|
+
message: `JSON5 syntax error: ${e.message}`,
|
|
196
|
+
line: e.lineNumber,
|
|
197
|
+
col: e.columnNumber,
|
|
198
|
+
},
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// [LAW:types-are-the-program] Returns RawDslConfig — absence of a top-level
|
|
204
|
+
// key survives the parse as `undefined`, distinct from explicit empty. The
|
|
205
|
+
// merge step downstream decides what "absent" means policy-wise (currently:
|
|
206
|
+
// inherit from default).
|
|
207
|
+
function validateTopLevel(
|
|
208
|
+
ctx: ValidateCtx,
|
|
209
|
+
raw: Record<string, unknown>,
|
|
210
|
+
): RawDslConfig {
|
|
211
|
+
for (const key of Object.keys(raw)) {
|
|
212
|
+
if (!TOP_LEVEL_KEYS.has(key)) {
|
|
213
|
+
ctx.issues.push({
|
|
214
|
+
path: key,
|
|
215
|
+
message: `Unknown top-level key "${key}". Expected one of: ${[...TOP_LEVEL_KEYS].join(", ")}`,
|
|
216
|
+
line: findKeyLine(ctx.source, [key]),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const out: Mutable<RawDslConfig> = {};
|
|
222
|
+
if (raw.globals !== undefined)
|
|
223
|
+
out.globals = validateGlobals(ctx, raw.globals);
|
|
224
|
+
if (raw.variables !== undefined)
|
|
225
|
+
out.variables = validateVariables(ctx, "variables", raw.variables);
|
|
226
|
+
if (raw.segments !== undefined)
|
|
227
|
+
out.segments = validateSegments(ctx, raw.segments);
|
|
228
|
+
// [LAW:no-silent-failure] `layout:` was removed in 2de.19. Reject loudly with
|
|
229
|
+
// a migration hint so the author knows exactly how to rewrite their config.
|
|
230
|
+
if (raw.layout !== undefined) {
|
|
231
|
+
ctx.issues.push({
|
|
232
|
+
path: "layout",
|
|
233
|
+
message:
|
|
234
|
+
`"layout" is no longer supported — use "root" with the A-grammar instead.\n` +
|
|
235
|
+
` Replace: layout: [["seg1", "seg2"], ["seg3"]]\n` +
|
|
236
|
+
` With: root: { v: [{ h: ["seg1", "seg2"] }, "seg3"] }\n` +
|
|
237
|
+
` Single-row example: root: { h: ["seg1", "seg2"] }`,
|
|
238
|
+
line: findKeyLine(ctx.source, ["layout"]),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (raw.root !== undefined) out.root = validateRoot(ctx, "root", raw.root);
|
|
242
|
+
if (raw.actions !== undefined)
|
|
243
|
+
out.actions = validateActions(ctx, raw.actions);
|
|
244
|
+
if (raw.helpers !== undefined)
|
|
245
|
+
out.helpers = validateHelpers(ctx, raw.helpers);
|
|
246
|
+
// [LAW:one-source-of-truth] Group sugar synthesis runs AFTER every section
|
|
247
|
+
// parsed: each group collected during the root walk emits its state var +
|
|
248
|
+
// cycle action + toggle segment into the raw sections (so they merge over the
|
|
249
|
+
// default and cross-ref like any user declaration), and user names under the
|
|
250
|
+
// reserved namespace are rejected against the fully-parsed sections.
|
|
251
|
+
synthesizeGroupDecls(ctx, out);
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// [LAW:no-silent-failure] `layout` is intentionally absent — a config that
|
|
256
|
+
// writes it gets an explicit migration error, not an "unknown key" message.
|
|
257
|
+
const TOP_LEVEL_KEYS = new Set([
|
|
258
|
+
"globals",
|
|
259
|
+
"variables",
|
|
260
|
+
"segments",
|
|
261
|
+
"layout",
|
|
262
|
+
"root",
|
|
263
|
+
"actions",
|
|
264
|
+
"helpers",
|
|
265
|
+
]);
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// [LAW:types-are-the-program] DslConfig is the strongest theorem we can write
|
|
2
|
+
// about a validated config: every legal config is representable, every illegal
|
|
3
|
+
// one is not. The loader is the proof — its body either narrows `unknown` to
|
|
4
|
+
// `DslConfig` or throws ConfigError. Downstream consumers receive a DslConfig
|
|
5
|
+
// and are free to assume invariants (closed source-kind set, exactly-one cache
|
|
6
|
+
// key, no dangling cross-refs, no template cycles) without re-checking.
|
|
7
|
+
//
|
|
8
|
+
// [LAW:one-source-of-truth] These shapes are the JSON-shape mirror of the
|
|
9
|
+
// var-system's runtime types (`CachePolicy`, `ShellOptions`, etc. in
|
|
10
|
+
// src/var-system/sources.ts). The loader is the single point that translates
|
|
11
|
+
// between the two; no other module should re-derive these shapes.
|
|
12
|
+
|
|
13
|
+
// [LAW:one-way-deps] The action schema lives in its own leaf module; DslConfig
|
|
14
|
+
// references it here. The dependency is one-way (this file → action.ts), never
|
|
15
|
+
// the reverse, so that shape can be lifted out without a cycle.
|
|
16
|
+
import type { ActionDecl } from "./action.js";
|
|
17
|
+
|
|
18
|
+
// [LAW:types-are-the-program] Three stages, three names.
|
|
19
|
+
//
|
|
20
|
+
// RawDslConfig — the user-file shape. Every top-level key is optional
|
|
21
|
+
// because "user didn't write this" is a representable,
|
|
22
|
+
// distinct state from "user wrote an explicit empty."
|
|
23
|
+
// Internal to the loader module; downstream consumers
|
|
24
|
+
// never see it.
|
|
25
|
+
//
|
|
26
|
+
// DslConfig — the effective shape: the user's deltas merged on top
|
|
27
|
+
// of DEFAULT_DSL_CONFIG. Every top-level key is required.
|
|
28
|
+
// Output of `loadConfig`. Cross-refs and cycles have NOT
|
|
29
|
+
// yet been checked at this stage.
|
|
30
|
+
//
|
|
31
|
+
// ValidatedConfig — DslConfig + a phantom brand proving validateConfig()
|
|
32
|
+
// has run. The renderer accepts only this type, so the
|
|
33
|
+
// compiler structurally enforces "no unvalidated config
|
|
34
|
+
// can reach rendering." The brand is module-scoped via
|
|
35
|
+
// `unique symbol`, so the only construction site is
|
|
36
|
+
// `validateConfig` itself.
|
|
37
|
+
// [LAW:types-are-the-program] The recursive layout substrate collapses to
|
|
38
|
+
// exactly two kinds: a `segment` leaf (a ref into the named `segments` block —
|
|
39
|
+
// THE unit of rendering, a single template that IS its content) or a
|
|
40
|
+
// `container` whose `direction` is DATA that decides how its children map onto
|
|
41
|
+
// the 2D plane. Both the bar and (a later child's) menu are projections of this
|
|
42
|
+
// one tree — they differ only in `direction`, not in code path
|
|
43
|
+
// [LAW:dataflow-not-control-flow].
|
|
44
|
+
//
|
|
45
|
+
// [LAW:types-are-the-program] `Direction` carries the projection a container
|
|
46
|
+
// applies to its child blocks as DATA. `vertical` STACKS them (concat the
|
|
47
|
+
// children's line-lists); `horizontal` ZIPS them (per row, the children's cells
|
|
48
|
+
// concatenate into one strip, so the powerline joiner caps ACROSS the seam —
|
|
49
|
+
// abut is never valid). `outline` (a later child's menu) is NOT in the union
|
|
50
|
+
// yet — it joins as a new arm only when its renderer exists, so the union stays
|
|
51
|
+
// the strongest theorem that is still TRUE, with no representable-but-
|
|
52
|
+
// unrenderable direction.
|
|
53
|
+
// [LAW:one-source-of-truth] The runtime list and the type derive from one
|
|
54
|
+
// declaration; the loader validates a container's `direction` against this set,
|
|
55
|
+
// and renderDsl's projection switch is exhaustive over it (adding an arm here
|
|
56
|
+
// forces a matching render arm).
|
|
57
|
+
export const DIRECTIONS = ["vertical", "horizontal"] as const;
|
|
58
|
+
export type Direction = (typeof DIRECTIONS)[number];
|
|
59
|
+
|
|
60
|
+
// [LAW:one-type-per-behavior] THE unit of rendering: a ref into the named
|
|
61
|
+
// `segments` block. A segment IS a single template (text, state-driven display,
|
|
62
|
+
// clickable regions — whatever the template produces); there is no `inline` /
|
|
63
|
+
// `stepper` / `picker` node kind, because "make a node flexible enough for
|
|
64
|
+
// whatever" = one template expresses anything. A segment renders to ONE strip
|
|
65
|
+
// item; the powerline joiner joins items, never inside one. A horizontal run of
|
|
66
|
+
// segments is spelled `{ h: ["seg1", "seg2"] }` in the A-grammar.
|
|
67
|
+
export interface SegmentNode {
|
|
68
|
+
readonly kind: "segment";
|
|
69
|
+
// A name into the `segments` block. The segment's own template/palette/`when`
|
|
70
|
+
// live on its SegmentDecl, not here; this node is purely the tree position.
|
|
71
|
+
readonly name: string;
|
|
72
|
+
// [LAW:dataflow-not-control-flow] Absent `when` ≡ always-rendered. A node-level
|
|
73
|
+
// predicate, ANDed with the segment-decl's own `when` at render.
|
|
74
|
+
readonly when?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ContainerNode {
|
|
78
|
+
readonly kind: "container";
|
|
79
|
+
readonly direction: Direction;
|
|
80
|
+
readonly children: readonly LayoutNode[];
|
|
81
|
+
// A container's `when` gates the whole subtree: a hidden container emits no
|
|
82
|
+
// lines, but its descendants are still walked so per-segment hue indices stay
|
|
83
|
+
// positionally stable.
|
|
84
|
+
readonly when?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type LayoutNode = ContainerNode | SegmentNode;
|
|
88
|
+
|
|
89
|
+
// [LAW:types-are-the-program] The `group` SUGAR as collected at parse — an
|
|
90
|
+
// INPUT-only shape, never a canonical LayoutNode kind: arranging + gating are
|
|
91
|
+
// behaviors `container` already has, so "group" may only be a spelling. The
|
|
92
|
+
// loader lowers each group to container/segment nodes and SYNTHESIZES its state
|
|
93
|
+
// var + cycle action + toggle segment under the reserved `groups.` namespace
|
|
94
|
+
// (one declaration; every derived artifact single-sourced from it
|
|
95
|
+
// [LAW:one-source-of-truth]). `path` records the node's tree position so the
|
|
96
|
+
// nesting invariant (an ancestor and a descendant must not share a state key)
|
|
97
|
+
// is checkable after the walk.
|
|
98
|
+
export interface GroupSugarDecl {
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly label: string;
|
|
101
|
+
readonly open?: boolean;
|
|
102
|
+
readonly direction?: Direction;
|
|
103
|
+
readonly key?: string;
|
|
104
|
+
readonly bg?: string;
|
|
105
|
+
readonly fg?: string;
|
|
106
|
+
readonly when?: string;
|
|
107
|
+
readonly path: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// [LAW:single-enforcer] THE one pre-order walk over a node tree. Every consumer
|
|
111
|
+
// that needs "which segments / which `when` predicates does this layout name"
|
|
112
|
+
// (the reachability closure, the debug dump, the cross-ref validator) iterates
|
|
113
|
+
// this — none re-recurses the tree itself.
|
|
114
|
+
export function* walkNodes(node: LayoutNode): IterableIterator<LayoutNode> {
|
|
115
|
+
yield node;
|
|
116
|
+
if (node.kind === "container") {
|
|
117
|
+
for (const child of node.children) yield* walkNodes(child);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface RawDslConfig {
|
|
122
|
+
readonly globals?: Partial<Globals>;
|
|
123
|
+
readonly variables?: Readonly<Record<string, VariableDecl>>;
|
|
124
|
+
readonly segments?: Readonly<Record<string, SegmentDecl>>;
|
|
125
|
+
readonly root?: LayoutNode;
|
|
126
|
+
readonly actions?: Readonly<Record<string, ActionDecl>>;
|
|
127
|
+
// [LAW:single-enforcer] Config-level shared helper templates: name → Go-template
|
|
128
|
+
// body. Each compiles to one `{{ define "name" }}body{{ end }}` block, and the
|
|
129
|
+
// whole set into a single output-neutral preamble prepended to every template
|
|
130
|
+
// this config parses — so a formatter (`{{ template "formatCost" .x }}`) is
|
|
131
|
+
// defined ONCE and callable from any segment/predicate, never re-inlined per
|
|
132
|
+
// segment. Absent ≡ no helpers; merges by-name (user overrides a helper).
|
|
133
|
+
readonly helpers?: Readonly<Record<string, string>>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface DslConfig {
|
|
137
|
+
readonly globals: Globals;
|
|
138
|
+
readonly variables: Readonly<Record<string, VariableDecl>>;
|
|
139
|
+
readonly segments: Readonly<Record<string, SegmentDecl>>;
|
|
140
|
+
// [LAW:one-source-of-truth] The SINGLE canonical layout representation authored
|
|
141
|
+
// via the A-grammar (seg/h/v node arms, group sugar). No legacy sugar reaches
|
|
142
|
+
// this field; the loader rejects `layout:` and `kind:"cells"` with migration errors.
|
|
143
|
+
readonly root: LayoutNode;
|
|
144
|
+
// [LAW:locality-or-seam] The named seam between click BEHAVIOR and the
|
|
145
|
+
// clickable REPRESENTATION. Each entry is a statically-declared effect a
|
|
146
|
+
// segment template binds a region to via `{{ action "name" … }}`. The
|
|
147
|
+
// writable-key gate derives from this table (deriveActionValidators), so a
|
|
148
|
+
// template cannot smuggle an un-gated write. Empty when no config declares
|
|
149
|
+
// actions — an absent `actions` key merges to `{}`.
|
|
150
|
+
readonly actions: Readonly<Record<string, ActionDecl>>;
|
|
151
|
+
// [LAW:single-enforcer] The effective helper set: a name → template-body map
|
|
152
|
+
// compiled to a defines-preamble at registerDslConfig. Empty when no config
|
|
153
|
+
// declares helpers — an absent `helpers` key merges to `{}` (same cascade as
|
|
154
|
+
// actions). The single definition site for each formatter/transform a template
|
|
155
|
+
// calls via `{{ template "name" .arg }}`.
|
|
156
|
+
readonly helpers: Readonly<Record<string, string>>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// [LAW:single-enforcer] The brand symbol is `unique` and module-private —
|
|
160
|
+
// nothing outside this file can construct a value carrying it. The only
|
|
161
|
+
// production-path producer is validateConfig() in dsl-loader.ts (one
|
|
162
|
+
// callsite of `config as ValidatedConfig`). Renderer signatures require
|
|
163
|
+
// ValidatedConfig; the type system therefore proves the validation step
|
|
164
|
+
// ran before any render path consumed the config.
|
|
165
|
+
declare const __validated: unique symbol;
|
|
166
|
+
export type ValidatedConfig = DslConfig & {
|
|
167
|
+
readonly [__validated]: true;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export interface Globals {
|
|
171
|
+
readonly default_bg?: string;
|
|
172
|
+
readonly default_fg?: string;
|
|
173
|
+
readonly default_empty_value?: string;
|
|
174
|
+
readonly default_separator?: string;
|
|
175
|
+
readonly default_truncate_marker?: string;
|
|
176
|
+
// [LAW:one-source-of-truth] A palette NAME, not a resolved Palette: DslConfig
|
|
177
|
+
// is the JSON-shape mirror, so the name is the authoritative datum and the
|
|
178
|
+
// renderer owns name→Palette resolution. The config default for the base
|
|
179
|
+
// theme; the daemon resolves the live base per render as
|
|
180
|
+
// `sessionState.theme ?? globals.palette ?? default`, and a per-segment
|
|
181
|
+
// `palette` is an explicit override that ignores the session theme.
|
|
182
|
+
readonly palette?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// [LAW:one-type-per-behavior] One discriminated union covers every source
|
|
186
|
+
// kind. Adding a new kind = code change here + matching runtime support in
|
|
187
|
+
// var-system. There is no "extension" path that bypasses this list.
|
|
188
|
+
export type VariableDecl =
|
|
189
|
+
| LiteralVarDecl
|
|
190
|
+
| InputVarDecl
|
|
191
|
+
| EnvVarDecl
|
|
192
|
+
| FileVarDecl
|
|
193
|
+
| ShellVarDecl
|
|
194
|
+
| TemplateVarDecl
|
|
195
|
+
| TimeVarDecl
|
|
196
|
+
| GitVarDecl
|
|
197
|
+
| StateVarDecl;
|
|
198
|
+
|
|
199
|
+
export interface LiteralVarDecl {
|
|
200
|
+
readonly kind: "literal";
|
|
201
|
+
readonly value: string | number | boolean;
|
|
202
|
+
readonly default?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// [LAW:types-are-the-program] `type` carries the runtime kind of the value at
|
|
206
|
+
// the resolved payload path. Number/bool are needed for the usage/cost/today
|
|
207
|
+
// family — token counts, cost amounts, percentages — whose formatters
|
|
208
|
+
// (`formatCost`, `formatTokens`, `round`, `budgetStatus`) take numeric inputs.
|
|
209
|
+
// Absent `type` defaults to "string" at the loader, preserving the historical
|
|
210
|
+
// behavior of every existing declaration. The default value's literal type
|
|
211
|
+
// must match the declared type — a number default on a string-typed input
|
|
212
|
+
// (or vice versa) is rejected at load time, not at first render.
|
|
213
|
+
export interface InputVarDecl {
|
|
214
|
+
readonly kind: "input";
|
|
215
|
+
readonly path: string;
|
|
216
|
+
readonly type?: "string" | "number" | "boolean";
|
|
217
|
+
readonly default?: string | number | boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface EnvVarDecl {
|
|
221
|
+
readonly kind: "env";
|
|
222
|
+
readonly name: string;
|
|
223
|
+
readonly default?: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface FileVarDecl {
|
|
227
|
+
readonly kind: "file";
|
|
228
|
+
readonly path: string;
|
|
229
|
+
readonly readMode?: "whole" | "first-line";
|
|
230
|
+
readonly regex?: string;
|
|
231
|
+
readonly cache: CacheDecl;
|
|
232
|
+
readonly default?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface ShellVarDecl {
|
|
236
|
+
readonly kind: "shell";
|
|
237
|
+
readonly command: string;
|
|
238
|
+
readonly regex?: string;
|
|
239
|
+
readonly cache: CacheDecl;
|
|
240
|
+
readonly default?: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface TemplateVarDecl {
|
|
244
|
+
readonly kind: "template";
|
|
245
|
+
readonly template: string;
|
|
246
|
+
readonly cache?: CacheDecl;
|
|
247
|
+
readonly default?: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// [LAW:types-are-the-program] Time vars refresh on a clock — ttl is the only
|
|
251
|
+
// cache form the runtime honors (declareTime always registers a TTL timer).
|
|
252
|
+
// The loader rejects the other CacheDecl arms at load, so past that boundary
|
|
253
|
+
// a non-ttl cache on a time var is unrepresentable, not silently coerced.
|
|
254
|
+
export interface TimeVarDecl {
|
|
255
|
+
readonly kind: "time";
|
|
256
|
+
readonly layout: string;
|
|
257
|
+
readonly cache?: TtlCacheDecl;
|
|
258
|
+
readonly default?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface GitVarDecl {
|
|
262
|
+
readonly kind: "git";
|
|
263
|
+
readonly field: GitField;
|
|
264
|
+
readonly cache: CacheDecl;
|
|
265
|
+
readonly default?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// [LAW:one-source-of-truth] A `state` variable reads through to the daemon's
|
|
269
|
+
// SessionState (the canonical store for per-session toggles, random picks,
|
|
270
|
+
// click-mutated values). Reactivity is wired by SessionState's internal MobX
|
|
271
|
+
// atom — a click verb that writes into SessionState invalidates this
|
|
272
|
+
// variable's downstream computeds automatically. Persistence rides for free
|
|
273
|
+
// on SessionState's disk backing.
|
|
274
|
+
//
|
|
275
|
+
// The session id is resolved from the conventional `session.id` variable —
|
|
276
|
+
// that name is the canonical anchor for "which session am I in," declared
|
|
277
|
+
// once by DSL configs as an input variable carrying hook_data.session_id.
|
|
278
|
+
// [LAW:no-mode-explosion] No per-decl override knob: a single canonical
|
|
279
|
+
// session-id source keeps every state var's resolution uniform and removes
|
|
280
|
+
// an axis along which configs could drift from each other.
|
|
281
|
+
export interface StateVarDecl {
|
|
282
|
+
readonly kind: "state";
|
|
283
|
+
readonly key: string;
|
|
284
|
+
readonly default?: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type GitField =
|
|
288
|
+
| "branch"
|
|
289
|
+
| "sha"
|
|
290
|
+
| "dirty"
|
|
291
|
+
| "ahead"
|
|
292
|
+
| "behind"
|
|
293
|
+
| "stash";
|
|
294
|
+
|
|
295
|
+
// [LAW:dataflow-not-control-flow] The discriminator is "which key is present"
|
|
296
|
+
// in the user's JSON — not a `kind` field. Encoded as a 5-arm union so the
|
|
297
|
+
// type system enforces "exactly one of these." The loader validates the
|
|
298
|
+
// runtime invariant (one and only one); the type then carries it forward.
|
|
299
|
+
export type CacheDecl =
|
|
300
|
+
| TtlCacheDecl
|
|
301
|
+
| { readonly watch_file: string }
|
|
302
|
+
| { readonly depends_on: readonly string[] }
|
|
303
|
+
| { readonly key: string }
|
|
304
|
+
| { readonly never: true };
|
|
305
|
+
|
|
306
|
+
// [LAW:one-source-of-truth] The ttl arm named once, so the kinds that honor
|
|
307
|
+
// only a refresh interval (time) reference the same member the full vocabulary
|
|
308
|
+
// is composed from — narrowing is a subset, never a parallel shape.
|
|
309
|
+
export interface TtlCacheDecl {
|
|
310
|
+
readonly ttl: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export const CACHE_KEYS = [
|
|
314
|
+
"ttl",
|
|
315
|
+
"watch_file",
|
|
316
|
+
"depends_on",
|
|
317
|
+
"key",
|
|
318
|
+
"never",
|
|
319
|
+
] as const;
|
|
320
|
+
export type CacheKey = (typeof CACHE_KEYS)[number];
|
|
321
|
+
|
|
322
|
+
export const SOURCE_KINDS = [
|
|
323
|
+
"literal",
|
|
324
|
+
"input",
|
|
325
|
+
"env",
|
|
326
|
+
"file",
|
|
327
|
+
"shell",
|
|
328
|
+
"template",
|
|
329
|
+
"time",
|
|
330
|
+
"git",
|
|
331
|
+
"state",
|
|
332
|
+
] as const;
|
|
333
|
+
export type SourceKind = (typeof SOURCE_KINDS)[number];
|
|
334
|
+
|
|
335
|
+
// [LAW:one-source-of-truth] The "which kinds have a cache field" predicate
|
|
336
|
+
// lives here once. The loader's cross-ref and cycle validators narrow via
|
|
337
|
+
// this guard instead of repeating the kind list (`!== "literal" && !==
|
|
338
|
+
// "input" && ...`) at every site — adding a new no-cache kind only requires
|
|
339
|
+
// updating the union and this guard.
|
|
340
|
+
export type VariableDeclWithCache =
|
|
341
|
+
| FileVarDecl
|
|
342
|
+
| ShellVarDecl
|
|
343
|
+
| TemplateVarDecl
|
|
344
|
+
| TimeVarDecl
|
|
345
|
+
| GitVarDecl;
|
|
346
|
+
|
|
347
|
+
export function hasCacheField(v: VariableDecl): v is VariableDeclWithCache {
|
|
348
|
+
return (
|
|
349
|
+
v.kind !== "literal" &&
|
|
350
|
+
v.kind !== "input" &&
|
|
351
|
+
v.kind !== "env" &&
|
|
352
|
+
v.kind !== "state"
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const GIT_FIELDS: readonly GitField[] = [
|
|
357
|
+
"branch",
|
|
358
|
+
"sha",
|
|
359
|
+
"dirty",
|
|
360
|
+
"ahead",
|
|
361
|
+
"behind",
|
|
362
|
+
"stash",
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
// Source kinds where the user MUST declare a cache policy (no sensible default).
|
|
366
|
+
// Aligns with the proposal's cache-invalidation table.
|
|
367
|
+
export const SOURCES_REQUIRING_CACHE: readonly SourceKind[] = [
|
|
368
|
+
"file",
|
|
369
|
+
"shell",
|
|
370
|
+
"git",
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
export interface SegmentDecl {
|
|
374
|
+
readonly template: string;
|
|
375
|
+
readonly width?: "auto" | number;
|
|
376
|
+
readonly justify?: JustifyMode;
|
|
377
|
+
readonly truncate?: TruncateMode;
|
|
378
|
+
readonly bg?: string;
|
|
379
|
+
readonly fg?: string;
|
|
380
|
+
readonly when?: string;
|
|
381
|
+
// [LAW:one-source-of-truth] Per-segment palette override (a NAME). Overrides
|
|
382
|
+
// globals.palette for this segment only; undefined = inherit the cascade base.
|
|
383
|
+
readonly palette?: string;
|
|
384
|
+
// Per-segment vars sub-block — lives in the same global MobX store at
|
|
385
|
+
// runtime under the namespaced key `<segment>.<var>`. Templates reference a
|
|
386
|
+
// segment local ONLY via that namespaced form (`.<segment>.local`), from any
|
|
387
|
+
// segment including the owning one; the loader rejects bare refs at load
|
|
388
|
+
// with a diagnostic naming the namespaced form. [LAW:one-source-of-truth]
|
|
389
|
+
readonly vars?: Readonly<Record<string, VariableDecl>>;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export type JustifyMode = "left" | "center" | "right";
|
|
393
|
+
export type TruncateMode = "right" | "left" | "middle";
|
|
394
|
+
|
|
395
|
+
export const JUSTIFY_MODES: readonly JustifyMode[] = [
|
|
396
|
+
"left",
|
|
397
|
+
"center",
|
|
398
|
+
"right",
|
|
399
|
+
];
|
|
400
|
+
export const TRUNCATE_MODES: readonly TruncateMode[] = [
|
|
401
|
+
"right",
|
|
402
|
+
"left",
|
|
403
|
+
"middle",
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
// ─── Conventional render-time variable names ─────────────────────────────────
|
|
407
|
+
//
|
|
408
|
+
// [LAW:one-source-of-truth] These are not widget types (those live in
|
|
409
|
+
// `./action.ts`); they are the conventional variable NAMES the renderer and the
|
|
410
|
+
// picker agree on. Kept here, with the other render/config conventions.
|
|
411
|
+
|
|
412
|
+
// [LAW:one-source-of-truth] The conventional variable a picker paginates against
|
|
413
|
+
// — the usable terminal width renderDsl injects each render. One name shared by
|
|
414
|
+
// the declaration (default config) and the picker's read, so they cannot drift.
|
|
415
|
+
export const TERM_COLS_VAR = "term.cols";
|
|
416
|
+
|
|
417
|
+
// [LAW:one-source-of-truth] The conventional variable per-segment hue rotation
|
|
418
|
+
// reads. hueStep is NOT a globals field (that would be a second source for a
|
|
419
|
+
// render-time value); it is a value in the store like every other render input.
|
|
420
|
+
// A config declares this variable — as a `state` var so a stepper can drive it
|
|
421
|
+
// live (session value over the declared default, the same session-over-default
|
|
422
|
+
// the theme uses), or as any kind for a fixed value. renderDsl reads it through
|
|
423
|
+
// this one name; a bounded stepper action writes the SessionState key it reads.
|
|
424
|
+
// Absent ≡ no rotation (step 0) — the degenerate case, not a special branch.
|
|
425
|
+
export const HUE_STEP_VAR = "hue.step";
|