@xynogen/pix-runtime 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/pix-command.ts +129 -33
package/package.json
CHANGED
package/src/pix-command.ts
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Key,
|
|
12
|
+
type KeybindingsManager,
|
|
13
|
+
matchesKey,
|
|
14
|
+
truncateToWidth,
|
|
15
|
+
visibleWidth,
|
|
16
|
+
wrapTextWithAnsi,
|
|
17
|
+
} from "@earendil-works/pi-tui";
|
|
11
18
|
import type { PixRuntime } from "./runtime.ts";
|
|
12
19
|
import type { DeepPartial, SectionHandle } from "./schema.ts";
|
|
13
20
|
import { collapseSection } from "./sections/collapse.ts";
|
|
@@ -104,16 +111,20 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
104
111
|
const boxW = 52;
|
|
105
112
|
await ui.custom(
|
|
106
113
|
(
|
|
107
|
-
tui: { requestRender(): void },
|
|
114
|
+
tui: { requestRender(): void; terminal?: { rows?: number } },
|
|
108
115
|
theme: {
|
|
109
116
|
fg(c: string, t: string): string;
|
|
110
117
|
bg(c: string, t: string): string;
|
|
111
118
|
bold(t: string): string;
|
|
112
119
|
},
|
|
113
|
-
|
|
120
|
+
kb: KeybindingsManager,
|
|
114
121
|
done: (v: null) => void,
|
|
115
122
|
) => {
|
|
116
123
|
let selected = 0;
|
|
124
|
+
let bodyOffset = 0;
|
|
125
|
+
let visibleBodyLines = 1;
|
|
126
|
+
let maxBodyOffset = 0;
|
|
127
|
+
let inspectingPage = false;
|
|
117
128
|
|
|
118
129
|
const cycle = (direction: -1 | 1) => {
|
|
119
130
|
const row = SETTINGS[selected];
|
|
@@ -126,18 +137,20 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
126
137
|
};
|
|
127
138
|
const move = (direction: -1 | 1) => {
|
|
128
139
|
selected = (selected + direction + SETTINGS.length) % SETTINGS.length;
|
|
140
|
+
inspectingPage = false;
|
|
129
141
|
};
|
|
130
142
|
|
|
131
143
|
return {
|
|
132
144
|
render: () => {
|
|
133
145
|
const labelW = Math.max(...SETTINGS.map((r) => r.label.length));
|
|
134
|
-
const
|
|
146
|
+
const body: string[] = [];
|
|
147
|
+
const settingBodyLines: number[] = [];
|
|
135
148
|
let lastSection = "";
|
|
136
149
|
for (let i = 0; i < SETTINGS.length; i++) {
|
|
137
150
|
const row = SETTINGS[i]!;
|
|
138
151
|
if (row.section !== lastSection) {
|
|
139
|
-
if (lastSection)
|
|
140
|
-
|
|
152
|
+
if (lastSection) body.push("");
|
|
153
|
+
body.push(theme.fg("dim", ` ${row.section}`));
|
|
141
154
|
lastSection = row.section;
|
|
142
155
|
}
|
|
143
156
|
const sel = i === selected;
|
|
@@ -145,19 +158,40 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
145
158
|
const label = theme.fg(sel ? "accent" : "text", row.label.padEnd(labelW));
|
|
146
159
|
const value = row.read(runtime.get(row.handle));
|
|
147
160
|
const isDefault = value === row.values[0];
|
|
148
|
-
|
|
161
|
+
settingBodyLines[i] = body.length;
|
|
162
|
+
body.push(`${cursor} ${label} ${theme.fg(isDefault ? "dim" : "success", value)}`);
|
|
149
163
|
}
|
|
150
|
-
|
|
151
|
-
lines.push(theme.fg("dim", "←→ change · ↑↓ move · esc close"));
|
|
152
|
-
return frameLines({
|
|
164
|
+
const result = frameModal({
|
|
153
165
|
width: boxW,
|
|
154
|
-
|
|
166
|
+
maxHeight: modalHeight(tui.terminal?.rows),
|
|
167
|
+
header: [theme.fg("accent", theme.bold(" pix settings")), ""],
|
|
168
|
+
body,
|
|
169
|
+
footer: [
|
|
170
|
+
"",
|
|
171
|
+
theme.fg("dim", "←→ change · ↑↓ move · PgUp/PgDn inspect · esc close"),
|
|
172
|
+
],
|
|
173
|
+
bodyOffset,
|
|
174
|
+
selectedBodyLine: inspectingPage ? undefined : settingBodyLines[selected],
|
|
155
175
|
color: (s: string) => theme.fg("accent", s),
|
|
156
176
|
bg: (s: string) => theme.bg("customMessageBg", s),
|
|
157
177
|
});
|
|
178
|
+
bodyOffset = result.bodyOffset;
|
|
179
|
+
visibleBodyLines = Math.max(1, result.visibleBodyLines);
|
|
180
|
+
maxBodyOffset = result.maxBodyOffset;
|
|
181
|
+
return result.lines;
|
|
158
182
|
},
|
|
159
183
|
invalidate: () => {},
|
|
160
184
|
handleInput: (data: string) => {
|
|
185
|
+
const pageUp = kb?.matches(data, "tui.select.pageUp") || matchesKey(data, Key.pageUp);
|
|
186
|
+
const pageDown =
|
|
187
|
+
kb?.matches(data, "tui.select.pageDown") || matchesKey(data, Key.pageDown);
|
|
188
|
+
if (pageUp || pageDown) {
|
|
189
|
+
const direction = pageUp ? -1 : 1;
|
|
190
|
+
bodyOffset = pageBodyOffset(bodyOffset, visibleBodyLines, maxBodyOffset, direction);
|
|
191
|
+
inspectingPage = true;
|
|
192
|
+
tui.requestRender();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
161
195
|
// matchesKey handles both legacy bytes and Kitty CSI-u encodings
|
|
162
196
|
// for letters and special keys alike — raw string compares like
|
|
163
197
|
// `data === "k"` silently fail under the Kitty keyboard protocol.
|
|
@@ -185,29 +219,96 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
185
219
|
});
|
|
186
220
|
}
|
|
187
221
|
|
|
188
|
-
// ── Inline
|
|
222
|
+
// ── Inline modal engine (runtime cannot depend on pix-pretty: pretty depends on runtime) ─
|
|
189
223
|
|
|
190
|
-
interface
|
|
224
|
+
interface RuntimeModalOptions {
|
|
191
225
|
width: number;
|
|
192
|
-
|
|
226
|
+
maxHeight: number;
|
|
227
|
+
header: string[];
|
|
228
|
+
body: string[];
|
|
229
|
+
footer: string[];
|
|
230
|
+
bodyOffset: number;
|
|
231
|
+
selectedBodyLine?: number;
|
|
193
232
|
color: (s: string) => string;
|
|
194
233
|
bg?: (s: string) => string;
|
|
195
234
|
}
|
|
196
235
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
236
|
+
interface RuntimeModalResult {
|
|
237
|
+
lines: string[];
|
|
238
|
+
bodyOffset: number;
|
|
239
|
+
visibleBodyLines: number;
|
|
240
|
+
maxBodyOffset: number;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function modalHeight(rows = 24): number {
|
|
244
|
+
const safe = Number.isFinite(rows) ? Math.max(1, Math.floor(rows)) : 24;
|
|
245
|
+
return Math.max(1, Math.min(safe, Math.floor(safe * 0.8)));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function pageBodyOffset(offset: number, size: number, max: number, direction: -1 | 1): number {
|
|
249
|
+
const s = Math.max(1, size);
|
|
250
|
+
const step = Math.max(1, Math.floor(s / 2));
|
|
251
|
+
return Math.min(max, Math.max(0, offset + direction * step));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function fitLines(lines: string[], width: number): string[] {
|
|
255
|
+
const rows: string[] = [];
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
if (line === "") rows.push("");
|
|
258
|
+
else rows.push(...wrapTextWithAnsi(line, Math.max(1, width)));
|
|
259
|
+
}
|
|
260
|
+
return rows;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function frameModal(opts: RuntimeModalOptions): RuntimeModalResult {
|
|
264
|
+
const inner = Math.max(1, opts.width - 4);
|
|
265
|
+
const header = fitLines(opts.header, inner);
|
|
266
|
+
const footer = fitLines(opts.footer, inner);
|
|
267
|
+
const body = fitLines(opts.body, inner);
|
|
268
|
+
const cap = Math.max(1, opts.maxHeight);
|
|
269
|
+
const bodyBudget = cap - 2 - header.length - footer.length;
|
|
270
|
+
const overflows = body.length > Math.max(0, bodyBudget);
|
|
271
|
+
const visibleBodyLines = overflows ? Math.max(0, bodyBudget - 1) : Math.max(0, bodyBudget);
|
|
272
|
+
if (visibleBodyLines === 0 && body.length > 0) {
|
|
273
|
+
return {
|
|
274
|
+
lines: frameRows(opts.width, ["Terminal too short — resize or press esc to cancel"], opts),
|
|
275
|
+
bodyOffset: 0,
|
|
276
|
+
visibleBodyLines: 0,
|
|
277
|
+
maxBodyOffset: 0,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const maxBodyOffset = Math.max(0, body.length - visibleBodyLines);
|
|
281
|
+
let offset = Math.min(maxBodyOffset, Math.max(0, opts.bodyOffset));
|
|
282
|
+
if (opts.selectedBodyLine !== undefined) {
|
|
283
|
+
const selected = Math.min(body.length - 1, Math.max(0, opts.selectedBodyLine));
|
|
284
|
+
if (selected < offset) offset = selected;
|
|
285
|
+
else if (selected >= offset + visibleBodyLines) offset = selected - visibleBodyLines + 1;
|
|
286
|
+
}
|
|
287
|
+
const end = Math.min(body.length, offset + visibleBodyLines);
|
|
288
|
+
const lines = [...header];
|
|
289
|
+
if (overflows) {
|
|
290
|
+
const step = Math.max(1, Math.floor(visibleBodyLines / 2));
|
|
291
|
+
const totalPages = Math.max(1, Math.ceil(maxBodyOffset / step) + 1);
|
|
292
|
+
const page = offset >= maxBodyOffset ? totalPages : Math.floor(offset / step) + 1;
|
|
293
|
+
lines.push(`PgUp/PgDn inspect • ${page}/${totalPages}`);
|
|
294
|
+
}
|
|
295
|
+
lines.push(...body.slice(offset, end), ...footer);
|
|
296
|
+
return {
|
|
297
|
+
lines: frameRows(opts.width, lines, opts),
|
|
298
|
+
bodyOffset: offset,
|
|
299
|
+
visibleBodyLines,
|
|
300
|
+
maxBodyOffset,
|
|
301
|
+
};
|
|
200
302
|
}
|
|
201
303
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
304
|
+
function frameRows(
|
|
305
|
+
width: number,
|
|
306
|
+
lines: string[],
|
|
307
|
+
opts: Pick<RuntimeModalOptions, "color" | "bg">,
|
|
308
|
+
): string[] {
|
|
205
309
|
const bg = opts.bg ?? ((s: string) => s);
|
|
206
|
-
const inner = Math.max(1, width - 4);
|
|
310
|
+
const inner = Math.max(1, width - 4);
|
|
207
311
|
const dashes = "─".repeat(width - 2);
|
|
208
|
-
|
|
209
|
-
// Re-assert the bg OPEN sequence after any full reset (\x1b[0m) or bg reset
|
|
210
|
-
// (\x1b[49m) embedded in themed content, so the fill has no transparent holes.
|
|
211
312
|
const SENTINEL = "\x00";
|
|
212
313
|
const bgOpen = bg(SENTINEL).split(SENTINEL)[0] ?? "";
|
|
213
314
|
const reassert = (s: string): string =>
|
|
@@ -216,15 +317,10 @@ function frameLines(opts: FrameOptions): string[] {
|
|
|
216
317
|
p === "0" || p.split(";").includes("49") ? `${seq}${bgOpen}` : seq,
|
|
217
318
|
)
|
|
218
319
|
: s;
|
|
219
|
-
|
|
220
320
|
const row = (content: string): string => {
|
|
221
|
-
const
|
|
222
|
-
const padded =
|
|
223
|
-
return bg(`${color("│")} ${reassert(padded)} ${color("│")}`);
|
|
321
|
+
const fitted = truncateToWidth(content, inner, "…");
|
|
322
|
+
const padded = fitted + " ".repeat(Math.max(0, inner - visibleWidth(fitted)));
|
|
323
|
+
return bg(`${opts.color("│")} ${reassert(padded)} ${opts.color("│")}`);
|
|
224
324
|
};
|
|
225
|
-
|
|
226
|
-
const out: string[] = [bg(color(`╭${dashes}╮`))];
|
|
227
|
-
for (const line of lines) out.push(row(line));
|
|
228
|
-
out.push(bg(color(`╰${dashes}╯`)));
|
|
229
|
-
return out;
|
|
325
|
+
return [bg(opts.color(`╭${dashes}╮`)), ...lines.map(row), bg(opts.color(`╰${dashes}╯`))];
|
|
230
326
|
}
|