@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/rendering/icons.ts
CHANGED
|
@@ -16,8 +16,8 @@ import { detectNerdFontSupport } from "./separators.js";
|
|
|
16
16
|
export interface IconSet {
|
|
17
17
|
// Core segments
|
|
18
18
|
model: string;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
apiState: string;
|
|
20
|
+
toolCount: string;
|
|
21
21
|
git: string;
|
|
22
22
|
context: string;
|
|
23
23
|
cost: string;
|
|
@@ -27,6 +27,10 @@ export interface IconSet {
|
|
|
27
27
|
session: string;
|
|
28
28
|
hostname: string;
|
|
29
29
|
time: string;
|
|
30
|
+
tps: string;
|
|
31
|
+
clock: string;
|
|
32
|
+
duration: string;
|
|
33
|
+
thinkingLevel: string;
|
|
30
34
|
|
|
31
35
|
// Compactor segments
|
|
32
36
|
sessionEvents: string;
|
|
@@ -80,38 +84,42 @@ export interface IconSet {
|
|
|
80
84
|
/** Nerd Font glyphs — requires a Nerd Font installed in the terminal */
|
|
81
85
|
export const NERD_ICONS: IconSet = {
|
|
82
86
|
// Core
|
|
83
|
-
model: "\
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
git: "\
|
|
87
|
-
context: "\
|
|
88
|
-
cost: "\uF155", //
|
|
89
|
-
tokens: "\
|
|
90
|
-
tokensIn: "\
|
|
91
|
-
tokensOut: "\
|
|
87
|
+
model: "\uDB81\uDE5B", // custom model icon
|
|
88
|
+
apiState: "\uF725", // api state icon
|
|
89
|
+
toolCount: "\uF0AD", // tool count icon
|
|
90
|
+
git: "\uF0E8", // git icon
|
|
91
|
+
context: "\uF8D8", // context icon
|
|
92
|
+
cost: "\uF155", // cost icon
|
|
93
|
+
tokens: "\uF07B", // tokens icon
|
|
94
|
+
tokensIn: "\uF07B", // tokens in icon
|
|
95
|
+
tokensOut: "\uF07B", // tokens out icon
|
|
92
96
|
session: "\uF550", // nf-md-identifier
|
|
93
97
|
hostname: "\uF109", // nf-fa-laptop
|
|
94
98
|
time: "\uF017", // nf-fa-clock_o
|
|
99
|
+
tps: "\uF062", // \u2191 up arrow
|
|
100
|
+
clock: "\uF017", // nf-fa-clock_o
|
|
101
|
+
duration: "\uF49B", // nf-md-timer_outline
|
|
102
|
+
thinkingLevel: "\uF4D8", // nf-fa-lightbulb_o
|
|
95
103
|
|
|
96
104
|
// Compactor
|
|
97
|
-
sessionEvents: "\
|
|
98
|
-
compactions: "\
|
|
99
|
-
tokensSaved: "\uF155", //
|
|
100
|
-
compressionRatio:"\
|
|
101
|
-
indexedDocs: "\
|
|
102
|
-
sandboxRuns: "\uF121", //
|
|
103
|
-
searchQueries: "\uF002", //
|
|
105
|
+
sessionEvents: "\uDBB1\uDECF", // session events icon
|
|
106
|
+
compactions: "\uDBB1\uDECF", // compactions icon
|
|
107
|
+
tokensSaved: "\uF155", // tokens saved icon
|
|
108
|
+
compressionRatio:"\uDBB1\uDECF", // compression ratio icon
|
|
109
|
+
indexedDocs: "\uDB81\uDE19", // indexed docs icon
|
|
110
|
+
sandboxRuns: "\uF121", // sandbox runs icon
|
|
111
|
+
searchQueries: "\uF002", // search queries icon
|
|
104
112
|
|
|
105
113
|
// Memory
|
|
106
|
-
projectCount: "\
|
|
107
|
-
totalCount: "\
|
|
108
|
-
consolidations: "\
|
|
114
|
+
projectCount: "\uDB81\uDED4", // memory icon
|
|
115
|
+
totalCount: "\uEB9C", // total count icon
|
|
116
|
+
consolidations: "\uDB81\uDED4", // consolidations icon
|
|
109
117
|
|
|
110
118
|
// MCP
|
|
111
|
-
serversTotal: "\
|
|
112
|
-
serversActive: "\uF058", //
|
|
113
|
-
toolsTotal: "\uF0AD", //
|
|
114
|
-
serversFailed: "\
|
|
119
|
+
serversTotal: "\uF0F6", // servers total icon
|
|
120
|
+
serversActive: "\uF058", // servers active icon
|
|
121
|
+
toolsTotal: "\uF0AD", // tools total icon
|
|
122
|
+
serversFailed: "\uF467", // servers failed icon
|
|
115
123
|
|
|
116
124
|
// Ralph
|
|
117
125
|
activeLoops: "\udb81\udf09", // ralph loop icon
|
|
@@ -119,22 +127,22 @@ export const NERD_ICONS: IconSet = {
|
|
|
119
127
|
loopStatus: "\udb81\udf09", // ralph loop icon
|
|
120
128
|
|
|
121
129
|
// Workflow
|
|
122
|
-
currentCommand: "\
|
|
123
|
-
sandboxLevel: "\
|
|
124
|
-
commandDuration: "\
|
|
130
|
+
currentCommand: "\uF0E8", // current command icon
|
|
131
|
+
sandboxLevel: "\uDBB1\uDDFE", // sandbox level icon
|
|
132
|
+
commandDuration: "\uDBB9\uDEAB", // command duration icon
|
|
125
133
|
|
|
126
134
|
// Kanboard
|
|
127
|
-
docsCount: "\
|
|
128
|
-
tasksDone: "\
|
|
129
|
-
tasksTotal: "\
|
|
130
|
-
taskPct: "\
|
|
135
|
+
docsCount: "\uDB81\uDE19", // docs count icon
|
|
136
|
+
tasksDone: "\uF0E8", // tasks done icon
|
|
137
|
+
tasksTotal: "\uF0E8", // tasks total icon
|
|
138
|
+
taskPct: "\uF0E8", // task pct icon
|
|
131
139
|
|
|
132
140
|
// Notify
|
|
133
141
|
platformsEnabled:"\uF0E0", // nf-fa-envelope
|
|
134
142
|
lastSent: "\uF017", // nf-fa-clock_o
|
|
135
143
|
|
|
136
144
|
// Extension status
|
|
137
|
-
extensionStatuses:"\
|
|
145
|
+
extensionStatuses:"\uDBB5\uDEAB", // extension statuses icon
|
|
138
146
|
|
|
139
147
|
separator: "\uE0B1", // nf-pl-left_soft_divider
|
|
140
148
|
};
|
|
@@ -144,61 +152,66 @@ export const NERD_ICONS: IconSet = {
|
|
|
144
152
|
/** Unicode emoji / symbol icons — works on most modern terminals */
|
|
145
153
|
export const EMOJI_ICONS: IconSet = {
|
|
146
154
|
// Core
|
|
147
|
-
model: "",
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
git: "
|
|
151
|
-
context: "",
|
|
152
|
-
cost: "
|
|
153
|
-
tokens: "
|
|
154
|
-
tokensIn: "
|
|
155
|
-
tokensOut: "
|
|
156
|
-
session: "
|
|
157
|
-
hostname: "
|
|
155
|
+
model: "🤖",
|
|
156
|
+
apiState: "🔄",
|
|
157
|
+
toolCount: "🔧",
|
|
158
|
+
git: "🔀",
|
|
159
|
+
context: "🗄️",
|
|
160
|
+
cost: "💲",
|
|
161
|
+
tokens: "📊",
|
|
162
|
+
tokensIn: "⬇️",
|
|
163
|
+
tokensOut: "⬆️",
|
|
164
|
+
session: "📋",
|
|
165
|
+
hostname: "🏠",
|
|
158
166
|
time: "⏱",
|
|
159
167
|
|
|
168
|
+
tps: "⚡",
|
|
169
|
+
clock: "🕔",
|
|
170
|
+
duration: "⏱",
|
|
171
|
+
thinkingLevel: "💡",
|
|
172
|
+
|
|
160
173
|
// Compactor
|
|
161
|
-
sessionEvents: "
|
|
162
|
-
compactions: "
|
|
163
|
-
tokensSaved: "
|
|
164
|
-
compressionRatio:"
|
|
165
|
-
indexedDocs: "
|
|
166
|
-
sandboxRuns: "
|
|
167
|
-
searchQueries: "
|
|
174
|
+
sessionEvents: "📈",
|
|
175
|
+
compactions: "🗜️",
|
|
176
|
+
tokensSaved: "💲",
|
|
177
|
+
compressionRatio:"📐",
|
|
178
|
+
indexedDocs: "📑",
|
|
179
|
+
sandboxRuns: "▶️",
|
|
180
|
+
searchQueries: "🔍",
|
|
168
181
|
|
|
169
182
|
// Memory
|
|
170
|
-
projectCount: "
|
|
171
|
-
totalCount: "
|
|
172
|
-
consolidations: "
|
|
183
|
+
projectCount: "🧠",
|
|
184
|
+
totalCount: "🧠",
|
|
185
|
+
consolidations: "🔄",
|
|
173
186
|
|
|
174
187
|
// MCP
|
|
175
|
-
serversTotal: "
|
|
176
|
-
serversActive: "
|
|
188
|
+
serversTotal: "🖥️",
|
|
189
|
+
serversActive: "🟢",
|
|
177
190
|
toolsTotal: "🔧",
|
|
178
|
-
serversFailed: "
|
|
191
|
+
serversFailed: "⚠️",
|
|
179
192
|
|
|
180
193
|
// Ralph
|
|
181
|
-
activeLoops: "",
|
|
182
|
-
totalIterations: "",
|
|
183
|
-
loopStatus: "",
|
|
194
|
+
activeLoops: "🔁",
|
|
195
|
+
totalIterations: "🔁",
|
|
196
|
+
loopStatus: "🔁",
|
|
184
197
|
|
|
185
198
|
// Workflow
|
|
186
|
-
currentCommand: "",
|
|
187
|
-
sandboxLevel: "
|
|
199
|
+
currentCommand: "▶️",
|
|
200
|
+
sandboxLevel: "🔒",
|
|
188
201
|
commandDuration: "⏱",
|
|
189
202
|
|
|
190
203
|
// Kanboard
|
|
191
|
-
docsCount: "
|
|
192
|
-
tasksDone: "
|
|
193
|
-
tasksTotal: "
|
|
194
|
-
taskPct: "
|
|
204
|
+
docsCount: "📑",
|
|
205
|
+
tasksDone: "✅",
|
|
206
|
+
tasksTotal: "📋",
|
|
207
|
+
taskPct: "📊",
|
|
195
208
|
|
|
196
209
|
// Notify
|
|
197
|
-
platformsEnabled:"
|
|
210
|
+
platformsEnabled:"🔔",
|
|
198
211
|
lastSent: "⏱",
|
|
199
212
|
|
|
200
213
|
// Extension status
|
|
201
|
-
extensionStatuses:"
|
|
214
|
+
extensionStatuses:"🧩",
|
|
202
215
|
|
|
203
216
|
separator: "|",
|
|
204
217
|
};
|
|
@@ -208,61 +221,66 @@ export const EMOJI_ICONS: IconSet = {
|
|
|
208
221
|
/** Plain text labels — works everywhere, most compact */
|
|
209
222
|
export const TEXT_ICONS: IconSet = {
|
|
210
223
|
// Core
|
|
211
|
-
model: "",
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
git: "",
|
|
215
|
-
context: "",
|
|
216
|
-
cost: "",
|
|
217
|
-
tokens: "",
|
|
218
|
-
tokensIn: "",
|
|
219
|
-
tokensOut: "",
|
|
220
|
-
session: "",
|
|
221
|
-
hostname: "",
|
|
222
|
-
time: "",
|
|
224
|
+
model: "MDL",
|
|
225
|
+
apiState: "API",
|
|
226
|
+
toolCount: "TLS",
|
|
227
|
+
git: "GIT",
|
|
228
|
+
context: "CTX",
|
|
229
|
+
cost: "CST",
|
|
230
|
+
tokens: "TOK",
|
|
231
|
+
tokensIn: "TKI",
|
|
232
|
+
tokensOut: "TKO",
|
|
233
|
+
session: "SES",
|
|
234
|
+
hostname: "HST",
|
|
235
|
+
time: "TIM",
|
|
236
|
+
|
|
237
|
+
tps: "TPS",
|
|
238
|
+
clock: "CLK",
|
|
239
|
+
duration: "DUR",
|
|
240
|
+
thinkingLevel: "THK",
|
|
223
241
|
|
|
224
242
|
// Compactor
|
|
225
|
-
sessionEvents: "
|
|
226
|
-
compactions: "
|
|
227
|
-
tokensSaved: "
|
|
228
|
-
compressionRatio:"
|
|
229
|
-
indexedDocs: "
|
|
230
|
-
sandboxRuns: "
|
|
231
|
-
searchQueries: "
|
|
243
|
+
sessionEvents: "EVT",
|
|
244
|
+
compactions: "CMP",
|
|
245
|
+
tokensSaved: "SVD",
|
|
246
|
+
compressionRatio:"RAT",
|
|
247
|
+
indexedDocs: "IDX",
|
|
248
|
+
sandboxRuns: "SBX",
|
|
249
|
+
searchQueries: "QRY",
|
|
232
250
|
|
|
233
251
|
// Memory
|
|
234
|
-
projectCount: "
|
|
235
|
-
totalCount: "
|
|
236
|
-
consolidations: "
|
|
252
|
+
projectCount: "MEM",
|
|
253
|
+
totalCount: "MEM",
|
|
254
|
+
consolidations: "CNS",
|
|
237
255
|
|
|
238
256
|
// MCP
|
|
239
|
-
serversTotal: "
|
|
240
|
-
serversActive: "
|
|
241
|
-
toolsTotal: "
|
|
242
|
-
serversFailed: "
|
|
257
|
+
serversTotal: "SRV",
|
|
258
|
+
serversActive: "ACT",
|
|
259
|
+
toolsTotal: "TLS",
|
|
260
|
+
serversFailed: "ERR",
|
|
243
261
|
|
|
244
262
|
// Ralph
|
|
245
|
-
activeLoops: "",
|
|
246
|
-
totalIterations: "",
|
|
247
|
-
loopStatus: "",
|
|
263
|
+
activeLoops: "LPS",
|
|
264
|
+
totalIterations: "ITR",
|
|
265
|
+
loopStatus: "STS",
|
|
248
266
|
|
|
249
267
|
// Workflow
|
|
250
|
-
currentCommand: "",
|
|
251
|
-
sandboxLevel: "
|
|
252
|
-
commandDuration: "
|
|
268
|
+
currentCommand: "CMD",
|
|
269
|
+
sandboxLevel: "SBX",
|
|
270
|
+
commandDuration: "DUR",
|
|
253
271
|
|
|
254
272
|
// Kanboard
|
|
255
|
-
docsCount: "
|
|
256
|
-
tasksDone: "
|
|
257
|
-
tasksTotal: "
|
|
258
|
-
taskPct: "
|
|
273
|
+
docsCount: "DOC",
|
|
274
|
+
tasksDone: "DNE",
|
|
275
|
+
tasksTotal: "TSK",
|
|
276
|
+
taskPct: "PCT",
|
|
259
277
|
|
|
260
278
|
// Notify
|
|
261
|
-
platformsEnabled:"
|
|
262
|
-
lastSent: "
|
|
279
|
+
platformsEnabled:"NTF",
|
|
280
|
+
lastSent: "LST",
|
|
263
281
|
|
|
264
282
|
// Extension status
|
|
265
|
-
extensionStatuses:"
|
|
283
|
+
extensionStatuses:"EXT",
|
|
266
284
|
|
|
267
285
|
separator: "|",
|
|
268
286
|
};
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
10
|
-
import type { PresetDef, FooterSegmentContext, FooterSegment, ColorScheme, RenderedSegment } from "../types.js";
|
|
10
|
+
import type { PresetDef, FooterSegmentContext, FooterSegment, ColorScheme, RenderedSegment, SegmentZone } from "../types.js";
|
|
11
11
|
import type { FooterRegistry } from "../registry/index.js";
|
|
12
|
+
import { visibleWidth as piVisibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
12
13
|
import { getSeparator, separatorVisibleWidth } from "./separators.js";
|
|
13
14
|
import { getDefaultColors } from "./theme.js";
|
|
14
15
|
import { setIconStyle } from "./icons.js";
|
|
@@ -29,10 +30,9 @@ interface RenderedSegmentWithWidth {
|
|
|
29
30
|
|
|
30
31
|
// ─── ANSI helpers ───────────────────────────────────────────────────────────
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
+
/** ANSI-aware visible width using pi-tui */
|
|
33
34
|
function visibleWidth(text: string): number {
|
|
34
|
-
|
|
35
|
-
return stripped.length;
|
|
35
|
+
return piVisibleWidth(text);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const ANSI_RESET = "\x1b[0m";
|
|
@@ -143,8 +143,8 @@ export class FooterRenderer {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
|
-
* Compute responsive layout for the given width.
|
|
147
|
-
* Segments
|
|
146
|
+
* Compute responsive zone-based layout for the given width.
|
|
147
|
+
* Segments are grouped by zone (left/center/right) and rendered with alignment.
|
|
148
148
|
*/
|
|
149
149
|
computeLayout(width: number): { topContent: string; secondaryContent: string } {
|
|
150
150
|
// Return cached layout if still valid
|
|
@@ -155,41 +155,45 @@ export class FooterRenderer {
|
|
|
155
155
|
|
|
156
156
|
const presetDef = getPreset(this.presetName);
|
|
157
157
|
const colors = presetDef.colors ?? getDefaultColors();
|
|
158
|
+
const settings = loadFooterSettings();
|
|
159
|
+
const labelMode = settings.showFullLabels ? "labeled" as const : "compact" as const;
|
|
158
160
|
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
...presetDef.rightSegments,
|
|
163
|
-
...presetDef.secondarySegments,
|
|
164
|
-
];
|
|
161
|
+
// Collect all segment IDs from preset
|
|
162
|
+
const primaryIds = [...presetDef.leftSegments, ...presetDef.rightSegments];
|
|
163
|
+
const secondaryIds = [...presetDef.secondarySegments];
|
|
165
164
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
// Render segments grouped by their zone
|
|
166
|
+
const zones: Record<SegmentZone, RenderedSegmentWithWidth[]> = {
|
|
167
|
+
left: [],
|
|
168
|
+
center: [],
|
|
169
|
+
right: [],
|
|
170
|
+
};
|
|
171
|
+
const overflowZones: Record<SegmentZone, RenderedSegmentWithWidth[]> = {
|
|
172
|
+
left: [],
|
|
173
|
+
center: [],
|
|
174
|
+
right: [],
|
|
175
|
+
};
|
|
169
176
|
|
|
177
|
+
// Render primary segments and group by zone
|
|
178
|
+
for (const segId of primaryIds) {
|
|
179
|
+
const rendered = this.renderSegment(segId, colors, width, labelMode);
|
|
180
|
+
if (!rendered) continue;
|
|
170
181
|
const segment = this.segmentLookup.get(segId);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const rendered = segment.render(ctx);
|
|
183
|
-
if (!rendered.visible || !rendered.content) continue;
|
|
184
|
-
|
|
185
|
-
renderedSegments.push({
|
|
186
|
-
content: rendered.content,
|
|
187
|
-
width: visibleWidth(rendered.content),
|
|
188
|
-
visible: true,
|
|
189
|
-
});
|
|
182
|
+
const zone = segment?.zone ?? "center";
|
|
183
|
+
zones[zone].push(rendered);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Render secondary segments
|
|
187
|
+
const secondaryRendered: RenderedSegmentWithWidth[] = [];
|
|
188
|
+
for (const segId of secondaryIds) {
|
|
189
|
+
const rendered = this.renderSegment(segId, colors, width, labelMode);
|
|
190
|
+
if (!rendered) continue;
|
|
191
|
+
secondaryRendered.push(rendered);
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
if
|
|
194
|
+
// Check if we have any content
|
|
195
|
+
const totalSegments = zones.left.length + zones.center.length + zones.right.length;
|
|
196
|
+
if (totalSegments === 0 && secondaryRendered.length === 0) {
|
|
193
197
|
this.lastLayoutResult = { topContent: "", secondaryContent: "" };
|
|
194
198
|
this.lastLayoutWidth = width;
|
|
195
199
|
this.lastLayoutTimestamp = now;
|
|
@@ -197,53 +201,46 @@ export class FooterRenderer {
|
|
|
197
201
|
return this.lastLayoutResult;
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
} else {
|
|
227
|
-
overflow = true;
|
|
228
|
-
overflowParts.push(seg);
|
|
204
|
+
const sepDef = getSeparator(settings.separator);
|
|
205
|
+
const sepWidth = visibleWidth(sepDef.left) + 2;
|
|
206
|
+
const zoneSep = presetDef.zoneSeparator ?? settings.zoneSeparator ?? "\u2502";
|
|
207
|
+
const zoneSepWidth = visibleWidth(zoneSep) + 2; // +2 for spaces around zone sep
|
|
208
|
+
const dimZoneSep = `\x1b[2m${zoneSep}\x1b[0m`; // dimmed zone separator
|
|
209
|
+
|
|
210
|
+
// Calculate widths per zone
|
|
211
|
+
const leftWidth = this.measureZoneWidth(zones.left, sepWidth);
|
|
212
|
+
const rightWidth = this.measureZoneWidth(zones.right, sepWidth);
|
|
213
|
+
const numZoneSeps = (leftWidth > 0 ? 1 : 0) + (rightWidth > 0 ? 1 : 0);
|
|
214
|
+
const availableForCenter = width - leftWidth - rightWidth - numZoneSeps * zoneSepWidth - 2; // -2 for margins
|
|
215
|
+
|
|
216
|
+
// Overflow check: if center doesn't fit, move excess to overflow
|
|
217
|
+
const centerWidth = this.measureZoneWidth(zones.center, sepWidth);
|
|
218
|
+
if (centerWidth > Math.max(0, availableForCenter)) {
|
|
219
|
+
// Move overflow center segments to secondary
|
|
220
|
+
let fitWidth = 0;
|
|
221
|
+
let cutoffIdx = 0;
|
|
222
|
+
for (let i = 0; i < zones.center.length; i++) {
|
|
223
|
+
const needed = zones.center[i].width + (i > 0 ? sepWidth : 0);
|
|
224
|
+
if (fitWidth + needed <= Math.max(0, availableForCenter)) {
|
|
225
|
+
fitWidth += needed;
|
|
226
|
+
cutoffIdx = i + 1;
|
|
227
|
+
} else {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
229
230
|
}
|
|
231
|
+
const overflow = zones.center.splice(cutoffIdx);
|
|
232
|
+
overflowZones.center.push(...overflow);
|
|
230
233
|
}
|
|
231
234
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
let secondaryParts: string[] = [];
|
|
235
|
-
for (const seg of overflowParts) {
|
|
236
|
-
const needed = seg.width + (secondaryParts.length > 0 ? sepWidth : 0);
|
|
237
|
-
if (secondaryWidth + needed <= width) {
|
|
238
|
-
secondaryParts.push(seg.content);
|
|
239
|
-
secondaryWidth += needed;
|
|
240
|
-
} else {
|
|
241
|
-
break; // Stop at first non-fitting segment
|
|
242
|
-
}
|
|
243
|
-
}
|
|
235
|
+
// Build top row with zones
|
|
236
|
+
const topContent = this.buildZoneRow(zones, width, sepDef, dimZoneSep);
|
|
244
237
|
|
|
245
|
-
|
|
246
|
-
const
|
|
238
|
+
// Build secondary row with overflow + preset secondary segments
|
|
239
|
+
const allSecondary = [...overflowZones.center, ...secondaryRendered];
|
|
240
|
+
const secondaryContent = this.buildContentFromParts(
|
|
241
|
+
allSecondary.map(s => s.content),
|
|
242
|
+
sepDef,
|
|
243
|
+
);
|
|
247
244
|
|
|
248
245
|
this.lastLayoutResult = { topContent, secondaryContent };
|
|
249
246
|
this.lastLayoutWidth = width;
|
|
@@ -253,6 +250,128 @@ export class FooterRenderer {
|
|
|
253
250
|
return this.lastLayoutResult;
|
|
254
251
|
}
|
|
255
252
|
|
|
253
|
+
/** Render a single segment by ID, returns null if not visible */
|
|
254
|
+
private renderSegment(
|
|
255
|
+
segId: string,
|
|
256
|
+
colors: ColorScheme,
|
|
257
|
+
fullWidth: number,
|
|
258
|
+
labelMode: "compact" | "labeled",
|
|
259
|
+
): RenderedSegmentWithWidth | null {
|
|
260
|
+
if (!isSegmentEnabled(this.getGroupForSegment(segId), segId)) return null;
|
|
261
|
+
|
|
262
|
+
const segment = this.segmentLookup.get(segId);
|
|
263
|
+
if (!segment) return null;
|
|
264
|
+
|
|
265
|
+
const ctx: FooterSegmentContext = {
|
|
266
|
+
theme: this.getThemeLike(),
|
|
267
|
+
colors,
|
|
268
|
+
data: this.registry.getGroupData(this.getGroupForSegment(segId)),
|
|
269
|
+
width: fullWidth,
|
|
270
|
+
piContext: this.piContext,
|
|
271
|
+
footerData: this.footerData,
|
|
272
|
+
labelMode,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const rendered = segment.render(ctx);
|
|
276
|
+
if (!rendered.visible || !rendered.content) return null;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
content: rendered.content,
|
|
280
|
+
width: visibleWidth(rendered.content),
|
|
281
|
+
visible: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Measure total width of a zone's rendered segments */
|
|
286
|
+
private measureZoneWidth(segments: RenderedSegmentWithWidth[], sepWidth: number): number {
|
|
287
|
+
if (segments.length === 0) return 0;
|
|
288
|
+
let total = 0;
|
|
289
|
+
for (let i = 0; i < segments.length; i++) {
|
|
290
|
+
total += segments[i].width + (i > 0 ? sepWidth : 0);
|
|
291
|
+
}
|
|
292
|
+
return total;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Build a zone-based row string */
|
|
296
|
+
private buildZoneRow(
|
|
297
|
+
zones: Record<SegmentZone, RenderedSegmentWithWidth[]>,
|
|
298
|
+
fullWidth: number,
|
|
299
|
+
sepDef: { left: string },
|
|
300
|
+
dimZoneSep: string,
|
|
301
|
+
): string {
|
|
302
|
+
const parts: string[] = [];
|
|
303
|
+
|
|
304
|
+
// Left zone
|
|
305
|
+
const leftContent = this.buildContentFromPartsRaw(
|
|
306
|
+
zones.left.map(s => s.content),
|
|
307
|
+
sepDef,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Center zone
|
|
311
|
+
const centerContent = this.buildContentFromPartsRaw(
|
|
312
|
+
zones.center.map(s => s.content),
|
|
313
|
+
sepDef,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Right zone
|
|
317
|
+
const rightContent = this.buildContentFromPartsRaw(
|
|
318
|
+
zones.right.map(s => s.content),
|
|
319
|
+
sepDef,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Assemble zones with alignment
|
|
323
|
+
const leftWidth = zones.left.length > 0 ? this.measureZoneWidth(zones.left, visibleWidth(sepDef.left) + 2) : 0;
|
|
324
|
+
const rightWidth = zones.right.length > 0 ? this.measureZoneWidth(zones.right, visibleWidth(sepDef.left) + 2) : 0;
|
|
325
|
+
|
|
326
|
+
// Simple case: no zones → return empty
|
|
327
|
+
if (!leftContent && !centerContent && !rightContent) return "";
|
|
328
|
+
|
|
329
|
+
// Build with zone separators
|
|
330
|
+
let result = " "; // leading margin
|
|
331
|
+
|
|
332
|
+
if (leftContent) {
|
|
333
|
+
result += leftContent;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (centerContent) {
|
|
337
|
+
if (leftContent) result += ` ${dimZoneSep} `;
|
|
338
|
+
result += centerContent;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (rightContent) {
|
|
342
|
+
const currentLen = visibleWidth(result);
|
|
343
|
+
const rightStart = fullWidth - rightWidth - 1; // -1 for trailing margin
|
|
344
|
+
const gap = rightStart - currentLen;
|
|
345
|
+
|
|
346
|
+
if (gap > 0) {
|
|
347
|
+
// Pad to right-align the right zone
|
|
348
|
+
result += " ".repeat(gap);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (centerContent || leftContent) {
|
|
352
|
+
// Only add zone separator if there's content before it
|
|
353
|
+
if (gap > visibleWidth(dimZoneSep) + 2) {
|
|
354
|
+
// Place zone sep right before right content
|
|
355
|
+
const sepPos = result.length - gap + Math.floor((gap - visibleWidth(dimZoneSep)) / 2);
|
|
356
|
+
// Simpler: just put it at the boundary
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
result += rightContent;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
result += " "; // trailing margin
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Build content from parts array (raw strings) */
|
|
368
|
+
private buildContentFromPartsRaw(parts: string[], sepDef: { left: string }): string {
|
|
369
|
+
if (parts.length === 0) return "";
|
|
370
|
+
const sep = sepDef.left;
|
|
371
|
+
const sepAnsi = getFgAnsiCode(getPreset(this.presetName).colors ?? getDefaultColors(), "separator");
|
|
372
|
+
return parts.join(` ${sepAnsi}${sep}${ANSI_RESET} `);
|
|
373
|
+
}
|
|
374
|
+
|
|
256
375
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
257
376
|
|
|
258
377
|
private buildContentFromParts(parts: string[], sepDef: { left: string }): string {
|
|
@@ -265,7 +384,7 @@ export class FooterRenderer {
|
|
|
265
384
|
/** Map a segment ID to its group ID */
|
|
266
385
|
private getGroupForSegment(segId: string): string {
|
|
267
386
|
// Core segments
|
|
268
|
-
const coreIds = ["model", "
|
|
387
|
+
const coreIds = ["model", "api_state", "tool_count", "git", "context_pct", "cost", "tokens_total", "tokens_in", "tokens_out", "session", "hostname", "time", "tps", "clock", "duration", "thinking_level"];
|
|
269
388
|
if (coreIds.includes(segId)) return "core";
|
|
270
389
|
|
|
271
390
|
// Compactor segments
|