@promptctl/cc-candybar 1.8.2 → 1.8.4
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 +65 -65
- package/package.json +5 -5
- package/src/config/default-dsl-config.ts +34 -38
- package/src/dsl/render.ts +17 -2
- package/src/render/action.ts +12 -1
- package/src/render/menu.ts +64 -21
- package/src/render/picker.ts +18 -1
- package/src/render/strip.ts +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
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.8.
|
|
95
|
-
"@promptctl/cc-candybar-darwin-x64": "1.8.
|
|
96
|
-
"@promptctl/cc-candybar-linux-x64": "1.8.
|
|
97
|
-
"@promptctl/cc-candybar-linux-arm64": "1.8.
|
|
94
|
+
"@promptctl/cc-candybar-darwin-arm64": "1.8.4",
|
|
95
|
+
"@promptctl/cc-candybar-darwin-x64": "1.8.4",
|
|
96
|
+
"@promptctl/cc-candybar-linux-x64": "1.8.4",
|
|
97
|
+
"@promptctl/cc-candybar-linux-arm64": "1.8.4"
|
|
98
98
|
},
|
|
99
99
|
"pnpm": {
|
|
100
100
|
"supportedArchitectures": {
|
|
@@ -496,10 +496,12 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
496
496
|
// and the render's read are one value. Empty default ⇒ the daemon's
|
|
497
497
|
// "powerline" floor is in effect and styleControl shows "(default)".
|
|
498
498
|
activeStyle: { kind: "state", key: "style", default: "" },
|
|
499
|
-
// The style
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
|
|
499
|
+
// The style picker's page index. Open/closed is the synthesized menus.* key
|
|
500
|
+
// the styleControl's {{ menu }} owns — NOT this var, which is purely the page
|
|
501
|
+
// the picker paginates by (default page 0; the disclosure resets it to 0 on
|
|
502
|
+
// every toggle, so a reopened menu never lands on a stale page). The stylePage
|
|
503
|
+
// action below declares the int gate this var reads back.
|
|
504
|
+
stylePage: { kind: "state", key: "style-page", default: "0" },
|
|
503
505
|
},
|
|
504
506
|
|
|
505
507
|
// ─── Segments ──────────────────────────────────────────────────────────────
|
|
@@ -736,26 +738,22 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
736
738
|
" .metrics.sessionDuration .metrics.messageCount" +
|
|
737
739
|
" .metrics.linesAdded .metrics.linesRemoved }}",
|
|
738
740
|
},
|
|
739
|
-
// Style control — the powerline-shape switcher
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
// row. [LAW:
|
|
744
|
-
//
|
|
741
|
+
// Style control — the powerline-shape switcher. A self-contained {{ menu }}
|
|
742
|
+
// disclosure: the ✦ glyph + current-style label is the REPRESENTATION, and the
|
|
743
|
+
// ▸/▾ disclosure (whose identity is DERIVED from this segment + the applyStyle
|
|
744
|
+
// action, see menu-keys.ts) toggles the picker body, which DROPS full-width
|
|
745
|
+
// onto the line below this row when open. [LAW:one-type-per-behavior] "a menu
|
|
746
|
+
// that opens and closes" is one behavior the substrate already expresses — no
|
|
747
|
+
// bespoke open-action + page-cursor-as-open-state + when-gated reveal row.
|
|
748
|
+
// paged omitted ⇒ false ⇒ one full-width page (the 3 powerline shapes are a
|
|
749
|
+
// small domain; FlexStrip soft-wraps if needed). closeOnPick omitted ⇒ false
|
|
750
|
+
// ⇒ stay-open, so shapes can be tried in a row; the ▾ collapses.
|
|
751
|
+
// [LAW:dataflow-not-control-flow] No display state from the provider — the
|
|
752
|
+
// label is the one "style" value the click writes and the render reads.
|
|
745
753
|
styleControl: {
|
|
746
754
|
template:
|
|
747
755
|
"✦ {{ if .activeStyle }}{{ .activeStyle }}{{ else }}(default){{ end }} " +
|
|
748
|
-
'{{
|
|
749
|
-
bg: "surface",
|
|
750
|
-
fg: "foreground",
|
|
751
|
-
},
|
|
752
|
-
// The expanded style picker: one full-width page (paged=false) over the
|
|
753
|
-
// `applyStyle` option domain — the 3 powerline shapes. closeOnPick defaults
|
|
754
|
-
// false (omitted here), so a pick reshapes the bar live and LEAVES THE ROW
|
|
755
|
-
// OPEN — shapes can be tried in a row; the ✕ affordance closes. The active
|
|
756
|
-
// shape is marked by the picker helper.
|
|
757
|
-
stylePicker: {
|
|
758
|
-
template: '{{ picker "applyStyle" "stylePage" }}',
|
|
756
|
+
'{{ menu "applyStyle" "stylePage" }}',
|
|
759
757
|
bg: "surface",
|
|
760
758
|
fg: "foreground",
|
|
761
759
|
},
|
|
@@ -764,10 +762,11 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
764
762
|
// Default layout — the canonical LayoutNode tree (`satisfies DslConfig`
|
|
765
763
|
// requires the lowered form here; the terse Option-A `{ h/v/seg }` grammar is
|
|
766
764
|
// the loader's authoring surface for user JSON, not this typed literal).
|
|
767
|
-
// [LAW:dataflow-not-control-flow]
|
|
768
|
-
//
|
|
769
|
-
// row
|
|
770
|
-
// The picker
|
|
765
|
+
// [LAW:dataflow-not-control-flow] One always-on control row. The styleControl's
|
|
766
|
+
// {{ menu }} disclosure drops its picker body onto the line directly below this
|
|
767
|
+
// row when open — openness is the value of the derived menus.* key, NOT a when-
|
|
768
|
+
// gated reveal row. The picker draws the ✕/←/→ affordances from the page + term
|
|
769
|
+
// width; the vertical container stacks the dropped body under row 0.
|
|
771
770
|
root: {
|
|
772
771
|
kind: "container",
|
|
773
772
|
direction: "vertical",
|
|
@@ -786,11 +785,6 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
786
785
|
{ kind: "segment", name: "styleControl" },
|
|
787
786
|
],
|
|
788
787
|
},
|
|
789
|
-
{
|
|
790
|
-
kind: "segment",
|
|
791
|
-
name: "stylePicker",
|
|
792
|
-
when: "{{ ge (int .stylePage) 0 }}",
|
|
793
|
-
},
|
|
794
788
|
],
|
|
795
789
|
},
|
|
796
790
|
|
|
@@ -814,14 +808,16 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
814
808
|
openProject: { open: "{{ .project_dir }}" },
|
|
815
809
|
openTranscript: { open: "{{ .transcript_path }}" },
|
|
816
810
|
|
|
817
|
-
// [LAW:locality-or-seam] The style
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
|
|
811
|
+
// [LAW:locality-or-seam] The style menu's behaviors, decoupled by NAME from the
|
|
812
|
+
// styleControl {{ menu }} above. The disclosure's open-state toggle + its
|
|
813
|
+
// backing state var are SYNTHESIZED by the menu pass (under the reserved
|
|
814
|
+
// menus.* namespace) — no hand-authored open/close action. These two are the
|
|
815
|
+
// picker body's effects, both gated by derivation (deriveActionValidators):
|
|
816
|
+
// applyStyle writes the chosen shape, gated to the STRIP_STYLES allow-list
|
|
817
|
+
// because its value source is `from: "styles"`; stylePage is the page cursor's
|
|
818
|
+
// unbounded int gate (←/→ navigation + the disclosure's page-0 reset). The
|
|
819
|
+
// rendered click and the wire gate share that one source — a template cannot
|
|
820
|
+
// smuggle an un-gated style write.
|
|
825
821
|
applyStyle: { set: "style", from: "styles" },
|
|
826
822
|
stylePage: { set: "style-page", int: true },
|
|
827
823
|
},
|
package/src/dsl/render.ts
CHANGED
|
@@ -282,6 +282,10 @@ export function registerDslConfig(
|
|
|
282
282
|
const actionRuntime: ActionRuntime = {
|
|
283
283
|
store: registry.variableStore,
|
|
284
284
|
compiled: new Map(),
|
|
285
|
+
// [LAW:types-are-the-program] Always present — renderDsl republishes the live
|
|
286
|
+
// style each render; "powerline" is the registration-time default so a
|
|
287
|
+
// compile-only path (no render) still has a valid value.
|
|
288
|
+
stripStyle: "powerline",
|
|
285
289
|
};
|
|
286
290
|
// [LAW:one-way-deps] Inject action + picker feature funcs as data — the engine
|
|
287
291
|
// stays generic. The picker shares the ACTION runtime (it resolves its
|
|
@@ -517,11 +521,22 @@ export function renderDsl(
|
|
|
517
521
|
perSegmentSink?: Map<string, readonly RichText[]>,
|
|
518
522
|
): string {
|
|
519
523
|
// [LAW:one-source-of-truth] Inject the usable width as `term.cols` from the
|
|
520
|
-
// SAME opts.width the strip wraps to (below), so a width-paginated widget
|
|
521
|
-
//
|
|
524
|
+
// SAME opts.width the strip wraps to (below), so a width-paginated widget reads
|
|
525
|
+
// the exact wrap width — never a cached or independently-measured copy. This is
|
|
526
|
+
// the RAW usable width (terminal cols minus the Claude-Code reserve), the honest
|
|
527
|
+
// meaning every template — incl. user configs reading `.term.cols` — expects.
|
|
528
|
+
// The picker's strip-chrome reservation is NOT folded in here: that is a
|
|
529
|
+
// picker-local concern (the strip's end-caps wrap the picker's row, not every
|
|
530
|
+
// segment), applied at the pagination seam in renderPicker. [LAW:locality-or-seam]
|
|
522
531
|
// Spreading a non-object payload yields no keys (compile-only callers), so the
|
|
523
532
|
// width is set regardless without a trust-boundary guard.
|
|
524
533
|
registry.applyInput({ ...(payload as object), term: { cols: opts.width } });
|
|
534
|
+
// [LAW:single-enforcer] Publish the render's strip style onto the shared action
|
|
535
|
+
// runtime so the picker can reserve the joiner's end-cap chrome at its
|
|
536
|
+
// pagination seam (the menu body renders through the same renderPicker). Set
|
|
537
|
+
// once per render here — the same one-owner, per-render-mutation idiom as the
|
|
538
|
+
// menu placement cursor below. [LAW:no-ambient-temporal-coupling]
|
|
539
|
+
compiled.menuRuntime.action.stripStyle = opts.style;
|
|
525
540
|
|
|
526
541
|
const scope = buildScope(store);
|
|
527
542
|
// [LAW:one-source-of-truth] hueStep is a value in the store like every other
|
package/src/render/action.ts
CHANGED
|
@@ -26,7 +26,11 @@ import type { VariableStore } from "../var-system/store.js";
|
|
|
26
26
|
import { toString as varToString } from "../var-system/types.js";
|
|
27
27
|
import { buildScope } from "../template-engine/scope.js";
|
|
28
28
|
import type { ActionDecl, OptionSource } from "../config/action.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
listResolvablePaletteNames,
|
|
31
|
+
STRIP_STYLES,
|
|
32
|
+
type StripStyle,
|
|
33
|
+
} from "../themes/policy.js";
|
|
30
34
|
import {
|
|
31
35
|
effectsUrl,
|
|
32
36
|
VERB_COPY,
|
|
@@ -123,6 +127,13 @@ export interface ActionRuntime {
|
|
|
123
127
|
// values from the same store the renderer reads.
|
|
124
128
|
store: VariableStore;
|
|
125
129
|
compiled: CompiledActions;
|
|
130
|
+
// [LAW:locality-or-seam] The current render's strip style, published per render
|
|
131
|
+
// by renderDsl. The picker reads it to reserve the joiner's end-cap chrome at
|
|
132
|
+
// its pagination seam — the one place that needs strip geometry, kept off the
|
|
133
|
+
// shared `term.cols` budget. Defaulted at registration; renders are sequential
|
|
134
|
+
// and synchronous, so the per-render write never leaks across renders.
|
|
135
|
+
// [LAW:no-ambient-temporal-coupling]
|
|
136
|
+
stripStyle: StripStyle;
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
// ─── Compilation ───────────────────────────────────────────────────────────────
|
package/src/render/menu.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// [LAW:locality-or-seam] The runtime half of the `{{ menu }}` seam — sibling to
|
|
2
2
|
// `{{ action }}`/`{{ picker }}`. A menu is a self-contained disclosure: an inline
|
|
3
3
|
// glyph that toggles open/closed, and (when open) its body — a picker grid —
|
|
4
|
-
// that DROPS onto the line(s) below the enclosing row.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// that DROPS onto the line(s) below the enclosing row. The body is the one picker
|
|
5
|
+
// renderer (`renderPicker`); the glyph is a coupled set-state the menu composes
|
|
6
|
+
// directly (like the picker's closeOnPick) — it toggles the open-state AND resets
|
|
7
|
+
// the page cursor in one atomic batch, gated by the synthesized cycle action.
|
|
8
8
|
//
|
|
9
9
|
// [LAW:effects-at-boundaries] The helper is a PURE function of its inputs (the
|
|
10
10
|
// walk-published placement + the live store): it computes the inline glyph and,
|
|
@@ -36,13 +36,14 @@
|
|
|
36
36
|
import type { RichText } from "@promptctl/rich-js";
|
|
37
37
|
import type { FuncMap } from "@promptctl/go-template-js";
|
|
38
38
|
import {
|
|
39
|
+
MENU_CLOSED,
|
|
39
40
|
MENU_GLYPH_CLOSED,
|
|
40
41
|
MENU_GLYPH_OPEN,
|
|
41
|
-
menuActionName,
|
|
42
42
|
menuMember,
|
|
43
43
|
menuStateKey,
|
|
44
44
|
} from "../config/menu-keys.js";
|
|
45
|
-
import {
|
|
45
|
+
import { effectsUrl, VERB_SET_STATE } from "../click/wire.js";
|
|
46
|
+
import { linkFragment, readVar, type ActionRuntime } from "./action.js";
|
|
46
47
|
import { renderPicker } from "./picker.js";
|
|
47
48
|
|
|
48
49
|
// [LAW:types-are-the-program] One menu placement: the structural fact a context-
|
|
@@ -82,6 +83,22 @@ export function collectMenuDrops(
|
|
|
82
83
|
return fragments.flatMap((f) => (f as GlyphWithDrop)[MENU_DROP] ?? []);
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// [LAW:single-enforcer] The page cursor's SessionState key, resolved from the page
|
|
87
|
+
// ACTION name the menu binds (its second arg). renderPicker proves pageName is a
|
|
88
|
+
// set-int when it builds the body, but the disclosure must reset the page even
|
|
89
|
+
// while CLOSED (no body is built then), so it resolves the key here too — the SAME
|
|
90
|
+
// set-int action, one source. A non-int page arg is an author error surfaced
|
|
91
|
+
// loudly (composeWithDiagnostics shows it), never a silent skipped reset.
|
|
92
|
+
function pageKeyOf(action: ActionRuntime, pageName: string): string {
|
|
93
|
+
const page = action.compiled.get(pageName);
|
|
94
|
+
if (!page || page.kind !== "set-int") {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`{{ menu }} page action "${pageName}" must be an int action ({ set, int: true })`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return page.key;
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
// Realize a `{{ menu }}` against the live placement + state: return its inline
|
|
86
103
|
// glyph, carrying the (open) body as out-of-band metadata for the boundary.
|
|
87
104
|
function renderMenu(
|
|
@@ -106,25 +123,51 @@ function renderMenu(
|
|
|
106
123
|
const stateKey = menuStateKey(placement.segName, applyName, sharedKey);
|
|
107
124
|
const member = menuMember(applyName);
|
|
108
125
|
|
|
109
|
-
// [LAW:single-enforcer] The disclosure glyph IS the synthesized cycle action —
|
|
110
|
-
// displays bound one-per-member (closed ▸ / open ▾), the current member's glyph
|
|
111
|
-
// renders, the click writes the successor. Same toggle as group sugar.
|
|
112
|
-
const glyph = renderAction(
|
|
113
|
-
menuActionName(stateKey, member),
|
|
114
|
-
[MENU_GLYPH_CLOSED, MENU_GLYPH_OPEN],
|
|
115
|
-
action,
|
|
116
|
-
);
|
|
117
|
-
|
|
118
126
|
// [LAW:dataflow-not-control-flow] Open ⇔ the state key holds this menu's member.
|
|
119
|
-
//
|
|
120
|
-
// closed —
|
|
121
|
-
//
|
|
122
|
-
//
|
|
127
|
+
// A foreign value (an accordion sibling's member under a shared key) reads as
|
|
128
|
+
// closed here — exactly the binary [closed, member] cycle the synthesized action
|
|
129
|
+
// gates. This ONE read drives both the glyph and the body, so what the glyph
|
|
130
|
+
// promises and what drops below cannot disagree.
|
|
123
131
|
const open = readVar(action.store, stateKey) === member;
|
|
124
|
-
|
|
132
|
+
|
|
133
|
+
// [LAW:one-source-of-truth] / [LAW:locality-or-seam] The disclosure click is ONE
|
|
134
|
+
// atomic set-state that keeps the two split keys coherent: it toggles the open-
|
|
135
|
+
// state (the binary cycle — successor is closed when open, the member when
|
|
136
|
+
// closed) AND resets the page cursor to page 0, in one batch. So a reopened menu
|
|
137
|
+
// is never stranded on a stale page left by ←/→ before the last close. This
|
|
138
|
+
// mirrors the picker's closeOnPick page-reset fold: the picker builds its set-
|
|
139
|
+
// state URLs directly (not via renderAction) so it can couple two writes; the
|
|
140
|
+
// menu — the one part that knows BOTH the open-state key and the page key
|
|
141
|
+
// [LAW:decomposition] — does the same. The synthesized cycle action stays the
|
|
142
|
+
// GATE source (deriveActionValidators); both keys are independently gated, so the
|
|
143
|
+
// coupled batch passes the same wire gate every click does [LAW:single-enforcer].
|
|
144
|
+
const sessionId = readVar(action.store, "session.id");
|
|
145
|
+
const successor = open ? MENU_CLOSED : member;
|
|
146
|
+
const glyph = linkFragment(
|
|
147
|
+
open ? MENU_GLYPH_OPEN : MENU_GLYPH_CLOSED,
|
|
148
|
+
effectsUrl([
|
|
149
|
+
{
|
|
150
|
+
verb: VERB_SET_STATE,
|
|
151
|
+
args: [
|
|
152
|
+
sessionId,
|
|
153
|
+
stateKey,
|
|
154
|
+
successor,
|
|
155
|
+
pageKeyOf(action, pageName),
|
|
156
|
+
"0",
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
]),
|
|
160
|
+
false,
|
|
161
|
+
) as GlyphWithDrop;
|
|
162
|
+
|
|
163
|
+
// [LAW:effects-at-boundaries] The body is a VALUE whose length carries open/
|
|
164
|
+
// closed — `[body]` open, `[]` closed — attached to the glyph the helper returns.
|
|
165
|
+
// No shared mutation: the boundary reads this metadata to place the body.
|
|
166
|
+
// (renderPicker is pure, so it is only built when open — skipping wasted
|
|
167
|
+
// computation, gating no effect.)
|
|
168
|
+
glyph[MENU_DROP] = open
|
|
125
169
|
? [renderPicker(applyName, pageName, closeOnPick, paged, action)]
|
|
126
170
|
: [];
|
|
127
|
-
(glyph as GlyphWithDrop)[MENU_DROP] = bodyLines;
|
|
128
171
|
return glyph;
|
|
129
172
|
}
|
|
130
173
|
|
package/src/render/picker.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { RichText } from "@promptctl/rich-js";
|
|
27
27
|
import type { FuncMap } from "@promptctl/go-template-js";
|
|
28
28
|
import { toNumber } from "../var-system/types.js";
|
|
29
|
+
import { stripChromeCols } from "./strip.js";
|
|
29
30
|
import { TERM_COLS_VAR } from "../config/dsl-types.js";
|
|
30
31
|
import { effectsUrl, VERB_SET_STATE } from "../click/wire.js";
|
|
31
32
|
import {
|
|
@@ -157,7 +158,23 @@ export function renderPicker(
|
|
|
157
158
|
// unconditionally is self-fulfilling (a run that fits with just ✕ could be
|
|
158
159
|
// forced to split, making arrows appear unnecessarily). In wrap mode
|
|
159
160
|
// (available = Infinity) paginate yields one page, so neither pass splits.
|
|
160
|
-
|
|
161
|
+
//
|
|
162
|
+
// [LAW:locality-or-seam] term.cols is the raw usable width the strip wraps to;
|
|
163
|
+
// the picker's row is itself a styled strip segment, so the joiner brackets it
|
|
164
|
+
// with end-caps (powerline's trailing separator, capsule's two caps) painted
|
|
165
|
+
// OUTSIDE that width. A page packed to the full term.cols is pushed past it by
|
|
166
|
+
// the caps — the maximally-packed middle pages overflowed and the terminal ate
|
|
167
|
+
// the trailing → (page 0 fit, page N did not). Reserve the chrome HERE, at the
|
|
168
|
+
// pagination seam, rather than shrinking the shared term.cols every template
|
|
169
|
+
// reads. stripChromeCols owns the per-style geometry; Infinity − chrome stays
|
|
170
|
+
// Infinity, so wrap mode is unaffected.
|
|
171
|
+
const available = paged
|
|
172
|
+
? Math.max(
|
|
173
|
+
1,
|
|
174
|
+
toNumber(store.read(TERM_COLS_VAR)) -
|
|
175
|
+
stripChromeCols(runtime.stripStyle),
|
|
176
|
+
)
|
|
177
|
+
: Infinity;
|
|
161
178
|
const closeReserve = cellWidth(PICKER_CLOSE) + 1;
|
|
162
179
|
const arrowReserve = cellWidth(PICKER_PREV) + 1 + cellWidth(PICKER_NEXT) + 1;
|
|
163
180
|
const firstPass = paginate(widths, available, closeReserve);
|
package/src/render/strip.ts
CHANGED
|
@@ -61,6 +61,35 @@ function pickJoiner(style: StripStyle, separator?: string): Joiner {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// [LAW:single-enforcer] Strip geometry has one owner — this module builds every
|
|
65
|
+
// joiner (pickJoiner) and so alone knows the structural chrome a styled row costs
|
|
66
|
+
// beyond its content: the joiner's end-caps, which FlexStrip paints OUTSIDE the
|
|
67
|
+
// width budget. A single full-width row's content can occupy only `width - chrome`
|
|
68
|
+
// before the caps push the line past `width`. Returned per style so a width-fit
|
|
69
|
+
// widget (the picker) can reserve it and never overflow the wrapped line.
|
|
70
|
+
//
|
|
71
|
+
// [LAW:dataflow-not-control-flow] / [LAW:types-are-the-program] Total over
|
|
72
|
+
// StripStyle — the `never` default makes adding a STRIP_STYLES member a compile
|
|
73
|
+
// error here until its chrome is declared, the same guard pickJoiner carries. The
|
|
74
|
+
// numbers are the default cap glyphs pickJoiner constructs: powerline appends ONE
|
|
75
|
+
// trailing separator (U+E0B0, 1 col); capsule brackets BOTH edges (U+E0B6 +
|
|
76
|
+
// U+E0B4, 2 cols); plain has no caps. test/picker-pagination.test.ts measures the
|
|
77
|
+
// real rendered chrome against these so the declaration cannot drift from rich-js.
|
|
78
|
+
export function stripChromeCols(style: StripStyle): number {
|
|
79
|
+
switch (style) {
|
|
80
|
+
case "powerline":
|
|
81
|
+
return 1;
|
|
82
|
+
case "capsule":
|
|
83
|
+
return 2;
|
|
84
|
+
case "plain":
|
|
85
|
+
return 0;
|
|
86
|
+
default: {
|
|
87
|
+
const _exhaustive: never = style;
|
|
88
|
+
return _exhaustive;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
function toCell(seg: RenderedSegmentLike): RichText {
|
|
65
94
|
// Padding mirrors the legacy buildLineFromSegments: one space on each side
|
|
66
95
|
// of the segment text. The joiners sit between cells; padding sits inside.
|