@eiei114/pi-sub-bar 1.5.1

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.
@@ -0,0 +1,651 @@
1
+ /**
2
+ * Settings types and defaults for sub-bar
3
+ */
4
+
5
+ import type { CoreSettings, ProviderName } from "@eiei114/pi-sub-shared";
6
+ import { PROVIDERS } from "@eiei114/pi-sub-shared";
7
+ import type { ThemeColor } from "@mariozechner/pi-coding-agent";
8
+
9
+ /**
10
+ * Bar display style
11
+ */
12
+ export type BarStyle = "bar" | "percentage" | "both";
13
+
14
+ /**
15
+ * Bar rendering type
16
+ */
17
+ export type BarType = "horizontal-bar" | "horizontal-single" | "vertical" | "braille" | "shade";
18
+
19
+ /**
20
+ * Color scheme for usage bars
21
+ */
22
+ export type ColorScheme = "monochrome" | "base-warning-error" | "success-base-warning-error";
23
+
24
+ /**
25
+ * Progress bar character style
26
+ */
27
+ export type BarCharacter = "light" | "heavy" | "double" | "block" | (string & {});
28
+
29
+ /**
30
+ * Divider character style
31
+ */
32
+ export type DividerCharacter =
33
+ | "none"
34
+ | "blank"
35
+ | "|"
36
+ | "│"
37
+ | "┃"
38
+ | "┆"
39
+ | "┇"
40
+ | "║"
41
+ | "•"
42
+ | "●"
43
+ | "○"
44
+ | "◇"
45
+ | (string & {});
46
+
47
+ /**
48
+ * Widget overflow mode
49
+ */
50
+ export type OverflowMode = "truncate" | "wrap";
51
+ export type WidgetWrapping = OverflowMode;
52
+
53
+ /**
54
+ * Widget placement
55
+ */
56
+ export type WidgetPlacement = "aboveEditor" | "belowEditor" | "status";
57
+
58
+ /**
59
+ * Alignment for the widget
60
+ */
61
+ export type DisplayAlignment = "left" | "center" | "right" | "split";
62
+
63
+ /**
64
+ * Provider label prefix
65
+ */
66
+ export type ProviderLabel = "plan" | "subscription" | "sub" | "none" | (string & {});
67
+
68
+ /**
69
+ * Reset timer format
70
+ */
71
+ export type ResetTimeFormat = "relative" | "datetime";
72
+
73
+ /**
74
+ * Reset timer containment style
75
+ */
76
+ export type ResetTimerContainment = "none" | "blank" | "()" | "[]" | "<>" | (string & {});
77
+
78
+ /**
79
+ * Status indicator display mode
80
+ */
81
+ export type StatusIndicatorMode = "icon" | "text" | "icon+text";
82
+
83
+ /**
84
+ * Status icon pack selection
85
+ */
86
+ export type StatusIconPack = "minimal" | "emoji" | "custom";
87
+
88
+ export interface UsageColorTargets {
89
+ title: boolean;
90
+ timer: boolean;
91
+ bar: boolean;
92
+ usageLabel: boolean;
93
+ status: boolean;
94
+ }
95
+
96
+ /**
97
+ * Divider color options (subset of theme colors).
98
+ */
99
+ export const DIVIDER_COLOR_OPTIONS = [
100
+ "primary",
101
+ "text",
102
+ "muted",
103
+ "dim",
104
+ "success",
105
+ "warning",
106
+ "error",
107
+ "border",
108
+ "borderMuted",
109
+ "borderAccent",
110
+ ] as const;
111
+
112
+ export type DividerColor = (typeof DIVIDER_COLOR_OPTIONS)[number];
113
+
114
+ /**
115
+ * Background color options (theme background colors).
116
+ */
117
+ export const BACKGROUND_COLOR_OPTIONS = [
118
+ "selectedBg",
119
+ "userMessageBg",
120
+ "customMessageBg",
121
+ "toolPendingBg",
122
+ "toolSuccessBg",
123
+ "toolErrorBg",
124
+ ] as const;
125
+
126
+ export type BackgroundColor = (typeof BACKGROUND_COLOR_OPTIONS)[number];
127
+
128
+ /**
129
+ * Base text/background color options.
130
+ */
131
+ export const BASE_COLOR_OPTIONS = [...DIVIDER_COLOR_OPTIONS, ...BACKGROUND_COLOR_OPTIONS] as const;
132
+
133
+ /**
134
+ * Base text color for widget labels
135
+ */
136
+ export type BaseTextColor = (typeof BASE_COLOR_OPTIONS)[number];
137
+
138
+ /**
139
+ * Background options for the widget line.
140
+ */
141
+ export const WIDGET_BACKGROUND_OPTIONS = ["none", ...BASE_COLOR_OPTIONS] as const;
142
+
143
+ export type WidgetBackgroundColor = (typeof WIDGET_BACKGROUND_OPTIONS)[number];
144
+
145
+ export function normalizeDividerColor(value?: string): DividerColor {
146
+ if (!value) return "borderMuted";
147
+ if (value === "accent" || value === "primary") return "primary";
148
+ if ((DIVIDER_COLOR_OPTIONS as readonly string[]).includes(value)) {
149
+ return value as DividerColor;
150
+ }
151
+ return "borderMuted";
152
+ }
153
+
154
+ export function resolveDividerColor(value?: string): ThemeColor {
155
+ const normalized = normalizeDividerColor(value);
156
+ switch (normalized) {
157
+ case "primary":
158
+ return "accent";
159
+ case "border":
160
+ case "borderMuted":
161
+ case "borderAccent":
162
+ case "success":
163
+ case "warning":
164
+ case "error":
165
+ case "muted":
166
+ case "dim":
167
+ case "text":
168
+ return normalized as ThemeColor;
169
+ default:
170
+ return "borderMuted";
171
+ }
172
+ }
173
+
174
+ export function isBackgroundColor(value?: BaseTextColor): value is BackgroundColor {
175
+ return !!value && (BACKGROUND_COLOR_OPTIONS as readonly string[]).includes(value);
176
+ }
177
+
178
+ export function normalizeBaseTextColor(value?: string): BaseTextColor {
179
+ if (!value) return "dim";
180
+ if (value === "accent" || value === "primary") return "primary";
181
+ if ((BASE_COLOR_OPTIONS as readonly string[]).includes(value)) {
182
+ return value as BaseTextColor;
183
+ }
184
+ return "dim";
185
+ }
186
+
187
+ export function normalizeBackgroundColor(value?: string): WidgetBackgroundColor {
188
+ if (!value || value === "none" || value === "transparent") return "none";
189
+ if (value === "accent" || value === "primary") return "primary";
190
+ if ((BASE_COLOR_OPTIONS as readonly string[]).includes(value)) {
191
+ return value as BaseTextColor;
192
+ }
193
+ return "none";
194
+ }
195
+
196
+ export function resolveBaseTextColor(value?: string): BaseTextColor {
197
+ return normalizeBaseTextColor(value);
198
+ }
199
+
200
+ export function resolveBackgroundColor(value?: string): WidgetBackgroundColor {
201
+ return normalizeBackgroundColor(value);
202
+ }
203
+
204
+ /**
205
+ * Bar width configuration
206
+ */
207
+ export type BarWidth = number | "fill";
208
+
209
+ /**
210
+ * Divider blank spacing configuration
211
+ */
212
+ export type DividerBlanks = number | "fill";
213
+
214
+ /**
215
+ * Provider settings (UI-only)
216
+ */
217
+ export interface BaseProviderSettings {
218
+ /** Show status indicator */
219
+ showStatus: boolean;
220
+ }
221
+
222
+ export interface AnthropicProviderSettings extends BaseProviderSettings {
223
+ windows: {
224
+ show5h: boolean;
225
+ show7d: boolean;
226
+ showExtra: boolean;
227
+ };
228
+ }
229
+
230
+ export interface CopilotProviderSettings extends BaseProviderSettings {
231
+ showMultiplier: boolean;
232
+ showRequestsLeft: boolean;
233
+ quotaDisplay: "percentage" | "requests";
234
+ windows: {
235
+ showMonth: boolean;
236
+ };
237
+ }
238
+
239
+ export interface GeminiProviderSettings extends BaseProviderSettings {
240
+ windows: {
241
+ showPro: boolean;
242
+ showFlash: boolean;
243
+ };
244
+ }
245
+
246
+ export interface AntigravityProviderSettings extends BaseProviderSettings {
247
+ showCurrentModel: boolean;
248
+ showScopedModels: boolean;
249
+ windows: {
250
+ showModels: boolean;
251
+ };
252
+ modelVisibility: Record<string, boolean>;
253
+ modelOrder: string[];
254
+ }
255
+
256
+ export interface CodexProviderSettings extends BaseProviderSettings {
257
+ invertUsage: boolean;
258
+ windows: {
259
+ showPrimary: boolean;
260
+ showSecondary: boolean;
261
+ };
262
+ }
263
+
264
+ export interface KiroProviderSettings extends BaseProviderSettings {
265
+ windows: {
266
+ showCredits: boolean;
267
+ };
268
+ }
269
+
270
+ export interface ZaiProviderSettings extends BaseProviderSettings {
271
+ windows: {
272
+ showTokens: boolean;
273
+ showMonthly: boolean;
274
+ };
275
+ }
276
+
277
+ export interface ProviderSettingsMap {
278
+ anthropic: AnthropicProviderSettings;
279
+ copilot: CopilotProviderSettings;
280
+ gemini: GeminiProviderSettings;
281
+ antigravity: AntigravityProviderSettings;
282
+ codex: CodexProviderSettings;
283
+ kiro: KiroProviderSettings;
284
+ zai: ZaiProviderSettings;
285
+ }
286
+
287
+ export type { BehaviorSettings, CoreSettings } from "@eiei114/pi-sub-shared";
288
+
289
+ /**
290
+ * Keybinding settings.
291
+ * Values are key-combo strings accepted by pi's registerShortcut (e.g. "ctrl+alt+p").
292
+ * Use "none" to disable a shortcut.
293
+ * Changes take effect after pi restart.
294
+ */
295
+ export interface KeybindingSettings {
296
+ /** Shortcut to cycle through providers */
297
+ cycleProvider: string;
298
+ /** Shortcut to toggle reset timer format */
299
+ toggleResetFormat: string;
300
+ }
301
+
302
+ /**
303
+ * Display settings
304
+ */
305
+ export interface DisplaySettings {
306
+ /** Alignment */
307
+ alignment: DisplayAlignment;
308
+ /** Bar display style */
309
+ barStyle: BarStyle;
310
+ /** Bar type */
311
+ barType: BarType;
312
+ /** Width of the progress bar in characters */
313
+ barWidth: BarWidth;
314
+ /** Progress bar character */
315
+ barCharacter: BarCharacter;
316
+ /** Contain bar within ▕ and ▏ */
317
+ containBar: boolean;
318
+ /** Fill empty braille segments with dim full blocks */
319
+ brailleFillEmpty: boolean;
320
+ /** Use full braille blocks for filled segments */
321
+ brailleFullBlocks: boolean;
322
+ /** Color scheme for bars */
323
+ colorScheme: ColorScheme;
324
+ /** Elements colored by the usage scheme */
325
+ usageColorTargets: UsageColorTargets;
326
+ /** Reset time display position */
327
+ resetTimePosition: "off" | "front" | "back" | "integrated";
328
+ /** Reset time format */
329
+ resetTimeFormat: ResetTimeFormat;
330
+ /** Reset timer containment */
331
+ resetTimeContainment: ResetTimerContainment;
332
+ /** Status indicator mode */
333
+ statusIndicatorMode: StatusIndicatorMode;
334
+ /** Status icon pack */
335
+ statusIconPack: StatusIconPack;
336
+ /** Custom status icon pack (four characters) */
337
+ statusIconCustom: string;
338
+ /** Show divider between status and provider */
339
+ statusProviderDivider: boolean;
340
+ /** Dismiss status when operational */
341
+ statusDismissOk: boolean;
342
+ /** Show provider display name */
343
+ showProviderName: boolean;
344
+ /** Provider label prefix */
345
+ providerLabel: ProviderLabel;
346
+ /** Show colon after provider label */
347
+ providerLabelColon: boolean;
348
+ /** Bold provider name and colon */
349
+ providerLabelBold: boolean;
350
+ /** Base text color for widget labels */
351
+ baseTextColor: BaseTextColor;
352
+ /** Background color for the widget line */
353
+ backgroundColor: WidgetBackgroundColor;
354
+ /** Show window titles (5h, Week, etc.) */
355
+ showWindowTitle: boolean;
356
+ /** Bold window titles (5h, Week, etc.) */
357
+ boldWindowTitle: boolean;
358
+ /** Show usage labels (used/rem.) */
359
+ showUsageLabels: boolean;
360
+ /** Divider character */
361
+ dividerCharacter: DividerCharacter;
362
+ /** Divider color */
363
+ dividerColor: DividerColor;
364
+ /** Blanks before and after divider */
365
+ dividerBlanks: DividerBlanks;
366
+ /** Show divider between provider label and usage */
367
+ showProviderDivider: boolean;
368
+ /** Show leading divider in status-line placement */
369
+ statusLeadingDivider: boolean;
370
+ /** Show trailing divider in status-line placement */
371
+ statusTrailingDivider: boolean;
372
+ /** Connect divider glyphs to the bottom divider line */
373
+ dividerFooterJoin: boolean;
374
+ /** Show divider line above the bar */
375
+ showTopDivider: boolean;
376
+ /** Show divider line below the bar */
377
+ showBottomDivider: boolean;
378
+ /** Widget overflow mode */
379
+ overflow: OverflowMode;
380
+ /** Left padding inside widget */
381
+ paddingLeft: number;
382
+ /** Right padding inside widget */
383
+ paddingRight: number;
384
+ /** Widget placement */
385
+ widgetPlacement: WidgetPlacement;
386
+ /** Show context window usage as leftmost progress bar */
387
+ showContextBar: boolean;
388
+ /** Error threshold (percentage remaining below this = red) */
389
+ errorThreshold: number;
390
+ /** Warning threshold (percentage remaining below this = yellow) */
391
+ warningThreshold: number;
392
+ /** Success threshold (percentage remaining above this = green, gradient only) */
393
+ successThreshold: number;
394
+ }
395
+
396
+
397
+ /**
398
+ * All settings
399
+ */
400
+ export interface DisplayTheme {
401
+ id: string;
402
+ name: string;
403
+ display: DisplaySettings;
404
+ source?: "saved" | "imported";
405
+ }
406
+
407
+ export interface Settings extends Omit<CoreSettings, "providers"> {
408
+ /** Version for migration */
409
+ version: number;
410
+ /** Provider-specific UI settings */
411
+ providers: ProviderSettingsMap;
412
+ /** Display settings */
413
+ display: DisplaySettings;
414
+ /** Stored display themes */
415
+ displayThemes: DisplayTheme[];
416
+ /** Snapshot of the previous display theme */
417
+ displayUserTheme: DisplaySettings | null;
418
+ /** Pinned provider override for display */
419
+ pinnedProvider: ProviderName | null;
420
+ /** Keybinding settings (changes require pi restart) */
421
+ keybindings: KeybindingSettings;
422
+ }
423
+
424
+ /**
425
+ * Current settings version
426
+ */
427
+ export const SETTINGS_VERSION = 2;
428
+
429
+ /**
430
+ * Default settings
431
+ */
432
+ export function getDefaultSettings(): Settings {
433
+ return {
434
+ version: SETTINGS_VERSION,
435
+ providers: {
436
+ anthropic: {
437
+ showStatus: true,
438
+ windows: {
439
+ show5h: true,
440
+ show7d: true,
441
+ showExtra: false,
442
+ },
443
+ },
444
+ copilot: {
445
+ showStatus: true,
446
+ showMultiplier: true,
447
+ showRequestsLeft: true,
448
+ quotaDisplay: "percentage",
449
+ windows: {
450
+ showMonth: true,
451
+ },
452
+ },
453
+ gemini: {
454
+ showStatus: true,
455
+ windows: {
456
+ showPro: true,
457
+ showFlash: true,
458
+ },
459
+ },
460
+ antigravity: {
461
+ showStatus: true,
462
+ showCurrentModel: true,
463
+ showScopedModels: true,
464
+ windows: {
465
+ showModels: true,
466
+ },
467
+ modelVisibility: {},
468
+ modelOrder: [],
469
+ },
470
+ codex: {
471
+ showStatus: true,
472
+ invertUsage: false,
473
+ windows: {
474
+ showPrimary: true,
475
+ showSecondary: true,
476
+ },
477
+ },
478
+ kiro: {
479
+ showStatus: false,
480
+ windows: {
481
+ showCredits: true,
482
+ },
483
+ },
484
+ zai: {
485
+ showStatus: false,
486
+ windows: {
487
+ showTokens: true,
488
+ showMonthly: true,
489
+ },
490
+ },
491
+ },
492
+ display: {
493
+ alignment: "split",
494
+ barStyle: "both",
495
+ barType: "horizontal-bar",
496
+ barWidth: "fill",
497
+ barCharacter: "heavy",
498
+ containBar: false,
499
+ brailleFillEmpty: false,
500
+ brailleFullBlocks: false,
501
+ colorScheme: "base-warning-error",
502
+ usageColorTargets: {
503
+ title: true,
504
+ timer: true,
505
+ bar: true,
506
+ usageLabel: true,
507
+ status: true,
508
+ },
509
+ resetTimePosition: "front",
510
+ resetTimeFormat: "relative",
511
+ resetTimeContainment: "blank",
512
+ statusIndicatorMode: "icon",
513
+ statusIconPack: "emoji",
514
+ statusIconCustom: "✓⚠×?",
515
+ statusProviderDivider: false,
516
+ statusDismissOk: true,
517
+ showProviderName: true,
518
+ providerLabel: "none",
519
+ providerLabelColon: false,
520
+ providerLabelBold: true,
521
+ baseTextColor: "muted",
522
+ backgroundColor: "none",
523
+ showWindowTitle: true,
524
+ boldWindowTitle: true,
525
+ showUsageLabels: true,
526
+ dividerCharacter: "│",
527
+ dividerColor: "dim",
528
+ dividerBlanks: 1,
529
+ showProviderDivider: true,
530
+ statusLeadingDivider: false,
531
+ statusTrailingDivider: false,
532
+ dividerFooterJoin: true,
533
+ showTopDivider: false,
534
+ showBottomDivider: true,
535
+ paddingLeft: 1,
536
+ paddingRight: 1,
537
+ widgetPlacement: "belowEditor",
538
+ showContextBar: false,
539
+ errorThreshold: 25,
540
+ warningThreshold: 50,
541
+ overflow: "truncate",
542
+ successThreshold: 75,
543
+ },
544
+
545
+ displayThemes: [],
546
+ displayUserTheme: null,
547
+ pinnedProvider: null,
548
+
549
+ keybindings: {
550
+ cycleProvider: "ctrl+alt+p",
551
+ toggleResetFormat: "ctrl+alt+r",
552
+ },
553
+
554
+ behavior: {
555
+ refreshInterval: 60,
556
+ minRefreshInterval: 10,
557
+ refreshOnTurnStart: false,
558
+ refreshOnToolResult: false,
559
+ },
560
+ statusRefresh: {
561
+ refreshInterval: 60,
562
+ minRefreshInterval: 10,
563
+ refreshOnTurnStart: false,
564
+ refreshOnToolResult: false,
565
+ },
566
+ providerOrder: [...PROVIDERS],
567
+ defaultProvider: null,
568
+ };
569
+ }
570
+
571
+ /**
572
+ * Deep merge two objects
573
+ */
574
+ function deepMerge<T extends object>(target: T, source: Partial<T>): T {
575
+ const result = { ...target };
576
+ for (const key of Object.keys(source) as (keyof T)[]) {
577
+ const sourceValue = source[key];
578
+ const targetValue = target[key];
579
+ if (
580
+ sourceValue !== undefined &&
581
+ typeof sourceValue === "object" &&
582
+ sourceValue !== null &&
583
+ !Array.isArray(sourceValue) &&
584
+ typeof targetValue === "object" &&
585
+ targetValue !== null &&
586
+ !Array.isArray(targetValue)
587
+ ) {
588
+ result[key] = deepMerge(targetValue, sourceValue as Partial<typeof targetValue>);
589
+ } else if (sourceValue !== undefined) {
590
+ result[key] = sourceValue as T[keyof T];
591
+ }
592
+ }
593
+ return result;
594
+ }
595
+
596
+ /**
597
+ * Merge settings with defaults (no legacy migrations).
598
+ */
599
+ export function mergeSettings(loaded: Partial<Settings>): Settings {
600
+ const migrated = migrateSettings(loaded);
601
+ return deepMerge(getDefaultSettings(), migrated);
602
+ }
603
+
604
+ const WIDGET_PLACEMENTS = ["aboveEditor", "belowEditor", "status"] as const;
605
+
606
+ function coerceWidgetPlacement(raw?: unknown): WidgetPlacement | undefined {
607
+ if (typeof raw !== "string") return undefined;
608
+ if ((WIDGET_PLACEMENTS as readonly string[]).includes(raw)) {
609
+ return raw as WidgetPlacement;
610
+ }
611
+ return undefined;
612
+ }
613
+
614
+ function migrateDisplaySettings(display?: Partial<DisplaySettings> | null): void {
615
+ if (!display) return;
616
+ const displayAny = display as Partial<DisplaySettings> & { widgetWrapping?: OverflowMode; paddingX?: number };
617
+ const normalizedPlacement = coerceWidgetPlacement(displayAny.widgetPlacement);
618
+ if (displayAny.widgetPlacement !== undefined) {
619
+ displayAny.widgetPlacement = normalizedPlacement ?? "belowEditor";
620
+ }
621
+ if (displayAny.widgetPlacement === "status") {
622
+ displayAny.alignment = "left";
623
+ displayAny.overflow = "truncate";
624
+ }
625
+ if (displayAny.widgetWrapping !== undefined && displayAny.overflow === undefined) {
626
+ displayAny.overflow = displayAny.widgetWrapping;
627
+ }
628
+ if (displayAny.paddingX !== undefined) {
629
+ if (displayAny.paddingLeft === undefined) {
630
+ displayAny.paddingLeft = displayAny.paddingX;
631
+ }
632
+ if (displayAny.paddingRight === undefined) {
633
+ displayAny.paddingRight = displayAny.paddingX;
634
+ }
635
+ delete (displayAny as { paddingX?: unknown }).paddingX;
636
+ }
637
+ if ("widgetWrapping" in displayAny) {
638
+ delete (displayAny as { widgetWrapping?: unknown }).widgetWrapping;
639
+ }
640
+ }
641
+
642
+ function migrateSettings(loaded: Partial<Settings>): Partial<Settings> {
643
+ migrateDisplaySettings(loaded.display);
644
+ migrateDisplaySettings(loaded.displayUserTheme);
645
+ if (Array.isArray(loaded.displayThemes)) {
646
+ for (const theme of loaded.displayThemes) {
647
+ migrateDisplaySettings(theme.display as Partial<DisplaySettings> | undefined);
648
+ }
649
+ }
650
+ return loaded;
651
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Settings UI entry point (re-export).
3
+ */
4
+
5
+ export { showSettingsUI } from "./settings/ui.js";