@promptctl/cc-candybar 1.20.0 → 1.22.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 +77 -76
- package/package.json +5 -5
- package/schema/cc-candybar.schema.json +27 -1
- package/src/check.ts +43 -4
- package/src/config/action.ts +12 -5
- package/src/config/default-dsl-config.ts +64 -0
- package/src/config/dsl-loader.ts +3 -0
- package/src/config/dsl-types.ts +28 -0
- package/src/config/loader/cross-ref.ts +14 -0
- package/src/config/loader/emit-schema.ts +2 -0
- package/src/config/loader/globals.ts +6 -0
- package/src/config/loader/looks.ts +96 -0
- package/src/config/loader/merge.ts +4 -0
- package/src/config/loader/validate-core.ts +35 -0
- package/src/daemon/fork-bomb-breaker.ts +351 -0
- package/src/daemon/parent-watchdog.ts +8 -1
- package/src/daemon/paths.ts +36 -8
- package/src/daemon/process-fingerprint.ts +11 -0
- package/src/daemon/render-payload.ts +13 -0
- package/src/daemon/server.ts +83 -19
- package/src/daemon/socket-lease.ts +4 -4
- package/src/daemon/verbs/state-validators.ts +21 -7
- package/src/demo/dsl.ts +15 -1
- package/src/dsl/node-registry.ts +29 -11
- package/src/dsl/render.ts +54 -12
- package/src/render/action.ts +18 -7
- package/src/themes/index.ts +2 -0
- package/src/themes/palette-resolvers.ts +33 -27
- package/src/themes/policy.ts +51 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
|
|
4
|
+
import type { ProcessIdentity } from "./process-fingerprint";
|
|
5
|
+
|
|
4
6
|
// ─── Socket ownership lease ──────────────────────────────────────────────────
|
|
5
7
|
//
|
|
6
8
|
// [LAW:one-source-of-truth] The authority for "who owns this socket path" is a
|
|
@@ -30,7 +32,7 @@ import process from "node:process";
|
|
|
30
32
|
export type LeaseRead =
|
|
31
33
|
| { kind: "absent" }
|
|
32
34
|
| { kind: "unreadable"; detail: string }
|
|
33
|
-
| { kind: "owned"
|
|
35
|
+
| ({ kind: "owned" } & ProcessIdentity);
|
|
34
36
|
|
|
35
37
|
// The EADDRINUSE arbitration outcome. The path already exists (something bound
|
|
36
38
|
// it or a stale file remains); this says whether a LIVE owner holds it.
|
|
@@ -44,11 +46,9 @@ export type SocketArbitration =
|
|
|
44
46
|
// root. `startTime` is the kernel start-time token (also human-readable, so it
|
|
45
47
|
// doubles as the "daemon started at" diagnostic the old `startedAt` gave), or
|
|
46
48
|
// null when this host could not fingerprint.
|
|
47
|
-
export interface LeaseRecord {
|
|
48
|
-
pid: number;
|
|
49
|
+
export interface LeaseRecord extends ProcessIdentity {
|
|
49
50
|
version: number;
|
|
50
51
|
binPath: string | undefined;
|
|
51
|
-
startTime: string | null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// [LAW:effects-at-boundaries][LAW:dataflow-not-control-flow] The whole
|
|
@@ -431,11 +431,18 @@ export function makeRangeValidator(
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
// [LAW:one-source-of-truth] The option members a picker draws from ARE the same
|
|
434
|
-
// canonical lists the `themes()`/`styles()` bindings and the baseline
|
|
435
|
-
// style validators consult — the rendered options and the derived gate
|
|
436
|
-
// diverge because there is no second enumeration.
|
|
437
|
-
|
|
438
|
-
|
|
434
|
+
// canonical lists the `themes()`/`styles()`/`looks()` bindings and the baseline
|
|
435
|
+
// theme/style validators consult — the rendered options and the derived gate
|
|
436
|
+
// cannot diverge because there is no second enumeration. "looks" is the one
|
|
437
|
+
// per-config domain: its names come from the config's merged looks block,
|
|
438
|
+
// threaded in as data (the render-side optionDomain takes the same list).
|
|
439
|
+
function optionValuesFor(
|
|
440
|
+
src: OptionSource,
|
|
441
|
+
lookNames: readonly string[],
|
|
442
|
+
): readonly string[] {
|
|
443
|
+
if (src === "themes") return RESOLVABLE_THEMES_LIST;
|
|
444
|
+
if (src === "styles") return STRIP_STYLES;
|
|
445
|
+
return lookNames;
|
|
439
446
|
}
|
|
440
447
|
|
|
441
448
|
// [LAW:types-are-the-program] Collapse one key's spec contributions into the
|
|
@@ -564,6 +571,7 @@ function mergeContributions(
|
|
|
564
571
|
function actionKeySpecs(
|
|
565
572
|
a: ActionDecl,
|
|
566
573
|
seeds: ReadonlyMap<string, number>,
|
|
574
|
+
lookNames: readonly string[],
|
|
567
575
|
): KeySpecContribution[] {
|
|
568
576
|
if (!("set" in a)) return [];
|
|
569
577
|
if ("to" in a) {
|
|
@@ -573,7 +581,10 @@ function actionKeySpecs(
|
|
|
573
581
|
return [
|
|
574
582
|
{
|
|
575
583
|
key: a.set,
|
|
576
|
-
spec: {
|
|
584
|
+
spec: {
|
|
585
|
+
kind: "allow-list",
|
|
586
|
+
allowed: optionValuesFor(a.from, lookNames),
|
|
587
|
+
},
|
|
577
588
|
},
|
|
578
589
|
];
|
|
579
590
|
}
|
|
@@ -635,8 +646,11 @@ function stateKeySeeds(config: DslConfig): ReadonlyMap<string, number> {
|
|
|
635
646
|
// realizes a click from are the gate the wire enforces.
|
|
636
647
|
function actionContributions(config: DslConfig): KeySpecContribution[] {
|
|
637
648
|
const seeds = stateKeySeeds(config);
|
|
649
|
+
const lookNames = Object.keys(config.looks);
|
|
638
650
|
return dropBaselineAllowLists(
|
|
639
|
-
Object.values(config.actions).flatMap((a) =>
|
|
651
|
+
Object.values(config.actions).flatMap((a) =>
|
|
652
|
+
actionKeySpecs(a, seeds, lookNames),
|
|
653
|
+
),
|
|
640
654
|
);
|
|
641
655
|
}
|
|
642
656
|
|
package/src/demo/dsl.ts
CHANGED
|
@@ -29,7 +29,12 @@ import { VariableStore } from "../var-system/store.js";
|
|
|
29
29
|
import { SourceRegistry } from "../var-system/sources.js";
|
|
30
30
|
import { SessionState } from "../daemon/session-state.js";
|
|
31
31
|
import { listResolvablePaletteNames } from "../themes/policy.js";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
effectiveThemeName,
|
|
34
|
+
effectiveLookName,
|
|
35
|
+
lookKeyByName,
|
|
36
|
+
resolverForThemeName,
|
|
37
|
+
} from "../themes/index.js";
|
|
33
38
|
import { registerDslConfig, renderDsl } from "../dsl/render.js";
|
|
34
39
|
import {
|
|
35
40
|
DEFAULT_CHARSET,
|
|
@@ -75,6 +80,13 @@ const payload = {
|
|
|
75
80
|
const basePalette = resolverForThemeName(
|
|
76
81
|
effectiveThemeName(null, config.globals.palette),
|
|
77
82
|
);
|
|
83
|
+
// Same fresh-session resolution one dimension over: the config-default look
|
|
84
|
+
// over the "none" identity floor — the exact mirror of the daemon's per-render
|
|
85
|
+
// effectiveLookName → lookKeyByName chain.
|
|
86
|
+
const lookKey = lookKeyByName(
|
|
87
|
+
config.looks,
|
|
88
|
+
effectiveLookName(null, config.globals.look, config.looks),
|
|
89
|
+
);
|
|
78
90
|
|
|
79
91
|
// A fresh store + registry for this run. (A hot-reloading daemon would
|
|
80
92
|
// dispose() the old pair and build new ones — see registerDslConfig's docs.)
|
|
@@ -125,6 +137,8 @@ try {
|
|
|
125
137
|
padding: config.globals.padding ?? DEFAULT_PADDING,
|
|
126
138
|
charset: config.globals.charset ?? DEFAULT_CHARSET,
|
|
127
139
|
},
|
|
140
|
+
undefined,
|
|
141
|
+
lookKey,
|
|
128
142
|
);
|
|
129
143
|
process.stdout.write(` ${line}\n`);
|
|
130
144
|
if (frame < FRAMES - 1) await sleep(FRAME_INTERVAL_MS);
|
package/src/dsl/node-registry.ts
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
// carries NO structural meaning — unit cohesion is structural (one segment = one
|
|
23
23
|
// strip item), not a function of matching backgrounds.
|
|
24
24
|
|
|
25
|
-
import { RichText } from "@promptctl/rich-js";
|
|
26
|
-
import type { PaletteResolver, Style } from "@promptctl/rich-js";
|
|
25
|
+
import { RichText, IDENTITY } from "@promptctl/rich-js";
|
|
26
|
+
import type { PaletteResolver, Style, ThemeKey } from "@promptctl/rich-js";
|
|
27
27
|
import type { Template } from "@promptctl/go-template-js";
|
|
28
28
|
import type {
|
|
29
29
|
LayoutNode,
|
|
@@ -100,6 +100,12 @@ export interface NodeCompileCtx {
|
|
|
100
100
|
export interface NodeRenderCtx {
|
|
101
101
|
readonly scope: object;
|
|
102
102
|
readonly basePalette: PaletteResolver;
|
|
103
|
+
// [LAW:one-source-of-truth] The render-wide look (the session's chosen
|
|
104
|
+
// theme-adaptation, resolved by the caller via effectiveLookName →
|
|
105
|
+
// lookKeyByName), threaded by the driver — one ThemeKey per render, IDENTITY
|
|
106
|
+
// when no look is chosen. Composed with the per-segment hue shift into ONE
|
|
107
|
+
// transposition key at the segment leaf.
|
|
108
|
+
readonly look: ThemeKey;
|
|
103
109
|
readonly visible: boolean;
|
|
104
110
|
// [LAW:one-source-of-truth] The render-wide intra-cell padding (resolved
|
|
105
111
|
// globals.padding), threaded by the driver from BuildLineOptions into every
|
|
@@ -108,6 +114,13 @@ export interface NodeRenderCtx {
|
|
|
108
114
|
// Advance the walk-owned hue cursor by one unit and return that unit's shift.
|
|
109
115
|
nextHueShift(): number;
|
|
110
116
|
readonly perSegmentSink?: Map<string, readonly RichText[]>;
|
|
117
|
+
// [LAW:no-silent-failure] Optional observer for the per-segment render catch
|
|
118
|
+
// below: a caught evaluation error renders as a visible ⚠ error cell (partial
|
|
119
|
+
// rendering — the daemon's channel), AND is reported here so a headless caller
|
|
120
|
+
// (`cc-candybar check`, a blind authoring agent's eyes) can turn it into a
|
|
121
|
+
// text verdict instead of blessing a bar it cannot see. Trusted non-throwing
|
|
122
|
+
// (the registry-dispose contract) — see RenderObservers.onSegmentError.
|
|
123
|
+
readonly onSegmentError?: (segName: string, message: string) => void;
|
|
111
124
|
// [LAW:locality-or-seam] The menu seam, injected as capabilities so this module
|
|
112
125
|
// never imports the menu feature. `beginSegment` runs BEFORE a segment template
|
|
113
126
|
// evaluates: it publishes the segment name so a `{{ menu }}` can derive its
|
|
@@ -261,10 +274,19 @@ const segmentType: NodeType<"segment"> = {
|
|
|
261
274
|
|
|
262
275
|
// [LAW:dataflow-not-control-flow] The per-segment variability is WHICH
|
|
263
276
|
// palette — the base resolver (per-segment override or basePalette)
|
|
264
|
-
// transposed by
|
|
277
|
+
// transposed by the render's look + this segment's hueShift, folded into
|
|
278
|
+
// ONE ThemeKey for a SINGLE transposePalette call (chaining two
|
|
279
|
+
// transpositions would double-pay OKLCH quantization and collide the
|
|
280
|
+
// transpose memo — see transposedResolver). bg and fg then resolve from
|
|
281
|
+
// this one palette. An explicit per-segment `palette:` pin IGNORES the
|
|
282
|
+
// look, exactly as it ignores the session theme: the pin's presence is
|
|
283
|
+
// the discriminator, and its arm carries the identity look — a value
|
|
284
|
+
// choice, not a skipped operation.
|
|
285
|
+
const lookKey =
|
|
286
|
+
segCompiled.paletteResolver !== undefined ? IDENTITY : ctx.look;
|
|
265
287
|
const resolver = transposedResolver(
|
|
266
288
|
segCompiled.paletteResolver ?? ctx.basePalette,
|
|
267
|
-
hueShift,
|
|
289
|
+
{ ...lookKey, hueShift: lookKey.hueShift + hueShift },
|
|
268
290
|
);
|
|
269
291
|
const resolvedStyle = resolveSegmentColors(
|
|
270
292
|
resolver,
|
|
@@ -312,13 +334,9 @@ const segmentType: NodeType<"segment"> = {
|
|
|
312
334
|
}
|
|
313
335
|
return laidLines;
|
|
314
336
|
} catch (err) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
`⚠ ${node.name}: ${(err as Error).message ?? String(err)}`,
|
|
319
|
-
),
|
|
320
|
-
],
|
|
321
|
-
];
|
|
337
|
+
const message = (err as Error).message ?? String(err);
|
|
338
|
+
ctx.onSegmentError?.(node.name, message);
|
|
339
|
+
return [[new RichText(`⚠ ${node.name}: ${message}`)]];
|
|
322
340
|
}
|
|
323
341
|
},
|
|
324
342
|
};
|
package/src/dsl/render.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// the input values (kind discriminators, layout length, palette presence)
|
|
12
12
|
// govern output, not whether operations run.
|
|
13
13
|
|
|
14
|
-
import type { RichText, PaletteResolver } from "@promptctl/rich-js";
|
|
15
|
-
import { ColorSpec, Style, lighten } from "@promptctl/rich-js";
|
|
14
|
+
import type { RichText, PaletteResolver, ThemeKey } from "@promptctl/rich-js";
|
|
15
|
+
import { ColorSpec, Style, lighten, IDENTITY } from "@promptctl/rich-js";
|
|
16
16
|
import type { Engine, Template } from "@promptctl/go-template-js";
|
|
17
17
|
import type {
|
|
18
18
|
ValidatedConfig,
|
|
@@ -304,12 +304,23 @@ export function registerDslConfig(
|
|
|
304
304
|
action: actionRuntime,
|
|
305
305
|
current: null,
|
|
306
306
|
};
|
|
307
|
+
// [LAW:one-source-of-truth] The config's look names — the one PER-CONFIG
|
|
308
|
+
// option domain. Computed once here and fed to BOTH consumers (the compiled
|
|
309
|
+
// set-option domains below and the `looks()` binding), so the rendered
|
|
310
|
+
// options, a hand-authored `range looks`, and the derived click gate (which
|
|
311
|
+
// reads the same config in deriveActionValidators) trace to one map.
|
|
312
|
+
const lookNames = Object.keys(config.looks);
|
|
307
313
|
const engine = createCcCandybarEngine(
|
|
308
314
|
undefined,
|
|
309
315
|
{
|
|
310
316
|
...actionFuncs(actionRuntime),
|
|
311
317
|
...pickerFuncs(actionRuntime),
|
|
312
318
|
...menuFuncs(menuRuntime),
|
|
319
|
+
// [LAW:one-type-per-behavior] The per-config sibling of the static
|
|
320
|
+
// themes()/styles() bindings (template-engine/funcs.ts): zero-arg
|
|
321
|
+
// projection of the "looks" option domain. Injected here — not in the
|
|
322
|
+
// static FuncMap — because the domain is this config's looks block.
|
|
323
|
+
looks: { fn: () => lookNames, argTypes: [] },
|
|
313
324
|
},
|
|
314
325
|
opts?.clock,
|
|
315
326
|
);
|
|
@@ -348,7 +359,12 @@ export function registerDslConfig(
|
|
|
348
359
|
// [LAW:one-source-of-truth] Actions resolve their set key → the reading
|
|
349
360
|
// variable through the stateKeyToVar map, so an apply action and the picker
|
|
350
361
|
// that references it read one value.
|
|
351
|
-
actionRuntime.compiled = compileActions(
|
|
362
|
+
actionRuntime.compiled = compileActions(
|
|
363
|
+
parse,
|
|
364
|
+
config.actions,
|
|
365
|
+
stateKeyToVar,
|
|
366
|
+
lookNames,
|
|
367
|
+
);
|
|
352
368
|
|
|
353
369
|
// [LAW:dataflow-not-control-flow] One variable failing to declare does not
|
|
354
370
|
// abort the rest. Errors are data (accumulated in loadWarnings); the store
|
|
@@ -501,14 +517,11 @@ function focusTint(style: Style): Style {
|
|
|
501
517
|
* into nested containers keeps every segment's color; toggling a node's
|
|
502
518
|
* visibility does not recolor the nodes after it.
|
|
503
519
|
*/
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
payload: unknown,
|
|
510
|
-
basePalette: PaletteResolver,
|
|
511
|
-
opts: BuildLineOptions,
|
|
520
|
+
// [LAW:locality-or-seam] The optional render observers, bundled as ONE named bag
|
|
521
|
+
// so a caller states what it passes by name — no positional tail to count, no
|
|
522
|
+
// `undefined` holes to reach a later observer, and a new observer is one field
|
|
523
|
+
// here rather than a signature change every caller re-counts.
|
|
524
|
+
export interface RenderObservers {
|
|
512
525
|
// [LAW:dataflow-not-control-flow] Optional per-segment cell sink. When
|
|
513
526
|
// present, each rendered segment's RichText array (post-layout, pre-
|
|
514
527
|
// serialization) is written to this map under its segment name. Storing
|
|
@@ -521,8 +534,35 @@ export function renderDsl(
|
|
|
521
534
|
// joined line (powerline joiners sit *between* segments and have no
|
|
522
535
|
// place in a one-segment render), but for debug visibility this is the
|
|
523
536
|
// natural per-segment shape.
|
|
524
|
-
perSegmentSink?: Map<string, readonly RichText[]
|
|
537
|
+
readonly perSegmentSink?: Map<string, readonly RichText[]>;
|
|
538
|
+
// [LAW:no-silent-failure] Optional observer for per-segment evaluation errors.
|
|
539
|
+
// A failing segment renders as a visible ⚠ error cell (partial rendering, the
|
|
540
|
+
// daemon's author-facing channel) — a headless caller with no one looking at
|
|
541
|
+
// the bar (`cc-candybar check`) passes this to receive the same errors as
|
|
542
|
+
// data and fold them into its text verdict. Trusted non-throwing (the
|
|
543
|
+
// registry-dispose contract): an observer that throws is a caller bug
|
|
544
|
+
// surfaced loudly, never caught and absorbed by the render walk.
|
|
545
|
+
readonly onSegmentError?: (segName: string, message: string) => void;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function renderDsl(
|
|
549
|
+
config: ValidatedConfig,
|
|
550
|
+
compiled: CompiledConfig,
|
|
551
|
+
store: VariableStore,
|
|
552
|
+
registry: SourceRegistry,
|
|
553
|
+
payload: unknown,
|
|
554
|
+
basePalette: PaletteResolver,
|
|
555
|
+
opts: BuildLineOptions,
|
|
556
|
+
observers?: RenderObservers,
|
|
557
|
+
// [LAW:dataflow-not-control-flow] The render's look (the session's chosen
|
|
558
|
+
// theme-adaptation), resolved per render by the caller — effectiveLookName
|
|
559
|
+
// over SessionState/globals, then lookKeyByName — exactly how basePalette
|
|
560
|
+
// resolves. IDENTITY is the domain's own no-adaptation element (the "none"
|
|
561
|
+
// look), so an omitting caller renders unadapted — a true default, not a
|
|
562
|
+
// fallback. Composed with each segment's hue shift into ONE transposition.
|
|
563
|
+
look: ThemeKey = IDENTITY,
|
|
525
564
|
): string {
|
|
565
|
+
const { perSegmentSink, onSegmentError } = observers ?? {};
|
|
526
566
|
// [LAW:one-source-of-truth] Inject the usable width as `term.cols` from the
|
|
527
567
|
// SAME opts.width the strip wraps to (below), so a width-paginated widget reads
|
|
528
568
|
// the exact wrap width — never a cached or independently-measured copy. This is
|
|
@@ -616,10 +656,12 @@ export function renderDsl(
|
|
|
616
656
|
const ctx: NodeRenderCtx = {
|
|
617
657
|
scope,
|
|
618
658
|
basePalette,
|
|
659
|
+
look,
|
|
619
660
|
visible,
|
|
620
661
|
padding: opts.padding,
|
|
621
662
|
nextHueShift,
|
|
622
663
|
perSegmentSink,
|
|
664
|
+
onSegmentError,
|
|
623
665
|
beginSegment,
|
|
624
666
|
collectDrops,
|
|
625
667
|
focusTint,
|
package/src/render/action.ts
CHANGED
|
@@ -107,11 +107,18 @@ export type CompiledActionDecl =
|
|
|
107
107
|
export type CompiledActions = ReadonlyMap<string, CompiledActionDecl>;
|
|
108
108
|
|
|
109
109
|
// [LAW:one-source-of-truth] An option source resolves to the SAME canonical list
|
|
110
|
-
// the `themes()`/`styles()` bindings and the derived gate consult —
|
|
111
|
-
// options and the gate cannot diverge. The render-side resolver (the
|
|
112
|
-
// validator-derivation has its own that must agree
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
// the `themes()`/`styles()`/`looks()` bindings and the derived gate consult —
|
|
111
|
+
// rendered options and the gate cannot diverge. The render-side resolver (the
|
|
112
|
+
// daemon's validator-derivation has its own that must agree — themes/styles from
|
|
113
|
+
// themes/policy, looks from the config's merged look names, threaded in as data
|
|
114
|
+
// because that one domain is per-config, not registry-static).
|
|
115
|
+
export function optionDomain(
|
|
116
|
+
src: OptionSource,
|
|
117
|
+
lookNames: readonly string[],
|
|
118
|
+
): readonly string[] {
|
|
119
|
+
if (src === "themes") return listResolvablePaletteNames();
|
|
120
|
+
if (src === "styles") return STRIP_STYLES;
|
|
121
|
+
return lookNames;
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
// [LAW:locality-or-seam] The runtime holder the `action` template function closes
|
|
@@ -158,10 +165,13 @@ export function compileActions(
|
|
|
158
165
|
parse: (src: string) => Template<RichText>,
|
|
159
166
|
actions: Readonly<Record<string, ActionDecl>>,
|
|
160
167
|
stateKeyToVar: ReadonlyMap<string, string>,
|
|
168
|
+
// The config's look names — the one per-config option domain optionDomain
|
|
169
|
+
// resolves from (themes/styles stay registry-static).
|
|
170
|
+
lookNames: readonly string[],
|
|
161
171
|
): CompiledActions {
|
|
162
172
|
const out = new Map<string, CompiledActionDecl>();
|
|
163
173
|
for (const [name, action] of Object.entries(actions)) {
|
|
164
|
-
out.set(name, compileAction(parse, name, action, stateKeyToVar));
|
|
174
|
+
out.set(name, compileAction(parse, name, action, stateKeyToVar, lookNames));
|
|
165
175
|
}
|
|
166
176
|
return out;
|
|
167
177
|
}
|
|
@@ -175,6 +185,7 @@ function compileAction(
|
|
|
175
185
|
name: string,
|
|
176
186
|
action: ActionDecl,
|
|
177
187
|
stateKeyToVar: ReadonlyMap<string, string>,
|
|
188
|
+
lookNames: readonly string[],
|
|
178
189
|
): CompiledActionDecl {
|
|
179
190
|
if ("set" in action) {
|
|
180
191
|
const stateVar = stateKeyToVar.get(action.set) ?? action.set;
|
|
@@ -191,7 +202,7 @@ function compileAction(
|
|
|
191
202
|
kind: "set-option",
|
|
192
203
|
key: action.set,
|
|
193
204
|
stateVar,
|
|
194
|
-
options: [...optionDomain(action.from)],
|
|
205
|
+
options: [...optionDomain(action.from, lookNames)],
|
|
195
206
|
};
|
|
196
207
|
}
|
|
197
208
|
if ("int" in action) {
|
package/src/themes/index.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
// Memoized PaletteResolver construction over rich-js. cc-candybar moves theme
|
|
2
|
-
// NAMES and
|
|
3
|
-
// memos live here: a theme name -> base resolver, and a (base,
|
|
2
|
+
// NAMES and ThemeKey axes (data); rich-js owns every color value operation. Two
|
|
3
|
+
// memos live here: a theme name -> base resolver, and a (base, ThemeKey) ->
|
|
4
4
|
// transposed resolver. They compose — the per-render base palette feeds the
|
|
5
|
-
// per-segment transposition
|
|
5
|
+
// per-segment transposition (the session look's axes + the segment's hue shift,
|
|
6
|
+
// folded into one key by the caller).
|
|
6
7
|
//
|
|
7
8
|
// [LAW:no-shared-mutable-globals] Single owner: this module. Both Maps are pure
|
|
8
9
|
// memos of pure rich-js functions, keyed by immutable inputs (resolved theme
|
|
9
|
-
// name; palette name +
|
|
10
|
-
// singletons, so a cached resolver never goes stale. Key spaces are
|
|
11
|
-
// #themes and #themes × #
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
10
|
+
// name; palette name + the four ThemeKey axes). rich-js palettes are immutable
|
|
11
|
+
// registry singletons, so a cached resolver never goes stale. Key spaces are
|
|
12
|
+
// bounded by #themes and #themes × #declared looks × #distinct hueShifts
|
|
13
|
+
// (hueShift = look shift + segIndex*hueStep, segIndex bounded by layout; look
|
|
14
|
+
// axes bounded by the loaded configs' looks blocks) — both small. Shared on
|
|
15
|
+
// purpose: a theme's base resolver and its gruvbox+42° transposition are each
|
|
16
|
+
// computed once per process, not once per RenderCache entry or per render.
|
|
17
|
+
// Read/written only through the two functions below.
|
|
16
18
|
|
|
17
19
|
import {
|
|
18
20
|
PaletteResolver,
|
|
@@ -54,32 +56,36 @@ export function resolverForThemeName(name: string): PaletteResolver {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
|
-
* The PaletteResolver for `base`'s palette transposed by
|
|
59
|
+
* The PaletteResolver for `base`'s palette transposed by a full ThemeKey — the
|
|
60
|
+
* adapted-resolver constructor: (base resolver, key) → resolver. The caller
|
|
61
|
+
* composes whatever axes it carries (a look's four axes, the per-segment hue
|
|
62
|
+
* shift) into ONE key and this makes ONE transposePalette call — never chain
|
|
63
|
+
* two transpositions: chaining double-pays OKLCH quantization AND collides this
|
|
64
|
+
* memo (a transposed palette keeps the base palette's name, so a re-transposed
|
|
65
|
+
* gruvbox-with-look and plain gruvbox would share cache keys).
|
|
58
66
|
*
|
|
59
|
-
* [LAW:dataflow-not-control-flow]
|
|
60
|
-
* transposePalette's
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
67
|
+
* [LAW:dataflow-not-control-flow] The key is data; the identity key flows
|
|
68
|
+
* through transposePalette's isIdentityKey fast-path (byte-exact, no
|
|
69
|
+
* round-trip) — no branch here. rich-js hue-locks ANCHORED_ROOTS
|
|
70
|
+
* (error/success/warning), so semantic meaning is preserved by construction —
|
|
71
|
+
* no local exemption list to drift.
|
|
64
72
|
*
|
|
65
|
-
* [LAW:single-enforcer] The sole place a transposed resolver is built
|
|
66
|
-
*
|
|
67
|
-
*
|
|
73
|
+
* [LAW:single-enforcer] The sole place a transposed resolver is built — a
|
|
74
|
+
* future look `roles` remap is additive at this one seam. The memo miss
|
|
75
|
+
* (undefined) is genuine optionality — not-yet-computed — not a defended
|
|
76
|
+
* invariant. [LAW:one-source-of-truth] The cache key carries every axis of the
|
|
77
|
+
* ThemeKey: two keys differing on any axis are distinct palettes.
|
|
68
78
|
*/
|
|
69
79
|
export function transposedResolver(
|
|
70
80
|
base: PaletteResolver,
|
|
71
|
-
|
|
81
|
+
key: ThemeKey,
|
|
72
82
|
): PaletteResolver {
|
|
73
|
-
const cacheKey =
|
|
83
|
+
const cacheKey =
|
|
84
|
+
`${base.palette.name} ${key.hueShift} ${key.chromaScale} ` +
|
|
85
|
+
`${key.lightnessScale} ${key.lightnessShift}`;
|
|
74
86
|
const hit = transposeCache.get(cacheKey);
|
|
75
87
|
if (hit !== undefined) return hit;
|
|
76
88
|
|
|
77
|
-
const key: ThemeKey = {
|
|
78
|
-
hueShift,
|
|
79
|
-
chromaScale: 1,
|
|
80
|
-
lightnessScale: 1,
|
|
81
|
-
lightnessShift: 0,
|
|
82
|
-
};
|
|
83
89
|
const resolver = new PaletteResolver(transposePalette(base.palette, key));
|
|
84
90
|
transposeCache.set(cacheKey, resolver);
|
|
85
91
|
return resolver;
|
package/src/themes/policy.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
// PaletteResolver, no ColorRgba, no hex. The semantic/anchor knowledge
|
|
6
6
|
// (which tokens keep their hue) stays in rich-js (ANCHORED_ROOTS), not here.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
listThemePalettes,
|
|
10
|
+
type ColorSystemSpec,
|
|
11
|
+
type ThemeKey,
|
|
12
|
+
} from "@promptctl/rich-js";
|
|
9
13
|
|
|
10
14
|
// --- Theme name aliasing ---
|
|
11
15
|
|
|
@@ -47,6 +51,52 @@ export function listResolvablePaletteNames(): readonly string[] {
|
|
|
47
51
|
return [...listThemePalettes(), ...listThemeAliases()];
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
// --- Look (theme-adaptation) identifiers ---
|
|
55
|
+
|
|
56
|
+
// The look name a render should use, as data. [LAW:dataflow-not-control-flow]
|
|
57
|
+
// [LAW:one-type-per-behavior] The exact shape of `effectiveThemeName`, one
|
|
58
|
+
// dimension over: session choice over config default over the "none" floor, no
|
|
59
|
+
// "if the session has a look" branch. Unlike the registry-static theme/style
|
|
60
|
+
// domains, the look domain is PER-CONFIG (the merged `looks` block), so the
|
|
61
|
+
// declared names arrive as data. A value outside them (a stale SessionState
|
|
62
|
+
// entry from a prior config's look vocabulary — config edits can orphan a
|
|
63
|
+
// clicked name the per-config gate once admitted) collapses to "none", which
|
|
64
|
+
// every merged DslConfig carries by construction (the bundled stdlib ships it;
|
|
65
|
+
// merge-by-name cannot remove it) — the same collapse-to-floor that keeps
|
|
66
|
+
// effectiveStripStyle's return honest rather than silently widening.
|
|
67
|
+
export function effectiveLookName(
|
|
68
|
+
sessionLook: string | null,
|
|
69
|
+
globalsLook: string | undefined,
|
|
70
|
+
declaredLooks: Readonly<Record<string, ThemeKey>>,
|
|
71
|
+
): string {
|
|
72
|
+
const chosen = sessionLook ?? globalsLook ?? "none";
|
|
73
|
+
return Object.prototype.hasOwnProperty.call(declaredLooks, chosen)
|
|
74
|
+
? chosen
|
|
75
|
+
: "none";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// [LAW:single-enforcer] The one place an effective look NAME becomes the
|
|
79
|
+
// ThemeKey a render transposes with. By the time a name reaches here it must be
|
|
80
|
+
// a member: effectiveLookName collapses unknown names to the "none" floor, and
|
|
81
|
+
// mergeWithDefault guarantees the bundled "none" exists in every DslConfig.
|
|
82
|
+
// [LAW:no-defensive-null-guards] the throw is the loud failure for that broken
|
|
83
|
+
// invariant (a hand-built config missing the stdlib), never a silent identity
|
|
84
|
+
// fallback that would hide the drift.
|
|
85
|
+
export function lookKeyByName(
|
|
86
|
+
looks: Readonly<Record<string, ThemeKey>>,
|
|
87
|
+
name: string,
|
|
88
|
+
): ThemeKey {
|
|
89
|
+
const key = looks[name];
|
|
90
|
+
if (key === undefined) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Look "${name}" is not declared in this config — effectiveLookName ` +
|
|
93
|
+
`collapses unknown names to "none", and every merged config carries ` +
|
|
94
|
+
`"none"; a miss here is merge/policy drift`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return key;
|
|
98
|
+
}
|
|
99
|
+
|
|
50
100
|
// --- Powerline strip-style identifiers ---
|
|
51
101
|
|
|
52
102
|
// [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
|