@pi-unipi/footer 0.1.2 → 0.1.4
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/README.md +73 -158
- package/package.json +1 -1
- package/src/commands.ts +38 -120
- package/src/config.ts +10 -6
- package/src/events.ts +34 -34
- package/src/help.ts +160 -0
- package/src/index.ts +46 -10
- package/src/presets.ts +40 -31
- package/src/registry/index.ts +5 -7
- package/src/rendering/icons.ts +125 -107
- package/src/rendering/renderer.ts +198 -79
- package/src/rendering/theme.ts +56 -29
- package/src/segments/compactor.ts +21 -10
- package/src/segments/core.ts +134 -67
- package/src/segments/kanboard.ts +24 -8
- package/src/segments/mcp.ts +25 -8
- package/src/segments/memory.ts +17 -11
- package/src/segments/notify.ts +16 -5
- package/src/segments/ralph.ts +33 -17
- package/src/segments/status-ext.ts +18 -13
- package/src/segments/workflow.ts +44 -21
- package/src/tps-tracker.ts +204 -0
- package/src/tui/settings-tui.ts +389 -157
- package/src/types.ts +51 -12
package/src/tui/settings-tui.ts
CHANGED
|
@@ -1,37 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pi-unipi/footer — Settings TUI
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Unified settings overlay with 3 categories:
|
|
5
|
+
* - Appearance: preset, separator, icon style, full labels
|
|
6
|
+
* - Segments: group → segment drill-down
|
|
7
|
+
* - Labels & Help: label mode, zone headers
|
|
8
|
+
*
|
|
9
|
+
* Uses pi-tui SettingsList for vim/arrow keybinding support.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
import { loadFooterSettings, saveFooterSettings
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
import { SettingsList, type SettingItem, type SettingsListTheme } from "@mariozechner/pi-tui";
|
|
14
|
+
import { loadFooterSettings, saveFooterSettings } from "../config.js";
|
|
15
|
+
import { PRESET_NAMES } from "../presets.js";
|
|
16
|
+
import { setIconStyle } from "../rendering/icons.js";
|
|
17
|
+
import type { FooterGroup, FooterSettings, SeparatorStyle, IconStyle } from "../types.js";
|
|
18
|
+
|
|
19
|
+
// ─── Section types ─────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
type Section = "appearance" | "segments" | "labels";
|
|
22
|
+
const SECTIONS: Section[] = ["appearance", "segments", "labels"];
|
|
23
|
+
const SECTION_LABELS: Record<Section, string> = {
|
|
24
|
+
appearance: "Appearance",
|
|
25
|
+
segments: "Segments",
|
|
26
|
+
labels: "Labels & Help",
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
const TOGGLE_OFF = `${ansi.dim}○${ansi.reset}`;
|
|
29
|
+
// ─── Valid option values ───────────────────────────────────────────────
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
const SEPARATOR_STYLES: SeparatorStyle[] = ["powerline", "powerline-thin", "slash", "pipe", "dot", "ascii"];
|
|
32
|
+
const ICON_STYLES: IconStyle[] = ["nerd", "emoji", "text"];
|
|
33
|
+
|
|
34
|
+
// ─── Theme for SettingsList ────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const THEME: SettingsListTheme = {
|
|
37
|
+
label: (text, selected) => selected ? `\x1b[1m${text}\x1b[0m` : `\x1b[2m${text}\x1b[0m`,
|
|
38
|
+
value: (text, selected) => selected ? `\x1b[35m${text}\x1b[0m` : `\x1b[35m${text}\x1b[0m`,
|
|
39
|
+
description: (text) => `\x1b[90m${text}\x1b[0m`,
|
|
40
|
+
cursor: `\x1b[36m▸\x1b[0m`,
|
|
41
|
+
hint: (text) => `\x1b[2m${text}\x1b[0m`,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ─── Helper: frame a line inside box drawing ───────────────────────────
|
|
45
|
+
|
|
46
|
+
function frameLine(content: string, innerWidth: number): string {
|
|
47
|
+
const visLen = visibleWidth(content);
|
|
48
|
+
const pad = Math.max(0, innerWidth - visLen);
|
|
49
|
+
return `\x1b[90m│\x1b[0m${content}${" ".repeat(pad)}\x1b[90m│\x1b[0m`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ruleLine(innerWidth: number): string {
|
|
53
|
+
return `\x1b[90m├${"─".repeat(innerWidth)}┤\x1b[0m`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
57
|
+
const left = edge === "top" ? "┌" : "└";
|
|
58
|
+
const right = edge === "top" ? "┐" : "┘";
|
|
59
|
+
return `\x1b[90m${left}${"─".repeat(innerWidth)}${right}\x1b[0m`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function visibleWidth(text: string): number {
|
|
63
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Show the footer settings overlay ──────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export function showFooterSettings(ctx: any, groups: FooterGroup[], onSettingsChanged?: () => void): void {
|
|
31
69
|
ctx.ui.custom(
|
|
32
70
|
(tui: any, _theme: any, _keybindings: any, done: (result: void) => void) => {
|
|
33
|
-
const overlay = new FooterSettingsOverlay(groups);
|
|
34
|
-
|
|
71
|
+
const overlay = new FooterSettingsOverlay(groups, onSettingsChanged);
|
|
35
72
|
overlay.onClose = () => done();
|
|
36
73
|
|
|
37
74
|
return {
|
|
@@ -52,200 +89,395 @@ export function showFooterSettings(ctx: any, groups: FooterGroup[]): void {
|
|
|
52
89
|
horizontalAlign: "center",
|
|
53
90
|
}),
|
|
54
91
|
},
|
|
55
|
-
).catch((
|
|
56
|
-
|
|
92
|
+
).catch(() => {
|
|
93
|
+
// Silently ignore — overlay errors are non-blocking.
|
|
57
94
|
});
|
|
58
95
|
}
|
|
59
96
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*/
|
|
97
|
+
// ─── Footer settings overlay component ─────────────────────────────────
|
|
98
|
+
|
|
63
99
|
class FooterSettingsOverlay {
|
|
64
100
|
private settings: FooterSettings;
|
|
65
101
|
private groups: FooterGroup[];
|
|
66
|
-
private
|
|
67
|
-
private savedGroupIndex = 0;
|
|
68
|
-
private mode: "groups" | "segments" = "groups";
|
|
102
|
+
private section: Section = "appearance";
|
|
69
103
|
private selectedGroupId: string | null = null;
|
|
70
104
|
onClose?: () => void;
|
|
105
|
+
private onSettingsChanged?: () => void;
|
|
71
106
|
|
|
72
|
-
|
|
107
|
+
// Per-section SettingsList instances
|
|
108
|
+
private appearanceList!: SettingsList;
|
|
109
|
+
private groupList!: SettingsList;
|
|
110
|
+
private segmentList: SettingsList | null = null;
|
|
111
|
+
private labelsList!: SettingsList;
|
|
112
|
+
|
|
113
|
+
constructor(groups: FooterGroup[], onSettingsChanged?: () => void) {
|
|
73
114
|
this.settings = loadFooterSettings();
|
|
74
115
|
this.groups = groups;
|
|
116
|
+
this.onSettingsChanged = onSettingsChanged;
|
|
117
|
+
this.buildAppearanceList();
|
|
118
|
+
this.buildGroupList();
|
|
119
|
+
this.buildLabelsList();
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
invalidate(): void {
|
|
78
|
-
|
|
123
|
+
this.appearanceList?.invalidate();
|
|
124
|
+
this.groupList?.invalidate();
|
|
125
|
+
this.segmentList?.invalidate();
|
|
126
|
+
this.labelsList?.invalidate();
|
|
79
127
|
}
|
|
80
128
|
|
|
81
129
|
handleInput(data: string): void {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
130
|
+
// Tab cycles sections
|
|
131
|
+
if (data === "\t" || data === "\x1b[Z") {
|
|
132
|
+
const idx = SECTIONS.indexOf(this.section);
|
|
133
|
+
if (data === "\t") {
|
|
134
|
+
this.section = SECTIONS[(idx + 1) % SECTIONS.length];
|
|
135
|
+
} else {
|
|
136
|
+
this.section = SECTIONS[(idx - 1 + SECTIONS.length) % SECTIONS.length];
|
|
137
|
+
}
|
|
138
|
+
// If leaving segments drill-down, go back to groups
|
|
139
|
+
if (this.section !== "segments") {
|
|
140
|
+
this.selectedGroupId = null;
|
|
141
|
+
this.segmentList = null;
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
86
144
|
}
|
|
87
|
-
}
|
|
88
145
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
case "\r": case "\x1b[C": case "l":
|
|
101
|
-
this.enterSegmentsMode(this.groups[this.selectedIndex].id);
|
|
102
|
-
break;
|
|
103
|
-
case "q": case "\x1b":
|
|
146
|
+
// q — always close
|
|
147
|
+
if (data === "q") {
|
|
148
|
+
this.onClose?.();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Escape — back from drill-down, or close at root
|
|
153
|
+
if (data === "\x1b") {
|
|
154
|
+
if (this.section === "segments" && this.selectedGroupId) {
|
|
155
|
+
this.backToGroups();
|
|
156
|
+
} else {
|
|
104
157
|
this.onClose?.();
|
|
105
|
-
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// j — navigate down (same as down arrow)
|
|
163
|
+
if (data === "j") {
|
|
164
|
+
this.currentList?.handleInput("\x1b[B");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// k — navigate up (same as up arrow)
|
|
169
|
+
if (data === "k") {
|
|
170
|
+
this.currentList?.handleInput("\x1b[A");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// l — drill into group from segments section (same as Enter)
|
|
175
|
+
if (data === "l" && this.section === "segments" && !this.selectedGroupId) {
|
|
176
|
+
const focusedId = this.getFocusedGroupId();
|
|
177
|
+
if (focusedId) {
|
|
178
|
+
this.enterSegmentsMode(focusedId);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Space — toggle on/off
|
|
184
|
+
if (data === " ") {
|
|
185
|
+
this.currentList?.handleInput("\r");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// h — back from segment drill-down
|
|
190
|
+
if (data === "h" && this.section === "segments" && this.selectedGroupId) {
|
|
191
|
+
this.backToGroups();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// / — focus search in segments section
|
|
196
|
+
if (data === "/") {
|
|
197
|
+
this.currentList?.handleInput("/");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Enter in segments/groups mode — enter segments for the focused group
|
|
202
|
+
if (data === "\r" && this.section === "segments" && !this.selectedGroupId) {
|
|
203
|
+
const focusedId = this.getFocusedGroupId();
|
|
204
|
+
if (focusedId) {
|
|
205
|
+
this.enterSegmentsMode(focusedId);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Left arrow / backspace in segment drill-down — back to groups
|
|
211
|
+
if (this.section === "segments" && this.selectedGroupId && (data === "\x1b[D" || data === "\x7f")) {
|
|
212
|
+
this.backToGroups();
|
|
213
|
+
return;
|
|
106
214
|
}
|
|
215
|
+
|
|
216
|
+
// Delegate to current SettingsList
|
|
217
|
+
this.currentList?.handleInput(data);
|
|
107
218
|
}
|
|
108
219
|
|
|
109
|
-
private
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
220
|
+
private get currentList(): SettingsList | null {
|
|
221
|
+
switch (this.section) {
|
|
222
|
+
case "appearance": return this.appearanceList;
|
|
223
|
+
case "segments":
|
|
224
|
+
return this.segmentList ?? this.groupList;
|
|
225
|
+
case "labels": return this.labelsList;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
113
228
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
229
|
+
// ─── Build SettingsList instances ──────────────────────────────────
|
|
230
|
+
|
|
231
|
+
private buildAppearanceList(): void {
|
|
232
|
+
const items: SettingItem[] = [
|
|
233
|
+
{
|
|
234
|
+
id: "preset",
|
|
235
|
+
label: "Preset",
|
|
236
|
+
description: "Footer layout preset",
|
|
237
|
+
currentValue: this.settings.preset,
|
|
238
|
+
values: PRESET_NAMES,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "separator",
|
|
242
|
+
label: "Separator",
|
|
243
|
+
description: "Segment divider style",
|
|
244
|
+
currentValue: this.settings.separator,
|
|
245
|
+
values: SEPARATOR_STYLES,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "iconStyle",
|
|
249
|
+
label: "Icon Style",
|
|
250
|
+
description: "Icon glyph set (nerd requires Nerd Font)",
|
|
251
|
+
currentValue: this.settings.iconStyle,
|
|
252
|
+
values: ICON_STYLES,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: "showFullLabels",
|
|
256
|
+
label: "Full Labels",
|
|
257
|
+
description: "Show descriptive labels instead of abbreviations",
|
|
258
|
+
currentValue: this.settings.showFullLabels ? "on" : "off",
|
|
259
|
+
values: ["on", "off"],
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
this.appearanceList = new SettingsList(
|
|
264
|
+
items,
|
|
265
|
+
Math.min(items.length + 2, 12),
|
|
266
|
+
THEME,
|
|
267
|
+
(id, newValue) => this.onAppearanceChange(id, newValue),
|
|
268
|
+
() => this.onClose?.(),
|
|
269
|
+
{ enableSearch: false },
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private buildGroupList(): void {
|
|
274
|
+
const items: SettingItem[] = this.groups.map((group) => {
|
|
275
|
+
const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
|
|
276
|
+
const isEnabled = groupSettings.show;
|
|
277
|
+
const segCount = group.segments.length;
|
|
278
|
+
const enabledCount = group.segments.filter(s => {
|
|
279
|
+
const segOverride = groupSettings.segments?.[s.id];
|
|
280
|
+
return segOverride !== undefined ? segOverride : s.defaultShow;
|
|
281
|
+
}).length;
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
id: group.id,
|
|
285
|
+
label: group.name,
|
|
286
|
+
description: `${enabledCount}/${segCount} segments · Enter to drill down`,
|
|
287
|
+
currentValue: isEnabled ? "on" : "off",
|
|
288
|
+
values: ["on", "off"],
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
this.groupList = new SettingsList(
|
|
293
|
+
items,
|
|
294
|
+
Math.min(items.length + 2, 15),
|
|
295
|
+
THEME,
|
|
296
|
+
(id, newValue) => this.onGroupChange(id, newValue),
|
|
297
|
+
() => this.onClose?.(),
|
|
298
|
+
{ enableSearch: true },
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private buildSegmentList(groupId: string): void {
|
|
303
|
+
const group = this.groups.find(g => g.id === groupId);
|
|
304
|
+
if (!group) {
|
|
305
|
+
this.segmentList = null;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
|
|
310
|
+
|
|
311
|
+
const items: SettingItem[] = group.segments.map((seg) => {
|
|
312
|
+
const isEnabled = groupSettings.segments?.[seg.id] ?? seg.defaultShow;
|
|
313
|
+
return {
|
|
314
|
+
id: seg.id,
|
|
315
|
+
label: `${seg.shortLabel} ${seg.label}`,
|
|
316
|
+
description: seg.description,
|
|
317
|
+
currentValue: isEnabled ? "on" : "off",
|
|
318
|
+
values: ["on", "off"],
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
this.segmentList = new SettingsList(
|
|
323
|
+
items,
|
|
324
|
+
Math.min(items.length + 2, 15),
|
|
325
|
+
THEME,
|
|
326
|
+
(id, newValue) => this.onSegmentChange(groupId, id, newValue),
|
|
327
|
+
() => this.backToGroups(),
|
|
328
|
+
{ enableSearch: true },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private buildLabelsList(): void {
|
|
333
|
+
const items: SettingItem[] = [
|
|
334
|
+
{
|
|
335
|
+
id: "showFullLabelsAlways",
|
|
336
|
+
label: "Full Labels",
|
|
337
|
+
description: "Always show descriptive labels instead of abbreviations",
|
|
338
|
+
currentValue: this.settings.showFullLabels ? "on" : "off",
|
|
339
|
+
values: ["on", "off"],
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: "showZoneHeaders",
|
|
343
|
+
label: "Zone Headers",
|
|
344
|
+
description: "Show zone labels (Identity / Metrics / Time)",
|
|
345
|
+
currentValue: "off", // TODO: implement zone headers
|
|
346
|
+
values: ["on", "off"],
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
this.labelsList = new SettingsList(
|
|
351
|
+
items,
|
|
352
|
+
Math.min(items.length + 2, 12),
|
|
353
|
+
THEME,
|
|
354
|
+
(id, newValue) => this.onLabelsChange(id, newValue),
|
|
355
|
+
() => this.onClose?.(),
|
|
356
|
+
{ enableSearch: false },
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ─── Change handlers ───────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
private onAppearanceChange(id: string, newValue: string): void {
|
|
363
|
+
switch (id) {
|
|
364
|
+
case "preset":
|
|
365
|
+
this.settings.preset = newValue;
|
|
120
366
|
break;
|
|
121
|
-
case "
|
|
122
|
-
this.
|
|
367
|
+
case "separator":
|
|
368
|
+
this.settings.separator = newValue as SeparatorStyle;
|
|
123
369
|
break;
|
|
124
|
-
case "
|
|
125
|
-
this.
|
|
370
|
+
case "iconStyle":
|
|
371
|
+
this.settings.iconStyle = newValue as IconStyle;
|
|
372
|
+
setIconStyle(newValue as IconStyle);
|
|
126
373
|
break;
|
|
127
|
-
case "
|
|
128
|
-
this.
|
|
374
|
+
case "showFullLabels":
|
|
375
|
+
this.settings.showFullLabels = newValue === "on";
|
|
376
|
+
// Sync with labels section
|
|
377
|
+
this.labelsList.updateValue("showFullLabelsAlways", newValue);
|
|
129
378
|
break;
|
|
130
379
|
}
|
|
380
|
+
saveFooterSettings(this.settings);
|
|
381
|
+
this.appearanceList.updateValue(id, newValue);
|
|
382
|
+
this.onSettingsChanged?.();
|
|
131
383
|
}
|
|
132
384
|
|
|
133
|
-
private
|
|
385
|
+
private onGroupChange(groupId: string, newValue: string): void {
|
|
134
386
|
const groupSettings = this.settings.groups[groupId] ?? { show: true, segments: {} };
|
|
135
|
-
groupSettings.show =
|
|
387
|
+
groupSettings.show = newValue === "on";
|
|
136
388
|
this.settings.groups[groupId] = groupSettings;
|
|
137
389
|
saveFooterSettings(this.settings);
|
|
390
|
+
this.groupList.updateValue(groupId, newValue);
|
|
391
|
+
this.onSettingsChanged?.();
|
|
138
392
|
}
|
|
139
393
|
|
|
140
|
-
private
|
|
394
|
+
private onSegmentChange(groupId: string, segmentId: string, newValue: string): void {
|
|
141
395
|
const groupSettings = this.settings.groups[groupId] ?? { show: true, segments: {} };
|
|
142
396
|
if (!groupSettings.segments) groupSettings.segments = {};
|
|
143
|
-
groupSettings.segments[segmentId] =
|
|
397
|
+
groupSettings.segments[segmentId] = newValue === "on";
|
|
144
398
|
this.settings.groups[groupId] = groupSettings;
|
|
145
399
|
saveFooterSettings(this.settings);
|
|
400
|
+
this.segmentList?.updateValue(segmentId, newValue);
|
|
401
|
+
this.onSettingsChanged?.();
|
|
146
402
|
}
|
|
147
403
|
|
|
148
|
-
private
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
404
|
+
private onLabelsChange(id: string, newValue: string): void {
|
|
405
|
+
switch (id) {
|
|
406
|
+
case "showFullLabelsAlways":
|
|
407
|
+
this.settings.showFullLabels = newValue === "on";
|
|
408
|
+
// Sync with appearance section
|
|
409
|
+
this.appearanceList.updateValue("showFullLabels", newValue);
|
|
410
|
+
break;
|
|
411
|
+
case "showZoneHeaders":
|
|
412
|
+
// TODO: implement zone headers setting
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
saveFooterSettings(this.settings);
|
|
416
|
+
this.labelsList.updateValue(id, newValue);
|
|
417
|
+
this.onSettingsChanged?.();
|
|
153
418
|
}
|
|
154
419
|
|
|
155
|
-
|
|
156
|
-
this.mode = "groups";
|
|
157
|
-
this.selectedIndex = this.savedGroupIndex;
|
|
158
|
-
this.selectedGroupId = null;
|
|
159
|
-
}
|
|
420
|
+
// ─── Section navigation ────────────────────────────────────────────
|
|
160
421
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return this.renderGroupsMode(width);
|
|
164
|
-
} else {
|
|
165
|
-
return this.renderSegmentsMode(width);
|
|
166
|
-
}
|
|
422
|
+
private getFocusedGroupId(): string | null {
|
|
423
|
+
return this.selectedGroupId ?? this.groups[0]?.id ?? null;
|
|
167
424
|
}
|
|
168
425
|
|
|
169
|
-
private
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return line + " ".repeat(pad);
|
|
426
|
+
private enterSegmentsMode(groupId: string): void {
|
|
427
|
+
this.selectedGroupId = groupId;
|
|
428
|
+
this.buildSegmentList(groupId);
|
|
173
429
|
}
|
|
174
430
|
|
|
175
|
-
private
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
431
|
+
private backToGroups(): void {
|
|
432
|
+
this.selectedGroupId = null;
|
|
433
|
+
this.segmentList = null;
|
|
434
|
+
// Rebuild group list to reflect segment changes
|
|
435
|
+
this.buildGroupList();
|
|
180
436
|
}
|
|
181
437
|
|
|
182
|
-
|
|
183
|
-
const lines: string[] = [];
|
|
184
|
-
const innerWidth = width - 2;
|
|
438
|
+
// ─── Render ────────────────────────────────────────────────────────
|
|
185
439
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
lines
|
|
189
|
-
|
|
190
|
-
for (let i = 0; i < this.groups.length; i++) {
|
|
191
|
-
const group = this.groups[i];
|
|
192
|
-
const isSelected = i === this.selectedIndex;
|
|
193
|
-
const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
|
|
194
|
-
const isEnabled = groupSettings.show;
|
|
440
|
+
render(width: number): string[] {
|
|
441
|
+
const innerWidth = Math.max(22, width - 2);
|
|
442
|
+
const lines: string[] = [];
|
|
195
443
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
444
|
+
// Header
|
|
445
|
+
lines.push(borderLine(innerWidth, "top"));
|
|
446
|
+
lines.push(frameLine(`\x1b[1m\x1b[36m⚙ Footer Settings\x1b[0m`, innerWidth));
|
|
199
447
|
|
|
200
|
-
|
|
201
|
-
|
|
448
|
+
// Section tabs
|
|
449
|
+
const tabParts = SECTIONS.map((s) => {
|
|
450
|
+
const label = SECTION_LABELS[s];
|
|
451
|
+
if (s === this.section) {
|
|
452
|
+
return `\x1b[1m\x1b[36m[${label}]\x1b[0m`;
|
|
202
453
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
454
|
+
return `\x1b[2m${label}\x1b[0m`;
|
|
455
|
+
});
|
|
456
|
+
lines.push(frameLine(` ${tabParts.join(" ")}`, innerWidth));
|
|
457
|
+
lines.push(ruleLine(innerWidth));
|
|
458
|
+
|
|
459
|
+
// Section content
|
|
460
|
+
const activeList = this.currentList;
|
|
461
|
+
if (activeList) {
|
|
462
|
+
const contentLines = activeList.render(innerWidth - 2);
|
|
463
|
+
for (const line of contentLines) {
|
|
464
|
+
lines.push(frameLine(` ${line}`, innerWidth));
|
|
206
465
|
}
|
|
207
|
-
|
|
208
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(line, innerWidth)}${ansi.dim}│${ansi.reset}`);
|
|
209
466
|
}
|
|
210
467
|
|
|
211
|
-
|
|
212
|
-
lines.push(
|
|
213
|
-
lines.push(`${ansi.dim}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
|
|
468
|
+
// Footer hints
|
|
469
|
+
lines.push(ruleLine(innerWidth));
|
|
214
470
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
|
|
224
|
-
const innerWidth = width - 2;
|
|
225
|
-
|
|
226
|
-
lines.push(`${ansi.dim}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
|
|
227
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${group.name} Segments`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
|
|
228
|
-
lines.push(`${ansi.dim}├${"─".repeat(innerWidth)}┤${ansi.reset}`);
|
|
229
|
-
|
|
230
|
-
for (let i = 0; i < group.segments.length; i++) {
|
|
231
|
-
const seg = group.segments[i];
|
|
232
|
-
const isSelected = i === this.selectedIndex;
|
|
233
|
-
const isEnabled = groupSettings.segments?.[seg.id] ?? seg.defaultShow;
|
|
234
|
-
|
|
235
|
-
const toggle = isEnabled ? TOGGLE_ON : TOGGLE_OFF;
|
|
236
|
-
const indicator = isSelected ? `${ansi.cyan}▸${ansi.reset}` : " ";
|
|
237
|
-
let line = ` ${indicator} ${toggle} ${seg.label}`;
|
|
238
|
-
|
|
239
|
-
if (visibleWidth(line) > innerWidth - 2) {
|
|
240
|
-
line = truncateToWidth(line, innerWidth - 2);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(line, innerWidth)}${ansi.dim}│${ansi.reset}`);
|
|
471
|
+
let hints: string;
|
|
472
|
+
if (this.section === "segments" && this.selectedGroupId) {
|
|
473
|
+
hints = "j/k navigate · Space toggle · h/Esc back · / search · q close";
|
|
474
|
+
} else if (this.section === "segments") {
|
|
475
|
+
hints = "j/k navigate · Space toggle · l/Enter segments · / search · q close";
|
|
476
|
+
} else {
|
|
477
|
+
hints = "j/k navigate · Space/Enter change · Tab section · q close";
|
|
244
478
|
}
|
|
245
|
-
|
|
246
|
-
lines.push(
|
|
247
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${ansi.dim}↑↓ select Space toggle ←/Enter back q close${ansi.reset}`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
|
|
248
|
-
lines.push(`${ansi.dim}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
|
|
479
|
+
lines.push(frameLine(`\x1b[2m${hints}\x1b[0m`, innerWidth));
|
|
480
|
+
lines.push(borderLine(innerWidth, "bottom"));
|
|
249
481
|
|
|
250
482
|
return lines;
|
|
251
483
|
}
|