@marckrenn/pi-sub-bar 1.0.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.
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Settings types and defaults for sub-bar
3
+ */
4
+
5
+ import type { CoreSettings, ProviderName } from "@marckrenn/pi-sub-shared";
6
+ import { PROVIDERS } from "@marckrenn/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 line wrapping mode
49
+ */
50
+ export type WidgetWrapping = "truncate" | "wrap";
51
+
52
+ /**
53
+ * Widget placement
54
+ */
55
+ export type WidgetPlacement = "belowEditor";
56
+
57
+ /**
58
+ * Alignment for the widget
59
+ */
60
+ export type DisplayAlignment = "left" | "center" | "right" | "split";
61
+
62
+ /**
63
+ * Provider label prefix
64
+ */
65
+ export type ProviderLabel = "plan" | "subscription" | "sub" | "none" | (string & {});
66
+
67
+ /**
68
+ * Reset timer format
69
+ */
70
+ export type ResetTimeFormat = "relative" | "datetime";
71
+
72
+ /**
73
+ * Reset timer containment style
74
+ */
75
+ export type ResetTimerContainment = "none" | "blank" | "()" | "[]" | "<>" | (string & {});
76
+
77
+ /**
78
+ * Status indicator display mode
79
+ */
80
+ export type StatusIndicatorMode = "icon" | "text" | "icon+text";
81
+
82
+ /**
83
+ * Status icon pack selection
84
+ */
85
+ export type StatusIconPack = "minimal" | "emoji" | "custom";
86
+
87
+ export interface UsageColorTargets {
88
+ title: boolean;
89
+ timer: boolean;
90
+ bar: boolean;
91
+ usageLabel: boolean;
92
+ status: boolean;
93
+ }
94
+
95
+ /**
96
+ * Divider color options (subset of theme colors).
97
+ */
98
+ export const DIVIDER_COLOR_OPTIONS = [
99
+ "primary",
100
+ "text",
101
+ "muted",
102
+ "dim",
103
+ "success",
104
+ "warning",
105
+ "error",
106
+ "border",
107
+ "borderMuted",
108
+ "borderAccent",
109
+ ] as const;
110
+
111
+ export type DividerColor = (typeof DIVIDER_COLOR_OPTIONS)[number];
112
+
113
+ /**
114
+ * Background color options (theme background colors).
115
+ */
116
+ export const BACKGROUND_COLOR_OPTIONS = [
117
+ "selectedBg",
118
+ "userMessageBg",
119
+ "customMessageBg",
120
+ "toolPendingBg",
121
+ "toolSuccessBg",
122
+ "toolErrorBg",
123
+ ] as const;
124
+
125
+ export type BackgroundColor = (typeof BACKGROUND_COLOR_OPTIONS)[number];
126
+
127
+ /**
128
+ * Base text/background color options.
129
+ */
130
+ export const BASE_COLOR_OPTIONS = [...DIVIDER_COLOR_OPTIONS, ...BACKGROUND_COLOR_OPTIONS] as const;
131
+
132
+ /**
133
+ * Base text color for widget labels
134
+ */
135
+ export type BaseTextColor = (typeof BASE_COLOR_OPTIONS)[number];
136
+
137
+ export function normalizeDividerColor(value?: string): DividerColor {
138
+ if (!value) return "borderMuted";
139
+ if (value === "accent" || value === "primary") return "primary";
140
+ if ((DIVIDER_COLOR_OPTIONS as readonly string[]).includes(value)) {
141
+ return value as DividerColor;
142
+ }
143
+ return "borderMuted";
144
+ }
145
+
146
+ export function resolveDividerColor(value?: string): ThemeColor {
147
+ const normalized = normalizeDividerColor(value);
148
+ switch (normalized) {
149
+ case "primary":
150
+ return "accent";
151
+ case "border":
152
+ case "borderMuted":
153
+ case "borderAccent":
154
+ case "success":
155
+ case "warning":
156
+ case "error":
157
+ case "muted":
158
+ case "dim":
159
+ case "text":
160
+ return normalized as ThemeColor;
161
+ default:
162
+ return "borderMuted";
163
+ }
164
+ }
165
+
166
+ export function isBackgroundColor(value?: BaseTextColor): value is BackgroundColor {
167
+ return !!value && (BACKGROUND_COLOR_OPTIONS as readonly string[]).includes(value);
168
+ }
169
+
170
+ export function normalizeBaseTextColor(value?: string): BaseTextColor {
171
+ if (!value) return "dim";
172
+ if (value === "accent" || value === "primary") return "primary";
173
+ if ((BASE_COLOR_OPTIONS as readonly string[]).includes(value)) {
174
+ return value as BaseTextColor;
175
+ }
176
+ return "dim";
177
+ }
178
+
179
+ export function resolveBaseTextColor(value?: string): BaseTextColor {
180
+ return normalizeBaseTextColor(value);
181
+ }
182
+
183
+ /**
184
+ * Bar width configuration
185
+ */
186
+ export type BarWidth = number | "fill";
187
+
188
+ /**
189
+ * Divider blank spacing configuration
190
+ */
191
+ export type DividerBlanks = number | "fill";
192
+
193
+ /**
194
+ * Provider settings (UI-only)
195
+ */
196
+ export interface BaseProviderSettings {
197
+ /** Show status indicator */
198
+ showStatus: boolean;
199
+ }
200
+
201
+ export interface AnthropicProviderSettings extends BaseProviderSettings {
202
+ windows: {
203
+ show5h: boolean;
204
+ show7d: boolean;
205
+ showExtra: boolean;
206
+ };
207
+ }
208
+
209
+ export interface CopilotProviderSettings extends BaseProviderSettings {
210
+ showMultiplier: boolean;
211
+ showRequestsLeft: boolean;
212
+ quotaDisplay: "percentage" | "requests";
213
+ windows: {
214
+ showMonth: boolean;
215
+ };
216
+ }
217
+
218
+ export interface GeminiProviderSettings extends BaseProviderSettings {
219
+ windows: {
220
+ showPro: boolean;
221
+ showFlash: boolean;
222
+ };
223
+ }
224
+
225
+ export interface AntigravityProviderSettings extends BaseProviderSettings {
226
+ windows: {
227
+ showClaude: boolean;
228
+ showPro: boolean;
229
+ showFlash: boolean;
230
+ };
231
+ }
232
+
233
+ export interface CodexProviderSettings extends BaseProviderSettings {
234
+ invertUsage: boolean;
235
+ windows: {
236
+ showPrimary: boolean;
237
+ showSecondary: boolean;
238
+ };
239
+ }
240
+
241
+ export interface KiroProviderSettings extends BaseProviderSettings {
242
+ windows: {
243
+ showCredits: boolean;
244
+ };
245
+ }
246
+
247
+ export interface ZaiProviderSettings extends BaseProviderSettings {
248
+ windows: {
249
+ showTokens: boolean;
250
+ showMonthly: boolean;
251
+ };
252
+ }
253
+
254
+ export interface ProviderSettingsMap {
255
+ anthropic: AnthropicProviderSettings;
256
+ copilot: CopilotProviderSettings;
257
+ gemini: GeminiProviderSettings;
258
+ antigravity: AntigravityProviderSettings;
259
+ codex: CodexProviderSettings;
260
+ kiro: KiroProviderSettings;
261
+ zai: ZaiProviderSettings;
262
+ }
263
+
264
+ export type { BehaviorSettings, CoreSettings } from "@marckrenn/pi-sub-shared";
265
+
266
+ /**
267
+ * Display settings
268
+ */
269
+ export interface DisplaySettings {
270
+ /** Alignment */
271
+ alignment: DisplayAlignment;
272
+ /** Bar display style */
273
+ barStyle: BarStyle;
274
+ /** Bar type */
275
+ barType: BarType;
276
+ /** Width of the progress bar in characters */
277
+ barWidth: BarWidth;
278
+ /** Progress bar character */
279
+ barCharacter: BarCharacter;
280
+ /** Contain bar within ▕ and ▏ */
281
+ containBar: boolean;
282
+ /** Fill empty braille segments with dim full blocks */
283
+ brailleFillEmpty: boolean;
284
+ /** Use full braille blocks for filled segments */
285
+ brailleFullBlocks: boolean;
286
+ /** Color scheme for bars */
287
+ colorScheme: ColorScheme;
288
+ /** Elements colored by the usage scheme */
289
+ usageColorTargets: UsageColorTargets;
290
+ /** Reset time display position */
291
+ resetTimePosition: "off" | "front" | "back" | "integrated";
292
+ /** Reset time format */
293
+ resetTimeFormat: ResetTimeFormat;
294
+ /** Reset timer containment */
295
+ resetTimeContainment: ResetTimerContainment;
296
+ /** Status indicator mode */
297
+ statusIndicatorMode: StatusIndicatorMode;
298
+ /** Status icon pack */
299
+ statusIconPack: StatusIconPack;
300
+ /** Custom status icon pack (four characters) */
301
+ statusIconCustom: string;
302
+ /** Show divider between status and provider */
303
+ statusProviderDivider: boolean;
304
+ /** Dismiss status when operational */
305
+ statusDismissOk: boolean;
306
+ /** Show provider display name */
307
+ showProviderName: boolean;
308
+ /** Provider label prefix */
309
+ providerLabel: ProviderLabel;
310
+ /** Show colon after provider label */
311
+ providerLabelColon: boolean;
312
+ /** Bold provider name and colon */
313
+ providerLabelBold: boolean;
314
+ /** Base text color for widget labels */
315
+ baseTextColor: BaseTextColor;
316
+ /** Background color for the widget line */
317
+ backgroundColor: BaseTextColor;
318
+ /** Show window titles (5h, Week, etc.) */
319
+ showWindowTitle: boolean;
320
+ /** Bold window titles (5h, Week, etc.) */
321
+ boldWindowTitle: boolean;
322
+ /** Show usage labels (used/rem.) */
323
+ showUsageLabels: boolean;
324
+ /** Divider character */
325
+ dividerCharacter: DividerCharacter;
326
+ /** Divider color */
327
+ dividerColor: DividerColor;
328
+ /** Blanks before and after divider */
329
+ dividerBlanks: DividerBlanks;
330
+ /** Show divider between provider label and usage */
331
+ showProviderDivider: boolean;
332
+ /** Connect divider glyphs to the bottom divider line */
333
+ dividerFooterJoin: boolean;
334
+ /** Show divider line above the bar */
335
+ showTopDivider: boolean;
336
+ /** Show divider line below the bar */
337
+ showBottomDivider: boolean;
338
+ /** Widget line wrapping */
339
+ widgetWrapping: WidgetWrapping;
340
+ /** Left/right padding inside widget */
341
+ paddingX: number;
342
+ /** Widget placement */
343
+ widgetPlacement: WidgetPlacement;
344
+ /** Error threshold (percentage remaining below this = red) */
345
+ errorThreshold: number;
346
+ /** Warning threshold (percentage remaining below this = yellow) */
347
+ warningThreshold: number;
348
+ /** Success threshold (percentage remaining above this = green, gradient only) */
349
+ successThreshold: number;
350
+ }
351
+
352
+
353
+ /**
354
+ * All settings
355
+ */
356
+ export interface DisplayTheme {
357
+ id: string;
358
+ name: string;
359
+ display: DisplaySettings;
360
+ source?: "saved" | "imported";
361
+ }
362
+
363
+ export interface Settings extends Omit<CoreSettings, "providers"> {
364
+ /** Version for migration */
365
+ version: number;
366
+ /** Provider-specific UI settings */
367
+ providers: ProviderSettingsMap;
368
+ /** Display settings */
369
+ display: DisplaySettings;
370
+ /** Stored display themes */
371
+ displayThemes: DisplayTheme[];
372
+ /** Snapshot of the previous display theme */
373
+ displayUserTheme: DisplaySettings | null;
374
+ /** Pinned provider override for display */
375
+ pinnedProvider: ProviderName | null;
376
+ }
377
+
378
+ /**
379
+ * Current settings version
380
+ */
381
+ export const SETTINGS_VERSION = 2;
382
+
383
+ /**
384
+ * Default settings
385
+ */
386
+ export function getDefaultSettings(): Settings {
387
+ return {
388
+ version: SETTINGS_VERSION,
389
+ providers: {
390
+ anthropic: {
391
+ showStatus: true,
392
+ windows: {
393
+ show5h: true,
394
+ show7d: true,
395
+ showExtra: true,
396
+ },
397
+ },
398
+ copilot: {
399
+ showStatus: true,
400
+ showMultiplier: true,
401
+ showRequestsLeft: true,
402
+ quotaDisplay: "percentage",
403
+ windows: {
404
+ showMonth: true,
405
+ },
406
+ },
407
+ gemini: {
408
+ showStatus: true,
409
+ windows: {
410
+ showPro: true,
411
+ showFlash: true,
412
+ },
413
+ },
414
+ antigravity: {
415
+ showStatus: false,
416
+ windows: {
417
+ showClaude: true,
418
+ showPro: true,
419
+ showFlash: true,
420
+ },
421
+ },
422
+ codex: {
423
+ showStatus: true,
424
+ invertUsage: false,
425
+ windows: {
426
+ showPrimary: true,
427
+ showSecondary: true,
428
+ },
429
+ },
430
+ kiro: {
431
+ showStatus: false,
432
+ windows: {
433
+ showCredits: true,
434
+ },
435
+ },
436
+ zai: {
437
+ showStatus: false,
438
+ windows: {
439
+ showTokens: true,
440
+ showMonthly: true,
441
+ },
442
+ },
443
+ },
444
+ display: {
445
+ alignment: "split",
446
+ barStyle: "both",
447
+ barType: "horizontal-bar",
448
+ barWidth: "fill",
449
+ barCharacter: "heavy",
450
+ containBar: false,
451
+ brailleFillEmpty: false,
452
+ brailleFullBlocks: false,
453
+ colorScheme: "base-warning-error",
454
+ usageColorTargets: {
455
+ title: true,
456
+ timer: true,
457
+ bar: true,
458
+ usageLabel: true,
459
+ status: true,
460
+ },
461
+ resetTimePosition: "front",
462
+ resetTimeFormat: "relative",
463
+ resetTimeContainment: "blank",
464
+ statusIndicatorMode: "icon",
465
+ statusIconPack: "emoji",
466
+ statusIconCustom: "✓⚠×?",
467
+ statusProviderDivider: false,
468
+ statusDismissOk: true,
469
+ showProviderName: true,
470
+ providerLabel: "none",
471
+ providerLabelColon: false,
472
+ providerLabelBold: true,
473
+ baseTextColor: "muted",
474
+ backgroundColor: "text",
475
+ showWindowTitle: true,
476
+ boldWindowTitle: true,
477
+ showUsageLabels: true,
478
+ dividerCharacter: "│",
479
+ dividerColor: "dim",
480
+ dividerBlanks: 1,
481
+ showProviderDivider: true,
482
+ dividerFooterJoin: true,
483
+ showTopDivider: false,
484
+ showBottomDivider: true,
485
+ paddingX: 1,
486
+ widgetPlacement: "belowEditor",
487
+ errorThreshold: 25,
488
+ warningThreshold: 50,
489
+ widgetWrapping: "truncate",
490
+ successThreshold: 75,
491
+ },
492
+
493
+ displayThemes: [],
494
+ displayUserTheme: null,
495
+ pinnedProvider: null,
496
+
497
+ behavior: {
498
+ refreshInterval: 60,
499
+ refreshOnTurnStart: false,
500
+ refreshOnToolResult: false,
501
+ },
502
+ statusRefresh: {
503
+ refreshInterval: 60,
504
+ refreshOnTurnStart: false,
505
+ refreshOnToolResult: false,
506
+ },
507
+ providerOrder: [...PROVIDERS],
508
+ defaultProvider: null,
509
+ };
510
+ }
511
+
512
+ /**
513
+ * Deep merge two objects
514
+ */
515
+ function deepMerge<T extends object>(target: T, source: Partial<T>): T {
516
+ const result = { ...target };
517
+ for (const key of Object.keys(source) as (keyof T)[]) {
518
+ const sourceValue = source[key];
519
+ const targetValue = target[key];
520
+ if (
521
+ sourceValue !== undefined &&
522
+ typeof sourceValue === "object" &&
523
+ sourceValue !== null &&
524
+ !Array.isArray(sourceValue) &&
525
+ typeof targetValue === "object" &&
526
+ targetValue !== null &&
527
+ !Array.isArray(targetValue)
528
+ ) {
529
+ result[key] = deepMerge(targetValue, sourceValue as Partial<typeof targetValue>);
530
+ } else if (sourceValue !== undefined) {
531
+ result[key] = sourceValue as T[keyof T];
532
+ }
533
+ }
534
+ return result;
535
+ }
536
+
537
+ /**
538
+ * Merge settings with defaults (no legacy migrations).
539
+ */
540
+ export function mergeSettings(loaded: Partial<Settings>): Settings {
541
+ return deepMerge(getDefaultSettings(), loaded);
542
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Settings UI entry point (re-export).
3
+ */
4
+
5
+ export { showSettingsUI } from "./settings/ui.js";
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Settings persistence for sub-bar
3
+ */
4
+
5
+ import * as path from "node:path";
6
+ import type { Settings } from "./settings-types.js";
7
+ import { getDefaultSettings, mergeSettings } from "./settings-types.js";
8
+ import { getStorage } from "./storage.js";
9
+ import { getSettingsPath } from "./paths.js";
10
+
11
+ /**
12
+ * Settings file path
13
+ */
14
+ export const SETTINGS_PATH = getSettingsPath();
15
+
16
+ /**
17
+ * In-memory settings cache
18
+ */
19
+ let cachedSettings: Settings | undefined;
20
+
21
+ /**
22
+ * Ensure the settings directory exists
23
+ */
24
+ function ensureSettingsDir(): void {
25
+ const storage = getStorage();
26
+ const dir = path.dirname(SETTINGS_PATH);
27
+ storage.ensureDir(dir);
28
+ }
29
+
30
+ /**
31
+ * Parse settings file contents
32
+ */
33
+ function parseSettings(content: string): Settings {
34
+ const loaded = JSON.parse(content) as Partial<Settings>;
35
+ return mergeSettings({
36
+ version: loaded.version,
37
+ display: loaded.display,
38
+ providers: loaded.providers,
39
+ displayThemes: loaded.displayThemes,
40
+ displayUserTheme: loaded.displayUserTheme,
41
+ pinnedProvider: loaded.pinnedProvider,
42
+ } as Partial<Settings>);
43
+ }
44
+
45
+ function loadSettingsFromDisk(): Settings | null {
46
+ const storage = getStorage();
47
+ try {
48
+ if (storage.exists(SETTINGS_PATH)) {
49
+ const content = storage.readFile(SETTINGS_PATH);
50
+ if (content) {
51
+ return parseSettings(content);
52
+ }
53
+ }
54
+ } catch {
55
+ return null;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Load settings from disk
62
+ */
63
+ export function loadSettings(): Settings {
64
+ if (cachedSettings) {
65
+ return cachedSettings;
66
+ }
67
+
68
+ try {
69
+ const diskSettings = loadSettingsFromDisk();
70
+ if (diskSettings) {
71
+ cachedSettings = diskSettings;
72
+ return cachedSettings;
73
+ }
74
+ } catch (error) {
75
+ console.error(`Failed to load settings from ${SETTINGS_PATH}:`, error);
76
+ }
77
+
78
+ // Return defaults if file doesn't exist or failed to load
79
+ cachedSettings = getDefaultSettings();
80
+ return cachedSettings;
81
+ }
82
+
83
+ /**
84
+ * Save settings to disk
85
+ */
86
+ export function saveSettings(settings: Settings): boolean {
87
+ const storage = getStorage();
88
+ try {
89
+ ensureSettingsDir();
90
+ let next = settings;
91
+ if (cachedSettings) {
92
+ const diskSettings = loadSettingsFromDisk();
93
+ if (diskSettings) {
94
+ const displayChanged = JSON.stringify(settings.display) !== JSON.stringify(cachedSettings.display);
95
+ const providersChanged = JSON.stringify(settings.providers) !== JSON.stringify(cachedSettings.providers);
96
+ const themesChanged = JSON.stringify(settings.displayThemes) !== JSON.stringify(cachedSettings.displayThemes);
97
+ const userThemeChanged = JSON.stringify(settings.displayUserTheme) !== JSON.stringify(cachedSettings.displayUserTheme);
98
+ const pinnedChanged = settings.pinnedProvider !== cachedSettings.pinnedProvider;
99
+
100
+ next = {
101
+ ...diskSettings,
102
+ version: settings.version,
103
+ display: displayChanged ? settings.display : diskSettings.display,
104
+ providers: providersChanged ? settings.providers : diskSettings.providers,
105
+ displayThemes: themesChanged ? settings.displayThemes : diskSettings.displayThemes,
106
+ displayUserTheme: userThemeChanged ? settings.displayUserTheme : diskSettings.displayUserTheme,
107
+ pinnedProvider: pinnedChanged ? settings.pinnedProvider : diskSettings.pinnedProvider,
108
+ };
109
+ }
110
+ }
111
+ const content = JSON.stringify({
112
+ version: next.version,
113
+ display: next.display,
114
+ providers: next.providers,
115
+ displayThemes: next.displayThemes,
116
+ displayUserTheme: next.displayUserTheme,
117
+ pinnedProvider: next.pinnedProvider,
118
+ }, null, 2);
119
+ storage.writeFile(SETTINGS_PATH, content);
120
+ cachedSettings = next;
121
+ return true;
122
+ } catch (error) {
123
+ console.error(`Failed to save settings to ${SETTINGS_PATH}:`, error);
124
+ return false;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Reset settings to defaults
130
+ */
131
+ export function resetSettings(): Settings {
132
+ const defaults = getDefaultSettings();
133
+ const current = getSettings();
134
+ const next = {
135
+ ...current,
136
+ display: defaults.display,
137
+ providers: defaults.providers,
138
+ displayThemes: defaults.displayThemes,
139
+ displayUserTheme: defaults.displayUserTheme,
140
+ pinnedProvider: defaults.pinnedProvider,
141
+ version: defaults.version,
142
+ };
143
+ saveSettings(next);
144
+ return next;
145
+ }
146
+
147
+ /**
148
+ * Get current settings (cached)
149
+ */
150
+ export function getSettings(): Settings {
151
+ return loadSettings();
152
+ }
153
+
154
+ /**
155
+ * Clear the settings cache (force reload on next access)
156
+ */
157
+ export function clearSettingsCache(): void {
158
+ cachedSettings = undefined;
159
+ }