@promptctl/cc-candybar 1.36.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 +36 -36
- package/package.json +5 -5
- package/src/check.ts +4 -4
- package/src/config/default-dsl-config.ts +23 -8
- package/src/config/loader/globals.ts +6 -1
- package/src/config/presets.ts +8 -3
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -91,9 +91,9 @@
|
|
|
91
91
|
"mobx": "^6.15.0"
|
|
92
92
|
},
|
|
93
93
|
"optionalDependencies": {
|
|
94
|
-
"@promptctl/cc-candybar-darwin-arm64": "1.
|
|
95
|
-
"@promptctl/cc-candybar-darwin-x64": "1.
|
|
96
|
-
"@promptctl/cc-candybar-linux-x64": "1.
|
|
97
|
-
"@promptctl/cc-candybar-linux-arm64": "1.
|
|
94
|
+
"@promptctl/cc-candybar-darwin-arm64": "1.37.0",
|
|
95
|
+
"@promptctl/cc-candybar-darwin-x64": "1.37.0",
|
|
96
|
+
"@promptctl/cc-candybar-linux-x64": "1.37.0",
|
|
97
|
+
"@promptctl/cc-candybar-linux-arm64": "1.37.0"
|
|
98
98
|
}
|
|
99
99
|
}
|
package/src/check.ts
CHANGED
|
@@ -36,14 +36,14 @@ import {
|
|
|
36
36
|
effectiveLookName,
|
|
37
37
|
lookKeyByName,
|
|
38
38
|
effectiveStripStyle,
|
|
39
|
+
effectiveAutoWrap,
|
|
40
|
+
effectivePadding,
|
|
39
41
|
} from "./themes/policy.js";
|
|
40
42
|
import { paletteForThemeName } from "./themes/palette-resolvers.js";
|
|
41
43
|
import { effectivePresetName, presetGlobals } from "./config/presets.js";
|
|
42
44
|
import {
|
|
43
45
|
DEFAULT_CHARSET,
|
|
44
46
|
DEFAULT_COLOR_COMPATIBILITY,
|
|
45
|
-
DEFAULT_PADDING,
|
|
46
|
-
DEFAULT_WRAP,
|
|
47
47
|
} from "./render/strip.js";
|
|
48
48
|
import type { EffectiveGlobals } from "./daemon/render-payload.js";
|
|
49
49
|
|
|
@@ -321,8 +321,8 @@ function loadRegisterRender(
|
|
|
321
321
|
theme: effectiveThemeName(null, globals.palette),
|
|
322
322
|
look: effectiveLookName(null, globals.look, config.looks),
|
|
323
323
|
style: effectiveStripStyle(null, globals.style),
|
|
324
|
-
autoWrap: globals.autoWrap
|
|
325
|
-
padding: globals.padding
|
|
324
|
+
autoWrap: effectiveAutoWrap(null, globals.autoWrap),
|
|
325
|
+
padding: effectivePadding(null, globals.padding),
|
|
326
326
|
charset: globals.charset ?? DEFAULT_CHARSET,
|
|
327
327
|
colorCompatibility:
|
|
328
328
|
globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import type { DslConfig, LayoutNode, SegmentDecl } from "./dsl-types.js";
|
|
30
30
|
import { parseDslConfig } from "./dsl-loader.js";
|
|
31
|
+
import { BOOLEAN_MEMBERS, PADDING_RANGE } from "../themes/policy.js";
|
|
31
32
|
import { mergeWithDefault } from "./loader/merge.js";
|
|
32
33
|
|
|
33
34
|
// ─── Shared template fragments ───────────────────────────────────────────────
|
|
@@ -250,7 +251,11 @@ function etaHeatFg(etaRef: string, warnRef: string): string {
|
|
|
250
251
|
// theme/style/look/preset (session `set` for a per-conversation preview, PLUS
|
|
251
252
|
// a persist-forever twin — candybar-config-engine-71o.5, brandon-presets-0yk.3
|
|
252
253
|
// — for pinning the choice as everyone's default), the four .3 globals
|
|
253
|
-
// steppers (persist-only
|
|
254
|
+
// steppers (persist-only HERE — `autoWrap`/`padding` have gained a session
|
|
255
|
+
// half since, but the drawer's own controls stay durable-by-default until
|
|
256
|
+
// candybar-settings-ui-aok.3 gives the panel one `persist?` toggle instead of
|
|
257
|
+
// a spelling per row; `charset`/`colorCompatibility` have no session half at
|
|
258
|
+
// all, by design), and one .6
|
|
254
259
|
// segment-scoped persist control (directoryPaletteControl, persist-only like
|
|
255
260
|
// the four steppers — a per-segment palette pin, not a whole-bar default).
|
|
256
261
|
// Placed as a sibling in row 1's horizontal container, toggled from beside the
|
|
@@ -1222,8 +1227,11 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
1222
1227
|
// ── The four .3 globals steppers, folded into the settingsDrawer group
|
|
1223
1228
|
// (candybar-config-engine-71o.4) alongside theme/style/look above. Each
|
|
1224
1229
|
// pairs a `persist` control with a `↺` reset (docs' persist/reset
|
|
1225
|
-
// convention) —
|
|
1226
|
-
// is their only seam,
|
|
1230
|
+
// convention) — `charset`/`colorCompatibility` have no SessionState half
|
|
1231
|
+
// at all, so persist is genuinely their only seam, while
|
|
1232
|
+
// `autoWrap`/`padding` do have one and are simply spelled durable here
|
|
1233
|
+
// (candybar-settings-ui-aok.3 owns whether the panel offers the choice).
|
|
1234
|
+
// Labels
|
|
1227
1235
|
// read `.field.effective` (the daemon-resolved value BuildLineOptions
|
|
1228
1236
|
// actually rendered with), never a restated literal.
|
|
1229
1237
|
charsetControl: {
|
|
@@ -1421,8 +1429,11 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
1421
1429
|
// (candybar-config-engine-71o.4), decoupled by NAME from
|
|
1422
1430
|
// charsetControl/colorCompatControl/wrapToggleControl/paddingControl
|
|
1423
1431
|
// below. Unlike theme/style/look (a per-session experiment via `set`),
|
|
1424
|
-
// these four
|
|
1425
|
-
//
|
|
1432
|
+
// these four are spelled durable: `charset`/`colorCompatibility` have no
|
|
1433
|
+
// SessionState half at all, and `autoWrap`/`padding` have one
|
|
1434
|
+
// (candybar-settings-ui-aok.2) but keep persist as the DRAWER's spelling
|
|
1435
|
+
// until .3 collapses the twins behind one `persist?`. So every one of
|
|
1436
|
+
// these writes the
|
|
1426
1437
|
// config-file DEFAULT through the daemon-owned overrides layer (never the
|
|
1427
1438
|
// hand-authored file itself), gated by the SAME deriveActionValidators
|
|
1428
1439
|
// pass as a `set` (persist mirrors set's value-source shapes one for
|
|
@@ -1435,10 +1446,14 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
1435
1446
|
from: "colorCompatibilities",
|
|
1436
1447
|
},
|
|
1437
1448
|
resetColorCompat: { reset: "colorCompatibility" },
|
|
1438
|
-
|
|
1449
|
+
// [LAW:one-source-of-truth] The toggle's members and the stepper's bounds
|
|
1450
|
+
// are the SAME literals the resolvers parse a session pick with
|
|
1451
|
+
// (BOOLEAN_MEMBERS / PADDING_RANGE in themes/policy.ts) — a click cannot
|
|
1452
|
+
// write a value the render's own resolution would then refuse.
|
|
1453
|
+
toggleWrapForever: { persist: "autoWrap", cycle: [...BOOLEAN_MEMBERS] },
|
|
1439
1454
|
resetAutoWrap: { reset: "autoWrap" },
|
|
1440
|
-
paddingDownForever: { persist: "padding",
|
|
1441
|
-
paddingUpForever: { persist: "padding",
|
|
1455
|
+
paddingDownForever: { persist: "padding", ...PADDING_RANGE, by: -1 },
|
|
1456
|
+
paddingUpForever: { persist: "padding", ...PADDING_RANGE, by: 1 },
|
|
1442
1457
|
resetPadding: { reset: "padding" },
|
|
1443
1458
|
|
|
1444
1459
|
// [LAW:locality-or-seam] The segment-palette control's behavior
|
|
@@ -7,6 +7,7 @@ import { type Globals } from "../dsl-types.js";
|
|
|
7
7
|
import {
|
|
8
8
|
CHARSETS,
|
|
9
9
|
COLOR_COMPATIBILITIES,
|
|
10
|
+
PADDING_RANGE,
|
|
10
11
|
STRIP_STYLES,
|
|
11
12
|
type ColorCompatibility,
|
|
12
13
|
} from "../../themes/policy.js";
|
|
@@ -84,7 +85,11 @@ const GLOBALS_FIELDS: FieldSpecMap<Globals> = {
|
|
|
84
85
|
// drive an unbounded `" ".repeat` allocation in the daemon
|
|
85
86
|
// [LAW:no-silent-failure] — an absurd value is a loud load error, not a
|
|
86
87
|
// silently-huge render.
|
|
87
|
-
|
|
88
|
+
// [LAW:one-source-of-truth] The bound comes from PADDING_RANGE, the same
|
|
89
|
+
// literal the bundled default's stepper actions bound clicks by and the
|
|
90
|
+
// session-half parse admits values from — so the file, the click, and the
|
|
91
|
+
// session pick cannot end up honouring three different ranges.
|
|
92
|
+
padding: optionalIntSpec(PADDING_RANGE),
|
|
88
93
|
// [LAW:types-are-the-program] Closed enum like `style`: the joiner glyph
|
|
89
94
|
// vocabularies pickJoiner can render — validates by membership, emits a
|
|
90
95
|
// JSON-Schema `enum` from the same CHARSETS literal.
|
package/src/config/presets.ts
CHANGED
|
@@ -168,9 +168,14 @@ export function presetGlobals(config: DslConfig, name: string): Globals {
|
|
|
168
168
|
// that is a typo in a file the author can see and fix — but a value that
|
|
169
169
|
// only exists because the overrides layer supplied it gets the SAME
|
|
170
170
|
// treatment a stale SessionState pick already gets from effectivePresetName:
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
171
|
+
// an undeclared name is not a pick at all, so the resolution moves on to the
|
|
172
|
+
// next rung of the precedence chain — visibly, never a throw. (For a stale
|
|
173
|
+
// SESSION pick that next rung is the config's own `globals.preset`, and only
|
|
174
|
+
// then the floor; for the field dropped here it is whatever the config file
|
|
175
|
+
// itself declares. Same rule, different starting rung —
|
|
176
|
+
// see effectiveGlobal in themes/policy.ts.) Dropping the field here (so it
|
|
177
|
+
// never reaches validateConfig) is what applies that rule to this layer too,
|
|
178
|
+
// instead of a fatal error replacing the whole bar.
|
|
174
179
|
export function sanitizePersistedPresetOverride(
|
|
175
180
|
globalsOverride: Partial<Globals>,
|
|
176
181
|
declaredPresets: Readonly<Record<string, PresetDecl>>,
|
|
@@ -48,11 +48,13 @@ import type {
|
|
|
48
48
|
// BuildLineOptions), so the value a trigger label displays and the value
|
|
49
49
|
// that actually shaped the render can never disagree — the same reasoning
|
|
50
50
|
// theme/look already followed, generalized to every globals field a menu or
|
|
51
|
-
// stepper can persist. `theme`/`look`/`style` compose
|
|
52
|
-
// config default (a session pick can diverge from the
|
|
53
|
-
// its own session); `charset
|
|
54
|
-
// no SessionState half
|
|
55
|
-
//
|
|
51
|
+
// stepper can persist. `theme`/`look`/`style`/`autoWrap`/`padding` compose
|
|
52
|
+
// SessionState over the config default (a session pick can diverge from the
|
|
53
|
+
// persisted default for its own session); `charset` and `colorCompatibility`
|
|
54
|
+
// have no SessionState half — they describe the terminal (glyph coverage,
|
|
55
|
+
// colour depth) rather than a per-session taste, so the resolved config global
|
|
56
|
+
// over its floor constant is their whole resolution. See CHARSETS in
|
|
57
|
+
// themes/policy.ts for why that is a decision rather than a gap.
|
|
56
58
|
export interface EffectiveGlobals {
|
|
57
59
|
readonly theme: string;
|
|
58
60
|
readonly look: string;
|
package/src/daemon/server.ts
CHANGED
|
@@ -65,6 +65,8 @@ import { expandHome } from "../config/dsl-loader.js";
|
|
|
65
65
|
import { renderDsl } from "../dsl/render.js";
|
|
66
66
|
import {
|
|
67
67
|
effectiveStripStyle,
|
|
68
|
+
effectiveAutoWrap,
|
|
69
|
+
effectivePadding,
|
|
68
70
|
effectiveThemeName,
|
|
69
71
|
effectiveLookName,
|
|
70
72
|
lookKeyByName,
|
|
@@ -867,11 +869,12 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
|
|
|
867
869
|
//
|
|
868
870
|
// [LAW:one-source-of-truth] Every globals field a menu/stepper can
|
|
869
871
|
// persist (candybar-config-engine-71o.3), resolved ONCE into one
|
|
870
|
-
// struct: theme/look/style compose the session's click
|
|
871
|
-
// over the config default over a floor — a click
|
|
872
|
-
// whole bar on the next render; charset
|
|
873
|
-
//
|
|
874
|
-
//
|
|
872
|
+
// struct: theme/look/style/autoWrap/padding compose the session's click
|
|
873
|
+
// (SessionState) over the config default over a floor — a click
|
|
874
|
+
// recolors/reshapes the whole bar on the next render; charset and
|
|
875
|
+
// colorCompatibility describe the TERMINAL rather than a taste and so
|
|
876
|
+
// have no SessionState half at all, making the config global over its
|
|
877
|
+
// floor constant their whole resolution. This struct feeds
|
|
875
878
|
// BOTH the payload's `*.effective` fields (trigger labels) AND
|
|
876
879
|
// renderOpts below (the actual render) — one resolution, two readers,
|
|
877
880
|
// so a label can never disagree with what was rendered.
|
|
@@ -918,8 +921,18 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
|
|
|
918
921
|
sessionState.get(req.hookData.session_id, "style"),
|
|
919
922
|
globals.style,
|
|
920
923
|
),
|
|
921
|
-
autoWrap:
|
|
922
|
-
|
|
924
|
+
autoWrap: effectiveAutoWrap(
|
|
925
|
+
sessionState.get(req.hookData.session_id, "autoWrap"),
|
|
926
|
+
globals.autoWrap,
|
|
927
|
+
),
|
|
928
|
+
padding: effectivePadding(
|
|
929
|
+
sessionState.get(req.hookData.session_id, "padding"),
|
|
930
|
+
globals.padding,
|
|
931
|
+
),
|
|
932
|
+
// charset and colorCompatibility have no session half by design —
|
|
933
|
+
// they describe the terminal, not a taste (see CHARSETS in
|
|
934
|
+
// themes/policy.ts) — so the config global over its floor is their
|
|
935
|
+
// whole resolution.
|
|
923
936
|
charset: globals.charset ?? DEFAULT_CHARSET,
|
|
924
937
|
colorCompatibility:
|
|
925
938
|
globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
|
package/src/demo/dsl.ts
CHANGED
|
@@ -33,6 +33,8 @@ import { listResolvablePaletteNames } from "../themes/policy.js";
|
|
|
33
33
|
import {
|
|
34
34
|
effectiveThemeName,
|
|
35
35
|
effectiveLookName,
|
|
36
|
+
effectiveAutoWrap,
|
|
37
|
+
effectivePadding,
|
|
36
38
|
lookKeyByName,
|
|
37
39
|
paletteForThemeName,
|
|
38
40
|
} from "../themes/index.js";
|
|
@@ -41,9 +43,7 @@ import { registerDslConfig, renderDsl } from "../dsl/render.js";
|
|
|
41
43
|
import {
|
|
42
44
|
DEFAULT_CHARSET,
|
|
43
45
|
DEFAULT_COLOR_COMPATIBILITY,
|
|
44
|
-
DEFAULT_PADDING,
|
|
45
46
|
DEFAULT_TERMINAL_WIDTH,
|
|
46
|
-
DEFAULT_WRAP,
|
|
47
47
|
} from "../render/strip.js";
|
|
48
48
|
import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
|
|
49
49
|
|
|
@@ -139,10 +139,11 @@ try {
|
|
|
139
139
|
width: applyClaudeCodeReserve(
|
|
140
140
|
process.stdout.columns ?? DEFAULT_TERMINAL_WIDTH,
|
|
141
141
|
),
|
|
142
|
-
// Same
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
// Same resolvers the daemon applies, with a null session pick — the
|
|
143
|
+
// demo has no SessionState, so both land on the config default over
|
|
144
|
+
// their floor.
|
|
145
|
+
wrap: effectiveAutoWrap(null, globals.autoWrap),
|
|
146
|
+
padding: effectivePadding(null, globals.padding),
|
|
146
147
|
charset: globals.charset ?? DEFAULT_CHARSET,
|
|
147
148
|
},
|
|
148
149
|
undefined,
|
package/src/render/strip.ts
CHANGED
|
@@ -37,16 +37,12 @@ export type { Charset, ColorCompatibility, StripStyle };
|
|
|
37
37
|
// applyClaudeCodeReserve from src/utils/terminal-width).
|
|
38
38
|
export const DEFAULT_TERMINAL_WIDTH = 120;
|
|
39
39
|
|
|
40
|
-
// [LAW:one-source-of-truth]
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// default (one space per side inside each segment cell — current behavior,
|
|
47
|
-
// matching the legacy display.padding). Every resolver of the config global
|
|
48
|
-
// (`globals.padding ?? DEFAULT_PADDING`) derives from this constant.
|
|
49
|
-
export const DEFAULT_PADDING = 1;
|
|
40
|
+
// [LAW:one-source-of-truth] autoWrap's and padding's defaults live in
|
|
41
|
+
// themes/policy.ts beside their resolvers and padding's range, because the
|
|
42
|
+
// config loader needs them too and config must not import render
|
|
43
|
+
// [LAW:one-way-deps]. Re-exported here on the same terms as the types above, so
|
|
44
|
+
// render-layer consumers keep importing them from the strip module.
|
|
45
|
+
export { DEFAULT_WRAP, DEFAULT_PADDING } from "../themes/policy.js";
|
|
50
46
|
|
|
51
47
|
// [LAW:one-source-of-truth] The one statement of the globals.charset default
|
|
52
48
|
// (powerline unicode glyphs — current behavior, matching the legacy
|
package/src/themes/index.ts
CHANGED
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
|
+
}
|