@danypops/pi-packed 0.16.0 → 0.17.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/README.md +11 -8
- package/extension/src/discover.ts +124 -140
- package/extension/src/index.ts +1 -1
- package/extension/src/menu-theme.ts +11 -1
- package/extension/src/resource-config.ts +170 -138
- package/extension/src/security-tui.ts +98 -18
- package/extension/src/tab-host.ts +30 -0
- package/extension/src/tui.ts +431 -342
- package/package.json +2 -2
package/extension/src/tui.ts
CHANGED
|
@@ -1,58 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tui.ts — /packed's
|
|
3
|
-
* convention (folke/lazy.nvim lua/lazy/view/config.lua): uppercase
|
|
4
|
-
* every row, lowercase acts on the row under the cursor -- U/u
|
|
5
|
-
* X/x was lazy's delete key (clean); here that pairing is
|
|
6
|
-
* only (x remove -- there is no safe "remove every
|
|
7
|
-
* bulk analog, so no X is bound). Adds d disable/
|
|
8
|
-
* extensions, c jump to
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
2
|
+
* tui.ts — /packed's one panel, four tabs. Mnemonics follow lazy.nvim's
|
|
3
|
+
* own convention (folke/lazy.nvim lua/lazy/view/config.lua): uppercase
|
|
4
|
+
* acts on every row, lowercase acts on the row under the cursor -- U/u
|
|
5
|
+
* update, X/x was lazy's delete key (clean); here that pairing is
|
|
6
|
+
* single-target only (x remove -- there is no safe "remove every
|
|
7
|
+
* installed package" bulk analog, so no X is bound). Adds d disable/
|
|
8
|
+
* enable this package's extensions, c jump to Config (scoped to the
|
|
9
|
+
* selected row), f jump to Find, s jump to Settings.
|
|
10
|
+
*
|
|
11
|
+
* All four (Packages/Find/Config/Settings) live inside ONE
|
|
12
|
+
* `ctx.ui.custom` floating overlay for the panel's whole lifetime --
|
|
13
|
+
* Malevich's Envelope for the box (rounded border), TabbedContainer for
|
|
14
|
+
* the persistent tab bar + active-tab content swap, and (on top of that)
|
|
15
|
+
* a Dialog swapped in for any y/n decision, the same content-replacement
|
|
16
|
+
* stacking Envelope's own setContent already provides, generalized one
|
|
17
|
+
* level further here. This replaced each tab opening its own separate
|
|
18
|
+
* screen (Find/Config: a second, visually distinct ctx.ui.custom with no
|
|
19
|
+
* Envelope at all; Settings: Pi's own native ctx.ui.select/confirm) --
|
|
20
|
+
* confirmed live as a real user complaint ("Find, Settings, Config all
|
|
21
|
+
* open a different TUI"). Left/Right cycle tabs; Escape returns to
|
|
22
|
+
* Packages from anywhere else, or closes the whole panel from Packages
|
|
23
|
+
* itself -- unless the active tab's own capturesEscape()/
|
|
24
|
+
* capturesHorizontalArrows() says it wants that key for itself right now
|
|
25
|
+
* (an active text filter, an in-flight mutation, or Find's always-on
|
|
26
|
+
* query box), in which case it's delegated straight through instead.
|
|
27
|
+
*
|
|
28
|
+
* Packages itself: an indeterminate spinner (Malevich's Spinner) renders
|
|
29
|
+
* inline next to the row currently updating, settling into a real ✓/✗
|
|
30
|
+
* plus a bounded tail of that row's own actual captured stdout/stderr
|
|
31
|
+
* once it finishes, never a determinate bar (a single subprocess call has
|
|
32
|
+
* no knowable percentage). Rows render through Malevich's Table (real
|
|
33
|
+
* column-aligned Package/Version/status cells, per-row selection styling
|
|
34
|
+
* baked into each cell since Table's own cellStyle is column-wide, not
|
|
35
|
+
* row-wide) inside this tab's own scroll-window slice -- Table
|
|
36
|
+
* deliberately owns no pagination of its own, so the visible-window-
|
|
37
|
+
* around-selectedIndex math stays here. u/x/d and the Enter action menu
|
|
38
|
+
* all run their whole approve+mutate+confirmReload flow inline, via the
|
|
39
|
+
* shared TabHost's inlineCtx -- every confirm() along the way renders as
|
|
40
|
+
* a real Malevich Dialog on this SAME overlay, dispatched by literal y/n
|
|
41
|
+
* keys, instead of ctx.ui.confirm's own separate native dialog. Every
|
|
42
|
+
* non-"changed" settle point (already up to date, pinned, deferred, a
|
|
43
|
+
* genuine failure) shows its own real reason on that row too via the same
|
|
44
|
+
* settled map U's batch path uses, not only as a scrollback toast. All
|
|
45
|
+
* data flows through the packed CLI (thin seam).
|
|
37
46
|
*/
|
|
38
47
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
39
48
|
import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
40
49
|
import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
41
|
-
import { Dialog, Envelope, Menu, Spinner, Table, type
|
|
50
|
+
import { Dialog, Envelope, Menu, Spinner, Table, TabbedContainer, type Component, type MenuItem, type TextMeasure } from "malevich-tui-components";
|
|
42
51
|
import { filterRows, mergeRows, nextMode, visibleRows } from "./model.js";
|
|
43
52
|
import type { Row, ViewMode } from "./model.js";
|
|
44
53
|
import type { Natives, PackageResources } from "./packed.js";
|
|
45
54
|
import { approvePackageOperation } from "./tools.js";
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import { dialogTheme, menuTheme } from "./menu-theme.js";
|
|
55
|
+
import { createSettingsTab, SettingsTab } from "./security-tui.js";
|
|
56
|
+
import { createConfigTab, ConfigTab, applyResourceToggle } from "./resource-config.js";
|
|
57
|
+
import { createFindTab, FindTab } from "./discover.js";
|
|
58
|
+
import { dialogTheme, menuTheme, tabBarTheme } from "./menu-theme.js";
|
|
50
59
|
import { confirmReload } from "./reload.js";
|
|
60
|
+
import type { TabHost } from "./tab-host.js";
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
type: "config" | "find" | "refresh" | "settings";
|
|
54
|
-
row?: Row;
|
|
55
|
-
}
|
|
62
|
+
export type PackedTabKey = "packages" | "find" | "config" | "settings";
|
|
56
63
|
|
|
57
64
|
/** Outcome of a confirmed row action, resolved after any real mutation and
|
|
58
65
|
* reload decision -- "changed" means the daemon state changed and Pi has
|
|
@@ -63,11 +70,19 @@ interface PanelAction {
|
|
|
63
70
|
* "unchanged"/"cancelled" mean the panel keeps running as-is. */
|
|
64
71
|
export type PackageChoiceOutcome = "changed" | "unchanged" | "cancelled" | "deferred";
|
|
65
72
|
|
|
73
|
+
/** A non-"changed" settle point's own detail -- the exact reason nothing
|
|
74
|
+
* (or only a partial thing) happened, so a caller with somewhere to show
|
|
75
|
+
* it inline (a row's own status cell) isn't limited to the scrollback
|
|
76
|
+
* toast every outcome already gets via ctx.ui.notify. "changed" never
|
|
77
|
+
* calls this: ctx.reload() already replaces the session by then. */
|
|
78
|
+
export interface PackageChoiceSettled { ok: boolean; message: string; }
|
|
79
|
+
|
|
66
80
|
export async function applyPackageChoice(
|
|
67
81
|
choice: string | undefined,
|
|
68
82
|
row: Row,
|
|
69
83
|
natives: Natives,
|
|
70
84
|
ctx: ExtensionCommandContext,
|
|
85
|
+
onSettled?: (settled: PackageChoiceSettled) => void,
|
|
71
86
|
): Promise<PackageChoiceOutcome> {
|
|
72
87
|
if (choice?.startsWith("Update")) {
|
|
73
88
|
try {
|
|
@@ -84,18 +99,22 @@ export async function applyPackageChoice(
|
|
|
84
99
|
? `pinned to ${version ?? "an exact version"} -- pi update intentionally leaves pinned packages unchanged`
|
|
85
100
|
: `already up to date${version ? ` at ${version}` : ""}`;
|
|
86
101
|
ctx.ui.notify(`${row.name} is ${reason}.`, "info");
|
|
102
|
+
onSettled?.({ ok: true, message: reason });
|
|
87
103
|
return "unchanged";
|
|
88
104
|
}
|
|
89
105
|
const transition = outcome.previousVersion && outcome.currentVersion ? ` (${outcome.previousVersion} → ${outcome.currentVersion})` : "";
|
|
90
106
|
if (!(await confirmReload(ctx))) {
|
|
91
107
|
ctx.ui.notify(`Updated ${row.name}${transition}; reload pending -- run /reload when ready.`, "warning");
|
|
108
|
+
onSettled?.({ ok: true, message: `updated${transition}; reload pending` });
|
|
92
109
|
return "deferred";
|
|
93
110
|
}
|
|
94
111
|
ctx.ui.notify(`Updated ${row.name}${transition}; reloading Pi resources.`, "info");
|
|
95
112
|
await ctx.reload();
|
|
96
113
|
return "changed";
|
|
97
114
|
} catch (e) {
|
|
98
|
-
|
|
115
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
116
|
+
ctx.ui.notify(`update failed: ${message}`, "error");
|
|
117
|
+
onSettled?.({ ok: false, message });
|
|
99
118
|
return "cancelled";
|
|
100
119
|
}
|
|
101
120
|
}
|
|
@@ -109,13 +128,16 @@ export async function applyPackageChoice(
|
|
|
109
128
|
await natives.remove(row.name, approval.approved);
|
|
110
129
|
if (!(await confirmReload(ctx))) {
|
|
111
130
|
ctx.ui.notify(`Removed ${row.name}; reload pending -- run /reload when ready.`, "warning");
|
|
131
|
+
onSettled?.({ ok: true, message: "removed; reload pending" });
|
|
112
132
|
return "deferred";
|
|
113
133
|
}
|
|
114
134
|
ctx.ui.notify(`Removed ${row.name}; reloading Pi resources.`, "info");
|
|
115
135
|
await ctx.reload();
|
|
116
136
|
return "changed";
|
|
117
137
|
} catch (e) {
|
|
118
|
-
|
|
138
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
139
|
+
ctx.ui.notify(`remove failed: ${message}`, "error");
|
|
140
|
+
onSettled?.({ ok: false, message });
|
|
119
141
|
return "cancelled";
|
|
120
142
|
}
|
|
121
143
|
}
|
|
@@ -349,86 +371,327 @@ async function showActionMenu(ctx: ExtensionCommandContext, row: Row): Promise<"
|
|
|
349
371
|
);
|
|
350
372
|
}
|
|
351
373
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
374
|
+
interface PackagesTabTheme { fg(color: string, s: string): string; bold(s: string): string; }
|
|
375
|
+
|
|
376
|
+
/** Packages -- the panel's default/"home" tab. A real Component (not the
|
|
377
|
+
* panel's own top-level ctx.ui.custom owner anymore); the shared TabHost
|
|
378
|
+
* gives it inline approval/reload dialogs and a way to signal the overlay
|
|
379
|
+
* should close once a mutation actually replaces the session. */
|
|
380
|
+
export class PackagesTab implements Component {
|
|
381
|
+
private rows: Row[];
|
|
382
|
+
private mode: ViewMode = "all";
|
|
383
|
+
private readonly searchInput = new Input();
|
|
384
|
+
private searchActive = false;
|
|
385
|
+
private filtered: Row[];
|
|
386
|
+
private selectedIndex = 0;
|
|
387
|
+
// Set only while U's batch update (or a single row's own u/x/d) is
|
|
388
|
+
// running -- blocks input for the duration (the mutation/installer owns
|
|
389
|
+
// input until it finishes). Which specific row currently shows a spinner
|
|
390
|
+
// vs. a settled ✓/✗ is settled's own job, not this -- a just-finished row
|
|
391
|
+
// must show its glyph immediately, even for the instant before the next
|
|
392
|
+
// row's "start" event reassigns this to the next name.
|
|
393
|
+
private updatingRowName: string | undefined;
|
|
394
|
+
private readonly spinner = new Spinner();
|
|
395
|
+
private readonly settled = new Map<string, { ok: boolean; tail: string | undefined }>();
|
|
396
|
+
private readonly table: Table;
|
|
397
|
+
private readonly maxVisible = 20;
|
|
398
|
+
|
|
399
|
+
constructor(
|
|
400
|
+
private readonly natives: Natives,
|
|
401
|
+
private readonly host: TabHost,
|
|
402
|
+
private readonly theme: PackagesTabTheme,
|
|
403
|
+
measure: TextMeasure,
|
|
404
|
+
initialRows: Row[],
|
|
405
|
+
/** c on a row, or the Enter action menu's "Configure resources" --
|
|
406
|
+
* switches the shared TabbedContainer to Config, seeded with that
|
|
407
|
+
* row's own name as its filter. f/s switch tabs with no filter. */
|
|
408
|
+
private readonly switchTab: (target: "find" | "config" | "settings", configFilter?: string) => void,
|
|
409
|
+
) {
|
|
410
|
+
this.rows = initialRows;
|
|
411
|
+
this.filtered = visibleRows(this.rows, this.mode);
|
|
412
|
+
this.table = new Table({
|
|
413
|
+
columns: [
|
|
414
|
+
{ header: "Package", key: "name" },
|
|
415
|
+
{ header: "Version", key: "version" },
|
|
416
|
+
{ header: "", key: "status" },
|
|
417
|
+
],
|
|
418
|
+
rows: [],
|
|
419
|
+
measure,
|
|
420
|
+
headerStyle: (s) => theme.fg("muted", s),
|
|
421
|
+
});
|
|
356
422
|
}
|
|
357
423
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
424
|
+
/** An active filter or an in-flight mutation means Escape/Left-Right
|
|
425
|
+
* belong to this tab (clearing the filter; blocking navigation away
|
|
426
|
+
* mid-mutation), not the host's own back/tab-cycle handling. Packages
|
|
427
|
+
* never uses Left/Right for itself otherwise. */
|
|
428
|
+
capturesEscape(): boolean {
|
|
429
|
+
return this.searchActive || this.updatingRowName !== undefined;
|
|
362
430
|
}
|
|
363
431
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// rows (real on-disk versions) and keep the panel open, same as "refresh".
|
|
367
|
-
async function refreshRows(): Promise<void> {
|
|
368
|
-
({ rows, error } = await loadRows(natives));
|
|
369
|
-
if (error) ctx.ui.notify(`refresh failed: ${error}`, "error");
|
|
432
|
+
invalidate(): void {
|
|
433
|
+
this.table.invalidate();
|
|
370
434
|
}
|
|
371
435
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
436
|
+
private applyFilter(): void {
|
|
437
|
+
this.filtered = filterRows(visibleRows(this.rows, this.mode), this.searchInput.getValue());
|
|
438
|
+
this.selectedIndex = 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private statusLine(): string {
|
|
442
|
+
if (this.updatingRowName) return "updating…";
|
|
443
|
+
const outdated = this.rows.filter((r) => r.hasUpdate).length;
|
|
444
|
+
return outdated > 0 ? `${outdated} update(s)` : "up to date";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
render(width: number): string[] {
|
|
448
|
+
const { theme } = this;
|
|
449
|
+
const hint = this.searchActive
|
|
450
|
+
? rawKeyHint("esc", "clear")
|
|
451
|
+
: rawKeyHint("enter", "menu") +
|
|
452
|
+
theme.fg("muted", " · ") +
|
|
453
|
+
rawKeyHint("u/U", "update/all") +
|
|
454
|
+
theme.fg("muted", " · ") +
|
|
455
|
+
rawKeyHint("x", "remove") +
|
|
456
|
+
theme.fg("muted", " · ") +
|
|
457
|
+
rawKeyHint("d", "disable") +
|
|
458
|
+
theme.fg("muted", " · ") +
|
|
459
|
+
rawKeyHint("c", "config") +
|
|
460
|
+
theme.fg("muted", " · ") +
|
|
461
|
+
rawKeyHint("r", "refresh");
|
|
462
|
+
const line1 = truncateToWidth(hint, width, "");
|
|
463
|
+
const dot = "·";
|
|
464
|
+
const line2 = truncateToWidth(
|
|
465
|
+
theme.fg("muted", `${this.statusLine()} ${dot} view: ${this.mode} ${dot} / filter ${dot} tab view ${dot} ${this.rows.length} installed`),
|
|
466
|
+
width,
|
|
467
|
+
"",
|
|
468
|
+
);
|
|
469
|
+
const lines: string[] = [line1, line2];
|
|
470
|
+
if (this.searchActive) lines.push(...this.searchInput.render(width));
|
|
471
|
+
lines.push("");
|
|
472
|
+
if (this.filtered.length === 0) {
|
|
473
|
+
lines.push(theme.fg("muted", " No packages"));
|
|
474
|
+
return lines;
|
|
384
475
|
}
|
|
476
|
+
// No scrolling of its own (Malevich's Table is deliberately
|
|
477
|
+
// unopinionated about pagination) -- the visible window around
|
|
478
|
+
// selectedIndex stays this tab's own job.
|
|
479
|
+
const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filtered.length - this.maxVisible));
|
|
480
|
+
const end = Math.min(start + this.maxVisible, this.filtered.length);
|
|
481
|
+
this.table.setRows(
|
|
482
|
+
this.filtered.slice(start, end).map((row, offset) => {
|
|
483
|
+
const i = start + offset;
|
|
484
|
+
const selected = i === this.selectedIndex;
|
|
485
|
+
const cursor = selected ? theme.fg("accent", "❯ ") : " ";
|
|
486
|
+
const name = selected ? theme.bold(row.name) : row.name;
|
|
487
|
+
// A settled row shows its real outcome even for the instant before
|
|
488
|
+
// the next row's "start" event moves updatingRowName off it --
|
|
489
|
+
// settled always wins over "still spinning".
|
|
490
|
+
const rowSettled = this.settled.get(row.name);
|
|
491
|
+
const isUpdating = !rowSettled && this.updatingRowName === row.name;
|
|
492
|
+
const status = isUpdating
|
|
493
|
+
? theme.fg("accent", `${this.spinner.glyph()} updating…`)
|
|
494
|
+
: rowSettled
|
|
495
|
+
? theme.fg(rowSettled.ok ? "success" : "error", `${rowSettled.ok ? "✓" : "✗"}${rowSettled.tail ? ` ${rowSettled.tail}` : ""}`)
|
|
496
|
+
: row.hasUpdate ? theme.fg("warning", `↑${row.latest}`) : "";
|
|
497
|
+
return { name: `${cursor}${name}`, version: theme.fg("dim", row.version), status };
|
|
498
|
+
}),
|
|
499
|
+
);
|
|
500
|
+
lines.push(...this.table.render(width));
|
|
501
|
+
const hasScroll = start > 0 || end < this.filtered.length;
|
|
502
|
+
lines.push(theme.fg("dim", ` ${hasScroll ? `${this.selectedIndex + 1}/${this.filtered.length} ` : ""}${this.mode}`));
|
|
503
|
+
return lines;
|
|
504
|
+
}
|
|
385
505
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
506
|
+
handleInput(data: string): void {
|
|
507
|
+
if (this.updatingRowName) return; // the mutation/installer owns input until it finishes
|
|
508
|
+
|
|
509
|
+
if (this.searchActive) {
|
|
510
|
+
if (data === "\x1b") {
|
|
511
|
+
this.searchActive = false;
|
|
512
|
+
this.searchInput.setValue?.("");
|
|
513
|
+
this.applyFilter();
|
|
514
|
+
} else if (data === "\r") {
|
|
515
|
+
this.searchActive = false;
|
|
516
|
+
} else {
|
|
517
|
+
this.searchInput.handleInput(data);
|
|
518
|
+
this.applyFilter();
|
|
519
|
+
}
|
|
520
|
+
this.host.requestRender();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
switch (data) {
|
|
525
|
+
case "\x1b[A":
|
|
526
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.filtered.length) % Math.max(this.filtered.length, 1);
|
|
527
|
+
break;
|
|
528
|
+
case "\x1b[B":
|
|
529
|
+
this.selectedIndex = (this.selectedIndex + 1) % Math.max(this.filtered.length, 1);
|
|
530
|
+
break;
|
|
531
|
+
case "\t":
|
|
532
|
+
this.mode = nextMode(this.mode);
|
|
533
|
+
this.applyFilter();
|
|
534
|
+
break;
|
|
535
|
+
case "/":
|
|
536
|
+
this.searchActive = true;
|
|
537
|
+
break;
|
|
538
|
+
case "r":
|
|
539
|
+
void this.refresh();
|
|
540
|
+
return;
|
|
541
|
+
case "s":
|
|
542
|
+
this.switchTab("settings");
|
|
543
|
+
return;
|
|
544
|
+
case "f":
|
|
545
|
+
this.switchTab("find");
|
|
546
|
+
return;
|
|
547
|
+
case "U":
|
|
548
|
+
void this.runUpdateAllInline();
|
|
549
|
+
return;
|
|
550
|
+
case "u": {
|
|
551
|
+
const row = this.filtered[this.selectedIndex];
|
|
552
|
+
if (row?.hasUpdate) void this.runRowActionInline({ type: "update", row });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
case "x": {
|
|
556
|
+
const row = this.filtered[this.selectedIndex];
|
|
557
|
+
if (row) void this.runRowActionInline({ type: "remove", row });
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
case "d": {
|
|
561
|
+
const row = this.filtered[this.selectedIndex];
|
|
562
|
+
if (row) void this.runRowActionInline({ type: "disable", row });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
case "c": {
|
|
566
|
+
const row = this.filtered[this.selectedIndex];
|
|
567
|
+
if (row) this.switchTab("config", row.name);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
case "\r": {
|
|
571
|
+
const row = this.filtered[this.selectedIndex];
|
|
572
|
+
if (!row) return;
|
|
573
|
+
void (async () => {
|
|
574
|
+
const choice = await showActionMenu(this.host.inlineCtx, row);
|
|
575
|
+
if (choice === "update" || choice === "remove" || choice === "disable") void this.runRowActionInline({ type: choice, row });
|
|
576
|
+
else if (choice === "config") this.switchTab("config", row.name);
|
|
577
|
+
})();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
default:
|
|
581
|
+
return;
|
|
389
582
|
}
|
|
583
|
+
this.host.requestRender();
|
|
584
|
+
}
|
|
390
585
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
586
|
+
private async refresh(): Promise<void> {
|
|
587
|
+
const reloaded = await loadRows(this.natives);
|
|
588
|
+
if (reloaded.error) this.host.ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
|
|
589
|
+
else this.rows = reloaded.rows;
|
|
590
|
+
this.applyFilter();
|
|
591
|
+
this.host.requestRender();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** u/x/d and the Enter action menu all funnel through here: approval and
|
|
595
|
+
* reload confirms render inline (via host.inlineCtx) on this same
|
|
596
|
+
* overlay, never closing it first. */
|
|
597
|
+
private async runRowActionInline(action: { type: "update" | "remove" | "disable"; row: Row }): Promise<void> {
|
|
598
|
+
this.updatingRowName = action.row.name;
|
|
599
|
+
this.host.requestRender();
|
|
600
|
+
// A non-"changed" settle point (already up to date, pinned, deferred, a
|
|
601
|
+
// genuine failure) shows its own real reason on this row, the same
|
|
602
|
+
// settled map U's batch path already renders -- not just a scrollback
|
|
603
|
+
// toast.
|
|
604
|
+
const onSettled = (result: PackageChoiceSettled) => this.settled.set(action.row.name, { ok: result.ok, tail: result.message });
|
|
605
|
+
const outcome = action.type === "disable"
|
|
606
|
+
? await applyDisableExtensions(action.row, this.natives, this.host.inlineCtx)
|
|
607
|
+
: await applyPackageChoice(action.type === "update" ? `Update to ${action.row.latest}` : "Remove", action.row, this.natives, this.host.inlineCtx, onSettled);
|
|
608
|
+
this.updatingRowName = undefined;
|
|
609
|
+
if (outcome === "changed") {
|
|
610
|
+
this.host.onSessionReplaced(); // ctx.reload() already replaced the session
|
|
611
|
+
return;
|
|
394
612
|
}
|
|
613
|
+
const reloaded = await loadRows(this.natives);
|
|
614
|
+
if (reloaded.error) this.host.ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
|
|
615
|
+
else this.rows = reloaded.rows;
|
|
616
|
+
this.applyFilter();
|
|
617
|
+
this.host.requestRender();
|
|
618
|
+
}
|
|
395
619
|
|
|
396
|
-
|
|
397
|
-
|
|
620
|
+
/** U -- runs the whole approve+update+notify+reload flow without ever
|
|
621
|
+
* closing this tab or replacing the list: each row's own settled outcome
|
|
622
|
+
* (spinner while in flight, then a real ✓/✗ plus a bounded tail of its
|
|
623
|
+
* actual captured output) appears inline next to that row. Rows refresh
|
|
624
|
+
* in place afterward unless a reload already ended the session. */
|
|
625
|
+
private async runUpdateAllInline(): Promise<void> {
|
|
626
|
+
const outdated = this.rows.filter((row) => row.hasUpdate);
|
|
627
|
+
this.settled.clear();
|
|
628
|
+
const outcome = await approveAndRunUpdateAll(outdated, this.natives, this.host.inlineCtx, (batch, approved) => {
|
|
629
|
+
this.spinner.start(() => this.host.requestRender());
|
|
630
|
+
return performUpdateAll(batch, this.natives, approved, (event) => {
|
|
631
|
+
this.updatingRowName = event.row.name;
|
|
632
|
+
if (event.phase === "done" && event.result) {
|
|
633
|
+
this.settled.set(event.row.name, { ok: event.result.ok, tail: logTail(event.result.output) });
|
|
634
|
+
}
|
|
635
|
+
this.host.requestRender();
|
|
636
|
+
}).finally(() => this.spinner.stop());
|
|
637
|
+
});
|
|
638
|
+
this.updatingRowName = undefined;
|
|
639
|
+
if (outcome === "changed") {
|
|
640
|
+
this.host.onSessionReplaced(); // ctx.reload() already replaced the session
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const reloaded = await loadRows(this.natives);
|
|
644
|
+
if (reloaded.error) this.host.ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
|
|
645
|
+
else this.rows = reloaded.rows;
|
|
646
|
+
this.settled.clear(); // fresh state for a subsequent batch
|
|
647
|
+
this.applyFilter();
|
|
648
|
+
this.host.requestRender();
|
|
398
649
|
}
|
|
399
650
|
}
|
|
400
651
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// before. The list stays fully visible throughout. Which specific row
|
|
412
|
-
// currently shows a spinner vs. a settled ✓/✗ is settled's own job
|
|
413
|
-
// (below), not this -- a just-finished row must show its glyph
|
|
414
|
-
// immediately, even for the instant before the next row's "start"
|
|
415
|
-
// event reassigns this to the next name.
|
|
416
|
-
let updatingRowName: string | undefined;
|
|
417
|
-
const spinner = new Spinner();
|
|
418
|
-
const settled = new Map<string, { ok: boolean; tail: string | undefined }>();
|
|
419
|
-
|
|
420
|
-
const maxVisible = 20;
|
|
421
|
-
|
|
422
|
-
function applyFilter(): void {
|
|
423
|
-
filtered = filterRows(visibleRows(rows, mode), searchInput.getValue());
|
|
424
|
-
selectedIndex = 0;
|
|
425
|
-
}
|
|
652
|
+
interface TabScope {
|
|
653
|
+
/** True while this tab wants Escape for itself right now (an active
|
|
654
|
+
* filter, an in-flight mutation) instead of the host's own "back to
|
|
655
|
+
* Packages, or close from Packages" handling. Defaults to false. */
|
|
656
|
+
capturesEscape?(): boolean;
|
|
657
|
+
/** True while this tab wants Left/Right for itself right now (Find's
|
|
658
|
+
* always-on query cursor) instead of the host's own tab-cycling.
|
|
659
|
+
* Defaults to false. */
|
|
660
|
+
capturesHorizontalArrows?(): boolean;
|
|
661
|
+
}
|
|
426
662
|
|
|
663
|
+
export async function showPackedPanel(
|
|
664
|
+
ctx: ExtensionCommandContext,
|
|
665
|
+
natives: Natives,
|
|
666
|
+
opts?: { initialTab?: PackedTabKey; initialConfigFilter?: string },
|
|
667
|
+
): Promise<void> {
|
|
668
|
+
if (!ctx.hasUI) {
|
|
669
|
+
ctx.ui.notify("/packed requires interactive mode", "warning");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const { rows, error } = await loadRows(natives);
|
|
674
|
+
if (error) {
|
|
675
|
+
ctx.ui.notify(`packed unavailable: ${error}`, "error");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
await renderUnifiedPanel(ctx, natives, rows, opts?.initialTab ?? "packages", opts?.initialConfigFilter);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function renderUnifiedPanel(
|
|
683
|
+
ctx: ExtensionCommandContext,
|
|
684
|
+
natives: Natives,
|
|
685
|
+
initialRows: Row[],
|
|
686
|
+
initialTab: PackedTabKey,
|
|
687
|
+
initialConfigFilter: string | undefined,
|
|
688
|
+
): Promise<void> {
|
|
689
|
+
return ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
427
690
|
// A y/n decision rendered as this SAME overlay's own content (via a real
|
|
428
691
|
// Malevich Dialog, dispatched by literal key press) instead of
|
|
429
|
-
// ctx.ui.confirm's separate native dialog --
|
|
430
|
-
//
|
|
431
|
-
//
|
|
692
|
+
// ctx.ui.confirm's separate native dialog -- shared by every tab via
|
|
693
|
+
// host.inlineCtx below, exactly like the panel's own approval/reload
|
|
694
|
+
// dialogs always have been.
|
|
432
695
|
let pendingDialog: Dialog | undefined;
|
|
433
696
|
|
|
434
697
|
function confirmInline(title: string, message: string): Promise<boolean> {
|
|
@@ -446,188 +709,75 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
446
709
|
{ label: "No", key: "n", action: () => settle(false) },
|
|
447
710
|
],
|
|
448
711
|
theme: dialogTheme(theme),
|
|
712
|
+
framed: false, // this overlay's own Envelope already draws a border; a second rule would double up on it
|
|
449
713
|
});
|
|
450
714
|
tui.requestRender();
|
|
451
715
|
});
|
|
452
716
|
}
|
|
453
717
|
|
|
454
|
-
// Every confirm() call inside a mutation flow driven from this open
|
|
455
|
-
// overlay (approvePackageOperation's own approval, confirmReload's
|
|
456
|
-
// separate reload gate) routes through confirmInline instead of the
|
|
457
|
-
// real ctx.ui.confirm -- notify/reload/etc. stay the genuine ctx.
|
|
458
718
|
const inlineCtx: ExtensionCommandContext = { ...ctx, hasUI: true, ui: { ...ctx.ui, confirm: confirmInline } };
|
|
719
|
+
const host: TabHost = {
|
|
720
|
+
ctx,
|
|
721
|
+
inlineCtx,
|
|
722
|
+
requestRender: () => tui.requestRender(),
|
|
723
|
+
onSessionReplaced: () => done(undefined),
|
|
724
|
+
};
|
|
459
725
|
|
|
460
|
-
|
|
461
|
-
* reload confirms render inline (via inlineCtx/confirmInline) on this
|
|
462
|
-
* same overlay, never closing it first the way done()-dispatch used to. */
|
|
463
|
-
async function runRowActionInline(action: { type: "update" | "remove" | "disable"; row: Row }): Promise<void> {
|
|
464
|
-
updatingRowName = action.row.name;
|
|
465
|
-
tui.requestRender();
|
|
466
|
-
const outcome = action.type === "disable"
|
|
467
|
-
? await applyDisableExtensions(action.row, natives, inlineCtx)
|
|
468
|
-
: await applyPackageChoice(action.type === "update" ? `Update to ${action.row.latest}` : "Remove", action.row, natives, inlineCtx);
|
|
469
|
-
updatingRowName = undefined;
|
|
470
|
-
if (outcome === "changed") {
|
|
471
|
-
done(undefined); // ctx.reload() already replaced the session
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
const reloaded = await loadRows(natives);
|
|
475
|
-
if (reloaded.error) ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
|
|
476
|
-
else rows = reloaded.rows;
|
|
477
|
-
applyFilter();
|
|
478
|
-
tui.requestRender();
|
|
479
|
-
}
|
|
726
|
+
const measure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
480
727
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
* updatingRowName/spinner/settled, which list's own render checks. Rows
|
|
486
|
-
* refresh in place afterward unless a reload already ended the session. */
|
|
487
|
-
async function runUpdateAllInline(): Promise<void> {
|
|
488
|
-
const outdated = rows.filter((row) => row.hasUpdate);
|
|
489
|
-
settled.clear();
|
|
490
|
-
const outcome = await approveAndRunUpdateAll(outdated, natives, inlineCtx, (batch, approved) => {
|
|
491
|
-
spinner.start(() => tui.requestRender());
|
|
492
|
-
return performUpdateAll(batch, natives, approved, (event) => {
|
|
493
|
-
updatingRowName = event.row.name;
|
|
494
|
-
if (event.phase === "done" && event.result) {
|
|
495
|
-
settled.set(event.row.name, { ok: event.result.ok, tail: logTail(event.result.output) });
|
|
496
|
-
}
|
|
497
|
-
tui.requestRender();
|
|
498
|
-
}).finally(() => spinner.stop());
|
|
499
|
-
});
|
|
500
|
-
updatingRowName = undefined;
|
|
501
|
-
if (outcome === "changed") {
|
|
502
|
-
done(undefined); // ctx.reload() already replaced the session
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
const reloaded = await loadRows(natives);
|
|
506
|
-
if (reloaded.error) ctx.ui.notify(`refresh failed: ${reloaded.error}`, "error");
|
|
507
|
-
else rows = reloaded.rows;
|
|
508
|
-
settled.clear(); // fresh state for a subsequent batch
|
|
509
|
-
applyFilter();
|
|
510
|
-
tui.requestRender();
|
|
511
|
-
}
|
|
728
|
+
const packagesTab = new PackagesTab(natives, host, theme, measure, initialRows, (target, configFilter) => {
|
|
729
|
+
if (target === "config" && configFilter !== undefined) configTab.setFilter(configFilter);
|
|
730
|
+
tabbedContainer.setActive(target);
|
|
731
|
+
});
|
|
512
732
|
|
|
513
|
-
|
|
514
|
-
if (updatingRowName) return "Packages · updating…";
|
|
515
|
-
const outdated = rows.filter((r) => r.hasUpdate).length;
|
|
516
|
-
return outdated > 0 ? `Packages · ${outdated} update(s)` : "Packages";
|
|
517
|
-
}
|
|
733
|
+
const findTab = createFindTab(natives, host, theme);
|
|
518
734
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
rawKeyHint("u/U", "update/all") +
|
|
527
|
-
theme.fg("muted", " · ") +
|
|
528
|
-
rawKeyHint("x", "remove") +
|
|
529
|
-
theme.fg("muted", " · ") +
|
|
530
|
-
rawKeyHint("d", "disable") +
|
|
531
|
-
theme.fg("muted", " · ") +
|
|
532
|
-
rawKeyHint("c", "config") +
|
|
533
|
-
theme.fg("muted", " · ") +
|
|
534
|
-
rawKeyHint("f", "find") +
|
|
535
|
-
theme.fg("muted", " · ") +
|
|
536
|
-
rawKeyHint("s", "settings") +
|
|
537
|
-
theme.fg("muted", " · ") +
|
|
538
|
-
rawKeyHint("esc", "close");
|
|
539
|
-
const line1 = truncateToWidth(hint, width, "");
|
|
540
|
-
const dot = "·";
|
|
541
|
-
const line2 = truncateToWidth(
|
|
542
|
-
theme.fg("muted", `view: ${mode} ${dot} / filter ${dot} tab view ${dot} r refresh ${dot} ${rows.length} installed`),
|
|
543
|
-
width,
|
|
544
|
-
"",
|
|
545
|
-
);
|
|
546
|
-
return [line1, line2];
|
|
547
|
-
},
|
|
548
|
-
};
|
|
735
|
+
// Config/Settings both need theme (only available once this factory
|
|
736
|
+
// runs) so they're constructed here rather than before the overlay
|
|
737
|
+
// opens; both load their own data asynchronously in the background and
|
|
738
|
+
// render their own "Loading…" state until it resolves -- no
|
|
739
|
+
// placeholder-swap machinery needed.
|
|
740
|
+
const configTab = new ConfigTab(natives, host, theme, initialTab === "config" ? initialConfigFilter : undefined);
|
|
741
|
+
void configTab.load().then(() => tui.requestRender());
|
|
549
742
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
{ header: "Version", key: "version" },
|
|
559
|
-
{ header: "", key: "status" },
|
|
560
|
-
];
|
|
561
|
-
const table = new Table({ columns, rows: [], measure, headerStyle: (s) => theme.fg("muted", s) });
|
|
562
|
-
|
|
563
|
-
const list = {
|
|
564
|
-
invalidate() {},
|
|
565
|
-
render(width: number): string[] {
|
|
566
|
-
const lines: string[] = [];
|
|
567
|
-
if (searchActive) lines.push(...searchInput.render(width));
|
|
568
|
-
lines.push("");
|
|
569
|
-
if (filtered.length === 0) {
|
|
570
|
-
lines.push(theme.fg("muted", " No packages"));
|
|
571
|
-
return lines;
|
|
572
|
-
}
|
|
573
|
-
// No scrolling of its own (Malevich's Table is deliberately
|
|
574
|
-
// unopinionated about pagination) -- the visible window around
|
|
575
|
-
// selectedIndex stays this panel's own job, same as before.
|
|
576
|
-
const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
577
|
-
const end = Math.min(start + maxVisible, filtered.length);
|
|
578
|
-
table.setRows(
|
|
579
|
-
filtered.slice(start, end).map((row, offset) => {
|
|
580
|
-
const i = start + offset;
|
|
581
|
-
const selected = i === selectedIndex;
|
|
582
|
-
const cursor = selected ? theme.fg("accent", "❯ ") : " ";
|
|
583
|
-
const name = selected ? theme.bold(row.name) : row.name;
|
|
584
|
-
// A settled row shows its real outcome even for the instant
|
|
585
|
-
// before the next row's "start" event moves updatingRowName
|
|
586
|
-
// off it -- settled always wins over "still spinning".
|
|
587
|
-
const rowSettled = settled.get(row.name);
|
|
588
|
-
const isUpdating = !rowSettled && updatingRowName === row.name;
|
|
589
|
-
const status = isUpdating
|
|
590
|
-
? theme.fg("accent", `${spinner.glyph()} updating…`)
|
|
591
|
-
: rowSettled
|
|
592
|
-
? theme.fg(rowSettled.ok ? "success" : "error", `${rowSettled.ok ? "✓" : "✗"}${rowSettled.tail ? ` ${rowSettled.tail}` : ""}`)
|
|
593
|
-
: row.hasUpdate ? theme.fg("warning", `↑${row.latest}`) : "";
|
|
594
|
-
return { name: `${cursor}${name}`, version: theme.fg("dim", row.version), status };
|
|
595
|
-
}),
|
|
596
|
-
);
|
|
597
|
-
lines.push(...table.render(width));
|
|
598
|
-
const hasScroll = start > 0 || end < filtered.length;
|
|
599
|
-
lines.push(theme.fg("dim", ` ${hasScroll ? `${selectedIndex + 1}/${filtered.length} ` : ""}${mode}`));
|
|
600
|
-
return lines;
|
|
601
|
-
},
|
|
743
|
+
const settingsTab = new SettingsTab(natives, host, theme);
|
|
744
|
+
void settingsTab.load().then(() => tui.requestRender());
|
|
745
|
+
|
|
746
|
+
const tabByKey: Record<PackedTabKey, Component & TabScope> = {
|
|
747
|
+
packages: packagesTab,
|
|
748
|
+
find: findTab,
|
|
749
|
+
config: configTab,
|
|
750
|
+
settings: settingsTab,
|
|
602
751
|
};
|
|
603
752
|
|
|
604
|
-
|
|
605
|
-
|
|
753
|
+
const tabbedContainer = new TabbedContainer({
|
|
754
|
+
tabs: [
|
|
755
|
+
{ key: "packages", label: "Packages", content: tabByKey.packages },
|
|
756
|
+
{ key: "find", label: "Find", content: tabByKey.find },
|
|
757
|
+
{ key: "config", label: "Config", content: tabByKey.config },
|
|
758
|
+
{ key: "settings", label: "Settings", content: tabByKey.settings },
|
|
759
|
+
],
|
|
760
|
+
theme: tabBarTheme(theme),
|
|
761
|
+
initialKey: initialTab,
|
|
762
|
+
});
|
|
763
|
+
|
|
606
764
|
// measure must be explicit: Envelope's own default is ASCII-only (raw
|
|
607
|
-
// .length, blind to ANSI escape codes) and every
|
|
608
|
-
// through theme.fg/theme.bold -- without this, Envelope pads
|
|
609
|
-
// against its own escape-code-inflated "length" instead of
|
|
610
|
-
// visible width, so the right border lands at a different
|
|
611
|
-
// every line depending on how much styling
|
|
765
|
+
// .length, blind to ANSI escape codes) and every tab's own content is
|
|
766
|
+
// styled through theme.fg/theme.bold -- without this, Envelope pads
|
|
767
|
+
// each line against its own escape-code-inflated "length" instead of
|
|
768
|
+
// its real visible width, so the right border lands at a different
|
|
769
|
+
// column on every line depending on how much styling it carries.
|
|
612
770
|
const envelope = new Envelope({
|
|
613
|
-
title:
|
|
771
|
+
title: "packed",
|
|
614
772
|
borderStyle: "rounded",
|
|
615
773
|
style: (s) => theme.fg("border", s),
|
|
616
774
|
titleStyle: (s) => theme.bold(theme.fg("accent", s)),
|
|
617
775
|
measure,
|
|
618
776
|
});
|
|
619
|
-
const body = {
|
|
620
|
-
invalidate() { header.invalidate(); list.invalidate(); },
|
|
621
|
-
render(width: number): string[] {
|
|
622
|
-
return [...header.render(width), "", ...list.render(width)];
|
|
623
|
-
},
|
|
624
|
-
};
|
|
625
|
-
envelope.setContent(body);
|
|
626
777
|
|
|
627
778
|
return {
|
|
628
779
|
render(width: number): string[] {
|
|
629
|
-
envelope.
|
|
630
|
-
envelope.setContent(pendingDialog ?? body);
|
|
780
|
+
envelope.setContent(pendingDialog ?? tabbedContainer);
|
|
631
781
|
return envelope.render(width);
|
|
632
782
|
},
|
|
633
783
|
invalidate: () => envelope.invalidate(),
|
|
@@ -637,85 +787,24 @@ function renderPanel(ctx: ExtensionCommandContext, natives: Natives, initialRows
|
|
|
637
787
|
tui.requestRender();
|
|
638
788
|
return;
|
|
639
789
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
} else {
|
|
650
|
-
searchInput.handleInput(data);
|
|
651
|
-
applyFilter();
|
|
652
|
-
}
|
|
790
|
+
const activeKey = tabbedContainer.getActiveKey() as PackedTabKey;
|
|
791
|
+
const activeTab = tabByKey[activeKey];
|
|
792
|
+
// Each host-level key is checked against that tab's own capture flag
|
|
793
|
+
// independently -- conflating them would, e.g., let Find's always-on
|
|
794
|
+
// capturesHorizontalArrows() also swallow Escape into the query box
|
|
795
|
+
// instead of navigating back.
|
|
796
|
+
if (data === "\x1b" && !(activeTab.capturesEscape?.() ?? false)) {
|
|
797
|
+
if (activeKey !== "packages") tabbedContainer.setActive("packages");
|
|
798
|
+
else { done(undefined); return; }
|
|
653
799
|
tui.requestRender();
|
|
654
800
|
return;
|
|
655
801
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
break;
|
|
661
|
-
case "\x1b[B": // down
|
|
662
|
-
selectedIndex = (selectedIndex + 1) % Math.max(filtered.length, 1);
|
|
663
|
-
break;
|
|
664
|
-
case "\t":
|
|
665
|
-
mode = nextMode(mode);
|
|
666
|
-
applyFilter();
|
|
667
|
-
break;
|
|
668
|
-
case "/":
|
|
669
|
-
searchActive = true;
|
|
670
|
-
break;
|
|
671
|
-
case "r":
|
|
672
|
-
done({ type: "refresh" });
|
|
673
|
-
return;
|
|
674
|
-
case "s":
|
|
675
|
-
done({ type: "settings" });
|
|
676
|
-
return;
|
|
677
|
-
case "f":
|
|
678
|
-
done({ type: "find" });
|
|
679
|
-
return;
|
|
680
|
-
case "U":
|
|
681
|
-
void runUpdateAllInline();
|
|
682
|
-
return;
|
|
683
|
-
case "u": {
|
|
684
|
-
const row = filtered[selectedIndex];
|
|
685
|
-
if (row?.hasUpdate) void runRowActionInline({ type: "update", row });
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
case "x": {
|
|
689
|
-
const row = filtered[selectedIndex];
|
|
690
|
-
if (row) void runRowActionInline({ type: "remove", row });
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
case "d": {
|
|
694
|
-
const row = filtered[selectedIndex];
|
|
695
|
-
if (row) void runRowActionInline({ type: "disable", row });
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
case "c": {
|
|
699
|
-
const row = filtered[selectedIndex];
|
|
700
|
-
if (row) done({ type: "config", row });
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
case "\r": {
|
|
704
|
-
const row = filtered[selectedIndex];
|
|
705
|
-
if (!row) return;
|
|
706
|
-
void (async () => {
|
|
707
|
-
const choice = await showActionMenu(ctx, row);
|
|
708
|
-
if (choice === "update" || choice === "remove" || choice === "disable") void runRowActionInline({ type: choice, row });
|
|
709
|
-
else if (choice === "config") done({ type: "config", row });
|
|
710
|
-
})();
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
case "\x1b":
|
|
714
|
-
done(undefined);
|
|
715
|
-
return;
|
|
716
|
-
default:
|
|
717
|
-
return;
|
|
802
|
+
if ((data === "\x1b[C" || data === "\x1b[D") && !(activeTab.capturesHorizontalArrows?.() ?? false)) {
|
|
803
|
+
tabbedContainer.handleInput(data); // owns Left/Right cycling itself
|
|
804
|
+
tui.requestRender();
|
|
805
|
+
return;
|
|
718
806
|
}
|
|
807
|
+
activeTab.handleInput?.(data);
|
|
719
808
|
tui.requestRender();
|
|
720
809
|
},
|
|
721
810
|
};
|