@xynogen/pix-pretty 1.7.27 → 1.8.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.
- package/package.json +1 -1
- package/src/confirm.ts +39 -31
- package/src/diff-render.ts +21 -12
- package/src/diff.test.ts +38 -12
- package/src/gate-overlay.ts +71 -39
- package/src/modal-frame.test.ts +441 -0
- package/src/modal-frame.ts +436 -13
- package/src/progress.ts +10 -10
package/package.json
CHANGED
package/src/confirm.ts
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* as gate-overlay and pix-ask. Returns true on confirm, false on deny/timeout.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { type SelectItem, SelectList } from "@earendil-works/pi-tui";
|
|
9
|
-
import {
|
|
8
|
+
import { type KeybindingsManager, type SelectItem, SelectList } from "@earendil-works/pi-tui";
|
|
9
|
+
import {
|
|
10
|
+
frameModal,
|
|
11
|
+
MIN_PERMISSION_MODAL_HEIGHT,
|
|
12
|
+
ModalPager,
|
|
13
|
+
modalWidth,
|
|
14
|
+
selectListTheme,
|
|
15
|
+
terminalModalHeight,
|
|
16
|
+
} from "./modal-frame.js";
|
|
10
17
|
|
|
11
18
|
// Minimal structural type for the `ctx.ui.custom` host call.
|
|
12
19
|
interface CustomTheme {
|
|
@@ -25,12 +32,12 @@ interface CustomComponent {
|
|
|
25
32
|
export interface ConfirmUI {
|
|
26
33
|
custom<T>(
|
|
27
34
|
cb: (
|
|
28
|
-
tui: { requestRender(): void },
|
|
35
|
+
tui: { requestRender(): void; terminal?: { rows?: number } },
|
|
29
36
|
theme: CustomTheme,
|
|
30
|
-
kb:
|
|
37
|
+
kb: KeybindingsManager,
|
|
31
38
|
done: (v: T) => void,
|
|
32
39
|
) => CustomComponent,
|
|
33
|
-
opts?: { overlay?: boolean },
|
|
40
|
+
opts?: { overlay?: boolean; overlayOptions?: { maxHeight?: number | `${number}%` } },
|
|
34
41
|
): Promise<T | undefined>;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -64,9 +71,10 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
|
|
|
64
71
|
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
|
|
65
72
|
|
|
66
73
|
ui.custom<boolean>(
|
|
67
|
-
(tui, theme,
|
|
74
|
+
(tui, theme, kb, done) => {
|
|
68
75
|
let ticker: ReturnType<typeof setInterval> | undefined;
|
|
69
76
|
let countdownLine: string | undefined;
|
|
77
|
+
const pager = new ModalPager();
|
|
70
78
|
|
|
71
79
|
const choices: SelectItem[] = [
|
|
72
80
|
{
|
|
@@ -112,43 +120,43 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
|
|
|
112
120
|
render: (w: number) => {
|
|
113
121
|
const mw = modalWidth(w);
|
|
114
122
|
const inner = mw - 4;
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Divider
|
|
126
|
-
lines.push(theme.fg("dim", "─".repeat(inner)));
|
|
127
|
-
|
|
128
|
-
// Countdown
|
|
129
|
-
if (countdownLine !== undefined) lines.push(countdownLine);
|
|
130
|
-
|
|
131
|
-
// Select list
|
|
132
|
-
for (const l of selectList.render(inner)) lines.push(l);
|
|
133
|
-
|
|
134
|
-
lines.push("");
|
|
135
|
-
lines.push(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"));
|
|
136
|
-
|
|
137
|
-
return frameLines({
|
|
123
|
+
const footer = [theme.fg("dim", "─".repeat(inner))];
|
|
124
|
+
if (countdownLine !== undefined) footer.push(countdownLine);
|
|
125
|
+
footer.push(...selectList.render(inner));
|
|
126
|
+
footer.push("");
|
|
127
|
+
footer.push(
|
|
128
|
+
theme.fg("dim", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc cancel"),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const result = frameModal({
|
|
138
132
|
width: mw,
|
|
139
|
-
|
|
133
|
+
maxHeight: terminalModalHeight(tui.terminal?.rows),
|
|
134
|
+
minHeight: MIN_PERMISSION_MODAL_HEIGHT,
|
|
135
|
+
header: [theme.fg(accent, theme.bold(opts.title))],
|
|
136
|
+
body: (opts.body ?? []).map((line) => theme.fg("text", line)),
|
|
137
|
+
footer,
|
|
138
|
+
bodyOffset: pager.bodyOffset,
|
|
140
139
|
color: (s) => theme.fg(accent, s),
|
|
141
140
|
bg: (s) => theme.bg("customMessageBg", s),
|
|
141
|
+
fg: (s) => theme.fg("text", s),
|
|
142
|
+
overflowLine: ({ page, totalPages }) =>
|
|
143
|
+
theme.fg("dim", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
|
|
142
144
|
});
|
|
145
|
+
pager.sync(result);
|
|
146
|
+
return result.lines;
|
|
143
147
|
},
|
|
144
148
|
invalidate: () => {},
|
|
145
149
|
handleInput: (data: string) => {
|
|
150
|
+
if (pager.handleInput(data, kb, true)) {
|
|
151
|
+
tui.requestRender();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
146
154
|
selectList.handleInput(data);
|
|
147
155
|
tui.requestRender();
|
|
148
156
|
},
|
|
149
157
|
};
|
|
150
158
|
},
|
|
151
|
-
{ overlay: true },
|
|
159
|
+
{ overlay: true, overlayOptions: { maxHeight: "80%" } },
|
|
152
160
|
).then((result) => {
|
|
153
161
|
if (timer !== undefined) clearTimeout(timer);
|
|
154
162
|
resolve(result ?? false);
|
package/src/diff-render.ts
CHANGED
|
@@ -80,16 +80,25 @@ export interface DiffColors {
|
|
|
80
80
|
bgGutterDel: string;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Faint row tints for changed lines. Kept dark enough to sit under syntax
|
|
84
|
+
// highlighting without washing it out, but visible enough that a scanned diff
|
|
85
|
+
// reads as add/remove bands rather than gutter chips alone.
|
|
86
|
+
const BG_ADD_FAINT = "\x1b[48;2;18;42;28m";
|
|
87
|
+
const BG_DEL_FAINT = "\x1b[48;2;48;22;26m";
|
|
88
|
+
/** Intra-line word-diff emphasis — one step brighter than the row tint. */
|
|
89
|
+
const BG_ADD_STRONG = "\x1b[48;2;28;72;44m";
|
|
90
|
+
const BG_DEL_STRONG = "\x1b[48;2;82;32;38m";
|
|
91
|
+
|
|
83
92
|
export const DEFAULT_DIFF_COLORS: DiffColors = {
|
|
84
93
|
fgAdd: FG_ADD,
|
|
85
94
|
fgDel: FG_DEL,
|
|
86
95
|
fgCtx: FG_DIM,
|
|
87
|
-
bgAdd:
|
|
88
|
-
bgDel:
|
|
89
|
-
bgAddHighlight:
|
|
90
|
-
bgDelHighlight:
|
|
91
|
-
bgGutterAdd:
|
|
92
|
-
bgGutterDel:
|
|
96
|
+
bgAdd: BG_ADD_FAINT,
|
|
97
|
+
bgDel: BG_DEL_FAINT,
|
|
98
|
+
bgAddHighlight: BG_ADD_STRONG,
|
|
99
|
+
bgDelHighlight: BG_DEL_STRONG,
|
|
100
|
+
bgGutterAdd: BG_ADD_FAINT,
|
|
101
|
+
bgGutterDel: BG_DEL_FAINT,
|
|
93
102
|
};
|
|
94
103
|
|
|
95
104
|
type DiffTheme = {
|
|
@@ -112,12 +121,12 @@ export function resolveDiffColors(theme?: DiffTheme): DiffColors {
|
|
|
112
121
|
fgDel: themeFg(theme, "toolDiffRemoved", FG_DEL),
|
|
113
122
|
fgCtx: themeFg(theme, "toolDiffContext", FG_DIM),
|
|
114
123
|
theme: typeof theme.fg === "function" ? (theme as FgTheme) : undefined,
|
|
115
|
-
bgAdd:
|
|
116
|
-
bgDel:
|
|
117
|
-
bgAddHighlight:
|
|
118
|
-
bgDelHighlight:
|
|
119
|
-
bgGutterAdd:
|
|
120
|
-
bgGutterDel:
|
|
124
|
+
bgAdd: BG_ADD_FAINT,
|
|
125
|
+
bgDel: BG_DEL_FAINT,
|
|
126
|
+
bgAddHighlight: BG_ADD_STRONG,
|
|
127
|
+
bgDelHighlight: BG_DEL_STRONG,
|
|
128
|
+
bgGutterAdd: BG_ADD_FAINT,
|
|
129
|
+
bgGutterDel: BG_DEL_FAINT,
|
|
121
130
|
};
|
|
122
131
|
}
|
|
123
132
|
|
package/src/diff.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { parseDiff } from "./diff.js";
|
|
3
3
|
import {
|
|
4
|
+
DEFAULT_DIFF_COLORS,
|
|
4
5
|
diffThemeCacheKey,
|
|
5
6
|
renderDiffSummary,
|
|
6
7
|
renderUnified,
|
|
@@ -22,20 +23,34 @@ describe("theme-derived diff rendering", () => {
|
|
|
22
23
|
},
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
it("uses semantic foregrounds
|
|
26
|
+
it("uses semantic foregrounds", () => {
|
|
26
27
|
const colors = resolveDiffColors(theme);
|
|
27
28
|
expect(colors.fgAdd).toBe("\x1b[38;2;120;210;150m");
|
|
28
29
|
expect(colors.fgDel).toBe("\x1b[38;2;230;120;130m");
|
|
29
30
|
expect(colors.fgCtx).toBe("\x1b[38;2;130;140;150m");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Regression: these six slots were all set to BG_BASE (\x1b[49m), which
|
|
34
|
+
// dropped the faint green/red row tint and left only the gutter chips
|
|
35
|
+
// colored. A diff must read as add/remove bands at a glance.
|
|
36
|
+
it("gives changed rows a faint tint background", () => {
|
|
37
|
+
for (const colors of [DEFAULT_DIFF_COLORS, resolveDiffColors(theme)]) {
|
|
38
|
+
for (const key of [
|
|
39
|
+
"bgAdd",
|
|
40
|
+
"bgDel",
|
|
41
|
+
"bgAddHighlight",
|
|
42
|
+
"bgDelHighlight",
|
|
43
|
+
"bgGutterAdd",
|
|
44
|
+
"bgGutterDel",
|
|
45
|
+
] as const) {
|
|
46
|
+
expect(colors[key]).not.toBe("\x1b[49m");
|
|
47
|
+
expect(colors[key]).toMatch(/^\x1b\[48;2;\d+;\d+;\d+m$/);
|
|
48
|
+
}
|
|
49
|
+
// Word-diff emphasis must be distinguishable from the row tint.
|
|
50
|
+
expect(colors.bgAddHighlight).not.toBe(colors.bgAdd);
|
|
51
|
+
expect(colors.bgDelHighlight).not.toBe(colors.bgDel);
|
|
52
|
+
// Add and remove must never collide.
|
|
53
|
+
expect(colors.bgAdd).not.toBe(colors.bgDel);
|
|
39
54
|
}
|
|
40
55
|
});
|
|
41
56
|
|
|
@@ -53,7 +68,19 @@ describe("theme-derived diff rendering", () => {
|
|
|
53
68
|
);
|
|
54
69
|
});
|
|
55
70
|
|
|
56
|
-
it("
|
|
71
|
+
it("emits tint backgrounds on changed rows", async () => {
|
|
72
|
+
const rendered = await renderUnified(
|
|
73
|
+
parseDiff("const oldValue = 1;", "const newValue = 2;"),
|
|
74
|
+
"typescript",
|
|
75
|
+
80,
|
|
76
|
+
resolveDiffColors({ ...theme, fg: (_key, text) => text }),
|
|
77
|
+
);
|
|
78
|
+
const { bgAdd, bgDel } = resolveDiffColors(theme);
|
|
79
|
+
expect(rendered).toContain(bgDel);
|
|
80
|
+
expect(rendered).toContain(bgAdd);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("keeps gutter numbering and rule layout intact", async () => {
|
|
57
84
|
const rendered = await renderUnified(
|
|
58
85
|
parseDiff("const oldValue = 1;", "const newValue = 2;"),
|
|
59
86
|
"typescript",
|
|
@@ -69,7 +96,6 @@ describe("theme-derived diff rendering", () => {
|
|
|
69
96
|
expect(lines[3]).toMatch(/^─+$/);
|
|
70
97
|
expect(rendered).toContain(theme.getFgAnsi("toolDiffRemoved"));
|
|
71
98
|
expect(rendered).toContain(theme.getFgAnsi("toolDiffAdded"));
|
|
72
|
-
expect(rendered).not.toMatch(/\x1b\[48(?:;[^m]*)?m/);
|
|
73
99
|
});
|
|
74
100
|
});
|
|
75
101
|
|
package/src/gate-overlay.ts
CHANGED
|
@@ -14,8 +14,16 @@
|
|
|
14
14
|
* - Single source of truth for the overlay look across pix-gate and pix-sudo.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { Input, type SelectItem, SelectList
|
|
18
|
-
import {
|
|
17
|
+
import { Input, type SelectItem, SelectList } from "@earendil-works/pi-tui";
|
|
18
|
+
import {
|
|
19
|
+
frameModal,
|
|
20
|
+
MIN_PERMISSION_MODAL_HEIGHT,
|
|
21
|
+
type ModalPageKeybindings,
|
|
22
|
+
ModalPager,
|
|
23
|
+
modalWidth,
|
|
24
|
+
selectListTheme,
|
|
25
|
+
terminalModalHeight,
|
|
26
|
+
} from "./modal-frame.js";
|
|
19
27
|
|
|
20
28
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
21
29
|
|
|
@@ -83,6 +91,7 @@ interface OverlayTheme {
|
|
|
83
91
|
|
|
84
92
|
interface OverlayTui {
|
|
85
93
|
requestRender(): void;
|
|
94
|
+
terminal?: { rows?: number };
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
interface OverlayComponent {
|
|
@@ -111,6 +120,13 @@ const DEFAULT_CHOICES: OverlayChoice[] = [
|
|
|
111
120
|
{ value: "no", label: "Deny", description: "Block" },
|
|
112
121
|
];
|
|
113
122
|
|
|
123
|
+
function asPageKeybindings(value: unknown): ModalPageKeybindings | undefined {
|
|
124
|
+
if (!value || typeof value !== "object") return undefined;
|
|
125
|
+
return typeof (value as { matches?: unknown }).matches === "function"
|
|
126
|
+
? (value as ModalPageKeybindings)
|
|
127
|
+
: undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
// ── Masked input (● per char) ─────────────────────────────────────────────────
|
|
115
131
|
|
|
116
132
|
class MaskedInput extends Input {
|
|
@@ -125,8 +141,14 @@ class MaskedInput extends Input {
|
|
|
125
141
|
|
|
126
142
|
// ── Renderer ──────────────────────────────────────────────────────────────────
|
|
127
143
|
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
interface OverlaySections {
|
|
145
|
+
header: string[];
|
|
146
|
+
body: string[];
|
|
147
|
+
footer: string[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Partition inspectable content from pinned approval controls. */
|
|
151
|
+
function buildSections(opts: {
|
|
130
152
|
theme: OverlayTheme;
|
|
131
153
|
accent: string;
|
|
132
154
|
config: OverlayConfig;
|
|
@@ -136,7 +158,7 @@ function buildLines(opts: {
|
|
|
136
158
|
countdownLine: string | undefined;
|
|
137
159
|
passwordStatus: string | undefined;
|
|
138
160
|
width: number;
|
|
139
|
-
}):
|
|
161
|
+
}): OverlaySections {
|
|
140
162
|
const {
|
|
141
163
|
theme,
|
|
142
164
|
accent,
|
|
@@ -149,42 +171,31 @@ function buildLines(opts: {
|
|
|
149
171
|
width,
|
|
150
172
|
} = opts;
|
|
151
173
|
const inner = width - 4; // CHROME = 2 border + 2 padding
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Body — wrap each line so long commands wrap instead of getting cut off.
|
|
160
|
-
for (const line of config.body ?? []) {
|
|
161
|
-
const wrapped = line === "" ? [""] : wrapTextWithAnsi(line, inner);
|
|
162
|
-
for (const w of wrapped) lines.push(theme.fg("text", w));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Divider after title/body
|
|
166
|
-
lines.push(theme.fg("dim", "─".repeat(inner)));
|
|
174
|
+
const header = [theme.fg(accent, theme.bold(config.title))];
|
|
175
|
+
const body = (config.body ?? []).map((line) => {
|
|
176
|
+
if (line.startsWith("Intent:")) return theme.fg("text", line.slice(7).trimStart());
|
|
177
|
+
if (line.startsWith("Command:")) return theme.fg("dim", line.slice(8).trimStart());
|
|
178
|
+
return theme.fg("text", line);
|
|
179
|
+
});
|
|
180
|
+
const footer = [theme.fg("dim", "─".repeat(inner))];
|
|
167
181
|
|
|
168
|
-
|
|
169
|
-
if (countdownLine !== undefined) lines.push(countdownLine);
|
|
182
|
+
if (countdownLine !== undefined) footer.push(countdownLine);
|
|
170
183
|
|
|
171
184
|
// Select or password stage
|
|
172
185
|
if (stage === "select") {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
lines.push(theme.fg("dim", "↑↓ navigate • enter select • esc deny"));
|
|
186
|
+
footer.push(...selectList.render(inner));
|
|
187
|
+
footer.push("");
|
|
188
|
+
footer.push(theme.fg("dim", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc deny"));
|
|
177
189
|
} else {
|
|
178
190
|
const label = config.mode === "sudo" ? (config.passwordLabel ?? "Sudo password:") : "Password:";
|
|
179
|
-
|
|
180
|
-
if (passwordStatus)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
lines.push(theme.fg("dim", "enter confirm • esc cancel"));
|
|
191
|
+
footer.push(theme.fg("muted", label));
|
|
192
|
+
if (passwordStatus) footer.push(theme.fg("error", passwordStatus));
|
|
193
|
+
footer.push(...maskedInput.render(inner));
|
|
194
|
+
footer.push("");
|
|
195
|
+
footer.push(theme.fg("dim", "←→/PgUp/PgDn inspect • enter confirm • esc cancel"));
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
return
|
|
198
|
+
return { header, body, footer };
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
@@ -225,13 +236,15 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
|
|
|
225
236
|
|
|
226
237
|
return new Promise((resolve) => {
|
|
227
238
|
ui.custom<OverlayResult>(
|
|
228
|
-
(tui, theme,
|
|
239
|
+
(tui, theme, kb, done) => {
|
|
240
|
+
const pageKeybindings = asPageKeybindings(kb);
|
|
229
241
|
type Stage = "select" | "password";
|
|
230
242
|
let stage: Stage = "select";
|
|
231
243
|
let countdownLine: string | undefined;
|
|
232
244
|
let passwordStatus: string | undefined;
|
|
233
245
|
let passwordAttempts = 0;
|
|
234
246
|
let validatingPassword = false;
|
|
247
|
+
const pager = new ModalPager();
|
|
235
248
|
|
|
236
249
|
// Dead-man's-switch timer: counts down only while untouched. The
|
|
237
250
|
// first keypress cancels it (user is present → let them decide). If
|
|
@@ -267,14 +280,21 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
|
|
|
267
280
|
|
|
268
281
|
// Arm the dead-man's switch (only when a timeout was requested).
|
|
269
282
|
if (timeoutMs > 0) {
|
|
270
|
-
|
|
283
|
+
const urgencyColor = (s: number): string =>
|
|
284
|
+
s <= 5 ? "error" : s <= 15 ? "warning" : "dim";
|
|
285
|
+
const countdownText = (s: number): string => {
|
|
286
|
+
const color = urgencyColor(s);
|
|
287
|
+
const text = `auto-deny in ${s}s`;
|
|
288
|
+
return s <= 5 ? theme.bold(theme.fg(color, text)) : theme.fg(color, text);
|
|
289
|
+
};
|
|
290
|
+
countdownLine = countdownText(remaining);
|
|
271
291
|
timer = setInterval(() => {
|
|
272
292
|
remaining -= 1;
|
|
273
293
|
if (remaining <= 0) {
|
|
274
294
|
finish({ action: "timeout" });
|
|
275
295
|
return;
|
|
276
296
|
}
|
|
277
|
-
countdownLine =
|
|
297
|
+
countdownLine = countdownText(remaining);
|
|
278
298
|
tui.requestRender();
|
|
279
299
|
}, 1000);
|
|
280
300
|
}
|
|
@@ -327,7 +347,7 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
|
|
|
327
347
|
return {
|
|
328
348
|
render: (w) => {
|
|
329
349
|
const mw = modalWidth(w);
|
|
330
|
-
const
|
|
350
|
+
const sections = buildSections({
|
|
331
351
|
theme,
|
|
332
352
|
accent,
|
|
333
353
|
config,
|
|
@@ -338,17 +358,29 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
|
|
|
338
358
|
passwordStatus,
|
|
339
359
|
width: mw,
|
|
340
360
|
});
|
|
341
|
-
|
|
361
|
+
const result = frameModal({
|
|
342
362
|
width: mw,
|
|
343
|
-
|
|
363
|
+
maxHeight: terminalModalHeight(tui.terminal?.rows),
|
|
364
|
+
minHeight: MIN_PERMISSION_MODAL_HEIGHT,
|
|
365
|
+
...sections,
|
|
366
|
+
bodyOffset: pager.bodyOffset,
|
|
344
367
|
color: (s) => theme.fg(accent, s),
|
|
345
368
|
bg: (s) => theme.bg("customMessageBg", s),
|
|
369
|
+
fg: (s) => theme.fg("text", s),
|
|
370
|
+
overflowLine: ({ page, totalPages }) =>
|
|
371
|
+
theme.fg("dim", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
|
|
346
372
|
});
|
|
373
|
+
pager.sync(result);
|
|
374
|
+
return result.lines;
|
|
347
375
|
},
|
|
348
376
|
invalidate: () => {},
|
|
349
377
|
handleInput: (data) => {
|
|
350
378
|
cancelTimer(); // user is present — stop the auto-deny countdown
|
|
351
379
|
if (validatingPassword) return;
|
|
380
|
+
if (pager.handleInput(data, pageKeybindings, true)) {
|
|
381
|
+
tui.requestRender();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
352
384
|
if (stage === "select") selectList.handleInput(data);
|
|
353
385
|
else maskedInput.handleInput(data);
|
|
354
386
|
tui.requestRender();
|