@promptctl/cc-candybar 1.23.0 → 1.24.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 +71 -71
- package/package.json +5 -5
- package/src/check.ts +2 -1
- package/src/config/default-dsl-config.ts +120 -10
- package/src/config/dsl-loader.ts +13 -5
- package/src/config/loader/merge.ts +15 -7
- package/src/daemon/cache/render.ts +5 -1
- package/src/demo/dsl.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.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.24.0",
|
|
95
|
+
"@promptctl/cc-candybar-darwin-x64": "1.24.0",
|
|
96
|
+
"@promptctl/cc-candybar-linux-x64": "1.24.0",
|
|
97
|
+
"@promptctl/cc-candybar-linux-arm64": "1.24.0"
|
|
98
98
|
}
|
|
99
99
|
}
|
package/src/check.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
ConfigError,
|
|
26
26
|
} from "./config/dsl-loader.js";
|
|
27
27
|
import { expandHome } from "./config/loader/discovery.js";
|
|
28
|
+
import { DEFAULT_DSL_CONFIG } from "./config/default-dsl-config.js";
|
|
28
29
|
import { VariableStore } from "./var-system/store.js";
|
|
29
30
|
import { SourceRegistry } from "./var-system/sources.js";
|
|
30
31
|
import { SessionState } from "./daemon/session-state.js";
|
|
@@ -250,7 +251,7 @@ function loadRegisterRender(
|
|
|
250
251
|
cwd: string,
|
|
251
252
|
warnings: string[],
|
|
252
253
|
): string {
|
|
253
|
-
const { config: merged, source } = loadConfig(configPath);
|
|
254
|
+
const { config: merged, source } = loadConfig(configPath, DEFAULT_DSL_CONFIG);
|
|
254
255
|
const config = validateConfig(merged, configPath ?? "<default>", source);
|
|
255
256
|
|
|
256
257
|
const store = new VariableStore();
|
|
@@ -19,12 +19,16 @@
|
|
|
19
19
|
// (add its name to the children), not new code. The same data flows through
|
|
20
20
|
// the same render path whether the root has 1 leaf or 16.
|
|
21
21
|
//
|
|
22
|
-
// [LAW:types-are-the-program]
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// [LAW:types-are-the-program] The authored literal (RAW_DEFAULT_DSL_CONFIG)
|
|
23
|
+
// uses `satisfies DslConfig` (not an annotation) so every declared segment
|
|
24
|
+
// and variable name is checked against the real shape at the point of
|
|
25
|
+
// authoring. The exported DEFAULT_DSL_CONFIG is that literal run through the
|
|
26
|
+
// loader's own synthesis pass (see the bottom of this file) — a `DslConfig`,
|
|
27
|
+
// the same effective shape every user config resolves to.
|
|
26
28
|
|
|
27
|
-
import type { DslConfig } from "./dsl-types.js";
|
|
29
|
+
import type { DslConfig, SegmentDecl } from "./dsl-types.js";
|
|
30
|
+
import { parseDslConfig } from "./dsl-loader.js";
|
|
31
|
+
import { mergeWithDefault } from "./loader/merge.js";
|
|
28
32
|
|
|
29
33
|
// ─── Shared template fragments ───────────────────────────────────────────────
|
|
30
34
|
//
|
|
@@ -144,7 +148,15 @@ function etaHeatFg(etaRef: string, warnRef: string): string {
|
|
|
144
148
|
|
|
145
149
|
// ─── The default config ──────────────────────────────────────────────────────
|
|
146
150
|
|
|
147
|
-
|
|
151
|
+
// [LAW:one-source-of-truth] The AUTHORED literal, pre-synthesis. Production
|
|
152
|
+
// code wants the synthesized DEFAULT_DSL_CONFIG below; this is exported only
|
|
153
|
+
// for tests that round-trip "what a user would get by copy-pasting the
|
|
154
|
+
// bundled default into their own file" through the real per-file parse —
|
|
155
|
+
// that round-trip must start from the AUTHORED declarations, never from
|
|
156
|
+
// DEFAULT_DSL_CONFIG's own already-synthesized `menus.*` entries (reparsing
|
|
157
|
+
// those would trip the reserved-namespace guard, which exists to catch a
|
|
158
|
+
// user hand-declaring a name only synthesis may write).
|
|
159
|
+
export const RAW_DEFAULT_DSL_CONFIG = {
|
|
148
160
|
globals: {
|
|
149
161
|
// Picked by the daemon's basePalette resolution; user overrides in their
|
|
150
162
|
// own config. catppuccin-latte ships every spec name the default
|
|
@@ -814,20 +826,56 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
814
826
|
bg: "surface",
|
|
815
827
|
fg: "foreground",
|
|
816
828
|
},
|
|
829
|
+
// Theme control — the palette switcher, wired into the DEFAULT bar
|
|
830
|
+
// (brandon-theming-8uj.1) so theme selection is discoverable without
|
|
831
|
+
// reading docs/interaction-authoring.md or hand-authoring a config.
|
|
832
|
+
// [LAW:one-source-of-truth] The trigger reads `.theme.effective` — the
|
|
833
|
+
// SAME daemon-resolved name (effectiveThemeName) the rendered basePalette
|
|
834
|
+
// is built from — so the label and the colors can never drift; unlike
|
|
835
|
+
// styleControl (no "effective style" input exists), no extra `state`
|
|
836
|
+
// variable is needed here. Shares the "pickers" accordion key with
|
|
837
|
+
// lookControl so opening one closes the other, the docs' canonical
|
|
838
|
+
// two-menu pairing.
|
|
839
|
+
themeControl: {
|
|
840
|
+
template:
|
|
841
|
+
"🎨 {{ .theme.effective }} " +
|
|
842
|
+
'{{ menu "applyTheme" (dict "key" "pickers") }}',
|
|
843
|
+
bg: "surface",
|
|
844
|
+
fg: "foreground",
|
|
845
|
+
},
|
|
846
|
+
// Look control — the theme-ADAPTATION switcher (see the `looks` block
|
|
847
|
+
// below), the exact twin of themeControl one dimension over:
|
|
848
|
+
// `.look.effective` is the daemon-resolved name (effectiveLookName) the
|
|
849
|
+
// rendered ThemeKey composes from. closeOnPick collapses the drop after a
|
|
850
|
+
// pick — looks are tried one at a time against the chosen theme, not
|
|
851
|
+
// stacked open.
|
|
852
|
+
lookControl: {
|
|
853
|
+
template:
|
|
854
|
+
"◐ {{ .look.effective }} " +
|
|
855
|
+
'{{ menu "applyLook" (dict "key" "pickers" "closeOnPick" true) }}',
|
|
856
|
+
bg: "surface",
|
|
857
|
+
fg: "foreground",
|
|
858
|
+
},
|
|
817
859
|
},
|
|
818
860
|
|
|
819
861
|
// Default layout — the canonical LayoutNode tree (`satisfies DslConfig`
|
|
820
862
|
// requires the lowered form here; the terse Option-A `{ h/v/seg }` grammar is
|
|
821
863
|
// the loader's authoring surface for user JSON, not this typed literal).
|
|
822
864
|
//
|
|
823
|
-
//
|
|
865
|
+
// Three rows stacked by the vertical container: an IDENTITY + ACTIONS row
|
|
824
866
|
// (where am I / what can I do here — the directory, the verbose `gitaculous`
|
|
825
867
|
// line (repo, sha, working-tree, upstream, stash, time-since-commit), then the
|
|
826
868
|
// quick-action tray: copy session id, open project / transcript in the editor)
|
|
827
869
|
// over a STATUS row (what's happening now — model, context-window fill,
|
|
828
|
-
// prompt-cache warmth, and the 5h / 7d rate-limit quotas)
|
|
829
|
-
//
|
|
830
|
-
//
|
|
870
|
+
// prompt-cache warmth, and the 5h / 7d rate-limit quotas) over an APPEARANCE
|
|
871
|
+
// row (theme + look pickers, brandon-theming-8uj.1 — the bundled default's
|
|
872
|
+
// only on-bar affordance to discover and use the theme/look feature without
|
|
873
|
+
// reading docs or hand-authoring a config). The tray sits on the identity
|
|
874
|
+
// row because its actions are workspace-scoped (this session, this
|
|
875
|
+
// project), not usage metrics; the pickers get their own row rather than
|
|
876
|
+
// crowding the identity row because a `{{ menu }}` drops its picker body
|
|
877
|
+
// onto the line immediately below its row, and that drop must not land on
|
|
878
|
+
// top of an unrelated row's content. Each row zips its segments through the
|
|
831
879
|
// powerline joiner; `\n` separates the rows.
|
|
832
880
|
//
|
|
833
881
|
// [LAW:dataflow-not-control-flow] Every status segment is when-gated on its
|
|
@@ -861,6 +909,14 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
861
909
|
{ kind: "segment", name: "weekly" },
|
|
862
910
|
],
|
|
863
911
|
},
|
|
912
|
+
{
|
|
913
|
+
kind: "container",
|
|
914
|
+
direction: "horizontal",
|
|
915
|
+
children: [
|
|
916
|
+
{ kind: "segment", name: "themeControl" },
|
|
917
|
+
{ kind: "segment", name: "lookControl" },
|
|
918
|
+
],
|
|
919
|
+
},
|
|
864
920
|
],
|
|
865
921
|
},
|
|
866
922
|
|
|
@@ -895,6 +951,17 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
895
951
|
// rendered click and the wire gate share that one source — a template cannot
|
|
896
952
|
// smuggle an un-gated style write.
|
|
897
953
|
applyStyle: { set: "style", from: "styles" },
|
|
954
|
+
|
|
955
|
+
// [LAW:locality-or-seam] The theme/look pickers' behaviors, decoupled by
|
|
956
|
+
// NAME from themeControl/lookControl {{ menu }}s above — same seam as
|
|
957
|
+
// applyStyle. "theme" is the baseline permanent state key (validateTheme,
|
|
958
|
+
// registered in state-validators.ts); dropBaselineAllowLists reuses that
|
|
959
|
+
// gate for an allow-list contribution instead of re-registering it, so
|
|
960
|
+
// this action derives nothing new. "look" has no baseline entry, so this
|
|
961
|
+
// action derives a fresh allow-list validator ranging the merged `looks`
|
|
962
|
+
// block's names — the same derivation test/dsl-looks.test.ts exercises.
|
|
963
|
+
applyTheme: { set: "theme", from: "themes" },
|
|
964
|
+
applyLook: { set: "look", from: "looks" },
|
|
898
965
|
},
|
|
899
966
|
|
|
900
967
|
// ─── Looks ───────────────────────────────────────────────────────────────
|
|
@@ -1058,3 +1125,46 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
1058
1125
|
"{{ else }}{{ . }}m{{ end }}",
|
|
1059
1126
|
},
|
|
1060
1127
|
} satisfies DslConfig;
|
|
1128
|
+
|
|
1129
|
+
// [LAW:locality-or-seam] The palette names this module-level parse is allowed
|
|
1130
|
+
// to accept — DERIVED from RAW_DEFAULT_DSL_CONFIG itself (globals.palette +
|
|
1131
|
+
// every per-segment palette: pin), never from the live theme registry
|
|
1132
|
+
// (listResolvablePaletteNames()). A real user file must validate against the
|
|
1133
|
+
// live registry (an author can type any name); this file validates against
|
|
1134
|
+
// ITSELF (every name here is a literal we wrote and every render test below
|
|
1135
|
+
// exercises against the real registry already). This is what keeps the
|
|
1136
|
+
// module-load parse below from ever depending on the registry being healthy
|
|
1137
|
+
// at import time — a registry-loading bug elsewhere would surface where it
|
|
1138
|
+
// actually matters (a real render failing), never as an uncatchable crash on
|
|
1139
|
+
// every importer of this file before any daemon/CLI error handling runs.
|
|
1140
|
+
const AUTHORED_PALETTE_NAMES = new Set(
|
|
1141
|
+
[
|
|
1142
|
+
RAW_DEFAULT_DSL_CONFIG.globals.palette,
|
|
1143
|
+
...(Object.values(RAW_DEFAULT_DSL_CONFIG.segments) as SegmentDecl[]).map(
|
|
1144
|
+
(s) => s.palette,
|
|
1145
|
+
),
|
|
1146
|
+
].filter((name): name is string => name !== undefined),
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
// [LAW:single-enforcer] Run the authored literal through the SAME
|
|
1150
|
+
// parse → synthesize pipeline every user config goes through (JSON5 stage +
|
|
1151
|
+
// synthesizeMenuDecls' `menus.*` synthesis, and any future group/menu
|
|
1152
|
+
// synthesis pass) instead of hand-duplicating that logic here. Without this,
|
|
1153
|
+
// the zero-config daemon path (loadConfig: no config file found ⇒ raw={},
|
|
1154
|
+
// merged directly against this constant — see src/config/dsl-loader.ts and
|
|
1155
|
+
// src/config/loader/merge.ts) would ship an UNSYNTHESIZED default: a
|
|
1156
|
+
// `{{ menu (dict "key" …) }}` accordion pairing (themeControl/lookControl,
|
|
1157
|
+
// brandon-theming-8uj.1) would render its glyph, but clicking it would reject
|
|
1158
|
+
// with "unknown state key" — the synthesis that derives a menu's `menus.*`
|
|
1159
|
+
// state var + cycle action only ever ran over TEXT a user typed, never over
|
|
1160
|
+
// this TS literal. Round-tripping through JSON is exactly what
|
|
1161
|
+
// test/default-dsl-config.test.ts's SERIALIZED-based tests already exercise,
|
|
1162
|
+
// so this is the same well-tested path, run once here instead of skipped.
|
|
1163
|
+
export const DEFAULT_DSL_CONFIG: DslConfig = mergeWithDefault(
|
|
1164
|
+
parseDslConfig(
|
|
1165
|
+
"<default>",
|
|
1166
|
+
JSON.stringify(RAW_DEFAULT_DSL_CONFIG),
|
|
1167
|
+
AUTHORED_PALETTE_NAMES,
|
|
1168
|
+
),
|
|
1169
|
+
RAW_DEFAULT_DSL_CONFIG,
|
|
1170
|
+
);
|
package/src/config/dsl-loader.ts
CHANGED
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
type RawDslConfig,
|
|
31
31
|
type ValidatedConfig,
|
|
32
32
|
} from "./dsl-types.js";
|
|
33
|
-
import { DEFAULT_DSL_CONFIG } from "./default-dsl-config.js";
|
|
34
33
|
import { listResolvablePaletteNames } from "../themes/policy.js";
|
|
35
34
|
import {
|
|
36
35
|
ConfigError,
|
|
@@ -78,14 +77,23 @@ export {
|
|
|
78
77
|
// ─── Three-stage pipeline ────────────────────────────────────────────────────
|
|
79
78
|
|
|
80
79
|
/**
|
|
81
|
-
* Load a JSON5 DSL config file from disk and merge it with the
|
|
80
|
+
* Load a JSON5 DSL config file from disk and merge it with the given
|
|
82
81
|
* default. Returns the effective DslConfig AND the raw source text.
|
|
83
82
|
*
|
|
84
|
-
* `path = null` means "no user file exists" — returns
|
|
85
|
-
* (uniform merge against an empty raw, which is deep-equal to
|
|
83
|
+
* `path = null` means "no user file exists" — returns `dflt` unchanged
|
|
84
|
+
* (uniform merge against an empty raw, which is deep-equal to `dflt`) and
|
|
86
85
|
* an empty source. No consumer branches on file presence; that branch lives
|
|
87
86
|
* inside loadConfig exactly once.
|
|
88
87
|
*
|
|
88
|
+
* [LAW:one-way-deps] `dflt` is a required parameter, not a default pointing at
|
|
89
|
+
* DEFAULT_DSL_CONFIG: this module is generic merge/parse machinery, and
|
|
90
|
+
* DEFAULT_DSL_CONFIG is a specific, higher-level instance built ON TOP of it
|
|
91
|
+
* (default-dsl-config.ts imports parseDslConfig/mergeWithDefault to
|
|
92
|
+
* synthesize itself — see that file). A default param here pointing back at
|
|
93
|
+
* DEFAULT_DSL_CONFIG would make this generic module depend on its own
|
|
94
|
+
* specific consumer — a cycle every caller who wants "the bundled default"
|
|
95
|
+
* resolves explicitly by importing DEFAULT_DSL_CONFIG themselves.
|
|
96
|
+
*
|
|
89
97
|
* [LAW:one-source-of-truth] The source is returned alongside the config so the
|
|
90
98
|
* caller can hand it to validateConfig — cross-ref diagnostics (line numbers,
|
|
91
99
|
* the authored-surface discriminator) are derived from it, and the file is read
|
|
@@ -99,7 +107,7 @@ export {
|
|
|
99
107
|
*/
|
|
100
108
|
export function loadConfig(
|
|
101
109
|
path: string | null,
|
|
102
|
-
dflt: DslConfig
|
|
110
|
+
dflt: DslConfig,
|
|
103
111
|
allowedPalettes?: ReadonlySet<string>,
|
|
104
112
|
): { config: DslConfig; source: string } {
|
|
105
113
|
const source = path === null ? "" : fs.readFileSync(path, "utf-8");
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
// [LAW:one-source-of-truth] The single point that
|
|
2
|
-
// fill missing keys. A user file declares only
|
|
3
|
-
// (shallow-merge globals, by-name merge
|
|
4
|
-
// root replacement) is the one place
|
|
5
|
-
// This file changes when the merge
|
|
1
|
+
// [LAW:one-source-of-truth] The single point that merges a raw user config
|
|
2
|
+
// onto a default DslConfig to fill missing keys. A user file declares only
|
|
3
|
+
// what differs; the cascade here (shallow-merge globals, by-name merge
|
|
4
|
+
// variables/segments/actions, wholesale root replacement) is the one place
|
|
5
|
+
// "absent means inherit" is decided. This file changes when the merge
|
|
6
|
+
// semantics change.
|
|
7
|
+
//
|
|
8
|
+
// [LAW:one-way-deps] `dflt` is a required parameter — this module is generic
|
|
9
|
+
// merge machinery and does not know about DEFAULT_DSL_CONFIG, the specific
|
|
10
|
+
// bundled instance built ON TOP of it (default-dsl-config.ts imports this
|
|
11
|
+
// function to synthesize itself). A default param pointing back at
|
|
12
|
+
// DEFAULT_DSL_CONFIG would make this generic module depend on its own
|
|
13
|
+
// specific consumer, a cycle. Callers who want "the bundled default" import
|
|
14
|
+
// DEFAULT_DSL_CONFIG from default-dsl-config.ts and pass it explicitly.
|
|
6
15
|
|
|
7
16
|
import { type DslConfig, type RawDslConfig } from "../dsl-types.js";
|
|
8
|
-
import { DEFAULT_DSL_CONFIG } from "../default-dsl-config.js";
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Merge a RawDslConfig on top of a default DslConfig. Pure function.
|
|
@@ -21,7 +29,7 @@ import { DEFAULT_DSL_CONFIG } from "../default-dsl-config.js";
|
|
|
21
29
|
*/
|
|
22
30
|
export function mergeWithDefault(
|
|
23
31
|
raw: RawDslConfig,
|
|
24
|
-
dflt: DslConfig
|
|
32
|
+
dflt: DslConfig,
|
|
25
33
|
): DslConfig {
|
|
26
34
|
return {
|
|
27
35
|
globals: { ...dflt.globals, ...(raw.globals ?? {}) },
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ConfigError,
|
|
12
12
|
} from "../../config/dsl-loader.js";
|
|
13
13
|
import type { ValidatedConfig } from "../../config/dsl-types.js";
|
|
14
|
+
import { DEFAULT_DSL_CONFIG } from "../../config/default-dsl-config.js";
|
|
14
15
|
import { registerDslConfig, type CompiledConfig } from "../../dsl/render.js";
|
|
15
16
|
import {
|
|
16
17
|
deriveActionValidators,
|
|
@@ -272,7 +273,10 @@ export class RenderCache {
|
|
|
272
273
|
// cross-ref diagnostics on the daemon path carry real line numbers and the
|
|
273
274
|
// authored-surface (root vs layout) discriminator works — the file is read
|
|
274
275
|
// once inside loadConfig, not re-read here.
|
|
275
|
-
const { config: merged, source } = loadConfig(
|
|
276
|
+
const { config: merged, source } = loadConfig(
|
|
277
|
+
resolvedPath,
|
|
278
|
+
DEFAULT_DSL_CONFIG,
|
|
279
|
+
);
|
|
276
280
|
const config = validateConfig(merged, resolvedPath ?? "<default>", source);
|
|
277
281
|
|
|
278
282
|
const store = new VariableStore();
|
package/src/demo/dsl.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
mergeWithDefault,
|
|
26
26
|
validateConfig,
|
|
27
27
|
} from "../config/dsl-loader.js";
|
|
28
|
+
import { DEFAULT_DSL_CONFIG } from "../config/default-dsl-config.js";
|
|
28
29
|
import { VariableStore } from "../var-system/store.js";
|
|
29
30
|
import { SourceRegistry } from "../var-system/sources.js";
|
|
30
31
|
import { SessionState } from "../daemon/session-state.js";
|
|
@@ -60,7 +61,7 @@ const source = readFileSync(configPath, "utf-8");
|
|
|
60
61
|
// only `ValidatedConfig`, so the chain is type-enforced.
|
|
61
62
|
const ALLOWED = new Set(listResolvablePaletteNames());
|
|
62
63
|
const raw = parseDslConfig(configPath, source, ALLOWED);
|
|
63
|
-
const merged = mergeWithDefault(raw);
|
|
64
|
+
const merged = mergeWithDefault(raw, DEFAULT_DSL_CONFIG);
|
|
64
65
|
const config = validateConfig(merged, configPath, source, ALLOWED);
|
|
65
66
|
|
|
66
67
|
// One Claude Code status-line hook event, faked. The `input` vars in the
|