@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.
@@ -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 its value list live in
23
- // themes/policy.ts (the style-identifier policy module, importable by 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 it from the strip module.
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
- function pickJoiner(style: StripStyle, separator?: string): Joiner {
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. [LAW:types-are-the-program] Total over StripStyle the `never`
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 default cap glyphs pickJoiner constructs: powerline appends ONE
88
- // trailing separator (U+E0B0, 1 col); capsule brackets BOTH edges (U+E0B6 +
89
- // U+E0B4, 2 cols); plain has no caps. test/picker-pagination.test.ts measures the
90
- // real rendered chrome against these so the declaration cannot drift from rich-js.
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
- // Padding mirrors the legacy buildLineFromSegments: one space on each side
108
- // of the segment text. The joiners sit between cells; padding sits inside.
109
- const padded = ` ${seg.text} `;
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(padded, { style, end: "", noWrap: true });
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(segments.map(toCell), options);
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 { width, justify, truncate, truncateMarker = "…", baseStyle } = options;
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
- const cell = collapseToCell(cells, baseStyle);
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) {
@@ -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];