@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,336 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import type { Theme } from "@mariozechner/pi-coding-agent";
4
+ import { visibleWidth } from "@mariozechner/pi-tui";
5
+ import { formatUsageStatus, formatUsageWindowParts } from "../src/formatting.js";
6
+ import { buildDisplayShareString, decodeDisplayShareString } from "../src/share.js";
7
+ import {
8
+ applyDisplayChange,
9
+ buildDisplayBarItems,
10
+ buildDisplayColorItems,
11
+ buildDisplayDividerItems,
12
+ buildDisplayLayoutItems,
13
+ } from "../src/settings/display.js";
14
+ import { buildDisplayThemeItems, resolveDisplayThemeTarget, saveDisplayTheme, upsertDisplayTheme } from "../src/settings/themes.js";
15
+ import { getDefaultSettings, mergeSettings, resolveBaseTextColor } from "../src/settings-types.js";
16
+ import type { UsageSnapshot } from "../src/types.js";
17
+
18
+ const theme = {
19
+ fg: (_color: string, text: string) => text,
20
+ bold: (text: string) => text,
21
+ } as unknown as Theme;
22
+
23
+ function buildUsage(): UsageSnapshot {
24
+ return {
25
+ provider: "codex",
26
+ displayName: "Codex Plan",
27
+ windows: [
28
+ {
29
+ label: "5h",
30
+ usedPercent: 12,
31
+ resetDescription: "4h",
32
+ resetAt: new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString(),
33
+ },
34
+ ],
35
+ };
36
+ }
37
+
38
+ test("custom provider label is appended", () => {
39
+ const settings = getDefaultSettings();
40
+ settings.display.providerLabel = "Team";
41
+ settings.display.providerLabelColon = true;
42
+
43
+ const output = formatUsageStatus(theme, buildUsage(), undefined, settings);
44
+ assert.ok(output);
45
+ assert.ok(output.startsWith("Codex Team:"));
46
+ });
47
+
48
+ test("custom bar character is used", () => {
49
+ const settings = getDefaultSettings();
50
+ settings.display.barType = "horizontal-bar";
51
+ settings.display.barStyle = "bar";
52
+ settings.display.barWidth = 4;
53
+ settings.display.barCharacter = "★";
54
+
55
+ const usage = buildUsage();
56
+ const parts = formatUsageWindowParts(theme, usage.windows[0], false, settings, usage);
57
+ assert.ok(parts.bar.includes("★"));
58
+ });
59
+
60
+ test("mixed bar characters fill full width", () => {
61
+ const settings = getDefaultSettings();
62
+ settings.display.barType = "horizontal-bar";
63
+ settings.display.barStyle = "bar";
64
+ settings.display.barWidth = 22;
65
+ settings.display.barCharacter = "🚀_";
66
+
67
+ const usage = buildUsage();
68
+ usage.windows[0].usedPercent = 57;
69
+
70
+ const parts = formatUsageWindowParts(theme, usage.windows[0], false, settings, usage);
71
+ assert.equal(visibleWidth(parts.bar), 22);
72
+ assert.ok(parts.bar.includes("🚀"));
73
+ assert.ok(parts.bar.includes("_"));
74
+ });
75
+
76
+ test("applyDisplayChange clamps custom numeric values", () => {
77
+ const settings = getDefaultSettings();
78
+
79
+ applyDisplayChange(settings, "paddingLeft", "-5");
80
+ assert.equal(settings.display.paddingLeft, 0);
81
+
82
+ applyDisplayChange(settings, "paddingRight", "-3");
83
+ assert.equal(settings.display.paddingRight, 0);
84
+
85
+ applyDisplayChange(settings, "barWidth", "150");
86
+ assert.equal(settings.display.barWidth, 100);
87
+
88
+ applyDisplayChange(settings, "dividerBlanks", "-2");
89
+ assert.equal(settings.display.dividerBlanks, 0);
90
+
91
+ applyDisplayChange(settings, "errorThreshold", "-10");
92
+ assert.equal(settings.display.errorThreshold, 0);
93
+
94
+ applyDisplayChange(settings, "warningThreshold", "250");
95
+ assert.equal(settings.display.warningThreshold, 100);
96
+ });
97
+
98
+ test("share string preserves custom values and tolerates unknown colors", () => {
99
+ const defaults = getDefaultSettings();
100
+ const display = {
101
+ ...defaults.display,
102
+ providerLabel: "Team",
103
+ barCharacter: "★",
104
+ baseTextColor: "not-a-color",
105
+ };
106
+
107
+ const share = buildDisplayShareString("Custom", display);
108
+ const decoded = decodeDisplayShareString(share);
109
+ assert.ok(decoded);
110
+ assert.equal(decoded?.display.providerLabel, "Team");
111
+ assert.equal(decoded?.display.barCharacter, "★");
112
+ assert.equal(resolveBaseTextColor(decoded?.display.baseTextColor), "dim");
113
+ });
114
+
115
+
116
+
117
+ test("theme list includes Default Footer preset", () => {
118
+ const items = buildDisplayThemeItems(getDefaultSettings());
119
+ const footerThemeItem = items.find((item) => item.value === "default-footer");
120
+ assert.equal(footerThemeItem?.label, "Default Footer");
121
+ });
122
+
123
+ test("theme source labels imported vs saved", () => {
124
+ const settings = getDefaultSettings();
125
+ upsertDisplayTheme(settings, "Imported", settings.display, "imported");
126
+ saveDisplayTheme(settings, "Saved");
127
+
128
+ const items = buildDisplayThemeItems(settings);
129
+ const importedItem = items.find((item) => item.label === "Imported");
130
+ const savedItem = items.find((item) => item.label === "Saved");
131
+
132
+ assert.equal(importedItem?.description, "manually imported theme");
133
+ assert.equal(savedItem?.description, "manually saved theme");
134
+ });
135
+
136
+ test("imported source persists when updated", () => {
137
+ const settings = getDefaultSettings();
138
+ upsertDisplayTheme(settings, "Imported", settings.display, "imported");
139
+ upsertDisplayTheme(settings, "Imported", { ...settings.display, barWidth: 8 });
140
+
141
+ const items = buildDisplayThemeItems(settings);
142
+ const importedItem = items.find((item) => item.label === "Imported");
143
+ assert.equal(importedItem?.description, "manually imported theme");
144
+ });
145
+
146
+ test("decode marks newer share versions", () => {
147
+ const payload = { v: 99, display: getDefaultSettings().display };
148
+ const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
149
+ const decoded = decodeDisplayShareString(`Future:${encoded}`);
150
+ assert.ok(decoded?.isNewerVersion);
151
+ });
152
+
153
+ test("applyDisplayChange ignores invalid numeric values", () => {
154
+ const settings = getDefaultSettings();
155
+ const initialBarWidth = settings.display.barWidth;
156
+ const initialDividerBlanks = settings.display.dividerBlanks;
157
+ const initialPaddingLeft = settings.display.paddingLeft;
158
+ const initialPaddingRight = settings.display.paddingRight;
159
+
160
+ applyDisplayChange(settings, "barWidth", "abc");
161
+ assert.equal(settings.display.barWidth, initialBarWidth);
162
+
163
+ applyDisplayChange(settings, "dividerBlanks", "nope");
164
+ assert.equal(settings.display.dividerBlanks, initialDividerBlanks);
165
+
166
+ applyDisplayChange(settings, "paddingLeft", "NaN");
167
+ assert.equal(settings.display.paddingLeft, initialPaddingLeft);
168
+
169
+ applyDisplayChange(settings, "paddingRight", "NaN");
170
+ assert.equal(settings.display.paddingRight, initialPaddingRight);
171
+ });
172
+
173
+ test("applyDisplayChange supports fill and numeric values", () => {
174
+ const settings = getDefaultSettings();
175
+
176
+ applyDisplayChange(settings, "barWidth", "fill");
177
+ assert.equal(settings.display.barWidth, "fill");
178
+ applyDisplayChange(settings, "barWidth", "12");
179
+ assert.equal(settings.display.barWidth, 12);
180
+
181
+ applyDisplayChange(settings, "dividerBlanks", "fill");
182
+ assert.equal(settings.display.dividerBlanks, "fill");
183
+ applyDisplayChange(settings, "dividerBlanks", "3");
184
+ assert.equal(settings.display.dividerBlanks, 3);
185
+ });
186
+
187
+ test("background color accepts none", () => {
188
+ const settings = getDefaultSettings();
189
+ applyDisplayChange(settings, "backgroundColor", "none");
190
+ assert.equal(settings.display.backgroundColor, "none");
191
+ });
192
+
193
+ test("display color items include none for background", () => {
194
+ const settings = getDefaultSettings();
195
+ const items = buildDisplayColorItems(settings);
196
+ const backgroundItem = items.find((item) => item.id === "backgroundColor");
197
+ assert.ok(backgroundItem);
198
+ assert.ok(backgroundItem.values?.includes("none"));
199
+ });
200
+
201
+ test("status icon pack parsing handles preview labels", () => {
202
+ const settings = getDefaultSettings();
203
+
204
+ applyDisplayChange(settings, "statusIconPack", "minimal (✓ ⚠ × ?)");
205
+ assert.equal(settings.display.statusIconPack, "minimal");
206
+
207
+ applyDisplayChange(settings, "statusIconPack", "emoji (✅ ⚠️ 🔴 ❓)");
208
+ assert.equal(settings.display.statusIconPack, "emoji");
209
+
210
+ applyDisplayChange(settings, "statusIconPack", "faces (😎 😳 😵 🤔)");
211
+ assert.equal(settings.display.statusIconPack, "custom");
212
+ assert.equal(settings.display.statusIconCustom, "😎😳😵🤔");
213
+
214
+ applyDisplayChange(settings, "statusIconPack", "__custom__");
215
+ assert.equal(settings.display.statusIconPack, "custom");
216
+ });
217
+
218
+ test("applyDisplayChange stores custom status icons", () => {
219
+ const settings = getDefaultSettings();
220
+ applyDisplayChange(settings, "statusIconCustom", "✓⚠×?");
221
+ assert.equal(settings.display.statusIconPack, "custom");
222
+ assert.equal(settings.display.statusIconCustom, "✓⚠×?");
223
+ });
224
+
225
+ test("applyDisplayChange toggles status/provider divider", () => {
226
+ const settings = getDefaultSettings();
227
+ applyDisplayChange(settings, "statusProviderDivider", "on");
228
+ assert.equal(settings.display.statusProviderDivider, true);
229
+ applyDisplayChange(settings, "statusProviderDivider", "off");
230
+ assert.equal(settings.display.statusProviderDivider, false);
231
+ });
232
+
233
+ test("applyDisplayChange accepts custom reset containment", () => {
234
+ const settings = getDefaultSettings();
235
+ applyDisplayChange(settings, "resetTimeContainment", "{}");
236
+ assert.equal(settings.display.resetTimeContainment, "{}");
237
+ });
238
+
239
+ test("layout items expose aboveEditor and hide status-only layout controls", () => {
240
+ const settings = getDefaultSettings();
241
+
242
+ let items = buildDisplayLayoutItems(settings);
243
+ const placementItem = items.find((item) => item.id === "widgetPlacement");
244
+ assert.deepEqual(placementItem?.values, ["aboveEditor", "belowEditor", "status"]);
245
+
246
+ settings.display.widgetPlacement = "status";
247
+ items = buildDisplayLayoutItems(settings);
248
+ assert.ok(!items.some((item) => item.id === "alignment"));
249
+ assert.ok(!items.some((item) => item.id === "overflow"));
250
+ assert.ok(items.some((item) => item.id === "paddingLeft"));
251
+ assert.ok(!items.some((item) => item.id === "paddingRight"));
252
+ });
253
+
254
+ test("status placement hides fill width option", () => {
255
+ const settings = getDefaultSettings();
256
+ settings.display.widgetPlacement = "status";
257
+
258
+ const items = buildDisplayBarItems(settings);
259
+ const barWidthItem = items.find((item) => item.id === "barWidth");
260
+ assert.ok(barWidthItem);
261
+ assert.ok(!(barWidthItem?.values ?? []).includes("fill"));
262
+ });
263
+
264
+ test("divider items switch between widget and status placement options", () => {
265
+ const settings = getDefaultSettings();
266
+
267
+ let items = buildDisplayDividerItems(settings);
268
+ assert.ok(items.some((item) => item.id === "showTopDivider"));
269
+ assert.ok(items.some((item) => item.id === "showBottomDivider"));
270
+ assert.ok(!items.some((item) => item.id === "statusLeadingDivider"));
271
+
272
+ settings.display.widgetPlacement = "status";
273
+ items = buildDisplayDividerItems(settings);
274
+ assert.ok(!items.some((item) => item.id === "showTopDivider"));
275
+ assert.ok(!items.some((item) => item.id === "showBottomDivider"));
276
+ assert.ok(items.some((item) => item.id === "statusLeadingDivider"));
277
+ assert.ok(items.some((item) => item.id === "statusTrailingDivider"));
278
+ });
279
+
280
+ test("applyDisplayChange toggles status edge dividers", () => {
281
+ const settings = getDefaultSettings();
282
+ applyDisplayChange(settings, "statusLeadingDivider", "on");
283
+ applyDisplayChange(settings, "statusTrailingDivider", "on");
284
+ assert.equal(settings.display.statusLeadingDivider, true);
285
+ assert.equal(settings.display.statusTrailingDivider, true);
286
+ });
287
+
288
+
289
+
290
+
291
+
292
+ test("Default Footer preset applies footer defaults", () => {
293
+ const defaults = getDefaultSettings();
294
+ const target = resolveDisplayThemeTarget("default-footer", defaults, defaults, null);
295
+ assert.ok(target);
296
+ assert.equal(target?.name, "Default Footer");
297
+ assert.equal(target?.display.widgetPlacement, "status");
298
+ assert.equal(target?.display.statusIndicatorMode, "icon+text");
299
+ assert.equal(target?.display.barWidth, 4);
300
+ assert.equal(target?.deletable, false);
301
+ });
302
+ test("default widget placement stays belowEditor", () => {
303
+ const settings = getDefaultSettings();
304
+ assert.equal(settings.display.widgetPlacement, "belowEditor");
305
+ });
306
+
307
+ test("status placement normalizes alignment and overflow to line-mode defaults", () => {
308
+ const settings = mergeSettings({
309
+ display: {
310
+ widgetPlacement: "status",
311
+ alignment: "center",
312
+ overflow: "wrap",
313
+ } as any,
314
+ } as any);
315
+ assert.equal(settings.display.widgetPlacement, "status");
316
+ assert.equal(settings.display.alignment, "left");
317
+ assert.equal(settings.display.overflow, "truncate");
318
+ });
319
+
320
+ test("invalid widget placement falls back to belowEditor", () => {
321
+ const settings = mergeSettings({
322
+ display: {
323
+ widgetPlacement: "invalid",
324
+ } as any,
325
+ } as any);
326
+ assert.equal(settings.display.widgetPlacement, "belowEditor");
327
+ });
328
+
329
+ test("decodeDisplayShareString rejects invalid payloads", () => {
330
+ assert.equal(decodeDisplayShareString("NoSeparator"), null);
331
+ assert.equal(decodeDisplayShareString("Name:"), null);
332
+ assert.equal(decodeDisplayShareString("Name:!!!"), null);
333
+
334
+ const nonObjectPayload = Buffer.from(JSON.stringify(42)).toString("base64url");
335
+ assert.equal(decodeDisplayShareString(`Name:${nonObjectPayload}`), null);
336
+ });
@@ -0,0 +1,27 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { getStatusLabel } from "../src/status.js";
4
+ import type { ProviderStatus } from "../src/types.js";
5
+
6
+ function buildStatus(indicator: ProviderStatus["indicator"], description?: string): ProviderStatus {
7
+ return { indicator, description };
8
+ }
9
+
10
+ test("abbreviated status labels include Status prefix", () => {
11
+ assert.equal(getStatusLabel(buildStatus("major"), true), "Status Crit.");
12
+ assert.equal(getStatusLabel(buildStatus("critical"), true), "Status Crit.");
13
+ assert.equal(getStatusLabel(buildStatus("maintenance"), true), "Status Maint.");
14
+ assert.equal(getStatusLabel(buildStatus("unknown"), true), "Status Unk.");
15
+ assert.equal(getStatusLabel(buildStatus("minor"), true), "Status Degr.");
16
+ assert.equal(getStatusLabel(buildStatus("none"), true), "Status OK");
17
+ });
18
+
19
+
20
+ test("status label description is still preferred in full mode", () => {
21
+ assert.equal(getStatusLabel(buildStatus("unknown", "Service is unavailable")), "Service is unavailable");
22
+ });
23
+
24
+
25
+ test("full status labels still include Status Unknown", () => {
26
+ assert.equal(getStatusLabel(buildStatus("unknown")), "Status Unknown");
27
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["index.ts", "src/**/*.ts"],
4
+ "exclude": ["node_modules"]
5
+ }