@promptctl/cc-candybar 1.42.1 → 1.43.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 +72 -71
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/currency.ts +0 -197
- package/src/install/index.ts +0 -557
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- package/src/version.ts +0 -17
|
@@ -1,1645 +0,0 @@
|
|
|
1
|
-
// [LAW:one-source-of-truth] The bundled default DslConfig — the statusline
|
|
2
|
-
// rendered when no `.cc-candybar.json5` (or `.cc-candybar.json`) is present
|
|
3
|
-
// at any resolution layer. This is the canonical port of every built-in
|
|
4
|
-
// segment as a DSL declaration, covering the surface previously expressed
|
|
5
|
-
// by the legacy renderer that was retired in bzh.2.
|
|
6
|
-
//
|
|
7
|
-
// [LAW:single-enforcer] One default. User configs merge on top via
|
|
8
|
-
// `mergeWithDefault`: globals shallow-merge per field, variables/segments/
|
|
9
|
-
// helpers/actions/helpers merge by name (user wins per name), root replaces
|
|
10
|
-
// wholesale when present. A user file only needs to declare what differs —
|
|
11
|
-
// overriding one segment or variable takes a few lines. JSON5 supports
|
|
12
|
-
// inline comments so users can declare only the delta. The `.json` extension
|
|
13
|
-
// is also accepted (JSON ⊂ JSON5, same parser); `.json5` is preferred when
|
|
14
|
-
// both exist at the same location.
|
|
15
|
-
//
|
|
16
|
-
// [LAW:dataflow-not-control-flow] Every segment is declared regardless of
|
|
17
|
-
// whether the default `root` includes it — `root` is the tree that
|
|
18
|
-
// chooses what renders. Switching a disabled segment on is a root edit
|
|
19
|
-
// (add its name to the children), not new code. The same data flows through
|
|
20
|
-
// the same render path whether the root has 1 leaf or 16.
|
|
21
|
-
//
|
|
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.
|
|
28
|
-
|
|
29
|
-
import type { DslConfig, LayoutNode, SegmentDecl } from "./dsl-types.js";
|
|
30
|
-
import { parseDslConfig } from "./dsl-loader.js";
|
|
31
|
-
// [LAW:one-source-of-truth] The bundled drawer's menus author their own
|
|
32
|
-
// disclosure glyphs now that `{{ menu }}` appends none. Interpolating the
|
|
33
|
-
// shared constants keeps the stdlib bar reading like every other disclosure
|
|
34
|
-
// without restating the vocabulary in three string literals.
|
|
35
|
-
import {
|
|
36
|
-
DISCLOSURE_GLYPH_CLOSED,
|
|
37
|
-
DISCLOSURE_GLYPH_OPEN,
|
|
38
|
-
} from "./disclosure.js";
|
|
39
|
-
import { mergeWithDefault } from "./loader/merge.js";
|
|
40
|
-
|
|
41
|
-
// ─── Shared template fragments ───────────────────────────────────────────────
|
|
42
|
-
//
|
|
43
|
-
// Factored out of the segments' `template` fields so:
|
|
44
|
-
// (1) the git working-tree counts and status icon can be shared by the two
|
|
45
|
-
// git-style segments (git, gitaculous) without duplication, and
|
|
46
|
-
// (2) the block/weekly threshold cascade can be parameterized on the
|
|
47
|
-
// variable name without resorting to runtime string surgery.
|
|
48
|
-
|
|
49
|
-
// Directory: ~ collapse under $HOME, project-relative under workspace.project_dir,
|
|
50
|
-
// else raw. Inline-recomputes the project-relative path because the DSL has no
|
|
51
|
-
// template-level `:=` (a `kind: "template"` var would express it once, but adds
|
|
52
|
-
// noise for a single use).
|
|
53
|
-
//
|
|
54
|
-
// Prefix checks are boundary-safe: a path is "under" a base iff it equals the
|
|
55
|
-
// base OR starts with `base + "/"`. The naive `hasPrefix base path` is a
|
|
56
|
-
// string match — it would treat `/home/alice` as a child of `/home/al`.
|
|
57
|
-
// `(printf "%s/" base)` adds the separator so the prefix can only land at a
|
|
58
|
-
// path boundary; the `eq` arm catches the exact-match case where the trailing
|
|
59
|
-
// slash would over-match.
|
|
60
|
-
//
|
|
61
|
-
// Equal-paths case (current_dir === project_dir) DOES enter the project-
|
|
62
|
-
// relative arm: DIR_REL evaluates to "" and the ternary picks basename
|
|
63
|
-
// (project_dir), so the project root renders as `<repo-name>` instead of the
|
|
64
|
-
// full absolute path. Same logic handles equal home & current_dir → just "~".
|
|
65
|
-
const DIR_REL = 'trimPrefix "/" (trimPrefix .project_dir .current_dir)';
|
|
66
|
-
// [LAW:decomposition] Two separable concerns: (1) COLLAPSE the absolute cwd to a
|
|
67
|
-
// short display form (`~`-relative, else project-relative, else absolute) and
|
|
68
|
-
// (2) ABBREVIATE fish-style. Collapse stays in the template (its inputs are the
|
|
69
|
-
// payload's home/project_dir/current_dir); abbreviation is a single helper
|
|
70
|
-
// applied to the collapsed result. `$dir` carries the collapsed path between the
|
|
71
|
-
// two — default is the absolute cwd, overridden only when it lives under home or
|
|
72
|
-
// the project root. brandon-directory-781 makes fish-abbreviation the DEFAULT;
|
|
73
|
-
// a user restores the full path by overriding `segments.directory.template`
|
|
74
|
-
// (drop the `abbreviatePath` wrapper) — the existing merge-by-name seam.
|
|
75
|
-
const DIR_TEMPLATE =
|
|
76
|
-
"{{ $dir := .current_dir }}" +
|
|
77
|
-
'{{ if and (ne .home "") (or (eq .home .current_dir) (hasPrefix (printf "%s/" .home) .current_dir)) }}' +
|
|
78
|
-
`{{ $dir = printf "~%s" (trimPrefix .home .current_dir) }}` +
|
|
79
|
-
"{{ else }}" +
|
|
80
|
-
'{{ if or (eq .project_dir .current_dir) (hasPrefix (printf "%s/" .project_dir) .current_dir) }}' +
|
|
81
|
-
`{{ $dir = ternary (${DIR_REL}) (basename .project_dir) (ne (${DIR_REL}) "") }}` +
|
|
82
|
-
"{{ end }}{{ end }}" +
|
|
83
|
-
"{{ abbreviatePath $dir }}";
|
|
84
|
-
|
|
85
|
-
// Git-fact → semantic palette color, ONE table both the `git` and
|
|
86
|
-
// `gitaculous` segment templates below read from. [LAW:one-source-of-truth]
|
|
87
|
-
// brandon-segments-3eo.1 (the recoloring itself) typed this choice
|
|
88
|
-
// independently into each template's literal string and the two drifted —
|
|
89
|
-
// gitaculous colored branch `accent` where git used `primary`, and left
|
|
90
|
-
// stash uncolored where git used `accent` — caught by live testing
|
|
91
|
-
// (brandon-segments-3eo.1.1). A shared table makes "what color is this
|
|
92
|
-
// fact" a value read twice, not a decision re-typed twice.
|
|
93
|
-
const GIT_COLOR = {
|
|
94
|
-
branch: "primary",
|
|
95
|
-
staged: "success",
|
|
96
|
-
unstaged: "warning",
|
|
97
|
-
untracked: "accent",
|
|
98
|
-
conflicts: "error",
|
|
99
|
-
ahead: "success",
|
|
100
|
-
behind: "warning",
|
|
101
|
-
stash: "accent",
|
|
102
|
-
} as const;
|
|
103
|
-
|
|
104
|
-
// Paint one git fact in its semantic color. The table above holds palette
|
|
105
|
-
// *variable names* (data), and this is the one place a name becomes a template
|
|
106
|
-
// call, so the fact→color decision and its spelling stay separate concerns.
|
|
107
|
-
// [LAW:one-source-of-truth]
|
|
108
|
-
//
|
|
109
|
-
// Note the shape: `fg (color "…")`, which is also what a composed color looks
|
|
110
|
-
// like — `fg (darken (color "…") 1)`. A template that wants to adjust one of
|
|
111
|
-
// these later wraps the color expression instead of rewriting the call.
|
|
112
|
-
const paint = (fact: keyof typeof GIT_COLOR, content: string): string =>
|
|
113
|
-
`{{ fg (color "${GIT_COLOR[fact]}") ${content} }}`;
|
|
114
|
-
|
|
115
|
-
// How far the two git segments' *structural* text — labels, punctuation,
|
|
116
|
-
// brackets, the sha, the upstream name, the elapsed-time annotation — sits
|
|
117
|
-
// toward their own background, as a percentage. It is applied as the segments'
|
|
118
|
-
// `fg:`, so structural text is simply what a token renders as when the
|
|
119
|
-
// template does NOT paint it; only the operative facts (branch, counts,
|
|
120
|
-
// status) name a color. [LAW:dataflow-not-control-flow] — the quiet case is
|
|
121
|
-
// the default that always applies, not a wrapper each token opts into. That
|
|
122
|
-
// inversion is why these templates carry no de-emphasis markup at all.
|
|
123
|
-
//
|
|
124
|
-
// The blend target is the segment's OWN background (`bgOf`), not the theme's
|
|
125
|
-
// `background`. The git segments render on `surface-active`, so a palette
|
|
126
|
-
// `foreground-muted` — which blends toward `background` — would be muted
|
|
127
|
-
// toward a color that is not behind them, and would read either too loud or
|
|
128
|
-
// too faint depending on how far the surface sits from the base.
|
|
129
|
-
//
|
|
130
|
-
// The `readableOn` floor is what makes the result theme-independent, and it is
|
|
131
|
-
// not decoration: the bare 60% blend measures 1.93–3.10 contrast across the
|
|
132
|
-
// bundled themes (catppuccin-latte at 1.93 is genuinely hard to read), because
|
|
133
|
-
// a fixed *percentage* of a distance is not a fixed amount of legibility when
|
|
134
|
-
// themes disagree about how far apart their own foreground and surface sit.
|
|
135
|
-
// Flooring at WCAG's 3:1 large-text threshold lands every theme at 3.00–3.10
|
|
136
|
-
// against full-strength foregrounds of 5.8–10.9 — as quiet as each theme can
|
|
137
|
-
// afford, and never quieter. Verified across the bundled themes in
|
|
138
|
-
// test/default-dsl-config.test.ts rather than eyeballed on one.
|
|
139
|
-
// [LAW:verifiable-goals]
|
|
140
|
-
const GIT_QUIET_PCT = 60;
|
|
141
|
-
const GIT_QUIET_MIN_CONTRAST = 3;
|
|
142
|
-
const GIT_QUIET_FG =
|
|
143
|
-
`{{ readableOn (mix (color "foreground") (bgOf) ${GIT_QUIET_PCT}) (bgOf) ` +
|
|
144
|
-
`${GIT_QUIET_MIN_CONTRAST} }}`;
|
|
145
|
-
|
|
146
|
-
// Git working-tree counts — each present count renders in its own semantic
|
|
147
|
-
// palette color (GIT_COLOR above) so a dirty tree reads at a glance,
|
|
148
|
-
// p10k/gitaculous-prompt style, instead of one uniform segment fg. `$first`
|
|
149
|
-
// tracks whether a separator space is still owed before the next present
|
|
150
|
-
// count.
|
|
151
|
-
// [LAW:dataflow-not-control-flow]: one variable carries the "have we emitted
|
|
152
|
-
// yet" state rather than four copies of positional space logic. `$first` is
|
|
153
|
-
// declared inside the outer `{{ if or ... }}` gate (unlike DIR_TEMPLATE's
|
|
154
|
-
// `$dir`, declared at the template's top level) and reassigned via `=` in
|
|
155
|
-
// nested `{{ if }}` blocks below — Go template variable scoping walks up to
|
|
156
|
-
// the declaring frame on `=` regardless of nesting depth, so this still
|
|
157
|
-
// works, just at one more scope level than DIR_TEMPLATE's `$dir`. Verified
|
|
158
|
-
// by test/default-dsl-config.test.ts's "worktree counts render
|
|
159
|
-
// single-space-separated" test, not merely asserted here.
|
|
160
|
-
const GIT_WORKTREE =
|
|
161
|
-
"{{ if or (gt .git.staged 0) (gt .git.unstaged 0) (gt .git.untracked 0) (gt .git.conflicts 0) }}" +
|
|
162
|
-
` ({{ $first := true }}` +
|
|
163
|
-
`{{ if gt .git.staged 0 }}${paint("staged", '(printf "+%v" .git.staged)')}{{ $first = false }}{{ end }}` +
|
|
164
|
-
`{{ if gt .git.unstaged 0 }}{{ if not $first }} {{ end }}${paint("unstaged", '(printf "~%v" .git.unstaged)')}{{ $first = false }}{{ end }}` +
|
|
165
|
-
`{{ if gt .git.untracked 0 }}{{ if not $first }} {{ end }}${paint("untracked", '(printf "?%v" .git.untracked)')}{{ $first = false }}{{ end }}` +
|
|
166
|
-
`{{ if gt .git.conflicts 0 }}{{ if not $first }} {{ end }}${paint("conflicts", '(printf "!%v" .git.conflicts)')}{{ $first = false }}{{ end }}` +
|
|
167
|
-
"){{ end }}";
|
|
168
|
-
|
|
169
|
-
// Status icon precedence: conflicts → ⚠ (error), dirty → ● (warning), else
|
|
170
|
-
// clean ✓ (success) — colored to match the state it reports.
|
|
171
|
-
const GIT_STATUS =
|
|
172
|
-
'{{ if eq .git.status "conflicts" }}{{ fg (color "error") "⚠" }}{{ else }}' +
|
|
173
|
-
'{{ if eq .git.status "dirty" }}{{ fg (color "warning") "●" }}' +
|
|
174
|
-
'{{ else }}{{ fg (color "success") "✓" }}{{ end }}{{ end }}';
|
|
175
|
-
|
|
176
|
-
// Every unpainted token here — the repo name, `⎇`, `♯`, the sha, the worktree
|
|
177
|
-
// parentheses, `→`, the upstream name — renders in the segment's quiet `fg:`
|
|
178
|
-
// (GIT_QUIET_FG). Only the operative facts name a color, so the template reads
|
|
179
|
-
// as the line it draws rather than as de-emphasis markup wrapped around it.
|
|
180
|
-
const GIT_TEMPLATE =
|
|
181
|
-
'{{ if ne .git.repoName "" }}{{ .git.repoName }} {{ end }}' +
|
|
182
|
-
`⎇ ${paint("branch", ".git.branch")}` +
|
|
183
|
-
"{{ if .git.sha }} ♯ {{ .git.sha }}{{ end }}" +
|
|
184
|
-
"{{ if or (gt .git.ahead 0) (gt .git.behind 0) }}" +
|
|
185
|
-
` {{ if gt .git.ahead 0 }}${paint("ahead", '(printf "↑%v" .git.ahead)')}{{ end }}` +
|
|
186
|
-
`{{ if gt .git.behind 0 }}${paint("behind", '(printf "↓%v" .git.behind)')}{{ end }}{{ end }}` +
|
|
187
|
-
GIT_WORKTREE +
|
|
188
|
-
"{{ if .git.upstream }} →{{ .git.upstream }}{{ end }}" +
|
|
189
|
-
`{{ if gt .git.stash 0 }} ${paint("stash", '(printf "⧇ %v" .git.stash)')}{{ end }}` +
|
|
190
|
-
" " +
|
|
191
|
-
GIT_STATUS;
|
|
192
|
-
|
|
193
|
-
// [LAW:dataflow-not-control-flow] block and weekly share the same threshold
|
|
194
|
-
// cascade (≥warningThreshold → error, ≥50 → warning, else panel) on a numeric
|
|
195
|
-
// ref. The builders parameterize BOTH the percentage ref and the threshold
|
|
196
|
-
// ref so each segment reads its own configured threshold from the var store
|
|
197
|
-
// rather than the literal 80 baked into the template. User overrides flow
|
|
198
|
-
// through the variables-merge-by-name cascade in mergeWithDefault — no new
|
|
199
|
-
// override mechanism required.
|
|
200
|
-
function blockLikeBg(pctRef: string, thresholdRef: string): string {
|
|
201
|
-
return (
|
|
202
|
-
`{{ if ge (round ${pctRef}) ${thresholdRef} }}error` +
|
|
203
|
-
`{{ else }}{{ if ge (round ${pctRef}) 50 }}warning` +
|
|
204
|
-
`{{ else }}panel{{ end }}{{ end }}`
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function blockLikeFg(pctRef: string): string {
|
|
209
|
-
return (
|
|
210
|
-
`{{ if ge (round ${pctRef}) 50 }}button-color-foreground` +
|
|
211
|
-
`{{ else }}foreground{{ end }}`
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// [LAW:dataflow-not-control-flow] The burn segment heats as the cap NEARS, so
|
|
216
|
-
// the cascade reads the projected minutes-to-cap (smaller = hotter), the
|
|
217
|
-
// inverse direction of blockLikeBg's fuller-is-hotter. The not-projectable
|
|
218
|
-
// sentinel (-1, sorts below every threshold) is caught first so "we cannot
|
|
219
|
-
// project" colors calm, never error. Thresholds are var refs so a user
|
|
220
|
-
// overrides them through the same by-name variables cascade.
|
|
221
|
-
function etaHeatBg(etaRef: string, warnRef: string, errRef: string): string {
|
|
222
|
-
return (
|
|
223
|
-
`{{ if lt ${etaRef} 0 }}panel` +
|
|
224
|
-
`{{ else }}{{ if lt ${etaRef} ${errRef} }}error` +
|
|
225
|
-
`{{ else }}{{ if lt ${etaRef} ${warnRef} }}warning` +
|
|
226
|
-
`{{ else }}panel{{ end }}{{ end }}{{ end }}`
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function etaHeatFg(etaRef: string, warnRef: string): string {
|
|
231
|
-
return (
|
|
232
|
-
`{{ if lt ${etaRef} 0 }}foreground` +
|
|
233
|
-
`{{ else }}{{ if lt ${etaRef} ${warnRef} }}button-color-foreground` +
|
|
234
|
-
`{{ else }}foreground{{ end }}{{ end }}`
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ─── The settings drawer (candybar-config-engine-71o.4) ──────────────────────
|
|
239
|
-
|
|
240
|
-
// [LAW:one-source-of-truth] exception: `kind: "group"` is authoring-grammar
|
|
241
|
-
// sugar the loader lowers at parse time (src/config/loader/layout.ts) —
|
|
242
|
-
// deliberately NOT a member of the canonical LayoutNode union DslConfig.root
|
|
243
|
-
// requires (arranging + gating are behaviors `container` already has; "group"
|
|
244
|
-
// is only a spelling), so a plain `satisfies DslConfig` cannot type-check it
|
|
245
|
-
// inline below. Hand-lowering it here instead (writing the toggle segment +
|
|
246
|
-
// gated body container by hand, under the reserved `groups.` namespace) is
|
|
247
|
-
// NOT an option: reservedNamespaceCollisions rejects any USER-authored
|
|
248
|
-
// variables/actions/segments name starting with `groups.` before synthesis
|
|
249
|
-
// ever runs, so a hand-authored `groups.settings` segment would be rejected
|
|
250
|
-
// as squatting the very namespace it's trying to populate — the sugar node is
|
|
251
|
-
// the only legal way to populate it. This literal is unconditionally
|
|
252
|
-
// round-tripped through the real parseDslConfig pipeline below (see the
|
|
253
|
-
// module-load parse near the bottom of this file) exactly like a user's
|
|
254
|
-
// hand-authored JSON5, so a malformed group is still caught loudly at import
|
|
255
|
-
// time — the type safety net just moves from tsc to that parse, never lost.
|
|
256
|
-
//
|
|
257
|
-
// One collapsed-by-default drawer holding what the SESSION-scoped settings
|
|
258
|
-
// menu deliberately does not: `charset` and `colorCompatibility` — terminal
|
|
259
|
-
// capability facts (glyph coverage, colour depth) rather than tastes that
|
|
260
|
-
// vary session to session, so they have no session half to choose between —
|
|
261
|
-
// and one segment-scoped persist control (directoryPaletteControl, a
|
|
262
|
-
// per-segment palette pin rather than a whole-bar default).
|
|
263
|
-
//
|
|
264
|
-
// [LAW:one-source-of-truth] candybar-settings-ui-aok.3 moved every setting
|
|
265
|
-
// with BOTH halves — theme/style/look/preset/autoWrap/padding — out of this
|
|
266
|
-
// drawer and into the synthesized settings menu, where each is ONE control
|
|
267
|
-
// whose destination the `persist?` selector chooses. They used to be spelled
|
|
268
|
-
// twice here (`{{ menu "applyTheme" }}` beside `📌{{ menu
|
|
269
|
-
// "applyThemeForever" }}`), which is exactly the second representation that
|
|
270
|
-
// collapse removed. What is left in this drawer is durable-only by nature,
|
|
271
|
-
// not by omission — there is nothing for a persist? selector to choose.
|
|
272
|
-
//
|
|
273
|
-
// Placed as a sibling in row 1's horizontal container, toggled from beside the
|
|
274
|
-
// quick-action tray — see `root` below.
|
|
275
|
-
//
|
|
276
|
-
// [LAW:one-source-of-truth] exception: this `kind: "group"` sugar node may
|
|
277
|
-
// appear EXACTLY ONCE in the whole config — group names are a synthesis-wide
|
|
278
|
-
// namespace (synthesizeGroupDecls collects every group across every preset's
|
|
279
|
-
// root too), so a second `settingsDrawer` reference embedded in a preset's
|
|
280
|
-
// own root would be a SECOND declaration of "settings" and collide with
|
|
281
|
-
// itself, not a reuse of the first. It stays only in the default `root`
|
|
282
|
-
// below; the library presets under `presets:` reach it by switching back to
|
|
283
|
-
// the "default" preset from the GLOBAL settings menu, which
|
|
284
|
-
// synthesizeSettingsMenu splices into every preset root
|
|
285
|
-
// (src/config/settings-menu.ts) — never by re-embedding this group.
|
|
286
|
-
const settingsDrawer = {
|
|
287
|
-
kind: "group",
|
|
288
|
-
name: "settings",
|
|
289
|
-
label: "⚙ terminal",
|
|
290
|
-
direction: "horizontal",
|
|
291
|
-
children: ["charsetControl", "colorCompatControl", "directoryPaletteControl"],
|
|
292
|
-
} as unknown as LayoutNode;
|
|
293
|
-
|
|
294
|
-
// ─── The default config ──────────────────────────────────────────────────────
|
|
295
|
-
|
|
296
|
-
// [LAW:one-source-of-truth] The AUTHORED literal, pre-synthesis. Production
|
|
297
|
-
// code wants the synthesized DEFAULT_DSL_CONFIG below; this is exported only
|
|
298
|
-
// for tests that round-trip "what a user would get by copy-pasting the
|
|
299
|
-
// bundled default into their own file" through the real per-file parse —
|
|
300
|
-
// that round-trip must start from the AUTHORED declarations, never from
|
|
301
|
-
// DEFAULT_DSL_CONFIG's own already-synthesized `menus.*` entries (reparsing
|
|
302
|
-
// those would trip the reserved-namespace guard, which exists to catch a
|
|
303
|
-
// user hand-declaring a name only synthesis may write).
|
|
304
|
-
export const RAW_DEFAULT_DSL_CONFIG = {
|
|
305
|
-
globals: {
|
|
306
|
-
// Picked by the daemon's basePalette resolution; user overrides in their
|
|
307
|
-
// own config. Every registry theme ships the same derived spec set
|
|
308
|
-
// (surface, panel, surface-active, foreground — see rich-js
|
|
309
|
-
// buildPalette), so this is a pure taste call, not a compatibility one.
|
|
310
|
-
// tokyo-night chosen (brandon-theming-8uj.2) over the prior
|
|
311
|
-
// catppuccin-latte — a light palette that landed as a drive-by in an
|
|
312
|
-
// unrelated formatting-cleanup commit and read poorly on the dark
|
|
313
|
-
// terminals most users run — after live-clicking every registry theme
|
|
314
|
-
// through the settings menu's theme picker: it stays legible as the
|
|
315
|
-
// per-row hue-step shifts each
|
|
316
|
-
// row's hue, where warmer bases (gruvbox, dracula) drifted toward mud
|
|
317
|
-
// and the pastel ones (rose-pine, atom-one) washed out at this
|
|
318
|
-
// contrast.
|
|
319
|
-
palette: "tokyo-night",
|
|
320
|
-
},
|
|
321
|
-
|
|
322
|
-
// ─── Variables ─────────────────────────────────────────────────────────────
|
|
323
|
-
// Every value the segment templates read. Sources:
|
|
324
|
-
// • input — daemon's augmented payload (see src/daemon/render-payload.ts)
|
|
325
|
-
// • env — process environment
|
|
326
|
-
// • shell — subprocess; cached
|
|
327
|
-
// • state — per-session daemon state
|
|
328
|
-
variables: {
|
|
329
|
-
// From hookData, pass-through.
|
|
330
|
-
current_dir: {
|
|
331
|
-
kind: "input",
|
|
332
|
-
path: "workspace.current_dir",
|
|
333
|
-
default: "?",
|
|
334
|
-
},
|
|
335
|
-
project_dir: {
|
|
336
|
-
kind: "input",
|
|
337
|
-
path: "workspace.project_dir",
|
|
338
|
-
default: "",
|
|
339
|
-
},
|
|
340
|
-
// Transcript path (a top-level hookData field, spread onto the payload
|
|
341
|
-
// root by buildRenderPayload). Read by the quick-action tray's
|
|
342
|
-
// openTranscript action — pass-through, no projection.
|
|
343
|
-
transcript_path: {
|
|
344
|
-
kind: "input",
|
|
345
|
-
path: "transcript_path",
|
|
346
|
-
default: "",
|
|
347
|
-
},
|
|
348
|
-
"model.display_name": {
|
|
349
|
-
kind: "input",
|
|
350
|
-
path: "model.display_name",
|
|
351
|
-
default: "",
|
|
352
|
-
},
|
|
353
|
-
"session.id": { kind: "input", path: "session_id", default: "" },
|
|
354
|
-
version: { kind: "input", path: "version", default: "" },
|
|
355
|
-
// [LAW:one-source-of-truth] The daemon-resolved effective theme name —
|
|
356
|
-
// effectiveThemeName(sessionState.theme, globals.palette), the SAME name the
|
|
357
|
-
// rendered basePalette is built from. A theme-picker config's trigger reads
|
|
358
|
-
// `{{ .theme.effective }}` to show the active theme, so the label and the
|
|
359
|
-
// colors trace to one resolution and cannot drift — no per-config restating
|
|
360
|
-
// of the initial theme (which JSON5, being inert data, cannot derive). The
|
|
361
|
-
// daemon always provides it; the "" default is the unreachable-absence floor.
|
|
362
|
-
"theme.effective": {
|
|
363
|
-
kind: "input",
|
|
364
|
-
path: "theme.effective",
|
|
365
|
-
default: "",
|
|
366
|
-
},
|
|
367
|
-
// [LAW:one-type-per-behavior] The effective LOOK name, the exact twin of
|
|
368
|
-
// theme.effective one dimension over — effectiveLookName(sessionState.look,
|
|
369
|
-
// globals.look, looks), the SAME name whose ThemeKey adapts the rendered
|
|
370
|
-
// palette. A look-picker trigger reads `{{ .look.effective }}` for its
|
|
371
|
-
// label; the label and the colors trace to one resolution.
|
|
372
|
-
"look.effective": {
|
|
373
|
-
kind: "input",
|
|
374
|
-
path: "look.effective",
|
|
375
|
-
default: "",
|
|
376
|
-
},
|
|
377
|
-
// [LAW:one-type-per-behavior] The effective PRESET name, theme/look's twin
|
|
378
|
-
// one level up — effectivePresetName(sessionState.preset, globals.preset,
|
|
379
|
-
// presets), the SAME name that selected the layout this render walked and
|
|
380
|
-
// the globals it rendered with. A preset-picker trigger reads
|
|
381
|
-
// `{{ .preset.effective }}` for its label, so the label and the arrangement
|
|
382
|
-
// trace to one resolution.
|
|
383
|
-
"preset.effective": {
|
|
384
|
-
kind: "input",
|
|
385
|
-
path: "preset.effective",
|
|
386
|
-
default: "",
|
|
387
|
-
},
|
|
388
|
-
// [LAW:one-source-of-truth] brandon-layout-edit-2gc.5 — presetIsCustomized
|
|
389
|
-
// over the SAME reload's presetRootOps, resolved alongside preset.effective
|
|
390
|
-
// (RenderPayload.preset.customized). edit-chrome.ts's synthesized "↺ …
|
|
391
|
-
// customized" segment gates on this directly; a hand-authored config can
|
|
392
|
-
// read it too for its own reset affordance.
|
|
393
|
-
"preset.customized": {
|
|
394
|
-
kind: "input",
|
|
395
|
-
path: "preset.customized",
|
|
396
|
-
type: "boolean",
|
|
397
|
-
default: false,
|
|
398
|
-
},
|
|
399
|
-
// [LAW:one-type-per-behavior] style/charset/colorCompatibility/autoWrap/
|
|
400
|
-
// padding are theme/look's twins over the remaining persistable globals
|
|
401
|
-
// (candybar-config-engine-71o.3) — the SAME values BuildLineOptions
|
|
402
|
-
// renders with, each read back through this projection so a `persist`
|
|
403
|
-
// action over the field shows a "current selection" highlight and a
|
|
404
|
-
// trigger label can display the active value without restating it.
|
|
405
|
-
"style.effective": {
|
|
406
|
-
kind: "input",
|
|
407
|
-
path: "style.effective",
|
|
408
|
-
default: "",
|
|
409
|
-
},
|
|
410
|
-
"charset.effective": {
|
|
411
|
-
kind: "input",
|
|
412
|
-
path: "charset.effective",
|
|
413
|
-
default: "",
|
|
414
|
-
},
|
|
415
|
-
"colorCompatibility.effective": {
|
|
416
|
-
kind: "input",
|
|
417
|
-
path: "colorCompatibility.effective",
|
|
418
|
-
default: "",
|
|
419
|
-
},
|
|
420
|
-
"autoWrap.effective": {
|
|
421
|
-
kind: "input",
|
|
422
|
-
path: "autoWrap.effective",
|
|
423
|
-
type: "boolean",
|
|
424
|
-
default: true,
|
|
425
|
-
},
|
|
426
|
-
"padding.effective": {
|
|
427
|
-
kind: "input",
|
|
428
|
-
path: "padding.effective",
|
|
429
|
-
type: "number",
|
|
430
|
-
default: 1,
|
|
431
|
-
},
|
|
432
|
-
|
|
433
|
-
// [LAW:one-source-of-truth] The usable terminal width for THIS render —
|
|
434
|
-
// the exact post-reserve cell count FlexStrip wraps to. renderDsl injects
|
|
435
|
-
// it into the payload from its own `opts.width` (the single value that
|
|
436
|
-
// feeds both the wrap and this variable), so a width-paginated picker and
|
|
437
|
-
// the wrap algebra can never disagree. Never cached: a resize is just a
|
|
438
|
-
// new value on the same path, re-read every render. The default only
|
|
439
|
-
// applies to compile-only callers that render without injecting a width.
|
|
440
|
-
"term.cols": {
|
|
441
|
-
kind: "input",
|
|
442
|
-
path: "term.cols",
|
|
443
|
-
type: "number",
|
|
444
|
-
default: 80,
|
|
445
|
-
},
|
|
446
|
-
|
|
447
|
-
// [LAW:one-source-of-truth] The per-segment hue-rotation step renderDsl
|
|
448
|
-
// reads (HUE_STEP_VAR) — a value in the store like every other render input,
|
|
449
|
-
// NOT a globals field. [LAW:types-are-the-program] In the bundled default it
|
|
450
|
-
// is a LITERAL: nothing here writes the "hue-step" SessionState key (the
|
|
451
|
-
// default declares no interactive actions), so a fixed 14° is the strongest
|
|
452
|
-
// TRUE theorem — a `state` var would claim a session-variability the default
|
|
453
|
-
// never exercises and force a SessionState on every consumer. A user makes
|
|
454
|
-
// hue live by overriding this one variable to `{ kind: "state", key:
|
|
455
|
-
// "hue-step" }` and adding a pair of bounded stepper actions — the same
|
|
456
|
-
// two-part pattern the theme picker uses.
|
|
457
|
-
// 14°: adjacent segments stay visually distinct without per-segment colors.
|
|
458
|
-
"hue.step": { kind: "literal", value: 14 },
|
|
459
|
-
|
|
460
|
-
// home flows through the augmented payload (buildRenderPayload reads
|
|
461
|
-
// HOME, falling back to USERPROFILE on Windows where HOME is often
|
|
462
|
-
// unset). Sourcing via `kind: "input"` rather than `kind: "env",
|
|
463
|
-
// name: "HOME"` makes the directory `~` collapse work on every
|
|
464
|
-
// platform without per-platform config edits.
|
|
465
|
-
home: { kind: "input", path: "home", default: "" },
|
|
466
|
-
|
|
467
|
-
// Tmux session id flows through the daemon's augmented payload
|
|
468
|
-
// (TmuxService caches by socket and never re-spawns for the lifetime of
|
|
469
|
-
// the daemon, so this stays cheap). A `kind: "shell"` declaration would
|
|
470
|
-
// spawn the subprocess at every cache-entry creation regardless of
|
|
471
|
-
// whether the tmux segment is in the active layout — buildNeededPrefixes
|
|
472
|
-
// gates the input variant so unused segments cost nothing.
|
|
473
|
-
"tmux.session": { kind: "input", path: "tmux.session", default: "" },
|
|
474
|
-
|
|
475
|
-
// Host identity — which machine this session is on, and whether the user
|
|
476
|
-
// arrived over SSH. All three come through the augmented payload rather
|
|
477
|
-
// than `kind: "env"` / `kind: "shell"`, and that is not a style choice:
|
|
478
|
-
//
|
|
479
|
-
// • `host.ssh` CANNOT be an env var here. Variables are evaluated in the
|
|
480
|
-
// DAEMON, which is detached and serves every session for this user at
|
|
481
|
-
// once — its `SSH_*` env describes whichever shell happened to spawn
|
|
482
|
-
// it. The fact is captured by the live client and carried as a wire
|
|
483
|
-
// hint (the `termCols` pattern); the payload is the only honest source.
|
|
484
|
-
// • `host.name`/`host.user` are machine facts the daemon reads directly,
|
|
485
|
-
// so they cost two syscalls instead of a per-render subprocess.
|
|
486
|
-
//
|
|
487
|
-
// Defaults are the "unknown" values, and for `ssh` that is `false`: an
|
|
488
|
-
// absent field (a client too old to send the hint) renders as local, which
|
|
489
|
-
// is the pre-feature behavior, while the input-fallback chain records a
|
|
490
|
-
// `last_error` so `cc-candybar debug vars` can still tell the two apart.
|
|
491
|
-
"host.name": { kind: "input", path: "host.name", default: "" },
|
|
492
|
-
"host.user": { kind: "input", path: "host.user", default: "" },
|
|
493
|
-
"host.ssh": {
|
|
494
|
-
kind: "input",
|
|
495
|
-
path: "host.ssh",
|
|
496
|
-
type: "boolean",
|
|
497
|
-
default: false,
|
|
498
|
-
},
|
|
499
|
-
|
|
500
|
-
// Git — every field flows from the daemon's projected GitInfo payload.
|
|
501
|
-
// The DSL's native `kind: "git"` source covers a 6-field subset
|
|
502
|
-
// (branch/sha/dirty/ahead/behind/stash); using `input` here gives the
|
|
503
|
-
// full 12-field surface uniformly via the augmented payload.
|
|
504
|
-
"git.repoName": {
|
|
505
|
-
kind: "input",
|
|
506
|
-
path: "git.repoName",
|
|
507
|
-
default: "",
|
|
508
|
-
},
|
|
509
|
-
// The repo's browsable web page, transposed from its remote by the daemon.
|
|
510
|
-
// "" is the genuine "no remote a browser can open" (local-only repo, bare
|
|
511
|
-
// path remote) — the toolbar's link reads that value, not a flag.
|
|
512
|
-
"git.repoUrl": { kind: "input", path: "git.repoUrl", default: "" },
|
|
513
|
-
"git.branch": { kind: "input", path: "git.branch", default: "" },
|
|
514
|
-
"git.sha": { kind: "input", path: "git.sha", default: "" },
|
|
515
|
-
"git.ahead": {
|
|
516
|
-
kind: "input",
|
|
517
|
-
path: "git.ahead",
|
|
518
|
-
type: "number",
|
|
519
|
-
default: 0,
|
|
520
|
-
},
|
|
521
|
-
"git.behind": {
|
|
522
|
-
kind: "input",
|
|
523
|
-
path: "git.behind",
|
|
524
|
-
type: "number",
|
|
525
|
-
default: 0,
|
|
526
|
-
},
|
|
527
|
-
"git.staged": {
|
|
528
|
-
kind: "input",
|
|
529
|
-
path: "git.staged",
|
|
530
|
-
type: "number",
|
|
531
|
-
default: 0,
|
|
532
|
-
},
|
|
533
|
-
"git.unstaged": {
|
|
534
|
-
kind: "input",
|
|
535
|
-
path: "git.unstaged",
|
|
536
|
-
type: "number",
|
|
537
|
-
default: 0,
|
|
538
|
-
},
|
|
539
|
-
"git.untracked": {
|
|
540
|
-
kind: "input",
|
|
541
|
-
path: "git.untracked",
|
|
542
|
-
type: "number",
|
|
543
|
-
default: 0,
|
|
544
|
-
},
|
|
545
|
-
"git.conflicts": {
|
|
546
|
-
kind: "input",
|
|
547
|
-
path: "git.conflicts",
|
|
548
|
-
type: "number",
|
|
549
|
-
default: 0,
|
|
550
|
-
},
|
|
551
|
-
"git.upstream": { kind: "input", path: "git.upstream", default: "" },
|
|
552
|
-
"git.stash": {
|
|
553
|
-
kind: "input",
|
|
554
|
-
path: "git.stash",
|
|
555
|
-
type: "number",
|
|
556
|
-
default: 0,
|
|
557
|
-
},
|
|
558
|
-
"git.status": { kind: "input", path: "git.status", default: "clean" },
|
|
559
|
-
"git.operation": { kind: "input", path: "git.operation", default: "" },
|
|
560
|
-
"git.timeSinceCommit": {
|
|
561
|
-
kind: "input",
|
|
562
|
-
path: "git.timeSinceCommit",
|
|
563
|
-
type: "number",
|
|
564
|
-
default: 0,
|
|
565
|
-
},
|
|
566
|
-
|
|
567
|
-
// Forge PR/MR — the daemon's git provider resolves the branch's open PR via
|
|
568
|
-
// gh/glab and projects it here. Declaring any of these turns on the network
|
|
569
|
-
// lookup. [LAW:no-silent-failure] prError is non-empty ONLY when the forge
|
|
570
|
-
// was asked but couldn't answer (auth/network) — distinct from "no PR"
|
|
571
|
-
// (every field empty). prNumber 0 (default) ⇒ no open PR.
|
|
572
|
-
"git.prNumber": {
|
|
573
|
-
kind: "input",
|
|
574
|
-
path: "git.prNumber",
|
|
575
|
-
type: "number",
|
|
576
|
-
default: 0,
|
|
577
|
-
},
|
|
578
|
-
"git.prState": { kind: "input", path: "git.prState", default: "" },
|
|
579
|
-
"git.prUrl": { kind: "input", path: "git.prUrl", default: "" },
|
|
580
|
-
"git.prError": { kind: "input", path: "git.prError", default: "" },
|
|
581
|
-
|
|
582
|
-
// Prompt-cache expiry — epoch seconds, projected by the cache provider.
|
|
583
|
-
// Same unit/shape as block/weekly resetsAt so the cacheTimer segment
|
|
584
|
-
// composes `minutesUntilReset` identically. 0 (default) ⇒ no cache
|
|
585
|
-
// activity found ⇒ segment's `when` hides it.
|
|
586
|
-
"cache.expiresAt": {
|
|
587
|
-
kind: "input",
|
|
588
|
-
path: "cache.expiresAt",
|
|
589
|
-
type: "number",
|
|
590
|
-
default: 0,
|
|
591
|
-
},
|
|
592
|
-
|
|
593
|
-
// Usage / cost — daemon folds from the SessionUsageStore; numeric.
|
|
594
|
-
"session.cost": {
|
|
595
|
-
kind: "input",
|
|
596
|
-
path: "session.cost",
|
|
597
|
-
type: "number",
|
|
598
|
-
default: 0,
|
|
599
|
-
},
|
|
600
|
-
"session.tokens": {
|
|
601
|
-
kind: "input",
|
|
602
|
-
path: "session.tokens",
|
|
603
|
-
type: "number",
|
|
604
|
-
default: 0,
|
|
605
|
-
},
|
|
606
|
-
// Budget knobs — pure config constants, user overrides in their file.
|
|
607
|
-
// [LAW:dataflow-not-control-flow] amount defaults 0, the budgetStatus
|
|
608
|
-
// helper's non-displayable value, so with no user override the session
|
|
609
|
-
// segment renders byte-identically to its pre-budget form through the
|
|
610
|
-
// same unconditional template — opt-in is a value, not a config mode.
|
|
611
|
-
// Matches the legacy shipped default (budget.session had a threshold but
|
|
612
|
-
// no amount ⇒ suffix off until the user sets an amount).
|
|
613
|
-
"session.budget.amount": { kind: "literal", value: 0 },
|
|
614
|
-
"session.budget.warningThreshold": { kind: "literal", value: 80 },
|
|
615
|
-
|
|
616
|
-
// Today — daemon folds today's cross-session total from the SessionUsageStore.
|
|
617
|
-
"today.cost": {
|
|
618
|
-
kind: "input",
|
|
619
|
-
path: "today.cost",
|
|
620
|
-
type: "number",
|
|
621
|
-
default: 0,
|
|
622
|
-
},
|
|
623
|
-
"today.tokens": {
|
|
624
|
-
kind: "input",
|
|
625
|
-
path: "today.tokens",
|
|
626
|
-
type: "number",
|
|
627
|
-
default: 0,
|
|
628
|
-
},
|
|
629
|
-
// Budget knobs — pure config constants, user overrides in their file.
|
|
630
|
-
"today.budget.amount": { kind: "literal", value: 50 },
|
|
631
|
-
"today.budget.warningThreshold": { kind: "literal", value: 80 },
|
|
632
|
-
|
|
633
|
-
// Block — daemon projects directly from hookData.rate_limits.five_hour;
|
|
634
|
-
// resetsAt is raw epoch seconds
|
|
635
|
-
// so the template can compose `minutesUntilReset .block.resetsAt` (the
|
|
636
|
-
// same chain weekly uses, single composition point).
|
|
637
|
-
"block.nativeUtilization": {
|
|
638
|
-
kind: "input",
|
|
639
|
-
path: "block.nativeUtilization",
|
|
640
|
-
type: "number",
|
|
641
|
-
default: 0,
|
|
642
|
-
},
|
|
643
|
-
"block.resetsAt": {
|
|
644
|
-
kind: "input",
|
|
645
|
-
path: "block.resetsAt",
|
|
646
|
-
type: "number",
|
|
647
|
-
default: 0,
|
|
648
|
-
},
|
|
649
|
-
// Budget knob — overridable per-config through the variables-merge-by-
|
|
650
|
-
// name cascade. Matches the legacy DEFAULT_CONFIG.budget.block.warning
|
|
651
|
-
// Threshold.
|
|
652
|
-
"block.budget.warningThreshold": { kind: "literal", value: 80 },
|
|
653
|
-
|
|
654
|
-
// Weekly — direct projection of hookData.rate_limits.seven_day.
|
|
655
|
-
"weekly.percentage": {
|
|
656
|
-
kind: "input",
|
|
657
|
-
path: "weekly.percentage",
|
|
658
|
-
type: "number",
|
|
659
|
-
default: 0,
|
|
660
|
-
},
|
|
661
|
-
"weekly.resetsAt": {
|
|
662
|
-
kind: "input",
|
|
663
|
-
path: "weekly.resetsAt",
|
|
664
|
-
type: "number",
|
|
665
|
-
default: 0,
|
|
666
|
-
},
|
|
667
|
-
"weekly.budget.warningThreshold": { kind: "literal", value: 80 },
|
|
668
|
-
|
|
669
|
-
// Burn rate + cap projection — daemon-derived (see render-payload.ts).
|
|
670
|
-
// Each projection is ABSENT when not projectable; the var-system fills the
|
|
671
|
-
// -1 default, a structurally-impossible value the burnrate helpers read as
|
|
672
|
-
// "—" [LAW:no-silent-failure] (0 minutes / $0-per-hr are real, displayable
|
|
673
|
-
// values, so they cannot double as the absence marker).
|
|
674
|
-
"burn.costPerHour": {
|
|
675
|
-
kind: "input",
|
|
676
|
-
path: "burn.costPerHour",
|
|
677
|
-
type: "number",
|
|
678
|
-
default: -1,
|
|
679
|
-
},
|
|
680
|
-
"block.etaMinutes": {
|
|
681
|
-
kind: "input",
|
|
682
|
-
path: "block.etaMinutes",
|
|
683
|
-
type: "number",
|
|
684
|
-
default: -1,
|
|
685
|
-
},
|
|
686
|
-
"weekly.etaMinutes": {
|
|
687
|
-
kind: "input",
|
|
688
|
-
path: "weekly.etaMinutes",
|
|
689
|
-
type: "number",
|
|
690
|
-
default: -1,
|
|
691
|
-
},
|
|
692
|
-
// ETA-heat thresholds (minutes-to-cap) — overridable per-config through the
|
|
693
|
-
// variables-merge-by-name cascade, like the *.budget.warningThreshold knobs.
|
|
694
|
-
"burn.eta.warnMinutes": { kind: "literal", value: 60 },
|
|
695
|
-
"burn.eta.errorMinutes": { kind: "literal", value: 30 },
|
|
696
|
-
|
|
697
|
-
// Token throughput for the active turn — daemon-derived tok/s on three lanes
|
|
698
|
-
// (render-payload.ts: successive-render delta over the SessionUsageStore).
|
|
699
|
-
// Same absence idiom as burn: -1 is the structurally-impossible default the
|
|
700
|
-
// `formatSpeed` helper reads as "—" [LAW:no-silent-failure] (0 tok/s is a
|
|
701
|
-
// real reading, so it cannot double as the absence marker). Each lane is
|
|
702
|
-
// independently absent — `input` reads "—" mid-stream while `output` flows.
|
|
703
|
-
"speed.input": {
|
|
704
|
-
kind: "input",
|
|
705
|
-
path: "speed.input",
|
|
706
|
-
type: "number",
|
|
707
|
-
default: -1,
|
|
708
|
-
},
|
|
709
|
-
"speed.output": {
|
|
710
|
-
kind: "input",
|
|
711
|
-
path: "speed.output",
|
|
712
|
-
type: "number",
|
|
713
|
-
default: -1,
|
|
714
|
-
},
|
|
715
|
-
"speed.total": {
|
|
716
|
-
kind: "input",
|
|
717
|
-
path: "speed.total",
|
|
718
|
-
type: "number",
|
|
719
|
-
default: -1,
|
|
720
|
-
},
|
|
721
|
-
// Recent burn-rate trend: a comma-delimited series of total-lane tok/s the
|
|
722
|
-
// daemon folds from its sample ring (render-payload.ts). A series cannot
|
|
723
|
-
// cross the scalar var-system seam, so it travels as a string the
|
|
724
|
-
// `sparkline` helper decodes. Default "" is the genuine "no history yet"
|
|
725
|
-
// form (the helper renders nothing); the segment gates on it being present.
|
|
726
|
-
"speed.history": {
|
|
727
|
-
kind: "input",
|
|
728
|
-
path: "speed.history",
|
|
729
|
-
type: "string",
|
|
730
|
-
default: "",
|
|
731
|
-
},
|
|
732
|
-
|
|
733
|
-
// Context — daemon fetches via ContextProvider; contextLeftPercentage.
|
|
734
|
-
"context.totalTokens": {
|
|
735
|
-
kind: "input",
|
|
736
|
-
path: "context.totalTokens",
|
|
737
|
-
type: "number",
|
|
738
|
-
default: 0,
|
|
739
|
-
},
|
|
740
|
-
"context.contextLeft": {
|
|
741
|
-
kind: "input",
|
|
742
|
-
path: "context.contextLeft",
|
|
743
|
-
type: "number",
|
|
744
|
-
default: 100,
|
|
745
|
-
},
|
|
746
|
-
|
|
747
|
-
// Metrics — daemon fetches via MetricsProvider; numeric.
|
|
748
|
-
"metrics.lastResponseTime": {
|
|
749
|
-
kind: "input",
|
|
750
|
-
path: "metrics.lastResponseTime",
|
|
751
|
-
type: "number",
|
|
752
|
-
default: 0,
|
|
753
|
-
},
|
|
754
|
-
"metrics.responseTime": {
|
|
755
|
-
kind: "input",
|
|
756
|
-
path: "metrics.responseTime",
|
|
757
|
-
type: "number",
|
|
758
|
-
default: 0,
|
|
759
|
-
},
|
|
760
|
-
"metrics.sessionDuration": {
|
|
761
|
-
kind: "input",
|
|
762
|
-
path: "metrics.sessionDuration",
|
|
763
|
-
type: "number",
|
|
764
|
-
default: 0,
|
|
765
|
-
},
|
|
766
|
-
"metrics.messageCount": {
|
|
767
|
-
kind: "input",
|
|
768
|
-
path: "metrics.messageCount",
|
|
769
|
-
type: "number",
|
|
770
|
-
default: 0,
|
|
771
|
-
},
|
|
772
|
-
"metrics.linesAdded": {
|
|
773
|
-
kind: "input",
|
|
774
|
-
path: "metrics.linesAdded",
|
|
775
|
-
type: "number",
|
|
776
|
-
default: 0,
|
|
777
|
-
},
|
|
778
|
-
"metrics.linesRemoved": {
|
|
779
|
-
kind: "input",
|
|
780
|
-
path: "metrics.linesRemoved",
|
|
781
|
-
type: "number",
|
|
782
|
-
default: 0,
|
|
783
|
-
},
|
|
784
|
-
|
|
785
|
-
// No page-cursor var: a {{ menu }} synthesizes its own page
|
|
786
|
-
// cursor (state var + int action, named by menuPageKey) under the reserved
|
|
787
|
-
// menus.* namespace, alongside its open-state.
|
|
788
|
-
},
|
|
789
|
-
|
|
790
|
-
// ─── Segments ──────────────────────────────────────────────────────────────
|
|
791
|
-
// Every built-in. Templates ported from the parity bindings; bg/fg are
|
|
792
|
-
// palette spec names resolved against the active theme. `when` predicates
|
|
793
|
-
// hide a segment when its primary signal is absent (no git repo, no version
|
|
794
|
-
// field, no env var, no tmux, no rate-limit window).
|
|
795
|
-
//
|
|
796
|
-
// [LAW:one-source-of-truth] Templates author CONTENT only — the intra-cell
|
|
797
|
-
// padding (the space each side of a cell) is render chrome synthesized
|
|
798
|
-
// structurally from the one resolved globals.padding (default 1), never
|
|
799
|
-
// authored here. A template with leading/trailing spaces would render them
|
|
800
|
-
// IN ADDITION to the structural padding.
|
|
801
|
-
segments: {
|
|
802
|
-
directory: {
|
|
803
|
-
template: DIR_TEMPLATE,
|
|
804
|
-
bg: "surface",
|
|
805
|
-
fg: "foreground",
|
|
806
|
-
},
|
|
807
|
-
model: {
|
|
808
|
-
template: "✱ {{ formatModelName .model.display_name }}",
|
|
809
|
-
bg: "panel",
|
|
810
|
-
fg: "foreground",
|
|
811
|
-
when: '{{ ne .model.display_name "" }}',
|
|
812
|
-
},
|
|
813
|
-
sessionId: {
|
|
814
|
-
template: "⌗{{ trunc 8 .session.id }}",
|
|
815
|
-
bg: "surface",
|
|
816
|
-
fg: "foreground",
|
|
817
|
-
when: '{{ ne .session.id "" }}',
|
|
818
|
-
},
|
|
819
|
-
version: {
|
|
820
|
-
template: "◈ v{{ .version }}",
|
|
821
|
-
bg: "surface",
|
|
822
|
-
fg: "foreground",
|
|
823
|
-
when: '{{ ne .version "" }}',
|
|
824
|
-
},
|
|
825
|
-
tmux: {
|
|
826
|
-
template: 'tmux:{{ .tmux.session | default "none" }}',
|
|
827
|
-
bg: "surface-active",
|
|
828
|
-
fg: "foreground",
|
|
829
|
-
when: '{{ ne .tmux.session "" }}',
|
|
830
|
-
},
|
|
831
|
-
// "You are not on your own machine." Modelled on the git-taculous zsh
|
|
832
|
-
// theme, which prepends `(%n@%m)` to the prompt under SSH and shows
|
|
833
|
-
// nothing locally — you already know your own hostname.
|
|
834
|
-
//
|
|
835
|
-
// [LAW:dataflow-not-control-flow] Presence IS the signal. There is no SSH
|
|
836
|
-
// "mode" and no force-on flag (git-taculous's GITTACULOUS_ENABLE_SSH_THEME
|
|
837
|
-
// would be a flag with no deletion date, [LAW:no-mode-explosion]); the cell
|
|
838
|
-
// exists exactly when the value says so, like tmux/block/weekly. A user who
|
|
839
|
-
// wants it always-on overrides this one segment's `when` to `"true"`.
|
|
840
|
-
//
|
|
841
|
-
// `bg: "warning"` is load-bearing, not decoration: warning is one of the
|
|
842
|
-
// hue-ANCHORED palette roots, so it survives every theme, look, and
|
|
843
|
-
// per-segment hue transposition still reading as an alert. Any other slot
|
|
844
|
-
// would drift with the hue stepper and could land camouflaged against its
|
|
845
|
-
// neighbours — exactly what a "wrong machine" warning must never do.
|
|
846
|
-
// `contrastOn (bgOf)` then derives a readable foreground from whatever that
|
|
847
|
-
// resolves to, rather than betting a fixed `foreground` stays legible.
|
|
848
|
-
//
|
|
849
|
-
// Each half falls back to "?" so a failed hostname/username read renders
|
|
850
|
-
// `⇄ ?@?` — still unmistakably "remote", and legibly missing its identity
|
|
851
|
-
// rather than a blank that reads as a rendering bug ([LAW:no-silent-failure]).
|
|
852
|
-
host: {
|
|
853
|
-
template:
|
|
854
|
-
'⇄ {{ .host.user | default "?" }}@{{ .host.name | default "?" }}',
|
|
855
|
-
bg: "warning",
|
|
856
|
-
fg: "{{ contrastOn (bgOf) }}",
|
|
857
|
-
when: "{{ .host.ssh }}",
|
|
858
|
-
},
|
|
859
|
-
git: {
|
|
860
|
-
template: GIT_TEMPLATE,
|
|
861
|
-
bg: "surface-active",
|
|
862
|
-
// A computed `fg:` — the field is a template evaluating to a color
|
|
863
|
-
// reference, and `bgOf` is available here because a segment's background
|
|
864
|
-
// is resolved before its foreground. Structural text therefore sits a
|
|
865
|
-
// fixed distance from THIS segment's background whatever theme, look, or
|
|
866
|
-
// hue shift is in effect.
|
|
867
|
-
fg: GIT_QUIET_FG,
|
|
868
|
-
when: '{{ ne .git.branch "" }}',
|
|
869
|
-
},
|
|
870
|
-
gitaculous: {
|
|
871
|
-
// Recolored from raw green/red (render-bugs-pdu.3's era) to semantic
|
|
872
|
-
// palette names (brandon-segments-3eo.1), then unified against `git`'s
|
|
873
|
-
// choice of color per fact via the shared GIT_COLOR table above
|
|
874
|
-
// (brandon-segments-3eo.1.1 — the two had drifted: branch, untracked,
|
|
875
|
-
// and stash each disagreed with `git`'s coloring of the same fact).
|
|
876
|
-
// staged/ahead share `success` (positive — ready to commit / unpushed
|
|
877
|
-
// additions), unstaged/behind share `warning` (needs attention),
|
|
878
|
-
// untracked/stash share `accent` (their own GIT_COLOR entries, kept
|
|
879
|
-
// visually distinct from unstaged by using a different glyph, not a
|
|
880
|
-
// different color, so "U" vs "?" reads apart at a glance), conflicts
|
|
881
|
-
// gets its own `error`, branch gets `primary`.
|
|
882
|
-
//
|
|
883
|
-
// Everything the template does NOT paint — the "(git)" label, repo name,
|
|
884
|
-
// the operation and upstream brackets, the sha, the elapsed-time
|
|
885
|
-
// annotation — renders in the segment's quiet `fg:` and recedes, so the
|
|
886
|
-
// eye lands on the operative colored facts first (brandon-segments-
|
|
887
|
-
// 3eo.1.1.1: live feedback that "most of it" read as one flat color when
|
|
888
|
-
// only two facts happened to be present). Note that this needs no markup
|
|
889
|
-
// in the template — including around `{{ template "formatTimeSince" }}`,
|
|
890
|
-
// which as a top-level Go-template action could never have been wrapped
|
|
891
|
-
// in a styling call at all. Making quiet the default rather than a
|
|
892
|
-
// wrapper is what put that token in reach.
|
|
893
|
-
template:
|
|
894
|
-
"(git)" +
|
|
895
|
-
'{{ if ne .git.repoName "" }} {{ .git.repoName }}{{ end }}' +
|
|
896
|
-
'{{ if ne .git.operation "" }} [{{ .git.operation }}]{{ end }}' +
|
|
897
|
-
'{{ if ne .git.sha "" }} {{ .git.sha }}{{ end }}' +
|
|
898
|
-
"{{ if or (gt .git.staged 0) (gt .git.unstaged 0) (gt .git.untracked 0) (gt .git.conflicts 0) }} " +
|
|
899
|
-
`{{ if gt .git.staged 0 }}${paint("staged", '"S"')}{{ end }}` +
|
|
900
|
-
`{{ if gt .git.unstaged 0 }}${paint("unstaged", '"U"')}{{ end }}` +
|
|
901
|
-
`{{ if gt .git.untracked 0 }}${paint("untracked", '"?"')}{{ end }}` +
|
|
902
|
-
`{{ if gt .git.conflicts 0 }}${paint("conflicts", '(printf "!%v" .git.conflicts)')}{{ end }}` +
|
|
903
|
-
"{{ end }}" +
|
|
904
|
-
` ⎇ ${paint("branch", ".git.branch")}` +
|
|
905
|
-
'{{ if ne .git.upstream "" }} [{{ .git.upstream }}' +
|
|
906
|
-
"{{ if or (gt .git.ahead 0) (gt .git.behind 0) }} " +
|
|
907
|
-
`{{ if gt .git.ahead 0 }}${paint("ahead", '(printf "+%v" .git.ahead)')}{{ end }}` +
|
|
908
|
-
"{{ if and (gt .git.ahead 0) (gt .git.behind 0) }}/{{ end }}" +
|
|
909
|
-
`{{ if gt .git.behind 0 }}${paint("behind", '(printf "-%v" .git.behind)')}{{ end }}` +
|
|
910
|
-
"{{ end }}]{{ end }}" +
|
|
911
|
-
`{{ if gt .git.stash 0 }} ${paint("stash", '(printf "(%v stashed)" .git.stash)')}{{ end }}` +
|
|
912
|
-
'{{ if gt .git.timeSinceCommit 0 }} ◷ {{ template "formatTimeSince" .git.timeSinceCommit }}{{ end }}',
|
|
913
|
-
bg: "surface-active",
|
|
914
|
-
fg: GIT_QUIET_FG,
|
|
915
|
-
when: '{{ ne .git.branch "" }}',
|
|
916
|
-
},
|
|
917
|
-
// Git PR/MR — the branch's open pull/merge request as a clickable link.
|
|
918
|
-
// OPT-IN: declared but NOT in the default root (it adds a network gh/glab
|
|
919
|
-
// call). Add "gitPr" to a container's children to enable it. The `{{ link
|
|
920
|
-
// url text }}` emits ONE OSC-8 region carrying the https PR url, so the
|
|
921
|
-
// CLICK is handled by the terminal/OS (opens the browser) — no daemon verb.
|
|
922
|
-
// [LAW:no-silent-failure] Three render states from the data: an open PR
|
|
923
|
-
// (prUrl set) renders the link; a lookup FAILURE (prError set, prUrl empty)
|
|
924
|
-
// renders a distinct ⚠ marker so an outage is not mistaken for "no PR";
|
|
925
|
-
// no PR (both empty) leaves the `when` gate false and the segment absent.
|
|
926
|
-
gitPr: {
|
|
927
|
-
// The pad spaces are structural chrome now, OUTSIDE the OSC-8 link
|
|
928
|
-
// region — the clickable area is the glyph text itself.
|
|
929
|
-
template:
|
|
930
|
-
'{{ if ne .git.prUrl "" }}' +
|
|
931
|
-
'{{ link .git.prUrl (printf "⇆ #%v" .git.prNumber) }}' +
|
|
932
|
-
"{{ else }}⚠ PR{{ end }}",
|
|
933
|
-
bg: "surface-active",
|
|
934
|
-
fg: "foreground",
|
|
935
|
-
when: '{{ or (ne .git.prUrl "") (ne .git.prError "") }}',
|
|
936
|
-
},
|
|
937
|
-
// Quick-action tray — the default bar's interactivity: copy the session id,
|
|
938
|
-
// open the project dir / transcript (this session's jsonl) in the editor,
|
|
939
|
-
// open the repo's web page in the browser, and toggle layout edit mode.
|
|
940
|
-
// (copyDir — copy the cwd — stays declared as an action below for users who
|
|
941
|
-
// want a fifth glyph; it is simply not in the default tray.)
|
|
942
|
-
// [LAW:locality-or-seam] The glyph is the REPRESENTATION; the named action
|
|
943
|
-
// (below) is the BEHAVIOR; the action name is the seam between them. Re-glyph
|
|
944
|
-
// without touching behavior; re-target without touching this template. Each
|
|
945
|
-
// `{{ action … }}` emits one OSC-8 clickable region whose URL the wire codec
|
|
946
|
-
// owns end-to-end.
|
|
947
|
-
//
|
|
948
|
-
// `↗ repo` is the one glyph here that is NOT an action: the daemon already
|
|
949
|
-
// resolved the remote to an https page, so `{{ link }}` hands that URL
|
|
950
|
-
// straight to the terminal/OS (same seam the gitPr segment uses) — routing a
|
|
951
|
-
// public web URL through a cc-candybar:// verb would buy nothing. It is
|
|
952
|
-
// gated on the VALUE (`ne … ""`), not on a flag: a local-only repo simply
|
|
953
|
-
// supplies no page and the glyph is absent. [LAW:dataflow-not-control-flow]
|
|
954
|
-
//
|
|
955
|
-
// `✎ edit`/`✎ done` (brandon-layout-edit-2gc.4) is the bundled default's
|
|
956
|
-
// ONLY reference to the reserved `edit.toggle` action — referencing it
|
|
957
|
-
// anywhere is what opts this config into edit mode (see
|
|
958
|
-
// docs/interaction-authoring.md's "Edit mode" section), and this tray
|
|
959
|
-
// segment is where it lives.
|
|
960
|
-
//
|
|
961
|
-
// [LAW:carrying-cost] Placement resolves a self-lockout tension .3 flagged
|
|
962
|
-
// (see the epic's tickets): once edit mode is open, EVERY ordinary segment
|
|
963
|
-
// gets its own removable `-`, including whichever one hosts the trigger —
|
|
964
|
-
// a config can, in principle, remove its own way back into edit mode.
|
|
965
|
-
// Giving the trigger its own standalone segment would make that a one-click
|
|
966
|
-
// accident. Folding it into `toolbar` instead means removing the trigger
|
|
967
|
-
// requires removing the WHOLE quick-action tray — the same deliberate,
|
|
968
|
-
// symmetric risk every other multi-purpose segment already carries, not a
|
|
969
|
-
// bespoke edit-mode hazard — and it costs the default bar one glyph of
|
|
970
|
-
// width instead of a whole new segment's cell+padding+joiner overhead. The
|
|
971
|
-
// risk is bounded either way: `-` only removes the segment from this
|
|
972
|
-
// preset's tree (`edit.mode` itself is untouched SessionState), so the
|
|
973
|
-
// rest of the chrome — every remaining `+`/`-` in the bar — stays visible,
|
|
974
|
-
// and any of them can `+` `toolbar` straight back
|
|
975
|
-
// (test/dsl-layout-edit.test.ts covers the full round trip through a
|
|
976
|
-
// real RenderCache reload; test/dsl-edit-mode.test.ts covers the click
|
|
977
|
-
// itself and that edit.mode survives it).
|
|
978
|
-
toolbar: {
|
|
979
|
-
template:
|
|
980
|
-
'{{ action "copySession" "⎘ id" }}' +
|
|
981
|
-
' {{ action "openProject" "↗ proj" }} {{ action "openTranscript" "↗ log" }}' +
|
|
982
|
-
'{{ if ne .git.repoUrl "" }} {{ link .git.repoUrl "↗ repo" }}{{ end }}' +
|
|
983
|
-
' {{ action "edit.toggle" "✎ edit" "✎ done" }}',
|
|
984
|
-
bg: "surface",
|
|
985
|
-
fg: "foreground",
|
|
986
|
-
},
|
|
987
|
-
session: {
|
|
988
|
-
template:
|
|
989
|
-
'§ {{ template "formatCost" .session.cost }} ({{ template "formatTokens" .session.tokens }})' +
|
|
990
|
-
'{{ template "budgetStatus" (dict "cost" .session.cost "budget" .session.budget.amount "warn" .session.budget.warningThreshold) }}',
|
|
991
|
-
bg: "surface",
|
|
992
|
-
fg: "foreground",
|
|
993
|
-
},
|
|
994
|
-
today: {
|
|
995
|
-
template:
|
|
996
|
-
'☉ {{ template "formatCost" .today.cost }} ({{ template "formatTokens" .today.tokens }})' +
|
|
997
|
-
'{{ template "budgetStatus" (dict "cost" .today.cost "budget" .today.budget.amount "warn" .today.budget.warningThreshold) }}',
|
|
998
|
-
bg: "surface",
|
|
999
|
-
fg: "foreground",
|
|
1000
|
-
},
|
|
1001
|
-
block: {
|
|
1002
|
-
template:
|
|
1003
|
-
"◱ {{ round .block.nativeUtilization }}% " +
|
|
1004
|
-
'({{ template "formatLongTimeRemaining" (minutesUntilReset .block.resetsAt) }})',
|
|
1005
|
-
bg: blockLikeBg(
|
|
1006
|
-
".block.nativeUtilization",
|
|
1007
|
-
".block.budget.warningThreshold",
|
|
1008
|
-
),
|
|
1009
|
-
fg: blockLikeFg(".block.nativeUtilization"),
|
|
1010
|
-
// Hide unless we have a five-hour-window snapshot.
|
|
1011
|
-
when: "{{ gt .block.resetsAt 0 }}",
|
|
1012
|
-
},
|
|
1013
|
-
weekly: {
|
|
1014
|
-
template:
|
|
1015
|
-
"◑ {{ round .weekly.percentage }}% " +
|
|
1016
|
-
'({{ template "formatLongTimeRemaining" (minutesUntilReset .weekly.resetsAt) }})',
|
|
1017
|
-
bg: blockLikeBg(".weekly.percentage", ".weekly.budget.warningThreshold"),
|
|
1018
|
-
fg: blockLikeFg(".weekly.percentage"),
|
|
1019
|
-
when: "{{ gt .weekly.resetsAt 0 }}",
|
|
1020
|
-
},
|
|
1021
|
-
// Burn rate + cap projection: "$X/hr · Nm to 5h · Nd to wk". The headline
|
|
1022
|
-
// number of a usage monitor — how fast you are spending and when you hit
|
|
1023
|
-
// the wall. All math is daemon-side (render-payload.ts); the template only
|
|
1024
|
-
// formats. Heats as the 5h cap nears (etaHeat*). Shown when either
|
|
1025
|
-
// rate-limit window is active — the same signal block/weekly gate on.
|
|
1026
|
-
burnrate: {
|
|
1027
|
-
template:
|
|
1028
|
-
'⚡ {{ template "formatRate" .burn.costPerHour }} · ' +
|
|
1029
|
-
'{{ template "formatEta" .block.etaMinutes }} to 5h · ' +
|
|
1030
|
-
'{{ template "formatEta" .weekly.etaMinutes }} to wk',
|
|
1031
|
-
bg: etaHeatBg(
|
|
1032
|
-
".block.etaMinutes",
|
|
1033
|
-
".burn.eta.warnMinutes",
|
|
1034
|
-
".burn.eta.errorMinutes",
|
|
1035
|
-
),
|
|
1036
|
-
fg: etaHeatFg(".block.etaMinutes", ".burn.eta.warnMinutes"),
|
|
1037
|
-
when: "{{ or (gt .block.resetsAt 0) (gt .weekly.resetsAt 0) }}",
|
|
1038
|
-
},
|
|
1039
|
-
// Token throughput for the active turn — output / input / total tok/s, each a
|
|
1040
|
-
// successive-render delta computed daemon-side (render-payload.ts); the
|
|
1041
|
-
// template only formats. Declared-but-opt-in (NOT in the default root, like
|
|
1042
|
-
// block/weekly/burnrate): a user adds `speed` to their layout. Each lane reads
|
|
1043
|
-
// "—" when idle/between turns ([LAW:no-silent-failure] — never a stale or
|
|
1044
|
-
// divide-by-zero number). Visible once the session has done any work (stable,
|
|
1045
|
-
// no layout flicker); `output` is the live generation rate, `input` spikes at
|
|
1046
|
-
// turn start, `total` is their sum.
|
|
1047
|
-
speed: {
|
|
1048
|
-
template:
|
|
1049
|
-
'⇅ out {{ template "formatSpeed" .speed.output }} · ' +
|
|
1050
|
-
'in {{ template "formatSpeed" .speed.input }} · ' +
|
|
1051
|
-
'tot {{ template "formatSpeed" .speed.total }}',
|
|
1052
|
-
bg: "panel",
|
|
1053
|
-
fg: "foreground",
|
|
1054
|
-
when: "{{ gt .session.tokens 0 }}",
|
|
1055
|
-
},
|
|
1056
|
-
// Burn-rate sparkline: the recent total-lane tok/s trend as a unicode
|
|
1057
|
-
// mini-graph. Declared-but-opt-in (NOT in the default root, like speed /
|
|
1058
|
-
// block / weekly): a user adds `tokenSparkline` to their layout. The
|
|
1059
|
-
// `sparkline` helper decodes the daemon-owned series and draws it; `24`
|
|
1060
|
-
// caps the glyph count to the cell, showing the live tail of the ring. The
|
|
1061
|
-
// segment's fg colors the whole graph (no per-glyph color). Gated on the
|
|
1062
|
-
// history being present so the cell never renders empty (the series needs
|
|
1063
|
-
// two samples before its first bar). [LAW:effects-at-boundaries] — all the
|
|
1064
|
-
// history lives in the daemon ring, the template only draws.
|
|
1065
|
-
tokenSparkline: {
|
|
1066
|
-
template: "⚡ {{ sparkline .speed.history 24 }}",
|
|
1067
|
-
bg: "panel",
|
|
1068
|
-
fg: "foreground",
|
|
1069
|
-
when: '{{ ne .speed.history "" }}',
|
|
1070
|
-
},
|
|
1071
|
-
// Prompt-cache warmth countdown. minutesUntilReset clamps a past expiry
|
|
1072
|
-
// to 0, so an expired cache renders "cold" (and reads red via the ≤8
|
|
1073
|
-
// arm) rather than a negative number. [LAW:dataflow-not-control-flow]
|
|
1074
|
-
// glyph + "cold"/"Nm" + color all derive from the one expiry value; the
|
|
1075
|
-
// provider supplies no display state. Constant `surface` bg with a
|
|
1076
|
-
// fg-only threshold cascade mirrors the legacy inline-colored text
|
|
1077
|
-
// (warm = normal, ≤20m = warning, ≤8m/cold = error).
|
|
1078
|
-
cacheTimer: {
|
|
1079
|
-
template:
|
|
1080
|
-
"◴ {{ if le (minutesUntilReset .cache.expiresAt) 0 }}cold" +
|
|
1081
|
-
"{{ else }}{{ minutesUntilReset .cache.expiresAt }}m{{ end }}",
|
|
1082
|
-
bg: "surface",
|
|
1083
|
-
fg:
|
|
1084
|
-
"{{ if le (minutesUntilReset .cache.expiresAt) 8 }}error" +
|
|
1085
|
-
"{{ else }}{{ if le (minutesUntilReset .cache.expiresAt) 20 }}warning" +
|
|
1086
|
-
"{{ else }}foreground{{ end }}{{ end }}",
|
|
1087
|
-
when: "{{ gt .cache.expiresAt 0 }}",
|
|
1088
|
-
},
|
|
1089
|
-
context: {
|
|
1090
|
-
template:
|
|
1091
|
-
"◔ {{ formatInteger .context.totalTokens }} ({{ .context.contextLeft }}%)",
|
|
1092
|
-
bg:
|
|
1093
|
-
"{{ if le .context.contextLeft 20 }}error" +
|
|
1094
|
-
"{{ else }}{{ if le .context.contextLeft 40 }}warning" +
|
|
1095
|
-
"{{ else }}surface-active{{ end }}{{ end }}",
|
|
1096
|
-
fg:
|
|
1097
|
-
"{{ if le .context.contextLeft 40 }}button-color-foreground" +
|
|
1098
|
-
"{{ else }}foreground{{ end }}",
|
|
1099
|
-
when: "{{ gt .context.totalTokens 0 }}",
|
|
1100
|
-
},
|
|
1101
|
-
metrics: {
|
|
1102
|
-
// [LAW:dataflow-not-control-flow] Each part guards on its own value
|
|
1103
|
-
// rather than gating the whole segment on a single dimension. With
|
|
1104
|
-
// MetricsPayload's fields independently optional and pickNonNull
|
|
1105
|
-
// dropping nulls (see src/daemon/render-payload.ts), an absent field
|
|
1106
|
-
// resolves through the var-system fallback chain to 0 — the same
|
|
1107
|
-
// falsy shape the per-part `if` test treats as hidden. The segment-
|
|
1108
|
-
// level `when` survives as a weak any-present check so a payload
|
|
1109
|
-
// with zero metrics data renders no cell at all (an empty template
|
|
1110
|
-
// would otherwise produce a single-space bg-styled cell).
|
|
1111
|
-
//
|
|
1112
|
-
// [LAW:one-source-of-truth] exception: each arm's leading space is the
|
|
1113
|
-
// SEPARATOR between present parts (only data can decide which part is
|
|
1114
|
-
// first, so no static strip can remove just the first one), and the
|
|
1115
|
-
// trailing space mirrors it for symmetry. At the default padding this
|
|
1116
|
-
// cell therefore reads one space wider per side than its siblings.
|
|
1117
|
-
template:
|
|
1118
|
-
'{{ if .metrics.lastResponseTime }} Δ {{ template "formatResponseTime" .metrics.lastResponseTime }}{{ end }}' +
|
|
1119
|
-
'{{ if .metrics.responseTime }} ⧖ {{ template "formatResponseTime" .metrics.responseTime }}{{ end }}' +
|
|
1120
|
-
'{{ if .metrics.sessionDuration }} ⧗ {{ template "formatDuration" .metrics.sessionDuration }}{{ end }}' +
|
|
1121
|
-
"{{ if .metrics.messageCount }} ◆ {{ .metrics.messageCount }}{{ end }}" +
|
|
1122
|
-
"{{ if .metrics.linesAdded }} + {{ .metrics.linesAdded }}{{ end }}" +
|
|
1123
|
-
"{{ if .metrics.linesRemoved }} - {{ .metrics.linesRemoved }}{{ end }} ",
|
|
1124
|
-
bg: "panel",
|
|
1125
|
-
fg: "foreground",
|
|
1126
|
-
when:
|
|
1127
|
-
"{{ or .metrics.lastResponseTime .metrics.responseTime" +
|
|
1128
|
-
" .metrics.sessionDuration .metrics.messageCount" +
|
|
1129
|
-
" .metrics.linesAdded .metrics.linesRemoved }}",
|
|
1130
|
-
},
|
|
1131
|
-
// ── The TWO globals steppers left in this drawer. `charset` and
|
|
1132
|
-
// `colorCompatibility` have no SessionState half at all — they describe
|
|
1133
|
-
// the terminal, not a taste that varies per session — so `persist` is
|
|
1134
|
-
// genuinely their only seam and there is no destination for a `persist?`
|
|
1135
|
-
// selector to choose between. Everything with both halves
|
|
1136
|
-
// (theme/style/look/preset/autoWrap/padding) moved to the synthesized
|
|
1137
|
-
// settings menu as ONE dual control each (candybar-settings-ui-aok.3).
|
|
1138
|
-
// Each pairs a `persist` control with a `↺` reset (docs' persist/reset
|
|
1139
|
-
// convention); labels read `.field.effective` (the daemon-resolved value
|
|
1140
|
-
// BuildLineOptions actually rendered with), never a restated literal.
|
|
1141
|
-
charsetControl: {
|
|
1142
|
-
template:
|
|
1143
|
-
"{{ .charset.effective }} " +
|
|
1144
|
-
`{{ menu "applyCharsetForever" "${DISCLOSURE_GLYPH_CLOSED}" "${DISCLOSURE_GLYPH_OPEN}" }} ` +
|
|
1145
|
-
'{{ action "resetCharset" "↺" }}',
|
|
1146
|
-
bg: "surface",
|
|
1147
|
-
fg: "foreground",
|
|
1148
|
-
},
|
|
1149
|
-
colorCompatControl: {
|
|
1150
|
-
template:
|
|
1151
|
-
"{{ .colorCompatibility.effective }} " +
|
|
1152
|
-
`{{ menu "applyColorCompatForever" "${DISCLOSURE_GLYPH_CLOSED}" "${DISCLOSURE_GLYPH_OPEN}" }} ` +
|
|
1153
|
-
'{{ action "resetColorCompat" "↺" }}',
|
|
1154
|
-
bg: "surface",
|
|
1155
|
-
fg: "foreground",
|
|
1156
|
-
},
|
|
1157
|
-
// [LAW:verifiable-goals] candybar-config-engine-71o.6's own acceptance
|
|
1158
|
-
// bar, mirrored from .3/.5: at least ONE segment-scoped field must be
|
|
1159
|
-
// menu-able from the BUNDLED default with no hand-authored actions.
|
|
1160
|
-
// `directory` is the demo target — always visible, palette-driven
|
|
1161
|
-
// bg/fg, so an override is immediately legible. The persist/reset pair
|
|
1162
|
-
// below targets `segments.directory.palette` (not a Globals field),
|
|
1163
|
-
// proving the option-domain-as-data seam generalizes to segment-scoped
|
|
1164
|
-
// keys with zero engine edits beyond opening the key namespace itself
|
|
1165
|
-
// (loader/persist-target.ts) — the SAME `from: "themes"` domain
|
|
1166
|
-
// applyThemeForever already uses.
|
|
1167
|
-
// [LAW:one-source-of-truth] exception: unlike charsetControl/
|
|
1168
|
-
// the other controls' `.field.effective` labels, there is no
|
|
1169
|
-
// `segments.directory.palette.effective` payload projection — adding one
|
|
1170
|
-
// would require threading the full DslConfig through
|
|
1171
|
-
// buildRenderPayload's signature (today built from EffectiveGlobals
|
|
1172
|
-
// alone), a change with no other motivation than this one label. The
|
|
1173
|
-
// control still writes/persists/resets correctly without it: per
|
|
1174
|
-
// render/action.ts's CONFIG_KEY_TO_EFFECTIVE_VAR, a persist key with no
|
|
1175
|
-
// effective-var entry writes fine and only loses the picker's "current
|
|
1176
|
-
// selection" highlight — a documented, already-accepted degrade path,
|
|
1177
|
-
// not a bug.
|
|
1178
|
-
directoryPaletteControl: {
|
|
1179
|
-
template:
|
|
1180
|
-
"🎨 directory " +
|
|
1181
|
-
`{{ menu "applyDirectoryPaletteForever" "${DISCLOSURE_GLYPH_CLOSED}" "${DISCLOSURE_GLYPH_OPEN}" }} ` +
|
|
1182
|
-
'{{ action "resetDirectoryPalette" "↺" }}',
|
|
1183
|
-
bg: "surface",
|
|
1184
|
-
fg: "foreground",
|
|
1185
|
-
},
|
|
1186
|
-
},
|
|
1187
|
-
|
|
1188
|
-
// Default layout — the canonical LayoutNode tree (`satisfies DslConfig`
|
|
1189
|
-
// requires the lowered form here; the terse Option-A `{ h/v/seg }` grammar is
|
|
1190
|
-
// the loader's authoring surface for user JSON, not this typed literal — the
|
|
1191
|
-
// one exception being `settingsDrawer` above, whose `kind: "group"` sugar has
|
|
1192
|
-
// no canonical-form equivalent it could be hand-lowered to; see its own
|
|
1193
|
-
// comment).
|
|
1194
|
-
//
|
|
1195
|
-
// Two always-visible rows stacked by the vertical container: an IDENTITY +
|
|
1196
|
-
// ACTIONS row (where am I / what can I do here — the directory, the verbose
|
|
1197
|
-
// `gitaculous` line, the quick-action tray: copy session id, open project /
|
|
1198
|
-
// transcript in the editor, and the settingsDrawer toggle) over a STATUS row
|
|
1199
|
-
// (what's happening now — model, context-window fill, prompt-cache warmth,
|
|
1200
|
-
// and the 5h / 7d rate-limit quotas). The settingsDrawer (candybar-config-
|
|
1201
|
-
// engine-71o.4) sits on the identity row beside the tray — collapsed by
|
|
1202
|
-
// default and visually silent (a single "⚙ settings ▸" cell) — and reveals a
|
|
1203
|
-
// third row of every bar-mutable display default (theme, style, look,
|
|
1204
|
-
// charset, colorCompatibility, autoWrap, padding) on the line immediately
|
|
1205
|
-
// below row 1 when opened, exactly where a `{{ menu }}`'s own picker body
|
|
1206
|
-
// would drop. Each row zips its segments through the powerline joiner; `\n`
|
|
1207
|
-
// separates the rows.
|
|
1208
|
-
//
|
|
1209
|
-
// [LAW:dataflow-not-control-flow] Every status segment is when-gated on its
|
|
1210
|
-
// own signal (no repo → the identity row is just the directory + tray; no
|
|
1211
|
-
// active rate-limit window → block/weekly drop; no cache activity → cacheTimer
|
|
1212
|
-
// drops). A row therefore only ever shows the segments that have real data —
|
|
1213
|
-
// the layout is chosen by the data, not by branches — so the default never
|
|
1214
|
-
// paints an empty or placeholder cell. The directory and the tray have no
|
|
1215
|
-
// `when`, so row 1 always anchors the bar.
|
|
1216
|
-
root: {
|
|
1217
|
-
kind: "container",
|
|
1218
|
-
direction: "vertical",
|
|
1219
|
-
children: [
|
|
1220
|
-
{
|
|
1221
|
-
kind: "container",
|
|
1222
|
-
direction: "horizontal",
|
|
1223
|
-
children: [
|
|
1224
|
-
// Leads the identity row: the first thing to read is WHICH MACHINE,
|
|
1225
|
-
// because it reframes every path and branch to its right. Same
|
|
1226
|
-
// placement git-taculous gives `(%n@%m)` — ahead of the directory.
|
|
1227
|
-
// Gated off entirely on a local session, so the row still opens with
|
|
1228
|
-
// `directory` where it always has.
|
|
1229
|
-
{ kind: "segment", name: "host" },
|
|
1230
|
-
{ kind: "segment", name: "directory" },
|
|
1231
|
-
{ kind: "segment", name: "gitaculous" },
|
|
1232
|
-
{ kind: "segment", name: "toolbar" },
|
|
1233
|
-
settingsDrawer,
|
|
1234
|
-
],
|
|
1235
|
-
},
|
|
1236
|
-
{
|
|
1237
|
-
kind: "container",
|
|
1238
|
-
direction: "horizontal",
|
|
1239
|
-
children: [
|
|
1240
|
-
{ kind: "segment", name: "model" },
|
|
1241
|
-
{ kind: "segment", name: "context" },
|
|
1242
|
-
{ kind: "segment", name: "cacheTimer" },
|
|
1243
|
-
{ kind: "segment", name: "block" },
|
|
1244
|
-
{ kind: "segment", name: "weekly" },
|
|
1245
|
-
],
|
|
1246
|
-
},
|
|
1247
|
-
],
|
|
1248
|
-
},
|
|
1249
|
-
|
|
1250
|
-
// [LAW:locality-or-seam] The quick-action tray's behaviors, decoupled by NAME
|
|
1251
|
-
// from the `toolbar` segment's glyphs above. copy/open evaluate a Go-template
|
|
1252
|
-
// against the live render scope at click time and write NO SessionState, so
|
|
1253
|
-
// they derive no state validator (no gate) — they are pure click effects.
|
|
1254
|
-
//
|
|
1255
|
-
// [LAW:single-enforcer] Each template emits a RAW value; the click-wire codec
|
|
1256
|
-
// (effectsUrl → encodeSegments) owns ALL percent-encoding and the verb's
|
|
1257
|
-
// `oneArg` owns the single matching decode — so the template never hand-rolls
|
|
1258
|
-
// a `urlEncode`, and the path round-trips untouched through one codec.
|
|
1259
|
-
//
|
|
1260
|
-
// open* route through the open-vscode verb (`open -a "Visual Studio Code"
|
|
1261
|
-
// <path>`), so they pass a bare filesystem path — a directory or a file the
|
|
1262
|
-
// editor opens directly — NOT a `vscode://` URL (which `open -a` would treat
|
|
1263
|
-
// as a literal filename, not a deep link).
|
|
1264
|
-
actions: {
|
|
1265
|
-
copySession: { copy: "{{ .session.id }}" },
|
|
1266
|
-
copyDir: { copy: "{{ .current_dir }}" },
|
|
1267
|
-
openProject: { open: "{{ .project_dir }}" },
|
|
1268
|
-
openTranscript: { open: "{{ .transcript_path }}" },
|
|
1269
|
-
|
|
1270
|
-
// [LAW:locality-or-seam] The settings-drawer controls' behaviors
|
|
1271
|
-
// (candybar-config-engine-71o.4), decoupled by NAME from
|
|
1272
|
-
// charsetControl/colorCompatControl below. These are durable-only by
|
|
1273
|
-
// NATURE, not by omission: `charset` and `colorCompatibility` describe the
|
|
1274
|
-
// terminal (glyph coverage, colour depth) rather than a taste that varies
|
|
1275
|
-
// between sessions, so they have no SessionState half for a `persist?`
|
|
1276
|
-
// selector to choose between — which is exactly why
|
|
1277
|
-
// candybar-settings-ui-aok.3 left them here while moving every
|
|
1278
|
-
// both-halves setting into the settings menu as one dual control. Each
|
|
1279
|
-
// writes the config DEFAULT through the daemon-owned overrides layer
|
|
1280
|
-
// (never the hand-authored file itself), gated by the SAME
|
|
1281
|
-
// deriveConfigActionValidators pass, and is paired with a `reset` so a
|
|
1282
|
-
// drawer choice is always undoable from the bar.
|
|
1283
|
-
applyCharsetForever: { persist: "charset", from: "charsets" },
|
|
1284
|
-
resetCharset: { reset: "charset" },
|
|
1285
|
-
applyColorCompatForever: {
|
|
1286
|
-
persist: "colorCompatibility",
|
|
1287
|
-
from: "colorCompatibilities",
|
|
1288
|
-
},
|
|
1289
|
-
resetColorCompat: { reset: "colorCompatibility" },
|
|
1290
|
-
|
|
1291
|
-
// [LAW:locality-or-seam] The segment-palette control's behavior
|
|
1292
|
-
// (candybar-config-engine-71o.6), decoupled by NAME from
|
|
1293
|
-
// directoryPaletteControl below. The target key is `segments.directory.
|
|
1294
|
-
// palette` — NOT a Globals field — so it rides the SAME generic
|
|
1295
|
-
// `from`/`reset` machinery every other persist pair here uses, over a
|
|
1296
|
-
// key namespace loader/persist-target.ts opened alongside the pre-
|
|
1297
|
-
// existing Globals-field one. Like charset/colorCompatibility, this field has
|
|
1298
|
-
// no SessionState half at all: a per-segment `palette:` is a static pin
|
|
1299
|
-
// that ignores the session theme by design (src/dsl/render.ts), so
|
|
1300
|
-
// `persist` is its only seam.
|
|
1301
|
-
applyDirectoryPaletteForever: {
|
|
1302
|
-
persist: "segments.directory.palette",
|
|
1303
|
-
from: "themes",
|
|
1304
|
-
},
|
|
1305
|
-
resetDirectoryPalette: { reset: "segments.directory.palette" },
|
|
1306
|
-
},
|
|
1307
|
-
|
|
1308
|
-
// ─── Looks ───────────────────────────────────────────────────────────────
|
|
1309
|
-
// Named theme ADAPTATIONS — each is a full rich-js ThemeKey applied on top
|
|
1310
|
-
// of whatever base theme is active (a transform, not a palette), so every
|
|
1311
|
-
// look composes with every theme: pick theme, then pick look. Selected per
|
|
1312
|
-
// session via the `look` SessionState key (an action `{ set: "look", from:
|
|
1313
|
-
// "looks" }` + a `{{ menu }}`), exactly the theme/style selection seam.
|
|
1314
|
-
// [LAW:one-source-of-truth] Merges by name (user wins per name), so this
|
|
1315
|
-
// stdlib — including the "none" identity floor effectiveLookName collapses
|
|
1316
|
-
// to — is present in every merged config by construction.
|
|
1317
|
-
looks: {
|
|
1318
|
-
// [LAW:dataflow-not-control-flow] "none" is just the identity look — the
|
|
1319
|
-
// resolution floor as a value, not a special case (rich-js's isIdentityKey
|
|
1320
|
-
// fast-path makes it free). Spelled literally (not rich-js IDENTITY /
|
|
1321
|
-
// INVERT_LIGHTNESS) so the bundled default remains inert JSON-shaped data
|
|
1322
|
-
// a user file can mirror axis-for-axis; the loader normalizes user specs
|
|
1323
|
-
// onto the same identity axes.
|
|
1324
|
-
none: { hueShift: 0, chromaScale: 1, lightnessScale: 1, lightnessShift: 0 },
|
|
1325
|
-
// Saturation up/down — chroma is multiplicative, hue and lightness held.
|
|
1326
|
-
vivid: {
|
|
1327
|
-
hueShift: 0,
|
|
1328
|
-
chromaScale: 1.35,
|
|
1329
|
-
lightnessScale: 1,
|
|
1330
|
-
lightnessShift: 0,
|
|
1331
|
-
},
|
|
1332
|
-
muted: {
|
|
1333
|
-
hueShift: 0,
|
|
1334
|
-
chromaScale: 0.55,
|
|
1335
|
-
lightnessScale: 1,
|
|
1336
|
-
lightnessShift: 0,
|
|
1337
|
-
},
|
|
1338
|
-
// Lightness down (scale) / up (shift) — dim compresses toward black,
|
|
1339
|
-
// bright lifts everything a step; anchors stay hue-locked by rich-js.
|
|
1340
|
-
dim: {
|
|
1341
|
-
hueShift: 0,
|
|
1342
|
-
chromaScale: 1,
|
|
1343
|
-
lightnessScale: 0.85,
|
|
1344
|
-
lightnessShift: 0,
|
|
1345
|
-
},
|
|
1346
|
-
bright: {
|
|
1347
|
-
hueShift: 0,
|
|
1348
|
-
chromaScale: 1,
|
|
1349
|
-
lightnessScale: 1,
|
|
1350
|
-
lightnessShift: 0.08,
|
|
1351
|
-
},
|
|
1352
|
-
// The dark↔light "octave" flip (rich-js INVERT_LIGHTNESS: L' = 1 - L) —
|
|
1353
|
-
// errors stay red, dark-on-light becomes light-on-dark.
|
|
1354
|
-
inverted: {
|
|
1355
|
-
hueShift: 0,
|
|
1356
|
-
chromaScale: 1,
|
|
1357
|
-
lightnessScale: -1,
|
|
1358
|
-
lightnessShift: 1,
|
|
1359
|
-
},
|
|
1360
|
-
},
|
|
1361
|
-
|
|
1362
|
-
// ─── Presets ─────────────────────────────────────────────────────────────
|
|
1363
|
-
// Named config FRAGMENTS — each an alternative `root` + display `globals`,
|
|
1364
|
-
// i.e. a whole arrangement of the bar rather than one knob. A preset is to
|
|
1365
|
-
// configuration what a look is to a theme, and rides the identical seam:
|
|
1366
|
-
// selected per session via the `preset` SessionState key — or pinned as the
|
|
1367
|
-
// durable default via `globals.preset` — through the settings menu's ONE
|
|
1368
|
-
// dual preset control (src/config/settings-menu.ts), resolved as session
|
|
1369
|
-
// pick over globals.preset over this floor.
|
|
1370
|
-
// [LAW:one-source-of-truth] Merges by name (user wins per name), so this
|
|
1371
|
-
// stdlib is present in every merged config by construction, exactly as
|
|
1372
|
-
// looks' "none"/"vivid"/"muted"/… is — a user redefining "compact" or
|
|
1373
|
-
// "verbose" wins per name; the floor cannot be shadowed by anything but an
|
|
1374
|
-
// empty fragment, because that IS what it already is.
|
|
1375
|
-
//
|
|
1376
|
-
// [LAW:carrying-cost] Each library preset stages only real deltas from the
|
|
1377
|
-
// bundled default's own root (declared above) — no preset here restates a
|
|
1378
|
-
// segment's template, bg, or fg, only which of the ALREADY-declared
|
|
1379
|
-
// segments appear and in what arrangement, per the field cap
|
|
1380
|
-
// (brandon-presets-0yk.1's premise note): a bundled preset can only STAGE
|
|
1381
|
-
// segments DEFAULT_DSL_CONFIG.segments already declares, never introduce
|
|
1382
|
-
// one.
|
|
1383
|
-
//
|
|
1384
|
-
// [LAW:verifiable-goals] Every entry here is asserted error-cell-free at
|
|
1385
|
-
// 80/120/200 columns against a RICH payload in
|
|
1386
|
-
// test/default-dsl-config.test.ts (the same checkPayload fixture check.ts
|
|
1387
|
-
// uses) — curated by actually rendering each arrangement, not by reading
|
|
1388
|
-
// the segment names off the page.
|
|
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. This is the bundled default's
|
|
1394
|
-
// OWN two-row (identity / status) arrangement declared as `root` above —
|
|
1395
|
-
// it needs no entry of its own here beyond the floor, because staging it
|
|
1396
|
-
// AS a named fragment would be a byte-for-byte copy of `root` that could
|
|
1397
|
-
// silently drift from it the moment either changed.
|
|
1398
|
-
default: {},
|
|
1399
|
-
|
|
1400
|
-
// Single-row arrangement for narrow terminals and split panes — the
|
|
1401
|
-
// situation where a user most wants a different arrangement and least
|
|
1402
|
-
// wants to hand-write one. Keeps only the three facts a split pane most
|
|
1403
|
-
// needs at a glance (where am I / what's the git state / how much context
|
|
1404
|
-
// is left), the quiet one-line `git` segment rather than the multi-fact
|
|
1405
|
-
// `gitaculous`, and `padding: 0` to buy back the chrome a narrow column
|
|
1406
|
-
// can't spare.
|
|
1407
|
-
//
|
|
1408
|
-
// [LAW:no-silent-failure] A session that switches TO compact is never
|
|
1409
|
-
// stranded: synthesizeSettingsMenu splices the global settings menu — and
|
|
1410
|
-
// with it the preset switcher — into EVERY preset root, so one click back
|
|
1411
|
-
// to "default" restores everything compact traded away for width. That is
|
|
1412
|
-
// why this root carries no preset control of its own: one guaranteed door
|
|
1413
|
-
// per root, minted once and referenced, not a segment each preset must
|
|
1414
|
-
// remember to carry [LAW:one-source-of-truth].
|
|
1415
|
-
compact: {
|
|
1416
|
-
root: {
|
|
1417
|
-
kind: "container",
|
|
1418
|
-
direction: "horizontal",
|
|
1419
|
-
children: [
|
|
1420
|
-
{ kind: "segment", name: "directory" },
|
|
1421
|
-
{ kind: "segment", name: "git" },
|
|
1422
|
-
{ kind: "segment", name: "context" },
|
|
1423
|
-
],
|
|
1424
|
-
},
|
|
1425
|
-
globals: { padding: 0 },
|
|
1426
|
-
},
|
|
1427
|
-
|
|
1428
|
-
// Verbose arrangement surfacing every segment that is declared but NOT in
|
|
1429
|
-
// the default root (gitPr, burnrate, speed, tokenSparkline — see each
|
|
1430
|
-
// segment's own "declared-but-opt-in" comment above) alongside the
|
|
1431
|
-
// default's own two rows, for a user who wants the full usage-monitor
|
|
1432
|
-
// picture rather than the quiet default. A third row carries the two
|
|
1433
|
-
// per-turn throughput segments, which read "—" between turns
|
|
1434
|
-
// ([LAW:no-silent-failure] on speed/tokenSparkline) rather than an empty
|
|
1435
|
-
// or stale row. Like `compact`, it carries no preset control of its own —
|
|
1436
|
-
// the global settings menu is spliced into every preset root and is the
|
|
1437
|
-
// one door back.
|
|
1438
|
-
verbose: {
|
|
1439
|
-
root: {
|
|
1440
|
-
kind: "container",
|
|
1441
|
-
direction: "vertical",
|
|
1442
|
-
children: [
|
|
1443
|
-
{
|
|
1444
|
-
kind: "container",
|
|
1445
|
-
direction: "horizontal",
|
|
1446
|
-
children: [
|
|
1447
|
-
{ kind: "segment", name: "directory" },
|
|
1448
|
-
{ kind: "segment", name: "gitaculous" },
|
|
1449
|
-
{ kind: "segment", name: "gitPr" },
|
|
1450
|
-
{ kind: "segment", name: "toolbar" },
|
|
1451
|
-
],
|
|
1452
|
-
},
|
|
1453
|
-
{
|
|
1454
|
-
kind: "container",
|
|
1455
|
-
direction: "horizontal",
|
|
1456
|
-
children: [
|
|
1457
|
-
{ kind: "segment", name: "model" },
|
|
1458
|
-
{ kind: "segment", name: "context" },
|
|
1459
|
-
{ kind: "segment", name: "cacheTimer" },
|
|
1460
|
-
{ kind: "segment", name: "block" },
|
|
1461
|
-
{ kind: "segment", name: "weekly" },
|
|
1462
|
-
{ kind: "segment", name: "burnrate" },
|
|
1463
|
-
],
|
|
1464
|
-
},
|
|
1465
|
-
{
|
|
1466
|
-
kind: "container",
|
|
1467
|
-
direction: "horizontal",
|
|
1468
|
-
children: [
|
|
1469
|
-
{ kind: "segment", name: "speed" },
|
|
1470
|
-
{ kind: "segment", name: "tokenSparkline" },
|
|
1471
|
-
],
|
|
1472
|
-
},
|
|
1473
|
-
],
|
|
1474
|
-
},
|
|
1475
|
-
},
|
|
1476
|
-
},
|
|
1477
|
-
|
|
1478
|
-
// [LAW:one-source-of-truth] What edit mode LOOKS like, as config a user can
|
|
1479
|
-
// retune — the whole point of candybar-settings-ui-aok.5, whose predecessor
|
|
1480
|
-
// was renderer constants. Powerline chrome exists to make adjacent segments
|
|
1481
|
-
// read as one continuous strip, which is precisely the wrong signal while a
|
|
1482
|
-
// user is trying to see where one segment ends and the next begins; `plain`
|
|
1483
|
-
// trades the caps for a visible separator between every cell.
|
|
1484
|
-
//
|
|
1485
|
-
// The separator is stated rather than left to PlainJoiner's own default: this
|
|
1486
|
-
// fragment layers over the user's globals, so a config that set
|
|
1487
|
-
// `default_separator` for its own powerline bar would otherwise carry that
|
|
1488
|
-
// choice into edit mode, where the separator is the entire affordance. " | "
|
|
1489
|
-
// (not "│") because it must survive `charset: "ascii"` — the fragment does
|
|
1490
|
-
// not, and should not, know the terminal's glyph coverage.
|
|
1491
|
-
editGlobals: {
|
|
1492
|
-
style: "plain",
|
|
1493
|
-
default_separator: " | ",
|
|
1494
|
-
},
|
|
1495
|
-
|
|
1496
|
-
// [LAW:single-enforcer] / [LAW:one-source-of-truth] Display-formatting policy
|
|
1497
|
-
// for the cost/token/budget family lives here as named template helpers, each
|
|
1498
|
-
// DEFINED ONCE and called from every segment via `{{ template "name" .arg }}`
|
|
1499
|
-
// — so how a cost/token string looks is data a user overrides by name, not
|
|
1500
|
-
// compiled JS. The K/M token-scale rule has a SINGLE home (`formatTokenCount`);
|
|
1501
|
-
// `formatTokens` suffixes " tokens" onto it and `formatTokenBreakdown` calls it
|
|
1502
|
-
// per part, so the scale policy can never drift between the three.
|
|
1503
|
-
// [LAW:dataflow-not-control-flow] A multi-input helper (budgetStatus,
|
|
1504
|
-
// formatTokenBreakdown) receives its inputs as one `dict` value through its
|
|
1505
|
-
// single dot arg — variability flows as data across one boundary, not as a
|
|
1506
|
-
// bespoke multi-arg signature.
|
|
1507
|
-
helpers: {
|
|
1508
|
-
// Cost: under a cent reads "<$0.01"; otherwise "$" + two decimals. (Null is
|
|
1509
|
-
// unrepresentable through the var-system — type:number with a numeric default
|
|
1510
|
-
// owns "missing" upstream — so no null branch is needed here.)
|
|
1511
|
-
formatCost:
|
|
1512
|
-
'{{ if lt . 0.01 }}<$0.01{{ else }}${{ printf "%.2f" . }}{{ end }}',
|
|
1513
|
-
// The single home of the K/M token-scale rule. >=1e6 → "X.YM", >=1e3 → "X.YK",
|
|
1514
|
-
// else the integer verbatim (0 and negatives fall through to this arm, exactly
|
|
1515
|
-
// as the retired JS did). No " tokens" suffix — that is formatTokens' job.
|
|
1516
|
-
formatTokenCount:
|
|
1517
|
-
'{{ if ge . 1000000 }}{{ printf "%.1f" (divf . 1000000) }}M' +
|
|
1518
|
-
'{{ else if ge . 1000 }}{{ printf "%.1f" (divf . 1000) }}K' +
|
|
1519
|
-
"{{ else }}{{ . }}{{ end }}",
|
|
1520
|
-
formatTokens: '{{ template "formatTokenCount" . }} tokens',
|
|
1521
|
-
// Burn rate: "$X.XX/hr" when projectable, "—/hr" otherwise. The daemon
|
|
1522
|
-
// emits -1 (a structurally-impossible rate) for not-projectable, so the
|
|
1523
|
-
// branch reads a VALUE, never a hidden control-flow flag. Reuses formatCost
|
|
1524
|
-
// so the dollar policy has one home.
|
|
1525
|
-
formatRate:
|
|
1526
|
-
'{{ if lt . 0 }}—/hr{{ else }}{{ template "formatCost" . }}/hr{{ end }}',
|
|
1527
|
-
// ETA to a rate-limit cap: humanized minutes when projectable, "—" when the
|
|
1528
|
-
// daemon could not project (-1 sentinel). Reuses the long-remaining cascade.
|
|
1529
|
-
formatEta:
|
|
1530
|
-
'{{ if lt . 0 }}—{{ else }}{{ template "formatLongTimeRemaining" . }}{{ end }}',
|
|
1531
|
-
// Token throughput: "N/s" (K/M-scaled, rounded) when measured (>= 0), "—"
|
|
1532
|
-
// when the daemon had no projectable sample (-1). Branches on the VALUE, like
|
|
1533
|
-
// formatRate; reuses formatTokenCount so the K/M scale policy has one home.
|
|
1534
|
-
formatSpeed:
|
|
1535
|
-
'{{ if lt . 0 }}—{{ else }}{{ template "formatTokenCount" (round .) }}/s{{ end }}',
|
|
1536
|
-
// Breakdown over a dict {input, output, cacheCreation, cacheRead}; each present
|
|
1537
|
-
// part is formatted by the shared formatTokenCount and joined with " + ". A
|
|
1538
|
-
// `$first` flag (reassigned across if-frames) inserts the separator before all
|
|
1539
|
-
// but the first present part; all-zero collapses to "0 tokens".
|
|
1540
|
-
formatTokenBreakdown:
|
|
1541
|
-
"{{ $first := true }}" +
|
|
1542
|
-
'{{ if gt .input 0 }}{{ template "formatTokenCount" .input }} in{{ $first = false }}{{ end }}' +
|
|
1543
|
-
'{{ if gt .output 0 }}{{ if not $first }} + {{ end }}{{ template "formatTokenCount" .output }} out{{ $first = false }}{{ end }}' +
|
|
1544
|
-
'{{ if or (gt .cacheCreation 0) (gt .cacheRead 0) }}{{ if not $first }} + {{ end }}{{ template "formatTokenCount" (add .cacheCreation .cacheRead) }} cached{{ $first = false }}{{ end }}' +
|
|
1545
|
-
"{{ if $first }}0 tokens{{ end }}",
|
|
1546
|
-
// Budget suffix over a dict {cost, budget, warn}. Non-displayable (budget<=0 or
|
|
1547
|
-
// cost<0) → "". Otherwise pct = min(100, cost/budget*100), rendered " !N%" at/above
|
|
1548
|
-
// warn, " +N%" at/above 50, " N%" below.
|
|
1549
|
-
budgetStatus:
|
|
1550
|
-
"{{ if or (le .budget 0) (lt .cost 0) }}{{ else }}" +
|
|
1551
|
-
"{{ $pct := minf 100 (mulf (divf .cost .budget) 100) }}" +
|
|
1552
|
-
'{{ $p := printf "%.0f%%" $pct }}' +
|
|
1553
|
-
"{{ if ge $pct .warn }} !{{ $p }}" +
|
|
1554
|
-
"{{ else }}{{ if ge $pct 50 }} +{{ $p }}{{ else }} {{ $p }}{{ end }}{{ end }}" +
|
|
1555
|
-
"{{ end }}",
|
|
1556
|
-
|
|
1557
|
-
// ─── Duration / time-remaining family (bdi.4) ──────────────────────────
|
|
1558
|
-
// Display policy for elapsed/remaining times, each DEFINED ONCE and called
|
|
1559
|
-
// from every segment via `{{ template "name" .x }}`. Input domain is a
|
|
1560
|
-
// non-negative number (seconds, or minutes for formatLongTimeRemaining); the
|
|
1561
|
-
// var-system owns "missing" as a numeric default upstream, so no null arm.
|
|
1562
|
-
//
|
|
1563
|
-
// The cascades branch on the VALUE (which unit threshold it falls in), never
|
|
1564
|
-
// on control flow [LAW:dataflow-not-control-flow]. `div`/`mod` are Go int64
|
|
1565
|
-
// (truncate toward zero == Math.floor for the non-negative domain); `printf
|
|
1566
|
-
// "%.Nf"` is the toFixed(N) stand-in (rounds, matching JS toFixed).
|
|
1567
|
-
|
|
1568
|
-
// Compact "since" stamp: <1m → "Ns"; then floored m/h/d/w. `div` truncates
|
|
1569
|
-
// exactly like Math.floor here (seconds ≥ 0). Used by the git segment's
|
|
1570
|
-
// time-since-commit affordance — verbatim seconds under a minute.
|
|
1571
|
-
formatTimeSince:
|
|
1572
|
-
"{{ if lt . 60 }}{{ . }}s" +
|
|
1573
|
-
"{{ else if lt . 3600 }}{{ div . 60 }}m" +
|
|
1574
|
-
"{{ else if lt . 86400 }}{{ div . 3600 }}h" +
|
|
1575
|
-
"{{ else if lt . 604800 }}{{ div . 86400 }}d" +
|
|
1576
|
-
"{{ else }}{{ div . 604800 }}w{{ end }}",
|
|
1577
|
-
// Elapsed duration: <1m toFixed(0)+s; <1h (/60).toFixed(0)+m; <1d
|
|
1578
|
-
// (/3600).toFixed(1)+h; else (/86400).toFixed(1)+d. printf rounds (not
|
|
1579
|
-
// truncates), reproducing toFixed.
|
|
1580
|
-
formatDuration:
|
|
1581
|
-
'{{ if lt . 60 }}{{ printf "%.0f" . }}s' +
|
|
1582
|
-
'{{ else if lt . 3600 }}{{ printf "%.0f" (divf . 60) }}m' +
|
|
1583
|
-
'{{ else if lt . 86400 }}{{ printf "%.1f" (divf . 3600) }}h' +
|
|
1584
|
-
'{{ else }}{{ printf "%.1f" (divf . 86400) }}d{{ end }}',
|
|
1585
|
-
// Response time: one-decimal seconds under a minute, else one-decimal
|
|
1586
|
-
// minutes.
|
|
1587
|
-
formatResponseTime:
|
|
1588
|
-
'{{ if lt . 60 }}{{ printf "%.1f" . }}s' +
|
|
1589
|
-
'{{ else }}{{ printf "%.1f" (divf . 60) }}m{{ end }}',
|
|
1590
|
-
// Long remaining (input = whole minutes): ≥1day → "Nd"/"Nd Nh"; ≥1hour →
|
|
1591
|
-
// "Nh"/"Nh Nm"; else "Nm". The lower unit is appended only when non-zero,
|
|
1592
|
-
// matching the JS hours>0/minutes>0 guards. `$d`/`$h`/`$m` declared in the
|
|
1593
|
-
// branch frame and read by the inner if (lexical scope reads enclosing
|
|
1594
|
-
// frames) — go-template-js cannot capture a value any other way.
|
|
1595
|
-
formatLongTimeRemaining:
|
|
1596
|
-
"{{ if ge . 1440 }}{{ $d := div . 1440 }}{{ $h := div (mod . 1440) 60 }}" +
|
|
1597
|
-
"{{ if gt $h 0 }}{{ $d }}d {{ $h }}h{{ else }}{{ $d }}d{{ end }}" +
|
|
1598
|
-
"{{ else if ge . 60 }}{{ $h := div . 60 }}{{ $m := mod . 60 }}" +
|
|
1599
|
-
"{{ if gt $m 0 }}{{ $h }}h {{ $m }}m{{ else }}{{ $h }}h{{ end }}" +
|
|
1600
|
-
"{{ else }}{{ . }}m{{ end }}",
|
|
1601
|
-
},
|
|
1602
|
-
} satisfies DslConfig;
|
|
1603
|
-
|
|
1604
|
-
// [LAW:locality-or-seam] The palette names this module-level parse is allowed
|
|
1605
|
-
// to accept — DERIVED from RAW_DEFAULT_DSL_CONFIG itself (globals.palette +
|
|
1606
|
-
// every per-segment palette: pin), never from the live theme registry
|
|
1607
|
-
// (listResolvablePaletteNames()). A real user file must validate against the
|
|
1608
|
-
// live registry (an author can type any name); this file validates against
|
|
1609
|
-
// ITSELF (every name here is a literal we wrote and every render test below
|
|
1610
|
-
// exercises against the real registry already). This is what keeps the
|
|
1611
|
-
// module-load parse below from ever depending on the registry being healthy
|
|
1612
|
-
// at import time — a registry-loading bug elsewhere would surface where it
|
|
1613
|
-
// actually matters (a real render failing), never as an uncatchable crash on
|
|
1614
|
-
// every importer of this file before any daemon/CLI error handling runs.
|
|
1615
|
-
const AUTHORED_PALETTE_NAMES = new Set(
|
|
1616
|
-
[
|
|
1617
|
-
RAW_DEFAULT_DSL_CONFIG.globals.palette,
|
|
1618
|
-
...(Object.values(RAW_DEFAULT_DSL_CONFIG.segments) as SegmentDecl[]).map(
|
|
1619
|
-
(s) => s.palette,
|
|
1620
|
-
),
|
|
1621
|
-
].filter((name): name is string => name !== undefined),
|
|
1622
|
-
);
|
|
1623
|
-
|
|
1624
|
-
// [LAW:single-enforcer] Run the authored literal through the SAME
|
|
1625
|
-
// parse → synthesize pipeline every user config goes through (JSON5 stage +
|
|
1626
|
-
// synthesizeMenuDecls' `menus.*` synthesis, and any future group/menu
|
|
1627
|
-
// synthesis pass) instead of hand-duplicating that logic here. Without this,
|
|
1628
|
-
// the zero-config daemon path (loadConfig: no config file found ⇒ raw={},
|
|
1629
|
-
// merged directly against this constant — see src/config/dsl-loader.ts and
|
|
1630
|
-
// src/config/loader/merge.ts) would ship an UNSYNTHESIZED default: a
|
|
1631
|
-
// `{{ menu (dict "key" …) }}` accordion pairing (the settings menu's pickers,
|
|
1632
|
-
// brandon-theming-8uj.1) would render its glyph, but clicking it would reject
|
|
1633
|
-
// with "unknown state key" — the synthesis that derives a menu's `menus.*`
|
|
1634
|
-
// state var + cycle action only ever ran over TEXT a user typed, never over
|
|
1635
|
-
// this TS literal. Round-tripping through JSON is exactly what
|
|
1636
|
-
// test/default-dsl-config.test.ts's SERIALIZED-based tests already exercise,
|
|
1637
|
-
// so this is the same well-tested path, run once here instead of skipped.
|
|
1638
|
-
export const DEFAULT_DSL_CONFIG: DslConfig = mergeWithDefault(
|
|
1639
|
-
parseDslConfig(
|
|
1640
|
-
"<default>",
|
|
1641
|
-
JSON.stringify(RAW_DEFAULT_DSL_CONFIG),
|
|
1642
|
-
AUTHORED_PALETTE_NAMES,
|
|
1643
|
-
),
|
|
1644
|
-
RAW_DEFAULT_DSL_CONFIG,
|
|
1645
|
-
);
|