@promptctl/cc-candybar 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/cc-candybar",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -91,10 +91,10 @@
91
91
  "mobx": "^6.15.0"
92
92
  },
93
93
  "optionalDependencies": {
94
- "@promptctl/cc-candybar-darwin-arm64": "1.10.0",
95
- "@promptctl/cc-candybar-darwin-x64": "1.10.0",
96
- "@promptctl/cc-candybar-linux-x64": "1.10.0",
97
- "@promptctl/cc-candybar-linux-arm64": "1.10.0"
94
+ "@promptctl/cc-candybar-darwin-arm64": "1.11.0",
95
+ "@promptctl/cc-candybar-darwin-x64": "1.11.0",
96
+ "@promptctl/cc-candybar-linux-x64": "1.11.0",
97
+ "@promptctl/cc-candybar-linux-arm64": "1.11.0"
98
98
  },
99
99
  "pnpm": {
100
100
  "supportedArchitectures": {
@@ -40,6 +40,12 @@
40
40
  "type": "integer",
41
41
  "minimum": 0,
42
42
  "maximum": 16
43
+ },
44
+ "charset": {
45
+ "enum": [
46
+ "unicode",
47
+ "ascii"
48
+ ]
43
49
  }
44
50
  },
45
51
  "additionalProperties": false
@@ -14,7 +14,7 @@
14
14
  // references it here. The dependency is one-way (this file → action.ts), never
15
15
  // the reverse, so that shape can be lifted out without a cycle.
16
16
  import type { ActionDecl } from "./action.js";
17
- import type { StripStyle } from "../themes/policy.js";
17
+ import type { Charset, StripStyle } from "../themes/policy.js";
18
18
 
19
19
  // [LAW:types-are-the-program] Three stages, three names.
20
20
  //
@@ -204,6 +204,15 @@ export interface Globals {
204
204
  // [config-only] The daemon resolves `globals.padding ?? 1` into
205
205
  // renderOpts.padding; no SessionState/click half.
206
206
  readonly padding?: number;
207
+
208
+ // The legacy display.charset knob: which glyph vocabulary the strip joiners
209
+ // render with. Default "unicode" (current behavior — rich-js's powerline
210
+ // caps, U+E0Bx). "ascii" swaps the caps for single-column ASCII glyphs so
211
+ // terminals/fonts without powerline glyphs render cleanly instead of tofu.
212
+ // Orthogonal to `style`: style picks the joiner shape, charset the glyphs.
213
+ // [config-only] The daemon resolves `globals.charset ?? "unicode"` into
214
+ // renderOpts.charset; no SessionState/click half.
215
+ readonly charset?: Charset;
207
216
  }
208
217
 
209
218
  // [LAW:one-type-per-behavior] One discriminated union covers every source
@@ -4,7 +4,7 @@
4
4
  // add a key to GLOBALS_SCHEMA and Globals; the engine does the rest.
5
5
 
6
6
  import { type Globals } from "../dsl-types.js";
7
- import { STRIP_STYLES } from "../../themes/policy.js";
7
+ import { CHARSETS, STRIP_STYLES } from "../../themes/policy.js";
8
8
  import {
9
9
  optionalBooleanSpec,
10
10
  optionalEnumSpec,
@@ -37,6 +37,10 @@ const GLOBALS_SCHEMA: RecordSchema<Globals> = {
37
37
  // [LAW:no-silent-failure] — an absurd value is a loud load error, not a
38
38
  // silently-huge render.
39
39
  padding: optionalIntSpec({ min: 0, max: 16 }),
40
+ // [LAW:types-are-the-program] Closed enum like `style`: the joiner glyph
41
+ // vocabularies pickJoiner can render — validates by membership, emits a
42
+ // JSON-Schema `enum` from the same CHARSETS literal.
43
+ charset: optionalEnumSpec(CHARSETS),
40
44
  },
41
45
  };
42
46
 
@@ -46,10 +46,12 @@ import {
46
46
  } from "../themes/index.js";
47
47
  import {
48
48
  renderStripCells,
49
+ DEFAULT_CHARSET,
49
50
  DEFAULT_PADDING,
50
51
  DEFAULT_TERMINAL_WIDTH,
51
52
  DEFAULT_WRAP,
52
53
  type BuildLineOptions,
54
+ type Charset,
53
55
  } from "../render/strip.js";
54
56
  import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
55
57
  import type { RichText } from "@promptctl/rich-js";
@@ -780,6 +782,12 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
780
782
  // reserve) derives from. [LAW:one-source-of-truth]
781
783
  renderOpts.padding =
782
784
  entry.state.config.globals.padding ?? DEFAULT_PADDING;
785
+ // [config-only] globals.charset, same shape as autoWrap/padding: the
786
+ // config global over the base floor is the whole resolution — the ONE
787
+ // home of the glyph-vocabulary choice pickJoiner derives from.
788
+ // [LAW:one-source-of-truth]
789
+ renderOpts.charset =
790
+ entry.state.config.globals.charset ?? DEFAULT_CHARSET;
783
791
  // [LAW:single-enforcer] renderDsl internally calls
784
792
  // `registry.applyInput(payload)` as its first step (see step 1 in
785
793
  // src/dsl/render.ts). The daemon must not pre-apply — doing so
@@ -884,7 +892,10 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
884
892
  compiled: dbgEntry.compiled,
885
893
  lastRenderBySegment:
886
894
  req.what === "segments"
887
- ? serializeSegmentCells(dbgEntry.lastRenderCellsBySegment)
895
+ ? serializeSegmentCells(
896
+ dbgEntry.lastRenderCellsBySegment,
897
+ dbgEntry.config.globals.charset ?? DEFAULT_CHARSET,
898
+ )
888
899
  : EMPTY_RENDER_MAP,
889
900
  };
890
901
  return stay({ ok: true, debug: buildDebugSnapshot(req.what, dbgState) });
@@ -1072,6 +1083,7 @@ const RENDER_OPTS_BASE = {
1072
1083
  colorCompatibility: "truecolor" as const,
1073
1084
  wrap: DEFAULT_WRAP,
1074
1085
  padding: DEFAULT_PADDING,
1086
+ charset: DEFAULT_CHARSET,
1075
1087
  };
1076
1088
  const DEBUG_RENDER_OPTS: BuildLineOptions = {
1077
1089
  ...RENDER_OPTS_BASE,
@@ -1083,12 +1095,22 @@ const DEBUG_RENDER_OPTS: BuildLineOptions = {
1083
1095
  // the DaemonDslState type requires the field.
1084
1096
  const EMPTY_RENDER_MAP = new Map<string, string>();
1085
1097
 
1098
+ // [LAW:one-source-of-truth] The joiner glyph vocabulary is a serialization-time
1099
+ // choice, and charset is config-only (no SessionState half) — so the faithful
1100
+ // value is fully derivable from the sampled entry's config, unlike style, whose
1101
+ // live session-over-config resolution needs a session a debug request doesn't
1102
+ // carry. The caller threads the entry-resolved charset; this serializer never
1103
+ // re-defaults it.
1086
1104
  function serializeSegmentCells(
1087
1105
  cells: ReadonlyMap<string, readonly RichText[]>,
1106
+ charset: Charset,
1088
1107
  ): Map<string, string> {
1089
1108
  const out = new Map<string, string>();
1090
1109
  for (const [name, segCells] of cells) {
1091
- out.set(name, renderStripCells(segCells, DEBUG_RENDER_OPTS));
1110
+ out.set(
1111
+ name,
1112
+ renderStripCells(segCells, { ...DEBUG_RENDER_OPTS, charset }),
1113
+ );
1092
1114
  }
1093
1115
  return out;
1094
1116
  }
package/src/demo/dsl.ts CHANGED
@@ -32,6 +32,7 @@ import { listResolvablePaletteNames } from "../themes/policy.js";
32
32
  import { effectiveThemeName, resolverForThemeName } from "../themes/index.js";
33
33
  import { registerDslConfig, renderDsl } from "../dsl/render.js";
34
34
  import {
35
+ DEFAULT_CHARSET,
35
36
  DEFAULT_PADDING,
36
37
  DEFAULT_TERMINAL_WIDTH,
37
38
  DEFAULT_WRAP,
@@ -118,6 +119,7 @@ try {
118
119
  // default-on floor.
119
120
  wrap: config.globals.autoWrap ?? DEFAULT_WRAP,
120
121
  padding: config.globals.padding ?? DEFAULT_PADDING,
122
+ charset: config.globals.charset ?? DEFAULT_CHARSET,
121
123
  },
122
124
  );
123
125
  process.stdout.write(` ${line}\n`);
@@ -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
@@ -43,6 +45,12 @@ export const DEFAULT_WRAP = true;
43
45
  // (`globals.padding ?? DEFAULT_PADDING`) derives from this constant.
44
46
  export const DEFAULT_PADDING = 1;
45
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
+
46
54
  export interface BuildLineOptions {
47
55
  style: StripStyle;
48
56
  colorCompatibility: ColorSystemSpec;
@@ -65,20 +73,51 @@ export interface BuildLineOptions {
65
73
  // site states the resolved value; the cell builders derive from it and
66
74
  // never re-default.
67
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;
68
81
  }
69
82
 
70
- 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 {
71
106
  // [LAW:dataflow-not-control-flow] joiner choice is data-driven; one arm per
72
- // 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`
73
112
  // default makes adding a STRIP_STYLES member a compile error here until it
74
113
  // gets a joiner, so the picker's domain can never offer an unrenderable shape.
75
114
  switch (style) {
76
115
  case "capsule":
77
- return new CapsuleJoiner();
116
+ return new CapsuleJoiner(CAPSULE_GLYPHS[charset]);
78
117
  case "plain":
79
118
  return new PlainJoiner(separator !== undefined ? { separator } : {});
80
119
  case "powerline":
81
- return new PowerlineJoiner();
120
+ return new PowerlineJoiner(POWERLINE_GLYPHS[charset]);
82
121
  default: {
83
122
  const _exhaustive: never = style;
84
123
  return _exhaustive;
@@ -96,10 +135,13 @@ function pickJoiner(style: StripStyle, separator?: string): Joiner {
96
135
  // [LAW:dataflow-not-control-flow] / [LAW:types-are-the-program] Total over
97
136
  // StripStyle — the `never` default makes adding a STRIP_STYLES member a compile
98
137
  // error here until its chrome is declared, the same guard pickJoiner carries. The
99
- // numbers are the default cap glyphs pickJoiner constructs: powerline appends ONE
100
- // trailing separator (U+E0B0, 1 col); capsule brackets BOTH edges (U+E0B6 +
101
- // U+E0B4, 2 cols); plain has no caps. test/picker-pagination.test.ts measures the
102
- // 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.
103
145
  export function stripChromeCols(style: StripStyle): number {
104
146
  switch (style) {
105
147
  case "powerline":
@@ -144,7 +186,7 @@ export function renderStripCells(
144
186
  options: BuildLineOptions,
145
187
  ): string {
146
188
  if (cells.length === 0) return "";
147
- const joiner = pickJoiner(options.style, options.separator);
189
+ const joiner = pickJoiner(options.style, options.charset, options.separator);
148
190
  if (options.wrap && Number.isFinite(options.width)) {
149
191
  const flex = new FlexStrip([...cells], { joiner });
150
192
  const out = renderToString(flex, {
@@ -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];