@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.
@@ -1,37 +1,74 @@
1
1
  /**
2
2
  * @pi-unipi/footer — Settings TUI
3
3
  *
4
- * Interactive settings overlay for toggling groups and individual segments.
5
- * Follows the info-screen SettingsOverlay pattern.
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 { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
10
- import { loadFooterSettings, saveFooterSettings, getGroupSettings } from "../config.js";
11
- import type { FooterGroup, FooterSettings } from "../types.js";
12
-
13
- /** ANSI escape codes */
14
- const ansi = {
15
- reset: "\x1b[0m",
16
- bold: "\x1b[1m",
17
- dim: "\x1b[2m",
18
- cyan: "\x1b[36m",
19
- green: "\x1b[32m",
20
- yellow: "\x1b[33m",
21
- gray: "\x1b[90m",
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
- const TOGGLE_ON = `${ansi.green}●${ansi.reset}`;
25
- const TOGGLE_OFF = `${ansi.dim}○${ansi.reset}`;
29
+ // ─── Valid option values ───────────────────────────────────────────────
26
30
 
27
- /**
28
- * Show the footer settings overlay.
29
- */
30
- export function showFooterSettings(ctx: any, groups: FooterGroup[]): void {
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((err: unknown) => {
56
- console.error("[footer] Settings overlay error:", err);
92
+ ).catch(() => {
93
+ // Silently ignore — overlay errors are non-blocking.
57
94
  });
58
95
  }
59
96
 
60
- /**
61
- * Footer settings overlay component.
62
- */
97
+ // ─── Footer settings overlay component ─────────────────────────────────
98
+
63
99
  class FooterSettingsOverlay {
64
100
  private settings: FooterSettings;
65
101
  private groups: FooterGroup[];
66
- private selectedIndex = 0;
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
- constructor(groups: FooterGroup[]) {
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
- // No cached state
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
- if (this.mode === "groups") {
83
- this.handleGroupsInput(data);
84
- } else {
85
- this.handleSegmentsInput(data);
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
- private handleGroupsInput(data: string): void {
90
- switch (data) {
91
- case "\x1b[A": case "k":
92
- this.selectedIndex = (this.selectedIndex - 1 + this.groups.length) % this.groups.length;
93
- break;
94
- case "\x1b[B": case "j":
95
- this.selectedIndex = (this.selectedIndex + 1) % this.groups.length;
96
- break;
97
- case " ":
98
- this.toggleGroup(this.groups[this.selectedIndex].id);
99
- break;
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
- break;
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 handleSegmentsInput(data: string): void {
110
- if (!this.selectedGroupId) return;
111
- const group = this.groups.find(g => g.id === this.selectedGroupId);
112
- if (!group) return;
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
- switch (data) {
115
- case "\x1b[A": case "k":
116
- this.selectedIndex = (this.selectedIndex - 1 + group.segments.length) % group.segments.length;
117
- break;
118
- case "\x1b[B": case "j":
119
- this.selectedIndex = (this.selectedIndex + 1) % group.segments.length;
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.toggleSegment(this.selectedGroupId, group.segments[this.selectedIndex].id);
367
+ case "separator":
368
+ this.settings.separator = newValue as SeparatorStyle;
123
369
  break;
124
- case "\x1b[D": case "h": case "\r":
125
- this.backToGroups();
370
+ case "iconStyle":
371
+ this.settings.iconStyle = newValue as IconStyle;
372
+ setIconStyle(newValue as IconStyle);
126
373
  break;
127
- case "q": case "\x1b":
128
- this.onClose?.();
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 toggleGroup(groupId: string): void {
385
+ private onGroupChange(groupId: string, newValue: string): void {
134
386
  const groupSettings = this.settings.groups[groupId] ?? { show: true, segments: {} };
135
- groupSettings.show = !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 toggleSegment(groupId: string, segmentId: string): void {
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] = !(groupSettings.segments[segmentId] ?? true);
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 enterSegmentsMode(groupId: string): void {
149
- this.savedGroupIndex = this.selectedIndex;
150
- this.mode = "segments";
151
- this.selectedGroupId = groupId;
152
- this.selectedIndex = 0;
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
- private backToGroups(): void {
156
- this.mode = "groups";
157
- this.selectedIndex = this.savedGroupIndex;
158
- this.selectedGroupId = null;
159
- }
420
+ // ─── Section navigation ────────────────────────────────────────────
160
421
 
161
- render(width: number): string[] {
162
- if (this.mode === "groups") {
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 padToWidth(line: string, targetWidth: number): string {
170
- const visLen = visibleWidth(line);
171
- const pad = Math.max(0, targetWidth - visLen);
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 renderCentered(text: string, width: number): string {
176
- const visLen = visibleWidth(text);
177
- if (visLen >= width) return text;
178
- const leftPad = Math.floor((width - visLen) / 2);
179
- return " ".repeat(leftPad) + text;
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
- private renderGroupsMode(width: number): string[] {
183
- const lines: string[] = [];
184
- const innerWidth = width - 2;
438
+ // ─── Render ────────────────────────────────────────────────────────
185
439
 
186
- lines.push(`${ansi.dim}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
187
- lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${ansi.bold}⚙ Footer Settings${ansi.reset}`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
188
- lines.push(`${ansi.dim}├${"─".repeat(innerWidth)}┤${ansi.reset}`);
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
- const toggle = isEnabled ? TOGGLE_ON : TOGGLE_OFF;
197
- const indicator = isSelected ? `${ansi.cyan}▸${ansi.reset}` : " ";
198
- let line = ` ${indicator} ${toggle} ${group.name}`;
444
+ // Header
445
+ lines.push(borderLine(innerWidth, "top"));
446
+ lines.push(frameLine(`\x1b[1m\x1b[36m⚙ Footer Settings\x1b[0m`, innerWidth));
199
447
 
200
- if (isSelected) {
201
- line += ` ${ansi.dim}→ segments${ansi.reset}`;
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
- if (visibleWidth(line) > innerWidth - 2) {
205
- line = truncateToWidth(line, innerWidth - 2);
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
- lines.push(`${ansi.dim}├${"─".repeat(innerWidth)}┤${ansi.reset}`);
212
- lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${ansi.dim}↑↓ select Space toggle Enter/→ segments q close${ansi.reset}`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
213
- lines.push(`${ansi.dim}╰${"─".repeat(innerWidth)}╯${ansi.reset}`);
468
+ // Footer hints
469
+ lines.push(ruleLine(innerWidth));
214
470
 
215
- return lines;
216
- }
217
-
218
- private renderSegmentsMode(width: number): string[] {
219
- const lines: string[] = [];
220
- const group = this.groups.find(g => g.id === this.selectedGroupId);
221
- if (!group) return lines;
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(`${ansi.dim}├${"".repeat(innerWidth)}┤${ansi.reset}`);
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
  }