@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,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
|
-
}
|
package/src/dsl/node-registry.ts
DELETED
|
@@ -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
|
-
}
|