@promptctl/cc-candybar 1.26.0 → 1.27.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 +86 -85
- package/package.json +6 -6
- package/schema/cc-candybar.schema.json +193 -4
- package/src/check.ts +49 -27
- package/src/click/wire.ts +16 -0
- package/src/config/action.ts +57 -22
- package/src/config/default-dsl-config.ts +424 -55
- package/src/config/dsl-loader.ts +14 -2
- package/src/config/dsl-types.ts +59 -0
- package/src/config/loader/actions.ts +283 -109
- package/src/config/loader/cross-ref.ts +148 -28
- package/src/config/loader/emit-schema.ts +2 -0
- package/src/config/loader/globals.ts +118 -31
- package/src/config/loader/merge.ts +58 -1
- package/src/config/loader/persist-target.ts +32 -0
- package/src/config/loader/presets.ts +107 -0
- package/src/config/option-domain.ts +164 -0
- package/src/config/presets.ts +156 -0
- package/src/daemon/cache/git.ts +1 -1
- package/src/daemon/cache/render.ts +61 -7
- package/src/daemon/config-overrides-store.ts +322 -0
- package/src/daemon/paths.ts +10 -0
- package/src/daemon/render-payload.ts +84 -19
- package/src/daemon/server.ts +68 -55
- package/src/daemon/verbs/config-validators.ts +127 -0
- package/src/daemon/verbs/index.ts +129 -2
- package/src/daemon/verbs/state-validators.ts +98 -586
- package/src/daemon/verbs/validator-registry.ts +457 -0
- package/src/demo/dsl.ts +17 -10
- package/src/dsl/node-registry.ts +54 -39
- package/src/dsl/render.ts +158 -46
- package/src/help-text.ts +3 -3
- package/src/install/index.ts +2 -2
- package/src/render/action.ts +155 -33
- package/src/render/active-segment.ts +78 -0
- package/src/render/menu.ts +16 -11
- package/src/render/picker.ts +51 -13
- package/src/render/segment-color.ts +74 -0
- package/src/segments/git.ts +389 -48
- package/src/template-engine/colors.ts +67 -45
- package/src/template-engine/engine.ts +11 -12
- package/src/themes/index.ts +1 -4
- package/src/themes/palette-resolvers.ts +22 -30
- package/src/themes/policy.ts +37 -16
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// loader's own synthesis pass (see the bottom of this file) — a `DslConfig`,
|
|
27
27
|
// the same effective shape every user config resolves to.
|
|
28
28
|
|
|
29
|
-
import type { DslConfig, SegmentDecl } from "./dsl-types.js";
|
|
29
|
+
import type { DslConfig, LayoutNode, SegmentDecl } from "./dsl-types.js";
|
|
30
30
|
import { parseDslConfig } from "./dsl-loader.js";
|
|
31
31
|
import { mergeWithDefault } from "./loader/merge.js";
|
|
32
32
|
|
|
@@ -74,30 +74,111 @@ const DIR_TEMPLATE =
|
|
|
74
74
|
"{{ end }}{{ end }}" +
|
|
75
75
|
"{{ abbreviatePath $dir }}";
|
|
76
76
|
|
|
77
|
-
// Git
|
|
78
|
-
//
|
|
77
|
+
// Git-fact → semantic palette color, ONE table both the `git` and
|
|
78
|
+
// `gitaculous` segment templates below read from. [LAW:one-source-of-truth]
|
|
79
|
+
// brandon-segments-3eo.1 (the recoloring itself) typed this choice
|
|
80
|
+
// independently into each template's literal string and the two drifted —
|
|
81
|
+
// gitaculous colored branch `accent` where git used `primary`, and left
|
|
82
|
+
// stash uncolored where git used `accent` — caught by live testing
|
|
83
|
+
// (brandon-segments-3eo.1.1). A shared table makes "what color is this
|
|
84
|
+
// fact" a value read twice, not a decision re-typed twice.
|
|
85
|
+
const GIT_COLOR = {
|
|
86
|
+
branch: "primary",
|
|
87
|
+
staged: "success",
|
|
88
|
+
unstaged: "warning",
|
|
89
|
+
untracked: "accent",
|
|
90
|
+
conflicts: "error",
|
|
91
|
+
ahead: "success",
|
|
92
|
+
behind: "warning",
|
|
93
|
+
stash: "accent",
|
|
94
|
+
} as const;
|
|
95
|
+
|
|
96
|
+
// Paint one git fact in its semantic color. The table above holds palette
|
|
97
|
+
// *variable names* (data), and this is the one place a name becomes a template
|
|
98
|
+
// call, so the fact→color decision and its spelling stay separate concerns.
|
|
99
|
+
// [LAW:one-source-of-truth]
|
|
100
|
+
//
|
|
101
|
+
// Note the shape: `fg (color "…")`, which is also what a composed color looks
|
|
102
|
+
// like — `fg (darken (color "…") 1)`. A template that wants to adjust one of
|
|
103
|
+
// these later wraps the color expression instead of rewriting the call.
|
|
104
|
+
const paint = (fact: keyof typeof GIT_COLOR, content: string): string =>
|
|
105
|
+
`{{ fg (color "${GIT_COLOR[fact]}") ${content} }}`;
|
|
106
|
+
|
|
107
|
+
// How far the two git segments' *structural* text — labels, punctuation,
|
|
108
|
+
// brackets, the sha, the upstream name, the elapsed-time annotation — sits
|
|
109
|
+
// toward their own background, as a percentage. It is applied as the segments'
|
|
110
|
+
// `fg:`, so structural text is simply what a token renders as when the
|
|
111
|
+
// template does NOT paint it; only the operative facts (branch, counts,
|
|
112
|
+
// status) name a color. [LAW:dataflow-not-control-flow] — the quiet case is
|
|
113
|
+
// the default that always applies, not a wrapper each token opts into. That
|
|
114
|
+
// inversion is why these templates carry no de-emphasis markup at all.
|
|
115
|
+
//
|
|
116
|
+
// The blend target is the segment's OWN background (`bgOf`), not the theme's
|
|
117
|
+
// `background`. The git segments render on `surface-active`, so a palette
|
|
118
|
+
// `foreground-muted` — which blends toward `background` — would be muted
|
|
119
|
+
// toward a color that is not behind them, and would read either too loud or
|
|
120
|
+
// too faint depending on how far the surface sits from the base.
|
|
121
|
+
//
|
|
122
|
+
// The `readableOn` floor is what makes the result theme-independent, and it is
|
|
123
|
+
// not decoration: the bare 60% blend measures 1.93–3.10 contrast across the
|
|
124
|
+
// bundled themes (catppuccin-latte at 1.93 is genuinely hard to read), because
|
|
125
|
+
// a fixed *percentage* of a distance is not a fixed amount of legibility when
|
|
126
|
+
// themes disagree about how far apart their own foreground and surface sit.
|
|
127
|
+
// Flooring at WCAG's 3:1 large-text threshold lands every theme at 3.00–3.10
|
|
128
|
+
// against full-strength foregrounds of 5.8–10.9 — as quiet as each theme can
|
|
129
|
+
// afford, and never quieter. Verified across the bundled themes in
|
|
130
|
+
// test/default-dsl-config.test.ts rather than eyeballed on one.
|
|
131
|
+
// [LAW:verifiable-goals]
|
|
132
|
+
const GIT_QUIET_PCT = 60;
|
|
133
|
+
const GIT_QUIET_MIN_CONTRAST = 3;
|
|
134
|
+
const GIT_QUIET_FG =
|
|
135
|
+
`{{ readableOn (mix (color "foreground") (bgOf) ${GIT_QUIET_PCT}) (bgOf) ` +
|
|
136
|
+
`${GIT_QUIET_MIN_CONTRAST} }}`;
|
|
137
|
+
|
|
138
|
+
// Git working-tree counts — each present count renders in its own semantic
|
|
139
|
+
// palette color (GIT_COLOR above) so a dirty tree reads at a glance,
|
|
140
|
+
// p10k/gitaculous-prompt style, instead of one uniform segment fg. `$first`
|
|
141
|
+
// tracks whether a separator space is still owed before the next present
|
|
142
|
+
// count.
|
|
143
|
+
// [LAW:dataflow-not-control-flow]: one variable carries the "have we emitted
|
|
144
|
+
// yet" state rather than four copies of positional space logic. `$first` is
|
|
145
|
+
// declared inside the outer `{{ if or ... }}` gate (unlike DIR_TEMPLATE's
|
|
146
|
+
// `$dir`, declared at the template's top level) and reassigned via `=` in
|
|
147
|
+
// nested `{{ if }}` blocks below — Go template variable scoping walks up to
|
|
148
|
+
// the declaring frame on `=` regardless of nesting depth, so this still
|
|
149
|
+
// works, just at one more scope level than DIR_TEMPLATE's `$dir`. Verified
|
|
150
|
+
// by test/default-dsl-config.test.ts's "worktree counts render
|
|
151
|
+
// single-space-separated" test, not merely asserted here.
|
|
79
152
|
const GIT_WORKTREE =
|
|
80
153
|
"{{ if or (gt .git.staged 0) (gt .git.unstaged 0) (gt .git.untracked 0) (gt .git.conflicts 0) }}" +
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
154
|
+
` ({{ $first := true }}` +
|
|
155
|
+
`{{ if gt .git.staged 0 }}${paint("staged", '(printf "+%v" .git.staged)')}{{ $first = false }}{{ end }}` +
|
|
156
|
+
`{{ if gt .git.unstaged 0 }}{{ if not $first }} {{ end }}${paint("unstaged", '(printf "~%v" .git.unstaged)')}{{ $first = false }}{{ end }}` +
|
|
157
|
+
`{{ if gt .git.untracked 0 }}{{ if not $first }} {{ end }}${paint("untracked", '(printf "?%v" .git.untracked)')}{{ $first = false }}{{ end }}` +
|
|
158
|
+
`{{ if gt .git.conflicts 0 }}{{ if not $first }} {{ end }}${paint("conflicts", '(printf "!%v" .git.conflicts)')}{{ $first = false }}{{ end }}` +
|
|
159
|
+
"){{ end }}";
|
|
86
160
|
|
|
87
|
-
// Status icon precedence: conflicts →
|
|
161
|
+
// Status icon precedence: conflicts → ⚠ (error), dirty → ● (warning), else
|
|
162
|
+
// clean ✓ (success) — colored to match the state it reports.
|
|
88
163
|
const GIT_STATUS =
|
|
89
|
-
'{{ if eq .git.status "conflicts" }}⚠{{ else }}' +
|
|
90
|
-
'{{ if eq .git.status "dirty" }}
|
|
164
|
+
'{{ if eq .git.status "conflicts" }}{{ fg (color "error") "⚠" }}{{ else }}' +
|
|
165
|
+
'{{ if eq .git.status "dirty" }}{{ fg (color "warning") "●" }}' +
|
|
166
|
+
'{{ else }}{{ fg (color "success") "✓" }}{{ end }}{{ end }}';
|
|
91
167
|
|
|
168
|
+
// Every unpainted token here — the repo name, `⎇`, `♯`, the sha, the worktree
|
|
169
|
+
// parentheses, `→`, the upstream name — renders in the segment's quiet `fg:`
|
|
170
|
+
// (GIT_QUIET_FG). Only the operative facts name a color, so the template reads
|
|
171
|
+
// as the line it draws rather than as de-emphasis markup wrapped around it.
|
|
92
172
|
const GIT_TEMPLATE =
|
|
93
|
-
'{{ if ne .git.repoName "" }}{{ .git.repoName }} {{ end }}
|
|
173
|
+
'{{ if ne .git.repoName "" }}{{ .git.repoName }} {{ end }}' +
|
|
174
|
+
`⎇ ${paint("branch", ".git.branch")}` +
|
|
94
175
|
"{{ if .git.sha }} ♯ {{ .git.sha }}{{ end }}" +
|
|
95
176
|
"{{ if or (gt .git.ahead 0) (gt .git.behind 0) }}" +
|
|
96
|
-
|
|
97
|
-
|
|
177
|
+
` {{ if gt .git.ahead 0 }}${paint("ahead", '(printf "↑%v" .git.ahead)')}{{ end }}` +
|
|
178
|
+
`{{ if gt .git.behind 0 }}${paint("behind", '(printf "↓%v" .git.behind)')}{{ end }}{{ end }}` +
|
|
98
179
|
GIT_WORKTREE +
|
|
99
180
|
"{{ if .git.upstream }} →{{ .git.upstream }}{{ end }}" +
|
|
100
|
-
|
|
181
|
+
`{{ if gt .git.stash 0 }} ${paint("stash", '(printf "⧇ %v" .git.stash)')}{{ end }}` +
|
|
101
182
|
" " +
|
|
102
183
|
GIT_STATUS;
|
|
103
184
|
|
|
@@ -146,6 +227,51 @@ function etaHeatFg(etaRef: string, warnRef: string): string {
|
|
|
146
227
|
);
|
|
147
228
|
}
|
|
148
229
|
|
|
230
|
+
// ─── The settings drawer (candybar-config-engine-71o.4) ──────────────────────
|
|
231
|
+
|
|
232
|
+
// [LAW:one-source-of-truth] exception: `kind: "group"` is authoring-grammar
|
|
233
|
+
// sugar the loader lowers at parse time (src/config/loader/layout.ts) —
|
|
234
|
+
// deliberately NOT a member of the canonical LayoutNode union DslConfig.root
|
|
235
|
+
// requires (arranging + gating are behaviors `container` already has; "group"
|
|
236
|
+
// is only a spelling), so a plain `satisfies DslConfig` cannot type-check it
|
|
237
|
+
// inline below. Hand-lowering it here instead (writing the toggle segment +
|
|
238
|
+
// gated body container by hand, under the reserved `groups.` namespace) is
|
|
239
|
+
// NOT an option: reservedNamespaceCollisions rejects any USER-authored
|
|
240
|
+
// variables/actions/segments name starting with `groups.` before synthesis
|
|
241
|
+
// ever runs, so a hand-authored `groups.settings` segment would be rejected
|
|
242
|
+
// as squatting the very namespace it's trying to populate — the sugar node is
|
|
243
|
+
// the only legal way to populate it. This literal is unconditionally
|
|
244
|
+
// round-tripped through the real parseDslConfig pipeline below (see the
|
|
245
|
+
// module-load parse near the bottom of this file) exactly like a user's
|
|
246
|
+
// hand-authored JSON5, so a malformed group is still caught loudly at import
|
|
247
|
+
// time — the type safety net just moves from tsc to that parse, never lost.
|
|
248
|
+
//
|
|
249
|
+
// One collapsed-by-default drawer holding every bar-mutable display default:
|
|
250
|
+
// theme/style/look (session `set` for a per-conversation preview, PLUS a
|
|
251
|
+
// persist-forever twin — candybar-config-engine-71o.5 — for pinning the
|
|
252
|
+
// choice as everyone's default), the four .3 globals steppers (persist-only,
|
|
253
|
+
// no SessionState half at all), and one .6 segment-scoped persist control
|
|
254
|
+
// (directoryPaletteControl, persist-only like the four steppers — a
|
|
255
|
+
// per-segment palette pin, not a whole-bar default). Placed as a sibling in
|
|
256
|
+
// row 1's horizontal container, toggled from beside the quick-action tray —
|
|
257
|
+
// see `root` below.
|
|
258
|
+
const settingsDrawer = {
|
|
259
|
+
kind: "group",
|
|
260
|
+
name: "settings",
|
|
261
|
+
label: "⚙ settings",
|
|
262
|
+
direction: "horizontal",
|
|
263
|
+
children: [
|
|
264
|
+
"themeControl",
|
|
265
|
+
"lookControl",
|
|
266
|
+
"styleControl",
|
|
267
|
+
"charsetControl",
|
|
268
|
+
"colorCompatControl",
|
|
269
|
+
"wrapToggleControl",
|
|
270
|
+
"paddingControl",
|
|
271
|
+
"directoryPaletteControl",
|
|
272
|
+
],
|
|
273
|
+
} as unknown as LayoutNode;
|
|
274
|
+
|
|
149
275
|
// ─── The default config ──────────────────────────────────────────────────────
|
|
150
276
|
|
|
151
277
|
// [LAW:one-source-of-truth] The AUTHORED literal, pre-synthesis. Production
|
|
@@ -229,6 +355,50 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
229
355
|
path: "look.effective",
|
|
230
356
|
default: "",
|
|
231
357
|
},
|
|
358
|
+
// [LAW:one-type-per-behavior] The effective PRESET name, theme/look's twin
|
|
359
|
+
// one level up — effectivePresetName(sessionState.preset, globals.preset,
|
|
360
|
+
// presets), the SAME name that selected the layout this render walked and
|
|
361
|
+
// the globals it rendered with. A preset-picker trigger reads
|
|
362
|
+
// `{{ .preset.effective }}` for its label, so the label and the arrangement
|
|
363
|
+
// trace to one resolution.
|
|
364
|
+
"preset.effective": {
|
|
365
|
+
kind: "input",
|
|
366
|
+
path: "preset.effective",
|
|
367
|
+
default: "",
|
|
368
|
+
},
|
|
369
|
+
// [LAW:one-type-per-behavior] style/charset/colorCompatibility/autoWrap/
|
|
370
|
+
// padding are theme/look's twins over the remaining persistable globals
|
|
371
|
+
// (candybar-config-engine-71o.3) — the SAME values BuildLineOptions
|
|
372
|
+
// renders with, each read back through this projection so a `persist`
|
|
373
|
+
// action over the field shows a "current selection" highlight and a
|
|
374
|
+
// trigger label can display the active value without restating it.
|
|
375
|
+
"style.effective": {
|
|
376
|
+
kind: "input",
|
|
377
|
+
path: "style.effective",
|
|
378
|
+
default: "",
|
|
379
|
+
},
|
|
380
|
+
"charset.effective": {
|
|
381
|
+
kind: "input",
|
|
382
|
+
path: "charset.effective",
|
|
383
|
+
default: "",
|
|
384
|
+
},
|
|
385
|
+
"colorCompatibility.effective": {
|
|
386
|
+
kind: "input",
|
|
387
|
+
path: "colorCompatibility.effective",
|
|
388
|
+
default: "",
|
|
389
|
+
},
|
|
390
|
+
"autoWrap.effective": {
|
|
391
|
+
kind: "input",
|
|
392
|
+
path: "autoWrap.effective",
|
|
393
|
+
type: "boolean",
|
|
394
|
+
default: true,
|
|
395
|
+
},
|
|
396
|
+
"padding.effective": {
|
|
397
|
+
kind: "input",
|
|
398
|
+
path: "padding.effective",
|
|
399
|
+
type: "number",
|
|
400
|
+
default: 1,
|
|
401
|
+
},
|
|
232
402
|
|
|
233
403
|
// [LAW:one-source-of-truth] The usable terminal width for THIS render —
|
|
234
404
|
// the exact post-reserve cell count FlexStrip wraps to. renderDsl injects
|
|
@@ -281,6 +451,10 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
281
451
|
path: "git.repoName",
|
|
282
452
|
default: "",
|
|
283
453
|
},
|
|
454
|
+
// The repo's browsable web page, transposed from its remote by the daemon.
|
|
455
|
+
// "" is the genuine "no remote a browser can open" (local-only repo, bare
|
|
456
|
+
// path remote) — the toolbar's link reads that value, not a flag.
|
|
457
|
+
"git.repoUrl": { kind: "input", path: "git.repoUrl", default: "" },
|
|
284
458
|
"git.branch": { kind: "input", path: "git.branch", default: "" },
|
|
285
459
|
"git.sha": { kind: "input", path: "git.sha", default: "" },
|
|
286
460
|
"git.ahead": {
|
|
@@ -609,31 +783,59 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
609
783
|
git: {
|
|
610
784
|
template: GIT_TEMPLATE,
|
|
611
785
|
bg: "surface-active",
|
|
612
|
-
fg
|
|
786
|
+
// A computed `fg:` — the field is a template evaluating to a color
|
|
787
|
+
// reference, and `bgOf` is available here because a segment's background
|
|
788
|
+
// is resolved before its foreground. Structural text therefore sits a
|
|
789
|
+
// fixed distance from THIS segment's background whatever theme, look, or
|
|
790
|
+
// hue shift is in effect.
|
|
791
|
+
fg: GIT_QUIET_FG,
|
|
613
792
|
when: '{{ ne .git.branch "" }}',
|
|
614
793
|
},
|
|
615
794
|
gitaculous: {
|
|
795
|
+
// Recolored from raw green/red (render-bugs-pdu.3's era) to semantic
|
|
796
|
+
// palette names (brandon-segments-3eo.1), then unified against `git`'s
|
|
797
|
+
// choice of color per fact via the shared GIT_COLOR table above
|
|
798
|
+
// (brandon-segments-3eo.1.1 — the two had drifted: branch, untracked,
|
|
799
|
+
// and stash each disagreed with `git`'s coloring of the same fact).
|
|
800
|
+
// staged/ahead share `success` (positive — ready to commit / unpushed
|
|
801
|
+
// additions), unstaged/behind share `warning` (needs attention),
|
|
802
|
+
// untracked/stash share `accent` (their own GIT_COLOR entries, kept
|
|
803
|
+
// visually distinct from unstaged by using a different glyph, not a
|
|
804
|
+
// different color, so "U" vs "?" reads apart at a glance), conflicts
|
|
805
|
+
// gets its own `error`, branch gets `primary`.
|
|
806
|
+
//
|
|
807
|
+
// Everything the template does NOT paint — the "(git)" label, repo name,
|
|
808
|
+
// the operation and upstream brackets, the sha, the elapsed-time
|
|
809
|
+
// annotation — renders in the segment's quiet `fg:` and recedes, so the
|
|
810
|
+
// eye lands on the operative colored facts first (brandon-segments-
|
|
811
|
+
// 3eo.1.1.1: live feedback that "most of it" read as one flat color when
|
|
812
|
+
// only two facts happened to be present). Note that this needs no markup
|
|
813
|
+
// in the template — including around `{{ template "formatTimeSince" }}`,
|
|
814
|
+
// which as a top-level Go-template action could never have been wrapped
|
|
815
|
+
// in a styling call at all. Making quiet the default rather than a
|
|
816
|
+
// wrapper is what put that token in reach.
|
|
616
817
|
template:
|
|
617
818
|
"(git)" +
|
|
618
819
|
'{{ if ne .git.repoName "" }} {{ .git.repoName }}{{ end }}' +
|
|
619
820
|
'{{ if ne .git.operation "" }} [{{ .git.operation }}]{{ end }}' +
|
|
620
821
|
'{{ if ne .git.sha "" }} {{ .git.sha }}{{ end }}' +
|
|
621
822
|
"{{ if or (gt .git.staged 0) (gt .git.unstaged 0) (gt .git.untracked 0) (gt .git.conflicts 0) }} " +
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
823
|
+
`{{ if gt .git.staged 0 }}${paint("staged", '"S"')}{{ end }}` +
|
|
824
|
+
`{{ if gt .git.unstaged 0 }}${paint("unstaged", '"U"')}{{ end }}` +
|
|
825
|
+
`{{ if gt .git.untracked 0 }}${paint("untracked", '"?"')}{{ end }}` +
|
|
826
|
+
`{{ if gt .git.conflicts 0 }}${paint("conflicts", '(printf "!%v" .git.conflicts)')}{{ end }}` +
|
|
625
827
|
"{{ end }}" +
|
|
626
|
-
|
|
828
|
+
` ⎇ ${paint("branch", ".git.branch")}` +
|
|
627
829
|
'{{ if ne .git.upstream "" }} [{{ .git.upstream }}' +
|
|
628
830
|
"{{ if or (gt .git.ahead 0) (gt .git.behind 0) }} " +
|
|
629
|
-
|
|
831
|
+
`{{ if gt .git.ahead 0 }}${paint("ahead", '(printf "+%v" .git.ahead)')}{{ end }}` +
|
|
630
832
|
"{{ if and (gt .git.ahead 0) (gt .git.behind 0) }}/{{ end }}" +
|
|
631
|
-
|
|
833
|
+
`{{ if gt .git.behind 0 }}${paint("behind", '(printf "-%v" .git.behind)')}{{ end }}` +
|
|
632
834
|
"{{ end }}]{{ end }}" +
|
|
633
|
-
|
|
835
|
+
`{{ if gt .git.stash 0 }} ${paint("stash", '(printf "(%v stashed)" .git.stash)')}{{ end }}` +
|
|
634
836
|
'{{ if gt .git.timeSinceCommit 0 }} ◷ {{ template "formatTimeSince" .git.timeSinceCommit }}{{ end }}',
|
|
635
837
|
bg: "surface-active",
|
|
636
|
-
fg:
|
|
838
|
+
fg: GIT_QUIET_FG,
|
|
637
839
|
when: '{{ ne .git.branch "" }}',
|
|
638
840
|
},
|
|
639
841
|
// Git PR/MR — the branch's open pull/merge request as a clickable link.
|
|
@@ -657,18 +859,27 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
657
859
|
when: '{{ or (ne .git.prUrl "") (ne .git.prError "") }}',
|
|
658
860
|
},
|
|
659
861
|
// Quick-action tray — the default bar's interactivity: copy the session id,
|
|
660
|
-
// open the project dir / transcript (this session's jsonl) in the editor
|
|
862
|
+
// open the project dir / transcript (this session's jsonl) in the editor,
|
|
863
|
+
// and open the repo's web page in the browser.
|
|
661
864
|
// (copyDir — copy the cwd — stays declared as an action below for users who
|
|
662
|
-
// want a
|
|
865
|
+
// want a fifth glyph; it is simply not in the default tray.)
|
|
663
866
|
// [LAW:locality-or-seam] The glyph is the REPRESENTATION; the named action
|
|
664
867
|
// (below) is the BEHAVIOR; the action name is the seam between them. Re-glyph
|
|
665
868
|
// without touching behavior; re-target without touching this template. Each
|
|
666
869
|
// `{{ action … }}` emits one OSC-8 clickable region whose URL the wire codec
|
|
667
870
|
// owns end-to-end.
|
|
871
|
+
//
|
|
872
|
+
// `↗ repo` is the one glyph here that is NOT an action: the daemon already
|
|
873
|
+
// resolved the remote to an https page, so `{{ link }}` hands that URL
|
|
874
|
+
// straight to the terminal/OS (same seam the gitPr segment uses) — routing a
|
|
875
|
+
// public web URL through a cc-candybar:// verb would buy nothing. It is
|
|
876
|
+
// gated on the VALUE (`ne … ""`), not on a flag: a local-only repo simply
|
|
877
|
+
// supplies no page and the glyph is absent. [LAW:dataflow-not-control-flow]
|
|
668
878
|
toolbar: {
|
|
669
879
|
template:
|
|
670
880
|
'{{ action "copySession" "⎘ id" }}' +
|
|
671
|
-
' {{ action "openProject" "↗ proj" }} {{ action "openTranscript" "↗ log" }}'
|
|
881
|
+
' {{ action "openProject" "↗ proj" }} {{ action "openTranscript" "↗ log" }}' +
|
|
882
|
+
'{{ if ne .git.repoUrl "" }} {{ link .git.repoUrl "↗ repo" }}{{ end }}',
|
|
672
883
|
bg: "surface",
|
|
673
884
|
fg: "foreground",
|
|
674
885
|
},
|
|
@@ -829,10 +1040,25 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
829
1040
|
// render) and stay-open, so shapes can be tried in a row; ▾/✕ collapse.
|
|
830
1041
|
// [LAW:dataflow-not-control-flow] No display state from the provider — the
|
|
831
1042
|
// label is the one "style" value the click writes and the render reads.
|
|
1043
|
+
// Lives inside the settingsDrawer group (candybar-config-engine-71o.4) — not
|
|
1044
|
+
// on `root` directly — so it renders only while the drawer is open.
|
|
1045
|
+
// The 📌 "make default" menu + ↺ reset, one per control below, is
|
|
1046
|
+
// styleControl/themeControl/lookControl's PERSIST twin (candybar-config-
|
|
1047
|
+
// engine-71o.5) — the exact pairing charsetControl/colorCompatControl/
|
|
1048
|
+
// wrapToggleControl/paddingControl already use, since theme/style/look
|
|
1049
|
+
// are the only three of the drawer's seven knobs with a session `set`
|
|
1050
|
+
// half at all. Its own "pickersForever" accordion key keeps the
|
|
1051
|
+
// persist tier visually distinct from the existing "pickers" try tier
|
|
1052
|
+
// (opening a session preview and opening a "pin as default" picker are
|
|
1053
|
+
// different intents; auto-closing one when the other opens would
|
|
1054
|
+
// conflate them) without touching the already-shipped/tested "pickers"
|
|
1055
|
+
// accordion's membership.
|
|
832
1056
|
styleControl: {
|
|
833
1057
|
template:
|
|
834
1058
|
"✦ {{ if .activeStyle }}{{ .activeStyle }}{{ else }}(default){{ end }} " +
|
|
835
|
-
'{{ menu "applyStyle" }}'
|
|
1059
|
+
'{{ menu "applyStyle" }} ' +
|
|
1060
|
+
'📌{{ menu "applyStyleForever" (dict "key" "pickersForever") }} ' +
|
|
1061
|
+
'{{ action "resetStyle" "↺" }}',
|
|
836
1062
|
bg: "surface",
|
|
837
1063
|
fg: "foreground",
|
|
838
1064
|
},
|
|
@@ -845,11 +1071,16 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
845
1071
|
// styleControl (no "effective style" input exists), no extra `state`
|
|
846
1072
|
// variable is needed here. Shares the "pickers" accordion key with
|
|
847
1073
|
// lookControl so opening one closes the other, the docs' canonical
|
|
848
|
-
// two-menu pairing.
|
|
1074
|
+
// two-menu pairing. Moved inside the settingsDrawer group
|
|
1075
|
+
// (candybar-config-engine-71o.4) alongside style/look/charset/
|
|
1076
|
+
// colorCompatibility/autoWrap/padding — one collapsed home for every
|
|
1077
|
+
// bar-mutable display default, instead of its own always-on row.
|
|
849
1078
|
themeControl: {
|
|
850
1079
|
template:
|
|
851
1080
|
"🎨 {{ .theme.effective }} " +
|
|
852
|
-
'{{ menu "applyTheme" (dict "key" "pickers") }}'
|
|
1081
|
+
'{{ menu "applyTheme" (dict "key" "pickers") }} ' +
|
|
1082
|
+
'📌{{ menu "applyThemeForever" (dict "key" "pickersForever") }} ' +
|
|
1083
|
+
'{{ action "resetTheme" "↺" }}',
|
|
853
1084
|
bg: "surface",
|
|
854
1085
|
fg: "foreground",
|
|
855
1086
|
},
|
|
@@ -862,7 +1093,73 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
862
1093
|
lookControl: {
|
|
863
1094
|
template:
|
|
864
1095
|
"◐ {{ .look.effective }} " +
|
|
865
|
-
'{{ menu "applyLook" (dict "key" "pickers" "closeOnPick" true) }}'
|
|
1096
|
+
'{{ menu "applyLook" (dict "key" "pickers" "closeOnPick" true) }} ' +
|
|
1097
|
+
'📌{{ menu "applyLookForever" (dict "key" "pickersForever" "closeOnPick" true) }} ' +
|
|
1098
|
+
'{{ action "resetLook" "↺" }}',
|
|
1099
|
+
bg: "surface",
|
|
1100
|
+
fg: "foreground",
|
|
1101
|
+
},
|
|
1102
|
+
// ── The four .3 globals steppers, folded into the settingsDrawer group
|
|
1103
|
+
// (candybar-config-engine-71o.4) alongside theme/style/look above. Each
|
|
1104
|
+
// pairs a `persist` control with a `↺` reset (docs' persist/reset
|
|
1105
|
+
// convention) — these four have no SessionState half at all, so persist
|
|
1106
|
+
// is their only seam, unlike theme/style/look's session `set`. Labels
|
|
1107
|
+
// read `.field.effective` (the daemon-resolved value BuildLineOptions
|
|
1108
|
+
// actually rendered with), never a restated literal.
|
|
1109
|
+
charsetControl: {
|
|
1110
|
+
template:
|
|
1111
|
+
"{{ .charset.effective }} " +
|
|
1112
|
+
'{{ menu "applyCharsetForever" }} {{ action "resetCharset" "↺" }}',
|
|
1113
|
+
bg: "surface",
|
|
1114
|
+
fg: "foreground",
|
|
1115
|
+
},
|
|
1116
|
+
colorCompatControl: {
|
|
1117
|
+
template:
|
|
1118
|
+
"{{ .colorCompatibility.effective }} " +
|
|
1119
|
+
'{{ menu "applyColorCompatForever" }} {{ action "resetColorCompat" "↺" }}',
|
|
1120
|
+
bg: "surface",
|
|
1121
|
+
fg: "foreground",
|
|
1122
|
+
},
|
|
1123
|
+
wrapToggleControl: {
|
|
1124
|
+
template:
|
|
1125
|
+
'{{ action "toggleWrapForever" "wrap: on" "wrap: off" }} ' +
|
|
1126
|
+
'{{ action "resetAutoWrap" "↺" }}',
|
|
1127
|
+
bg: "surface",
|
|
1128
|
+
fg: "foreground",
|
|
1129
|
+
},
|
|
1130
|
+
paddingControl: {
|
|
1131
|
+
template:
|
|
1132
|
+
'{{ action "paddingDownForever" "◀" }} padding {{ .padding.effective }} ' +
|
|
1133
|
+
'{{ action "paddingUpForever" "▶" }} {{ action "resetPadding" "↺" }}',
|
|
1134
|
+
bg: "surface",
|
|
1135
|
+
fg: "foreground",
|
|
1136
|
+
},
|
|
1137
|
+
// [LAW:verifiable-goals] candybar-config-engine-71o.6's own acceptance
|
|
1138
|
+
// bar, mirrored from .3/.5: at least ONE segment-scoped field must be
|
|
1139
|
+
// menu-able from the BUNDLED default with no hand-authored actions.
|
|
1140
|
+
// `directory` is the demo target — always visible, palette-driven
|
|
1141
|
+
// bg/fg, so an override is immediately legible. The persist/reset pair
|
|
1142
|
+
// below targets `segments.directory.palette` (not a Globals field),
|
|
1143
|
+
// proving the option-domain-as-data seam generalizes to segment-scoped
|
|
1144
|
+
// keys with zero engine edits beyond opening the key namespace itself
|
|
1145
|
+
// (loader/persist-target.ts) — the SAME `from: "themes"` domain
|
|
1146
|
+
// applyThemeForever already uses.
|
|
1147
|
+
// [LAW:one-source-of-truth] exception: unlike charsetControl/
|
|
1148
|
+
// paddingControl's `.field.effective` label, there is no
|
|
1149
|
+
// `segments.directory.palette.effective` payload projection — adding one
|
|
1150
|
+
// would require threading the full DslConfig through
|
|
1151
|
+
// buildRenderPayload's signature (today built from EffectiveGlobals
|
|
1152
|
+
// alone), a change with no other motivation than this one label. The
|
|
1153
|
+
// control still writes/persists/resets correctly without it: per
|
|
1154
|
+
// render/action.ts's CONFIG_KEY_TO_EFFECTIVE_VAR, a persist key with no
|
|
1155
|
+
// effective-var entry writes fine and only loses the picker's "current
|
|
1156
|
+
// selection" highlight — a documented, already-accepted degrade path,
|
|
1157
|
+
// not a bug.
|
|
1158
|
+
directoryPaletteControl: {
|
|
1159
|
+
template:
|
|
1160
|
+
"🎨 directory " +
|
|
1161
|
+
'{{ menu "applyDirectoryPaletteForever" }} ' +
|
|
1162
|
+
'{{ action "resetDirectoryPalette" "↺" }}',
|
|
866
1163
|
bg: "surface",
|
|
867
1164
|
fg: "foreground",
|
|
868
1165
|
},
|
|
@@ -870,23 +1167,24 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
870
1167
|
|
|
871
1168
|
// Default layout — the canonical LayoutNode tree (`satisfies DslConfig`
|
|
872
1169
|
// requires the lowered form here; the terse Option-A `{ h/v/seg }` grammar is
|
|
873
|
-
// the loader's authoring surface for user JSON, not this typed literal
|
|
1170
|
+
// the loader's authoring surface for user JSON, not this typed literal — the
|
|
1171
|
+
// one exception being `settingsDrawer` above, whose `kind: "group"` sugar has
|
|
1172
|
+
// no canonical-form equivalent it could be hand-lowered to; see its own
|
|
1173
|
+
// comment).
|
|
874
1174
|
//
|
|
875
|
-
//
|
|
876
|
-
// (where am I / what can I do here — the directory, the verbose
|
|
877
|
-
// line
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
// top of an unrelated row's content. Each row zips its segments through the
|
|
889
|
-
// powerline joiner; `\n` separates the rows.
|
|
1175
|
+
// Two always-visible rows stacked by the vertical container: an IDENTITY +
|
|
1176
|
+
// ACTIONS row (where am I / what can I do here — the directory, the verbose
|
|
1177
|
+
// `gitaculous` line, the quick-action tray: copy session id, open project /
|
|
1178
|
+
// transcript in the editor, and the settingsDrawer toggle) over a STATUS row
|
|
1179
|
+
// (what's happening now — model, context-window fill, prompt-cache warmth,
|
|
1180
|
+
// and the 5h / 7d rate-limit quotas). The settingsDrawer (candybar-config-
|
|
1181
|
+
// engine-71o.4) sits on the identity row beside the tray — collapsed by
|
|
1182
|
+
// default and visually silent (a single "⚙ settings ▸" cell) — and reveals a
|
|
1183
|
+
// third row of every bar-mutable display default (theme, style, look,
|
|
1184
|
+
// charset, colorCompatibility, autoWrap, padding) on the line immediately
|
|
1185
|
+
// below row 1 when opened, exactly where a `{{ menu }}`'s own picker body
|
|
1186
|
+
// would drop. Each row zips its segments through the powerline joiner; `\n`
|
|
1187
|
+
// separates the rows.
|
|
890
1188
|
//
|
|
891
1189
|
// [LAW:dataflow-not-control-flow] Every status segment is when-gated on its
|
|
892
1190
|
// own signal (no repo → the identity row is just the directory + tray; no
|
|
@@ -906,6 +1204,7 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
906
1204
|
{ kind: "segment", name: "directory" },
|
|
907
1205
|
{ kind: "segment", name: "gitaculous" },
|
|
908
1206
|
{ kind: "segment", name: "toolbar" },
|
|
1207
|
+
settingsDrawer,
|
|
909
1208
|
],
|
|
910
1209
|
},
|
|
911
1210
|
{
|
|
@@ -919,14 +1218,6 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
919
1218
|
{ kind: "segment", name: "weekly" },
|
|
920
1219
|
],
|
|
921
1220
|
},
|
|
922
|
-
{
|
|
923
|
-
kind: "container",
|
|
924
|
-
direction: "horizontal",
|
|
925
|
-
children: [
|
|
926
|
-
{ kind: "segment", name: "themeControl" },
|
|
927
|
-
{ kind: "segment", name: "lookControl" },
|
|
928
|
-
],
|
|
929
|
-
},
|
|
930
1221
|
],
|
|
931
1222
|
},
|
|
932
1223
|
|
|
@@ -972,6 +1263,63 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
972
1263
|
// block's names — the same derivation test/dsl-looks.test.ts exercises.
|
|
973
1264
|
applyTheme: { set: "theme", from: "themes" },
|
|
974
1265
|
applyLook: { set: "look", from: "looks" },
|
|
1266
|
+
|
|
1267
|
+
// [LAW:one-source-of-truth] The persist-forever twins of applyTheme/
|
|
1268
|
+
// applyStyle/applyLook above (candybar-config-engine-71o.5) — same
|
|
1269
|
+
// domain sources (`from`), same picker mechanism, but the target is the
|
|
1270
|
+
// Globals field the config DEFAULT reads (`palette`/`style`/`look`,
|
|
1271
|
+
// isGlobalsField-checked at load), not the SessionState key the session
|
|
1272
|
+
// preview writes. Precedence is unchanged: a session's own `set` pick
|
|
1273
|
+
// (applyTheme/applyStyle/applyLook) still wins over a persisted default
|
|
1274
|
+
// for that session — effectiveThemeName/effectiveStripStyle/
|
|
1275
|
+
// effectiveLookName all read SessionState before globals. Paired with a
|
|
1276
|
+
// `reset` each, per the docs' persist/reset convention.
|
|
1277
|
+
applyThemeForever: { persist: "palette", from: "themes" },
|
|
1278
|
+
resetTheme: { reset: "palette" },
|
|
1279
|
+
applyStyleForever: { persist: "style", from: "styles" },
|
|
1280
|
+
resetStyle: { reset: "style" },
|
|
1281
|
+
applyLookForever: { persist: "look", from: "looks" },
|
|
1282
|
+
resetLook: { reset: "look" },
|
|
1283
|
+
|
|
1284
|
+
// [LAW:locality-or-seam] The settings-drawer steppers' behaviors
|
|
1285
|
+
// (candybar-config-engine-71o.4), decoupled by NAME from
|
|
1286
|
+
// charsetControl/colorCompatControl/wrapToggleControl/paddingControl
|
|
1287
|
+
// below. Unlike theme/style/look (a per-session experiment via `set`),
|
|
1288
|
+
// these four have no SessionState half at all — .3's handoff established
|
|
1289
|
+
// `persist` as their ONLY seam — so every one of these writes the
|
|
1290
|
+
// config-file DEFAULT through the daemon-owned overrides layer (never the
|
|
1291
|
+
// hand-authored file itself), gated by the SAME deriveActionValidators
|
|
1292
|
+
// pass as a `set` (persist mirrors set's value-source shapes one for
|
|
1293
|
+
// one). Each is paired with a `reset` so a drawer choice is always
|
|
1294
|
+
// undoable from the bar, per the docs' persist/reset convention.
|
|
1295
|
+
applyCharsetForever: { persist: "charset", from: "charsets" },
|
|
1296
|
+
resetCharset: { reset: "charset" },
|
|
1297
|
+
applyColorCompatForever: {
|
|
1298
|
+
persist: "colorCompatibility",
|
|
1299
|
+
from: "colorCompatibilities",
|
|
1300
|
+
},
|
|
1301
|
+
resetColorCompat: { reset: "colorCompatibility" },
|
|
1302
|
+
toggleWrapForever: { persist: "autoWrap", cycle: ["true", "false"] },
|
|
1303
|
+
resetAutoWrap: { reset: "autoWrap" },
|
|
1304
|
+
paddingDownForever: { persist: "padding", min: 0, max: 16, by: -1 },
|
|
1305
|
+
paddingUpForever: { persist: "padding", min: 0, max: 16, by: 1 },
|
|
1306
|
+
resetPadding: { reset: "padding" },
|
|
1307
|
+
|
|
1308
|
+
// [LAW:locality-or-seam] The segment-palette control's behavior
|
|
1309
|
+
// (candybar-config-engine-71o.6), decoupled by NAME from
|
|
1310
|
+
// directoryPaletteControl below. The target key is `segments.directory.
|
|
1311
|
+
// palette` — NOT a Globals field — so it rides the SAME generic
|
|
1312
|
+
// `from`/`reset` machinery every other persist pair here uses, over a
|
|
1313
|
+
// key namespace loader/persist-target.ts opened alongside the pre-
|
|
1314
|
+
// existing Globals-field one. Like the four .3 steppers, this field has
|
|
1315
|
+
// no SessionState half at all: a per-segment `palette:` is a static pin
|
|
1316
|
+
// that ignores the session theme by design (src/dsl/render.ts), so
|
|
1317
|
+
// `persist` is its only seam.
|
|
1318
|
+
applyDirectoryPaletteForever: {
|
|
1319
|
+
persist: "segments.directory.palette",
|
|
1320
|
+
from: "themes",
|
|
1321
|
+
},
|
|
1322
|
+
resetDirectoryPalette: { reset: "segments.directory.palette" },
|
|
975
1323
|
},
|
|
976
1324
|
|
|
977
1325
|
// ─── Looks ───────────────────────────────────────────────────────────────
|
|
@@ -1028,6 +1376,27 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
1028
1376
|
},
|
|
1029
1377
|
},
|
|
1030
1378
|
|
|
1379
|
+
// ─── Presets ─────────────────────────────────────────────────────────────
|
|
1380
|
+
// Named config FRAGMENTS — each an alternative `root` + display `globals`,
|
|
1381
|
+
// i.e. a whole arrangement of the bar rather than one knob. A preset is to
|
|
1382
|
+
// configuration what a look is to a theme, and rides the identical seam:
|
|
1383
|
+
// selected per session via the `preset` SessionState key (an action
|
|
1384
|
+
// `{ set: "preset", from: "presets" }` + a `{{ menu }}`), resolved as session
|
|
1385
|
+
// pick over globals.preset over this floor.
|
|
1386
|
+
// [LAW:one-source-of-truth] Merges by name (user wins per name), so this
|
|
1387
|
+
// stdlib — currently just the floor — is present in every merged config by
|
|
1388
|
+
// construction, exactly as looks' "none" is.
|
|
1389
|
+
presets: {
|
|
1390
|
+
// [LAW:dataflow-not-control-flow] "default" is just the identity fragment
|
|
1391
|
+
// — the resolution floor as a value, not a special case. An empty fragment
|
|
1392
|
+
// declares no `root` and no `globals`, so it stages the config's own, which
|
|
1393
|
+
// is precisely what "no preset chosen" means. A usable preset LIBRARY on
|
|
1394
|
+
// top of this floor is brandon-presets-0yk.3; the floor itself belongs here
|
|
1395
|
+
// because effectivePresetName's collapse target must exist in every merged
|
|
1396
|
+
// config, the same load-bearing reason "none" ships beside the real looks.
|
|
1397
|
+
default: {},
|
|
1398
|
+
},
|
|
1399
|
+
|
|
1031
1400
|
// [LAW:single-enforcer] / [LAW:one-source-of-truth] Display-formatting policy
|
|
1032
1401
|
// for the cost/token/budget family lives here as named template helpers, each
|
|
1033
1402
|
// DEFINED ONCE and called from every segment via `{{ template "name" .arg }}`
|