@promptctl/cc-candybar 1.42.1 → 1.44.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.
Files changed (126) hide show
  1. package/dist/index.mjs +98 -84
  2. package/package.json +5 -6
  3. package/src/check.ts +0 -478
  4. package/src/cli-flags.ts +0 -8
  5. package/src/click/wire.ts +0 -158
  6. package/src/config/action.ts +0 -329
  7. package/src/config/cli.ts +0 -71
  8. package/src/config/default-dsl-config.ts +0 -1645
  9. package/src/config/disclosure.ts +0 -170
  10. package/src/config/dsl-loader.ts +0 -339
  11. package/src/config/dsl-types.ts +0 -581
  12. package/src/config/edit-chrome.ts +0 -559
  13. package/src/config/help.ts +0 -151
  14. package/src/config/ident.ts +0 -22
  15. package/src/config/layout-ops.ts +0 -177
  16. package/src/config/loader/actions.ts +0 -972
  17. package/src/config/loader/cache.ts +0 -206
  18. package/src/config/loader/cross-ref.ts +0 -714
  19. package/src/config/loader/cycles.ts +0 -148
  20. package/src/config/loader/diagnostics.ts +0 -99
  21. package/src/config/loader/discovery.ts +0 -182
  22. package/src/config/loader/edit-mode.ts +0 -137
  23. package/src/config/loader/emit-schema.ts +0 -68
  24. package/src/config/loader/globals.ts +0 -269
  25. package/src/config/loader/helpers.ts +0 -48
  26. package/src/config/loader/layout.ts +0 -693
  27. package/src/config/loader/looks.ts +0 -96
  28. package/src/config/loader/menu-synth.ts +0 -435
  29. package/src/config/loader/merge.ts +0 -115
  30. package/src/config/loader/persist-target.ts +0 -67
  31. package/src/config/loader/presets.ts +0 -119
  32. package/src/config/loader/refs.ts +0 -100
  33. package/src/config/loader/reserved-namespace.ts +0 -38
  34. package/src/config/loader/segments.ts +0 -120
  35. package/src/config/loader/validate-core.ts +0 -737
  36. package/src/config/loader/variables.ts +0 -260
  37. package/src/config/menu-keys.ts +0 -139
  38. package/src/config/option-domain.ts +0 -164
  39. package/src/config/presets.ts +0 -326
  40. package/src/config/settings-menu.ts +0 -775
  41. package/src/daemon/acquire.ts +0 -684
  42. package/src/daemon/cache/git.ts +0 -649
  43. package/src/daemon/cache/render.ts +0 -623
  44. package/src/daemon/cache/session-usage-store.ts +0 -720
  45. package/src/daemon/cache/watchers.ts +0 -249
  46. package/src/daemon/client-debug.ts +0 -120
  47. package/src/daemon/client-stats.ts +0 -130
  48. package/src/daemon/client-transport.ts +0 -273
  49. package/src/daemon/client.ts +0 -78
  50. package/src/daemon/config-overrides-store.ts +0 -663
  51. package/src/daemon/debug-types.ts +0 -91
  52. package/src/daemon/debug.ts +0 -264
  53. package/src/daemon/fork-bomb-breaker.ts +0 -351
  54. package/src/daemon/limits.ts +0 -211
  55. package/src/daemon/log.ts +0 -81
  56. package/src/daemon/parent-watchdog.ts +0 -87
  57. package/src/daemon/paths.ts +0 -211
  58. package/src/daemon/process-fingerprint.ts +0 -146
  59. package/src/daemon/protocol.ts +0 -292
  60. package/src/daemon/render-payload.ts +0 -1256
  61. package/src/daemon/server.ts +0 -1330
  62. package/src/daemon/session-state-file.ts +0 -108
  63. package/src/daemon/session-state.ts +0 -237
  64. package/src/daemon/socket-lease.ts +0 -209
  65. package/src/daemon/socket-ownership.ts +0 -209
  66. package/src/daemon/stats.ts +0 -235
  67. package/src/daemon/verbs/config-validators.ts +0 -250
  68. package/src/daemon/verbs/index.ts +0 -706
  69. package/src/daemon/verbs/state-validators.ts +0 -249
  70. package/src/daemon/verbs/validator-registry.ts +0 -457
  71. package/src/demo/dsl.ts +0 -143
  72. package/src/demo/mock-data.ts +0 -67
  73. package/src/demo/statusline.json5 +0 -94
  74. package/src/dsl/node-registry.ts +0 -374
  75. package/src/dsl/render.ts +0 -803
  76. package/src/help-text.ts +0 -90
  77. package/src/index.ts +0 -210
  78. package/src/install/currency.ts +0 -197
  79. package/src/install/index.ts +0 -557
  80. package/src/proc/launch.ts +0 -459
  81. package/src/proc/stats-handle.ts +0 -13
  82. package/src/render/action.ts +0 -883
  83. package/src/render/active-segment.ts +0 -78
  84. package/src/render/diagnostic-style.ts +0 -23
  85. package/src/render/diagnostic-text.ts +0 -77
  86. package/src/render/error-glyph.ts +0 -53
  87. package/src/render/menu.ts +0 -257
  88. package/src/render/outcome-plan.ts +0 -45
  89. package/src/render/picker.ts +0 -372
  90. package/src/render/segment-color.ts +0 -74
  91. package/src/render/split-lines.ts +0 -51
  92. package/src/render/strip.ts +0 -228
  93. package/src/segments/cache.ts +0 -131
  94. package/src/segments/context.ts +0 -190
  95. package/src/segments/git.ts +0 -1084
  96. package/src/segments/metrics.ts +0 -187
  97. package/src/segments/pricing.ts +0 -452
  98. package/src/segments/session.ts +0 -23
  99. package/src/segments/tmux.ts +0 -74
  100. package/src/template-engine/cells.ts +0 -90
  101. package/src/template-engine/colors.ts +0 -124
  102. package/src/template-engine/engine.ts +0 -108
  103. package/src/template-engine/funcs.ts +0 -232
  104. package/src/template-engine/index.ts +0 -11
  105. package/src/template-engine/layout.ts +0 -133
  106. package/src/template-engine/scope.ts +0 -62
  107. package/src/template-engine/sparkline.ts +0 -79
  108. package/src/themes/index.ts +0 -20
  109. package/src/themes/palette-resolvers.ts +0 -84
  110. package/src/themes/policy.ts +0 -393
  111. package/src/utils/cache.ts +0 -206
  112. package/src/utils/claude.ts +0 -683
  113. package/src/utils/color-support.ts +0 -118
  114. package/src/utils/formatters.ts +0 -99
  115. package/src/utils/logger.ts +0 -5
  116. package/src/utils/outcome.ts +0 -33
  117. package/src/utils/schema-validator.ts +0 -126
  118. package/src/utils/single-flight.ts +0 -57
  119. package/src/utils/terminal-width.ts +0 -51
  120. package/src/utils/terminal.ts +0 -11
  121. package/src/utils/transcript-fs.ts +0 -279
  122. package/src/var-system/index.ts +0 -24
  123. package/src/var-system/sources.ts +0 -1047
  124. package/src/var-system/store.ts +0 -223
  125. package/src/var-system/types.ts +0 -57
  126. package/src/version.ts +0 -17
@@ -1,74 +0,0 @@
1
- import { launch } from "../proc/launch";
2
- import { debug } from "../utils/logger";
3
- import { ABSENT, failed, ok, type Outcome } from "../utils/outcome";
4
-
5
- // [LAW:single-enforcer] One module-level cache of tmux session lookups, keyed
6
- // by the tmux *socket path* (the substring of `$TMUX` before the first comma —
7
- // `$TMUX` is "<socket>,<client-pid>,<session-num>"). The session ID for a
8
- // given socket doesn't change for the life of the daemon, so this collapses
9
- // N per-render `tmux display-message` spawns to one per (daemon, socket).
10
- //
11
- // [LAW:dataflow-not-control-flow] The lookup always runs the same path: read
12
- // the env key, look up the cached value, spawn-and-record on miss, return.
13
- // Variability lives in the cached values, not in whether the spawn happens.
14
- //
15
- // Value type: the resolved Outcome. `ok` is the session id; `absent` means
16
- // the previous resolution reported an empty session id (definitively no
17
- // session); `failed` means the spawn itself failed. Both non-ok outcomes are
18
- // durable for the daemon's lifetime — retrying every render would burn
19
- // subprocesses without changing the answer — but the cached `failed` still
20
- // flows to the payload boundary as a value, so the failure stays visible
21
- // instead of being demoted to absence ([LAW:no-silent-failure]). A map miss
22
- // (undefined) means not yet attempted — the next call will spawn.
23
- const sessionIdCache = new Map<string, Outcome<string>>();
24
-
25
- // [LAW:one-source-of-truth] One place that extracts the socket-path prefix
26
- // from `$TMUX`. Empty string means "tmux env not set" — the resolver returns
27
- // early before reaching the cache, but we still tolerate the empty key in
28
- // case a future caller invokes us without the early-return.
29
- function tmuxCacheKey(raw: string | undefined): string {
30
- if (!raw) return "";
31
- const comma = raw.indexOf(",");
32
- return comma < 0 ? raw : raw.slice(0, comma);
33
- }
34
-
35
- // Exposed for tests only — resets the cache so each test starts cold.
36
- export function __resetTmuxCacheForTest(): void {
37
- sessionIdCache.clear();
38
- }
39
-
40
- export class TmuxService {
41
- async getSessionId(): Promise<Outcome<string>> {
42
- if (!process.env.TMUX_PANE) {
43
- return ABSENT;
44
- }
45
-
46
- const cacheKey = tmuxCacheKey(process.env.TMUX);
47
- const cached = sessionIdCache.get(cacheKey);
48
- if (cached !== undefined) return cached;
49
-
50
- debug(`Getting tmux session ID, TMUX_PANE: ${process.env.TMUX_PANE}`);
51
-
52
- const result = await launch({
53
- bin: "tmux",
54
- args: ["display-message", "-p", "#S"],
55
- timeoutMs: 1000,
56
- category: "tmux",
57
- });
58
-
59
- // A failed tmux invocation in a session that has $TMUX_PANE set is a real
60
- // misconfiguration; cache it so renders don't re-spawn, and let the
61
- // outcome carry the reason to the payload boundary's log site.
62
- const outcome: Outcome<string> = !result.ok
63
- ? failed(`tmux display-message: ${result.reason}`)
64
- : result.stdout.trim()
65
- ? ok(result.stdout.trim())
66
- : ABSENT;
67
- sessionIdCache.set(cacheKey, outcome);
68
- return outcome;
69
- }
70
-
71
- isInTmux(): boolean {
72
- return !!process.env.TMUX_PANE;
73
- }
74
- }
@@ -1,90 +0,0 @@
1
- // [LAW:dataflow-not-control-flow] The fragment walk is unconditional: every
2
- // fragment is visited; style.link (a value on the fragment) decides whether
3
- // it becomes its own cell or coalesces with neighbours. No branching on
4
- // "are there any cells" — variability lives entirely in the data.
5
- //
6
- // [LAW:one-type-per-behavior] Cells are RichText. There is no parallel
7
- // "cell" type with a single-bg invariant: rich-js's joiner protocol asks
8
- // each item only for its edge style, so the interior can vary freely.
9
- // What was previously expressed as "split this run into N cells at bg
10
- // boundaries" or "lift the modal style to cell-level so parts survive a
11
- // slice" is now structurally impossible to need — RichText carries per-
12
- // character styling via spans, and every layout op (truncate / align /
13
- // pad / slice) preserves spans by construction.
14
-
15
- import { RichText } from "@promptctl/rich-js";
16
- import type { Style } from "@promptctl/rich-js";
17
-
18
- /**
19
- * Convert template-engine fragments (`RichText[]`) into Strip cells
20
- * (`RichText[]`), splitting at OSC-8 link boundaries so each clickable
21
- * region is its own cell. Non-link runs coalesce into one cell whose
22
- * interior styling is carried as spans.
23
- *
24
- * `baseStyle` is the segment-level default (resolved bg + fg). It becomes
25
- * the cell's wrapping style so segment-wide bg+fg cascade across every
26
- * character, and per-fragment fg overlays land as spans on top.
27
- *
28
- * [LAW:single-enforcer] The only mapper from template fragments to Strip
29
- * cells. Callers do not assemble cells by hand.
30
- */
31
- export function fragmentsToCells(
32
- fragments: RichText[],
33
- baseStyle?: Style,
34
- ): RichText[] {
35
- const cells: RichText[] = [];
36
- let group: RichText[] = [];
37
-
38
- const flush = () => {
39
- if (!group.length) return;
40
- const cell = buildCell(group, baseStyle);
41
- if (cell.plain.length > 0) cells.push(cell);
42
- group = [];
43
- };
44
-
45
- for (const frag of fragments) {
46
- if (frag.style.link) {
47
- flush();
48
- const cell = buildCell([frag], baseStyle);
49
- if (cell.plain.length > 0) cells.push(cell);
50
- } else {
51
- group.push(frag);
52
- }
53
- }
54
- flush();
55
-
56
- return cells;
57
- }
58
-
59
- function buildCell(fragments: RichText[], baseStyle?: Style): RichText {
60
- // [LAW:types-are-the-program] Each fragment carries its own style (and
61
- // possibly spans). We merge baseStyle UNDER each fragment's style before
62
- // assembling so the segment-wide default flows through every character,
63
- // with the fragment's own style winning on overlap. That merged style
64
- // then lands as a span on the assembled RichText, so per-fragment styles
65
- // are addressable as overlays.
66
- const layered =
67
- baseStyle !== undefined && !baseStyle.isNull
68
- ? fragments.map((f) => withBaseStyle(f, baseStyle))
69
- : fragments;
70
- const cell = RichText.fromFragments(layered);
71
- cell.end = "";
72
- cell.noWrap = true;
73
- // [LAW:one-source-of-truth] For a single-fragment cell (a link cell, or a
74
- // single-styled non-link fragment), the cell's wrapping style IS that
75
- // fragment's effective style. This keeps the link / linked-region claim
76
- // structurally at cell level (where joiners and click dispatch read it)
77
- // and matches the old per-cell-style contract.
78
- if (layered.length === 1) {
79
- cell.style = layered[0]!.style;
80
- } else if (baseStyle !== undefined && !baseStyle.isNull) {
81
- cell.style = baseStyle;
82
- }
83
- return cell;
84
- }
85
-
86
- function withBaseStyle(f: RichText, base: Style): RichText {
87
- const copy = f.copy();
88
- copy.style = base.add(f.style);
89
- return copy;
90
- }
@@ -1,124 +0,0 @@
1
- // [LAW:single-enforcer] All per-segment bg/fg resolution flows through
2
- // resolveSegmentColors. No second path; a second path would silently drift
3
- // from the ordered pipeline this function owns.
4
- //
5
- // [LAW:no-ambient-temporal-coupling] That ordering — publish the segment's
6
- // palette, resolve bg, publish bg, resolve fg, then hand the body its turn —
7
- // is not incidental execution order that happens to work. It is the phase
8
- // structure of a segment's color resolution, and this function is its one
9
- // owner. Each phase's output is *published* into the active-segment record
10
- // before the next phase runs, so a later template can read an earlier
11
- // phase's result (`fg: '{{ contrastOn (bgOf) }}'`) and an earlier one
12
- // cannot read a later one — it gets a message naming the phase instead.
13
- //
14
- // [LAW:dataflow-not-control-flow] Steps execute unconditionally; the option
15
- // values (undefined template = no spec) are what decides the output, not
16
- // whether steps run. Absent bg or fg → Style fields are undefined → Style.isNull
17
- // → fragmentsToStripCells's baseStyle merge is a no-op, cells flow through
18
- // unchanged.
19
-
20
- import {
21
- Style,
22
- ColorSpec,
23
- resolveColorRef,
24
- ColorRefError,
25
- } from "@promptctl/rich-js";
26
- import type { ColorRgba, Palette } from "@promptctl/rich-js";
27
- import type { RichText } from "@promptctl/rich-js";
28
- import type { Template } from "@promptctl/go-template-js";
29
- import type { ActiveSegmentRef } from "../render/active-segment.js";
30
-
31
- export class ColorSpecError extends Error {
32
- constructor(spec: string, role: "bg" | "fg", detail: string) {
33
- super(`Invalid ${role} color ${JSON.stringify(spec)}: ${detail}`);
34
- this.name = "ColorSpecError";
35
- }
36
- }
37
-
38
- /**
39
- * Resolve a segment's `bg:` and `fg:` templates into the Style that becomes
40
- * its baseStyle, publishing each phase's result into `ref` as it goes.
41
- *
42
- * A `bg:`/`fg:` field is a template evaluated to a **color reference** — a
43
- * palette variable name (`"surface-active"`) or a `#RRGGBB` literal. Since
44
- * rich-js's `resolveColorRef` accepts both and is idempotent, the plain
45
- * authoring form and a computed one take the identical path:
46
- *
47
- * ```json5
48
- * bg: "surface-active" // a name, evaluated as itself
49
- * bg: '{{ darken (color "surface-active") 1 }}' // a computed literal
50
- * ```
51
- *
52
- * There is no "is this a name or a color" branch anywhere — one total
53
- * function over both. [LAW:dataflow-not-control-flow]
54
- *
55
- * Undefined template → that color is not set in the returned Style, so cells
56
- * fall through to their own style (or no color if they have none).
57
- *
58
- * Hue rotation and looks are not this function's concern: they live upstream
59
- * as WHICH palette it is handed, so bg, fg, and the body all resolve from one
60
- * palette and their theme-designed relationships are preserved.
61
- */
62
- export function resolveSegmentColors(
63
- ref: ActiveSegmentRef,
64
- segName: string,
65
- palette: Palette,
66
- bgTemplate: Template<RichText> | undefined,
67
- fgTemplate: Template<RichText> | undefined,
68
- scope: object,
69
- ): Style {
70
- // Phase 0 — the palette is live from here until the walk clears it, so
71
- // `{{ color … }}` in the bg template, the fg template, and the body all read
72
- // this one palette. `bg` starts undefined: it is what phase 1 computes.
73
- const active = { segName, palette, bg: undefined as ColorRgba | undefined };
74
- ref.current = active;
75
-
76
- // Phase 1 — background.
77
- const bgSpec = evalToPlainText(bgTemplate, scope);
78
- const bgColor =
79
- bgSpec !== undefined ? resolveRef(palette, bgSpec, "bg") : undefined;
80
-
81
- // Phase 2 — publish it, then foreground, which may now ask about it.
82
- active.bg = bgColor;
83
- const fgSpec = evalToPlainText(fgTemplate, scope);
84
- const fgColor =
85
- fgSpec !== undefined ? resolveRef(palette, fgSpec, "fg") : undefined;
86
-
87
- return new Style({
88
- bgcolor: bgColor !== undefined ? ColorSpec.fromRgba(bgColor) : undefined,
89
- color: fgColor !== undefined ? ColorSpec.fromRgba(fgColor) : undefined,
90
- });
91
- }
92
-
93
- // Evaluate a template against scope and flatten all fragments to plain text.
94
- // Returns undefined when no template is configured (no bg/fg override).
95
- function evalToPlainText(
96
- template: Template<RichText> | undefined,
97
- scope: object,
98
- ): string | undefined {
99
- if (template === undefined) return undefined;
100
- return template
101
- .evaluate(scope)
102
- .map((f) => f.plain)
103
- .join("");
104
- }
105
-
106
- // [LAW:one-source-of-truth] The same rich-js checkpoint the `{{ color }}`
107
- // template function crosses, so a reference that works in a body works in a
108
- // `bg:` field and vice versa. Re-thrown with the field's role attached —
109
- // rich-js knows the reference failed, only cc-candybar knows it came from a
110
- // `fg:`. [LAW:no-silent-failure] never a substituted default: a broken color
111
- // reference is a config bug, and the fix is the config.
112
- function resolveRef(
113
- palette: Palette,
114
- ref: string,
115
- role: "bg" | "fg",
116
- ): ColorRgba {
117
- try {
118
- return resolveColorRef(palette, ref);
119
- } catch (e) {
120
- if (e instanceof ColorRefError)
121
- throw new ColorSpecError(ref, role, e.message);
122
- throw e;
123
- }
124
- }
@@ -1,108 +0,0 @@
1
- // [LAW:one-source-of-truth] One Engine instance covers all segment template
2
- // evaluation. Callers parse their template once (engine.parse(src)) and call
3
- // template.evaluate(scope) per render — the expensive parse step is not
4
- // repeated per render cycle.
5
- //
6
- // Function registry (closed set for cc-candybar):
7
- // • Go builtins (always registered by the engine): printf, print, println,
8
- // eq/ne/lt/gt/le/ge, and/or/not, len, index, slice, call.
9
- // • sprigDefaults: default, empty, coalesce, ternary, fromJson, toJson.
10
- // • sprigStrings: trunc, lower, upper, replace, trim/trimPrefix/trimSuffix,
11
- // split/join, contains, hasPrefix, hasSuffix, and more string utils.
12
- // • sprigLists: has (membership test: `has "v" $list`).
13
- // • sprigMath: add, sub, mul, div, mod, floor, ceil, round, min, max,
14
- // seq, until (round here is shadowed by formatterFuncs' Math.round below).
15
- // • sprigDatetime(clock): now, date, ago, unixEpoch, dateInZone, dateModify,
16
- // toDate, duration — all reading "now" through the injected clock seam.
17
- // • sprigConversions: atoi, int, int64, float64, toString, toStrings
18
- // (int here is shadowed by ccCandybarFuncs' var-system cast below).
19
- // • sprigDicts: dict, get, set, keys, values, pick, omit, hasKey, merge —
20
- // `dict` lets a helper take multiple named inputs through its one dot arg.
21
- // • richTextFuncs: fg, bg, bold, italic, link, style (painting) PLUS the
22
- // palette-free color math (darken, lighten, mix, contrastOn, readableOn,
23
- // shiftHue, scaleChroma, scaleLightness, shiftLightness) — all from rich-js.
24
- // Naming a THEME color (`{{ color "primary" }}`) and asking for the current
25
- // segment's background (`{{ bgOf }}`) are segment-scoped, so they arrive
26
- // through `extraFuncs` from the DSL render layer, which is the only place
27
- // that knows which segment is rendering.
28
- // • ccCandybarFuncs: basename, dirname, int, string, bool, urlEncode,
29
- // themes, styles, sparkline.
30
- // • formatterFuncs: minutesUntilReset (clock-reading numeric primitive),
31
- // formatInteger, round, formatModelName, shortenModelName. (The cost/token/
32
- // budget AND duration/time-remaining formatters moved to DSL helper templates
33
- // — see DEFAULT_DSL_CONFIG.helpers.)
34
-
35
- import {
36
- createEngine,
37
- type Engine,
38
- type FuncMap,
39
- sprigDefaults,
40
- sprigStrings,
41
- sprigLists,
42
- sprigMath,
43
- sprigDatetime,
44
- sprigConversions,
45
- sprigDicts,
46
- } from "@promptctl/go-template-js";
47
- import { richTextFuncs, RichText } from "@promptctl/rich-js";
48
- import { ccCandybarFuncs, formatterFuncs } from "./funcs.js";
49
-
50
- // [LAW:single-enforcer] fromString/toString are declared once here.
51
- // richTextFuncs() provides the theme-independent vocabulary: painting, color
52
- // math, attributes, links. It needs no configuration, so it is unconditional.
53
- // [LAW:one-way-deps] `extraFuncs` is an INJECTED FuncMap (e.g. the action +
54
- // picker feature funcs, built in render/action.ts + render/picker.ts). The
55
- // generic engine never imports a specific feature — the caller hands it the
56
- // capability as data, so the dependency runs caller → engine, never engine → feature.
57
- // [LAW:one-type-per-behavior] extraFuncs? is a value, not a mode — one factory,
58
- // one engine shape; the data (its presence) governs what's registered.
59
- // [LAW:single-enforcer] `clock` is the one time source. It feeds sprigDatetime
60
- // (the funcs that read "now") AND createEngine's clock option, so every
61
- // time-dependent evaluation in this engine reads from one seam. Defaulted here
62
- // so the default literal `() => new Date()` lives in exactly one place; callers
63
- // that omit it (and forwarders passing `undefined`) inherit it unchanged.
64
- export function createCcCandybarEngine(
65
- extraFuncs?: FuncMap,
66
- clock: () => Date = () => new Date(),
67
- ): Engine<RichText> {
68
- return createEngine<RichText>({
69
- fromString: (s) => new RichText(s),
70
- toString: (rt) => rt.plain,
71
- clock,
72
- // [LAW:no-defensive-null-guards] missing fields must throw at the boundary,
73
- // not silently produce "<no value>". Callers (SourceRegistry, segments)
74
- // depend on MissingFieldError to drive varDefault / defaultEmptyValue.
75
- missingKey: "error",
76
- funcs: {
77
- ...sprigDefaults(),
78
- ...sprigStrings(),
79
- ...sprigLists(),
80
- ...sprigMath(),
81
- // [LAW:single-enforcer] one clock seam: the same source createEngine holds.
82
- ...sprigDatetime(clock),
83
- ...sprigConversions(),
84
- // [LAW:types-are-the-program] `dict` is the substrate primitive a helper
85
- // uses to receive more than one input through its single dot arg:
86
- // `{{ template "budgetStatus" (dict "cost" .x "budget" .y "warn" .z) }}`.
87
- // It makes a multi-input formatter's domain exactly {named scalars},
88
- // decoupled from any payload's nesting — no per-payload helper variant.
89
- ...sprigDicts(),
90
- ...richTextFuncs(),
91
- // Domain-specific overrides last (wins on collision with sprig aliases).
92
- // [LAW:one-source-of-truth] ccCandybarFuncs' `int` is the var-system cast
93
- // (toNumber over VarValue); it intentionally shadows sprigConversions' `int`
94
- // so a template's `int` keeps one meaning. Position is the override policy.
95
- ...ccCandybarFuncs(),
96
- // [LAW:one-source-of-truth] formatter funcs delegate to
97
- // src/utils/formatters.ts; minutesUntilReset reads the same `clock` seam.
98
- // The only sprig collision is `round`: formatterFuncs' Math.round shadows
99
- // sprigMath's precision-aware round, registered last so the domain meaning
100
- // wins (revisited by the bdi cleanup ticket).
101
- ...formatterFuncs(clock),
102
- // [LAW:locality-or-seam] Injected feature funcs (the daemon's per-config
103
- // engine supplies the `action` + `picker` funcs; resolver-less compile-only
104
- // paths do not). Last so a feature can override on collision.
105
- ...(extraFuncs ?? {}),
106
- },
107
- });
108
- }
@@ -1,232 +0,0 @@
1
- // [LAW:one-source-of-truth] Cast semantics live in var-system/types.ts.
2
- // This module wraps them as FuncMap entries — it does not duplicate logic.
3
- // [LAW:single-enforcer] The boundary gate (argTypes) lives at the engine
4
- // dispatch site; the bodies here trust the runtime types they declared.
5
-
6
- import { basename as pathBasename, dirname as pathDirname } from "path";
7
- import type { FuncMap } from "@promptctl/go-template-js";
8
- import {
9
- toNumber,
10
- toString,
11
- toBool,
12
- type VarValue,
13
- } from "../var-system/types.js";
14
- import {
15
- abbreviatePath,
16
- formatInteger,
17
- formatModelName,
18
- shortenModelName,
19
- } from "../utils/formatters.js";
20
- import { listResolvablePaletteNames, STRIP_STYLES } from "../themes/policy.js";
21
- import { renderSparkline, parseSeries } from "./sparkline.js";
22
-
23
- // [LAW:one-source-of-truth] The DSL `themes()` and `styles()` bindings
24
- // project the SAME canonical sources the set-state validator consults
25
- // (listResolvablePaletteNames / STRIP_STYLES). A picker (or a config that
26
- // `range`s over themes() to emit OSC-8 cells) iterates the allow-list the
27
- // validator will enforce on the resulting click — the list and the gate cannot
28
- // diverge because there is no second list.
29
- //
30
- // Module-init caching is correct by construction: rich-js THEMES is a
31
- // static import (no dynamic palette registration at runtime) and
32
- // STRIP_STYLES is a const array. The "reactivity" requirement from the
33
- // ticket is satisfied vacuously — the lists never change during a
34
- // daemon lifetime, so a cached snapshot IS the current truth.
35
- const THEMES_LIST: readonly string[] = listResolvablePaletteNames();
36
- const STYLES_LIST: readonly string[] = [...STRIP_STYLES];
37
-
38
- // [LAW:single-enforcer] Numeric validation lives at ONE boundary — the engine's
39
- // `int`/`float` argType gate (@promptctl/go-template-js), which proves membership
40
- // and normalizes the carrier to a JS `number` before the func body runs. `int`
41
- // admits only finite integer-valued numbers + safe-integer bigints (rejecting
42
- // fractionals and precision-losing/overflowing bigints loudly at the gate);
43
- // `float` admits any finite number. So a formatter wrapper receives a clean
44
- // `number` and needs no bigint guard of its own — the prior `num()` helper was a
45
- // second enforcer of what the gate now owns, removed when the formatters adopted
46
- // int/float. [LAW:no-silent-failure] the gate's rejection is the loud failure on
47
- // a precision-losing integer input; this is not weaker than the old runtime
48
- // check, it is the same guarantee moved to the true boundary.
49
-
50
- // cc-candybar-specific functions not already covered by sprig or Go builtins.
51
- // The engine also includes sprigDefaults(), sprigStrings(), and sprigLists()
52
- // which cover: default, trunc, lower, upper, replace, trim/trimPrefix/trimSuffix,
53
- // split/join, contains/hasPrefix/hasSuffix, has.
54
- // Go builtins cover: printf, eq/ne/lt/gt/le/ge, and/or/not.
55
- export function ccCandybarFuncs(): FuncMap {
56
- return {
57
- // Path operations absent from sprig in this package.
58
- basename: {
59
- fn: (s: string) => pathBasename(s),
60
- argTypes: ["string"],
61
- },
62
- dirname: {
63
- fn: (s: string) => pathDirname(s),
64
- argTypes: ["string"],
65
- },
66
-
67
- // [LAW:decomposition] Fish-style path abbreviation — one thing: shorten
68
- // every segment but the leaf. Composes over the directory template's
69
- // already-collapsed output (`~`/project-relative), never duplicating that
70
- // logic. Impl in utils/formatters.ts, mirroring formatModelName's split.
71
- abbreviatePath: {
72
- fn: (s: string) => abbreviatePath(s),
73
- argTypes: ["string"],
74
- },
75
-
76
- // [LAW:single-enforcer] Type casts delegate to var-system/types.ts.
77
- // "value" argType: these funcs enforce their own constraints and emit
78
- // a useful TypeError on ambiguous input — no need for the engine gate
79
- // to pre-filter (it can't describe the partial-cast semantics anyway).
80
- int: {
81
- fn: (v: VarValue) => toNumber(v),
82
- argTypes: ["value"],
83
- },
84
- string: {
85
- fn: (v: VarValue) => toString(v),
86
- argTypes: ["value"],
87
- },
88
- bool: {
89
- fn: (v: VarValue) => toBool(v),
90
- argTypes: ["value"],
91
- },
92
-
93
- // [LAW:single-enforcer] One URL-encoding function for click-verb URL
94
- // construction in templates. encodeURIComponent matches the legacy
95
- // src/segments/renderer.ts toolbar/tray renderers, so DSL-emitted
96
- // cc-candybar://verb/<value> URLs are byte-identical to legacy ones.
97
- // [LAW:types-are-the-program] Domain primitive surfaced by chunk-7/8
98
- // migration (vhi.3) — the proposal explicitly budgets "may add one or
99
- // two filters during migration." This is one of those.
100
- urlEncode: {
101
- fn: (s: string) => encodeURIComponent(s),
102
- argTypes: ["string"],
103
- },
104
-
105
- // [LAW:one-source-of-truth] themes() and styles() are zero-arg
106
- // projections of the daemon's canonical domain lists. A picker (or a
107
- // hand-authored `range`) expresses "options come from list Y" by
108
- // iterating these bindings; the same lists feed the set-state
109
- // validator's allow-list checks, so the rendered options are exactly
110
- // the values the next click will be allowed to write. No second
111
- // enumeration in user config.
112
- // [LAW:dataflow-not-control-flow] A range loop over a list IS the
113
- // option primitive — `{{ range themes }}…{{ end }}` produces one
114
- // rendered cell per allowed value. Adding a theme adds a cell;
115
- // removing a theme removes a cell; no template branch on "how many
116
- // themes are there."
117
- themes: {
118
- fn: () => THEMES_LIST,
119
- argTypes: [],
120
- },
121
- styles: {
122
- fn: () => STYLES_LIST,
123
- argTypes: [],
124
- },
125
-
126
- // [LAW:effects-at-boundaries] Pure trend renderer: a numeric series (the
127
- // daemon-owned ring, projected through the payload as a delimited string)
128
- // becomes a unicode mini-graph. The series crosses the scalar var-system
129
- // seam as a string, so the FuncMap slot is "string"; `parseSeries` decodes
130
- // it and `renderSparkline` draws it — neither accumulates state. The
131
- // optional trailing "int" slot caps the glyph count to fit a cell (the
132
- // evaluator validates only supplied args, so `{{ sparkline .series }}` and
133
- // `{{ sparkline .series 24 }}` are both well-typed). Returns a bare string;
134
- // the engine lifts it to RichText so the segment's fg/bg palette colors the
135
- // whole graph — no per-glyph color math here.
136
- sparkline: {
137
- fn: (series: string, width?: number) =>
138
- renderSparkline(parseSeries(series), width),
139
- argTypes: ["string", "int"],
140
- },
141
- };
142
- }
143
-
144
- // [LAW:one-source-of-truth] Domain value formatters. What remains after the
145
- // formatting-as-data epic (bdi) are primitives with NO template-native
146
- // expression — the bdi migration is complete. Do NOT migrate these to DSL
147
- // helpers; each one is retained for a load-bearing reason:
148
- //
149
- // minutesUntilReset — returns a NUMBER for comparisons and arithmetic
150
- // (`le (minutesUntilReset .x) 8`). A template helper writes to output
151
- // and cannot return a value, so a helper form would duplicate the formula
152
- // across every comparison site [LAW:one-source-of-truth].
153
- //
154
- // formatInteger — locale-aware grouping via toLocaleString(). A regex
155
- // helper would be locale-blind (always comma+3), a second divergent
156
- // producer [LAW:one-source-of-truth]. The daemon inherits shell LANG/LC_*
157
- // from the Rust spawner so grouping honors the user's locale at runtime.
158
- //
159
- // round — Math.round (half-away-from-zero) consumed in `{{ round .pct }}%`
160
- // segments. shadows sprigMath's precision-aware round intentionally:
161
- // block/weekly/context need integer-rounding, not decimal rounding.
162
- //
163
- // formatModelName / shortenModelName — regex parsing of external model IDs
164
- // (named capture groups, version assembly, variant stripping). Trust-
165
- // boundary normalization, not display policy. No regex primitive in DSL.
166
- //
167
- // The display-formatting families moved to DSL helpers in DEFAULT_DSL_CONFIG:
168
- // cost/token/budget (bdi.3), duration/time-remaining (bdi.4).
169
- //
170
- // [LAW:single-enforcer] minutesUntilReset reads "now" from the injected
171
- // `clock` — the SAME seam createCcCandybarEngine threads to sprigDatetime
172
- // (now/unixEpoch) and createEngine. One clock governs every time-dependent
173
- // evaluation; tests inject a frozen clock for determinism.
174
- export function formatterFuncs(clock: () => Date = () => new Date()): FuncMap {
175
- return {
176
- // Epoch-seconds → whole minutes until that instant, clamped at 0 for a past
177
- // expiry: round(max(0, epoch*1000 − now)/60000). Consumed by the block/weekly
178
- // segments (`formatLongTimeRemaining (minutesUntilReset .resetsAt)`) and the
179
- // cacheTimer warmth countdown (numeric `le` thresholds).
180
- minutesUntilReset: {
181
- fn: (epochSeconds: number) =>
182
- Math.round(
183
- Math.max(0, epochSeconds * 1000 - clock().getTime()) / 60000,
184
- ),
185
- // [LAW:types-are-the-program] An epoch is integer-valued; `int` rejects a
186
- // fractional or precision-losing carrier at the gate.
187
- argTypes: ["int"],
188
- },
189
-
190
- // ─── Locale-grouped integer (context's "50,000") ──────────────────
191
- // [LAW:one-source-of-truth] bdi.5: deliberately RETAINED as a primitive
192
- // (not migrated to a DSL helper). toLocaleString reads the host locale the
193
- // daemon inherits, so grouping is locale-correct (en_US "50,000" /
194
- // de_DE "50.000"). A regex helper would be a second, locale-blind producer
195
- // of grouping policy — same parsing/formatting boundary that keeps
196
- // formatModelName here.
197
- formatInteger: {
198
- fn: (n: number) => formatInteger(n),
199
- // [LAW:types-are-the-program] Integer grouping is meaningful only for an
200
- // integer; `int` rejects a fractional/precision-losing carrier at the gate.
201
- argTypes: ["int"],
202
- },
203
-
204
- // ─── Numeric helper (block/weekly's Math.round of pct) ────────────
205
- // [LAW:one-source-of-truth] Math.round is a JS built-in shared between
206
- // legacy and DSL — no wrapper indirection makes sense for it. The
207
- // formatters.ts module documents domain-meaningful rules; rounding is
208
- // not domain-meaningful, so it stays here.
209
- round: {
210
- fn: (n: number) => Math.round(n),
211
- // [LAW:types-are-the-program] round takes a fractional value (e.g. a
212
- // percentage) → `float` admits any finite number.
213
- argTypes: ["float"],
214
- },
215
-
216
- // ─── Model-name normalizers (chunk-7 model dsl-pending → dsl-parity) ─
217
- // [LAW:one-source-of-truth] formatModelName / shortenModelName are regex-
218
- // based normalizers; the DSL function set has no regex primitive, so the
219
- // only honest way to express them is to wrap the canonical impls. The
220
- // model binding can then move from "echo display_name verbatim" (only
221
- // byte-parity for friendly names) to "echo normalized model name" (full
222
- // behavioral parity, including raw IDs like "claude-sonnet-4-6").
223
- formatModelName: {
224
- fn: (raw: string) => formatModelName(raw),
225
- argTypes: ["string"],
226
- },
227
- shortenModelName: {
228
- fn: (formatted: string) => shortenModelName(formatted),
229
- argTypes: ["string"],
230
- },
231
- };
232
- }
@@ -1,11 +0,0 @@
1
- export { createCcCandybarEngine } from "./engine.js";
2
- export { buildScope } from "./scope.js";
3
- export { ccCandybarFuncs } from "./funcs.js";
4
- export { fragmentsToCells } from "./cells.js";
5
- export { evaluateWhen, applySegmentLayout } from "./layout.js";
6
- export type {
7
- SegmentLayoutOptions,
8
- JustifyMode,
9
- TruncateMode,
10
- } from "./layout.js";
11
- export { resolveSegmentColors, ColorSpecError } from "./colors.js";