@promptctl/cc-candybar 1.0.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/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/cc-candybar +6 -0
- package/dist/index.mjs +185 -0
- package/package.json +99 -0
- package/plugin/.claude-plugin/plugin.json +11 -0
- package/plugin/bin/preview.sh +305 -0
- package/plugin/commands/candybar.md +403 -0
- package/plugin/templates/config-essential.json +36 -0
- package/plugin/templates/config-full.json +55 -0
- package/plugin/templates/config-standard.json +39 -0
- package/plugin/templates/config-tui-compact.json +48 -0
- package/plugin/templates/config-tui-full.json +89 -0
- package/plugin/templates/config-tui-standard.json +56 -0
- package/plugin/templates/config-tui.json +18 -0
- package/plugin/templates/nerd-fonts-sample.txt +5 -0
- package/schema/cc-candybar.schema.json +1379 -0
- package/src/click/wire.ts +113 -0
- package/src/config/action.ts +91 -0
- package/src/config/cli.ts +170 -0
- package/src/config/default-dsl-config.ts +661 -0
- package/src/config/dsl-loader.ts +265 -0
- package/src/config/dsl-types.ts +425 -0
- package/src/config/loader/actions.ts +530 -0
- package/src/config/loader/cache.ts +206 -0
- package/src/config/loader/cross-ref.ts +326 -0
- package/src/config/loader/cycles.ts +148 -0
- package/src/config/loader/diagnostics.ts +99 -0
- package/src/config/loader/discovery.ts +182 -0
- package/src/config/loader/emit-schema.ts +63 -0
- package/src/config/loader/globals.ts +42 -0
- package/src/config/loader/helpers.ts +48 -0
- package/src/config/loader/layout.ts +688 -0
- package/src/config/loader/merge.ts +40 -0
- package/src/config/loader/refs.ts +96 -0
- package/src/config/loader/segments.ts +120 -0
- package/src/config/loader/validate-core.ts +674 -0
- package/src/config/loader/variables.ts +260 -0
- package/src/daemon/acquire.ts +411 -0
- package/src/daemon/cache/git.ts +553 -0
- package/src/daemon/cache/render.ts +449 -0
- package/src/daemon/cache/session-usage-store.ts +446 -0
- package/src/daemon/cache/watchers.ts +245 -0
- package/src/daemon/client-debug.ts +120 -0
- package/src/daemon/client-stats.ts +129 -0
- package/src/daemon/client-transport.ts +273 -0
- package/src/daemon/client.ts +75 -0
- package/src/daemon/debug-types.ts +91 -0
- package/src/daemon/debug.ts +264 -0
- package/src/daemon/limits.ts +154 -0
- package/src/daemon/log.ts +69 -0
- package/src/daemon/parent-watchdog.ts +80 -0
- package/src/daemon/paths.ts +127 -0
- package/src/daemon/protocol.ts +235 -0
- package/src/daemon/render-payload.ts +611 -0
- package/src/daemon/server.ts +1103 -0
- package/src/daemon/session-state-file.ts +108 -0
- package/src/daemon/session-state.ts +237 -0
- package/src/daemon/stats.ts +229 -0
- package/src/daemon/verbs/index.ts +458 -0
- package/src/daemon/verbs/state-validators.ts +708 -0
- package/src/demo/dsl.ts +117 -0
- package/src/demo/mock-data.ts +67 -0
- package/src/demo/statusline.json5 +92 -0
- package/src/dsl/node-registry.ts +281 -0
- package/src/dsl/render.ts +558 -0
- package/src/index.ts +206 -0
- package/src/install/index.ts +410 -0
- package/src/proc/launch.ts +451 -0
- package/src/proc/stats-handle.ts +13 -0
- package/src/render/action.ts +458 -0
- package/src/render/diagnostic-style.ts +23 -0
- package/src/render/diagnostic-text.ts +77 -0
- package/src/render/error-glyph.ts +53 -0
- package/src/render/outcome-plan.ts +45 -0
- package/src/render/picker.ts +231 -0
- package/src/render/split-lines.ts +51 -0
- package/src/render/strip.ts +103 -0
- package/src/segments/cache.ts +131 -0
- package/src/segments/context.ts +190 -0
- package/src/segments/git.ts +561 -0
- package/src/segments/metrics.ts +101 -0
- package/src/segments/pricing.ts +452 -0
- package/src/segments/session.ts +188 -0
- package/src/segments/tmux.ts +74 -0
- package/src/template-engine/cells.ts +90 -0
- package/src/template-engine/colors.ts +102 -0
- package/src/template-engine/engine.ts +108 -0
- package/src/template-engine/funcs.ts +216 -0
- package/src/template-engine/index.ts +11 -0
- package/src/template-engine/layout.ts +112 -0
- package/src/template-engine/scope.ts +62 -0
- package/src/themes/index.ts +19 -0
- package/src/themes/palette-resolvers.ts +86 -0
- package/src/themes/policy.ts +79 -0
- package/src/themes/session-random.ts +88 -0
- package/src/utils/cache.ts +206 -0
- package/src/utils/claude.ts +616 -0
- package/src/utils/color-support.ts +118 -0
- package/src/utils/formatters.ts +77 -0
- package/src/utils/logger.ts +5 -0
- package/src/utils/outcome.ts +33 -0
- package/src/utils/schema-validator.ts +126 -0
- package/src/utils/single-flight.ts +57 -0
- package/src/utils/terminal-width.ts +43 -0
- package/src/utils/terminal.ts +11 -0
- package/src/utils/transcript-fs.ts +162 -0
- package/src/var-system/index.ts +24 -0
- package/src/var-system/sources.ts +1038 -0
- package/src/var-system/store.ts +223 -0
- package/src/var-system/types.ts +57 -0
|
@@ -0,0 +1,661 @@
|
|
|
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] `satisfies DslConfig` (not an annotation)
|
|
23
|
+
// preserves the literal's narrow keys for downstream consumers — every
|
|
24
|
+
// declared segment name shows up as a known key, every variable name shows
|
|
25
|
+
// up in the input set the daemon must populate.
|
|
26
|
+
|
|
27
|
+
import type { DslConfig } from "./dsl-types.js";
|
|
28
|
+
|
|
29
|
+
// ─── Shared template fragments ───────────────────────────────────────────────
|
|
30
|
+
//
|
|
31
|
+
// Factored out of the segments' `template` fields so:
|
|
32
|
+
// (1) the git working-tree counts and status icon can be shared by the two
|
|
33
|
+
// git-style segments (git, gitTaculous) without duplication, and
|
|
34
|
+
// (2) the block/weekly threshold cascade can be parameterized on the
|
|
35
|
+
// variable name without resorting to runtime string surgery.
|
|
36
|
+
|
|
37
|
+
// Directory: ~ collapse under $HOME, project-relative under workspace.project_dir,
|
|
38
|
+
// else raw. Inline-recomputes the project-relative path because the DSL has no
|
|
39
|
+
// template-level `:=` (a `kind: "template"` var would express it once, but adds
|
|
40
|
+
// noise for a single use).
|
|
41
|
+
//
|
|
42
|
+
// Prefix checks are boundary-safe: a path is "under" a base iff it equals the
|
|
43
|
+
// base OR starts with `base + "/"`. The naive `hasPrefix base path` is a
|
|
44
|
+
// string match — it would treat `/home/alice` as a child of `/home/al`.
|
|
45
|
+
// `(printf "%s/" base)` adds the separator so the prefix can only land at a
|
|
46
|
+
// path boundary; the `eq` arm catches the exact-match case where the trailing
|
|
47
|
+
// slash would over-match.
|
|
48
|
+
//
|
|
49
|
+
// Equal-paths case (current_dir === project_dir) DOES enter the project-
|
|
50
|
+
// relative arm: DIR_REL evaluates to "" and the ternary picks basename
|
|
51
|
+
// (project_dir), so the project root renders as `<repo-name>` instead of the
|
|
52
|
+
// full absolute path. Same logic handles equal home & current_dir → just "~".
|
|
53
|
+
const DIR_REL = 'trimPrefix "/" (trimPrefix .project_dir .current_dir)';
|
|
54
|
+
const DIR_TEMPLATE =
|
|
55
|
+
' {{ if and (ne .home "") (or (eq .home .current_dir) (hasPrefix (printf "%s/" .home) .current_dir)) }}~{{ trimPrefix .home .current_dir }}' +
|
|
56
|
+
"{{ else }}" +
|
|
57
|
+
'{{ if or (eq .project_dir .current_dir) (hasPrefix (printf "%s/" .project_dir) .current_dir) }}' +
|
|
58
|
+
`{{ ternary (${DIR_REL}) (basename .project_dir) (ne (${DIR_REL}) "") }}` +
|
|
59
|
+
"{{ else }}{{ .current_dir }}{{ end }}{{ end }} ";
|
|
60
|
+
|
|
61
|
+
// Git working-tree counts — leading-space-then-trim idiom: each present count
|
|
62
|
+
// contributes " +N", trim drops the leading space, survivors single-spaced.
|
|
63
|
+
const GIT_WORKTREE =
|
|
64
|
+
"{{ if or (gt .git.staged 0) (gt .git.unstaged 0) (gt .git.untracked 0) (gt .git.conflicts 0) }}" +
|
|
65
|
+
' ({{ printf "%s%s%s%s"' +
|
|
66
|
+
' (ternary (printf " +%v" .git.staged) "" (gt .git.staged 0))' +
|
|
67
|
+
' (ternary (printf " ~%v" .git.unstaged) "" (gt .git.unstaged 0))' +
|
|
68
|
+
' (ternary (printf " ?%v" .git.untracked) "" (gt .git.untracked 0))' +
|
|
69
|
+
' (ternary (printf " !%v" .git.conflicts) "" (gt .git.conflicts 0)) | trim }}){{ end }}';
|
|
70
|
+
|
|
71
|
+
// Status icon precedence: conflicts → ⚠, dirty → ●, else clean ✓.
|
|
72
|
+
const GIT_STATUS =
|
|
73
|
+
'{{ if eq .git.status "conflicts" }}⚠{{ else }}' +
|
|
74
|
+
'{{ if eq .git.status "dirty" }}●{{ else }}✓{{ end }}{{ end }}';
|
|
75
|
+
|
|
76
|
+
const GIT_TEMPLATE =
|
|
77
|
+
' {{ if ne .git.repoName "" }}{{ .git.repoName }} {{ end }}⎇ {{ .git.branch }}' +
|
|
78
|
+
"{{ if .git.sha }} ♯ {{ .git.sha }}{{ end }}" +
|
|
79
|
+
"{{ if or (gt .git.ahead 0) (gt .git.behind 0) }}" +
|
|
80
|
+
" {{ if gt .git.ahead 0 }}↑{{ .git.ahead }}{{ end }}" +
|
|
81
|
+
"{{ if gt .git.behind 0 }}↓{{ .git.behind }}{{ end }}{{ end }}" +
|
|
82
|
+
GIT_WORKTREE +
|
|
83
|
+
"{{ if .git.upstream }} →{{ .git.upstream }}{{ end }}" +
|
|
84
|
+
"{{ if gt .git.stash 0 }} ⧇ {{ .git.stash }}{{ end }}" +
|
|
85
|
+
" " +
|
|
86
|
+
GIT_STATUS +
|
|
87
|
+
" ";
|
|
88
|
+
|
|
89
|
+
// [LAW:dataflow-not-control-flow] block and weekly share the same threshold
|
|
90
|
+
// cascade (≥warningThreshold → error, ≥50 → warning, else panel) on a numeric
|
|
91
|
+
// ref. The builders parameterize BOTH the percentage ref and the threshold
|
|
92
|
+
// ref so each segment reads its own configured threshold from the var store
|
|
93
|
+
// rather than the literal 80 baked into the template. User overrides flow
|
|
94
|
+
// through the variables-merge-by-name cascade in mergeWithDefault — no new
|
|
95
|
+
// override mechanism required.
|
|
96
|
+
function blockLikeBg(pctRef: string, thresholdRef: string): string {
|
|
97
|
+
return (
|
|
98
|
+
`{{ if ge (round ${pctRef}) ${thresholdRef} }}error` +
|
|
99
|
+
`{{ else }}{{ if ge (round ${pctRef}) 50 }}warning` +
|
|
100
|
+
`{{ else }}panel{{ end }}{{ end }}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function blockLikeFg(pctRef: string): string {
|
|
105
|
+
return (
|
|
106
|
+
`{{ if ge (round ${pctRef}) 50 }}button-color-foreground` +
|
|
107
|
+
`{{ else }}foreground{{ end }}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── The default config ──────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export const DEFAULT_DSL_CONFIG = {
|
|
114
|
+
globals: {
|
|
115
|
+
// Picked by the daemon's basePalette resolution; user overrides in their
|
|
116
|
+
// own config. catppuccin-latte ships every spec name the default
|
|
117
|
+
// segments reference (surface, panel, surface-active, foreground).
|
|
118
|
+
palette: "catppuccin-latte",
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// ─── Variables ─────────────────────────────────────────────────────────────
|
|
122
|
+
// Every value the segment templates read. Sources:
|
|
123
|
+
// • input — daemon's augmented payload (see src/daemon/render-payload.ts)
|
|
124
|
+
// • env — process environment
|
|
125
|
+
// • shell — subprocess; cached
|
|
126
|
+
// • state — per-session daemon state
|
|
127
|
+
variables: {
|
|
128
|
+
// From hookData, pass-through.
|
|
129
|
+
current_dir: {
|
|
130
|
+
kind: "input",
|
|
131
|
+
path: "workspace.current_dir",
|
|
132
|
+
default: "?",
|
|
133
|
+
},
|
|
134
|
+
project_dir: {
|
|
135
|
+
kind: "input",
|
|
136
|
+
path: "workspace.project_dir",
|
|
137
|
+
default: "",
|
|
138
|
+
},
|
|
139
|
+
"model.display_name": {
|
|
140
|
+
kind: "input",
|
|
141
|
+
path: "model.display_name",
|
|
142
|
+
default: "",
|
|
143
|
+
},
|
|
144
|
+
"session.id": { kind: "input", path: "session_id", default: "" },
|
|
145
|
+
version: { kind: "input", path: "version", default: "" },
|
|
146
|
+
|
|
147
|
+
// [LAW:one-source-of-truth] The usable terminal width for THIS render —
|
|
148
|
+
// the exact post-reserve cell count FlexStrip wraps to. renderDsl injects
|
|
149
|
+
// it into the payload from its own `opts.width` (the single value that
|
|
150
|
+
// feeds both the wrap and this variable), so a width-paginated picker and
|
|
151
|
+
// the wrap algebra can never disagree. Never cached: a resize is just a
|
|
152
|
+
// new value on the same path, re-read every render. The default only
|
|
153
|
+
// applies to compile-only callers that render without injecting a width.
|
|
154
|
+
"term.cols": {
|
|
155
|
+
kind: "input",
|
|
156
|
+
path: "term.cols",
|
|
157
|
+
type: "number",
|
|
158
|
+
default: 80,
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// [LAW:one-source-of-truth] The per-segment hue-rotation step renderDsl
|
|
162
|
+
// reads (HUE_STEP_VAR) — a value in the store like every other render input,
|
|
163
|
+
// NOT a globals field. [LAW:types-are-the-program] In the bundled default it
|
|
164
|
+
// is a LITERAL: nothing here writes the "hue-step" SessionState key (the
|
|
165
|
+
// default declares no interactive actions), so a fixed 14° is the strongest
|
|
166
|
+
// TRUE theorem — a `state` var would claim a session-variability the default
|
|
167
|
+
// never exercises and force a SessionState on every consumer. A user makes
|
|
168
|
+
// hue live by overriding this one variable to `{ kind: "state", key:
|
|
169
|
+
// "hue-step" }` and adding a pair of bounded stepper actions — the same
|
|
170
|
+
// two-part pattern the theme picker uses.
|
|
171
|
+
// 14°: adjacent segments stay visually distinct without per-segment colors.
|
|
172
|
+
"hue.step": { kind: "literal", value: 14 },
|
|
173
|
+
|
|
174
|
+
// home flows through the augmented payload (buildRenderPayload reads
|
|
175
|
+
// HOME, falling back to USERPROFILE on Windows where HOME is often
|
|
176
|
+
// unset). Sourcing via `kind: "input"` rather than `kind: "env",
|
|
177
|
+
// name: "HOME"` makes the directory `~` collapse work on every
|
|
178
|
+
// platform without per-platform config edits.
|
|
179
|
+
home: { kind: "input", path: "home", default: "" },
|
|
180
|
+
|
|
181
|
+
// Tmux session id flows through the daemon's augmented payload
|
|
182
|
+
// (TmuxService caches by socket and never re-spawns for the lifetime of
|
|
183
|
+
// the daemon, so this stays cheap). A `kind: "shell"` declaration would
|
|
184
|
+
// spawn the subprocess at every cache-entry creation regardless of
|
|
185
|
+
// whether the tmux segment is in the active layout — buildNeededPrefixes
|
|
186
|
+
// gates the input variant so unused segments cost nothing.
|
|
187
|
+
"tmux.session": { kind: "input", path: "tmux.session", default: "" },
|
|
188
|
+
|
|
189
|
+
// Git — every field flows from the daemon's projected GitInfo payload.
|
|
190
|
+
// The DSL's native `kind: "git"` source covers a 6-field subset
|
|
191
|
+
// (branch/sha/dirty/ahead/behind/stash); using `input` here gives the
|
|
192
|
+
// full 12-field surface uniformly via the augmented payload.
|
|
193
|
+
"git.repoName": {
|
|
194
|
+
kind: "input",
|
|
195
|
+
path: "git.repoName",
|
|
196
|
+
default: "",
|
|
197
|
+
},
|
|
198
|
+
"git.branch": { kind: "input", path: "git.branch", default: "" },
|
|
199
|
+
"git.sha": { kind: "input", path: "git.sha", default: "" },
|
|
200
|
+
"git.ahead": {
|
|
201
|
+
kind: "input",
|
|
202
|
+
path: "git.ahead",
|
|
203
|
+
type: "number",
|
|
204
|
+
default: 0,
|
|
205
|
+
},
|
|
206
|
+
"git.behind": {
|
|
207
|
+
kind: "input",
|
|
208
|
+
path: "git.behind",
|
|
209
|
+
type: "number",
|
|
210
|
+
default: 0,
|
|
211
|
+
},
|
|
212
|
+
"git.staged": {
|
|
213
|
+
kind: "input",
|
|
214
|
+
path: "git.staged",
|
|
215
|
+
type: "number",
|
|
216
|
+
default: 0,
|
|
217
|
+
},
|
|
218
|
+
"git.unstaged": {
|
|
219
|
+
kind: "input",
|
|
220
|
+
path: "git.unstaged",
|
|
221
|
+
type: "number",
|
|
222
|
+
default: 0,
|
|
223
|
+
},
|
|
224
|
+
"git.untracked": {
|
|
225
|
+
kind: "input",
|
|
226
|
+
path: "git.untracked",
|
|
227
|
+
type: "number",
|
|
228
|
+
default: 0,
|
|
229
|
+
},
|
|
230
|
+
"git.conflicts": {
|
|
231
|
+
kind: "input",
|
|
232
|
+
path: "git.conflicts",
|
|
233
|
+
type: "number",
|
|
234
|
+
default: 0,
|
|
235
|
+
},
|
|
236
|
+
"git.upstream": { kind: "input", path: "git.upstream", default: "" },
|
|
237
|
+
"git.stash": {
|
|
238
|
+
kind: "input",
|
|
239
|
+
path: "git.stash",
|
|
240
|
+
type: "number",
|
|
241
|
+
default: 0,
|
|
242
|
+
},
|
|
243
|
+
"git.status": { kind: "input", path: "git.status", default: "clean" },
|
|
244
|
+
"git.operation": { kind: "input", path: "git.operation", default: "" },
|
|
245
|
+
"git.timeSinceCommit": {
|
|
246
|
+
kind: "input",
|
|
247
|
+
path: "git.timeSinceCommit",
|
|
248
|
+
type: "number",
|
|
249
|
+
default: 0,
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// Prompt-cache expiry — epoch seconds, projected by the cache provider.
|
|
253
|
+
// Same unit/shape as block/weekly resetsAt so the cacheTimer segment
|
|
254
|
+
// composes `minutesUntilReset` identically. 0 (default) ⇒ no cache
|
|
255
|
+
// activity found ⇒ segment's `when` hides it.
|
|
256
|
+
"cache.expiresAt": {
|
|
257
|
+
kind: "input",
|
|
258
|
+
path: "cache.expiresAt",
|
|
259
|
+
type: "number",
|
|
260
|
+
default: 0,
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
// Usage / cost — daemon folds from the SessionUsageStore; numeric.
|
|
264
|
+
"session.cost": {
|
|
265
|
+
kind: "input",
|
|
266
|
+
path: "session.cost",
|
|
267
|
+
type: "number",
|
|
268
|
+
default: 0,
|
|
269
|
+
},
|
|
270
|
+
"session.tokens": {
|
|
271
|
+
kind: "input",
|
|
272
|
+
path: "session.tokens",
|
|
273
|
+
type: "number",
|
|
274
|
+
default: 0,
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
// Today — daemon folds today's cross-session total from the SessionUsageStore.
|
|
278
|
+
"today.cost": {
|
|
279
|
+
kind: "input",
|
|
280
|
+
path: "today.cost",
|
|
281
|
+
type: "number",
|
|
282
|
+
default: 0,
|
|
283
|
+
},
|
|
284
|
+
"today.tokens": {
|
|
285
|
+
kind: "input",
|
|
286
|
+
path: "today.tokens",
|
|
287
|
+
type: "number",
|
|
288
|
+
default: 0,
|
|
289
|
+
},
|
|
290
|
+
// Budget knobs — pure config constants, user overrides in their file.
|
|
291
|
+
"today.budget.amount": { kind: "literal", value: 50 },
|
|
292
|
+
"today.budget.warningThreshold": { kind: "literal", value: 80 },
|
|
293
|
+
|
|
294
|
+
// Block — daemon projects directly from hookData.rate_limits.five_hour;
|
|
295
|
+
// resetsAt is raw epoch seconds
|
|
296
|
+
// so the template can compose `minutesUntilReset .block.resetsAt` (the
|
|
297
|
+
// same chain weekly uses, single composition point).
|
|
298
|
+
"block.nativeUtilization": {
|
|
299
|
+
kind: "input",
|
|
300
|
+
path: "block.nativeUtilization",
|
|
301
|
+
type: "number",
|
|
302
|
+
default: 0,
|
|
303
|
+
},
|
|
304
|
+
"block.resetsAt": {
|
|
305
|
+
kind: "input",
|
|
306
|
+
path: "block.resetsAt",
|
|
307
|
+
type: "number",
|
|
308
|
+
default: 0,
|
|
309
|
+
},
|
|
310
|
+
// Budget knob — overridable per-config through the variables-merge-by-
|
|
311
|
+
// name cascade. Matches the legacy DEFAULT_CONFIG.budget.block.warning
|
|
312
|
+
// Threshold.
|
|
313
|
+
"block.budget.warningThreshold": { kind: "literal", value: 80 },
|
|
314
|
+
|
|
315
|
+
// Weekly — direct projection of hookData.rate_limits.seven_day.
|
|
316
|
+
"weekly.percentage": {
|
|
317
|
+
kind: "input",
|
|
318
|
+
path: "weekly.percentage",
|
|
319
|
+
type: "number",
|
|
320
|
+
default: 0,
|
|
321
|
+
},
|
|
322
|
+
"weekly.resetsAt": {
|
|
323
|
+
kind: "input",
|
|
324
|
+
path: "weekly.resetsAt",
|
|
325
|
+
type: "number",
|
|
326
|
+
default: 0,
|
|
327
|
+
},
|
|
328
|
+
"weekly.budget.warningThreshold": { kind: "literal", value: 80 },
|
|
329
|
+
|
|
330
|
+
// Context — daemon fetches via ContextProvider; contextLeftPercentage.
|
|
331
|
+
"context.totalTokens": {
|
|
332
|
+
kind: "input",
|
|
333
|
+
path: "context.totalTokens",
|
|
334
|
+
type: "number",
|
|
335
|
+
default: 0,
|
|
336
|
+
},
|
|
337
|
+
"context.contextLeft": {
|
|
338
|
+
kind: "input",
|
|
339
|
+
path: "context.contextLeft",
|
|
340
|
+
type: "number",
|
|
341
|
+
default: 100,
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
// Metrics — daemon fetches via MetricsProvider; numeric.
|
|
345
|
+
"metrics.lastResponseTime": {
|
|
346
|
+
kind: "input",
|
|
347
|
+
path: "metrics.lastResponseTime",
|
|
348
|
+
type: "number",
|
|
349
|
+
default: 0,
|
|
350
|
+
},
|
|
351
|
+
"metrics.responseTime": {
|
|
352
|
+
kind: "input",
|
|
353
|
+
path: "metrics.responseTime",
|
|
354
|
+
type: "number",
|
|
355
|
+
default: 0,
|
|
356
|
+
},
|
|
357
|
+
"metrics.sessionDuration": {
|
|
358
|
+
kind: "input",
|
|
359
|
+
path: "metrics.sessionDuration",
|
|
360
|
+
type: "number",
|
|
361
|
+
default: 0,
|
|
362
|
+
},
|
|
363
|
+
"metrics.messageCount": {
|
|
364
|
+
kind: "input",
|
|
365
|
+
path: "metrics.messageCount",
|
|
366
|
+
type: "number",
|
|
367
|
+
default: 0,
|
|
368
|
+
},
|
|
369
|
+
"metrics.linesAdded": {
|
|
370
|
+
kind: "input",
|
|
371
|
+
path: "metrics.linesAdded",
|
|
372
|
+
type: "number",
|
|
373
|
+
default: 0,
|
|
374
|
+
},
|
|
375
|
+
"metrics.linesRemoved": {
|
|
376
|
+
kind: "input",
|
|
377
|
+
path: "metrics.linesRemoved",
|
|
378
|
+
type: "number",
|
|
379
|
+
default: 0,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
// ─── Segments ──────────────────────────────────────────────────────────────
|
|
384
|
+
// Every built-in. Templates ported from the parity bindings; bg/fg are
|
|
385
|
+
// palette spec names resolved against the active theme. `when` predicates
|
|
386
|
+
// hide a segment when its primary signal is absent (no git repo, no version
|
|
387
|
+
// field, no env var, no tmux, no rate-limit window).
|
|
388
|
+
segments: {
|
|
389
|
+
directory: {
|
|
390
|
+
template: DIR_TEMPLATE,
|
|
391
|
+
bg: "surface",
|
|
392
|
+
fg: "foreground",
|
|
393
|
+
},
|
|
394
|
+
model: {
|
|
395
|
+
template: " ✱ {{ formatModelName .model.display_name }} ",
|
|
396
|
+
bg: "panel",
|
|
397
|
+
fg: "foreground",
|
|
398
|
+
when: '{{ ne .model.display_name "" }}',
|
|
399
|
+
},
|
|
400
|
+
sessionId: {
|
|
401
|
+
template: " ⌗{{ trunc 8 .session.id }} ",
|
|
402
|
+
bg: "surface",
|
|
403
|
+
fg: "foreground",
|
|
404
|
+
when: '{{ ne .session.id "" }}',
|
|
405
|
+
},
|
|
406
|
+
version: {
|
|
407
|
+
template: " ◈ v{{ .version }} ",
|
|
408
|
+
bg: "surface",
|
|
409
|
+
fg: "foreground",
|
|
410
|
+
when: '{{ ne .version "" }}',
|
|
411
|
+
},
|
|
412
|
+
tmux: {
|
|
413
|
+
template: ' tmux:{{ .tmux.session | default "none" }} ',
|
|
414
|
+
bg: "surface-active",
|
|
415
|
+
fg: "foreground",
|
|
416
|
+
when: '{{ ne .tmux.session "" }}',
|
|
417
|
+
},
|
|
418
|
+
git: {
|
|
419
|
+
template: GIT_TEMPLATE,
|
|
420
|
+
bg: "surface-active",
|
|
421
|
+
fg: "foreground",
|
|
422
|
+
when: '{{ ne .git.branch "" }}',
|
|
423
|
+
},
|
|
424
|
+
gitTaculous: {
|
|
425
|
+
template:
|
|
426
|
+
" (git)" +
|
|
427
|
+
'{{ if ne .git.repoName "" }} {{ .git.repoName }}{{ end }}' +
|
|
428
|
+
'{{ if ne .git.operation "" }} [{{ .git.operation }}]{{ end }}' +
|
|
429
|
+
'{{ if ne .git.sha "" }} {{ .git.sha }}{{ end }}' +
|
|
430
|
+
"{{ if or (gt .git.staged 0) (gt .git.unstaged 0) (gt .git.untracked 0) (gt .git.conflicts 0) }} " +
|
|
431
|
+
'{{ if gt .git.staged 0 }}{{ green "S" }}{{ end }}' +
|
|
432
|
+
'{{ if or (gt .git.unstaged 0) (gt .git.untracked 0) }}{{ red "U" }}{{ end }}' +
|
|
433
|
+
'{{ if gt .git.conflicts 0 }}{{ red (printf "!%v" .git.conflicts) }}{{ end }}' +
|
|
434
|
+
"{{ end }}" +
|
|
435
|
+
" ⎇ {{ .git.branch }}" +
|
|
436
|
+
'{{ if ne .git.upstream "" }} [{{ .git.upstream }}' +
|
|
437
|
+
"{{ if or (gt .git.ahead 0) (gt .git.behind 0) }} " +
|
|
438
|
+
'{{ if gt .git.ahead 0 }}{{ green (printf "+%v" .git.ahead) }}{{ end }}' +
|
|
439
|
+
"{{ if and (gt .git.ahead 0) (gt .git.behind 0) }}/{{ end }}" +
|
|
440
|
+
'{{ if gt .git.behind 0 }}{{ red (printf "-%v" .git.behind) }}{{ end }}' +
|
|
441
|
+
"{{ end }}]{{ end }}" +
|
|
442
|
+
"{{ if gt .git.stash 0 }} ({{ .git.stash }} stashed){{ end }}" +
|
|
443
|
+
'{{ if gt .git.timeSinceCommit 0 }} ◷ {{ template "formatTimeSince" .git.timeSinceCommit }}{{ end }}' +
|
|
444
|
+
" ",
|
|
445
|
+
bg: "surface-active",
|
|
446
|
+
fg: "foreground",
|
|
447
|
+
when: '{{ ne .git.branch "" }}',
|
|
448
|
+
},
|
|
449
|
+
toolbar: {
|
|
450
|
+
template:
|
|
451
|
+
' {{ link (printf "cc-candybar://open-vscode/%s" (urlEncode .current_dir)) "\u{1F4C2}" }}' +
|
|
452
|
+
' {{ link (printf "cc-candybar://copy/%s" (urlEncode (trunc 8 .session.id))) "⎘" }} ',
|
|
453
|
+
bg: "surface",
|
|
454
|
+
fg: "foreground",
|
|
455
|
+
},
|
|
456
|
+
session: {
|
|
457
|
+
template:
|
|
458
|
+
' § {{ template "formatCost" .session.cost }} ({{ template "formatTokens" .session.tokens }}) ',
|
|
459
|
+
bg: "surface",
|
|
460
|
+
fg: "foreground",
|
|
461
|
+
},
|
|
462
|
+
today: {
|
|
463
|
+
template:
|
|
464
|
+
' ☉ {{ template "formatCost" .today.cost }} ({{ template "formatTokens" .today.tokens }})' +
|
|
465
|
+
'{{ template "budgetStatus" (dict "cost" .today.cost "budget" .today.budget.amount "warn" .today.budget.warningThreshold) }} ',
|
|
466
|
+
bg: "surface",
|
|
467
|
+
fg: "foreground",
|
|
468
|
+
},
|
|
469
|
+
block: {
|
|
470
|
+
template:
|
|
471
|
+
" ◱ {{ round .block.nativeUtilization }}% " +
|
|
472
|
+
'({{ template "formatLongTimeRemaining" (minutesUntilReset .block.resetsAt) }}) ',
|
|
473
|
+
bg: blockLikeBg(
|
|
474
|
+
".block.nativeUtilization",
|
|
475
|
+
".block.budget.warningThreshold",
|
|
476
|
+
),
|
|
477
|
+
fg: blockLikeFg(".block.nativeUtilization"),
|
|
478
|
+
// Hide unless we have a five-hour-window snapshot.
|
|
479
|
+
when: "{{ gt .block.resetsAt 0 }}",
|
|
480
|
+
},
|
|
481
|
+
weekly: {
|
|
482
|
+
template:
|
|
483
|
+
" ◑ {{ round .weekly.percentage }}% " +
|
|
484
|
+
'({{ template "formatLongTimeRemaining" (minutesUntilReset .weekly.resetsAt) }}) ',
|
|
485
|
+
bg: blockLikeBg(".weekly.percentage", ".weekly.budget.warningThreshold"),
|
|
486
|
+
fg: blockLikeFg(".weekly.percentage"),
|
|
487
|
+
when: "{{ gt .weekly.resetsAt 0 }}",
|
|
488
|
+
},
|
|
489
|
+
// Prompt-cache warmth countdown. minutesUntilReset clamps a past expiry
|
|
490
|
+
// to 0, so an expired cache renders "cold" (and reads red via the ≤8
|
|
491
|
+
// arm) rather than a negative number. [LAW:dataflow-not-control-flow]
|
|
492
|
+
// glyph + "cold"/"Nm" + color all derive from the one expiry value; the
|
|
493
|
+
// provider supplies no display state. Constant `surface` bg with a
|
|
494
|
+
// fg-only threshold cascade mirrors the legacy inline-colored text
|
|
495
|
+
// (warm = normal, ≤20m = warning, ≤8m/cold = error).
|
|
496
|
+
cacheTimer: {
|
|
497
|
+
template:
|
|
498
|
+
" ◴ {{ if le (minutesUntilReset .cache.expiresAt) 0 }}cold" +
|
|
499
|
+
"{{ else }}{{ minutesUntilReset .cache.expiresAt }}m{{ end }} ",
|
|
500
|
+
bg: "surface",
|
|
501
|
+
fg:
|
|
502
|
+
"{{ if le (minutesUntilReset .cache.expiresAt) 8 }}error" +
|
|
503
|
+
"{{ else }}{{ if le (minutesUntilReset .cache.expiresAt) 20 }}warning" +
|
|
504
|
+
"{{ else }}foreground{{ end }}{{ end }}",
|
|
505
|
+
when: "{{ gt .cache.expiresAt 0 }}",
|
|
506
|
+
},
|
|
507
|
+
context: {
|
|
508
|
+
template:
|
|
509
|
+
" ◔ {{ formatInteger .context.totalTokens }} ({{ .context.contextLeft }}%) ",
|
|
510
|
+
bg:
|
|
511
|
+
"{{ if le .context.contextLeft 20 }}error" +
|
|
512
|
+
"{{ else }}{{ if le .context.contextLeft 40 }}warning" +
|
|
513
|
+
"{{ else }}surface-active{{ end }}{{ end }}",
|
|
514
|
+
fg:
|
|
515
|
+
"{{ if le .context.contextLeft 40 }}button-color-foreground" +
|
|
516
|
+
"{{ else }}foreground{{ end }}",
|
|
517
|
+
when: "{{ gt .context.totalTokens 0 }}",
|
|
518
|
+
},
|
|
519
|
+
metrics: {
|
|
520
|
+
// [LAW:dataflow-not-control-flow] Each part guards on its own value
|
|
521
|
+
// rather than gating the whole segment on a single dimension. With
|
|
522
|
+
// MetricsPayload's fields independently optional and pickNonNull
|
|
523
|
+
// dropping nulls (see src/daemon/render-payload.ts), an absent field
|
|
524
|
+
// resolves through the var-system fallback chain to 0 — the same
|
|
525
|
+
// falsy shape the per-part `if` test treats as hidden. The segment-
|
|
526
|
+
// level `when` survives as a weak any-present check so a payload
|
|
527
|
+
// with zero metrics data renders no cell at all (an empty template
|
|
528
|
+
// would otherwise produce a single-space bg-styled cell).
|
|
529
|
+
template:
|
|
530
|
+
'{{ if .metrics.lastResponseTime }} Δ {{ template "formatResponseTime" .metrics.lastResponseTime }}{{ end }}' +
|
|
531
|
+
'{{ if .metrics.responseTime }} ⧖ {{ template "formatResponseTime" .metrics.responseTime }}{{ end }}' +
|
|
532
|
+
'{{ if .metrics.sessionDuration }} ⧗ {{ template "formatDuration" .metrics.sessionDuration }}{{ end }}' +
|
|
533
|
+
"{{ if .metrics.messageCount }} ◆ {{ .metrics.messageCount }}{{ end }}" +
|
|
534
|
+
"{{ if .metrics.linesAdded }} + {{ .metrics.linesAdded }}{{ end }}" +
|
|
535
|
+
"{{ if .metrics.linesRemoved }} - {{ .metrics.linesRemoved }}{{ end }} ",
|
|
536
|
+
bg: "panel",
|
|
537
|
+
fg: "foreground",
|
|
538
|
+
when:
|
|
539
|
+
"{{ or .metrics.lastResponseTime .metrics.responseTime" +
|
|
540
|
+
" .metrics.sessionDuration .metrics.messageCount" +
|
|
541
|
+
" .metrics.linesAdded .metrics.linesRemoved }}",
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
// Default layout — a single horizontal row of segment refs.
|
|
546
|
+
// A-grammar equivalent: { h: ["directory","git","model","session","today","context"] }
|
|
547
|
+
// [LAW:one-source-of-truth] The bundled default now authors the same terse
|
|
548
|
+
// surface every user config lowers to, so the default is the reference spelling.
|
|
549
|
+
// Adding rows = wrapping in { kind:"container", direction:"vertical", children:[...] };
|
|
550
|
+
// every segment is already declared above.
|
|
551
|
+
root: {
|
|
552
|
+
kind: "container",
|
|
553
|
+
direction: "horizontal",
|
|
554
|
+
children: [
|
|
555
|
+
{ kind: "segment", name: "directory" },
|
|
556
|
+
{ kind: "segment", name: "git" },
|
|
557
|
+
{ kind: "segment", name: "model" },
|
|
558
|
+
{ kind: "segment", name: "session" },
|
|
559
|
+
{ kind: "segment", name: "today" },
|
|
560
|
+
{ kind: "segment", name: "context" },
|
|
561
|
+
],
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
// [LAW:locality-or-seam] No decoupled actions in the bundled default either —
|
|
565
|
+
// the baseline statusline binds no clickable regions. A user config declares
|
|
566
|
+
// named actions and binds them from a segment template via
|
|
567
|
+
// `{{ action "name" … }}`; the merge cascade adds them by name.
|
|
568
|
+
actions: {},
|
|
569
|
+
|
|
570
|
+
// [LAW:single-enforcer] / [LAW:one-source-of-truth] Display-formatting policy
|
|
571
|
+
// for the cost/token/budget family lives here as named template helpers, each
|
|
572
|
+
// DEFINED ONCE and called from every segment via `{{ template "name" .arg }}`
|
|
573
|
+
// — so how a cost/token string looks is data a user overrides by name, not
|
|
574
|
+
// compiled JS. The K/M token-scale rule has a SINGLE home (`formatTokenCount`);
|
|
575
|
+
// `formatTokens` suffixes " tokens" onto it and `formatTokenBreakdown` calls it
|
|
576
|
+
// per part, so the scale policy can never drift between the three.
|
|
577
|
+
// [LAW:dataflow-not-control-flow] A multi-input helper (budgetStatus,
|
|
578
|
+
// formatTokenBreakdown) receives its inputs as one `dict` value through its
|
|
579
|
+
// single dot arg — variability flows as data across one boundary, not as a
|
|
580
|
+
// bespoke multi-arg signature.
|
|
581
|
+
helpers: {
|
|
582
|
+
// Cost: under a cent reads "<$0.01"; otherwise "$" + two decimals. (Null is
|
|
583
|
+
// unrepresentable through the var-system — type:number with a numeric default
|
|
584
|
+
// owns "missing" upstream — so no null branch is needed here.)
|
|
585
|
+
formatCost:
|
|
586
|
+
'{{ if lt . 0.01 }}<$0.01{{ else }}${{ printf "%.2f" . }}{{ end }}',
|
|
587
|
+
// The single home of the K/M token-scale rule. >=1e6 → "X.YM", >=1e3 → "X.YK",
|
|
588
|
+
// else the integer verbatim (0 and negatives fall through to this arm, exactly
|
|
589
|
+
// as the retired JS did). No " tokens" suffix — that is formatTokens' job.
|
|
590
|
+
formatTokenCount:
|
|
591
|
+
'{{ if ge . 1000000 }}{{ printf "%.1f" (divf . 1000000) }}M' +
|
|
592
|
+
'{{ else if ge . 1000 }}{{ printf "%.1f" (divf . 1000) }}K' +
|
|
593
|
+
"{{ else }}{{ . }}{{ end }}",
|
|
594
|
+
formatTokens: '{{ template "formatTokenCount" . }} tokens',
|
|
595
|
+
// Breakdown over a dict {input, output, cacheCreation, cacheRead}; each present
|
|
596
|
+
// part is formatted by the shared formatTokenCount and joined with " + ". A
|
|
597
|
+
// `$first` flag (reassigned across if-frames) inserts the separator before all
|
|
598
|
+
// but the first present part; all-zero collapses to "0 tokens".
|
|
599
|
+
formatTokenBreakdown:
|
|
600
|
+
"{{ $first := true }}" +
|
|
601
|
+
'{{ if gt .input 0 }}{{ template "formatTokenCount" .input }} in{{ $first = false }}{{ end }}' +
|
|
602
|
+
'{{ if gt .output 0 }}{{ if not $first }} + {{ end }}{{ template "formatTokenCount" .output }} out{{ $first = false }}{{ end }}' +
|
|
603
|
+
'{{ if or (gt .cacheCreation 0) (gt .cacheRead 0) }}{{ if not $first }} + {{ end }}{{ template "formatTokenCount" (add .cacheCreation .cacheRead) }} cached{{ $first = false }}{{ end }}' +
|
|
604
|
+
"{{ if $first }}0 tokens{{ end }}",
|
|
605
|
+
// Budget suffix over a dict {cost, budget, warn}. Non-displayable (budget<=0 or
|
|
606
|
+
// cost<0) → "". Otherwise pct = min(100, cost/budget*100), rendered " !N%" at/above
|
|
607
|
+
// warn, " +N%" at/above 50, " N%" below.
|
|
608
|
+
budgetStatus:
|
|
609
|
+
"{{ if or (le .budget 0) (lt .cost 0) }}{{ else }}" +
|
|
610
|
+
"{{ $pct := minf 100 (mulf (divf .cost .budget) 100) }}" +
|
|
611
|
+
'{{ $p := printf "%.0f%%" $pct }}' +
|
|
612
|
+
"{{ if ge $pct .warn }} !{{ $p }}" +
|
|
613
|
+
"{{ else }}{{ if ge $pct 50 }} +{{ $p }}{{ else }} {{ $p }}{{ end }}{{ end }}" +
|
|
614
|
+
"{{ end }}",
|
|
615
|
+
|
|
616
|
+
// ─── Duration / time-remaining family (bdi.4) ──────────────────────────
|
|
617
|
+
// Display policy for elapsed/remaining times, each DEFINED ONCE and called
|
|
618
|
+
// from every segment via `{{ template "name" .x }}`. Input domain is a
|
|
619
|
+
// non-negative number (seconds, or minutes for formatLongTimeRemaining); the
|
|
620
|
+
// var-system owns "missing" as a numeric default upstream, so no null arm.
|
|
621
|
+
//
|
|
622
|
+
// The cascades branch on the VALUE (which unit threshold it falls in), never
|
|
623
|
+
// on control flow [LAW:dataflow-not-control-flow]. `div`/`mod` are Go int64
|
|
624
|
+
// (truncate toward zero == Math.floor for the non-negative domain); `printf
|
|
625
|
+
// "%.Nf"` is the toFixed(N) stand-in (rounds, matching JS toFixed).
|
|
626
|
+
|
|
627
|
+
// Compact "since" stamp: <1m → "Ns"; then floored m/h/d/w. `div` truncates
|
|
628
|
+
// exactly like Math.floor here (seconds ≥ 0). Used by the git segment's
|
|
629
|
+
// time-since-commit affordance — verbatim seconds under a minute.
|
|
630
|
+
formatTimeSince:
|
|
631
|
+
"{{ if lt . 60 }}{{ . }}s" +
|
|
632
|
+
"{{ else if lt . 3600 }}{{ div . 60 }}m" +
|
|
633
|
+
"{{ else if lt . 86400 }}{{ div . 3600 }}h" +
|
|
634
|
+
"{{ else if lt . 604800 }}{{ div . 86400 }}d" +
|
|
635
|
+
"{{ else }}{{ div . 604800 }}w{{ end }}",
|
|
636
|
+
// Elapsed duration: <1m toFixed(0)+s; <1h (/60).toFixed(0)+m; <1d
|
|
637
|
+
// (/3600).toFixed(1)+h; else (/86400).toFixed(1)+d. printf rounds (not
|
|
638
|
+
// truncates), reproducing toFixed.
|
|
639
|
+
formatDuration:
|
|
640
|
+
'{{ if lt . 60 }}{{ printf "%.0f" . }}s' +
|
|
641
|
+
'{{ else if lt . 3600 }}{{ printf "%.0f" (divf . 60) }}m' +
|
|
642
|
+
'{{ else if lt . 86400 }}{{ printf "%.1f" (divf . 3600) }}h' +
|
|
643
|
+
'{{ else }}{{ printf "%.1f" (divf . 86400) }}d{{ end }}',
|
|
644
|
+
// Response time: one-decimal seconds under a minute, else one-decimal
|
|
645
|
+
// minutes.
|
|
646
|
+
formatResponseTime:
|
|
647
|
+
'{{ if lt . 60 }}{{ printf "%.1f" . }}s' +
|
|
648
|
+
'{{ else }}{{ printf "%.1f" (divf . 60) }}m{{ end }}',
|
|
649
|
+
// Long remaining (input = whole minutes): ≥1day → "Nd"/"Nd Nh"; ≥1hour →
|
|
650
|
+
// "Nh"/"Nh Nm"; else "Nm". The lower unit is appended only when non-zero,
|
|
651
|
+
// matching the JS hours>0/minutes>0 guards. `$d`/`$h`/`$m` declared in the
|
|
652
|
+
// branch frame and read by the inner if (lexical scope reads enclosing
|
|
653
|
+
// frames) — go-template-js cannot capture a value any other way.
|
|
654
|
+
formatLongTimeRemaining:
|
|
655
|
+
"{{ if ge . 1440 }}{{ $d := div . 1440 }}{{ $h := div (mod . 1440) 60 }}" +
|
|
656
|
+
"{{ if gt $h 0 }}{{ $d }}d {{ $h }}h{{ else }}{{ $d }}d{{ end }}" +
|
|
657
|
+
"{{ else if ge . 60 }}{{ $h := div . 60 }}{{ $m := mod . 60 }}" +
|
|
658
|
+
"{{ if gt $m 0 }}{{ $h }}h {{ $m }}m{{ else }}{{ $h }}h{{ end }}" +
|
|
659
|
+
"{{ else }}{{ . }}m{{ end }}",
|
|
660
|
+
},
|
|
661
|
+
} satisfies DslConfig;
|