@pi-unipi/notify 0.1.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/README.md +103 -0
- package/commands.ts +210 -0
- package/events.ts +192 -0
- package/index.ts +57 -0
- package/package.json +53 -0
- package/platforms/gotify.ts +36 -0
- package/platforms/native.ts +47 -0
- package/platforms/node-notifier.d.ts +20 -0
- package/platforms/telegram.ts +77 -0
- package/settings.ts +115 -0
- package/skills/configure-notify/SKILL.md +134 -0
- package/skills/notify/SKILL.md +56 -0
- package/tools.ts +96 -0
- package/tui/gotify-setup.ts +528 -0
- package/tui/settings-overlay.ts +251 -0
- package/tui/telegram-setup.ts +301 -0
- package/types.ts +88 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/notify — Gotify Setup TUI Component
|
|
3
|
+
*
|
|
4
|
+
* Interactive overlay for setting up Gotify push notifications.
|
|
5
|
+
* Guides user through server URL, app token, and priority configuration.
|
|
6
|
+
* Tests connection before saving.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { sendGotifyNotification } from "../platforms/gotify.js";
|
|
13
|
+
import { updateConfig, loadConfig } from "../settings.js";
|
|
14
|
+
|
|
15
|
+
type SetupPhase =
|
|
16
|
+
| "instructions"
|
|
17
|
+
| "server-url"
|
|
18
|
+
| "app-token"
|
|
19
|
+
| "priority"
|
|
20
|
+
| "testing"
|
|
21
|
+
| "success"
|
|
22
|
+
| "error"
|
|
23
|
+
| "test-failed";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gotify setup overlay component.
|
|
27
|
+
*/
|
|
28
|
+
export class GotifySetupOverlay implements Component {
|
|
29
|
+
private phase: SetupPhase = "instructions";
|
|
30
|
+
private serverUrl = "";
|
|
31
|
+
private appToken = "";
|
|
32
|
+
private priority = "5";
|
|
33
|
+
private error: string | null = null;
|
|
34
|
+
private testError: string | null = null;
|
|
35
|
+
private isInPaste = false;
|
|
36
|
+
private pasteBuffer = "";
|
|
37
|
+
private pasteTarget: "server-url" | "app-token" = "server-url";
|
|
38
|
+
onClose?: () => void;
|
|
39
|
+
requestRender?: () => void;
|
|
40
|
+
private theme: Theme | null = null;
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
// Pre-fill from existing config if available
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
if (config.gotify.serverUrl) this.serverUrl = config.gotify.serverUrl;
|
|
46
|
+
if (config.gotify.appToken) this.appToken = config.gotify.appToken;
|
|
47
|
+
if (config.gotify.priority) this.priority = String(config.gotify.priority);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setTheme(theme: Theme): void {
|
|
51
|
+
this.theme = theme;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
invalidate(): void {}
|
|
55
|
+
|
|
56
|
+
handleInput(data: string): void {
|
|
57
|
+
switch (this.phase) {
|
|
58
|
+
case "instructions":
|
|
59
|
+
if (data === "\r" || data === " ") {
|
|
60
|
+
this.phase = this.serverUrl ? "app-token" : "server-url";
|
|
61
|
+
} else if (data === "\x1b") {
|
|
62
|
+
this.onClose?.();
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case "server-url":
|
|
67
|
+
this.handleTextInput(data, "server-url", () => {
|
|
68
|
+
this.phase = "app-token";
|
|
69
|
+
});
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case "app-token":
|
|
73
|
+
this.handleTextInput(data, "app-token", () => {
|
|
74
|
+
this.phase = "priority";
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
|
|
78
|
+
case "priority":
|
|
79
|
+
if (data === "\r" && this.isValidPriority()) {
|
|
80
|
+
this.testConnection();
|
|
81
|
+
} else if (data === "\x1b") {
|
|
82
|
+
this.onClose?.();
|
|
83
|
+
} else if (data === "\x7f" || data === "\b") {
|
|
84
|
+
this.priority = this.priority.slice(0, -1);
|
|
85
|
+
} else {
|
|
86
|
+
const ch = data.replace(/[^\d]/g, "");
|
|
87
|
+
if (ch && this.priority.length < 2) {
|
|
88
|
+
this.priority += ch;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case "testing":
|
|
94
|
+
if (data === "\x1b") {
|
|
95
|
+
this.onClose?.();
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case "success":
|
|
100
|
+
case "error":
|
|
101
|
+
case "test-failed":
|
|
102
|
+
if (data === "\r" || data === " " || data === "\x1b") {
|
|
103
|
+
this.onClose?.();
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private handleTextInput(
|
|
110
|
+
data: string,
|
|
111
|
+
target: "server-url" | "app-token",
|
|
112
|
+
onEnter: () => void
|
|
113
|
+
): void {
|
|
114
|
+
// Handle bracketed paste mode
|
|
115
|
+
if (this.isInPaste) {
|
|
116
|
+
this.pasteBuffer += data;
|
|
117
|
+
const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
|
|
118
|
+
if (endIndex !== -1) {
|
|
119
|
+
const pasteContent = this.pasteBuffer.substring(0, endIndex).trim();
|
|
120
|
+
if (target === "server-url") {
|
|
121
|
+
this.serverUrl = pasteContent;
|
|
122
|
+
} else {
|
|
123
|
+
this.appToken = pasteContent;
|
|
124
|
+
}
|
|
125
|
+
this.isInPaste = false;
|
|
126
|
+
this.pasteBuffer = "";
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Detect start of bracketed paste
|
|
131
|
+
if (data.includes("\x1b[200~")) {
|
|
132
|
+
this.isInPaste = true;
|
|
133
|
+
this.pasteTarget = target;
|
|
134
|
+
this.pasteBuffer = data.replace("\x1b[200~", "");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (data === "\r") {
|
|
138
|
+
onEnter();
|
|
139
|
+
} else if (data === "\x1b") {
|
|
140
|
+
this.onClose?.();
|
|
141
|
+
} else if (data === "\x7f" || data === "\b") {
|
|
142
|
+
if (target === "server-url") {
|
|
143
|
+
this.serverUrl = this.serverUrl.slice(0, -1);
|
|
144
|
+
} else {
|
|
145
|
+
this.appToken = this.appToken.slice(0, -1);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// Ignore escape sequences
|
|
149
|
+
if (data.startsWith("\x1b[")) return;
|
|
150
|
+
if (target === "server-url") {
|
|
151
|
+
this.serverUrl += data;
|
|
152
|
+
} else {
|
|
153
|
+
this.appToken += data;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private isValidPriority(): boolean {
|
|
159
|
+
const num = parseInt(this.priority, 10);
|
|
160
|
+
return !isNaN(num) && num >= 1 && num <= 10;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async testConnection(): Promise<void> {
|
|
164
|
+
this.phase = "testing";
|
|
165
|
+
this.requestRender?.();
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await sendGotifyNotification(
|
|
169
|
+
this.serverUrl.replace(/\/$/, ""),
|
|
170
|
+
this.appToken,
|
|
171
|
+
"Pi — Setup Test",
|
|
172
|
+
`Gotify configured successfully at ${new Date().toLocaleTimeString()}`,
|
|
173
|
+
parseInt(this.priority, 10) || 5
|
|
174
|
+
);
|
|
175
|
+
this.saveConfig();
|
|
176
|
+
this.phase = "success";
|
|
177
|
+
this.requestRender?.();
|
|
178
|
+
setTimeout(() => this.onClose?.(), 1500);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
this.testError = err instanceof Error ? err.message : String(err);
|
|
181
|
+
this.phase = "test-failed";
|
|
182
|
+
this.requestRender?.();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private saveConfig(): void {
|
|
187
|
+
updateConfig({
|
|
188
|
+
gotify: {
|
|
189
|
+
enabled: true,
|
|
190
|
+
serverUrl: this.serverUrl.replace(/\/$/, ""),
|
|
191
|
+
appToken: this.appToken,
|
|
192
|
+
priority: parseInt(this.priority, 10) || 5,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
private fg(color: string, text: string): string {
|
|
200
|
+
if (this.theme) return this.theme.fg(color as any, text);
|
|
201
|
+
const c: Record<string, string> = {
|
|
202
|
+
accent: "\x1b[36m",
|
|
203
|
+
success: "\x1b[32m",
|
|
204
|
+
warning: "\x1b[33m",
|
|
205
|
+
error: "\x1b[31m",
|
|
206
|
+
dim: "\x1b[2m",
|
|
207
|
+
borderMuted: "\x1b[90m",
|
|
208
|
+
};
|
|
209
|
+
return `${c[color] ?? ""}${text}\x1b[0m`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private bold(text: string): string {
|
|
213
|
+
return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private frameLine(content: string, innerWidth: number): string {
|
|
217
|
+
const truncated = truncateToWidth(content, innerWidth, "");
|
|
218
|
+
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
219
|
+
return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private ruleLine(innerWidth: number): string {
|
|
223
|
+
return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
227
|
+
const left = edge === "top" ? "┌" : "└";
|
|
228
|
+
const right = edge === "top" ? "┐" : "┘";
|
|
229
|
+
return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private maskToken(token: string): string {
|
|
233
|
+
if (token.length <= 8) return token;
|
|
234
|
+
return token.slice(0, 4) + "•".repeat(token.length - 8) + token.slice(-4);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
render(width: number): string[] {
|
|
238
|
+
const innerWidth = Math.max(22, width - 2);
|
|
239
|
+
const lines: string[] = [];
|
|
240
|
+
|
|
241
|
+
lines.push(this.borderLine(innerWidth, "top"));
|
|
242
|
+
lines.push(
|
|
243
|
+
this.frameLine(
|
|
244
|
+
this.fg("accent", this.bold("📡 Gotify Setup")),
|
|
245
|
+
innerWidth
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
lines.push(this.ruleLine(innerWidth));
|
|
249
|
+
|
|
250
|
+
switch (this.phase) {
|
|
251
|
+
case "instructions":
|
|
252
|
+
lines.push(
|
|
253
|
+
this.frameLine(
|
|
254
|
+
this.fg("dim", "Set up Gotify push notifications:"),
|
|
255
|
+
innerWidth
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
lines.push(this.frameLine("", innerWidth));
|
|
259
|
+
lines.push(
|
|
260
|
+
this.frameLine(
|
|
261
|
+
` ${this.bold("1.")} Run a Gotify server (or use an existing one)`,
|
|
262
|
+
innerWidth
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
lines.push(
|
|
266
|
+
this.frameLine(
|
|
267
|
+
` ${this.fg("dim", "See: https://gotify.net/docs/install")}`,
|
|
268
|
+
innerWidth
|
|
269
|
+
)
|
|
270
|
+
);
|
|
271
|
+
lines.push(this.frameLine("", innerWidth));
|
|
272
|
+
lines.push(
|
|
273
|
+
this.frameLine(
|
|
274
|
+
` ${this.bold("2.")} Open your Gotify web UI`,
|
|
275
|
+
innerWidth
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
lines.push(
|
|
279
|
+
this.frameLine(
|
|
280
|
+
` Go to ${this.fg("accent", "Apps")} → Create Application`,
|
|
281
|
+
innerWidth
|
|
282
|
+
)
|
|
283
|
+
);
|
|
284
|
+
lines.push(
|
|
285
|
+
this.frameLine(
|
|
286
|
+
` Copy the ${this.fg("accent", "app token")}`,
|
|
287
|
+
innerWidth
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
lines.push(this.frameLine("", innerWidth));
|
|
291
|
+
lines.push(
|
|
292
|
+
this.frameLine(
|
|
293
|
+
` ${this.bold("3.")} Enter your server URL and app token below`,
|
|
294
|
+
innerWidth
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
if (this.serverUrl) {
|
|
298
|
+
lines.push(
|
|
299
|
+
this.frameLine(
|
|
300
|
+
` ${this.fg("success", "✓")} Server URL pre-filled from existing config`,
|
|
301
|
+
innerWidth
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (this.appToken) {
|
|
306
|
+
lines.push(
|
|
307
|
+
this.frameLine(
|
|
308
|
+
` ${this.fg("success", "✓")} App token pre-filled from existing config`,
|
|
309
|
+
innerWidth
|
|
310
|
+
)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
lines.push(this.ruleLine(innerWidth));
|
|
314
|
+
lines.push(
|
|
315
|
+
this.frameLine(
|
|
316
|
+
this.fg("dim", "Press Enter to continue, Esc to cancel"),
|
|
317
|
+
innerWidth
|
|
318
|
+
)
|
|
319
|
+
);
|
|
320
|
+
break;
|
|
321
|
+
|
|
322
|
+
case "server-url":
|
|
323
|
+
lines.push(
|
|
324
|
+
this.frameLine(
|
|
325
|
+
this.fg("dim", "Enter your Gotify server URL:"),
|
|
326
|
+
innerWidth
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
lines.push(this.frameLine("", innerWidth));
|
|
330
|
+
lines.push(
|
|
331
|
+
this.frameLine(
|
|
332
|
+
` ${this.fg("accent", this.bold(this.serverUrl || " "))}${this.fg("dim", "█")}`,
|
|
333
|
+
innerWidth
|
|
334
|
+
)
|
|
335
|
+
);
|
|
336
|
+
lines.push(this.frameLine("", innerWidth));
|
|
337
|
+
lines.push(
|
|
338
|
+
this.frameLine(
|
|
339
|
+
this.fg("dim", "Example: https://gotify.example.com"),
|
|
340
|
+
innerWidth
|
|
341
|
+
)
|
|
342
|
+
);
|
|
343
|
+
lines.push(this.ruleLine(innerWidth));
|
|
344
|
+
lines.push(
|
|
345
|
+
this.frameLine(
|
|
346
|
+
this.fg("dim", "Enter to continue · Esc to cancel"),
|
|
347
|
+
innerWidth
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
break;
|
|
351
|
+
|
|
352
|
+
case "app-token":
|
|
353
|
+
lines.push(
|
|
354
|
+
this.frameLine(
|
|
355
|
+
this.fg("dim", "Enter your app token:"),
|
|
356
|
+
innerWidth
|
|
357
|
+
)
|
|
358
|
+
);
|
|
359
|
+
lines.push(this.frameLine("", innerWidth));
|
|
360
|
+
const displayToken = this.appToken
|
|
361
|
+
? this.fg("accent", this.bold(this.maskToken(this.appToken)))
|
|
362
|
+
: " ";
|
|
363
|
+
lines.push(
|
|
364
|
+
this.frameLine(
|
|
365
|
+
` ${displayToken}${this.fg("dim", "█")}`,
|
|
366
|
+
innerWidth
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
lines.push(this.frameLine("", innerWidth));
|
|
370
|
+
lines.push(
|
|
371
|
+
this.frameLine(
|
|
372
|
+
this.fg("dim", "Found in Gotify → Apps → your app"),
|
|
373
|
+
innerWidth
|
|
374
|
+
)
|
|
375
|
+
);
|
|
376
|
+
lines.push(this.ruleLine(innerWidth));
|
|
377
|
+
lines.push(
|
|
378
|
+
this.frameLine(
|
|
379
|
+
this.fg("dim", "Enter to continue · Esc to cancel"),
|
|
380
|
+
innerWidth
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
break;
|
|
384
|
+
|
|
385
|
+
case "priority":
|
|
386
|
+
lines.push(
|
|
387
|
+
this.frameLine(
|
|
388
|
+
this.fg("dim", "Set notification priority (1-10):"),
|
|
389
|
+
innerWidth
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
lines.push(this.frameLine("", innerWidth));
|
|
393
|
+
lines.push(
|
|
394
|
+
this.frameLine(
|
|
395
|
+
` ${this.fg("accent", this.bold(this.priority || " "))}${this.fg("dim", "█")}`,
|
|
396
|
+
innerWidth
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
lines.push(this.frameLine("", innerWidth));
|
|
400
|
+
lines.push(
|
|
401
|
+
this.frameLine(
|
|
402
|
+
` ${this.fg("dim", "1")} = low · ${this.fg("dim", "5")} = normal · ${this.fg("dim", "10")} = high`,
|
|
403
|
+
innerWidth
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
lines.push(this.ruleLine(innerWidth));
|
|
407
|
+
lines.push(
|
|
408
|
+
this.frameLine(
|
|
409
|
+
this.fg("dim", "Enter to test connection · Esc to cancel"),
|
|
410
|
+
innerWidth
|
|
411
|
+
)
|
|
412
|
+
);
|
|
413
|
+
break;
|
|
414
|
+
|
|
415
|
+
case "testing":
|
|
416
|
+
lines.push(this.frameLine("", innerWidth));
|
|
417
|
+
lines.push(
|
|
418
|
+
this.frameLine(
|
|
419
|
+
` ${this.fg("accent", "⠋")} ${this.bold("Testing connection...")}`,
|
|
420
|
+
innerWidth
|
|
421
|
+
)
|
|
422
|
+
);
|
|
423
|
+
lines.push(this.frameLine("", innerWidth));
|
|
424
|
+
lines.push(
|
|
425
|
+
this.frameLine(
|
|
426
|
+
` ${this.fg("dim", `Sending test to ${this.serverUrl}`)}`,
|
|
427
|
+
innerWidth
|
|
428
|
+
)
|
|
429
|
+
);
|
|
430
|
+
lines.push(this.ruleLine(innerWidth));
|
|
431
|
+
lines.push(
|
|
432
|
+
this.frameLine(
|
|
433
|
+
this.fg("dim", "Esc to cancel"),
|
|
434
|
+
innerWidth
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
break;
|
|
438
|
+
|
|
439
|
+
case "success":
|
|
440
|
+
lines.push(this.frameLine("", innerWidth));
|
|
441
|
+
lines.push(
|
|
442
|
+
this.frameLine(
|
|
443
|
+
` ${this.fg("success", "✓ Gotify configured successfully!")}`,
|
|
444
|
+
innerWidth
|
|
445
|
+
)
|
|
446
|
+
);
|
|
447
|
+
lines.push(this.frameLine("", innerWidth));
|
|
448
|
+
lines.push(
|
|
449
|
+
this.frameLine(
|
|
450
|
+
` ${this.fg("dim", `Server: ${this.serverUrl}`)}`,
|
|
451
|
+
innerWidth
|
|
452
|
+
)
|
|
453
|
+
);
|
|
454
|
+
lines.push(
|
|
455
|
+
this.frameLine(
|
|
456
|
+
` ${this.fg("dim", `Priority: ${this.priority}`)}`,
|
|
457
|
+
innerWidth
|
|
458
|
+
)
|
|
459
|
+
);
|
|
460
|
+
lines.push(this.ruleLine(innerWidth));
|
|
461
|
+
lines.push(
|
|
462
|
+
this.frameLine(
|
|
463
|
+
this.fg("dim", "Closing..."),
|
|
464
|
+
innerWidth
|
|
465
|
+
)
|
|
466
|
+
);
|
|
467
|
+
break;
|
|
468
|
+
|
|
469
|
+
case "test-failed":
|
|
470
|
+
lines.push(this.frameLine("", innerWidth));
|
|
471
|
+
lines.push(
|
|
472
|
+
this.frameLine(
|
|
473
|
+
` ${this.fg("error", "✗ Connection test failed")}`,
|
|
474
|
+
innerWidth
|
|
475
|
+
)
|
|
476
|
+
);
|
|
477
|
+
lines.push(this.frameLine("", innerWidth));
|
|
478
|
+
lines.push(
|
|
479
|
+
this.frameLine(
|
|
480
|
+
` ${this.fg("dim", this.testError || "Unknown error")}`,
|
|
481
|
+
innerWidth
|
|
482
|
+
)
|
|
483
|
+
);
|
|
484
|
+
lines.push(this.frameLine("", innerWidth));
|
|
485
|
+
lines.push(
|
|
486
|
+
this.frameLine(
|
|
487
|
+
` ${this.fg("dim", "Check your server URL and app token")}`,
|
|
488
|
+
innerWidth
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
lines.push(this.ruleLine(innerWidth));
|
|
492
|
+
lines.push(
|
|
493
|
+
this.frameLine(
|
|
494
|
+
this.fg("dim", "Press Enter to close"),
|
|
495
|
+
innerWidth
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
case "error":
|
|
501
|
+
lines.push(this.frameLine("", innerWidth));
|
|
502
|
+
lines.push(
|
|
503
|
+
this.frameLine(
|
|
504
|
+
` ${this.fg("error", "✗ Setup failed")}`,
|
|
505
|
+
innerWidth
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
lines.push(this.frameLine("", innerWidth));
|
|
509
|
+
lines.push(
|
|
510
|
+
this.frameLine(
|
|
511
|
+
` ${this.fg("dim", this.error || "Unknown error")}`,
|
|
512
|
+
innerWidth
|
|
513
|
+
)
|
|
514
|
+
);
|
|
515
|
+
lines.push(this.ruleLine(innerWidth));
|
|
516
|
+
lines.push(
|
|
517
|
+
this.frameLine(
|
|
518
|
+
this.fg("dim", "Press Enter to close"),
|
|
519
|
+
innerWidth
|
|
520
|
+
)
|
|
521
|
+
);
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
526
|
+
return lines;
|
|
527
|
+
}
|
|
528
|
+
}
|