@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
package/src/segments/tmux.ts
DELETED
|
@@ -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";
|