@promptctl/cc-candybar 1.19.0 → 1.21.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 +62 -59
- package/package.json +5 -5
- package/schema/cc-candybar.schema.json +27 -1
- package/src/check.ts +398 -0
- package/src/config/action.ts +12 -5
- package/src/config/cli.ts +19 -118
- 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/layout.ts +8 -4
- 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/render-payload.ts +13 -0
- package/src/daemon/server.ts +16 -1
- 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/index.ts +12 -5
- 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
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/index.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { tryRenderViaDaemon } from "./daemon/client";
|
|
|
11
11
|
import { runDaemonStats } from "./daemon/client-stats";
|
|
12
12
|
import { runDebug } from "./daemon/client-debug";
|
|
13
13
|
import { isDebugWhat } from "./daemon/debug-types";
|
|
14
|
-
import {
|
|
14
|
+
import { runSchema } from "./config/cli";
|
|
15
|
+
import { runCheck } from "./check";
|
|
15
16
|
import { obtainDaemonKick } from "./daemon/acquire";
|
|
16
17
|
import { planOutcome } from "./render/outcome-plan";
|
|
17
18
|
|
|
@@ -66,8 +67,11 @@ Subcommands (macOS):
|
|
|
66
67
|
request totals. Does not spawn a daemon.
|
|
67
68
|
|
|
68
69
|
Config tooling:
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
check [config-file] Validate a config on the full render pipeline (parse
|
|
71
|
+
→ merge → validate → register → render) with no
|
|
72
|
+
daemon. With no path, checks the same file the daemon
|
|
73
|
+
would load from here. Exit 0 clean (warnings on
|
|
74
|
+
stderr), 1 invalid, 2 unreadable. "lint" is an alias.
|
|
71
75
|
schema Print the JSON Schema for the config file shape
|
|
72
76
|
(.cc-candybar.json5). Point an editor's $schema at it
|
|
73
77
|
for autocomplete + structural validation.
|
|
@@ -113,8 +117,11 @@ async function main(): Promise<void> {
|
|
|
113
117
|
await runDaemonStats(process.argv.slice(3));
|
|
114
118
|
process.exit(0);
|
|
115
119
|
}
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
// [LAW:one-type-per-behavior] `lint` is an alias of `check` — one config
|
|
121
|
+
// verdict, one pipeline, one exit-code contract (0/1/2). check subsumes the
|
|
122
|
+
// old lint (same loader, plus register + render coverage).
|
|
123
|
+
if (subcommand === "check" || subcommand === "lint") {
|
|
124
|
+
runCheck(process.argv.slice(3)); // owns its own exit code (0/1/2)
|
|
118
125
|
return;
|
|
119
126
|
}
|
|
120
127
|
if (subcommand === "schema") {
|
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
|