@promptctl/cc-candybar 1.40.0 → 1.41.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 +94 -92
- package/package.json +5 -5
- package/src/config/disclosure.ts +61 -0
- package/src/config/edit-chrome.ts +114 -21
- package/src/config/help.ts +151 -0
- package/src/config/loader/edit-mode.ts +15 -3
- package/src/config/loader/layout.ts +8 -10
- package/src/config/settings-menu.ts +107 -28
- package/src/help-text.ts +28 -2
|
@@ -49,8 +49,13 @@ import {
|
|
|
49
49
|
DISCLOSURE_GLYPH_CLOSED,
|
|
50
50
|
DISCLOSURE_GLYPH_OPEN,
|
|
51
51
|
disclosureCycleAction,
|
|
52
|
+
disclosureGate,
|
|
52
53
|
disclosureStateVar,
|
|
54
|
+
disclosureTrigger,
|
|
55
|
+
type DisclosureRef,
|
|
53
56
|
} from "./disclosure.js";
|
|
57
|
+
import { declareHelp, type HelpDisclosure } from "./help.js";
|
|
58
|
+
import { PERSIST_HELP } from "../help-text.js";
|
|
54
59
|
import {
|
|
55
60
|
EDIT_MODE_KEY,
|
|
56
61
|
EDIT_MODE_OPEN,
|
|
@@ -127,11 +132,34 @@ const CONFIG_SEG = `${SETTINGS_NS}config`;
|
|
|
127
132
|
// cannot silently write a durable default today — SessionState is per session.
|
|
128
133
|
const PERSIST_KEY = PERSIST_SEG;
|
|
129
134
|
|
|
130
|
-
// [LAW:one-source-of-truth] The
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// different
|
|
134
|
-
const
|
|
135
|
+
// [LAW:one-source-of-truth] The two disclosures this menu IS, as refs rather
|
|
136
|
+
// than as gate strings: every gate below — and every `(?)` nested inside them —
|
|
137
|
+
// derives from these, so the toggle that writes a key and the `when` that reads
|
|
138
|
+
// it cannot name different variables.
|
|
139
|
+
const SETTINGS_REF: DisclosureRef = {
|
|
140
|
+
variable: SETTINGS_ANCHOR,
|
|
141
|
+
member: SETTINGS_OPEN,
|
|
142
|
+
};
|
|
143
|
+
const CONFIG_REF: DisclosureRef = {
|
|
144
|
+
variable: CONFIG_SEG,
|
|
145
|
+
member: SETTINGS_OPEN,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Gated on BOTH keys — a config row left open yesterday must not render beside
|
|
149
|
+
// a closed menu today. Nesting is conjunction, which is why it is one list.
|
|
150
|
+
const CONFIG_OPEN_GATE = disclosureGate(SETTINGS_REF, CONFIG_REF);
|
|
151
|
+
|
|
152
|
+
// The `(?)` that explains `persist?` — the one control in this menu whose
|
|
153
|
+
// behaviour a user cannot infer from its label, which is exactly why the ticket
|
|
154
|
+
// named it as a required use site. Its body says what the NEXT click does, in
|
|
155
|
+
// the same two sentences `--help` prints.
|
|
156
|
+
const PERSIST_HELP_SEG = `${SETTINGS_NS}help.persist`;
|
|
157
|
+
|
|
158
|
+
// [LAW:one-source-of-truth] The panel's surface colours, spelled once. The help
|
|
159
|
+
// cells must wear the same ones as the controls they explain — a `(?)` body in
|
|
160
|
+
// a different colour reads as a different panel — and that agreement is only
|
|
161
|
+
// guaranteed if there is one value to hand both.
|
|
162
|
+
const SETTINGS_SURFACE = { bg: "surface", fg: "foreground" } as const;
|
|
135
163
|
|
|
136
164
|
// [LAW:one-source-of-truth] One accordion key for every picker in the menu:
|
|
137
165
|
// one key holds one open member, so opening a theme picker closes the look
|
|
@@ -278,7 +306,7 @@ const controlReset = (name: string): string => `${SETTINGS_NS}reset.${name}`;
|
|
|
278
306
|
// from the same anchor string the toggle's cycle writes — spelled once here,
|
|
279
307
|
// exactly as lowerGroup derives a group body's `when` from the group's own
|
|
280
308
|
// reference name.
|
|
281
|
-
const SETTINGS_OPEN_GATE =
|
|
309
|
+
const SETTINGS_OPEN_GATE = disclosureGate(SETTINGS_REF);
|
|
282
310
|
|
|
283
311
|
// [LAW:single-enforcer] The one answer to "is this segment reference the global
|
|
284
312
|
// menu's anchor". cross-ref.ts asks it to accept an authored placement of a name
|
|
@@ -379,7 +407,10 @@ export function countAnchors(node: LayoutNode): number {
|
|
|
379
407
|
// a vertical pair of the toggle segment and a `when`-gated body. Replaces the
|
|
380
408
|
// anchor leaf wherever it sits, so the author's chosen position is the menu's
|
|
381
409
|
// position with nothing else moved.
|
|
382
|
-
function expandAnchor(
|
|
410
|
+
function expandAnchor(
|
|
411
|
+
node: AnchoredRoot | LayoutNode,
|
|
412
|
+
help: HelpDisclosure,
|
|
413
|
+
): LayoutNode {
|
|
383
414
|
if (node.kind === "segment") {
|
|
384
415
|
return isSettingsAnchor(node.name)
|
|
385
416
|
? {
|
|
@@ -395,6 +426,32 @@ function expandAnchor(node: AnchoredRoot | LayoutNode): LayoutNode {
|
|
|
395
426
|
direction: "horizontal",
|
|
396
427
|
children: [
|
|
397
428
|
{ kind: "segment", name: PERSIST_SEG },
|
|
429
|
+
// The `(?)` rides the row that already exists, immediately
|
|
430
|
+
// after the control it explains — so closed help costs no row
|
|
431
|
+
// and widens the bar by one cell, and open help reads as an
|
|
432
|
+
// answer to the checkbox on its left.
|
|
433
|
+
//
|
|
434
|
+
// Mid-row, DELIBERATELY, unlike edit mode's `(?)`, which
|
|
435
|
+
// edit-chrome.ts goes to lengths to trail. The difference is
|
|
436
|
+
// structural, not a discipline applied in one file and skipped
|
|
437
|
+
// here. `nextHueShift` (src/dsl/render.ts:697) counts segment
|
|
438
|
+
// leaves in pre-order, so a leaf's hue index is the number of
|
|
439
|
+
// leaves before it — which makes the consequence arithmetic:
|
|
440
|
+
// reordering leaves WITHIN a subtree cannot change the index of
|
|
441
|
+
// any leaf AFTER it, since the subtree's leaf count does not
|
|
442
|
+
// move. Edit chrome WRAPS the whole tree, so trailing there is
|
|
443
|
+
// after every existing leaf and costs zero. This menu splices
|
|
444
|
+
// MID-TREE at an anchor `withAnchor` lets the author put
|
|
445
|
+
// anywhere, so no position inside it is after the rest of the
|
|
446
|
+
// bar: the leaves it adds — this trigger plus one per
|
|
447
|
+
// PERSIST_HELP line, a count that lives in help-text.ts and is
|
|
448
|
+
// deliberately not copied here — shift everything past the
|
|
449
|
+
// anchor wherever inside the menu they sit. Trailing would cost
|
|
450
|
+
// the adjacency that IS the affordance. The fix is decoupling
|
|
451
|
+
// colour from tree position — candybar-render-y5h, which fixes
|
|
452
|
+
// every mid-tree synthesis at once rather than one file at a
|
|
453
|
+
// time.
|
|
454
|
+
help.trigger,
|
|
398
455
|
...PRIMARY_CONTROLS.map(
|
|
399
456
|
(c): LayoutNode => ({
|
|
400
457
|
kind: "segment",
|
|
@@ -406,6 +463,9 @@ function expandAnchor(node: AnchoredRoot | LayoutNode): LayoutNode {
|
|
|
406
463
|
],
|
|
407
464
|
when: SETTINGS_OPEN_GATE,
|
|
408
465
|
},
|
|
466
|
+
// The help body: one row, present only while the `(?)` is open,
|
|
467
|
+
// directly under the row that asked the question.
|
|
468
|
+
help.body,
|
|
409
469
|
// Row two: the display settings, behind their own disclosure so
|
|
410
470
|
// the menu opens narrow. Gated on BOTH keys — a config row left
|
|
411
471
|
// open yesterday must not render beside a closed menu today; one
|
|
@@ -429,7 +489,10 @@ function expandAnchor(node: AnchoredRoot | LayoutNode): LayoutNode {
|
|
|
429
489
|
}
|
|
430
490
|
: node;
|
|
431
491
|
}
|
|
432
|
-
return {
|
|
492
|
+
return {
|
|
493
|
+
...node,
|
|
494
|
+
children: node.children.map((child) => expandAnchor(child, help)),
|
|
495
|
+
};
|
|
433
496
|
}
|
|
434
497
|
|
|
435
498
|
// ─── The artifacts ──────────────────────────────────────────────────────────
|
|
@@ -476,7 +539,10 @@ function declareHostedMenu(
|
|
|
476
539
|
// reference, and a second reference to one declaration is a reuse, not the
|
|
477
540
|
// self-collision a second `kind: "group"` node would be (see the settingsDrawer
|
|
478
541
|
// comment in default-dsl-config.ts for that hazard in its original form).
|
|
479
|
-
function settingsArtifacts():
|
|
542
|
+
function settingsArtifacts(): {
|
|
543
|
+
artifacts: MenuArtifacts;
|
|
544
|
+
help: HelpDisclosure;
|
|
545
|
+
} {
|
|
480
546
|
const artifacts: MenuArtifacts = {
|
|
481
547
|
variables: {
|
|
482
548
|
[SETTINGS_ANCHOR]: disclosureStateVar(SETTINGS_ANCHOR, DISCLOSURE_CLOSED),
|
|
@@ -497,22 +563,27 @@ function settingsArtifacts(): MenuArtifacts {
|
|
|
497
563
|
// [LAW:representation] The glyph trails the label it gates, per the
|
|
498
564
|
// disclosure vocabulary every other toggle in the bar reads by.
|
|
499
565
|
[SETTINGS_ANCHOR]: {
|
|
500
|
-
template:
|
|
501
|
-
|
|
502
|
-
|
|
566
|
+
template: disclosureTrigger(
|
|
567
|
+
SETTINGS_ANCHOR,
|
|
568
|
+
`☰ ${DISCLOSURE_GLYPH_CLOSED}`,
|
|
569
|
+
`☰ ${DISCLOSURE_GLYPH_OPEN}`,
|
|
570
|
+
),
|
|
571
|
+
...SETTINGS_SURFACE,
|
|
503
572
|
},
|
|
504
573
|
// [LAW:representation] The checkbox states what the NEXT write does,
|
|
505
574
|
// which is why the glyph and the word live together: "☑ persist?" is
|
|
506
575
|
// the whole explanation of where the click below it lands.
|
|
507
576
|
[PERSIST_SEG]: {
|
|
508
577
|
template: `{{ action "${PERSIST_SEG}" "☐ persist?" "☑ persist?" }}`,
|
|
509
|
-
|
|
510
|
-
fg: "foreground",
|
|
578
|
+
...SETTINGS_SURFACE,
|
|
511
579
|
},
|
|
512
580
|
[CONFIG_SEG]: {
|
|
513
|
-
template:
|
|
514
|
-
|
|
515
|
-
|
|
581
|
+
template: disclosureTrigger(
|
|
582
|
+
CONFIG_SEG,
|
|
583
|
+
`⚙ config ${DISCLOSURE_GLYPH_CLOSED}`,
|
|
584
|
+
`⚙ config ${DISCLOSURE_GLYPH_OPEN}`,
|
|
585
|
+
),
|
|
586
|
+
...SETTINGS_SURFACE,
|
|
516
587
|
},
|
|
517
588
|
// [LAW:one-type-per-behavior] Both non-picker controls read the same
|
|
518
589
|
// `.effective` projection their picker siblings read, and write the
|
|
@@ -522,8 +593,7 @@ function settingsArtifacts(): MenuArtifacts {
|
|
|
522
593
|
template:
|
|
523
594
|
`{{ action "${controlApply("wrap")}" "wrap: on" "wrap: off" }} ` +
|
|
524
595
|
`{{ action "${controlReset("wrap")}" "↺" }}`,
|
|
525
|
-
|
|
526
|
-
fg: "foreground",
|
|
596
|
+
...SETTINGS_SURFACE,
|
|
527
597
|
},
|
|
528
598
|
[PADDING_SEG]: {
|
|
529
599
|
template:
|
|
@@ -531,8 +601,7 @@ function settingsArtifacts(): MenuArtifacts {
|
|
|
531
601
|
"padding {{ .padding.effective }} " +
|
|
532
602
|
`{{ action "${controlApply("padding")}.up" "▶" }} ` +
|
|
533
603
|
`{{ action "${controlReset("padding")}" "↺" }}`,
|
|
534
|
-
|
|
535
|
-
fg: "foreground",
|
|
604
|
+
...SETTINGS_SURFACE,
|
|
536
605
|
},
|
|
537
606
|
// The entry point edit mode never had: `edit.toggle` is a reserved action
|
|
538
607
|
// whose only bundled reference lives in the `toolbar` segment, which a
|
|
@@ -540,8 +609,7 @@ function settingsArtifacts(): MenuArtifacts {
|
|
|
540
609
|
// from a segment no config can drop.
|
|
541
610
|
[EDIT_SEG]: {
|
|
542
611
|
template: `{{ action "${EDIT_TOGGLE_ACTION}" "✎ edit" "✎ done" }}`,
|
|
543
|
-
|
|
544
|
-
fg: "foreground",
|
|
612
|
+
...SETTINGS_SURFACE,
|
|
545
613
|
},
|
|
546
614
|
},
|
|
547
615
|
};
|
|
@@ -555,7 +623,19 @@ function settingsArtifacts(): MenuArtifacts {
|
|
|
555
623
|
DISCLOSURE_CLOSED,
|
|
556
624
|
);
|
|
557
625
|
declareSettingControls(artifacts);
|
|
558
|
-
|
|
626
|
+
// [LAW:one-source-of-truth] The `(?)` is minted here, with the panel it
|
|
627
|
+
// belongs to, and its two NODES are returned so `expandAnchor` places them by
|
|
628
|
+
// the value it is handed rather than by re-deriving names this pass already
|
|
629
|
+
// owns. Nested in SETTINGS_REF, so closing the menu takes the open help with
|
|
630
|
+
// it.
|
|
631
|
+
const help = declareHelp(
|
|
632
|
+
PERSIST_HELP_SEG,
|
|
633
|
+
PERSIST_HELP,
|
|
634
|
+
[SETTINGS_REF],
|
|
635
|
+
artifacts,
|
|
636
|
+
SETTINGS_SURFACE,
|
|
637
|
+
);
|
|
638
|
+
return { artifacts, help };
|
|
559
639
|
}
|
|
560
640
|
|
|
561
641
|
// [LAW:one-source-of-truth] Every setting the menu offers, minted from the one
|
|
@@ -581,8 +661,7 @@ function declareSettingControls(artifacts: MenuArtifacts): void {
|
|
|
581
661
|
`{{ menu "${apply}" "${DISCLOSURE_GLYPH_CLOSED}" "${DISCLOSURE_GLYPH_OPEN}" ` +
|
|
582
662
|
`(dict "key" "${PICKER_KEY}" "closeOnPick" true) }} ` +
|
|
583
663
|
`{{ action "${controlReset(c.name)}" "↺" }}`,
|
|
584
|
-
|
|
585
|
-
fg: "foreground",
|
|
664
|
+
...SETTINGS_SURFACE,
|
|
586
665
|
};
|
|
587
666
|
artifacts.actions[apply] = {
|
|
588
667
|
set: c.sessionKey,
|
|
@@ -676,14 +755,14 @@ export function canHostSessionState(config: DslConfig): boolean {
|
|
|
676
755
|
// and every name now declares one.
|
|
677
756
|
export function synthesizeSettingsMenu(config: DslConfig): DslConfig {
|
|
678
757
|
if (!canHostSessionState(config)) return config;
|
|
679
|
-
const artifacts = settingsArtifacts();
|
|
758
|
+
const { artifacts, help } = settingsArtifacts();
|
|
680
759
|
ensureEditToggle(artifacts);
|
|
681
760
|
const presets: Record<string, PresetDecl> = { ...config.presets };
|
|
682
761
|
for (const name of presetNames(config.presets)) {
|
|
683
762
|
const { node } = presetRoot(config, name);
|
|
684
763
|
presets[name] = {
|
|
685
764
|
...presetByName(config.presets, name),
|
|
686
|
-
root: expandAnchor(withAnchor(node)),
|
|
765
|
+
root: expandAnchor(withAnchor(node), help),
|
|
687
766
|
};
|
|
688
767
|
}
|
|
689
768
|
return {
|
package/src/help-text.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DISCLOSURE_GLYPH_CLOSED } from "./config/disclosure";
|
|
2
|
+
import { HELP_GLYPH_CLOSED } from "./config/help";
|
|
2
3
|
|
|
3
4
|
// [LAW:effects-at-boundaries] Pure data, no I/O — index.ts owns the console.log
|
|
4
5
|
// effect. Kept as its own module so the text is importable (and testable) without
|
|
@@ -6,6 +7,29 @@ import { DISCLOSURE_GLYPH_CLOSED } from "./config/disclosure";
|
|
|
6
7
|
// [LAW:one-source-of-truth] The disclosure glyph comes from config/disclosure.ts
|
|
7
8
|
// (the same constant the theme/look picker itself renders with), so this text
|
|
8
9
|
// can't drift from what a user actually sees on the bar.
|
|
10
|
+
// [LAW:one-source-of-truth] THE help corpus, as data. `--help` and the bar's
|
|
11
|
+
// own `(?)` disclosures are two RENDERINGS of these arrays, never two copies of
|
|
12
|
+
// the sentences: a `(?)` segment's template IS one of these strings, and the
|
|
13
|
+
// paragraphs below interpolate the same values. A help sentence typed into a
|
|
14
|
+
// segment template — where nothing would ever notice it drifting from the CLI's
|
|
15
|
+
// wording — is the defect this shape exists to make unrepresentable.
|
|
16
|
+
//
|
|
17
|
+
// [LAW:representation] One line is one CELL on the bar, so each is a complete
|
|
18
|
+
// thought that stands alone and each stays short: `(?)` bodies drop below their
|
|
19
|
+
// row, and a body that overflows `term.cols` wraps into more rows than the fact
|
|
20
|
+
// it explains is worth. Every line leads with the glyph it explains, so the
|
|
21
|
+
// reader matches text to affordance by shape rather than by reading order.
|
|
22
|
+
export const EDIT_MODE_HELP = [
|
|
23
|
+
"+ inserts here",
|
|
24
|
+
"- removes the one left of it",
|
|
25
|
+
"↺ undoes edits",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export const PERSIST_HELP = [
|
|
29
|
+
"☐ this session only",
|
|
30
|
+
"☑ default for every session",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
9
33
|
export const HELP_TEXT = `
|
|
10
34
|
cc-candybar - Beautiful powerline statusline for Claude Code
|
|
11
35
|
|
|
@@ -27,8 +51,10 @@ Configuration:
|
|
|
27
51
|
needed, and writing your own \`root\` cannot delete it. Click
|
|
28
52
|
☰ ${DISCLOSURE_GLYPH_CLOSED} on the bar for preset switching, edit mode, and a config menu
|
|
29
53
|
of clickable theme/look/style/wrap/padding controls. The \`persist?\`
|
|
30
|
-
checkbox there chooses where a change lands:
|
|
31
|
-
|
|
54
|
+
checkbox there chooses where a change lands: ${PERSIST_HELP.join(", ")}.
|
|
55
|
+
|
|
56
|
+
Anywhere the bar shows ${HELP_GLYPH_CLOSED}, clicking it reveals these same instructions
|
|
57
|
+
in place. In edit mode: ${EDIT_MODE_HELP.join(", ")}.
|
|
32
58
|
|
|
33
59
|
Subcommands:
|
|
34
60
|
install One-shot setup: stages the runtime (native render
|