@pi-unipi/info-screen 0.1.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.
- package/config.ts +171 -0
- package/core-groups.ts +482 -0
- package/index.ts +191 -0
- package/package.json +50 -0
- package/registry.ts +183 -0
- package/settings/settings-tui.ts +287 -0
- package/tui/info-overlay.ts +406 -0
- package/types.ts +73 -0
- package/usage-parser.ts +308 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/info-screen — TUI Overlay Component
|
|
3
|
+
*
|
|
4
|
+
* Main dashboard overlay with tabbed navigation.
|
|
5
|
+
* Displays registered groups as tabs with their stats.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
9
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
10
|
+
import { infoRegistry } from "../registry.js";
|
|
11
|
+
import type { InfoGroup, GroupData, StatData } from "../types.js";
|
|
12
|
+
|
|
13
|
+
/** ANSI escape codes */
|
|
14
|
+
const ansi = {
|
|
15
|
+
reset: "\x1b[0m",
|
|
16
|
+
bold: "\x1b[1m",
|
|
17
|
+
dim: "\x1b[2m",
|
|
18
|
+
underline: "\x1b[4m",
|
|
19
|
+
// Colors
|
|
20
|
+
blue: "\x1b[34m",
|
|
21
|
+
cyan: "\x1b[36m",
|
|
22
|
+
green: "\x1b[32m",
|
|
23
|
+
yellow: "\x1b[33m",
|
|
24
|
+
magenta: "\x1b[35m",
|
|
25
|
+
white: "\x1b[37m",
|
|
26
|
+
red: "\x1b[31m",
|
|
27
|
+
gray: "\x1b[90m",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Tab color palette */
|
|
31
|
+
const TAB_COLORS = [
|
|
32
|
+
ansi.cyan,
|
|
33
|
+
ansi.green,
|
|
34
|
+
ansi.yellow,
|
|
35
|
+
ansi.magenta,
|
|
36
|
+
ansi.blue,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Info overlay component with tabbed navigation.
|
|
41
|
+
*/
|
|
42
|
+
export class InfoOverlay implements Component {
|
|
43
|
+
private groups: InfoGroup[] = [];
|
|
44
|
+
private activeTabIndex = 0;
|
|
45
|
+
private groupData = new Map<string, GroupData>();
|
|
46
|
+
private loading = true;
|
|
47
|
+
private error: string | null = null;
|
|
48
|
+
private scrollOffset = 0;
|
|
49
|
+
/** Callback when overlay should close */
|
|
50
|
+
onClose?: () => void;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
this.loadData();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Invalidate cached render state.
|
|
58
|
+
*/
|
|
59
|
+
invalidate(): void {
|
|
60
|
+
// No cached state to invalidate
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load data for all groups.
|
|
65
|
+
*/
|
|
66
|
+
private async loadData(): Promise<void> {
|
|
67
|
+
this.loading = true;
|
|
68
|
+
// Always re-fetch ALL groups to catch late registrations
|
|
69
|
+
this.groups = infoRegistry.getAllGroups();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Load data for all groups in parallel
|
|
73
|
+
const promises = this.groups.map(async (group) => {
|
|
74
|
+
const data = await infoRegistry.getGroupData(group.id);
|
|
75
|
+
this.groupData.set(group.id, data);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await Promise.all(promises);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.loading = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handle keyboard input.
|
|
88
|
+
*/
|
|
89
|
+
handleInput(data: string): void {
|
|
90
|
+
if (this.loading) return;
|
|
91
|
+
|
|
92
|
+
// Arrow keys for tab navigation
|
|
93
|
+
if (data === "\x1b[C" || data === "l") {
|
|
94
|
+
// Right arrow - switch tab
|
|
95
|
+
this.activeTabIndex = (this.activeTabIndex + 1) % this.groups.length;
|
|
96
|
+
this.scrollOffset = 0; // Reset scroll on tab switch
|
|
97
|
+
} else if (data === "\x1b[D" || data === "h") {
|
|
98
|
+
// Left arrow - switch tab
|
|
99
|
+
this.activeTabIndex = (this.activeTabIndex - 1 + this.groups.length) % this.groups.length;
|
|
100
|
+
this.scrollOffset = 0; // Reset scroll on tab switch
|
|
101
|
+
} else if (data === "\x1b[B" || data === "j") {
|
|
102
|
+
// Down arrow - scroll down
|
|
103
|
+
this.scrollOffset++;
|
|
104
|
+
} else if (data === "\x1b[A" || data === "k") {
|
|
105
|
+
// Up arrow - scroll up
|
|
106
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
107
|
+
} else if (data === "g") {
|
|
108
|
+
// g - go to top
|
|
109
|
+
this.scrollOffset = 0;
|
|
110
|
+
} else if (data === "G") {
|
|
111
|
+
// G - go to bottom (will be clamped in render)
|
|
112
|
+
this.scrollOffset = Infinity;
|
|
113
|
+
} else if (data === "q" || data === "\x1b") {
|
|
114
|
+
// q or Escape - close overlay
|
|
115
|
+
this.onClose?.();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Render the component.
|
|
121
|
+
*/
|
|
122
|
+
render(width: number): string[] {
|
|
123
|
+
if (this.loading) {
|
|
124
|
+
return this.renderLoading(width);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (this.error) {
|
|
128
|
+
return this.renderError(width);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for new groups (but don't re-trigger loading)
|
|
132
|
+
const allGroups = infoRegistry.getAllGroups();
|
|
133
|
+
const groupIds = allGroups.map(g => g.id).join(",");
|
|
134
|
+
const currentIds = this.groups.map(g => g.id).join(",");
|
|
135
|
+
|
|
136
|
+
if (groupIds !== currentIds) {
|
|
137
|
+
this.groups = allGroups;
|
|
138
|
+
// Load data for any new groups (non-blocking)
|
|
139
|
+
this.loadDataForNewGroups(allGroups);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (this.groups.length === 0) {
|
|
143
|
+
return this.renderEmpty(width);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return this.renderDashboard(width);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Load data for groups we don't have data for yet.
|
|
151
|
+
*/
|
|
152
|
+
private async loadDataForNewGroups(groups: InfoGroup[]): Promise<void> {
|
|
153
|
+
for (const group of groups) {
|
|
154
|
+
if (!this.groupData.has(group.id)) {
|
|
155
|
+
try {
|
|
156
|
+
const data = await infoRegistry.getGroupData(group.id);
|
|
157
|
+
this.groupData.set(group.id, data);
|
|
158
|
+
} catch {
|
|
159
|
+
// Silently skip groups with errors
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Render loading state.
|
|
167
|
+
*/
|
|
168
|
+
private renderLoading(width: number): string[] {
|
|
169
|
+
const lines: string[] = [];
|
|
170
|
+
const padding = " ".repeat(Math.max(0, Math.floor((width - 20) / 2)));
|
|
171
|
+
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push(`${padding}${ansi.cyan}${ansi.bold}📊 UniPi Info Screen${ansi.reset}`);
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push(`${padding}${ansi.dim}Loading dashboard...${ansi.reset}`);
|
|
176
|
+
lines.push("");
|
|
177
|
+
|
|
178
|
+
return lines;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Render error state.
|
|
183
|
+
*/
|
|
184
|
+
private renderError(width: number): string[] {
|
|
185
|
+
const lines: string[] = [];
|
|
186
|
+
const padding = " ".repeat(Math.max(0, Math.floor((width - 20) / 2)));
|
|
187
|
+
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push(`${padding}${ansi.yellow}${ansi.bold}⚠️ Error${ansi.reset}`);
|
|
190
|
+
lines.push(`${padding}${ansi.dim}${this.error ?? "Unknown error"}${ansi.reset}`);
|
|
191
|
+
lines.push("");
|
|
192
|
+
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Render empty state.
|
|
198
|
+
*/
|
|
199
|
+
private renderEmpty(width: number): string[] {
|
|
200
|
+
const lines: string[] = [];
|
|
201
|
+
const padding = " ".repeat(Math.max(0, Math.floor((width - 30) / 2)));
|
|
202
|
+
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push(`${padding}${ansi.cyan}${ansi.bold}📊 UniPi Info Screen${ansi.reset}`);
|
|
205
|
+
lines.push("");
|
|
206
|
+
lines.push(`${padding}${ansi.dim}No groups registered.${ansi.reset}`);
|
|
207
|
+
lines.push(`${padding}${ansi.dim}Modules will register groups on startup.${ansi.reset}`);
|
|
208
|
+
lines.push("");
|
|
209
|
+
|
|
210
|
+
return lines;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Pad a line to fill a target visual width.
|
|
215
|
+
*/
|
|
216
|
+
private padToWidth(line: string, targetWidth: number): string {
|
|
217
|
+
const visLen = visibleWidth(line);
|
|
218
|
+
const pad = Math.max(0, targetWidth - visLen);
|
|
219
|
+
return line + " ".repeat(pad);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Render the full dashboard.
|
|
224
|
+
*/
|
|
225
|
+
private renderDashboard(width: number): string[] {
|
|
226
|
+
const lines: string[] = [];
|
|
227
|
+
const group = this.groups[this.activeTabIndex];
|
|
228
|
+
const data = this.groupData.get(group.id) ?? {};
|
|
229
|
+
// Inner width for content (subtract 2 for left+right borders)
|
|
230
|
+
// Subtract extra 1 to prevent right border clipping on some terminals
|
|
231
|
+
const innerWidth = width - 3;
|
|
232
|
+
|
|
233
|
+
// Top border
|
|
234
|
+
lines.push(`${ansi.dim}╭${"─".repeat(innerWidth + 1)}╮${ansi.reset}`);
|
|
235
|
+
|
|
236
|
+
// Header
|
|
237
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderHeader(innerWidth, group), innerWidth)}${ansi.dim} │${ansi.reset}`);
|
|
238
|
+
lines.push(`${ansi.dim}├${"─".repeat(innerWidth + 1)}┤${ansi.reset}`);
|
|
239
|
+
|
|
240
|
+
// Tab bar
|
|
241
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderTabBar(innerWidth), innerWidth)}${ansi.dim} │${ansi.reset}`);
|
|
242
|
+
lines.push(`${ansi.dim}├${"─".repeat(innerWidth + 1)}┤${ansi.reset}`);
|
|
243
|
+
|
|
244
|
+
// Content with scrolling
|
|
245
|
+
const contentLines = this.renderGroupContent(innerWidth, group, data);
|
|
246
|
+
const maxVisibleLines = 15; // Max content lines visible
|
|
247
|
+
|
|
248
|
+
// Clamp scroll offset
|
|
249
|
+
const maxScroll = Math.max(0, contentLines.length - maxVisibleLines);
|
|
250
|
+
this.scrollOffset = Math.min(this.scrollOffset, maxScroll);
|
|
251
|
+
|
|
252
|
+
// Get visible slice
|
|
253
|
+
const visibleContent = contentLines.slice(this.scrollOffset, this.scrollOffset + maxVisibleLines);
|
|
254
|
+
|
|
255
|
+
for (const line of visibleContent) {
|
|
256
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(line, innerWidth)}${ansi.dim} │${ansi.reset}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Show scroll indicator if needed
|
|
260
|
+
if (contentLines.length > maxVisibleLines) {
|
|
261
|
+
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} │${ansi.reset}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Footer
|
|
266
|
+
const hasScroll = contentLines.length > maxVisibleLines;
|
|
267
|
+
lines.push(`${ansi.dim}├${"─".repeat(innerWidth + 1)}┤${ansi.reset}`);
|
|
268
|
+
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderFooter(innerWidth, hasScroll), innerWidth)}${ansi.dim} │${ansi.reset}`);
|
|
269
|
+
lines.push(`${ansi.dim}╰${"─".repeat(innerWidth + 1)}╯${ansi.reset}`);
|
|
270
|
+
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Render header with title and group info.
|
|
276
|
+
*/
|
|
277
|
+
private renderHeader(width: number, group: InfoGroup): string {
|
|
278
|
+
const title = `${group.icon} ${group.name}`;
|
|
279
|
+
const paddedTitle = ` ${title} `;
|
|
280
|
+
const visLen = visibleWidth(paddedTitle);
|
|
281
|
+
|
|
282
|
+
if (visLen >= width - 4) {
|
|
283
|
+
return ansi.bold + truncateToWidth(paddedTitle, width - 4) + ansi.reset;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Center the title
|
|
287
|
+
const leftPad = Math.floor((width - visLen) / 2);
|
|
288
|
+
|
|
289
|
+
return " ".repeat(leftPad) + ansi.bold + paddedTitle + ansi.reset;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Render tab bar.
|
|
294
|
+
*/
|
|
295
|
+
private renderTabBar(width: number): string {
|
|
296
|
+
const tabs: string[] = [];
|
|
297
|
+
|
|
298
|
+
for (let i = 0; i < this.groups.length; i++) {
|
|
299
|
+
const group = this.groups[i];
|
|
300
|
+
const isActive = i === this.activeTabIndex;
|
|
301
|
+
const color = TAB_COLORS[i % TAB_COLORS.length];
|
|
302
|
+
|
|
303
|
+
if (isActive) {
|
|
304
|
+
tabs.push(`${color}${ansi.bold} ${group.icon} ${group.name} ${ansi.reset}`);
|
|
305
|
+
} else {
|
|
306
|
+
tabs.push(`${ansi.dim} ${group.icon} ${group.name} ${ansi.reset}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const tabStr = tabs.join(`${ansi.dim}│${ansi.reset}`);
|
|
311
|
+
const visLen = visibleWidth(tabStr);
|
|
312
|
+
|
|
313
|
+
// Truncate if too wide
|
|
314
|
+
if (visLen > width - 2) {
|
|
315
|
+
return truncateToWidth(tabStr, width - 2);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return tabStr;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Render a separator line.
|
|
323
|
+
*/
|
|
324
|
+
private renderSeparator(width: number): string {
|
|
325
|
+
return ansi.dim + "─".repeat(width) + ansi.reset;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Render group content.
|
|
330
|
+
*/
|
|
331
|
+
private renderGroupContent(width: number, group: InfoGroup, data: GroupData): string[] {
|
|
332
|
+
const lines: string[] = [];
|
|
333
|
+
const visibleStats = infoRegistry.getVisibleStats(group.id);
|
|
334
|
+
|
|
335
|
+
if (visibleStats.length === 0) {
|
|
336
|
+
lines.push(` ${ansi.dim}No stats configured for this group.${ansi.reset}`);
|
|
337
|
+
return lines;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Calculate label width for alignment
|
|
341
|
+
const maxLabelLen = Math.max(...visibleStats.map((s) => s.label.length));
|
|
342
|
+
|
|
343
|
+
for (const stat of visibleStats) {
|
|
344
|
+
const statData = data[stat.id];
|
|
345
|
+
const value = statData?.value ?? "—";
|
|
346
|
+
const detail = statData?.detail;
|
|
347
|
+
|
|
348
|
+
const label = `${stat.label}:`.padEnd(maxLabelLen + 1);
|
|
349
|
+
let line = ` ${ansi.dim}${label}${ansi.reset} ${ansi.bold}${value}${ansi.reset}`;
|
|
350
|
+
|
|
351
|
+
// Handle multi-line detail
|
|
352
|
+
if (detail) {
|
|
353
|
+
const detailLines = detail.split("\n");
|
|
354
|
+
if (detailLines.length === 1) {
|
|
355
|
+
// Single line detail - show inline
|
|
356
|
+
line += ` ${ansi.dim}(${detail})${ansi.reset}`;
|
|
357
|
+
} else {
|
|
358
|
+
// Multiple lines - show value on first line, details indented below
|
|
359
|
+
lines.push(line);
|
|
360
|
+
for (const dLine of detailLines) {
|
|
361
|
+
const indent = " ".repeat(maxLabelLen + 4);
|
|
362
|
+
let detailLine = `${indent}${dLine}`;
|
|
363
|
+
if (visibleWidth(detailLine) > width - 2) {
|
|
364
|
+
detailLine = truncateToWidth(detailLine, width - 2);
|
|
365
|
+
}
|
|
366
|
+
lines.push(detailLine);
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Truncate if too wide
|
|
373
|
+
if (visibleWidth(line) > width - 2) {
|
|
374
|
+
line = truncateToWidth(line, width - 2);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
lines.push(line);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return lines;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Render footer with navigation hints.
|
|
385
|
+
*/
|
|
386
|
+
private renderFooter(width: number, hasScroll?: boolean): string {
|
|
387
|
+
const hints = [
|
|
388
|
+
`${ansi.cyan}←/→${ansi.reset} tabs`,
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
hints.push(`${ansi.green}↑/↓${ansi.reset} scroll`);
|
|
392
|
+
hints.push(`${ansi.yellow}g/G${ansi.reset} top/bottom`);
|
|
393
|
+
hints.push(`${ansi.red}q/Esc${ansi.reset} close`);
|
|
394
|
+
|
|
395
|
+
const hintStr = hints.join(` ${ansi.dim}•${ansi.reset} `);
|
|
396
|
+
const visLen = visibleWidth(hintStr);
|
|
397
|
+
|
|
398
|
+
if (visLen >= width - 4) {
|
|
399
|
+
return truncateToWidth(hintStr, width - 4);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const leftPad = Math.floor((width - visLen) / 2);
|
|
403
|
+
|
|
404
|
+
return " ".repeat(leftPad) + hintStr;
|
|
405
|
+
}
|
|
406
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/info-screen — Type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** A single stat within a group */
|
|
6
|
+
export interface InfoStat {
|
|
7
|
+
/** Stat identifier */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Display label */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Whether to show by default */
|
|
12
|
+
show: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Configuration for a group's display */
|
|
16
|
+
export interface GroupConfig {
|
|
17
|
+
/** Whether group is shown by default */
|
|
18
|
+
showByDefault: boolean;
|
|
19
|
+
/** Stats within this group */
|
|
20
|
+
stats: InfoStat[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Data for a single stat */
|
|
24
|
+
export interface StatData {
|
|
25
|
+
/** Display value */
|
|
26
|
+
value: string;
|
|
27
|
+
/** Optional detail text */
|
|
28
|
+
detail?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Data returned by a group's data provider */
|
|
32
|
+
export type GroupData = Record<string, StatData>;
|
|
33
|
+
|
|
34
|
+
/** Registration for an info group */
|
|
35
|
+
export interface InfoGroup {
|
|
36
|
+
/** Unique group identifier */
|
|
37
|
+
id: string;
|
|
38
|
+
/** Display name */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Icon emoji */
|
|
41
|
+
icon: string;
|
|
42
|
+
/** Priority for tab ordering (lower = earlier) */
|
|
43
|
+
priority: number;
|
|
44
|
+
/** Group configuration */
|
|
45
|
+
config: GroupConfig;
|
|
46
|
+
/** Async data provider */
|
|
47
|
+
dataProvider: () => Promise<GroupData>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Settings for info-screen in settings.json */
|
|
51
|
+
export interface InfoScreenSettings {
|
|
52
|
+
/** Whether to show dashboard on boot */
|
|
53
|
+
showOnBoot: boolean;
|
|
54
|
+
/** Timeout in ms waiting for modules at boot */
|
|
55
|
+
bootTimeoutMs: number;
|
|
56
|
+
/** Per-group settings */
|
|
57
|
+
groups: Record<string, GroupSettings>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Settings for a single group */
|
|
61
|
+
export interface GroupSettings {
|
|
62
|
+
/** Whether group is visible */
|
|
63
|
+
show: boolean;
|
|
64
|
+
/** Per-stat visibility overrides */
|
|
65
|
+
stats?: Record<string, boolean>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Default settings */
|
|
69
|
+
export const DEFAULT_SETTINGS: InfoScreenSettings = {
|
|
70
|
+
showOnBoot: true,
|
|
71
|
+
bootTimeoutMs: 2000,
|
|
72
|
+
groups: {},
|
|
73
|
+
};
|