@promptctl/cc-candybar 1.9.0 → 1.11.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 +37 -37
- package/package.json +5 -5
- package/schema/cc-candybar.schema.json +11 -0
- package/src/config/default-dsl-config.ts +43 -31
- package/src/config/dsl-types.ts +18 -1
- package/src/config/loader/globals.ts +11 -1
- package/src/config/loader/validate-core.ts +28 -0
- package/src/daemon/server.ts +32 -2
- package/src/demo/dsl.ts +8 -1
- package/src/dsl/node-registry.ts +9 -0
- package/src/dsl/render.ts +9 -1
- package/src/render/action.ts +6 -0
- package/src/render/picker.ts +5 -1
- package/src/render/strip.ts +77 -20
- package/src/template-engine/layout.ts +23 -2
- package/src/themes/policy.ts +17 -0
package/src/render/strip.ts
CHANGED
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
renderToString,
|
|
10
10
|
type Joiner,
|
|
11
11
|
type ColorSystemSpec,
|
|
12
|
+
type PowerlineJoinerOptions,
|
|
13
|
+
type CapsuleJoinerOptions,
|
|
12
14
|
} from "@promptctl/rich-js";
|
|
13
|
-
import type { StripStyle } from "../themes/policy.js";
|
|
15
|
+
import type { Charset, StripStyle } from "../themes/policy.js";
|
|
14
16
|
|
|
15
17
|
export interface RenderedSegmentLike {
|
|
16
18
|
type: string;
|
|
@@ -19,11 +21,11 @@ export interface RenderedSegmentLike {
|
|
|
19
21
|
fgHex?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
// [LAW:one-source-of-truth] `StripStyle` and
|
|
23
|
-
// themes/policy.ts (the
|
|
24
|
+
// [LAW:one-source-of-truth] `StripStyle`/`Charset` and their value lists live
|
|
25
|
+
// in themes/policy.ts (the render-identifier policy module, importable by the
|
|
24
26
|
// option-source machinery without a render→template-engine cycle). Re-exported
|
|
25
|
-
// here so render-layer consumers can keep importing
|
|
26
|
-
export type { StripStyle };
|
|
27
|
+
// here so render-layer consumers can keep importing them from the strip module.
|
|
28
|
+
export type { Charset, StripStyle };
|
|
27
29
|
|
|
28
30
|
// [LAW:one-source-of-truth] Raw terminal cols we assume when the wire
|
|
29
31
|
// didn't give us one (older client, env-stripped spawn). RAW — not
|
|
@@ -37,6 +39,18 @@ export const DEFAULT_TERMINAL_WIDTH = 120;
|
|
|
37
39
|
// (`globals.autoWrap ?? DEFAULT_WRAP`) derives from this constant.
|
|
38
40
|
export const DEFAULT_WRAP = true;
|
|
39
41
|
|
|
42
|
+
// [LAW:one-source-of-truth] The one statement of the globals.padding
|
|
43
|
+
// default (one space per side inside each segment cell — current behavior,
|
|
44
|
+
// matching the legacy display.padding). Every resolver of the config global
|
|
45
|
+
// (`globals.padding ?? DEFAULT_PADDING`) derives from this constant.
|
|
46
|
+
export const DEFAULT_PADDING = 1;
|
|
47
|
+
|
|
48
|
+
// [LAW:one-source-of-truth] The one statement of the globals.charset default
|
|
49
|
+
// (powerline unicode glyphs — current behavior, matching the legacy
|
|
50
|
+
// display.charset). Every resolver of the config global
|
|
51
|
+
// (`globals.charset ?? DEFAULT_CHARSET`) derives from this constant.
|
|
52
|
+
export const DEFAULT_CHARSET: Charset = "unicode";
|
|
53
|
+
|
|
40
54
|
export interface BuildLineOptions {
|
|
41
55
|
style: StripStyle;
|
|
42
56
|
colorCompatibility: ColorSystemSpec;
|
|
@@ -53,20 +67,57 @@ export interface BuildLineOptions {
|
|
|
53
67
|
// explicitly — encoding "no wrap" as width=Infinity would corrupt the
|
|
54
68
|
// picker's pagination, which reads the same width value.
|
|
55
69
|
wrap: boolean;
|
|
70
|
+
// [LAW:one-source-of-truth] Spaces inside each segment cell per side
|
|
71
|
+
// (globals.padding, default 1 — the legacy display.padding, intra-cell,
|
|
72
|
+
// not rich-js FlexStrip's inter-item gap). Required so every construction
|
|
73
|
+
// site states the resolved value; the cell builders derive from it and
|
|
74
|
+
// never re-default.
|
|
75
|
+
padding: number;
|
|
76
|
+
// [LAW:one-source-of-truth] Which glyph vocabulary the joiners render with
|
|
77
|
+
// (globals.charset, default "unicode" — the legacy display.charset).
|
|
78
|
+
// Required for the same reason as padding: the resolved value reaches every
|
|
79
|
+
// render explicitly; pickJoiner derives from it and never re-defaults.
|
|
80
|
+
charset: Charset;
|
|
56
81
|
}
|
|
57
82
|
|
|
58
|
-
|
|
83
|
+
// [LAW:dataflow-not-control-flow] Charset variability lives in these VALUES,
|
|
84
|
+
// not in branches: per style, each charset names the joiner-construction
|
|
85
|
+
// options. The unicode entries are empty on purpose — rich-js owns its
|
|
86
|
+
// canonical powerline glyphs (U+E0B0 / U+E0B6+U+E0B4) and restating them here
|
|
87
|
+
// would be a second source that could drift [LAW:one-source-of-truth]. The
|
|
88
|
+
// ascii glyphs are DELIBERATELY single-column (same display width as the
|
|
89
|
+
// unicode caps) so stripChromeCols stays charset-invariant; the
|
|
90
|
+
// measured-chrome pin in test/picker-pagination.test.ts checks that for every
|
|
91
|
+
// style × charset. Widen a glyph and that pin fails loudly.
|
|
92
|
+
const POWERLINE_GLYPHS: Record<Charset, PowerlineJoinerOptions> = {
|
|
93
|
+
unicode: {},
|
|
94
|
+
ascii: { glyph: ">" },
|
|
95
|
+
};
|
|
96
|
+
const CAPSULE_GLYPHS: Record<Charset, CapsuleJoinerOptions> = {
|
|
97
|
+
unicode: {},
|
|
98
|
+
ascii: { left: "(", right: ")" },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function pickJoiner(
|
|
102
|
+
style: StripStyle,
|
|
103
|
+
charset: Charset,
|
|
104
|
+
separator?: string,
|
|
105
|
+
): Joiner {
|
|
59
106
|
// [LAW:dataflow-not-control-flow] joiner choice is data-driven; one arm per
|
|
60
|
-
// shape.
|
|
107
|
+
// shape. Style picks the joiner CLASS, charset indexes the glyph options fed
|
|
108
|
+
// to it — the two dimensions stay orthogonal (any style renders under either
|
|
109
|
+
// charset). Plain takes no charset lookup: its separator is already user
|
|
110
|
+
// data (globals.default_separator) and its default (" | ") is ASCII-safe.
|
|
111
|
+
// [LAW:types-are-the-program] Total over StripStyle — the `never`
|
|
61
112
|
// default makes adding a STRIP_STYLES member a compile error here until it
|
|
62
113
|
// gets a joiner, so the picker's domain can never offer an unrenderable shape.
|
|
63
114
|
switch (style) {
|
|
64
115
|
case "capsule":
|
|
65
|
-
return new CapsuleJoiner();
|
|
116
|
+
return new CapsuleJoiner(CAPSULE_GLYPHS[charset]);
|
|
66
117
|
case "plain":
|
|
67
118
|
return new PlainJoiner(separator !== undefined ? { separator } : {});
|
|
68
119
|
case "powerline":
|
|
69
|
-
return new PowerlineJoiner();
|
|
120
|
+
return new PowerlineJoiner(POWERLINE_GLYPHS[charset]);
|
|
70
121
|
default: {
|
|
71
122
|
const _exhaustive: never = style;
|
|
72
123
|
return _exhaustive;
|
|
@@ -84,10 +135,13 @@ function pickJoiner(style: StripStyle, separator?: string): Joiner {
|
|
|
84
135
|
// [LAW:dataflow-not-control-flow] / [LAW:types-are-the-program] Total over
|
|
85
136
|
// StripStyle — the `never` default makes adding a STRIP_STYLES member a compile
|
|
86
137
|
// error here until its chrome is declared, the same guard pickJoiner carries. The
|
|
87
|
-
// numbers are the
|
|
88
|
-
// trailing separator (
|
|
89
|
-
//
|
|
90
|
-
//
|
|
138
|
+
// numbers are the cap glyphs pickJoiner constructs: powerline appends ONE
|
|
139
|
+
// trailing separator (1 col); capsule brackets BOTH edges (2 cols); plain has no
|
|
140
|
+
// caps. Charset does NOT change these — both glyph vocabularies use
|
|
141
|
+
// single-column caps by construction (see the glyph tables above), which is why
|
|
142
|
+
// this stays total over StripStyle alone. test/picker-pagination.test.ts
|
|
143
|
+
// measures the real rendered chrome against these for every style × charset so
|
|
144
|
+
// the declaration cannot drift from rich-js or from the ascii glyph choice.
|
|
91
145
|
export function stripChromeCols(style: StripStyle): number {
|
|
92
146
|
switch (style) {
|
|
93
147
|
case "powerline":
|
|
@@ -103,15 +157,15 @@ export function stripChromeCols(style: StripStyle): number {
|
|
|
103
157
|
}
|
|
104
158
|
}
|
|
105
159
|
|
|
106
|
-
function toCell(seg: RenderedSegmentLike): RichText {
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
160
|
+
function toCell(seg: RenderedSegmentLike, padding: number): RichText {
|
|
161
|
+
// [LAW:one-source-of-truth] Intra-cell padding derives from the one resolved
|
|
162
|
+
// globals.padding value on BuildLineOptions — the joiners sit between cells;
|
|
163
|
+
// padding sits inside, inheriting the cell's wrapping style (bg fill).
|
|
110
164
|
const style = new Style({
|
|
111
165
|
bgcolor: seg.bgHex || undefined,
|
|
112
166
|
color: seg.fgHex || undefined,
|
|
113
167
|
});
|
|
114
|
-
return new RichText(
|
|
168
|
+
return new RichText(seg.text, { style, end: "", noWrap: true }).pad(padding);
|
|
115
169
|
}
|
|
116
170
|
|
|
117
171
|
/**
|
|
@@ -132,7 +186,7 @@ export function renderStripCells(
|
|
|
132
186
|
options: BuildLineOptions,
|
|
133
187
|
): string {
|
|
134
188
|
if (cells.length === 0) return "";
|
|
135
|
-
const joiner = pickJoiner(options.style, options.separator);
|
|
189
|
+
const joiner = pickJoiner(options.style, options.charset, options.separator);
|
|
136
190
|
if (options.wrap && Number.isFinite(options.width)) {
|
|
137
191
|
const flex = new FlexStrip([...cells], { joiner });
|
|
138
192
|
const out = renderToString(flex, {
|
|
@@ -156,5 +210,8 @@ export function buildLineStrip(
|
|
|
156
210
|
segments: readonly RenderedSegmentLike[],
|
|
157
211
|
options: BuildLineOptions,
|
|
158
212
|
): string {
|
|
159
|
-
return renderStripCells(
|
|
213
|
+
return renderStripCells(
|
|
214
|
+
segments.map((seg) => toCell(seg, options.padding)),
|
|
215
|
+
options,
|
|
216
|
+
);
|
|
160
217
|
}
|
|
@@ -30,6 +30,15 @@ export interface SegmentLayoutOptions {
|
|
|
30
30
|
justify: JustifyMode;
|
|
31
31
|
/** Overflow strategy when content exceeds a fixed width. Ignored when "auto". */
|
|
32
32
|
truncate: TruncateMode;
|
|
33
|
+
/**
|
|
34
|
+
* Spaces synthesized inside the collapsed cell on each side (the resolved
|
|
35
|
+
* globals.padding). [LAW:types-are-the-program] Required so every layout
|
|
36
|
+
* site states the value — the walk threads the one render-wide resolution;
|
|
37
|
+
* there is no per-site re-default. Applied BEFORE width sizing, so padding
|
|
38
|
+
* sits inside a fixed `width` (the legacy intra-cell semantics: the joiners
|
|
39
|
+
* sit between cells; padding sits inside the bg fill).
|
|
40
|
+
*/
|
|
41
|
+
padding: number;
|
|
33
42
|
/** Glyph inserted at the overflow cut point. Default "…". */
|
|
34
43
|
truncateMarker?: string;
|
|
35
44
|
/**
|
|
@@ -95,11 +104,23 @@ export function applySegmentLayout(
|
|
|
95
104
|
cells: readonly RichText[],
|
|
96
105
|
options: SegmentLayoutOptions,
|
|
97
106
|
): RichText[] {
|
|
98
|
-
const {
|
|
107
|
+
const {
|
|
108
|
+
width,
|
|
109
|
+
justify,
|
|
110
|
+
truncate,
|
|
111
|
+
truncateMarker = "…",
|
|
112
|
+
baseStyle,
|
|
113
|
+
padding,
|
|
114
|
+
} = options;
|
|
99
115
|
|
|
100
116
|
if (cells.length === 0) return [];
|
|
101
117
|
|
|
102
|
-
|
|
118
|
+
// [LAW:one-source-of-truth] The DSL path's ONE intra-cell padding
|
|
119
|
+
// application — the same rich-js pad primitive toCell (render/strip.ts)
|
|
120
|
+
// uses on the legacy shape, threading the same resolved globals.padding.
|
|
121
|
+
// pad() shifts spans, so OSC-8 link regions survive; the spaces inherit
|
|
122
|
+
// the cell's wrapping style, so the segment bg is continuous.
|
|
123
|
+
const cell = collapseToCell(cells, baseStyle).pad(padding);
|
|
103
124
|
if (width === "auto") return [cell];
|
|
104
125
|
|
|
105
126
|
if (cell.cellLength > width) {
|
package/src/themes/policy.ts
CHANGED
|
@@ -79,3 +79,20 @@ export function effectiveStripStyle(
|
|
|
79
79
|
const chosen = sessionStyle ?? globalsStyle ?? "powerline";
|
|
80
80
|
return isStripStyle(chosen) ? chosen : "powerline";
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
// --- Joiner charset identifiers ---
|
|
84
|
+
|
|
85
|
+
// [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
|
|
86
|
+
// of glyph vocabularies the strip joiners can render with (the legacy
|
|
87
|
+
// display.charset). Same species as STRIP_STYLES — a closed render-vocabulary
|
|
88
|
+
// enum hosted in this leaf policy module so the config loader (validation +
|
|
89
|
+
// JSON-schema emit) and the render layer (glyph dispatch) both derive from one
|
|
90
|
+
// literal without a config↔render cycle [LAW:one-way-deps]. "ascii" swaps the
|
|
91
|
+
// powerline-private-use cap glyphs (U+E0Bx — tofu without a Nerd Font) for
|
|
92
|
+
// plain-ASCII equivalents; it is orthogonal to StripStyle: style picks the
|
|
93
|
+
// joiner SHAPE, charset picks the glyph VALUES fed to it.
|
|
94
|
+
// [config-only] Unlike STRIP_STYLES there is no SessionState/click half, so no
|
|
95
|
+
// narrowing guard or effective* resolver — the config global over the default
|
|
96
|
+
// is the whole resolution.
|
|
97
|
+
export const CHARSETS = ["unicode", "ascii"] as const;
|
|
98
|
+
export type Charset = (typeof CHARSETS)[number];
|