@pi-unipi/footer 0.1.3 → 2.0.0

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,20 +1,36 @@
1
1
  /**
2
2
  * @pi-unipi/footer — Settings TUI
3
3
  *
4
- * Interactive settings overlay for toggling groups and individual segments.
5
- * Uses pi-tui SettingsList for proper vim/arrow keybinding support and search.
6
- * Modeled after the compactor settings overlay 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.
7
10
  */
8
11
 
9
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
10
13
  import { SettingsList, type SettingItem, type SettingsListTheme } from "@mariozechner/pi-tui";
11
14
  import { loadFooterSettings, saveFooterSettings } from "../config.js";
12
- import type { FooterGroup, FooterSettings } from "../types.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";
13
18
 
14
19
  // ─── Section types ─────────────────────────────────────────────────────
15
20
 
16
- type Section = "groups" | "segments";
17
- const SECTIONS: Section[] = ["groups", "segments"];
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",
27
+ };
28
+
29
+ // ─── Valid option values ───────────────────────────────────────────────
30
+
31
+ const SEPARATOR_STYLES: SeparatorStyle[] = ["powerline", "powerline-thin", "slash", "pipe", "dot", "ascii"];
32
+ const ICON_STYLES: IconStyle[] = ["nerd", "emoji", "text"];
33
+ const ZONE_SEPARATOR_OPTIONS = ["│", "╎", "·", "─", "none"];
18
34
 
19
35
  // ─── Theme for SettingsList ────────────────────────────────────────────
20
36
 
@@ -44,17 +60,16 @@ function borderLine(innerWidth: number, edge: "top" | "bottom"): string {
44
60
  return `\x1b[90m${left}${"─".repeat(innerWidth)}${right}\x1b[0m`;
45
61
  }
46
62
 
47
- // Visible width helper (strip ANSI)
48
63
  function visibleWidth(text: string): number {
49
64
  return text.replace(/\x1b\[[0-9;]*m/g, "").length;
50
65
  }
51
66
 
52
67
  // ─── Show the footer settings overlay ──────────────────────────────────
53
68
 
54
- export function showFooterSettings(ctx: any, groups: FooterGroup[]): void {
55
- ctx.ui.custom(
56
- (tui: any, _theme: any, _keybindings: any, done: (result: void) => void) => {
57
- const overlay = new FooterSettingsOverlay(groups);
69
+ export function showFooterSettings(ctx: ExtensionCommandContext, groups: FooterGroup[], onSettingsChanged?: () => void): void {
70
+ ctx.ui.custom<void>(
71
+ (tui, _theme, _keybindings, done) => {
72
+ const overlay = new FooterSettingsOverlay(groups, onSettingsChanged);
58
73
  overlay.onClose = () => done();
59
74
 
60
75
  return {
@@ -71,8 +86,7 @@ export function showFooterSettings(ctx: any, groups: FooterGroup[]): void {
71
86
  {
72
87
  overlay: true,
73
88
  overlayOptions: () => ({
74
- verticalAlign: "center",
75
- horizontalAlign: "center",
89
+ anchor: "center" as const,
76
90
  }),
77
91
  },
78
92
  ).catch(() => {
@@ -82,53 +96,83 @@ export function showFooterSettings(ctx: any, groups: FooterGroup[]): void {
82
96
 
83
97
  // ─── Footer settings overlay component ─────────────────────────────────
84
98
 
85
- /**
86
- * Footer settings overlay component.
87
- * Uses SettingsList from pi-tui for proper vim/arrow keybinding support.
88
- * Two sections: Groups (toggle groups) and Segments (toggle segments within a group).
89
- */
90
99
  class FooterSettingsOverlay {
91
100
  private settings: FooterSettings;
92
101
  private groups: FooterGroup[];
93
- private section: Section = "groups";
102
+ private section: Section = "appearance";
94
103
  private selectedGroupId: string | null = null;
95
104
  onClose?: () => void;
105
+ private onSettingsChanged?: () => void;
96
106
 
97
107
  // Per-section SettingsList instances
108
+ private appearanceList!: SettingsList;
98
109
  private groupList!: SettingsList;
99
110
  private segmentList: SettingsList | null = null;
111
+ private labelsList!: SettingsList;
100
112
 
101
- constructor(groups: FooterGroup[]) {
113
+ constructor(groups: FooterGroup[], onSettingsChanged?: () => void) {
102
114
  this.settings = loadFooterSettings();
103
115
  this.groups = groups;
116
+ this.onSettingsChanged = onSettingsChanged;
117
+ this.buildAppearanceList();
104
118
  this.buildGroupList();
119
+ this.buildLabelsList();
105
120
  }
106
121
 
107
122
  invalidate(): void {
123
+ this.appearanceList?.invalidate();
108
124
  this.groupList?.invalidate();
109
125
  this.segmentList?.invalidate();
126
+ this.labelsList?.invalidate();
110
127
  }
111
128
 
112
129
  handleInput(data: string): void {
113
- // Tab switches section (only if segments are available)
130
+ // Tab cycles sections
114
131
  if (data === "\t" || data === "\x1b[Z") {
115
- if (this.section === "groups" && this.selectedGroupId) {
116
- this.section = "segments";
132
+ const idx = SECTIONS.indexOf(this.section);
133
+ if (data === "\t") {
134
+ this.section = SECTIONS[(idx + 1) % SECTIONS.length];
117
135
  } else {
118
- this.section = "groups";
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") {
119
140
  this.selectedGroupId = null;
141
+ this.segmentList = null;
120
142
  }
121
143
  return;
122
144
  }
123
145
 
124
- // Escape / q — close
125
- if (data === "\x1b" || data === "q") {
146
+ // q — always close
147
+ if (data === "q") {
126
148
  this.onClose?.();
127
149
  return;
128
150
  }
129
151
 
130
- // Enter in groups mode enter segments for the focused group
131
- if (data === "\r" && this.section === "groups") {
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 {
157
+ this.onClose?.();
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) {
132
176
  const focusedId = this.getFocusedGroupId();
133
177
  if (focusedId) {
134
178
  this.enterSegmentsMode(focusedId);
@@ -136,8 +180,35 @@ class FooterSettingsOverlay {
136
180
  return;
137
181
  }
138
182
 
139
- // Left arrow / backspace in segments back to groups
140
- if (this.section === "segments" && (data === "\x1b[D" || data === "h" || data === "\x7f")) {
183
+ // Spacetoggle 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")) {
141
212
  this.backToGroups();
142
213
  return;
143
214
  }
@@ -147,12 +218,65 @@ class FooterSettingsOverlay {
147
218
  }
148
219
 
149
220
  private get currentList(): SettingsList | null {
150
- if (this.section === "segments") return this.segmentList;
151
- return this.groupList;
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
+ }
152
227
  }
153
228
 
154
229
  // ─── Build SettingsList instances ──────────────────────────────────
155
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: "zoneSeparator",
256
+ label: "Zone Separator",
257
+ description: "Divider between zones (left · center · right)",
258
+ currentValue: this.settings.zoneSeparator ?? "│",
259
+ values: ZONE_SEPARATOR_OPTIONS,
260
+ },
261
+ {
262
+ id: "showFullLabels",
263
+ label: "Full Labels",
264
+ description: "Show descriptive labels instead of abbreviations",
265
+ currentValue: this.settings.showFullLabels ? "on" : "off",
266
+ values: ["on", "off"],
267
+ },
268
+ ];
269
+
270
+ this.appearanceList = new SettingsList(
271
+ items,
272
+ Math.min(items.length + 2, 12),
273
+ THEME,
274
+ (id, newValue) => this.onAppearanceChange(id, newValue),
275
+ () => this.onClose?.(),
276
+ { enableSearch: false },
277
+ );
278
+ }
279
+
156
280
  private buildGroupList(): void {
157
281
  const items: SettingItem[] = this.groups.map((group) => {
158
282
  const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
@@ -166,7 +290,7 @@ class FooterSettingsOverlay {
166
290
  return {
167
291
  id: group.id,
168
292
  label: group.name,
169
- description: `${enabledCount}/${segCount} segments active`,
293
+ description: `${enabledCount}/${segCount} segments · Enter to drill down`,
170
294
  currentValue: isEnabled ? "on" : "off",
171
295
  values: ["on", "off"],
172
296
  };
@@ -195,8 +319,8 @@ class FooterSettingsOverlay {
195
319
  const isEnabled = groupSettings.segments?.[seg.id] ?? seg.defaultShow;
196
320
  return {
197
321
  id: seg.id,
198
- label: seg.label,
199
- description: "",
322
+ label: `${seg.shortLabel} ${seg.label}`,
323
+ description: seg.description,
200
324
  currentValue: isEnabled ? "on" : "off",
201
325
  values: ["on", "off"],
202
326
  };
@@ -212,16 +336,69 @@ class FooterSettingsOverlay {
212
336
  );
213
337
  }
214
338
 
339
+ private buildLabelsList(): void {
340
+ const items: SettingItem[] = [
341
+ {
342
+ id: "showFullLabelsAlways",
343
+ label: "Full Labels",
344
+ description: "Always show descriptive labels instead of abbreviations",
345
+ currentValue: this.settings.showFullLabels ? "on" : "off",
346
+ values: ["on", "off"],
347
+ },
348
+ {
349
+ id: "showZoneHeaders",
350
+ label: "Zone Headers",
351
+ description: "Show zone labels (Identity / Metrics / Time)",
352
+ currentValue: "off", // TODO: implement zone headers
353
+ values: ["on", "off"],
354
+ },
355
+ ];
356
+
357
+ this.labelsList = new SettingsList(
358
+ items,
359
+ Math.min(items.length + 2, 12),
360
+ THEME,
361
+ (id, newValue) => this.onLabelsChange(id, newValue),
362
+ () => this.onClose?.(),
363
+ { enableSearch: false },
364
+ );
365
+ }
366
+
215
367
  // ─── Change handlers ───────────────────────────────────────────────
216
368
 
369
+ private onAppearanceChange(id: string, newValue: string): void {
370
+ switch (id) {
371
+ case "preset":
372
+ this.settings.preset = newValue;
373
+ break;
374
+ case "separator":
375
+ this.settings.separator = newValue as SeparatorStyle;
376
+ break;
377
+ case "iconStyle":
378
+ this.settings.iconStyle = newValue as IconStyle;
379
+ setIconStyle(newValue as IconStyle);
380
+ break;
381
+ case "zoneSeparator":
382
+ this.settings.zoneSeparator = newValue;
383
+ break;
384
+ case "showFullLabels":
385
+ this.settings.showFullLabels = newValue === "on";
386
+ // Sync with labels section
387
+ this.labelsList.updateValue("showFullLabelsAlways", newValue);
388
+ break;
389
+ }
390
+ saveFooterSettings(this.settings);
391
+ this.appearanceList.updateValue(id, newValue);
392
+ this.onSettingsChanged?.();
393
+ }
394
+
217
395
  private onGroupChange(groupId: string, newValue: string): void {
218
396
  const groupSettings = this.settings.groups[groupId] ?? { show: true, segments: {} };
219
397
  groupSettings.show = newValue === "on";
220
398
  this.settings.groups[groupId] = groupSettings;
221
399
  saveFooterSettings(this.settings);
222
-
223
- // Update display
224
400
  this.groupList.updateValue(groupId, newValue);
401
+ this.onSettingsChanged?.();
225
402
  }
226
403
 
227
404
  private onSegmentChange(groupId: string, segmentId: string, newValue: string): void {
@@ -230,41 +407,51 @@ class FooterSettingsOverlay {
230
407
  groupSettings.segments[segmentId] = newValue === "on";
231
408
  this.settings.groups[groupId] = groupSettings;
232
409
  saveFooterSettings(this.settings);
233
-
234
- // Update the segment list display
235
410
  this.segmentList?.updateValue(segmentId, newValue);
411
+ this.onSettingsChanged?.();
412
+ }
236
413
 
237
- // Update group description (segment count)
238
- const group = this.groups.find(g => g.id === groupId);
239
- if (group) {
240
- const segCount = group.segments.length;
241
- const enabledCount = group.segments.filter(s => {
242
- const segOverride = groupSettings.segments?.[s.id];
243
- return segOverride !== undefined ? segOverride : s.defaultShow;
244
- }).length;
245
- this.groupList.updateValue(groupId, groupSettings.show ? "on" : "off");
414
+ private onLabelsChange(id: string, newValue: string): void {
415
+ switch (id) {
416
+ case "showFullLabelsAlways":
417
+ this.settings.showFullLabels = newValue === "on";
418
+ // Sync with appearance section
419
+ this.appearanceList.updateValue("showFullLabels", newValue);
420
+ break;
421
+ case "showZoneHeaders":
422
+ // TODO: implement zone headers setting
423
+ break;
246
424
  }
425
+ saveFooterSettings(this.settings);
426
+ this.labelsList.updateValue(id, newValue);
427
+ this.onSettingsChanged?.();
247
428
  }
248
429
 
249
430
  // ─── Section navigation ────────────────────────────────────────────
250
431
 
251
432
  private getFocusedGroupId(): string | null {
252
- // Walk the group list items in order; return the first one
253
- // that matches the focused index. Since SettingsList doesn't
254
- // expose selectedIndex, we track by the group array order.
255
- return this.selectedGroupId ?? this.groups[0]?.id ?? null;
433
+ if (this.selectedGroupId) return this.selectedGroupId;
434
+ // Access SettingsList internal state it doesn't expose a getSelectedId() method
435
+ const list = this.groupList as unknown as {
436
+ selectedIndex: number;
437
+ items: SettingItem[];
438
+ filteredItems: SettingItem[];
439
+ searchEnabled: boolean;
440
+ };
441
+ const displayItems = list.searchEnabled ? list.filteredItems : list.items;
442
+ return displayItems[list.selectedIndex]?.id ?? null;
256
443
  }
257
444
 
258
445
  private enterSegmentsMode(groupId: string): void {
259
446
  this.selectedGroupId = groupId;
260
- this.section = "segments";
261
447
  this.buildSegmentList(groupId);
262
448
  }
263
449
 
264
450
  private backToGroups(): void {
265
- this.section = "groups";
266
451
  this.selectedGroupId = null;
267
452
  this.segmentList = null;
453
+ // Rebuild group list to reflect segment changes
454
+ this.buildGroupList();
268
455
  }
269
456
 
270
457
  // ─── Render ────────────────────────────────────────────────────────
@@ -279,19 +466,16 @@ class FooterSettingsOverlay {
279
466
 
280
467
  // Section tabs
281
468
  const tabParts = SECTIONS.map((s) => {
282
- const label = s.charAt(0).toUpperCase() + s.slice(1);
469
+ const label = SECTION_LABELS[s];
283
470
  if (s === this.section) {
284
471
  return `\x1b[1m\x1b[36m[${label}]\x1b[0m`;
285
472
  }
286
- if (s === "segments" && !this.selectedGroupId) {
287
- return `\x1b[2m${label}\x1b[0m`; // dimmed if no group selected
288
- }
289
473
  return `\x1b[2m${label}\x1b[0m`;
290
474
  });
291
475
  lines.push(frameLine(` ${tabParts.join(" ")}`, innerWidth));
292
476
  lines.push(ruleLine(innerWidth));
293
477
 
294
- // Section content (rendered by SettingsList)
478
+ // Section content
295
479
  const activeList = this.currentList;
296
480
  if (activeList) {
297
481
  const contentLines = activeList.render(innerWidth - 2);
@@ -302,9 +486,15 @@ class FooterSettingsOverlay {
302
486
 
303
487
  // Footer hints
304
488
  lines.push(ruleLine(innerWidth));
305
- const hints = this.section === "groups"
306
- ? "↑↓ navigate · Space toggle · Enter segments · / search · q close"
307
- : "↑↓ navigate · Space toggle · ← back · / search · q close";
489
+
490
+ let hints: string;
491
+ if (this.section === "segments" && this.selectedGroupId) {
492
+ hints = "j/k navigate · Space toggle · h/Esc back · / search · q close";
493
+ } else if (this.section === "segments") {
494
+ hints = "j/k navigate · Space toggle · l/Enter segments · / search · q close";
495
+ } else {
496
+ hints = "j/k navigate · Space/Enter change · Tab section · q close";
497
+ }
308
498
  lines.push(frameLine(`\x1b[2m${hints}\x1b[0m`, innerWidth));
309
499
  lines.push(borderLine(innerWidth, "bottom"));
310
500
 
package/src/types.ts CHANGED
@@ -9,39 +9,64 @@ import type { Theme, ThemeColor } from "@mariozechner/pi-coding-agent";
9
9
 
10
10
  // ─── Semantic Colors ────────────────────────────────────────────────────────
11
11
 
12
+ /** Zone assignment for segment positioning */
13
+ export type SegmentZone = "left" | "center" | "right";
14
+
12
15
  /** Semantic color names mapped to segment groups */
13
16
  export type SemanticColor =
17
+ // ── Model & Identity (Left zone) ──
14
18
  | "model"
15
19
  | "path"
16
20
  | "git"
17
- | "compactor"
18
- | "memory"
19
- | "mcp"
20
- | "ralph"
21
- | "ralphOn"
22
- | "ralphOff"
21
+ | "gitClean"
22
+ | "gitDirty"
23
+ | "session"
24
+ | "worktree"
25
+ // ── Workflow (Left zone) ──
23
26
  | "workflow"
27
+ | "workflowNone"
24
28
  | "workflowBrainstorm"
25
29
  | "workflowPlan"
26
30
  | "workflowWork"
27
31
  | "workflowReview"
28
32
  | "workflowAuto"
33
+ | "workflowDebug"
34
+ | "workflowChoreExec"
29
35
  | "workflowOther"
36
+ // ── TPS tiers (Center zone) ──
37
+ | "tpsSlow"
38
+ | "tpsModerate"
39
+ | "tpsGood"
40
+ | "tpsFast"
41
+ | "tpsBlazing"
42
+ | "tpsIdle"
43
+ // ── Metrics (Center zone) ──
44
+ | "compactor"
45
+ | "memory"
46
+ | "mcp"
47
+ | "ralph"
48
+ | "ralphOn"
49
+ | "ralphOff"
30
50
  | "kanboard"
31
51
  | "notify"
32
- | "separator"
33
- | "border"
34
52
  | "context"
35
53
  | "contextWarn"
36
54
  | "contextError"
37
55
  | "cost"
38
56
  | "tokens"
57
+ // ── Time (Right zone) ──
58
+ | "clock"
59
+ | "duration"
60
+ // ── Thinking levels ──
39
61
  | "thinking"
40
62
  | "thinkingMinimal"
41
63
  | "thinkingLow"
42
64
  | "thinkingMedium"
43
65
  | "thinkingHigh"
44
- | "thinkingXhigh";
66
+ | "thinkingXhigh"
67
+ // ── UI chrome ──
68
+ | "separator"
69
+ | "border";
45
70
 
46
71
  /** A theme color name or custom hex color */
47
72
  export type ColorValue = ThemeColor | `#${string}`;
@@ -98,6 +123,8 @@ export interface FooterSegmentContext {
98
123
  piContext?: unknown;
99
124
  /** Footer data provider (for core segments that need git, extension statuses) */
100
125
  footerData?: unknown;
126
+ /** Label mode: compact (shortLabel) or labeled (full label) */
127
+ labelMode?: "compact" | "labeled";
101
128
  }
102
129
 
103
130
  /** Segment render function type */
@@ -107,8 +134,14 @@ export type SegmentRenderFn = (ctx: FooterSegmentContext) => RenderedSegment;
107
134
  export interface FooterSegment {
108
135
  /** Unique segment identifier (e.g., "model", "compactions") */
109
136
  id: string;
110
- /** Display label */
137
+ /** Display label (full name, used in labeled mode) */
111
138
  label: string;
139
+ /** Compact display name (used in compact mode, e.g. "ses", "tps", "ctx") */
140
+ shortLabel: string;
141
+ /** Human-readable description (shown in footer-help overlay) */
142
+ description: string;
143
+ /** Layout zone assignment */
144
+ zone: SegmentZone;
112
145
  /** Icon glyph (Nerd Font or ASCII) */
113
146
  icon: string;
114
147
  /** Render function */
@@ -153,6 +186,10 @@ export interface FooterSettings {
153
186
  separator: SeparatorStyle;
154
187
  /** Icon style: nerd (Nerd Font glyphs), emoji (Unicode emoji), text (plain labels) */
155
188
  iconStyle: IconStyle;
189
+ /** Zone separator string (between zones, default: "│") */
190
+ zoneSeparator?: string;
191
+ /** Show full labels instead of compact short labels */
192
+ showFullLabels?: boolean;
156
193
  /** Per-group settings */
157
194
  groups: Record<string, FooterGroupSettings>;
158
195
  }
@@ -174,10 +211,12 @@ export interface PresetDef {
174
211
  rightSegments: string[];
175
212
  /** Secondary row segments (shown when terminal is narrow) */
176
213
  secondarySegments: string[];
177
- /** Separator style for this preset */
178
- separator: SeparatorStyle;
179
214
  /** Color scheme for this preset */
180
215
  colors?: ColorScheme;
181
216
  /** Per-segment options */
182
217
  segmentOptions?: Record<string, Record<string, unknown>>;
218
+ /** Zone order (default: left, center, right) */
219
+ zoneOrder?: ("left" | "center" | "right")[];
220
+ /** Zone separator string (between zones) */
221
+ zoneSeparator?: string;
183
222
  }