@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,183 @@
1
+ /**
2
+ * Settings menu item builders.
3
+ */
4
+
5
+ import type { SelectItem } from "@mariozechner/pi-tui";
6
+ import type { CoreProviderSettingsMap } from "@eiei114/pi-sub-shared";
7
+ import type { Settings } from "../settings-types.js";
8
+ import type { ProviderName } from "../types.js";
9
+ import { PROVIDERS, PROVIDER_DISPLAY_NAMES } from "../providers/metadata.js";
10
+
11
+ export type TooltipSelectItem = SelectItem & { tooltip?: string };
12
+
13
+ export function buildMainMenuItems(settings: Settings, pinnedProvider?: ProviderName | null): TooltipSelectItem[] {
14
+ const pinnedLabel = pinnedProvider ? PROVIDER_DISPLAY_NAMES[pinnedProvider] : "auto (current provider)";
15
+ const kb = settings.keybindings;
16
+ const kbDesc = `cycle: ${kb.cycleProvider}, reset: ${kb.toggleResetFormat}`;
17
+ return [
18
+ {
19
+ value: "display-theme",
20
+ label: "Themes",
21
+ description: "save, manage, share",
22
+ tooltip: "Save, load, and share display themes.",
23
+ },
24
+ {
25
+ value: "display",
26
+ label: "Adv. Display Settings",
27
+ description: "layout, bars, colors",
28
+ tooltip: "Adjust layout, colors, bar styling, status indicators, and dividers.",
29
+ },
30
+ {
31
+ value: "providers",
32
+ label: "Provider Settings",
33
+ description: "provider specific settings",
34
+ tooltip: "Configure provider display toggles and window visibility.",
35
+ },
36
+ {
37
+ value: "pin-provider",
38
+ label: "Provider Shown",
39
+ description: pinnedLabel,
40
+ tooltip: "Select which provider is shown in the widget.",
41
+ },
42
+ {
43
+ value: "keybindings",
44
+ label: "Keybindings",
45
+ description: kbDesc,
46
+ tooltip: "Configure keyboard shortcuts. Changes take effect after pi restart.",
47
+ },
48
+ {
49
+ value: "open-core-settings",
50
+ label: "Additional settings",
51
+ description: "in /sub-core:settings",
52
+ tooltip: "Open /sub-core:settings for refresh behavior and provider enablement.",
53
+ },
54
+ ];
55
+ }
56
+
57
+ export function buildProviderListItems(settings: Settings, coreProviders?: CoreProviderSettingsMap): TooltipSelectItem[] {
58
+ const orderedProviders = settings.providerOrder.length > 0 ? settings.providerOrder : PROVIDERS;
59
+ const items: TooltipSelectItem[] = orderedProviders.map((provider) => {
60
+ const ps = settings.providers[provider];
61
+ const core = coreProviders?.[provider];
62
+ const enabledValue = core
63
+ ? core.enabled === "auto"
64
+ ? "auto"
65
+ : core.enabled === true || core.enabled === "on"
66
+ ? "on"
67
+ : "off"
68
+ : "auto";
69
+ const status = ps.showStatus ? "status on" : "status off";
70
+ return {
71
+ value: `provider-${provider}`,
72
+ label: PROVIDER_DISPLAY_NAMES[provider],
73
+ description: `enabled ${enabledValue}, ${status}`,
74
+ tooltip: `Configure ${PROVIDER_DISPLAY_NAMES[provider]} display settings.`,
75
+ };
76
+ });
77
+
78
+ items.push({
79
+ value: "reset-providers",
80
+ label: "Reset Provider Defaults",
81
+ description: "restore provider settings",
82
+ tooltip: "Restore provider display settings to their defaults.",
83
+ });
84
+
85
+ return items;
86
+ }
87
+
88
+ export function buildDisplayMenuItems(): TooltipSelectItem[] {
89
+ return [
90
+ {
91
+ value: "display-layout",
92
+ label: "Layout & Structure",
93
+ description: "alignment, wrapping, padding",
94
+ tooltip: "Control alignment, wrapping, and padding.",
95
+ },
96
+ {
97
+ value: "display-bar",
98
+ label: "Bars",
99
+ description: "style, width, character",
100
+ tooltip: "Customize bar type, width, and bar styling.",
101
+ },
102
+ {
103
+ value: "display-provider",
104
+ label: "Labels & Text",
105
+ description: "labels, titles, usage text",
106
+ tooltip: "Adjust provider label visibility and text styling.",
107
+ },
108
+ {
109
+ value: "display-reset",
110
+ label: "Reset Timer",
111
+ description: "position, format, wrapping",
112
+ tooltip: "Control reset timer placement and formatting.",
113
+ },
114
+ {
115
+ value: "display-status",
116
+ label: "Status",
117
+ description: "mode, icons, text",
118
+ tooltip: "Configure status mode and icon packs.",
119
+ },
120
+ {
121
+ value: "display-divider",
122
+ label: "Dividers",
123
+ description: "character, blanks, status separators",
124
+ tooltip: "Change divider character, spacing, status separators, and widget divider lines.",
125
+ },
126
+ {
127
+ value: "display-color",
128
+ label: "Colors",
129
+ description: "base, scheme, thresholds",
130
+ tooltip: "Tune base colors, color scheme, and thresholds.",
131
+ },
132
+ ];
133
+ }
134
+
135
+ export function buildDisplayThemeMenuItems(): TooltipSelectItem[] {
136
+ return [
137
+ {
138
+ value: "display-theme-save",
139
+ label: "Save Theme",
140
+ description: "store current theme",
141
+ tooltip: "Save the current display theme with a custom name.",
142
+ },
143
+ {
144
+ value: "display-theme-load",
145
+ label: "Load & Manage themes",
146
+ description: "load, share, rename and delete themes",
147
+ tooltip: "Load, share, delete, rename, and restore saved themes.",
148
+ },
149
+ {
150
+ value: "display-theme-share",
151
+ label: "Share Theme",
152
+ description: "share current theme",
153
+ tooltip: "Post a share string for the current theme.",
154
+ },
155
+ {
156
+ value: "display-theme-import",
157
+ label: "Import theme",
158
+ description: "from share string",
159
+ tooltip: "Import a shared theme string.",
160
+ },
161
+ {
162
+ value: "display-theme-random",
163
+ label: "Random theme",
164
+ description: "generate a new theme",
165
+ tooltip: "Generate a random display theme as inspiration or a starting point.",
166
+ },
167
+ {
168
+ value: "display-theme-restore",
169
+ label: "Restore previous state",
170
+ description: "restore your last theme",
171
+ tooltip: "Restore your previous display theme.",
172
+ },
173
+ ];
174
+ }
175
+
176
+ export function buildProviderSettingsItems(settings: Settings): TooltipSelectItem[] {
177
+ return buildProviderListItems(settings);
178
+ }
179
+
180
+ export function getProviderFromCategory(category: string): ProviderName | null {
181
+ const match = category.match(/^provider-(\w+)$/);
182
+ return match ? (match[1] as ProviderName) : null;
183
+ }
@@ -0,0 +1,378 @@
1
+ import type { Settings } from "../settings-types.js";
2
+ import type { TooltipSelectItem } from "./menu.js";
3
+
4
+ type DisplaySettings = Settings["display"];
5
+ type BarType = DisplaySettings["barType"];
6
+ type BarStyle = DisplaySettings["barStyle"];
7
+ type BarCharacter = DisplaySettings["barCharacter"];
8
+ type BarWidth = DisplaySettings["barWidth"];
9
+ type DividerCharacter = DisplaySettings["dividerCharacter"];
10
+ type DividerBlanks = DisplaySettings["dividerBlanks"];
11
+ type DisplayAlignment = DisplaySettings["alignment"];
12
+ type OverflowMode = DisplaySettings["overflow"];
13
+ type BaseTextColor = DisplaySettings["baseTextColor"];
14
+ type DividerColor = DisplaySettings["dividerColor"];
15
+ type ResetTimeFormat = DisplaySettings["resetTimeFormat"];
16
+ type ResetTimerContainment = DisplaySettings["resetTimeContainment"];
17
+ type StatusIndicatorMode = DisplaySettings["statusIndicatorMode"];
18
+ type StatusIconPack = DisplaySettings["statusIconPack"];
19
+ type ProviderLabel = DisplaySettings["providerLabel"];
20
+
21
+ const RANDOM_BAR_TYPES: BarType[] = ["horizontal-bar", "horizontal-single", "vertical", "braille", "shade"];
22
+ const RANDOM_BAR_STYLES: BarStyle[] = ["bar", "percentage", "both"];
23
+ const RANDOM_BAR_WIDTHS: BarWidth[] = [1, 4, 6, 8, 10, 12, "fill"];
24
+ const RANDOM_BAR_CHARACTERS: BarCharacter[] = [
25
+ "light",
26
+ "heavy",
27
+ "double",
28
+ "block",
29
+ "▮▯",
30
+ "■□",
31
+ "●○",
32
+ "▲△",
33
+ "◆◇",
34
+ "🚀_",
35
+ ];
36
+ const RANDOM_ALIGNMENTS: DisplayAlignment[] = ["left", "center", "right", "split"];
37
+ const RANDOM_OVERFLOW: OverflowMode[] = ["truncate", "wrap"];
38
+ const RANDOM_RESET_POSITIONS: DisplaySettings["resetTimePosition"][] = ["off", "front", "back", "integrated"];
39
+ const RANDOM_RESET_FORMATS: ResetTimeFormat[] = ["relative", "datetime"];
40
+ const RANDOM_RESET_CONTAINMENTS: ResetTimerContainment[] = ["none", "blank", "()", "[]", "<>"];
41
+ const RANDOM_STATUS_MODES: StatusIndicatorMode[] = ["icon", "text", "icon+text"];
42
+ const RANDOM_STATUS_PACKS: StatusIconPack[] = ["minimal", "emoji"];
43
+ const RANDOM_PROVIDER_LABELS: ProviderLabel[] = ["plan", "subscription", "sub", "none"];
44
+ const RANDOM_DIVIDER_CHARACTERS: DividerCharacter[] = ["none", "blank", "|", "│", "┃", "┆", "┇", "║", "•", "●", "○", "◇"];
45
+ const RANDOM_DIVIDER_BLANKS: DividerBlanks[] = [0, 1, 2, 3];
46
+ const RANDOM_COLOR_SCHEMES: DisplaySettings["colorScheme"][] = [
47
+ "base-warning-error",
48
+ "success-base-warning-error",
49
+ "monochrome",
50
+ ];
51
+ const RANDOM_BASE_TEXT_COLORS: BaseTextColor[] = ["dim", "muted", "text", "primary", "success", "warning", "error", "border", "borderMuted"];
52
+ const RANDOM_BACKGROUND_COLORS: BaseTextColor[] = [
53
+ "text",
54
+ "selectedBg",
55
+ "userMessageBg",
56
+ "customMessageBg",
57
+ "toolPendingBg",
58
+ "toolSuccessBg",
59
+ "toolErrorBg",
60
+ ];
61
+ const RANDOM_DIVIDER_COLORS: DividerColor[] = [
62
+ "primary",
63
+ "text",
64
+ "muted",
65
+ "dim",
66
+ "success",
67
+ "warning",
68
+ "error",
69
+ "border",
70
+ "borderMuted",
71
+ "borderAccent",
72
+ ];
73
+ const RANDOM_PADDING: number[] = [0, 1, 2, 3, 4];
74
+
75
+ function pickRandom<T>(items: readonly T[]): T {
76
+ return items[Math.floor(Math.random() * items.length)] ?? items[0]!;
77
+ }
78
+
79
+ function randomBool(probability = 0.5): boolean {
80
+ return Math.random() < probability;
81
+ }
82
+
83
+ const THEME_ID_LENGTH = 24;
84
+ const THEME_ID_FALLBACK = "theme";
85
+
86
+ function buildThemeId(name: string): string {
87
+ return name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").slice(0, THEME_ID_LENGTH) || THEME_ID_FALLBACK;
88
+ }
89
+
90
+ export interface DisplayThemeTarget {
91
+ id?: string;
92
+ name: string;
93
+ display: Settings["display"];
94
+ deletable: boolean;
95
+ }
96
+
97
+ export function buildDisplayThemeItems(
98
+ settings: Settings,
99
+ ): TooltipSelectItem[] {
100
+ const items: TooltipSelectItem[] = [];
101
+ items.push({
102
+ value: "user",
103
+ label: "Restore backup",
104
+ description: "restore your last theme",
105
+ tooltip: "Restore your previous display theme.",
106
+ });
107
+ items.push({
108
+ value: "default",
109
+ label: "Default",
110
+ description: "restore default settings",
111
+ tooltip: "Reset display settings to defaults.",
112
+ });
113
+ items.push({
114
+ value: "minimal",
115
+ label: "Default Minimal",
116
+ description: "compact display",
117
+ tooltip: "Apply the default minimal theme.",
118
+ });
119
+ items.push({
120
+ value: "default-footer",
121
+ label: "Default Footer",
122
+ description: "status-line optimized default footer style",
123
+ tooltip: "Apply a compact footer-style layout.",
124
+ });
125
+ for (const theme of settings.displayThemes) {
126
+ const description = theme.source === "imported" ? "manually imported theme" : "manually saved theme";
127
+ items.push({
128
+ value: `theme:${theme.id}`,
129
+ label: theme.name,
130
+ description,
131
+ tooltip: `Manage ${theme.name}.`,
132
+ });
133
+ }
134
+ return items;
135
+ }
136
+
137
+ export function resolveDisplayThemeTarget(
138
+ value: string,
139
+ settings: Settings,
140
+ defaults: Settings,
141
+ fallbackUser: Settings["display"] | null,
142
+ ): DisplayThemeTarget | null {
143
+ if (value === "user") {
144
+ const display = settings.displayUserTheme ?? fallbackUser ?? settings.display;
145
+ return { name: "Restore backup", display, deletable: false };
146
+ }
147
+ if (value === "default") {
148
+ return { name: "Default", display: { ...defaults.display }, deletable: false };
149
+ }
150
+ if (value === "default-footer") {
151
+ return {
152
+ name: "Default Footer",
153
+ display: {
154
+ ...defaults.display,
155
+ alignment: "left",
156
+ barWidth: 4,
157
+ showUsageLabels: false,
158
+ statusIndicatorMode: "icon+text",
159
+ statusProviderDivider: true,
160
+ showProviderName: false,
161
+ statusLeadingDivider: true,
162
+ widgetPlacement: "status",
163
+ },
164
+ deletable: false,
165
+ };
166
+ }
167
+ if (value === "minimal") {
168
+ return {
169
+ name: "Default Minimal",
170
+ display: {
171
+ ...defaults.display,
172
+ alignment: "split",
173
+ barStyle: "percentage",
174
+ barType: "horizontal-bar",
175
+ barWidth: 1,
176
+ barCharacter: "heavy",
177
+ containBar: true,
178
+ brailleFillEmpty: false,
179
+ brailleFullBlocks: false,
180
+ colorScheme: "base-warning-error",
181
+ usageColorTargets: {
182
+ title: true,
183
+ timer: true,
184
+ bar: true,
185
+ usageLabel: true,
186
+ status: true,
187
+ },
188
+ resetTimePosition: "off",
189
+ resetTimeFormat: "relative",
190
+ resetTimeContainment: "blank",
191
+ statusIndicatorMode: "icon",
192
+ statusIconPack: "minimal",
193
+ statusProviderDivider: false,
194
+ statusDismissOk: true,
195
+ showProviderName: false,
196
+ providerLabel: "none",
197
+ providerLabelColon: false,
198
+ providerLabelBold: true,
199
+ baseTextColor: "muted",
200
+ backgroundColor: "none",
201
+ showWindowTitle: false,
202
+ boldWindowTitle: true,
203
+ showUsageLabels: false,
204
+ dividerCharacter: "none",
205
+ dividerColor: "dim",
206
+ dividerBlanks: 1,
207
+ showProviderDivider: true,
208
+ statusLeadingDivider: false,
209
+ statusTrailingDivider: false,
210
+ dividerFooterJoin: true,
211
+ showTopDivider: false,
212
+ showBottomDivider: false,
213
+ paddingLeft: 1,
214
+ paddingRight: 1,
215
+ widgetPlacement: "belowEditor",
216
+ errorThreshold: 25,
217
+ warningThreshold: 50,
218
+ overflow: "truncate",
219
+ successThreshold: 75,
220
+ },
221
+ deletable: false,
222
+ };
223
+ }
224
+ if (value.startsWith("theme:")) {
225
+ const id = value.replace("theme:", "");
226
+ const theme = settings.displayThemes.find((entry) => entry.id === id);
227
+ if (!theme) return null;
228
+ return { id: theme.id, name: theme.name, display: theme.display, deletable: true };
229
+ }
230
+ return null;
231
+ }
232
+
233
+ export function buildRandomDisplay(base: DisplaySettings): DisplaySettings {
234
+ const display: DisplaySettings = { ...base };
235
+
236
+ display.alignment = pickRandom(RANDOM_ALIGNMENTS);
237
+ display.overflow = pickRandom(RANDOM_OVERFLOW);
238
+ const padding = pickRandom(RANDOM_PADDING);
239
+ display.paddingLeft = padding;
240
+ display.paddingRight = padding;
241
+ display.barStyle = pickRandom(RANDOM_BAR_STYLES);
242
+ display.barType = pickRandom(RANDOM_BAR_TYPES);
243
+ display.barWidth = pickRandom(RANDOM_BAR_WIDTHS);
244
+ display.barCharacter = pickRandom(RANDOM_BAR_CHARACTERS);
245
+ display.containBar = randomBool();
246
+ display.brailleFillEmpty = randomBool();
247
+ display.brailleFullBlocks = randomBool();
248
+ display.colorScheme = pickRandom(RANDOM_COLOR_SCHEMES);
249
+
250
+ const usageColorTargets = {
251
+ title: randomBool(),
252
+ timer: randomBool(),
253
+ bar: randomBool(),
254
+ usageLabel: randomBool(),
255
+ status: randomBool(),
256
+ };
257
+ if (!usageColorTargets.title && !usageColorTargets.timer && !usageColorTargets.bar && !usageColorTargets.usageLabel && !usageColorTargets.status) {
258
+ usageColorTargets.bar = true;
259
+ }
260
+ display.usageColorTargets = usageColorTargets;
261
+ display.resetTimePosition = pickRandom(RANDOM_RESET_POSITIONS);
262
+ display.resetTimeFormat = pickRandom(RANDOM_RESET_FORMATS);
263
+ display.resetTimeContainment = pickRandom(RANDOM_RESET_CONTAINMENTS);
264
+ display.statusIndicatorMode = pickRandom(RANDOM_STATUS_MODES);
265
+ display.statusIconPack = pickRandom(RANDOM_STATUS_PACKS);
266
+ display.statusProviderDivider = randomBool();
267
+ display.statusDismissOk = randomBool();
268
+ display.showProviderName = randomBool();
269
+ display.providerLabel = pickRandom(RANDOM_PROVIDER_LABELS);
270
+ display.providerLabelColon = display.providerLabel !== "none" && randomBool();
271
+ display.providerLabelBold = randomBool();
272
+ display.baseTextColor = pickRandom(RANDOM_BASE_TEXT_COLORS);
273
+ display.backgroundColor = pickRandom(RANDOM_BACKGROUND_COLORS);
274
+ display.boldWindowTitle = randomBool();
275
+ display.showUsageLabels = randomBool();
276
+ display.dividerCharacter = pickRandom(RANDOM_DIVIDER_CHARACTERS);
277
+ display.dividerColor = pickRandom(RANDOM_DIVIDER_COLORS);
278
+ display.dividerBlanks = pickRandom(RANDOM_DIVIDER_BLANKS);
279
+ display.showProviderDivider = randomBool();
280
+ display.statusLeadingDivider = randomBool();
281
+ display.statusTrailingDivider = randomBool();
282
+ display.dividerFooterJoin = randomBool();
283
+ display.showTopDivider = randomBool();
284
+ display.showBottomDivider = randomBool();
285
+
286
+ if (display.dividerCharacter === "none") {
287
+ display.showProviderDivider = false;
288
+ display.statusLeadingDivider = false;
289
+ display.statusTrailingDivider = false;
290
+ display.dividerFooterJoin = false;
291
+ display.showTopDivider = false;
292
+ display.showBottomDivider = false;
293
+ }
294
+ if (display.providerLabel === "none") {
295
+ display.providerLabelColon = false;
296
+ }
297
+
298
+ return display;
299
+ }
300
+
301
+ export function buildThemeActionItems(target: DisplayThemeTarget): TooltipSelectItem[] {
302
+ const items: TooltipSelectItem[] = [
303
+ {
304
+ value: "load",
305
+ label: "Load",
306
+ description: "apply this theme",
307
+ tooltip: "Apply the selected theme.",
308
+ },
309
+ {
310
+ value: "share",
311
+ label: "Share",
312
+ description: "post share string",
313
+ tooltip: "Post a shareable theme string to chat.",
314
+ },
315
+ ];
316
+ if (target.deletable) {
317
+ items.push({
318
+ value: "rename",
319
+ label: "Rename",
320
+ description: "rename saved theme",
321
+ tooltip: "Rename this saved theme.",
322
+ });
323
+ items.push({
324
+ value: "delete",
325
+ label: "Delete",
326
+ description: "remove saved theme",
327
+ tooltip: "Remove this theme from saved themes.",
328
+ });
329
+ }
330
+ return items;
331
+ }
332
+
333
+ export function upsertDisplayTheme(
334
+ settings: Settings,
335
+ name: string,
336
+ display: Settings["display"],
337
+ source?: "saved" | "imported",
338
+ ): Settings {
339
+ const trimmed = name.trim() || "Theme";
340
+ const id = buildThemeId(trimmed);
341
+ const snapshot = { ...display };
342
+ const existing = settings.displayThemes.find((theme) => theme.id === id);
343
+ const resolvedSource = source ?? existing?.source ?? "saved";
344
+ if (existing) {
345
+ existing.name = trimmed;
346
+ existing.display = snapshot;
347
+ existing.source = resolvedSource;
348
+ } else {
349
+ settings.displayThemes.push({ id, name: trimmed, display: snapshot, source: resolvedSource });
350
+ }
351
+ return settings;
352
+ }
353
+
354
+ export function renameDisplayTheme(settings: Settings, id: string, name: string): Settings {
355
+ const trimmed = name.trim() || "Theme";
356
+ const nextId = buildThemeId(trimmed);
357
+ const existing = settings.displayThemes.find((theme) => theme.id === id);
358
+ if (!existing) return settings;
359
+ if (nextId === id) {
360
+ existing.name = trimmed;
361
+ return settings;
362
+ }
363
+ const collision = settings.displayThemes.find((theme) => theme.id === nextId);
364
+ if (collision) {
365
+ collision.name = trimmed;
366
+ collision.display = existing.display;
367
+ collision.source = existing.source;
368
+ settings.displayThemes = settings.displayThemes.filter((theme) => theme.id !== id);
369
+ return settings;
370
+ }
371
+ existing.id = nextId;
372
+ existing.name = trimmed;
373
+ return settings;
374
+ }
375
+
376
+ export function saveDisplayTheme(settings: Settings, name: string): Settings {
377
+ return upsertDisplayTheme(settings, name, settings.display, "saved");
378
+ }