@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.
Files changed (126) hide show
  1. package/dist/index.mjs +72 -71
  2. package/package.json +5 -6
  3. package/src/check.ts +0 -478
  4. package/src/cli-flags.ts +0 -8
  5. package/src/click/wire.ts +0 -158
  6. package/src/config/action.ts +0 -329
  7. package/src/config/cli.ts +0 -71
  8. package/src/config/default-dsl-config.ts +0 -1645
  9. package/src/config/disclosure.ts +0 -170
  10. package/src/config/dsl-loader.ts +0 -339
  11. package/src/config/dsl-types.ts +0 -581
  12. package/src/config/edit-chrome.ts +0 -559
  13. package/src/config/help.ts +0 -151
  14. package/src/config/ident.ts +0 -22
  15. package/src/config/layout-ops.ts +0 -177
  16. package/src/config/loader/actions.ts +0 -972
  17. package/src/config/loader/cache.ts +0 -206
  18. package/src/config/loader/cross-ref.ts +0 -714
  19. package/src/config/loader/cycles.ts +0 -148
  20. package/src/config/loader/diagnostics.ts +0 -99
  21. package/src/config/loader/discovery.ts +0 -182
  22. package/src/config/loader/edit-mode.ts +0 -137
  23. package/src/config/loader/emit-schema.ts +0 -68
  24. package/src/config/loader/globals.ts +0 -269
  25. package/src/config/loader/helpers.ts +0 -48
  26. package/src/config/loader/layout.ts +0 -693
  27. package/src/config/loader/looks.ts +0 -96
  28. package/src/config/loader/menu-synth.ts +0 -435
  29. package/src/config/loader/merge.ts +0 -115
  30. package/src/config/loader/persist-target.ts +0 -67
  31. package/src/config/loader/presets.ts +0 -119
  32. package/src/config/loader/refs.ts +0 -100
  33. package/src/config/loader/reserved-namespace.ts +0 -38
  34. package/src/config/loader/segments.ts +0 -120
  35. package/src/config/loader/validate-core.ts +0 -737
  36. package/src/config/loader/variables.ts +0 -260
  37. package/src/config/menu-keys.ts +0 -139
  38. package/src/config/option-domain.ts +0 -164
  39. package/src/config/presets.ts +0 -326
  40. package/src/config/settings-menu.ts +0 -775
  41. package/src/daemon/acquire.ts +0 -684
  42. package/src/daemon/cache/git.ts +0 -649
  43. package/src/daemon/cache/render.ts +0 -623
  44. package/src/daemon/cache/session-usage-store.ts +0 -720
  45. package/src/daemon/cache/watchers.ts +0 -249
  46. package/src/daemon/client-debug.ts +0 -120
  47. package/src/daemon/client-stats.ts +0 -130
  48. package/src/daemon/client-transport.ts +0 -273
  49. package/src/daemon/client.ts +0 -78
  50. package/src/daemon/config-overrides-store.ts +0 -663
  51. package/src/daemon/debug-types.ts +0 -91
  52. package/src/daemon/debug.ts +0 -264
  53. package/src/daemon/fork-bomb-breaker.ts +0 -351
  54. package/src/daemon/limits.ts +0 -211
  55. package/src/daemon/log.ts +0 -81
  56. package/src/daemon/parent-watchdog.ts +0 -87
  57. package/src/daemon/paths.ts +0 -211
  58. package/src/daemon/process-fingerprint.ts +0 -146
  59. package/src/daemon/protocol.ts +0 -292
  60. package/src/daemon/render-payload.ts +0 -1256
  61. package/src/daemon/server.ts +0 -1330
  62. package/src/daemon/session-state-file.ts +0 -108
  63. package/src/daemon/session-state.ts +0 -237
  64. package/src/daemon/socket-lease.ts +0 -209
  65. package/src/daemon/socket-ownership.ts +0 -209
  66. package/src/daemon/stats.ts +0 -235
  67. package/src/daemon/verbs/config-validators.ts +0 -250
  68. package/src/daemon/verbs/index.ts +0 -706
  69. package/src/daemon/verbs/state-validators.ts +0 -249
  70. package/src/daemon/verbs/validator-registry.ts +0 -457
  71. package/src/demo/dsl.ts +0 -143
  72. package/src/demo/mock-data.ts +0 -67
  73. package/src/demo/statusline.json5 +0 -94
  74. package/src/dsl/node-registry.ts +0 -374
  75. package/src/dsl/render.ts +0 -803
  76. package/src/help-text.ts +0 -90
  77. package/src/index.ts +0 -210
  78. package/src/install/currency.ts +0 -197
  79. package/src/install/index.ts +0 -557
  80. package/src/proc/launch.ts +0 -459
  81. package/src/proc/stats-handle.ts +0 -13
  82. package/src/render/action.ts +0 -883
  83. package/src/render/active-segment.ts +0 -78
  84. package/src/render/diagnostic-style.ts +0 -23
  85. package/src/render/diagnostic-text.ts +0 -77
  86. package/src/render/error-glyph.ts +0 -53
  87. package/src/render/menu.ts +0 -257
  88. package/src/render/outcome-plan.ts +0 -45
  89. package/src/render/picker.ts +0 -372
  90. package/src/render/segment-color.ts +0 -74
  91. package/src/render/split-lines.ts +0 -51
  92. package/src/render/strip.ts +0 -228
  93. package/src/segments/cache.ts +0 -131
  94. package/src/segments/context.ts +0 -190
  95. package/src/segments/git.ts +0 -1084
  96. package/src/segments/metrics.ts +0 -187
  97. package/src/segments/pricing.ts +0 -452
  98. package/src/segments/session.ts +0 -23
  99. package/src/segments/tmux.ts +0 -74
  100. package/src/template-engine/cells.ts +0 -90
  101. package/src/template-engine/colors.ts +0 -124
  102. package/src/template-engine/engine.ts +0 -108
  103. package/src/template-engine/funcs.ts +0 -232
  104. package/src/template-engine/index.ts +0 -11
  105. package/src/template-engine/layout.ts +0 -133
  106. package/src/template-engine/scope.ts +0 -62
  107. package/src/template-engine/sparkline.ts +0 -79
  108. package/src/themes/index.ts +0 -20
  109. package/src/themes/palette-resolvers.ts +0 -84
  110. package/src/themes/policy.ts +0 -393
  111. package/src/utils/cache.ts +0 -206
  112. package/src/utils/claude.ts +0 -683
  113. package/src/utils/color-support.ts +0 -118
  114. package/src/utils/formatters.ts +0 -99
  115. package/src/utils/logger.ts +0 -5
  116. package/src/utils/outcome.ts +0 -33
  117. package/src/utils/schema-validator.ts +0 -126
  118. package/src/utils/single-flight.ts +0 -57
  119. package/src/utils/terminal-width.ts +0 -51
  120. package/src/utils/terminal.ts +0 -11
  121. package/src/utils/transcript-fs.ts +0 -279
  122. package/src/var-system/index.ts +0 -24
  123. package/src/var-system/sources.ts +0 -1047
  124. package/src/var-system/store.ts +0 -223
  125. package/src/var-system/types.ts +0 -57
  126. package/src/version.ts +0 -17
@@ -1,94 +0,0 @@
1
- // Demo DSL statusline — a runnable example of the segment DSL.
2
- //
3
- // Run it: pnpm demo:dsl
4
- // Hack it: edit a template / palette / layout entry below and re-run.
5
- // No rebuild — the config IS the program.
6
- //
7
- // This file is rendered by the SAME two functions the daemon calls
8
- // (registerDslConfig + renderDsl in src/dsl/render.ts). There is no
9
- // demo-only render path — what you see here is what production produces.
10
- {
11
- globals: {
12
- // Base palette for every segment that doesn't pull its own (see `branch`).
13
- palette: 'textual-dark',
14
- },
15
-
16
- // ── Variables: where each segment's data comes from ──────────────────────
17
- // Every `kind` maps to one runtime source. Templates reference these by name
18
- // with a leading dot (`.user`, `.cwd`, …). A referenced-but-undeclared name
19
- // is a load-time error, not a silent blank.
20
- variables: {
21
- // env — read straight from the process environment.
22
- user: { kind: 'env', name: 'USER', default: 'anon' },
23
-
24
- // input — pulled from the JSON payload Claude Code sends each render tick.
25
- cwd: { kind: 'input', path: 'workspace.current_dir', default: '?' },
26
- model: { kind: 'input', path: 'model.display_name', default: '' },
27
- session: { kind: 'input', path: 'session_id', default: '' },
28
-
29
- // template — derived from other vars; recomputed reactively when they move.
30
- sid: { kind: 'template', template: '{{ trunc 8 .session }}' },
31
- here: { kind: 'template', template: '{{ basename .cwd }}' },
32
-
33
- // shell — run a command, cache the result for a TTL window. Populates
34
- // asynchronously: until the command returns, `.branch` is the default ''.
35
- branch: { kind: 'shell', command: 'git branch --show-current', cache: { ttl: '5s' }, default: '' },
36
-
37
- // time — the system clock, formatted with a Go reference-time layout.
38
- clock: { kind: 'time', layout: '15:04:05', cache: { ttl: '1s' } },
39
-
40
- // literal — a fixed value read like any other var. Here it drives the
41
- // per-segment hue rotation (HUE_STEP_VAR): adjacent segments rotate 14° so
42
- // they stay distinct. Make it a `state` var + a stepper widget to adjust it
43
- // live; a literal is the fixed-value form.
44
- 'hue.step': { kind: 'literal', value: 14 },
45
-
46
- // (file and git kinds exist too — omitted here to stay minimal.)
47
- },
48
-
49
- // ── Segments: how each piece of data is drawn ────────────────────────────
50
- // `bg`/`fg` are palette spec strings, and either may also be a template
51
- // that computes a color — `fg: '{{ contrastOn (bgOf) }}'` below picks
52
- // black or white, whichever is readable against this segment's own
53
- // background. `when` hides a segment when it evaluates false.
54
- segments: {
55
- user: {
56
- template: ' {{ .user }} ',
57
- bg: 'primary',
58
- fg: '{{ contrastOn (bgOf) }}',
59
- },
60
- directory: {
61
- template: ' {{ .here }} ',
62
- bg: 'surface',
63
- fg: 'foreground',
64
- },
65
- branch: {
66
- template: ' {{ .branch }} ',
67
- bg: 'accent',
68
- fg: '{{ contrastOn (bgOf) }}',
69
- when: '{{ ne .branch "" }}', // hidden entirely outside a git repo
70
- palette: 'gruvbox', // this segment pulls its OWN palette (per-segment switch)
71
- },
72
- model: {
73
- template: ' {{ .model }} ',
74
- bg: 'secondary',
75
- fg: '{{ contrastOn (bgOf) }}',
76
- when: '{{ ne .model "" }}',
77
- },
78
- session: {
79
- template: ' ⌗{{ .sid }} ',
80
- bg: 'surface',
81
- fg: 'foreground',
82
- },
83
- clock: {
84
- template: ' {{ .clock }} ',
85
- bg: 'primary',
86
- fg: '{{ contrastOn (bgOf) }}',
87
- },
88
- },
89
-
90
- // ── Layout: Option A shape grammar — one horizontal row of segment refs.
91
- // { v: [...] } for multiple rows; { h: ['a', 'b'], when: '...' } for a gated row;
92
- // bare string 'segname' for a single segment ref.
93
- root: { h: ['user', 'directory', 'branch', 'model', 'session', 'clock'] },
94
- }
@@ -1,374 +0,0 @@
1
- // [LAW:single-enforcer] THE node-type registry: the one place each layout node
2
- // kind's render-time behavior (compile + render) is defined, dispatched through a
3
- // single typed lookup. The walk is ONE uniform dispatch:
4
- // nodeType(node.kind).render(node, ctx).
5
- //
6
- // [LAW:one-type-per-behavior] The layout is exactly two kinds — `container`
7
- // (arranges children) and `segment` (THE unit of rendering: one ref into the
8
- // named segments map, rendered to ONE strip item). Interaction, state-driven
9
- // display, and multi-region clickability all live in a segment's TEMPLATE, not
10
- // in extra node kinds — so there is no inline/stepper/picker node arm to add.
11
- // A horizontal run of segments is spelled `{ h: ["seg1", "seg2"] }` in the
12
- // A-grammar (the `cells` form and `layout` rows were deleted in 2de.19).
13
- //
14
- // [LAW:one-way-deps] This module sits BELOW render.ts (the driver): it imports
15
- // the leaf render/template helpers directly and receives the two recursive
16
- // capabilities (compileChild, renderChild) + the hue counter as DATA from the
17
- // driver. It must NOT import render.ts — that would invert the layering. render.ts
18
- // imports the compiled types + nodeType() from here, one-way.
19
- //
20
- // Hue is per-segment DECORATIVE only: each `segment` advances the cursor by one
21
- // unit (a container advances none), so colors stay positionally stable. It
22
- // carries NO structural meaning — unit cohesion is structural (one segment = one
23
- // strip item), not a function of matching backgrounds.
24
-
25
- import { RichText, IDENTITY } from "@promptctl/rich-js";
26
- import type { Palette, Style, ThemeKey } from "@promptctl/rich-js";
27
- import type { Template } from "@promptctl/go-template-js";
28
- import type {
29
- LayoutNode,
30
- Direction,
31
- SegmentDecl,
32
- } from "../config/dsl-types.js";
33
- import { splitCellsIntoLines } from "../render/split-lines.js";
34
- import { transposedPalette } from "../themes/index.js";
35
- import {
36
- fragmentsToCells,
37
- evaluateWhen,
38
- applySegmentLayout,
39
- } from "../template-engine/index.js";
40
-
41
- // ─── Compiled node shapes ──────────────────────────────────────────────────────
42
-
43
- // [LAW:dataflow-not-control-flow] The compiled mirror of a LayoutNode: the same
44
- // recursive shape with every `when` parsed ONCE at registration. renderDsl walks
45
- // this compiled tree — never the raw config — so the parse-once guarantee covers
46
- // every node.
47
- export interface CompiledSegmentNode {
48
- readonly kind: "segment";
49
- readonly when?: Template<RichText>;
50
- readonly name: string;
51
- }
52
- export interface CompiledContainerNode {
53
- readonly kind: "container";
54
- readonly direction: Direction;
55
- readonly when?: Template<RichText>;
56
- readonly children: readonly CompiledNode[];
57
- }
58
- export type CompiledNode = CompiledSegmentNode | CompiledContainerNode;
59
-
60
- // Pre-parsed templates and pre-resolved palette for one segment, built once at
61
- // registration. A `segment` node names one; render looks it up via
62
- // ctx.lookupSegment.
63
- export interface CompiledSegment {
64
- readonly when?: Template<RichText>;
65
- readonly template: Template<RichText>;
66
- readonly bg?: Template<RichText>;
67
- readonly fg?: Template<RichText>;
68
- readonly palette?: Palette;
69
- }
70
- export type CompiledSegments = Readonly<Record<string, CompiledSegment>>;
71
-
72
- // A rendered node is a LIST OF LINES, each line a list of cells — NOT yet
73
- // serialized. [LAW:types-are-the-program] Cells (not ANSI bytes) are the
74
- // composition substrate: the powerline joiner caps between adjacent cells, so
75
- // serializing a node before composition would freeze its last cell's edge and
76
- // make a cap across a sibling seam unrecoverable. Serialization (the single
77
- // joiner pass) runs exactly once, at the root, after the whole tree composes.
78
- export type RenderedLines = ReadonlyArray<readonly RichText[]>;
79
-
80
- // ─── Compile / render contexts (the injected capabilities) ──────────────────────
81
-
82
- // [LAW:locality-or-seam] The compile-time context the driver hands each node
83
- // type. `when` is PRE-COMPILED by the driver (walk-owned, uniform across kinds);
84
- // the type only assembles it in. compileChild is the recursion, injected so this
85
- // module needn't import the driver.
86
- export interface NodeCompileCtx {
87
- readonly path: string;
88
- // The node's own `when`, already parsed by the driver (one parse-when site).
89
- readonly when?: Template<RichText>;
90
- // Compile a child node (the recursion, injected so this module needn't import
91
- // the driver).
92
- compileChild(node: LayoutNode, path: string): CompiledNode;
93
- }
94
-
95
- // [LAW:single-enforcer] The render-time context. The hue COUNTER lives in the
96
- // driver; ctx exposes only nextHueShift() (advance + return this unit's shift) so
97
- // there is exactly one mutator. `visible` is THIS node's computed visibility
98
- // (the driver ANDs node.when with the parent's). renderChild continues the walk.
99
- export interface NodeRenderCtx {
100
- readonly scope: object;
101
- readonly basePalette: Palette;
102
- // [LAW:one-source-of-truth] The render-wide look (the session's chosen
103
- // theme-adaptation, resolved by the caller via effectiveLookName →
104
- // lookKeyByName), threaded by the driver — one ThemeKey per render, IDENTITY
105
- // when no look is chosen. Composed with the per-segment hue shift into ONE
106
- // transposition key at the segment leaf.
107
- readonly look: ThemeKey;
108
- readonly visible: boolean;
109
- // [LAW:one-source-of-truth] The render-wide intra-cell padding (resolved
110
- // globals.padding), threaded by the driver from BuildLineOptions into every
111
- // segment's layout — one value per render, never re-defaulted per node.
112
- readonly padding: number;
113
- // Advance the walk-owned hue cursor by one unit and return that unit's shift.
114
- nextHueShift(): number;
115
- readonly perSegmentSink?: Map<string, readonly RichText[]>;
116
- // [LAW:no-silent-failure] Optional observer for the per-segment render catch
117
- // below: a caught evaluation error renders as a visible ⚠ error cell (partial
118
- // rendering — the daemon's channel), AND is reported here so a headless caller
119
- // (`cc-candybar check`, a blind authoring agent's eyes) can turn it into a
120
- // text verdict instead of blessing a bar it cannot see. Trusted non-throwing
121
- // (the registry-dispose contract) — see RenderObservers.onSegmentError.
122
- readonly onSegmentError?: (segName: string, message: string) => void;
123
- // [LAW:locality-or-seam] The segment seam, injected as a capability pair so
124
- // this module never imports the menu or color features — it only says when a
125
- // segment starts and stops.
126
- //
127
- // `enterSegment` runs BEFORE any of the segment's templates evaluate. It
128
- // publishes what the segment's own templates may ask about themselves — the
129
- // name a `{{ menu }}` derives its identity from, the palette `{{ color }}`
130
- // resolves against, the background `{{ bgOf }}` returns — and resolves the
131
- // segment's `bg:`/`fg:` into its base Style along the way. The bg/fg
132
- // resolution HAS to happen here rather than after the body: a body asking for
133
- // its own background can only be answered once the background exists.
134
- //
135
- // `exitSegment` runs AFTER eval: it reads the open menu bodies the menus
136
- // carried as metadata on the evaluated fragments (template order) for the
137
- // boundary to stack below the row, and tears the published record down.
138
- enterSegment(
139
- segName: string,
140
- palette: Palette,
141
- bgTemplate: Template<RichText> | undefined,
142
- fgTemplate: Template<RichText> | undefined,
143
- ): Style;
144
- exitSegment(fragments: readonly RichText[]): readonly RichText[];
145
- // [LAW:locality-or-seam] The focus transform, injected as a capability (rich-js
146
- // owns the color math — see render.ts). Applied to the segment's baseStyle when
147
- // it has an open menu (it contributed drops), so the whole focused segment —
148
- // inline trigger + dropped body band — reads as highlighted.
149
- focusTint(style: Style): Style;
150
- // Resolve a segment name to its decl + compiled form (the driver closes over
151
- // config.segments + the compiled segments).
152
- lookupSegment(
153
- name: string,
154
- ):
155
- | { readonly seg: SegmentDecl; readonly compiled: CompiledSegment }
156
- | undefined;
157
- // Continue the walk into a child node (parentVisible = this node's visibility).
158
- renderChild(node: CompiledNode, parentVisible: boolean): RenderedLines;
159
- }
160
-
161
- // ─── Composition ───────────────────────────────────────────────────────────────
162
-
163
- // [LAW:dataflow-not-control-flow] A container's `direction` is the projection it
164
- // applies to its already-rendered child blocks — DATA selecting a fold, not a
165
- // branch that skips work. `vertical` STACKS (concatenate the children's line-
166
- // lists). The switch is exhaustive over `Direction`; adding `outline` to
167
- // DIRECTIONS forces a new arm here.
168
- //
169
- // [LAW:decomposition] `horizontal` composes ONLY row 0 across the seam — row 0 is
170
- // every child's FIRST line zipped (the inline powerline run, so the joiner caps
171
- // across the seam, no abut). Every line BELOW row 0 is a child's DROP (a menu body
172
- // dropping below its trigger, a genuinely multi-line segment): drops STACK full-
173
- // width in child order, never zipped. Multi-line side-by-side column alignment is
174
- // explicitly UNSUPPORTED — aligning two children's row-i cells would require
175
- // background-as-structure, and bg is never structural. For an all-single-line row
176
- // (no child has a drop) this is byte-identical to a plain per-row zip.
177
- function composeBlocks(
178
- direction: Direction,
179
- blocks: readonly RenderedLines[],
180
- ): RenderedLines {
181
- switch (direction) {
182
- case "vertical":
183
- return blocks.flatMap((b) => b);
184
- case "horizontal": {
185
- // [LAW:dataflow-not-control-flow] height 0 (every child hidden/empty) ⇒ the
186
- // container contributes NO line — not one empty row. This is the value-driven
187
- // identity of the fold, preserved from the per-row zip it replaces; a stray
188
- // [[]] here would render as a spurious blank line.
189
- const height = blocks.reduce((m, b) => Math.max(m, b.length), 0);
190
- if (height === 0) return [];
191
- const row0 = blocks.flatMap((b) => b[0] ?? []);
192
- const drops = blocks.flatMap((b) => b.slice(1));
193
- return [row0, ...drops];
194
- }
195
- }
196
- }
197
-
198
- // ─── The node-type contract + registry ──────────────────────────────────────────
199
-
200
- type NodeKind = LayoutNode["kind"];
201
-
202
- // [LAW:types-are-the-program] One contract per node kind, generic over the kind so
203
- // each entry's compile/render see their OWN narrowed node arm — never the union,
204
- // so no internal re-narrow guard. compile (registration: LayoutNode → compiled,
205
- // parse-once) and render (per-render: compiled → lines) are co-located per kind.
206
- export interface NodeType<K extends NodeKind> {
207
- compile(
208
- node: Extract<LayoutNode, { kind: K }>,
209
- cctx: NodeCompileCtx,
210
- ): Extract<CompiledNode, { kind: K }>;
211
- render(
212
- node: Extract<CompiledNode, { kind: K }>,
213
- ctx: NodeRenderCtx,
214
- ): RenderedLines;
215
- }
216
-
217
- const containerType: NodeType<"container"> = {
218
- compile(node, cctx) {
219
- return {
220
- kind: "container",
221
- direction: node.direction,
222
- when: cctx.when,
223
- children: node.children.map((child, i) =>
224
- cctx.compileChild(child, `${cctx.path}.children[${i}]`),
225
- ),
226
- };
227
- },
228
- render(node, ctx) {
229
- // [LAW:dataflow-not-control-flow] A container advances NO hue unit itself; its
230
- // children do, walked in order so positional hue stays stable. Hidden or not,
231
- // every child is rendered (parentVisible threads the gate) so hidden subtrees
232
- // still advance the cursor.
233
- return composeBlocks(
234
- node.direction,
235
- node.children.map((child) => ctx.renderChild(child, ctx.visible)),
236
- );
237
- },
238
- };
239
-
240
- const segmentType: NodeType<"segment"> = {
241
- compile(node, cctx) {
242
- return {
243
- kind: "segment",
244
- when: cctx.when,
245
- name: node.name,
246
- };
247
- },
248
- render(node, ctx) {
249
- const found = ctx.lookupSegment(node.name);
250
- // [LAW:no-defensive-null-guards] The loader validates every segment ref
251
- // against the segments map and registerDslConfig compiles every declared
252
- // segment; a miss is a caller bug (renderDsl given a mismatched compiled
253
- // object).
254
- if (!found) {
255
- throw new Error(`Layout segment "${node.name}" has no matching segment`);
256
- }
257
- const { seg, compiled: segCompiled } = found;
258
-
259
- // [LAW:single-enforcer] Advance the hue cursor BEFORE the visibility gate so a
260
- // hidden segment still consumes its unit — siblings after it keep their
261
- // positionally-stable colors regardless of which segments are hidden.
262
- const hueShift = ctx.nextHueShift();
263
- if (!ctx.visible) return [];
264
-
265
- // [LAW:no-silent-failure] Wrap the whole render body in a try/catch so a
266
- // partial-load consequence (e.g. a variable that failed to declare, leaving a
267
- // MissingFieldError when the template or when-predicate accesses it) surfaces
268
- // as a visible error cell rather than crashing the whole bar. The remaining
269
- // segments render normally. This is the render-time complement to the per-
270
- // variable catch in registerDslConfig — together they implement option-2
271
- // partial rendering: the new config stays active, working segments render, and
272
- // broken segments show an error cell.
273
- try {
274
- if (!evaluateWhen(segCompiled.when, ctx.scope)) return [];
275
-
276
- // [LAW:dataflow-not-control-flow] The per-segment variability is WHICH
277
- // palette — the base palette (per-segment override or basePalette)
278
- // transposed by the render's look + this segment's hueShift, folded into
279
- // ONE ThemeKey for a SINGLE transposePalette call (chaining two
280
- // transpositions would double-pay OKLCH quantization and collide the
281
- // transpose memo — see transposedPalette). An explicit per-segment
282
- // `palette:` pin IGNORES the look, exactly as it ignores the session
283
- // theme: the pin's presence is the discriminator, and its arm carries the
284
- // identity look — a value choice, not a skipped operation.
285
- const lookKey = segCompiled.palette !== undefined ? IDENTITY : ctx.look;
286
- const palette = transposedPalette(
287
- segCompiled.palette ?? ctx.basePalette,
288
- {
289
- ...lookKey,
290
- hueShift: lookKey.hueShift + hueShift,
291
- },
292
- );
293
-
294
- // [LAW:one-source-of-truth] ONE palette for this segment: its `bg:`, its
295
- // `fg:`, and every `{{ color }}` in its body resolve from this same
296
- // object. That is the whole reason the segment is entered before its body
297
- // evaluates rather than after — a body coloured from a palette resolved
298
- // independently of the cell it sits in is two palettes in one segment,
299
- // and they diverge the moment a theme, look, or hue shift moves.
300
- const resolvedStyle = ctx.enterSegment(
301
- node.name,
302
- palette,
303
- segCompiled.bg,
304
- segCompiled.fg,
305
- );
306
- const fragments = segCompiled.template.evaluate(ctx.scope);
307
- // [LAW:decomposition] The open menu bodies, carried as out-of-band metadata
308
- // on the evaluated fragments — invisible to the inline render, so a menu can
309
- // sit anywhere in the template and content after it stays inline. Each
310
- // becomes one full-width line stacked below the segment's row.
311
- const drops = ctx.exitSegment(fragments);
312
- // [LAW:dataflow-not-control-flow] Focus is the PRESENCE of a drop: a segment
313
- // with an open menu (it contributed a body) lightens, so the whole focused
314
- // segment — inline trigger + dropped band — reads as highlighted. No state
315
- // re-read; the drop list IS the open-menu signal.
316
- const baseStyle =
317
- drops.length > 0 ? ctx.focusTint(resolvedStyle) : resolvedStyle;
318
- const layout = {
319
- width: seg.width ?? "auto",
320
- justify: seg.justify ?? "left",
321
- truncate: seg.truncate ?? "right",
322
- baseStyle,
323
- // [LAW:dataflow-not-control-flow] Padding is uniform across every line
324
- // a segment contributes — inline rows AND dropped menu bands — one
325
- // value, no per-line-kind branch. The picker reserves 2×padding at its
326
- // pagination seam so a padded band still fits the width budget.
327
- padding: ctx.padding,
328
- } as const;
329
-
330
- // [LAW:single-enforcer] Partition the segment's authored "\n" into visual
331
- // lines BEFORE per-segment layout — width/justify/truncate then measure each
332
- // line cleanly. A newline-free segment is the degenerate one-line case. Each
333
- // laid line is ONE strip item: applySegmentLayout collapses a line's cells to
334
- // 0-or-1 item (OSC-8 links survive as interior spans), so the joiner caps only
335
- // at the segment's edges, never inside it.
336
- const inlineLines = splitCellsIntoLines(
337
- fragmentsToCells(fragments, baseStyle),
338
- ).map((line) => applySegmentLayout(line, layout));
339
- // Each open menu body is one full-width dropped line, in the segment's
340
- // (focus-tinted) bg, stacked after the inline row(s). composeBlocks then
341
- // drops every line below row 0 below the enclosing horizontal row.
342
- const dropLines = drops.map((body) =>
343
- applySegmentLayout(fragmentsToCells([body], baseStyle), layout),
344
- );
345
- const laidLines = [...inlineLines, ...dropLines];
346
-
347
- if (ctx.perSegmentSink !== undefined) {
348
- ctx.perSegmentSink.set(node.name, laidLines.flat());
349
- }
350
- return laidLines;
351
- } catch (err) {
352
- const message = (err as Error).message ?? String(err);
353
- ctx.onSegmentError?.(node.name, message);
354
- return [[new RichText(`⚠ ${node.name}: ${message}`)]];
355
- }
356
- },
357
- };
358
-
359
- // [LAW:single-enforcer] THE registry. `satisfies` forces an entry for every
360
- // LayoutNode kind — adding a kind to the union breaks compilation here until its
361
- // behavior is registered, so "register a type" is one mechanically-enforced act.
362
- const REGISTRY = {
363
- container: containerType,
364
- segment: segmentType,
365
- } satisfies { [K in NodeKind]: NodeType<K> };
366
-
367
- // [LAW:types-are-the-program] The one dispatch primitive. Indexing by a node's OWN
368
- // kind returns the entry built FOR that kind, so the pairing is sound by
369
- // construction; the cast only widens the static K to the union (TS cannot prove
370
- // the index/arm link across a heterogeneous registry). Every consumer calls
371
- // nodeType(node.kind).method(node) — no consumer re-switches on kind.
372
- export function nodeType(kind: NodeKind): NodeType<NodeKind> {
373
- return REGISTRY[kind] as unknown as NodeType<NodeKind>;
374
- }