@shanepadgett/tau-tui 0.29.0 → 0.31.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
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
getKeybindings,
|
|
5
|
+
truncateToWidth,
|
|
6
|
+
type TUI,
|
|
7
|
+
visibleWidth,
|
|
8
|
+
wrapTextWithAnsi,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import { bindingHint } from "./key-hints.ts";
|
|
3
11
|
import { formatAge, preview } from "./text.ts";
|
|
4
12
|
import {
|
|
5
13
|
SelectableList,
|
|
@@ -7,7 +15,7 @@ import {
|
|
|
7
15
|
type SelectableListItem,
|
|
8
16
|
type SelectableListRowState,
|
|
9
17
|
} from "./selectable-list.ts";
|
|
10
|
-
import { ToolPanel } from "./tool-panel.ts";
|
|
18
|
+
import { ToolPanel, type ToolPanelConfig } from "./tool-panel.ts";
|
|
11
19
|
|
|
12
20
|
export interface TextRecordSelectItem extends SelectableListItem {
|
|
13
21
|
text: string;
|
|
@@ -19,27 +27,38 @@ export type TextRecordSelectResult<T extends TextRecordSelectItem> =
|
|
|
19
27
|
| { kind: "primary"; item: T }
|
|
20
28
|
| { kind: "action"; actionId: string; item: T };
|
|
21
29
|
|
|
22
|
-
export interface
|
|
30
|
+
export interface TextRecordDestructiveAction<T extends TextRecordSelectItem> extends SelectableListAction {
|
|
31
|
+
confirmLabel(item: T): string;
|
|
32
|
+
runningLabel: string;
|
|
33
|
+
onConfirm(item: T): Promise<readonly T[]>;
|
|
34
|
+
onError(error: unknown): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TextRecordSelectPanelConfig<T extends TextRecordSelectItem> {
|
|
23
38
|
title: string;
|
|
24
39
|
path: string;
|
|
25
40
|
emptyMessage: string;
|
|
26
41
|
primaryLabel: string;
|
|
27
42
|
actions: readonly SelectableListAction[];
|
|
43
|
+
destructiveAction: TextRecordDestructiveAction<T>;
|
|
28
44
|
expandActiveItem: boolean;
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
export function createTextRecordSelectPanel<T extends TextRecordSelectItem>(
|
|
48
|
+
tui: TUI,
|
|
32
49
|
theme: Theme,
|
|
33
50
|
items: readonly T[],
|
|
34
|
-
config: TextRecordSelectPanelConfig
|
|
51
|
+
config: TextRecordSelectPanelConfig<T>,
|
|
35
52
|
done: (result: TextRecordSelectResult<T>) => void,
|
|
36
53
|
): Component {
|
|
54
|
+
let currentItems = items;
|
|
55
|
+
let destructiveState: { item: T; running: boolean } | undefined;
|
|
37
56
|
const list = new SelectableList(theme, {
|
|
38
57
|
items,
|
|
39
58
|
emptyMessage: config.emptyMessage,
|
|
40
59
|
selection: { kind: "single", primaryLabel: config.primaryLabel },
|
|
41
60
|
filter: { searchText: (item) => item.text },
|
|
42
|
-
actions: config.actions,
|
|
61
|
+
actions: [...config.actions, config.destructiveAction],
|
|
43
62
|
cancelLabel: "cancel",
|
|
44
63
|
maxVisible: 12,
|
|
45
64
|
renderItem: (item, state, width) => renderTextRecord(theme, item, state, config.expandActiveItem, width),
|
|
@@ -50,20 +69,69 @@ export function createTextRecordSelectPanel<T extends TextRecordSelectItem>(
|
|
|
50
69
|
}
|
|
51
70
|
const item = result.items[0];
|
|
52
71
|
if (!item) return;
|
|
72
|
+
if (result.kind === "action" && result.actionId === config.destructiveAction.id) {
|
|
73
|
+
destructiveState = { item, running: false };
|
|
74
|
+
syncPanel();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
53
77
|
done(
|
|
54
78
|
result.kind === "primary" ? { kind: "primary", item } : { kind: "action", actionId: result.actionId, item },
|
|
55
79
|
);
|
|
56
80
|
},
|
|
57
81
|
});
|
|
58
|
-
const
|
|
82
|
+
const panelConfig: ToolPanelConfig = {
|
|
59
83
|
title: config.title,
|
|
60
84
|
secondary: `${items.length} total · ${config.path}`,
|
|
61
85
|
body: list,
|
|
62
86
|
footer: { kind: "hints", hints: list.getKeyHints() },
|
|
63
|
-
}
|
|
87
|
+
};
|
|
88
|
+
const panel = new ToolPanel(theme, panelConfig);
|
|
89
|
+
|
|
90
|
+
function syncPanel(): void {
|
|
91
|
+
panelConfig.secondary = `${currentItems.length} total · ${config.path}`;
|
|
92
|
+
panelConfig.footer = destructiveState
|
|
93
|
+
? destructiveState.running
|
|
94
|
+
? { kind: "infoAck", message: config.destructiveAction.runningLabel, hints: [] }
|
|
95
|
+
: {
|
|
96
|
+
kind: "destructiveAck",
|
|
97
|
+
message: config.destructiveAction.confirmLabel(destructiveState.item),
|
|
98
|
+
hints: [bindingHint("tui.select.confirm", "confirm"), bindingHint("tui.select.cancel", "cancel")],
|
|
99
|
+
}
|
|
100
|
+
: { kind: "hints", hints: list.getKeyHints() };
|
|
101
|
+
tui.requestRender();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function confirmDestructive(item: T): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
currentItems = await config.destructiveAction.onConfirm(item);
|
|
107
|
+
list.setItems(currentItems);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
config.destructiveAction.onError(error);
|
|
110
|
+
}
|
|
111
|
+
destructiveState = undefined;
|
|
112
|
+
syncPanel();
|
|
113
|
+
}
|
|
64
114
|
|
|
65
115
|
return {
|
|
66
|
-
handleInput: (data) =>
|
|
116
|
+
handleInput: (data) => {
|
|
117
|
+
if (!destructiveState) {
|
|
118
|
+
list.handleInput(data);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (destructiveState.running) return;
|
|
122
|
+
const keybindings = getKeybindings();
|
|
123
|
+
if (keybindings.matches(data, "tui.select.confirm")) {
|
|
124
|
+
const item = destructiveState.item;
|
|
125
|
+
destructiveState.running = true;
|
|
126
|
+
syncPanel();
|
|
127
|
+
void confirmDestructive(item);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
131
|
+
destructiveState = undefined;
|
|
132
|
+
syncPanel();
|
|
133
|
+
}
|
|
134
|
+
},
|
|
67
135
|
render: (width) => panel.render(width),
|
|
68
136
|
invalidate: () => panel.invalidate(),
|
|
69
137
|
};
|