@promptctl/cc-candybar 1.35.0 → 1.37.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 +58 -58
- package/package.json +5 -5
- package/src/check.ts +4 -4
- package/src/config/default-dsl-config.ts +23 -8
- package/src/config/dsl-loader.ts +20 -1
- package/src/config/edit-chrome.ts +62 -22
- package/src/config/loader/cross-ref.ts +44 -0
- package/src/config/loader/edit-mode.ts +14 -0
- package/src/config/loader/globals.ts +6 -1
- package/src/config/presets.ts +8 -3
- package/src/config/settings-menu.ts +373 -0
- package/src/daemon/render-payload.ts +7 -5
- package/src/daemon/server.ts +20 -7
- package/src/demo/dsl.ts +7 -6
- package/src/render/strip.ts +6 -10
- package/src/themes/index.ts +2 -0
- package/src/themes/policy.ts +182 -40
package/src/themes/policy.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
// [LAW:single-enforcer] The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// [LAW:single-enforcer] The vocabulary and the resolution for every `globals`
|
|
2
|
+
// field a config can set or a click can pick: what values each field admits,
|
|
3
|
+
// what its floor is, and the one function that turns a session pick, a config
|
|
4
|
+
// default and that floor into the value a render actually uses.
|
|
5
|
+
//
|
|
6
|
+
// It sits in the themes module because theme/look/style were the first three
|
|
7
|
+
// fields to need it, and it is a LEAF: the config loader (validation, JSON
|
|
8
|
+
// schema) and the render layer both import it, which is what keeps a
|
|
9
|
+
// config↔render cycle from forming [LAW:one-way-deps].
|
|
10
|
+
//
|
|
11
|
+
// No color arithmetic lives here. cc-candybar selects theme NAMES and style
|
|
12
|
+
// IDENTIFIERS; every color *value* operation (hydrate hex, resolve specs,
|
|
13
|
+
// darken/contrast, hue/transpose) lives in rich-js, as does the semantic/anchor
|
|
14
|
+
// knowledge of which tokens keep their hue (ANCHORED_ROOTS).
|
|
7
15
|
|
|
8
16
|
import {
|
|
9
17
|
listThemePalettes,
|
|
@@ -22,19 +30,55 @@ export function resolvePaletteName(name: string): string {
|
|
|
22
30
|
return THEME_ALIASES[name] ?? name;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
// The
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
// the
|
|
29
|
-
//
|
|
33
|
+
// --- The one globals resolution ---
|
|
34
|
+
|
|
35
|
+
// [LAW:one-type-per-behavior] THE resolution every globals field a click can
|
|
36
|
+
// pick shares: the session's own value, over the config default, over a floor.
|
|
37
|
+
// Written once because the fields differ only in DATA — which floor, and how a
|
|
38
|
+
// raw SessionState string becomes a value of that field's type. Theme, look,
|
|
39
|
+
// preset, style, autoWrap and padding are all this function with different
|
|
40
|
+
// arguments, so the precedence order cannot land on one field and miss another.
|
|
41
|
+
//
|
|
42
|
+
// [LAW:dataflow-not-control-flow] The `??` chain IS the precedence; there is no
|
|
43
|
+
// "does this session have one" branch. A session that has never clicked passes
|
|
44
|
+
// null and lands on the config default by the same code path a session that
|
|
45
|
+
// clicked lands on its pick.
|
|
46
|
+
//
|
|
47
|
+
// [LAW:parse-dont-validate] `parseSession` is the boundary between an untyped
|
|
48
|
+
// SessionState string and this field's domain: it returns the typed value or
|
|
49
|
+
// null, and null means "no session pick" — indistinguishable, on purpose, from
|
|
50
|
+
// never having clicked. That is what makes a stale entry from a prior config's
|
|
51
|
+
// vocabulary (or a value a since-narrowed gate would now refuse) resolve to the
|
|
52
|
+
// default rather than throw or render something the label disagrees with
|
|
53
|
+
// [LAW:no-silent-failure] — the caller publishes what this returns as
|
|
54
|
+
// `<field>.effective`, so bar and label always trace to one value.
|
|
55
|
+
export function effectiveGlobal<T>(
|
|
56
|
+
sessionPick: string | null,
|
|
57
|
+
configDefault: T | null | undefined,
|
|
58
|
+
floor: T,
|
|
59
|
+
parseSession: (raw: string) => T | null,
|
|
60
|
+
): T {
|
|
61
|
+
const picked = sessionPick === null ? null : parseSession(sessionPick);
|
|
62
|
+
return picked ?? configDefault ?? floor;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The theme name a render should use, as data.
|
|
30
66
|
// [LAW:one-source-of-truth] The single definition of "which theme is effective";
|
|
31
67
|
// every render derives basePalette through this, so the rendered palette can
|
|
32
|
-
// never disagree with the chosen theme.
|
|
68
|
+
// never disagree with the chosen theme. The theme domain is OPEN — registry
|
|
69
|
+
// names, aliases, and per-session sentinels all resolve downstream — so its
|
|
70
|
+
// parse is identity: there is no membership to check here, and pretending
|
|
71
|
+
// otherwise would collapse names `paletteForThemeName` handles fine.
|
|
33
72
|
export function effectiveThemeName(
|
|
34
73
|
sessionTheme: string | null,
|
|
35
74
|
globalsPalette: string | undefined,
|
|
36
75
|
): string {
|
|
37
|
-
return
|
|
76
|
+
return effectiveGlobal(
|
|
77
|
+
sessionTheme,
|
|
78
|
+
globalsPalette,
|
|
79
|
+
"textual-dark",
|
|
80
|
+
(raw) => raw,
|
|
81
|
+
);
|
|
38
82
|
}
|
|
39
83
|
|
|
40
84
|
function listThemeAliases(): readonly string[] {
|
|
@@ -53,20 +97,16 @@ export function listResolvablePaletteNames(): readonly string[] {
|
|
|
53
97
|
|
|
54
98
|
// --- Per-config member selection ---
|
|
55
99
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// both exactly this — what differs between them is only the floor name and
|
|
61
|
-
// which map holds the members, i.e. DATA. Written once here rather than twice,
|
|
62
|
-
// so a change to the collapse rule cannot land on one and miss the other.
|
|
100
|
+
// The `effectiveGlobal` instance for every selection whose domain is PER-CONFIG
|
|
101
|
+
// (declared in the config, not a registry-static list). `looks` and `presets`
|
|
102
|
+
// are both exactly this — what differs between them is only the floor name and
|
|
103
|
+
// which map holds the members, i.e. DATA.
|
|
63
104
|
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// payload so a menu label can never claim a member the render did not use.
|
|
105
|
+
// The config default runs through the SAME membership parse the session pick
|
|
106
|
+
// does, one layer down: a `look:`/`preset:` naming a member the config no longer
|
|
107
|
+
// declares is no default at all, and collapses to the floor exactly as a stale
|
|
108
|
+
// session pick does. The loader cannot catch that for a per-config domain, so
|
|
109
|
+
// this resolution is where it is caught.
|
|
70
110
|
//
|
|
71
111
|
// The floor's membership is a load-time guarantee, not a runtime hope: the
|
|
72
112
|
// bundled stdlib ships it and merge-by-name cannot remove it.
|
|
@@ -76,10 +116,14 @@ export function effectiveMemberName(
|
|
|
76
116
|
floor: string,
|
|
77
117
|
declared: Readonly<Record<string, unknown>>,
|
|
78
118
|
): string {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
const member = (raw: string): string | null =>
|
|
120
|
+
Object.prototype.hasOwnProperty.call(declared, raw) ? raw : null;
|
|
121
|
+
return effectiveGlobal(
|
|
122
|
+
sessionPick,
|
|
123
|
+
member(configDefault ?? floor),
|
|
124
|
+
floor,
|
|
125
|
+
member,
|
|
126
|
+
);
|
|
83
127
|
}
|
|
84
128
|
|
|
85
129
|
// --- Look (theme-adaptation) identifiers ---
|
|
@@ -136,19 +180,28 @@ export function isStripStyle(value: string): value is StripStyle {
|
|
|
136
180
|
return (STRIP_STYLES as readonly string[]).includes(value);
|
|
137
181
|
}
|
|
138
182
|
|
|
139
|
-
// The strip style a render should use, as data.
|
|
140
|
-
// [LAW:one-type-per-behavior]
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
183
|
+
// The strip style a render should use, as data.
|
|
184
|
+
// [LAW:one-type-per-behavior] `effectiveGlobal` over a closed registry-static
|
|
185
|
+
// vocabulary: the narrowing guard IS the parse. `pickJoiner` would render an
|
|
186
|
+
// unknown style as powerline anyway; parsing here keeps the returned TYPE
|
|
187
|
+
// honest rather than silently widening it.
|
|
188
|
+
//
|
|
189
|
+
// A stale SessionState entry (a member of a prior option vocabulary) is an
|
|
190
|
+
// ABSENT session pick, not a pick of the floor: it falls through to the config
|
|
191
|
+
// default, and only reaches "powerline" when the config declares no style
|
|
192
|
+
// either. That is a deliberate change from the pre-`effectiveGlobal` spelling,
|
|
193
|
+
// which collapsed straight to the floor and skipped the user's own declared
|
|
194
|
+
// default — a config saying `style: "capsule"` deserves capsule when a session
|
|
195
|
+
// entry goes stale, not powerline. Every field here now shares that one rule
|
|
196
|
+
// [LAW:one-source-of-truth]; test/session-globals.test.ts pins it with a stale
|
|
197
|
+
// pick over a valid non-floor default, the case the old tests never exercised.
|
|
146
198
|
export function effectiveStripStyle(
|
|
147
199
|
sessionStyle: string | null,
|
|
148
200
|
globalsStyle: StripStyle | undefined,
|
|
149
201
|
): StripStyle {
|
|
150
|
-
|
|
151
|
-
|
|
202
|
+
return effectiveGlobal(sessionStyle, globalsStyle, "powerline", (raw) =>
|
|
203
|
+
isStripStyle(raw) ? raw : null,
|
|
204
|
+
);
|
|
152
205
|
}
|
|
153
206
|
|
|
154
207
|
// --- Joiner charset identifiers ---
|
|
@@ -164,7 +217,11 @@ export function effectiveStripStyle(
|
|
|
164
217
|
// joiner SHAPE, charset picks the glyph VALUES fed to it.
|
|
165
218
|
// [config-only] Unlike STRIP_STYLES there is no SessionState/click half, so no
|
|
166
219
|
// narrowing guard or effective* resolver — the config global over the default
|
|
167
|
-
// is the whole resolution.
|
|
220
|
+
// is the whole resolution. That is a decision, not a gap: charset describes the
|
|
221
|
+
// TERMINAL (does its font carry the powerline private-use glyphs), not a taste.
|
|
222
|
+
// It does not vary session-to-session on one machine, so a per-session override
|
|
223
|
+
// would be a knob whose only honest setting is the one already in the config.
|
|
224
|
+
// Same for COLOR_COMPATIBILITIES below.
|
|
168
225
|
export const CHARSETS = ["unicode", "ascii"] as const;
|
|
169
226
|
export type Charset = (typeof CHARSETS)[number];
|
|
170
227
|
|
|
@@ -192,3 +249,88 @@ export const COLOR_COMPATIBILITIES = [
|
|
|
192
249
|
"none",
|
|
193
250
|
] as const satisfies readonly ColorSystemSpec[];
|
|
194
251
|
export type ColorCompatibility = (typeof COLOR_COMPATIBILITIES)[number];
|
|
252
|
+
|
|
253
|
+
// --- Layout globals (autoWrap, padding) ---
|
|
254
|
+
//
|
|
255
|
+
// These two DO have a session half, unlike charset/colorCompatibility above:
|
|
256
|
+
// wrapping and cell padding are how much bar you want on your screen right now
|
|
257
|
+
// — a taste that legitimately differs between one session in a wide terminal
|
|
258
|
+
// and another in a split pane. Their floors and domains live here, beside the
|
|
259
|
+
// other globals vocabularies, because both the config loader (range validation,
|
|
260
|
+
// JSON-schema emit) and the render layer need them and config must not import
|
|
261
|
+
// render [LAW:one-way-deps]. src/render/strip.ts re-exports them so render-layer
|
|
262
|
+
// callers keep their existing import site.
|
|
263
|
+
|
|
264
|
+
// [LAW:one-source-of-truth] The one statement of the globals.autoWrap default
|
|
265
|
+
// (on — current behavior).
|
|
266
|
+
export const DEFAULT_WRAP = true;
|
|
267
|
+
|
|
268
|
+
// [LAW:one-source-of-truth] The spelling of a boolean as a SessionState string.
|
|
269
|
+
// SessionState holds strings, so "true"/"false" is the wire vocabulary for every
|
|
270
|
+
// boolean globals field — the same two members the bundled default's
|
|
271
|
+
// `cycle: [...]` toggle writes and the parse below reads. Spelled once so a
|
|
272
|
+
// toggle cannot write a member the resolver refuses to parse.
|
|
273
|
+
export const BOOLEAN_MEMBERS = ["true", "false"] as const;
|
|
274
|
+
|
|
275
|
+
// [LAW:one-source-of-truth] The one statement of the globals.padding default
|
|
276
|
+
// (one space per side inside each segment cell — current behavior, matching the
|
|
277
|
+
// legacy display.padding).
|
|
278
|
+
export const DEFAULT_PADDING = 1;
|
|
279
|
+
|
|
280
|
+
// [LAW:one-source-of-truth] THE padding domain: an integer, inclusive both ends.
|
|
281
|
+
// Read by the loader's `padding` int spec (config-file values), by the bundled
|
|
282
|
+
// default's stepper actions (`min`/`max`, which bound what a click may persist),
|
|
283
|
+
// and by the session parse below. When those were three copies of `0`/`16`, a
|
|
284
|
+
// widened range could land on the file and miss the clicks.
|
|
285
|
+
export const PADDING_RANGE = { min: 0, max: 16 } as const;
|
|
286
|
+
|
|
287
|
+
// [LAW:parse-dont-validate] A SessionState string to a boolean, or null for
|
|
288
|
+
// anything else. `??` in effectiveGlobal (never `||`) is what keeps a parsed
|
|
289
|
+
// `false` a real answer rather than falling through to the default.
|
|
290
|
+
function parseBoolean(raw: string): boolean | null {
|
|
291
|
+
return raw === "true" ? true : raw === "false" ? false : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// [LAW:parse-dont-validate] A SessionState string to a padding value inside the
|
|
295
|
+
// one declared range. The digits test comes first because `Number("")` is 0 and
|
|
296
|
+
// `Number(" 3 ")` is 3 — an empty or padded entry would otherwise parse to a
|
|
297
|
+
// value nobody wrote.
|
|
298
|
+
function parsePadding(raw: string): number | null {
|
|
299
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
300
|
+
const value = Number(raw);
|
|
301
|
+
return value >= PADDING_RANGE.min && value <= PADDING_RANGE.max
|
|
302
|
+
? value
|
|
303
|
+
: null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Whether a render wraps over-wide rows, as data. The session's pick over the
|
|
307
|
+
// config default over the on floor — `effectiveGlobal` with a boolean domain.
|
|
308
|
+
export function effectiveAutoWrap(
|
|
309
|
+
sessionAutoWrap: string | null,
|
|
310
|
+
globalsAutoWrap: boolean | undefined,
|
|
311
|
+
): boolean {
|
|
312
|
+
return effectiveGlobal(
|
|
313
|
+
sessionAutoWrap,
|
|
314
|
+
globalsAutoWrap,
|
|
315
|
+
DEFAULT_WRAP,
|
|
316
|
+
parseBoolean,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// The intra-cell padding a render uses, as data. A session value outside
|
|
321
|
+
// PADDING_RANGE falls through to the config default, the same rule every other
|
|
322
|
+
// field here follows: the gate already refuses out-of-range clicks, so a value
|
|
323
|
+
// that gets here is a stale entry from a narrower-since range, and the user's
|
|
324
|
+
// own default is the honest answer rather than a render at a width nobody
|
|
325
|
+
// chose.
|
|
326
|
+
export function effectivePadding(
|
|
327
|
+
sessionPadding: string | null,
|
|
328
|
+
globalsPadding: number | undefined,
|
|
329
|
+
): number {
|
|
330
|
+
return effectiveGlobal(
|
|
331
|
+
sessionPadding,
|
|
332
|
+
globalsPadding,
|
|
333
|
+
DEFAULT_PADDING,
|
|
334
|
+
parsePadding,
|
|
335
|
+
);
|
|
336
|
+
}
|