@promptctl/cc-candybar 1.39.0 → 1.40.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/dist/index.mjs +78 -78
- package/package.json +5 -5
- package/schema/cc-candybar.schema.json +61 -1
- package/src/check.ts +14 -31
- package/src/config/default-dsl-config.ts +18 -0
- package/src/config/dsl-loader.ts +8 -1
- package/src/config/dsl-types.ts +23 -0
- package/src/config/loader/emit-schema.ts +2 -1
- package/src/config/loader/globals.ts +66 -32
- package/src/config/loader/merge.ts +6 -0
- package/src/config/presets.ts +20 -1
- package/src/daemon/render-payload.ts +98 -1
- package/src/daemon/server.ts +29 -88
- package/src/demo/dsl.ts +21 -37
- package/src/themes/policy.ts +44 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -91,9 +91,9 @@
|
|
|
91
91
|
"mobx": "^6.15.0"
|
|
92
92
|
},
|
|
93
93
|
"optionalDependencies": {
|
|
94
|
-
"@promptctl/cc-candybar-darwin-arm64": "1.
|
|
95
|
-
"@promptctl/cc-candybar-darwin-x64": "1.
|
|
96
|
-
"@promptctl/cc-candybar-linux-x64": "1.
|
|
97
|
-
"@promptctl/cc-candybar-linux-arm64": "1.
|
|
94
|
+
"@promptctl/cc-candybar-darwin-arm64": "1.40.0",
|
|
95
|
+
"@promptctl/cc-candybar-darwin-x64": "1.40.0",
|
|
96
|
+
"@promptctl/cc-candybar-linux-x64": "1.40.0",
|
|
97
|
+
"@promptctl/cc-candybar-linux-arm64": "1.40.0"
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -1648,7 +1648,7 @@
|
|
|
1648
1648
|
},
|
|
1649
1649
|
"preset": {
|
|
1650
1650
|
"not": {},
|
|
1651
|
-
"description": "not allowed
|
|
1651
|
+
"description": "not allowed here — a preset cannot select a preset"
|
|
1652
1652
|
},
|
|
1653
1653
|
"style": {
|
|
1654
1654
|
"enum": [
|
|
@@ -1686,6 +1686,66 @@
|
|
|
1686
1686
|
"additionalProperties": false
|
|
1687
1687
|
}
|
|
1688
1688
|
},
|
|
1689
|
+
"editGlobals": {
|
|
1690
|
+
"type": "object",
|
|
1691
|
+
"properties": {
|
|
1692
|
+
"default_bg": {
|
|
1693
|
+
"type": "string"
|
|
1694
|
+
},
|
|
1695
|
+
"default_fg": {
|
|
1696
|
+
"type": "string"
|
|
1697
|
+
},
|
|
1698
|
+
"default_empty_value": {
|
|
1699
|
+
"type": "string"
|
|
1700
|
+
},
|
|
1701
|
+
"default_separator": {
|
|
1702
|
+
"type": "string"
|
|
1703
|
+
},
|
|
1704
|
+
"default_truncate_marker": {
|
|
1705
|
+
"type": "string"
|
|
1706
|
+
},
|
|
1707
|
+
"palette": {
|
|
1708
|
+
"type": "string"
|
|
1709
|
+
},
|
|
1710
|
+
"look": {
|
|
1711
|
+
"type": "string"
|
|
1712
|
+
},
|
|
1713
|
+
"preset": {
|
|
1714
|
+
"not": {},
|
|
1715
|
+
"description": "not allowed here — the editGlobals fragment cannot select a preset"
|
|
1716
|
+
},
|
|
1717
|
+
"style": {
|
|
1718
|
+
"enum": [
|
|
1719
|
+
"powerline",
|
|
1720
|
+
"capsule",
|
|
1721
|
+
"plain"
|
|
1722
|
+
]
|
|
1723
|
+
},
|
|
1724
|
+
"autoWrap": {
|
|
1725
|
+
"type": "boolean"
|
|
1726
|
+
},
|
|
1727
|
+
"padding": {
|
|
1728
|
+
"type": "integer",
|
|
1729
|
+
"minimum": 0,
|
|
1730
|
+
"maximum": 16
|
|
1731
|
+
},
|
|
1732
|
+
"charset": {
|
|
1733
|
+
"enum": [
|
|
1734
|
+
"unicode",
|
|
1735
|
+
"ascii"
|
|
1736
|
+
]
|
|
1737
|
+
},
|
|
1738
|
+
"colorCompatibility": {
|
|
1739
|
+
"enum": [
|
|
1740
|
+
"truecolor",
|
|
1741
|
+
"256",
|
|
1742
|
+
"ansi",
|
|
1743
|
+
"none"
|
|
1744
|
+
]
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1747
|
+
"additionalProperties": false
|
|
1748
|
+
},
|
|
1689
1749
|
"helpers": {
|
|
1690
1750
|
"type": "object",
|
|
1691
1751
|
"additionalProperties": {
|
package/src/check.ts
CHANGED
|
@@ -31,21 +31,12 @@ import { SourceRegistry } from "./var-system/sources.js";
|
|
|
31
31
|
import { SessionState } from "./daemon/session-state.js";
|
|
32
32
|
import { registerDslConfig, renderDsl } from "./dsl/render.js";
|
|
33
33
|
import { deriveActionValidators } from "./daemon/verbs/state-validators.js";
|
|
34
|
-
import {
|
|
35
|
-
effectiveThemeName,
|
|
36
|
-
effectiveLookName,
|
|
37
|
-
lookKeyByName,
|
|
38
|
-
effectiveStripStyle,
|
|
39
|
-
effectiveAutoWrap,
|
|
40
|
-
effectivePadding,
|
|
41
|
-
} from "./themes/policy.js";
|
|
34
|
+
import { lookKeyByName } from "./themes/policy.js";
|
|
42
35
|
import { paletteForThemeName } from "./themes/palette-resolvers.js";
|
|
43
|
-
import { effectivePresetName, presetGlobals } from "./config/presets.js";
|
|
44
36
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} from "./render
|
|
48
|
-
import type { EffectiveGlobals } from "./daemon/render-payload.js";
|
|
37
|
+
resolveEffectiveGlobals,
|
|
38
|
+
type EffectiveGlobals,
|
|
39
|
+
} from "./daemon/render-payload.js";
|
|
49
40
|
|
|
50
41
|
// [LAW:no-ambient-temporal-coupling] A fixed width keeps the verdict a function
|
|
51
42
|
// of the config alone, not of whichever terminal invoked the check. Templates
|
|
@@ -302,14 +293,13 @@ function loadRegisterRender(
|
|
|
302
293
|
// globals feed every field below, the SAME order the daemon resolves in
|
|
303
294
|
// (server.ts) — so `check` renders the arrangement a fresh session actually
|
|
304
295
|
// opens in, not the config's un-presetted root.
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
preset,
|
|
296
|
+
const effective: EffectiveGlobals = resolveEffectiveGlobals(
|
|
297
|
+
config,
|
|
298
|
+
// A fresh session: no clicked theme/style/look, and edit mode off. The
|
|
299
|
+
// resolution is THE daemon's (resolveEffectiveGlobals), not a copy that
|
|
300
|
+
// agrees with it today — which is the whole reason check renders what the
|
|
301
|
+
// daemon would render rather than something adjacent.
|
|
302
|
+
() => null,
|
|
313
303
|
// [LAW:no-silent-failure] `check` validates a config file in isolation
|
|
314
304
|
// — it never reads the daemon-owned overrides file, so there is no
|
|
315
305
|
// rootOps log to be customized BY. false is the honest value for THIS
|
|
@@ -317,16 +307,8 @@ function loadRegisterRender(
|
|
|
317
307
|
// has never customized anything. A second render pass below also
|
|
318
308
|
// exercises `true`, so a `.preset.customized`-gated segment still
|
|
319
309
|
// gets checked — just not through this value.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
look: effectiveLookName(null, globals.look, config.looks),
|
|
323
|
-
style: effectiveStripStyle(null, globals.style),
|
|
324
|
-
autoWrap: effectiveAutoWrap(null, globals.autoWrap),
|
|
325
|
-
padding: effectivePadding(null, globals.padding),
|
|
326
|
-
charset: globals.charset ?? DEFAULT_CHARSET,
|
|
327
|
-
colorCompatibility:
|
|
328
|
-
globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
|
|
329
|
-
};
|
|
310
|
+
() => false,
|
|
311
|
+
);
|
|
330
312
|
// [LAW:no-silent-failure] A segment whose template THROWS while evaluating
|
|
331
313
|
// (an `{{ action }}` display-arity mismatch, a MissingFieldError from a
|
|
332
314
|
// partially-declared variable) renders as a visible ⚠ error cell — partial
|
|
@@ -352,6 +334,7 @@ function loadRegisterRender(
|
|
|
352
334
|
paletteForThemeName(payloadEffective.theme),
|
|
353
335
|
{
|
|
354
336
|
style: payloadEffective.style,
|
|
337
|
+
separator: payloadEffective.separator,
|
|
355
338
|
width: CHECK_WIDTH,
|
|
356
339
|
colorCompatibility: payloadEffective.colorCompatibility,
|
|
357
340
|
wrap: payloadEffective.autoWrap,
|
|
@@ -1475,6 +1475,24 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
1475
1475
|
},
|
|
1476
1476
|
},
|
|
1477
1477
|
|
|
1478
|
+
// [LAW:one-source-of-truth] What edit mode LOOKS like, as config a user can
|
|
1479
|
+
// retune — the whole point of candybar-settings-ui-aok.5, whose predecessor
|
|
1480
|
+
// was renderer constants. Powerline chrome exists to make adjacent segments
|
|
1481
|
+
// read as one continuous strip, which is precisely the wrong signal while a
|
|
1482
|
+
// user is trying to see where one segment ends and the next begins; `plain`
|
|
1483
|
+
// trades the caps for a visible separator between every cell.
|
|
1484
|
+
//
|
|
1485
|
+
// The separator is stated rather than left to PlainJoiner's own default: this
|
|
1486
|
+
// fragment layers over the user's globals, so a config that set
|
|
1487
|
+
// `default_separator` for its own powerline bar would otherwise carry that
|
|
1488
|
+
// choice into edit mode, where the separator is the entire affordance. " | "
|
|
1489
|
+
// (not "│") because it must survive `charset: "ascii"` — the fragment does
|
|
1490
|
+
// not, and should not, know the terminal's glyph coverage.
|
|
1491
|
+
editGlobals: {
|
|
1492
|
+
style: "plain",
|
|
1493
|
+
default_separator: " | ",
|
|
1494
|
+
},
|
|
1495
|
+
|
|
1478
1496
|
// [LAW:single-enforcer] / [LAW:one-source-of-truth] Display-formatting policy
|
|
1479
1497
|
// for the cost/token/budget family lives here as named template helpers, each
|
|
1480
1498
|
// DEFINED ONCE and called from every segment via `{{ template "name" .arg }}`
|
package/src/config/dsl-loader.ts
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
type ValidateCtx,
|
|
44
44
|
} from "./loader/validate-core.js";
|
|
45
45
|
import { mergeWithDefault } from "./loader/merge.js";
|
|
46
|
-
import { validateGlobals } from "./loader/globals.js";
|
|
46
|
+
import { validateEditGlobals, validateGlobals } from "./loader/globals.js";
|
|
47
47
|
import { validateVariables } from "./loader/variables.js";
|
|
48
48
|
import { validateSegments } from "./loader/segments.js";
|
|
49
49
|
import { synthesizeGroupDecls, validateRoot } from "./loader/layout.js";
|
|
@@ -280,6 +280,12 @@ function validateTopLevel(
|
|
|
280
280
|
// was staged from.
|
|
281
281
|
if (raw.presets !== undefined)
|
|
282
282
|
out.presets = validatePresets(ctx, raw.presets);
|
|
283
|
+
// [LAW:one-type-per-behavior] Edit mode's staged display globals — the same
|
|
284
|
+
// fragment shape a preset carries, one rung later in the precedence chain, so
|
|
285
|
+
// it runs through the same field table (validateEditGlobals) rather than a
|
|
286
|
+
// parallel schema listing which globals edit mode may set.
|
|
287
|
+
if (raw.editGlobals !== undefined)
|
|
288
|
+
out.editGlobals = validateEditGlobals(ctx, "editGlobals", raw.editGlobals);
|
|
283
289
|
if (raw.helpers !== undefined)
|
|
284
290
|
out.helpers = validateHelpers(ctx, raw.helpers);
|
|
285
291
|
// [LAW:one-source-of-truth] Group sugar synthesis runs AFTER every section
|
|
@@ -328,5 +334,6 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
328
334
|
"actions",
|
|
329
335
|
"looks",
|
|
330
336
|
"presets",
|
|
337
|
+
"editGlobals",
|
|
331
338
|
"helpers",
|
|
332
339
|
]);
|
package/src/config/dsl-types.ts
CHANGED
|
@@ -172,6 +172,9 @@ export interface RawDslConfig {
|
|
|
172
172
|
// arrangement selected per session, the exact twin of `looks` one level up
|
|
173
173
|
// (a look adapts the THEME; a preset adapts the LAYOUT + display globals).
|
|
174
174
|
readonly presets?: Readonly<Record<string, PresetDecl>>;
|
|
175
|
+
// The display globals edit mode stages while it is on — see DslConfig's own
|
|
176
|
+
// `editGlobals` for the shape, the merge, and where it sits in the chain.
|
|
177
|
+
readonly editGlobals?: Partial<Globals>;
|
|
175
178
|
// Named theme-adaptation bundles ("looks"): each is a full ThemeKey (the
|
|
176
179
|
// loader normalizes absent axes to identity at parse). Applied ON TOP of the
|
|
177
180
|
// active theme at render — a transform composing with every theme, selected
|
|
@@ -216,6 +219,26 @@ export interface DslConfig {
|
|
|
216
219
|
// `{ set: …, from: "presets" }` ranges these names; the derived click gate and
|
|
217
220
|
// the rendered options read this one map.
|
|
218
221
|
readonly presets: Readonly<Record<string, PresetDecl>>;
|
|
222
|
+
// [LAW:one-source-of-truth] The display globals edit mode stages while it is
|
|
223
|
+
// on — the `globals` half of the fragment whose `root` half edit chrome
|
|
224
|
+
// already stages (src/config/edit-chrome.ts). Merges FIELD BY FIELD with the
|
|
225
|
+
// bundled default's (like `globals` itself, not wholesale like `root`), so a
|
|
226
|
+
// user retuning the separator keeps the bundled `style: "plain"`.
|
|
227
|
+
//
|
|
228
|
+
// [LAW:types-are-the-program] `Partial<Globals>`, deliberately NOT
|
|
229
|
+
// `PresetDecl`: a preset is root + globals, and edit mode needs only the
|
|
230
|
+
// globals half. Taking the wider type to use half of it would make "an edit
|
|
231
|
+
// fragment that restages the layout" representable — a second authority over
|
|
232
|
+
// a tree edit-chrome already owns. The loader additionally rejects `preset`
|
|
233
|
+
// inside it, for the same reason a preset may not select a preset.
|
|
234
|
+
//
|
|
235
|
+
// Its rung in the precedence chain is the RIGHTMOST one (see
|
|
236
|
+
// src/config/presets.ts): it outranks even a session pick, because entering
|
|
237
|
+
// edit mode is decided later than picking a style. Nothing writes it back to
|
|
238
|
+
// SessionState or the overrides layer, which is why leaving edit mode
|
|
239
|
+
// restores the previous look with no save/restore path
|
|
240
|
+
// [LAW:dataflow-not-control-flow].
|
|
241
|
+
readonly editGlobals: Partial<Globals>;
|
|
219
242
|
// [LAW:single-enforcer] The effective helper set: a name → template-body map
|
|
220
243
|
// compiled to a defines-preamble at registerDslConfig. Empty when no config
|
|
221
244
|
// declares helpers — an absent `helpers` key merges to `{}` (same cascade as
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// cycles. Those stay SEMANTIC checks the loader carries — schema = shape, lint =
|
|
13
13
|
// meaning, the same complementary boundary `config-schema.test.ts` pins.
|
|
14
14
|
|
|
15
|
-
import { globalsJson } from "./globals.js";
|
|
15
|
+
import { editGlobalsJson, globalsJson } from "./globals.js";
|
|
16
16
|
import { variablesMapJson } from "./variables.js";
|
|
17
17
|
import { segmentsJson } from "./segments.js";
|
|
18
18
|
import { actionsJson } from "./actions.js";
|
|
@@ -50,6 +50,7 @@ export function emitConfigSchema(): JsonNode {
|
|
|
50
50
|
actions: actionsJson(),
|
|
51
51
|
looks: looksJson(),
|
|
52
52
|
presets: presetsJson(),
|
|
53
|
+
editGlobals: editGlobalsJson(),
|
|
53
54
|
helpers: { type: "object", additionalProperties: { type: "string" } },
|
|
54
55
|
},
|
|
55
56
|
definitions: {
|
|
@@ -104,43 +104,63 @@ const GLOBALS_SCHEMA: RecordSchema<Globals> = {
|
|
|
104
104
|
fields: GLOBALS_FIELDS,
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
// [LAW:one-source-of-truth] A
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// a
|
|
107
|
+
// [LAW:one-source-of-truth] A globals FRAGMENT — a delta layered over the
|
|
108
|
+
// config's own globals at render time — may not carry `preset`: which preset is
|
|
109
|
+
// active has exactly one authority (session pick over globals.preset over the
|
|
110
|
+
// floor), and a fragment re-selecting a preset would be a second one. For a
|
|
111
|
+
// preset's own fragment that second authority is also cyclic; for edit mode's
|
|
112
|
+
// it would let a look-only fragment restage the whole layout, which edit chrome
|
|
113
|
+
// already owns. Same species of bespoke, migration-pointing rejection as
|
|
111
114
|
// colorCompatibility's "auto" above, and for the same reason: an author who
|
|
112
115
|
// writes it deserves to be told WHY, not handed a bare unknown-key message.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
};
|
|
116
|
+
//
|
|
117
|
+
// [LAW:one-type-per-behavior] Both fragments reject the field identically and
|
|
118
|
+
// differ only in the SUBJECT a diagnostic names, so this is one spec taking
|
|
119
|
+
// that noun as data — never two specs that could drift in what they reject.
|
|
120
|
+
function nestedPresetSpec(subject: string): FieldSpec<string> {
|
|
121
|
+
return {
|
|
122
|
+
required: false,
|
|
123
|
+
// Always-fail: JSON Schema's `not: {}` matches nothing, so an editor flags
|
|
124
|
+
// the key at the same moment the validator does.
|
|
125
|
+
json: {
|
|
126
|
+
not: {},
|
|
127
|
+
description: `not allowed here — ${subject} cannot select a preset`,
|
|
128
|
+
},
|
|
129
|
+
parse: (ctx, path, field, raw) => {
|
|
130
|
+
if (raw[field] !== undefined) {
|
|
131
|
+
ctx.issues.push({
|
|
132
|
+
path: `${path}.${field}`,
|
|
133
|
+
message:
|
|
134
|
+
`${path}.${field}: ${subject} cannot select a preset. Which preset is active is ` +
|
|
135
|
+
`resolved once, as session pick over globals.preset over "default"; a fragment ` +
|
|
136
|
+
`naming another would be a second authority over that. ` +
|
|
137
|
+
`Set the default arrangement in the top-level globals.preset instead.`,
|
|
138
|
+
line: findKeyLine(ctx.source, [...path.split("."), field]),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
137
145
|
|
|
138
|
-
// [LAW:one-source-of-truth]
|
|
139
|
-
// table with exactly one field swapped for its rejection — not a
|
|
140
|
-
// subset that a future globals field could be forgotten from.
|
|
146
|
+
// [LAW:one-source-of-truth] Each fragment-scoped globals schema is the SAME
|
|
147
|
+
// field table with exactly one field swapped for its rejection — not a
|
|
148
|
+
// hand-listed subset that a future globals field could be forgotten from.
|
|
141
149
|
const PRESET_GLOBALS_SCHEMA: RecordSchema<Globals> = {
|
|
142
150
|
noun: "preset globals key",
|
|
143
|
-
fields: { ...GLOBALS_FIELDS, preset: nestedPresetSpec },
|
|
151
|
+
fields: { ...GLOBALS_FIELDS, preset: nestedPresetSpec("a preset") },
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// [LAW:one-type-per-behavior] Edit mode's staged globals are the same shape one
|
|
155
|
+
// rung later in the precedence chain, so they reuse the same table rather than
|
|
156
|
+
// declaring which fields edit mode "supports" — a field added to Globals is
|
|
157
|
+
// edit-settable the same day, with no edit here.
|
|
158
|
+
const EDIT_GLOBALS_SCHEMA: RecordSchema<Globals> = {
|
|
159
|
+
noun: "editGlobals key",
|
|
160
|
+
fields: {
|
|
161
|
+
...GLOBALS_FIELDS,
|
|
162
|
+
preset: nestedPresetSpec("the editGlobals fragment"),
|
|
163
|
+
},
|
|
144
164
|
};
|
|
145
165
|
|
|
146
166
|
// An absent globals block is the empty default (no issue); a non-object is a
|
|
@@ -177,6 +197,20 @@ export function presetGlobalsJson(): JsonNode {
|
|
|
177
197
|
return recordJson(PRESET_GLOBALS_SCHEMA);
|
|
178
198
|
}
|
|
179
199
|
|
|
200
|
+
// Edit mode's twin of the two above: same interpreter, same field table, the
|
|
201
|
+
// schema whose `preset` rejection names the editGlobals fragment.
|
|
202
|
+
export function validateEditGlobals(
|
|
203
|
+
ctx: ValidateCtx,
|
|
204
|
+
path: string,
|
|
205
|
+
raw: unknown,
|
|
206
|
+
): Globals {
|
|
207
|
+
return record(ctx, EDIT_GLOBALS_SCHEMA, path, raw) ?? {};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function editGlobalsJson(): JsonNode {
|
|
211
|
+
return recordJson(EDIT_GLOBALS_SCHEMA);
|
|
212
|
+
}
|
|
213
|
+
|
|
180
214
|
// [LAW:one-source-of-truth] THE membership check for "is this a real Globals
|
|
181
215
|
// field" — derived from GLOBALS_SCHEMA.fields, the same declaration
|
|
182
216
|
// validateGlobals/globalsJson interpret, so a `persist`/`reset` action's
|
|
@@ -53,6 +53,12 @@ export function mergeWithDefault(
|
|
|
53
53
|
// (incl. the "default" empty-fragment floor effectivePresetName collapses
|
|
54
54
|
// to) survives every merge by construction, exactly as looks' "none" does.
|
|
55
55
|
presets: { ...dflt.presets, ...(raw.presets ?? {}) },
|
|
56
|
+
// [LAW:one-source-of-truth] editGlobals merges FIELD by field — the
|
|
57
|
+
// `globals` cascade above, not the by-name cascades around it, because it
|
|
58
|
+
// IS a globals fragment: a user retuning edit mode's separator says nothing
|
|
59
|
+
// about its `style`, exactly as a user setting `globals.padding` says
|
|
60
|
+
// nothing about `globals.charset`.
|
|
61
|
+
editGlobals: { ...dflt.editGlobals, ...(raw.editGlobals ?? {}) },
|
|
56
62
|
// [LAW:one-source-of-truth] helpers merge by name, same cascade — a user
|
|
57
63
|
// overrides one formatter helper by re-declaring its name; the rest inherit
|
|
58
64
|
// from the bundled default.
|
package/src/config/presets.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// place it is written down:
|
|
16
16
|
//
|
|
17
17
|
// bundled default < user config file < persisted overrides
|
|
18
|
-
// < ACTIVE PRESET < session pick
|
|
18
|
+
// < ACTIVE PRESET < session pick < EDIT MODE
|
|
19
19
|
//
|
|
20
20
|
// The preset's position is forced by its lifetime, not chosen. Everything to
|
|
21
21
|
// its left is resolved once per RenderCache entry (an entry serves many
|
|
@@ -26,6 +26,18 @@
|
|
|
26
26
|
// "compact" arrangement must actually change padding, even for a user who once
|
|
27
27
|
// persisted a padding they liked) while a session's own click still wins over
|
|
28
28
|
// the preset (a click is later still).
|
|
29
|
+
//
|
|
30
|
+
// The same rule places the last rung (candybar-settings-ui-aok.5): edit mode's
|
|
31
|
+
// `editGlobals` fragment is decided later than ANY session pick — a user picks
|
|
32
|
+
// a style, and only afterwards enters edit mode — so it is the new rightmost
|
|
33
|
+
// layer, and a session pick of "capsule" cannot survive into a mode whose whole
|
|
34
|
+
// job is to stop segments reading as one continuous strip. It differs from
|
|
35
|
+
// every rung to its left in LIFETIME rather than in kind: nothing writes it to
|
|
36
|
+
// SessionState or the overrides layer, so leaving edit mode restores the
|
|
37
|
+
// previous look with no save/restore path — the session's own pick was never
|
|
38
|
+
// overwritten, only out-ranked [LAW:dataflow-not-control-flow]. The rung itself
|
|
39
|
+
// is the `staged` parameter of effectiveGlobal (themes/policy.ts); this comment
|
|
40
|
+
// is the ONE place the order is written down.
|
|
29
41
|
|
|
30
42
|
// [LAW:one-way-deps] Type-only, so nothing is emitted and option-domain.ts (a
|
|
31
43
|
// leaf that deliberately never imports dsl-types.ts) can import PRESET_NAMES
|
|
@@ -87,7 +99,14 @@ export function effectivePresetName(
|
|
|
87
99
|
globalsPreset: string | undefined,
|
|
88
100
|
declaredPresets: Readonly<Record<string, PresetDecl>>,
|
|
89
101
|
): string {
|
|
102
|
+
// [LAW:types-are-the-program] No staged rung, and the absence is enforced
|
|
103
|
+
// rather than assumed: every globals fragment that could stage one — a
|
|
104
|
+
// preset's own `globals`, edit mode's `editGlobals` — has `preset` swapped for
|
|
105
|
+
// a rejection in its schema (loader/globals.ts), so "a fragment selected a
|
|
106
|
+
// preset" is unrepresentable and there is nothing here to resolve against.
|
|
107
|
+
// Which preset is active keeps exactly one authority.
|
|
90
108
|
return effectiveMemberName(
|
|
109
|
+
undefined,
|
|
91
110
|
sessionPreset,
|
|
92
111
|
globalsPreset,
|
|
93
112
|
PRESET_FLOOR,
|
|
@@ -20,7 +20,20 @@ import path from "node:path";
|
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
import type { ClaudeHookData } from "../utils/claude.js";
|
|
22
22
|
import type { ClientHints } from "./protocol.js";
|
|
23
|
-
import type { DslConfig, VariableDecl } from "../config/dsl-types.js";
|
|
23
|
+
import type { DslConfig, Globals, VariableDecl } from "../config/dsl-types.js";
|
|
24
|
+
import { effectivePresetName, presetGlobals } from "../config/presets.js";
|
|
25
|
+
import { EDIT_MODE_KEY, EDIT_MODE_OPEN } from "../config/loader/edit-mode.js";
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_CHARSET,
|
|
28
|
+
DEFAULT_COLOR_COMPATIBILITY,
|
|
29
|
+
} from "../render/strip.js";
|
|
30
|
+
import {
|
|
31
|
+
effectiveAutoWrap,
|
|
32
|
+
effectiveLookName,
|
|
33
|
+
effectivePadding,
|
|
34
|
+
effectiveStripStyle,
|
|
35
|
+
effectiveThemeName,
|
|
36
|
+
} from "../themes/policy.js";
|
|
24
37
|
import { walkNodes } from "../config/dsl-types.js";
|
|
25
38
|
import { extractTemplateRefs } from "../config/dsl-loader.js";
|
|
26
39
|
import type { GitInfo, GitInfoOptions } from "../segments/git.js";
|
|
@@ -75,12 +88,96 @@ export interface EffectiveGlobals {
|
|
|
75
88
|
// own comment.
|
|
76
89
|
readonly presetCustomized: boolean;
|
|
77
90
|
readonly style: StripStyle;
|
|
91
|
+
// [LAW:one-source-of-truth] The cell separator `plain` renders between
|
|
92
|
+
// segments (globals.default_separator). `string | undefined`, not a resolved
|
|
93
|
+
// string, precisely because its floor is NOT ours: PlainJoiner owns " | " and
|
|
94
|
+
// pickJoiner already reads undefined as "use the class default", so naming a
|
|
95
|
+
// floor here would be a second copy of a constant that lives in rich-js.
|
|
96
|
+
// Like charset it has no SessionState half — the config global (as staged by
|
|
97
|
+
// whatever fragment is on top) is its whole resolution.
|
|
98
|
+
readonly separator: string | undefined;
|
|
78
99
|
readonly charset: Charset;
|
|
79
100
|
readonly colorCompatibility: ColorCompatibility;
|
|
80
101
|
readonly autoWrap: boolean;
|
|
81
102
|
readonly padding: number;
|
|
82
103
|
}
|
|
83
104
|
|
|
105
|
+
// [LAW:one-source-of-truth] THE resolution — one function, so the precedence
|
|
106
|
+
// chain has one implementation rather than one per caller. It previously stood
|
|
107
|
+
// as two structurally identical struct literals (the daemon's, in server.ts,
|
|
108
|
+
// and `cc-candybar check`'s), which is two clocks: the check command's job is
|
|
109
|
+
// to render what the daemon would render, and a rung added to one copy is a
|
|
110
|
+
// rung silently missing from the other. The callers differ only in WHERE a
|
|
111
|
+
// session value comes from and whether an overrides log exists to be customized
|
|
112
|
+
// by, so both arrive as parameters and nothing else forks
|
|
113
|
+
// [LAW:dataflow-not-control-flow].
|
|
114
|
+
//
|
|
115
|
+
// `sessionPick` is the reader for one SessionState key. `check` passes a
|
|
116
|
+
// function returning null for every key — a fresh session that has never
|
|
117
|
+
// clicked — rather than a null store, so "no session" travels as a VALUE
|
|
118
|
+
// through the same chain a real session travels [LAW:no-mode-explosion].
|
|
119
|
+
export function resolveEffectiveGlobals(
|
|
120
|
+
config: DslConfig,
|
|
121
|
+
sessionPick: (key: string) => string | null,
|
|
122
|
+
presetCustomized: (preset: string) => boolean,
|
|
123
|
+
): EffectiveGlobals {
|
|
124
|
+
// The preset resolves FIRST: every field below reads globals, and which
|
|
125
|
+
// globals is exactly what the preset decides.
|
|
126
|
+
const preset = effectivePresetName(
|
|
127
|
+
sessionPick("preset"),
|
|
128
|
+
config.globals.preset,
|
|
129
|
+
config.presets,
|
|
130
|
+
);
|
|
131
|
+
const globals = presetGlobals(config, preset);
|
|
132
|
+
// [LAW:dataflow-not-control-flow] The staged fragment is a VALUE, and "edit
|
|
133
|
+
// mode is off" is the EMPTY value — the identity fragment, exactly as
|
|
134
|
+
// PRESET_FLOOR's is. Every field below is resolved by the same expression
|
|
135
|
+
// whether or not edit mode is on; only the contents of `staged` differ. This
|
|
136
|
+
// is the whole of "no render-walk branch on edit mode": there is no branch
|
|
137
|
+
// here either, so there is none to leak downstream.
|
|
138
|
+
const staged: Partial<Globals> =
|
|
139
|
+
sessionPick(EDIT_MODE_KEY) === EDIT_MODE_OPEN ? config.editGlobals : {};
|
|
140
|
+
return {
|
|
141
|
+
preset,
|
|
142
|
+
presetCustomized: presetCustomized(preset),
|
|
143
|
+
theme: effectiveThemeName(
|
|
144
|
+
staged.palette,
|
|
145
|
+
sessionPick("theme"),
|
|
146
|
+
globals.palette,
|
|
147
|
+
),
|
|
148
|
+
look: effectiveLookName(
|
|
149
|
+
staged.look,
|
|
150
|
+
sessionPick("look"),
|
|
151
|
+
globals.look,
|
|
152
|
+
config.looks,
|
|
153
|
+
),
|
|
154
|
+
style: effectiveStripStyle(
|
|
155
|
+
staged.style,
|
|
156
|
+
sessionPick("style"),
|
|
157
|
+
globals.style,
|
|
158
|
+
),
|
|
159
|
+
// [LAW:one-source-of-truth] The fields with no SessionState half resolve as
|
|
160
|
+
// `staged ?? config ?? floor` — the same chain minus the rung they do not
|
|
161
|
+
// have, spelled with the same `??` rather than a second mechanism.
|
|
162
|
+
separator: staged.default_separator ?? globals.default_separator,
|
|
163
|
+
autoWrap: effectiveAutoWrap(
|
|
164
|
+
staged.autoWrap,
|
|
165
|
+
sessionPick("autoWrap"),
|
|
166
|
+
globals.autoWrap,
|
|
167
|
+
),
|
|
168
|
+
padding: effectivePadding(
|
|
169
|
+
staged.padding,
|
|
170
|
+
sessionPick("padding"),
|
|
171
|
+
globals.padding,
|
|
172
|
+
),
|
|
173
|
+
charset: staged.charset ?? globals.charset ?? DEFAULT_CHARSET,
|
|
174
|
+
colorCompatibility:
|
|
175
|
+
staged.colorCompatibility ??
|
|
176
|
+
globals.colorCompatibility ??
|
|
177
|
+
DEFAULT_COLOR_COMPATIBILITY,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
84
181
|
// ─── Augmented payload shape ─────────────────────────────────────────────────
|
|
85
182
|
|
|
86
183
|
// [LAW:types-are-the-program] The RenderPayload extends ClaudeHookData with
|