@pi-unipi/info-screen 0.1.1 → 0.1.2
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/index.ts +23 -17
- package/package.json +1 -1
- package/settings/settings-tui.ts +52 -1
- package/tui/info-overlay.ts +128 -35
- package/types.ts +3 -0
package/index.ts
CHANGED
|
@@ -99,18 +99,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
99
99
|
// Wait for other modules to announce
|
|
100
100
|
await waitForModules();
|
|
101
101
|
|
|
102
|
-
// Show the overlay
|
|
102
|
+
// Show the overlay using three-method object pattern
|
|
103
103
|
ctx.ui.custom(
|
|
104
104
|
(tui, _theme, _keybindings, done) => {
|
|
105
105
|
const overlay = new InfoOverlay();
|
|
106
106
|
overlay.onClose = () => done(undefined);
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
// Return three-method object as per pi-tui docs
|
|
108
|
+
return {
|
|
109
|
+
render: (w: number) => overlay.render(w),
|
|
110
|
+
invalidate: () => overlay.invalidate(),
|
|
111
|
+
handleInput: (data: string) => {
|
|
112
|
+
overlay.handleInput?.(data);
|
|
113
|
+
tui.requestRender();
|
|
114
|
+
},
|
|
112
115
|
};
|
|
113
|
-
return overlay;
|
|
114
116
|
},
|
|
115
117
|
{
|
|
116
118
|
overlay: true,
|
|
@@ -141,12 +143,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
141
143
|
(tui, _theme, _keybindings, done) => {
|
|
142
144
|
const overlay = new InfoOverlay();
|
|
143
145
|
overlay.onClose = () => done(undefined);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
return {
|
|
147
|
+
render: (w: number) => overlay.render(w),
|
|
148
|
+
invalidate: () => overlay.invalidate(),
|
|
149
|
+
handleInput: (data: string) => {
|
|
150
|
+
overlay.handleInput?.(data);
|
|
151
|
+
tui.requestRender();
|
|
152
|
+
},
|
|
148
153
|
};
|
|
149
|
-
return overlay;
|
|
150
154
|
},
|
|
151
155
|
{
|
|
152
156
|
overlay: true,
|
|
@@ -169,12 +173,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
173
|
(tui, _theme, _keybindings, done) => {
|
|
170
174
|
const overlay = new SettingsOverlay();
|
|
171
175
|
overlay.onClose = () => done(undefined);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
return {
|
|
177
|
+
render: (w: number) => overlay.render(w),
|
|
178
|
+
invalidate: () => overlay.invalidate(),
|
|
179
|
+
handleInput: (data: string) => {
|
|
180
|
+
overlay.handleInput?.(data);
|
|
181
|
+
tui.requestRender();
|
|
182
|
+
},
|
|
176
183
|
};
|
|
177
|
-
return overlay;
|
|
178
184
|
},
|
|
179
185
|
{
|
|
180
186
|
overlay: true,
|
package/package.json
CHANGED
package/settings/settings-tui.ts
CHANGED
|
@@ -47,6 +47,15 @@ export class SettingsOverlay implements Component {
|
|
|
47
47
|
name: g.name,
|
|
48
48
|
icon: g.icon,
|
|
49
49
|
}));
|
|
50
|
+
// Apply saved order if exists
|
|
51
|
+
if (this.settings.groupOrder) {
|
|
52
|
+
const order = this.settings.groupOrder;
|
|
53
|
+
this.groups.sort((a, b) => {
|
|
54
|
+
const ai = order.indexOf(a.id);
|
|
55
|
+
const bi = order.indexOf(b.id);
|
|
56
|
+
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
/**
|
|
@@ -88,6 +97,12 @@ export class SettingsOverlay implements Component {
|
|
|
88
97
|
case "l":
|
|
89
98
|
this.enterStatsMode(this.groups[this.selectedIndex].id);
|
|
90
99
|
break;
|
|
100
|
+
case "J": // Shift+J - move group down
|
|
101
|
+
this.moveGroupDown();
|
|
102
|
+
break;
|
|
103
|
+
case "K": // Shift+K - move group up
|
|
104
|
+
this.moveGroupUp();
|
|
105
|
+
break;
|
|
91
106
|
case "q": // Quit
|
|
92
107
|
case "\x1b": // Escape
|
|
93
108
|
this.onClose?.();
|
|
@@ -166,6 +181,42 @@ export class SettingsOverlay implements Component {
|
|
|
166
181
|
this.selectedIndex = 0;
|
|
167
182
|
}
|
|
168
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Move selected group up in order.
|
|
186
|
+
*/
|
|
187
|
+
private moveGroupUp(): void {
|
|
188
|
+
if (this.selectedIndex <= 0) return;
|
|
189
|
+
const i = this.selectedIndex;
|
|
190
|
+
// Swap with previous
|
|
191
|
+
const temp = this.groups[i]!;
|
|
192
|
+
this.groups[i] = this.groups[i - 1]!;
|
|
193
|
+
this.groups[i - 1] = temp;
|
|
194
|
+
this.selectedIndex--;
|
|
195
|
+
this.saveGroupOrder();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Move selected group down in order.
|
|
200
|
+
*/
|
|
201
|
+
private moveGroupDown(): void {
|
|
202
|
+
if (this.selectedIndex >= this.groups.length - 1) return;
|
|
203
|
+
const i = this.selectedIndex;
|
|
204
|
+
// Swap with next
|
|
205
|
+
const temp = this.groups[i]!;
|
|
206
|
+
this.groups[i] = this.groups[i + 1]!;
|
|
207
|
+
this.groups[i + 1] = temp;
|
|
208
|
+
this.selectedIndex++;
|
|
209
|
+
this.saveGroupOrder();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Save group order to settings.
|
|
214
|
+
*/
|
|
215
|
+
private saveGroupOrder(): void {
|
|
216
|
+
this.settings.groupOrder = this.groups.map((g) => g.id);
|
|
217
|
+
saveInfoSettings(this.settings);
|
|
218
|
+
}
|
|
219
|
+
|
|
169
220
|
/**
|
|
170
221
|
* Render the component.
|
|
171
222
|
*/
|
|
@@ -218,7 +269,7 @@ export class SettingsOverlay implements Component {
|
|
|
218
269
|
// Footer
|
|
219
270
|
lines.push("");
|
|
220
271
|
lines.push(ansi.dim + "─".repeat(width) + ansi.reset);
|
|
221
|
-
lines.push(this.renderCentered(`${ansi.dim}↑↓ select Space toggle → stats q close${ansi.reset}`, width));
|
|
272
|
+
lines.push(this.renderCentered(`${ansi.dim}↑↓ select Space toggle → stats J/K reorder q close${ansi.reset}`, width));
|
|
222
273
|
lines.push("");
|
|
223
274
|
|
|
224
275
|
return lines;
|
package/tui/info-overlay.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import type { Component } from "@mariozechner/pi-tui";
|
|
9
9
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
10
10
|
import { infoRegistry } from "../registry.js";
|
|
11
|
-
import
|
|
11
|
+
import { getInfoSettings } from "../config.js";
|
|
12
|
+
import type { InfoGroup, GroupData } from "../types.js";
|
|
12
13
|
|
|
13
14
|
/** ANSI escape codes */
|
|
14
15
|
const ansi = {
|
|
@@ -25,6 +26,9 @@ const ansi = {
|
|
|
25
26
|
white: "\x1b[37m",
|
|
26
27
|
red: "\x1b[31m",
|
|
27
28
|
gray: "\x1b[90m",
|
|
29
|
+
// Backgrounds
|
|
30
|
+
bgDarkGray: "\x1b[48;5;235m",
|
|
31
|
+
bgDarkerGray: "\x1b[48;5;233m",
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
/** Tab color palette */
|
|
@@ -46,6 +50,8 @@ export class InfoOverlay implements Component {
|
|
|
46
50
|
private loading = true;
|
|
47
51
|
private error: string | null = null;
|
|
48
52
|
private scrollOffset = 0;
|
|
53
|
+
/** Tab scroll offset for horizontal scrolling */
|
|
54
|
+
private tabScrollOffset = 0;
|
|
49
55
|
/** Callback when overlay should close */
|
|
50
56
|
onClose?: () => void;
|
|
51
57
|
|
|
@@ -67,6 +73,17 @@ export class InfoOverlay implements Component {
|
|
|
67
73
|
this.loading = true;
|
|
68
74
|
// Always re-fetch ALL groups to catch late registrations
|
|
69
75
|
this.groups = infoRegistry.getAllGroups();
|
|
76
|
+
|
|
77
|
+
// Apply saved order from settings
|
|
78
|
+
const settings = getInfoSettings();
|
|
79
|
+
if (settings.groupOrder && settings.groupOrder.length > 0) {
|
|
80
|
+
const order = settings.groupOrder;
|
|
81
|
+
this.groups.sort((a, b) => {
|
|
82
|
+
const ai = order.indexOf(a.id);
|
|
83
|
+
const bi = order.indexOf(b.id);
|
|
84
|
+
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
70
87
|
|
|
71
88
|
try {
|
|
72
89
|
// Load data for all groups in parallel
|
|
@@ -94,10 +111,12 @@ export class InfoOverlay implements Component {
|
|
|
94
111
|
// Right arrow - switch tab
|
|
95
112
|
this.activeTabIndex = (this.activeTabIndex + 1) % this.groups.length;
|
|
96
113
|
this.scrollOffset = 0; // Reset scroll on tab switch
|
|
114
|
+
this.ensureTabVisible();
|
|
97
115
|
} else if (data === "\x1b[D" || data === "h") {
|
|
98
116
|
// Left arrow - switch tab
|
|
99
117
|
this.activeTabIndex = (this.activeTabIndex - 1 + this.groups.length) % this.groups.length;
|
|
100
118
|
this.scrollOffset = 0; // Reset scroll on tab switch
|
|
119
|
+
this.ensureTabVisible();
|
|
101
120
|
} else if (data === "\x1b[B" || data === "j") {
|
|
102
121
|
// Down arrow - scroll down
|
|
103
122
|
this.scrollOffset++;
|
|
@@ -116,6 +135,14 @@ export class InfoOverlay implements Component {
|
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Ensure active tab is visible in the tab bar (horizontal scroll).
|
|
140
|
+
*/
|
|
141
|
+
private ensureTabVisible(): void {
|
|
142
|
+
// Tab bar shows ~maxTabsVisible tabs, centered around active
|
|
143
|
+
// This is handled in renderTabBar
|
|
144
|
+
}
|
|
145
|
+
|
|
119
146
|
/**
|
|
120
147
|
* Render the component.
|
|
121
148
|
*/
|
|
@@ -135,6 +162,16 @@ export class InfoOverlay implements Component {
|
|
|
135
162
|
|
|
136
163
|
if (groupIds !== currentIds) {
|
|
137
164
|
this.groups = allGroups;
|
|
165
|
+
// Apply saved order
|
|
166
|
+
const settings = getInfoSettings();
|
|
167
|
+
if (settings.groupOrder && settings.groupOrder.length > 0) {
|
|
168
|
+
const order = settings.groupOrder;
|
|
169
|
+
this.groups.sort((a, b) => {
|
|
170
|
+
const ai = order.indexOf(a.id);
|
|
171
|
+
const bi = order.indexOf(b.id);
|
|
172
|
+
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
138
175
|
// Load data for any new groups (non-blocking)
|
|
139
176
|
this.loadDataForNewGroups(allGroups);
|
|
140
177
|
}
|
|
@@ -169,11 +206,9 @@ export class InfoOverlay implements Component {
|
|
|
169
206
|
const lines: string[] = [];
|
|
170
207
|
const padding = " ".repeat(Math.max(0, Math.floor((width - 20) / 2)));
|
|
171
208
|
|
|
172
|
-
lines.push("");
|
|
173
209
|
lines.push(`${padding}${ansi.cyan}${ansi.bold}📊 UniPi Info Screen${ansi.reset}`);
|
|
174
210
|
lines.push("");
|
|
175
211
|
lines.push(`${padding}${ansi.dim}Loading dashboard...${ansi.reset}`);
|
|
176
|
-
lines.push("");
|
|
177
212
|
|
|
178
213
|
return lines;
|
|
179
214
|
}
|
|
@@ -185,10 +220,8 @@ export class InfoOverlay implements Component {
|
|
|
185
220
|
const lines: string[] = [];
|
|
186
221
|
const padding = " ".repeat(Math.max(0, Math.floor((width - 20) / 2)));
|
|
187
222
|
|
|
188
|
-
lines.push("");
|
|
189
223
|
lines.push(`${padding}${ansi.yellow}${ansi.bold}⚠️ Error${ansi.reset}`);
|
|
190
224
|
lines.push(`${padding}${ansi.dim}${this.error ?? "Unknown error"}${ansi.reset}`);
|
|
191
|
-
lines.push("");
|
|
192
225
|
|
|
193
226
|
return lines;
|
|
194
227
|
}
|
|
@@ -200,22 +233,23 @@ export class InfoOverlay implements Component {
|
|
|
200
233
|
const lines: string[] = [];
|
|
201
234
|
const padding = " ".repeat(Math.max(0, Math.floor((width - 30) / 2)));
|
|
202
235
|
|
|
203
|
-
lines.push("");
|
|
204
236
|
lines.push(`${padding}${ansi.cyan}${ansi.bold}📊 UniPi Info Screen${ansi.reset}`);
|
|
205
237
|
lines.push("");
|
|
206
238
|
lines.push(`${padding}${ansi.dim}No groups registered.${ansi.reset}`);
|
|
207
239
|
lines.push(`${padding}${ansi.dim}Modules will register groups on startup.${ansi.reset}`);
|
|
208
|
-
lines.push("");
|
|
209
240
|
|
|
210
241
|
return lines;
|
|
211
242
|
}
|
|
212
243
|
|
|
213
244
|
/**
|
|
214
|
-
* Pad a line to fill a target visual width.
|
|
245
|
+
* Pad a line to fill a target visual width with background.
|
|
215
246
|
*/
|
|
216
|
-
private padToWidth(line: string, targetWidth: number): string {
|
|
247
|
+
private padToWidth(line: string, targetWidth: number, bg?: string): string {
|
|
217
248
|
const visLen = visibleWidth(line);
|
|
218
249
|
const pad = Math.max(0, targetWidth - visLen);
|
|
250
|
+
if (bg) {
|
|
251
|
+
return bg + line + " ".repeat(pad) + ansi.reset;
|
|
252
|
+
}
|
|
219
253
|
return line + " ".repeat(pad);
|
|
220
254
|
}
|
|
221
255
|
|
|
@@ -227,19 +261,22 @@ export class InfoOverlay implements Component {
|
|
|
227
261
|
const group = this.groups[this.activeTabIndex];
|
|
228
262
|
const data = this.groupData.get(group.id) ?? {};
|
|
229
263
|
// Inner width for content (subtract 2 for left+right borders)
|
|
230
|
-
|
|
231
|
-
|
|
264
|
+
const innerWidth = width - 2;
|
|
265
|
+
|
|
266
|
+
// Background colors
|
|
267
|
+
const bgHeader = ansi.bgDarkGray;
|
|
268
|
+
const bgContent = ansi.bgDarkerGray;
|
|
232
269
|
|
|
233
270
|
// Top border
|
|
234
|
-
lines.push(`${ansi.dim}╭${"─".repeat(innerWidth
|
|
271
|
+
lines.push(`${ansi.dim}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
|
|
235
272
|
|
|
236
|
-
// Header
|
|
237
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderHeader(innerWidth, group), innerWidth)}${ansi.dim}
|
|
238
|
-
lines.push(`${ansi.dim}├${"─".repeat(innerWidth
|
|
273
|
+
// Header with background
|
|
274
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderHeader(innerWidth, group), innerWidth, bgHeader)}${ansi.dim}│${ansi.reset}`);
|
|
275
|
+
lines.push(`${ansi.dim}├${"─".repeat(innerWidth)}┤${ansi.reset}`);
|
|
239
276
|
|
|
240
|
-
// Tab bar
|
|
241
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderTabBar(innerWidth), innerWidth)}${ansi.dim}
|
|
242
|
-
lines.push(`${ansi.dim}├${"─".repeat(innerWidth
|
|
277
|
+
// Tab bar with horizontal scrolling
|
|
278
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderTabBar(innerWidth), innerWidth, bgHeader)}${ansi.dim}│${ansi.reset}`);
|
|
279
|
+
lines.push(`${ansi.dim}├${"─".repeat(innerWidth)}┤${ansi.reset}`);
|
|
243
280
|
|
|
244
281
|
// Content with scrolling
|
|
245
282
|
const contentLines = this.renderGroupContent(innerWidth, group, data);
|
|
@@ -253,20 +290,20 @@ export class InfoOverlay implements Component {
|
|
|
253
290
|
const visibleContent = contentLines.slice(this.scrollOffset, this.scrollOffset + maxVisibleLines);
|
|
254
291
|
|
|
255
292
|
for (const line of visibleContent) {
|
|
256
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(line, innerWidth)}${ansi.dim}
|
|
293
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(line, innerWidth, bgContent)}${ansi.dim}│${ansi.reset}`);
|
|
257
294
|
}
|
|
258
295
|
|
|
259
296
|
// Show scroll indicator if needed
|
|
260
297
|
if (contentLines.length > maxVisibleLines) {
|
|
261
298
|
const scrollInfo = ` ${this.scrollOffset + 1}-${Math.min(this.scrollOffset + maxVisibleLines, contentLines.length)}/${contentLines.length} `;
|
|
262
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(scrollInfo, innerWidth)}${ansi.dim}
|
|
299
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(scrollInfo, innerWidth, bgContent)}${ansi.dim}│${ansi.reset}`);
|
|
263
300
|
}
|
|
264
301
|
|
|
265
302
|
// Footer
|
|
266
303
|
const hasScroll = contentLines.length > maxVisibleLines;
|
|
267
|
-
lines.push(`${ansi.dim}├${"─".repeat(innerWidth
|
|
268
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderFooter(innerWidth, hasScroll), innerWidth)}${ansi.dim}
|
|
269
|
-
lines.push(`${ansi.dim}╰${"─".repeat(innerWidth
|
|
304
|
+
lines.push(`${ansi.dim}├${"─".repeat(innerWidth)}┤${ansi.reset}`);
|
|
305
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderFooter(innerWidth, hasScroll), innerWidth, bgHeader)}${ansi.dim}│${ansi.reset}`);
|
|
306
|
+
lines.push(`${ansi.dim}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
|
|
270
307
|
|
|
271
308
|
return lines;
|
|
272
309
|
}
|
|
@@ -290,15 +327,78 @@ export class InfoOverlay implements Component {
|
|
|
290
327
|
}
|
|
291
328
|
|
|
292
329
|
/**
|
|
293
|
-
* Render tab bar.
|
|
330
|
+
* Render tab bar with horizontal scrolling.
|
|
331
|
+
* When tabs overflow, slides to keep active tab visible.
|
|
294
332
|
*/
|
|
295
333
|
private renderTabBar(width: number): string {
|
|
334
|
+
if (this.groups.length === 0) return "";
|
|
335
|
+
|
|
336
|
+
// Calculate tab widths
|
|
337
|
+
const tabWidths = this.groups.map(g => visibleWidth(` ${g.icon} ${g.name} `));
|
|
338
|
+
const separatorWidth = visibleWidth(`${ansi.dim}│${ansi.reset}`);
|
|
339
|
+
|
|
340
|
+
// Find how many tabs fit
|
|
341
|
+
let maxTabs = 0;
|
|
342
|
+
let totalWidth = 0;
|
|
343
|
+
for (let i = 0; i < this.groups.length; i++) {
|
|
344
|
+
const tabW = tabWidths[i]!;
|
|
345
|
+
const sepW = i > 0 ? separatorWidth : 0;
|
|
346
|
+
if (totalWidth + sepW + tabW > width - 2) break;
|
|
347
|
+
totalWidth += sepW + tabW;
|
|
348
|
+
maxTabs = i + 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// If all tabs fit, show all
|
|
352
|
+
if (maxTabs >= this.groups.length) {
|
|
353
|
+
return this.renderAllTabs(width);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Calculate scroll offset to keep active tab visible
|
|
357
|
+
// Center the active tab in the visible window
|
|
358
|
+
const halfVisible = Math.floor(maxTabs / 2);
|
|
359
|
+
let startIdx = this.activeTabIndex - halfVisible;
|
|
360
|
+
startIdx = Math.max(0, Math.min(startIdx, this.groups.length - maxTabs));
|
|
361
|
+
|
|
362
|
+
// Build visible tabs
|
|
363
|
+
const tabs: string[] = [];
|
|
364
|
+
for (let i = startIdx; i < startIdx + maxTabs && i < this.groups.length; i++) {
|
|
365
|
+
const group = this.groups[i]!;
|
|
366
|
+
const isActive = i === this.activeTabIndex;
|
|
367
|
+
const color = TAB_COLORS[i % TAB_COLORS.length]!;
|
|
368
|
+
|
|
369
|
+
if (isActive) {
|
|
370
|
+
tabs.push(`${color}${ansi.bold} ${group.icon} ${group.name} ${ansi.reset}`);
|
|
371
|
+
} else {
|
|
372
|
+
tabs.push(`${ansi.dim} ${group.icon} ${group.name} ${ansi.reset}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const tabStr = tabs.join(`${ansi.dim}│${ansi.reset}`);
|
|
377
|
+
|
|
378
|
+
// Add scroll indicators
|
|
379
|
+
const hasLeft = startIdx > 0;
|
|
380
|
+
const hasRight = startIdx + maxTabs < this.groups.length;
|
|
381
|
+
|
|
382
|
+
if (hasLeft) {
|
|
383
|
+
return `${ansi.dim}◀${ansi.reset} ${tabStr}`;
|
|
384
|
+
}
|
|
385
|
+
if (hasRight) {
|
|
386
|
+
return `${tabStr} ${ansi.dim}▶${ansi.reset}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return tabStr;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Render all tabs (when they all fit).
|
|
394
|
+
*/
|
|
395
|
+
private renderAllTabs(width: number): string {
|
|
296
396
|
const tabs: string[] = [];
|
|
297
397
|
|
|
298
398
|
for (let i = 0; i < this.groups.length; i++) {
|
|
299
|
-
const group = this.groups[i]
|
|
399
|
+
const group = this.groups[i]!;
|
|
300
400
|
const isActive = i === this.activeTabIndex;
|
|
301
|
-
const color = TAB_COLORS[i % TAB_COLORS.length]
|
|
401
|
+
const color = TAB_COLORS[i % TAB_COLORS.length]!;
|
|
302
402
|
|
|
303
403
|
if (isActive) {
|
|
304
404
|
tabs.push(`${color}${ansi.bold} ${group.icon} ${group.name} ${ansi.reset}`);
|
|
@@ -310,7 +410,7 @@ export class InfoOverlay implements Component {
|
|
|
310
410
|
const tabStr = tabs.join(`${ansi.dim}│${ansi.reset}`);
|
|
311
411
|
const visLen = visibleWidth(tabStr);
|
|
312
412
|
|
|
313
|
-
// Truncate if too wide
|
|
413
|
+
// Truncate if too wide (shouldn't happen if maxTabs calculation is correct)
|
|
314
414
|
if (visLen > width - 2) {
|
|
315
415
|
return truncateToWidth(tabStr, width - 2);
|
|
316
416
|
}
|
|
@@ -318,13 +418,6 @@ export class InfoOverlay implements Component {
|
|
|
318
418
|
return tabStr;
|
|
319
419
|
}
|
|
320
420
|
|
|
321
|
-
/**
|
|
322
|
-
* Render a separator line.
|
|
323
|
-
*/
|
|
324
|
-
private renderSeparator(width: number): string {
|
|
325
|
-
return ansi.dim + "─".repeat(width) + ansi.reset;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
421
|
/**
|
|
329
422
|
* Render group content.
|
|
330
423
|
*/
|
|
@@ -383,7 +476,7 @@ export class InfoOverlay implements Component {
|
|
|
383
476
|
/**
|
|
384
477
|
* Render footer with navigation hints.
|
|
385
478
|
*/
|
|
386
|
-
private renderFooter(width: number,
|
|
479
|
+
private renderFooter(width: number, _hasScroll?: boolean): string {
|
|
387
480
|
const hints = [
|
|
388
481
|
`${ansi.cyan}←/→${ansi.reset} tabs`,
|
|
389
482
|
];
|
package/types.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface InfoScreenSettings {
|
|
|
55
55
|
bootTimeoutMs: number;
|
|
56
56
|
/** Per-group settings */
|
|
57
57
|
groups: Record<string, GroupSettings>;
|
|
58
|
+
/** Group display order (array of group ids) */
|
|
59
|
+
groupOrder?: string[];
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/** Settings for a single group */
|
|
@@ -70,4 +72,5 @@ export const DEFAULT_SETTINGS: InfoScreenSettings = {
|
|
|
70
72
|
showOnBoot: true,
|
|
71
73
|
bootTimeoutMs: 2000,
|
|
72
74
|
groups: {},
|
|
75
|
+
groupOrder: [],
|
|
73
76
|
};
|