@promptctl/cc-candybar 1.42.1 → 1.43.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 +72 -71
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/currency.ts +0 -197
- package/src/install/index.ts +0 -557
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- package/src/version.ts +0 -17
|
@@ -1,559 +0,0 @@
|
|
|
1
|
-
// [LAW:one-source-of-truth] brandon-layout-edit-2gc.3's CHROME half — the
|
|
2
|
-
// LOWERING that turns "edit mode is a session toggle" into "each row's
|
|
3
|
-
// segments render interleaved with +/- affordances" without a render-walk
|
|
4
|
-
// branch [LAW:dataflow-not-control-flow]. Follows the SAME move `kind:
|
|
5
|
-
// "group"` sugar makes (src/config/loader/layout.ts): one pass produces a NEW
|
|
6
|
-
// tree with synthesized nodes spliced in, each gated by an ordinary `when` —
|
|
7
|
-
// the walk that renders it learns nothing new. The difference from group
|
|
8
|
-
// sugar is WHEN this can run: a group is authored data, lowered per file
|
|
9
|
-
// before merge; edit chrome is DERIVED from which segments are actually in
|
|
10
|
-
// the tree, which is only known after merge, preset-root resolution, and
|
|
11
|
-
// rootOps replay. So this runs from validateConfig, on the fully resolved
|
|
12
|
-
// config each declared preset stages — see synthesizeEditChrome below.
|
|
13
|
-
//
|
|
14
|
-
// [LAW:single-enforcer] The +/- affordances reuse EXISTING primitives
|
|
15
|
-
// wholesale rather than inventing parallel ones: `-` is an ordinary
|
|
16
|
-
// `{ persist, removeSegment }` action behind `{{ action }}` (2gc.1); `+` is an
|
|
17
|
-
// ordinary `{ persist, insertSegmentFrom }` action behind `{{ menu }}`
|
|
18
|
-
// (2gc.3's new arm — see action.ts), synthesized by calling the SAME pure
|
|
19
|
-
// functions `{{ menu }}`'s own load-time synthesis calls
|
|
20
|
-
// (menu-keys.ts/disclosure.ts) so a synthesized menu and a hand-authored one
|
|
21
|
-
// are indistinguishable at render. Nothing here is a new render concept.
|
|
22
|
-
|
|
23
|
-
import type { ActionDecl as ActionDeclType, OptionDomain } from "./action.js";
|
|
24
|
-
import type {
|
|
25
|
-
ContainerNode,
|
|
26
|
-
DslConfig,
|
|
27
|
-
LayoutNode,
|
|
28
|
-
PresetDecl,
|
|
29
|
-
SegmentDecl,
|
|
30
|
-
SegmentNode,
|
|
31
|
-
VariableDecl,
|
|
32
|
-
} from "./dsl-types.js";
|
|
33
|
-
import { collectSegmentNames } from "./layout-ops.js";
|
|
34
|
-
import { presetByName, presetNames, presetRoot } from "./presets.js";
|
|
35
|
-
import { presetRootOpsKey } from "./loader/persist-target.js";
|
|
36
|
-
import { ident } from "./ident.js";
|
|
37
|
-
import {
|
|
38
|
-
EDIT_MODE_GATE,
|
|
39
|
-
EDIT_MODE_REF,
|
|
40
|
-
EDIT_NS,
|
|
41
|
-
EDIT_TOGGLE_ACTION,
|
|
42
|
-
} from "./loader/edit-mode.js";
|
|
43
|
-
import { declareHelp, type HelpDisclosure } from "./help.js";
|
|
44
|
-
import { EDIT_MODE_HELP } from "../help-text.js";
|
|
45
|
-
import { GROUP_NS } from "./loader/layout.js";
|
|
46
|
-
import { SETTINGS_NS } from "./settings-menu.js";
|
|
47
|
-
import {
|
|
48
|
-
menuActionName,
|
|
49
|
-
menuMember,
|
|
50
|
-
menuPageKey,
|
|
51
|
-
menuStateKey,
|
|
52
|
-
MENU_NS,
|
|
53
|
-
} from "./menu-keys.js";
|
|
54
|
-
import {
|
|
55
|
-
DISCLOSURE_CLOSED,
|
|
56
|
-
DISCLOSURE_GLYPH_CLOSE,
|
|
57
|
-
escapeTemplateLiteral,
|
|
58
|
-
disclosureCycleAction,
|
|
59
|
-
disclosureStateVar,
|
|
60
|
-
} from "./disclosure.js";
|
|
61
|
-
|
|
62
|
-
// [LAW:dataflow-not-control-flow] brandon-layout-edit-2gc.5's diagnostic gate
|
|
63
|
-
// — read the SAME way EDIT_MODE_GATE is: a bare boolean input var, false
|
|
64
|
-
// only on the literal text "false" (evaluateWhen's documented contract).
|
|
65
|
-
// `.preset.customized` is a per-render payload fact (presetIsCustomized over
|
|
66
|
-
// entry.state.presetRootOps for whichever preset is ACTIVE), not config-time
|
|
67
|
-
// knowledge, so the banner below is spliced UNCONDITIONALLY for every
|
|
68
|
-
// preset — same shape, every reload — and this predicate is what decides
|
|
69
|
-
// whether it's visible, never a branch in this synthesis pass.
|
|
70
|
-
// [LAW:one-source-of-truth] Exported: test/helpers/ambient-chrome.ts filters this
|
|
71
|
-
// ensured name out of "what did the AUTHOR declare" assertions and must read the
|
|
72
|
-
// same string, never a second copy that a rename here would leave behind.
|
|
73
|
-
export const PRESET_CUSTOMIZED_VAR = "preset.customized";
|
|
74
|
-
const PRESET_CUSTOMIZED_GATE = `{{ .${PRESET_CUSTOMIZED_VAR} }}`;
|
|
75
|
-
|
|
76
|
-
// [LAW:one-source-of-truth] group/menu-synthesized segments (`groups.`/
|
|
77
|
-
// `menus.`) and edit mode's own trigger/chrome (`edit.`) are structural —
|
|
78
|
-
// removing one via `-` would strand its sibling artifacts (a toggle segment
|
|
79
|
-
// with no body, a menu with no host), and offering one back via `+` would
|
|
80
|
-
// insert a bare ref with none of the synthesis that made it work. Ordinary
|
|
81
|
-
// content segments only.
|
|
82
|
-
function isChromeExempt(name: string): boolean {
|
|
83
|
-
return (
|
|
84
|
-
name.startsWith(EDIT_NS) ||
|
|
85
|
-
name.startsWith(MENU_NS) ||
|
|
86
|
-
name.startsWith(GROUP_NS) ||
|
|
87
|
-
// The global settings menu is the entry point edit mode is REACHED from
|
|
88
|
-
// (candybar-settings-ui-aok.1) — offering a `-` beside it would let one
|
|
89
|
-
// click delete the door back in, the self-lockout the `toolbar` trigger's
|
|
90
|
-
// placement was chosen to avoid. Structural, like the three above it.
|
|
91
|
-
name.startsWith(SETTINGS_NS)
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// [LAW:one-source-of-truth] Every synthesized decl this pass produces, keyed
|
|
96
|
-
// by its final name — one accumulator threaded through every preset's splice
|
|
97
|
-
// so cross-preset names (disambiguated by `presetIdent`) can never collide.
|
|
98
|
-
interface ChromeArtifacts {
|
|
99
|
-
readonly variables: Record<string, VariableDecl>;
|
|
100
|
-
// [LAW:no-silent-failure] Declarations this synthesis DEPENDS on rather than
|
|
101
|
-
// OWNS: merged UNDER the config so a user's own declaration of the same name
|
|
102
|
-
// wins, unlike `variables` above, which lives in a reserved namespace no user
|
|
103
|
-
// may write and therefore merges over.
|
|
104
|
-
readonly ensured: Record<string, VariableDecl>;
|
|
105
|
-
readonly actions: Record<string, ActionDeclType>;
|
|
106
|
-
readonly segments: Record<string, SegmentDecl>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// [LAW:one-source-of-truth] The domain name a preset's `+` pickers range —
|
|
110
|
-
// computed once per preset (declared segments minus the ones already present
|
|
111
|
-
// in ITS current tree) and consumed two ways: here (by name, for every
|
|
112
|
-
// insertSegmentFrom action this preset's splice synthesizes) and by
|
|
113
|
-
// registerDslConfig/deriveConfigActionValidators (which call
|
|
114
|
-
// `addableSegmentDomains` directly to populate `perConfigDomains` before
|
|
115
|
-
// resolving `from`). Both read the SAME string shape so a synthesized
|
|
116
|
-
// action's domain name always resolves.
|
|
117
|
-
export function addableDomainName(presetName: string): string {
|
|
118
|
-
return `${EDIT_NS}addable.${presetName}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// [LAW:one-source-of-truth] THE per-preset "what can `+` offer here" set:
|
|
122
|
-
// every declared, non-exempt segment name minus the ones already present
|
|
123
|
-
// anywhere in that preset's CURRENT (merged, rootOps-replayed) tree. Exported
|
|
124
|
-
// so render.ts's registerDslConfig and config-validators.ts's
|
|
125
|
-
// deriveConfigActionValidators — the two sites that resolve `from` domains —
|
|
126
|
-
// merge this into `perConfigDomainsFor`'s map without each re-deriving it
|
|
127
|
-
// [LAW:locality-or-seam]; option-domain.ts itself stays untouched (its
|
|
128
|
-
// `perConfigDomainsFor` deliberately never imports dsl-types.ts — see that
|
|
129
|
-
// file's own header — so a third per-preset domain merges at the two call
|
|
130
|
-
// sites instead of inside it).
|
|
131
|
-
export function addableSegmentDomains(
|
|
132
|
-
config: DslConfig,
|
|
133
|
-
): ReadonlyMap<string, readonly string[]> {
|
|
134
|
-
const declared = Object.keys(config.segments).filter(
|
|
135
|
-
(n) => !isChromeExempt(n),
|
|
136
|
-
);
|
|
137
|
-
const domains = new Map<string, readonly string[]>();
|
|
138
|
-
for (const name of presetNames(config.presets)) {
|
|
139
|
-
const { node } = presetRoot(config, name);
|
|
140
|
-
const present = collectSegmentNames(node);
|
|
141
|
-
domains.set(
|
|
142
|
-
addableDomainName(name),
|
|
143
|
-
declared.filter((n) => !present.has(n)),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
return domains;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Synthesize the `-` affordance for one segment instance: a literal
|
|
150
|
-
// `removeSegment` action plus the segment that hosts its `{{ action }}`.
|
|
151
|
-
function removeChrome(
|
|
152
|
-
presetIdent: string,
|
|
153
|
-
rootOpsKey: string,
|
|
154
|
-
segName: string,
|
|
155
|
-
artifacts: ChromeArtifacts,
|
|
156
|
-
): SegmentNode {
|
|
157
|
-
const actionName = `${EDIT_NS}${presetIdent}.remove.${ident(segName)}`;
|
|
158
|
-
const chromeSegName = `${EDIT_NS}${presetIdent}.removeSeg.${ident(segName)}`;
|
|
159
|
-
artifacts.actions[actionName] = {
|
|
160
|
-
persist: rootOpsKey,
|
|
161
|
-
removeSegment: segName,
|
|
162
|
-
};
|
|
163
|
-
artifacts.segments[chromeSegName] = {
|
|
164
|
-
template: `{{ action "${actionName}" "-" }}`,
|
|
165
|
-
when: EDIT_MODE_GATE,
|
|
166
|
-
};
|
|
167
|
-
return { kind: "segment", name: chromeSegName };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Synthesize the `+` affordance for one gap: an `insertSegmentFrom` action
|
|
171
|
-
// over this preset's addable domain, plus a segment hosting `{{ menu }}` over
|
|
172
|
-
// it. The menu's own disclosure (open state, page cursor, toggle action) is
|
|
173
|
-
// synthesized here by calling the SAME pure functions menu-synth.ts's
|
|
174
|
-
// file-parse-time pass calls — this pass runs too late to piggyback on that
|
|
175
|
-
// pass directly (it needs post-merge/post-rootOps data menu-synth.ts's
|
|
176
|
-
// per-file timing does not have), so parity is achieved by sharing the
|
|
177
|
-
// functions, not by re-deriving the shape.
|
|
178
|
-
function insertChrome(
|
|
179
|
-
presetIdent: string,
|
|
180
|
-
rootOpsKey: string,
|
|
181
|
-
posIdent: string,
|
|
182
|
-
domainName: OptionDomain,
|
|
183
|
-
anchor: string,
|
|
184
|
-
relation: "before" | "after",
|
|
185
|
-
artifacts: ChromeArtifacts,
|
|
186
|
-
): SegmentNode {
|
|
187
|
-
const applyName = `${EDIT_NS}${presetIdent}.insert.${posIdent}`;
|
|
188
|
-
const chromeSegName = `${EDIT_NS}${presetIdent}.insertSeg.${posIdent}`;
|
|
189
|
-
artifacts.actions[applyName] = {
|
|
190
|
-
persist: rootOpsKey,
|
|
191
|
-
insertSegmentFrom: domainName,
|
|
192
|
-
anchor,
|
|
193
|
-
relation,
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const member = menuMember(applyName);
|
|
197
|
-
const stateKey = menuStateKey(chromeSegName, applyName, undefined);
|
|
198
|
-
const pageKey = menuPageKey(stateKey);
|
|
199
|
-
const identity = menuActionName(stateKey, member);
|
|
200
|
-
artifacts.variables[stateKey] = disclosureStateVar(
|
|
201
|
-
stateKey,
|
|
202
|
-
DISCLOSURE_CLOSED,
|
|
203
|
-
);
|
|
204
|
-
artifacts.variables[pageKey] = { kind: "state", key: pageKey, default: "0" };
|
|
205
|
-
artifacts.actions[identity] = disclosureCycleAction(stateKey, member);
|
|
206
|
-
artifacts.actions[pageKey] = { set: pageKey, int: true };
|
|
207
|
-
|
|
208
|
-
// The `+` IS the trigger — no appended arrow (candybar-settings-ui-aok.4).
|
|
209
|
-
// Beside a `-` that means something else entirely, a ▸ read as part of the
|
|
210
|
-
// affordance rather than as a disclosure hint, so the trigger names the ACTION
|
|
211
|
-
// its click performs instead: `+` inserts here, `✕` closes what `+` opened —
|
|
212
|
-
// the same glyph, and the same effect, as the body's own close cell.
|
|
213
|
-
//
|
|
214
|
-
// [LAW:no-silent-failure] It is deliberately NOT one static display. A preset
|
|
215
|
-
// has N insertion points whose rendered rows are byte-identical, and their
|
|
216
|
-
// dropped bodies are identical too — so with no per-state display, an open `+`
|
|
217
|
-
// is indistinguishable from the two beside it and the bar silently stops
|
|
218
|
-
// answering "which one did I open". The tint that marks other open menus
|
|
219
|
-
// (node-registry's `drops.length > 0`) cannot answer it either: this segment
|
|
220
|
-
// declares no bg, so there is nothing to tint.
|
|
221
|
-
artifacts.segments[chromeSegName] = {
|
|
222
|
-
template: `{{ menu "${applyName}" "+" "${DISCLOSURE_GLYPH_CLOSE}" }}`,
|
|
223
|
-
when: EDIT_MODE_GATE,
|
|
224
|
-
};
|
|
225
|
-
return { kind: "segment", name: chromeSegName };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// [LAW:dataflow-not-control-flow] One recursive splice: a container's
|
|
229
|
-
// non-exempt segment children get a `+` before and a `-` after (so N
|
|
230
|
-
// consecutive segments read `+ [seg1 -] + [seg2 -] + [seg3 -] +` — N+1 insert
|
|
231
|
-
// points, N remove points); a container child recurses; an exempt segment
|
|
232
|
-
// (a group toggle, a menu host, edit mode's own chrome) passes through
|
|
233
|
-
// untouched. `posCounter` is threaded by reference so position identifiers
|
|
234
|
-
// stay unique across the WHOLE preset tree, not just one container.
|
|
235
|
-
function spliceContainer(
|
|
236
|
-
node: ContainerNode,
|
|
237
|
-
presetIdent: string,
|
|
238
|
-
rootOpsKey: string,
|
|
239
|
-
domainName: OptionDomain,
|
|
240
|
-
artifacts: ChromeArtifacts,
|
|
241
|
-
posCounter: { n: number },
|
|
242
|
-
): ContainerNode {
|
|
243
|
-
const children: LayoutNode[] = [];
|
|
244
|
-
// [LAW:one-source-of-truth] The trailing `+`'s position is "after the last
|
|
245
|
-
// CONTENT segment", not "after the last child". Those coincided until a
|
|
246
|
-
// synthesis started appending exempt chrome (the global settings menu,
|
|
247
|
-
// candybar-settings-ui-aok.1) to a row's end, at which point reading the last
|
|
248
|
-
// child silently dropped the row's final insert point — N segments offering
|
|
249
|
-
// only N insert points instead of N+1.
|
|
250
|
-
const lastContent = node.children.reduce(
|
|
251
|
-
(idx, child, i) =>
|
|
252
|
-
child.kind === "segment" && !isChromeExempt(child.name) ? i : idx,
|
|
253
|
-
-1,
|
|
254
|
-
);
|
|
255
|
-
for (const [i, child] of node.children.entries()) {
|
|
256
|
-
if (child.kind === "container") {
|
|
257
|
-
children.push(
|
|
258
|
-
spliceContainer(
|
|
259
|
-
child,
|
|
260
|
-
presetIdent,
|
|
261
|
-
rootOpsKey,
|
|
262
|
-
domainName,
|
|
263
|
-
artifacts,
|
|
264
|
-
posCounter,
|
|
265
|
-
),
|
|
266
|
-
);
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
if (isChromeExempt(child.name)) {
|
|
270
|
-
children.push(child);
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
children.push(
|
|
274
|
-
insertChrome(
|
|
275
|
-
presetIdent,
|
|
276
|
-
rootOpsKey,
|
|
277
|
-
String(posCounter.n++),
|
|
278
|
-
domainName,
|
|
279
|
-
child.name,
|
|
280
|
-
"before",
|
|
281
|
-
artifacts,
|
|
282
|
-
),
|
|
283
|
-
);
|
|
284
|
-
children.push(child);
|
|
285
|
-
children.push(removeChrome(presetIdent, rootOpsKey, child.name, artifacts));
|
|
286
|
-
if (i === lastContent) {
|
|
287
|
-
children.push(
|
|
288
|
-
insertChrome(
|
|
289
|
-
presetIdent,
|
|
290
|
-
rootOpsKey,
|
|
291
|
-
String(posCounter.n++),
|
|
292
|
-
domainName,
|
|
293
|
-
child.name,
|
|
294
|
-
"after",
|
|
295
|
-
artifacts,
|
|
296
|
-
),
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return { ...node, children };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// brandon-layout-edit-2gc.5's other per-preset affordance: a `+`/`-` sibling
|
|
304
|
-
// that isn't about ONE gap but about the preset's rootOps log as a whole —
|
|
305
|
-
// synthesized the SAME way (one reset action targeting this preset's exact
|
|
306
|
-
// `persist` key, one segment hosting `{{ action }}`), UNCONDITIONALLY, with
|
|
307
|
-
// visibility carried entirely by PRESET_CUSTOMIZED_GATE
|
|
308
|
-
// [LAW:dataflow-not-control-flow]. It gets its own ROW (not a slot in the
|
|
309
|
-
// row-interleaved chrome spliceContainer builds) because it is not bound to any
|
|
310
|
-
// one segment gap — it is a fact about the whole tree — visible or not by the
|
|
311
|
-
// SAME `when` every other synthesized affordance here already uses.
|
|
312
|
-
//
|
|
313
|
-
// candybar-settings-ui-aok.6 hangs edit mode's `(?)` off the same content — its
|
|
314
|
-
// BODY is a per-preset row on the same footing as the banner, so this function
|
|
315
|
-
// now brackets the content rather than only preceding it, which is what its name
|
|
316
|
-
// says and why it is no longer "prepend". Its TRIGGER is not a row; see
|
|
317
|
-
// withTrailingCell.
|
|
318
|
-
function wrapWithPresetRows(
|
|
319
|
-
splicedRoot: LayoutNode,
|
|
320
|
-
presetName: string,
|
|
321
|
-
presetIdent: string,
|
|
322
|
-
rootOpsKey: string,
|
|
323
|
-
artifacts: ChromeArtifacts,
|
|
324
|
-
help: HelpDisclosure,
|
|
325
|
-
): LayoutNode {
|
|
326
|
-
const actionName = `${EDIT_NS}${presetIdent}.resetLayout`;
|
|
327
|
-
const chromeSegName = `${EDIT_NS}${presetIdent}.customized`;
|
|
328
|
-
// [LAW:no-silent-failure] `reset` clears `rootOpsKey` outright, restoring
|
|
329
|
-
// presetRoot's own fallback (the config's literal, hand-authored root) on
|
|
330
|
-
// the next reload — the exact undo the ticket's guardrail asked for.
|
|
331
|
-
// `rootOpsKey` is gated the SAME way every other `presets.<name>.rootOps`
|
|
332
|
-
// target is (deriveConfigActionValidators), and is ALWAYS a registered
|
|
333
|
-
// key — config-validators.ts's presetRootOpsContributions registers it
|
|
334
|
-
// for every declared preset UNCONDITIONALLY, specifically so a preset
|
|
335
|
-
// edited down to zero non-exempt segments (no removeChrome/insertChrome
|
|
336
|
-
// persist actions left to register it) doesn't orphan this exact click.
|
|
337
|
-
artifacts.actions[actionName] = { reset: rootOpsKey };
|
|
338
|
-
// [LAW:one-source-of-truth] The banner reads `.preset.customized`, so THIS
|
|
339
|
-
// pass is what requires that variable — not whichever config happens to
|
|
340
|
-
// declare it. The bundled default does, which is why the dependency stayed
|
|
341
|
-
// invisible until the global settings menu made edit mode reachable from
|
|
342
|
-
// configs that never declared it, and the missing field surfaced as a ⚠ on
|
|
343
|
-
// the bar. Ensured, never overridden: a user declaration of the same name
|
|
344
|
-
// wins (see the merge in synthesizeEditChrome), so this only supplies the
|
|
345
|
-
// floor the synthesis itself depends on.
|
|
346
|
-
artifacts.ensured[PRESET_CUSTOMIZED_VAR] = {
|
|
347
|
-
kind: "input",
|
|
348
|
-
path: PRESET_CUSTOMIZED_VAR,
|
|
349
|
-
type: "boolean",
|
|
350
|
-
default: false,
|
|
351
|
-
};
|
|
352
|
-
const label = escapeTemplateLiteral(presetName);
|
|
353
|
-
artifacts.segments[chromeSegName] = {
|
|
354
|
-
template: `{{ action "${actionName}" "↺ ${label} customized" }}`,
|
|
355
|
-
when: PRESET_CUSTOMIZED_GATE,
|
|
356
|
-
};
|
|
357
|
-
// [LAW:no-silent-failure] A preset's root may carry its OWN top-level
|
|
358
|
-
// `when` (the A-grammar's container schemas all permit one) — an author
|
|
359
|
-
// gating the whole preset behind a condition. `spliceContainer` preserves
|
|
360
|
-
// that onto `splicedRoot` via its `{...node, children}` spread, but this
|
|
361
|
-
// new OUTER wrapper is a brand-new node with no `when` of its own; without
|
|
362
|
-
// carrying it up, the reset banner would render even when the author's
|
|
363
|
-
// own condition is false, leaking past a gate they wrote.
|
|
364
|
-
return {
|
|
365
|
-
kind: "container",
|
|
366
|
-
direction: "vertical",
|
|
367
|
-
children: [
|
|
368
|
-
{ kind: "segment", name: chromeSegName },
|
|
369
|
-
withTrailingCell(splicedRoot, help.trigger),
|
|
370
|
-
// The body is a ROW of its own, and only while the disclosure is open —
|
|
371
|
-
// dropping BELOW the row that revealed it, like every other disclosure
|
|
372
|
-
// body in this codebase.
|
|
373
|
-
help.body,
|
|
374
|
-
],
|
|
375
|
-
...(splicedRoot.when !== undefined && { when: splicedRoot.when }),
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// [LAW:one-source-of-truth] `HelpDisclosure.trigger` is a CELL, and its contract
|
|
380
|
-
// is that the caller joins it to a row it ALREADY HAS — the settings menu pushes
|
|
381
|
-
// it into the row holding `persist?`. Edit mode's rows are the ones
|
|
382
|
-
// spliceContainer just built, so the trigger joins the last of them. Two
|
|
383
|
-
// constraints pin that placement and nothing else satisfies both:
|
|
384
|
-
//
|
|
385
|
-
// - Closed help must cost no LINE. A trigger given its own vertical slot is a
|
|
386
|
-
// permanent row for the whole time edit mode is on, since a trigger's `when`
|
|
387
|
-
// is its host surface's, never its own open state (a trigger you must open in
|
|
388
|
-
// order to see could never be opened).
|
|
389
|
-
// - Closed help must cost no COLOUR. The hue cursor (src/dsl/render.ts:696)
|
|
390
|
-
// advances in pre-order over every segment leaf — VISIBLE OR NOT, so that
|
|
391
|
-
// toggling a disclosure never recolours the bar — which means a leaf inserted
|
|
392
|
-
// AHEAD of the content shifts the hue index of everything after it. An
|
|
393
|
-
// earlier draft put the `(?)` first and recoloured every cell of the bundled
|
|
394
|
-
// default (status row 33;41;59 → 49;36;52) with edit mode still OFF. Trailing
|
|
395
|
-
// the last row is the one position that moves no other leaf.
|
|
396
|
-
//
|
|
397
|
-
// Those two together disqualify the reset-banner row, which sits above the
|
|
398
|
-
// content.
|
|
399
|
-
//
|
|
400
|
-
// A THIRD requirement decides how far the descent may go, and it outranks the
|
|
401
|
-
// other two: the trigger must be visible exactly when EDIT MODE is, since a
|
|
402
|
-
// trigger you must already have opened something else to reach is not a trigger.
|
|
403
|
-
// A container's `when` reaches every descendant, so descending into a
|
|
404
|
-
// `when`-bearing container would silently make its gate the trigger's gate.
|
|
405
|
-
// `kind: "group"` is the shape that makes this concrete rather than theoretical:
|
|
406
|
-
// lowerGroup emits `{vertical, children: [toggle, {…, when: groupGate}]}`, so a
|
|
407
|
-
// preset root ending in a group — ordinary authoring the A-grammar endorses —
|
|
408
|
-
// would otherwise put the `(?)` INSIDE that group's collapsible body, gated on
|
|
409
|
-
// edit mode AND a disclosure most groups default closed. Pairing beside a gated
|
|
410
|
-
// SEGMENT is a different act and stays allowed: `{h: [gatedSeg, cell]}` puts the
|
|
411
|
-
// cell beside the gate rather than under it.
|
|
412
|
-
//
|
|
413
|
-
// For a root whose last row is gated the three requirements are jointly
|
|
414
|
-
// unsatisfiable, so the priority is stated rather than left to whichever branch
|
|
415
|
-
// the recursion happens to reach: visible (always) > no recolour (always, since
|
|
416
|
-
// appending is still after every existing leaf) > no extra line (surrendered
|
|
417
|
-
// here, in exactly the configs where riding a row was never possible).
|
|
418
|
-
//
|
|
419
|
-
// [LAW:dataflow-not-control-flow] Total over the node shapes with no guard: a
|
|
420
|
-
// segment is a row of one that cannot hold a second cell, so it pairs into one;
|
|
421
|
-
// a vertical container's rows are its children, so it descends into the last one
|
|
422
|
-
// it may; anything else appends. An empty container has no last child and
|
|
423
|
-
// appends, which is the same answer.
|
|
424
|
-
function withTrailingCell(node: LayoutNode, cell: LayoutNode): LayoutNode {
|
|
425
|
-
if (node.kind === "segment") {
|
|
426
|
-
return {
|
|
427
|
-
kind: "container",
|
|
428
|
-
direction: "horizontal",
|
|
429
|
-
children: [node, cell],
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
const last = node.children.at(-1);
|
|
433
|
-
if (
|
|
434
|
-
node.direction === "vertical" &&
|
|
435
|
-
last !== undefined &&
|
|
436
|
-
(last.kind === "segment" || last.when === undefined)
|
|
437
|
-
) {
|
|
438
|
-
return {
|
|
439
|
-
...node,
|
|
440
|
-
children: [...node.children.slice(0, -1), withTrailingCell(last, cell)],
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
return { ...node, children: [...node.children, cell] };
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// One preset's chrome-spliced root. A bare-segment root (the A-grammar
|
|
447
|
-
// collapses a single top-level segment ref to `{ kind: "segment", name }`
|
|
448
|
-
// with no enclosing container) is wrapped in a synthetic horizontal
|
|
449
|
-
// container for splicing purposes — the wrap becomes the real returned root,
|
|
450
|
-
// which is exactly right: a lone segment needs a `+` on each side too.
|
|
451
|
-
function spliceEditChromeForPreset(
|
|
452
|
-
config: DslConfig,
|
|
453
|
-
presetName: string,
|
|
454
|
-
artifacts: ChromeArtifacts,
|
|
455
|
-
help: HelpDisclosure,
|
|
456
|
-
): LayoutNode {
|
|
457
|
-
const { node } = presetRoot(config, presetName);
|
|
458
|
-
const rootOpsKey = presetRootOpsKey(presetName);
|
|
459
|
-
const domainName = addableDomainName(presetName);
|
|
460
|
-
const presetIdent = ident(presetName);
|
|
461
|
-
const posCounter = { n: 0 };
|
|
462
|
-
// [LAW:no-silent-failure] The bare-segment-root case (the A-grammar's
|
|
463
|
-
// `{ seg, when }` shorthand is a legal PresetDecl.root) carries its OWN
|
|
464
|
-
// `when` onto this synthetic wrapper too — wrapWithPresetRows's own
|
|
465
|
-
// when-carry-up reads `splicedRoot.when`, which is this wrapper's `when`
|
|
466
|
-
// once spliceContainer's `{...node, children}` passes it through
|
|
467
|
-
// unchanged; without copying it here, a bare-segment preset root's own
|
|
468
|
-
// gate would never reach that carry-up at all, leaking the reset banner
|
|
469
|
-
// past it exactly like the container case did before that fix.
|
|
470
|
-
const container: ContainerNode =
|
|
471
|
-
node.kind === "container"
|
|
472
|
-
? node
|
|
473
|
-
: {
|
|
474
|
-
kind: "container",
|
|
475
|
-
direction: "horizontal",
|
|
476
|
-
children: [node],
|
|
477
|
-
...(node.when !== undefined && { when: node.when }),
|
|
478
|
-
};
|
|
479
|
-
const spliced = spliceContainer(
|
|
480
|
-
container,
|
|
481
|
-
presetIdent,
|
|
482
|
-
rootOpsKey,
|
|
483
|
-
domainName,
|
|
484
|
-
artifacts,
|
|
485
|
-
posCounter,
|
|
486
|
-
);
|
|
487
|
-
return wrapWithPresetRows(
|
|
488
|
-
spliced,
|
|
489
|
-
presetName,
|
|
490
|
-
presetIdent,
|
|
491
|
-
rootOpsKey,
|
|
492
|
-
artifacts,
|
|
493
|
-
help,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// [LAW:single-enforcer] THE synthesis entry point, called once from
|
|
498
|
-
// validateConfig after cross-ref/cycle checks pass. Every declared preset
|
|
499
|
-
// (the floor "default" included — presetNames/presetByName/presetRoot
|
|
500
|
-
// already treat it uniformly) gets an explicit `presets[name].root` carrying
|
|
501
|
-
// its spliced tree; `config.root` itself is left untouched (presetRoot falls
|
|
502
|
-
// back to it only when a preset declares no root of its own, and every name
|
|
503
|
-
// now does). The synthesized variables/actions/segments merge additively —
|
|
504
|
-
// nothing here can collide with user data, since every name it mints lives
|
|
505
|
-
// under the `edit.`/`menus.` namespaces `synthesizeEditModeToggle` and
|
|
506
|
-
// `synthesizeMenuDecls` already reserve unconditionally at parse time.
|
|
507
|
-
export function synthesizeEditChrome(config: DslConfig): DslConfig {
|
|
508
|
-
// [LAW:carrying-cost] Demand-driven, mirroring synthesizeEditModeToggle's
|
|
509
|
-
// own gate: `edit.toggle` exists in the merged config iff SOME file's
|
|
510
|
-
// Phase A synthesis fired (iff some segment referenced it), which iff some
|
|
511
|
-
// author actually placed an edit-mode trigger. A config that never opted
|
|
512
|
-
// in gets back the identical config, untouched — no extra segments,
|
|
513
|
-
// actions, or variables, and critically no NEW `set`/`state` surface that
|
|
514
|
-
// would force session.id onto an otherwise fully static bar.
|
|
515
|
-
if (!(EDIT_TOGGLE_ACTION in config.actions)) return config;
|
|
516
|
-
const artifacts: ChromeArtifacts = {
|
|
517
|
-
variables: {},
|
|
518
|
-
ensured: {},
|
|
519
|
-
actions: {},
|
|
520
|
-
segments: {},
|
|
521
|
-
};
|
|
522
|
-
// [LAW:one-source-of-truth] Edit mode's `(?)` is minted ONCE and merely
|
|
523
|
-
// REFERENCED from every preset root — the same move the settings menu makes
|
|
524
|
-
// with its anchor, and for the same reason: one disclosure means one open
|
|
525
|
-
// state, so switching presets cannot land you beside a second `(?)` that
|
|
526
|
-
// disagrees about whether help is showing. The text is identical for every
|
|
527
|
-
// preset because what `+` and `-` do is a fact about edit mode, not about a
|
|
528
|
-
// layout.
|
|
529
|
-
const help = declareHelp(
|
|
530
|
-
`${EDIT_NS}help`,
|
|
531
|
-
EDIT_MODE_HELP,
|
|
532
|
-
[EDIT_MODE_REF],
|
|
533
|
-
artifacts,
|
|
534
|
-
);
|
|
535
|
-
const presets: Record<string, PresetDecl> = { ...config.presets };
|
|
536
|
-
for (const name of presetNames(config.presets)) {
|
|
537
|
-
const splicedRoot = spliceEditChromeForPreset(
|
|
538
|
-
config,
|
|
539
|
-
name,
|
|
540
|
-
artifacts,
|
|
541
|
-
help,
|
|
542
|
-
);
|
|
543
|
-
presets[name] = {
|
|
544
|
-
...presetByName(config.presets, name),
|
|
545
|
-
root: splicedRoot,
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
return {
|
|
549
|
-
...config,
|
|
550
|
-
variables: {
|
|
551
|
-
...artifacts.ensured,
|
|
552
|
-
...config.variables,
|
|
553
|
-
...artifacts.variables,
|
|
554
|
-
},
|
|
555
|
-
actions: { ...config.actions, ...artifacts.actions },
|
|
556
|
-
segments: { ...config.segments, ...artifacts.segments },
|
|
557
|
-
presets,
|
|
558
|
-
};
|
|
559
|
-
}
|