@posthog/wizard 2.79.0 → 2.79.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/dist/PickerMenu-dG5DQCZA.js +758 -0
- package/dist/PickerMenu-dG5DQCZA.js.map +1 -0
- package/dist/{add-mcp-server-to-clients-amlJWW7W.js → add-mcp-server-to-clients-DIaTRWMY.js} +7 -18
- package/dist/{add-mcp-server-to-clients-amlJWW7W.js.map → add-mcp-server-to-clients-DIaTRWMY.js.map} +1 -1
- package/dist/add-mcp-server-to-clients-artmN7mz.js +2 -0
- package/dist/agent-BEjARD2a.js +6 -0
- package/dist/{agent-CNrEC-9v.js → agent-BWFhuLgo.js} +18 -24
- package/dist/{agent-CNrEC-9v.js.map → agent-BWFhuLgo.js.map} +1 -1
- package/dist/{agent-interface-BXvmAm4c.js → agent-interface-FkbaBJwz.js} +29 -17
- package/dist/agent-interface-FkbaBJwz.js.map +1 -0
- package/dist/{agent-prompt-loader-DiKUbhi_.js → agent-prompt-loader-D0weQ-7k.js} +5 -5
- package/dist/{agent-prompt-loader-DiKUbhi_.js.map → agent-prompt-loader-D0weQ-7k.js.map} +1 -1
- package/dist/analytics-DFtbRjYh.js +2 -0
- package/dist/{analytics-VRYdebXW.js → analytics-DXK1gc12.js} +4 -4
- package/dist/{analytics-VRYdebXW.js.map → analytics-DXK1gc12.js.map} +1 -1
- package/dist/api-Cm9UtKZh.js +2 -0
- package/dist/{api-DGuJTzjr.js → api-CrbCIQOz.js} +4 -16
- package/dist/{api-DGuJTzjr.js.map → api-CrbCIQOz.js.map} +1 -1
- package/dist/bin.js +5 -2960
- package/dist/bin.js.map +1 -1
- package/dist/{bounded-fs-Cpn129A4.js → bounded-fs-vAq-T-K7.js} +3 -3
- package/dist/{bounded-fs-Cpn129A4.js.map → bounded-fs-vAq-T-K7.js.map} +1 -1
- package/dist/{ci-install-BPA6gzEq.js → ci-install-COcV8SY2.js} +8 -7
- package/dist/{ci-install-BPA6gzEq.js.map → ci-install-COcV8SY2.js.map} +1 -1
- package/dist/{constants-CuJfQEcC.js → constants-DHCBSfnx.js} +3 -3
- package/dist/{constants-CuJfQEcC.js.map → constants-DHCBSfnx.js.map} +1 -1
- package/dist/{debug-B9k4ideV.js → debug-BuYHGr6_.js} +1 -1
- package/dist/{debug-Cj-8vfXY.js → debug-kAHwWGg7.js} +2 -2
- package/dist/{debug-Cj-8vfXY.js.map → debug-kAHwWGg7.js.map} +1 -1
- package/dist/{defaults-C6J7yL4O.js → defaults-a9TelDhJ.js} +1 -1
- package/dist/{defaults-C6J7yL4O.js.map → defaults-a9TelDhJ.js.map} +1 -1
- package/dist/emit-BSetXzDV.js +69 -0
- package/dist/emit-BSetXzDV.js.map +1 -0
- package/dist/{endpoints-DnIX0pZH.js → endpoints-BVMYm9F4.js} +3 -3
- package/dist/{endpoints-DnIX0pZH.js.map → endpoints-BVMYm9F4.js.map} +1 -1
- package/dist/{env-api-key-BWdKpGEr.js → env-api-key-BP-NFFXt.js} +3 -5
- package/dist/{env-api-key-BWdKpGEr.js.map → env-api-key-BP-NFFXt.js.map} +1 -1
- package/dist/env-api-key-CaWEDj_9.js +2 -0
- package/dist/{environment-DZYUSCpV.js → environment-BATlQxcQ.js} +5 -10
- package/dist/{environment-DZYUSCpV.js.map → environment-BATlQxcQ.js.map} +1 -1
- package/dist/environment-CwD8OPYj.js +2 -0
- package/dist/{errors-ddDE6MbA.js → errors-C2domSkK.js} +3 -67
- package/dist/errors-C2domSkK.js.map +1 -0
- package/dist/file-C5JMsyR1.js +2 -0
- package/dist/{file-dEhr-l3n.js → file-DcRKycV8.js} +3 -3
- package/dist/{file-dEhr-l3n.js.map → file-DcRKycV8.js.map} +1 -1
- package/dist/{file-watcher-CPM-Nn9L.js → file-watcher-Brq2nnHV.js} +2 -2
- package/dist/{file-watcher-CPM-Nn9L.js.map → file-watcher-Brq2nnHV.js.map} +1 -1
- package/dist/{gateway-CVU1G0H3.js → gateway-C9-GlLO9.js} +8 -7
- package/dist/{gateway-CVU1G0H3.js.map → gateway-C9-GlLO9.js.map} +1 -1
- package/dist/{headless-ui-pY2xB5ug.js → headless-ui-CqI2gXlD.js} +2 -2
- package/dist/{headless-ui-pY2xB5ug.js.map → headless-ui-CqI2gXlD.js.map} +1 -1
- package/dist/{interactive-BOGlinZe.js → interactive-B_Ti_CsS.js} +3 -3
- package/dist/{interactive-BOGlinZe.js.map → interactive-B_Ti_CsS.js.map} +1 -1
- package/dist/{logging-ui-CUxNYF2t.js → logging-ui-Dza4D0MK.js} +5 -5
- package/dist/{logging-ui-CUxNYF2t.js.map → logging-ui-Dza4D0MK.js.map} +1 -1
- package/dist/main-CgKxbwMP.js +1396 -0
- package/dist/main-CgKxbwMP.js.map +1 -0
- package/dist/{mcp-JcsBBnWv.js → mcp-DnAyW0-w.js} +4 -4
- package/dist/{mcp-JcsBBnWv.js.map → mcp-DnAyW0-w.js.map} +1 -1
- package/dist/{mcp-prompt-streaming-B-p_Pvb2.js → mcp-prompt-streaming-DDWQ-FCA.js} +6 -6
- package/dist/{mcp-prompt-streaming-B-p_Pvb2.js.map → mcp-prompt-streaming-DDWQ-FCA.js.map} +1 -1
- package/dist/{mint-failure-CZFZpHpW.js → mint-failure-u8xQKNOn.js} +23 -22
- package/dist/{mint-failure-CZFZpHpW.js.map → mint-failure-u8xQKNOn.js.map} +1 -1
- package/dist/{models-DnAY67Lg.js → models-WcwUnSKb.js} +2 -2
- package/dist/{models-DnAY67Lg.js.map → models-WcwUnSKb.js.map} +1 -1
- package/dist/{non-interactive-DR1CWXY1.js → non-interactive-DyVC1AGc.js} +4 -3
- package/dist/{non-interactive-DR1CWXY1.js.map → non-interactive-DyVC1AGc.js.map} +1 -1
- package/dist/{oauth-session-CQTFOk-I.js → oauth-session-DYM2Hrl2.js} +2 -2
- package/dist/{oauth-session-CQTFOk-I.js.map → oauth-session-DYM2Hrl2.js.map} +1 -1
- package/dist/{orchestrator-tools-CBdt-SXO.js → orchestrator-tools-DbgNBFGA.js} +3 -3
- package/dist/{orchestrator-tools-CBdt-SXO.js.map → orchestrator-tools-DbgNBFGA.js.map} +1 -1
- package/dist/{package-json-DY70kkNp.js → package-json-Atg34fa4.js} +2 -8
- package/dist/{package-json-DY70kkNp.js.map → package-json-Atg34fa4.js.map} +1 -1
- package/dist/package-json-D8Ga03tK.js +2 -0
- package/dist/{pi-BtqcLX3y.js → pi-8KsYsPmM.js} +17 -16
- package/dist/{pi-BtqcLX3y.js.map → pi-8KsYsPmM.js.map} +1 -1
- package/dist/{playground-Dz-ZVjBk.js → playground-BlYRBMj7.js} +11 -11
- package/dist/{playground-Dz-ZVjBk.js.map → playground-BlYRBMj7.js.map} +1 -1
- package/dist/{posthog-6Uqw_Eux.js → posthog-Bt0Aplb8.js} +3 -8
- package/dist/{posthog-6Uqw_Eux.js.map → posthog-Bt0Aplb8.js.map} +1 -1
- package/dist/posthog-CUgCEhH0.js +2 -0
- package/dist/{provisioning-Zf7yPcEs.js → provisioning-Bo2NRxFz.js} +5 -11
- package/dist/{provisioning-Zf7yPcEs.js.map → provisioning-Bo2NRxFz.js.map} +1 -1
- package/dist/provisioning-OGFGlzf7.js +2 -0
- package/dist/{queue-tools-B7_aCyd-.js → queue-tools-C8mXOh7e.js} +3 -3
- package/dist/{queue-tools-B7_aCyd-.js.map → queue-tools-C8mXOh7e.js.map} +1 -1
- package/dist/registry-C06e_x-p.js +2 -0
- package/dist/{registry-B6iLGorH.js → registry-CD5RQt38.js} +7 -9
- package/dist/{registry-B6iLGorH.js.map → registry-CD5RQt38.js.map} +1 -1
- package/dist/run-agent-legacy-ChacZImU.js +2 -0
- package/dist/{run-agent-legacy-6WcYMaR9.js → run-agent-legacy-DOlM6DQu.js} +20 -21
- package/dist/{run-agent-legacy-6WcYMaR9.js.map → run-agent-legacy-DOlM6DQu.js.map} +1 -1
- package/dist/runners-ov3EXF1n.js +834 -0
- package/dist/runners-ov3EXF1n.js.map +1 -0
- package/dist/{security-lkBwWbrK.js → security-C71pkwqH.js} +5 -5
- package/dist/{security-lkBwWbrK.js.map → security-C71pkwqH.js.map} +1 -1
- package/dist/{setup-utils-dCViCC6s.js → setup-utils-B26Lf2yF.js} +11 -21
- package/dist/{setup-utils-dCViCC6s.js.map → setup-utils-B26Lf2yF.js.map} +1 -1
- package/dist/setup-utils-CVS86NYR.js +2 -0
- package/dist/{start-tui-oCWMgsDP.js → start-tui-Bo2L0bar.js} +28 -26
- package/dist/{start-tui-oCWMgsDP.js.map → start-tui-Bo2L0bar.js.map} +1 -1
- package/dist/{steps-gx0Pq4I4.js → steps-BPcg3_3Q.js} +7 -7
- package/dist/{steps-gx0Pq4I4.js.map → steps-BPcg3_3Q.js.map} +1 -1
- package/dist/{store-xYy_6Ruv.js → store-BTzcvRGT.js} +11 -17
- package/dist/{store-xYy_6Ruv.js.map → store-BTzcvRGT.js.map} +1 -1
- package/dist/store-BpqySPXm.js +5 -0
- package/dist/{subagent-Bfy0sfhU.js → subagent-D1TrTchE.js} +3 -3
- package/dist/{subagent-Bfy0sfhU.js.map → subagent-D1TrTchE.js.map} +1 -1
- package/dist/{task-BOfw7GPc.js → task-DqRlQBfs.js} +18 -16
- package/dist/task-DqRlQBfs.js.map +1 -0
- package/dist/{task-stream-kYlm9qd0.js → task-stream-Bw6JaLCX.js} +8 -8
- package/dist/{task-stream-kYlm9qd0.js.map → task-stream-Bw6JaLCX.js.map} +1 -1
- package/dist/{tasks-Bbv_lOBG.js → tasks-ChWufsdO.js} +1 -1
- package/dist/{tasks-Bbv_lOBG.js.map → tasks-ChWufsdO.js.map} +1 -1
- package/dist/{telemetry-Bpv-Mh5B.js → telemetry-B7zJdRmA.js} +3 -3
- package/dist/{telemetry-Bpv-Mh5B.js.map → telemetry-B7zJdRmA.js.map} +1 -1
- package/dist/{terminal-D07EQ2y0.js → terminal-zgkEfoCL.js} +15 -15
- package/dist/{terminal-D07EQ2y0.js.map → terminal-zgkEfoCL.js.map} +1 -1
- package/dist/{tools-Cc41af5W.js → tools-CkJ2y9Wt.js} +9 -9
- package/dist/tools-CkJ2y9Wt.js.map +1 -0
- package/dist/{ui-CGXtC7Vn.js → ui-Q6-RvERF.js} +3 -3
- package/dist/{ui-CGXtC7Vn.js.map → ui-Q6-RvERF.js.map} +1 -1
- package/dist/{version-Cadof0Z_.js → version-CHhJjz_H.js} +2 -2
- package/dist/{version-Cadof0Z_.js.map → version-CHhJjz_H.js.map} +1 -1
- package/dist/{wizard-abort-HDbP3YcS.js → wizard-abort-BPdv07np.js} +7 -6
- package/dist/{wizard-abort-HDbP3YcS.js.map → wizard-abort-BPdv07np.js.map} +1 -1
- package/dist/wizard-abort-Br1qMDNa.js +3 -0
- package/dist/{wizard-ui-DoyOZxK8.js → wizard-ui-DVqrYaOM.js} +1 -1
- package/dist/{wizard-ui-DoyOZxK8.js.map → wizard-ui-DVqrYaOM.js.map} +1 -1
- package/dist/{yara-hooks-DCfROpZt.js → yara-hooks-BnxejtXK.js} +5 -5
- package/dist/{yara-hooks-DCfROpZt.js.map → yara-hooks-BnxejtXK.js.map} +1 -1
- package/dist/yara-hooks-DSuExwb4.js +2 -0
- package/package.json +1 -1
- package/dist/agent-interface-BXvmAm4c.js.map +0 -1
- package/dist/analytics-DxwRi2Hf.js +0 -2
- package/dist/errors-ddDE6MbA.js.map +0 -1
- package/dist/rolldown-runtime-B_-DWIq7.js +0 -15
- package/dist/task-BOfw7GPc.js.map +0 -1
- package/dist/tools-Cc41af5W.js.map +0 -1
- package/dist/wizard-abort-Y-Ip2K4z.js +0 -3
- package/dist/yara-hooks-B49RIk5d.js +0 -2
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { $ as wordWrap, it as Icons, rt as Colors } from "./mint-failure-u8xQKNOn.js";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import Fuse from "fuse.js";
|
|
6
|
+
//#region src/ui/tui/primitives/picker-filter.ts
|
|
7
|
+
/**
|
|
8
|
+
* Filter-as-you-type matching for PickerMenu. Split out from the component so the
|
|
9
|
+
* ranking is testable on its own — the component only decides when to call it.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Fuzzy fallback config. `ignoreLocation` because a term matching at the end of a
|
|
13
|
+
* description is as good as one at the start, and Fuse's default distance window
|
|
14
|
+
* would score the late match away. 0.5 is the loosest threshold that still bottoms
|
|
15
|
+
* out: below it `gthb` misses `GitHub` entirely, which is the whole point of having
|
|
16
|
+
* a fuzzy pass; above it the tail grows without surfacing anything new at the top.
|
|
17
|
+
*/
|
|
18
|
+
const FUSE_OPTIONS = {
|
|
19
|
+
keys: [
|
|
20
|
+
{
|
|
21
|
+
name: "label",
|
|
22
|
+
weight: 3
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "hint",
|
|
26
|
+
weight: 1
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "description",
|
|
30
|
+
weight: 1
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
ignoreLocation: true,
|
|
34
|
+
threshold: .5,
|
|
35
|
+
includeScore: true
|
|
36
|
+
};
|
|
37
|
+
function buildPickerIndex(options) {
|
|
38
|
+
return new Fuse(options, FUSE_OPTIONS);
|
|
39
|
+
}
|
|
40
|
+
function haystack(option) {
|
|
41
|
+
return `${option.label} ${option.hint ?? ""} ${option.description ?? ""}`.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Rank options against a query, best first, or `null` for an empty query.
|
|
45
|
+
*
|
|
46
|
+
* Two passes, literal before fuzzy. Anything that literally contains every
|
|
47
|
+
* whitespace-separated term wins outright, in the list's own order — that covers how
|
|
48
|
+
* people actually type (`git iss` → `GitHub issues`) and keeps the common case as
|
|
49
|
+
* tight as a plain substring filter, which fuzzy matching on its own is not: Fuse at
|
|
50
|
+
* a threshold loose enough to forgive `gthb` also lets a third of the list through
|
|
51
|
+
* on `replay`.
|
|
52
|
+
*
|
|
53
|
+
* Only when nothing matches literally does Fuse get a turn, so a typo degrades to a
|
|
54
|
+
* ranked guess instead of an empty list. Terms are AND-ed there too, each matched
|
|
55
|
+
* fuzzily on its own — Fuse's bitap pass over the whole query would otherwise need
|
|
56
|
+
* `git iss` to be a near-edit of the text it's matching. Scores are distances (0 is
|
|
57
|
+
* exact) and sum across terms, hence the ascending sort; ties keep the order the
|
|
58
|
+
* first term put them in, since the sort is stable.
|
|
59
|
+
*/
|
|
60
|
+
function rankOptions(fuse, options, query) {
|
|
61
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
62
|
+
if (!terms.length) return null;
|
|
63
|
+
const literal = options.filter((option) => terms.every((term) => haystack(option).includes(term)));
|
|
64
|
+
if (literal.length) return literal;
|
|
65
|
+
let surviving = null;
|
|
66
|
+
for (const term of terms) {
|
|
67
|
+
const hits = new Map(fuse.search(term).map(({ item, score }) => [item, score ?? 0]));
|
|
68
|
+
if (surviving === null) {
|
|
69
|
+
surviving = hits;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const [option, total] of surviving) {
|
|
73
|
+
const score = hits.get(option);
|
|
74
|
+
if (score === void 0) surviving.delete(option);
|
|
75
|
+
else surviving.set(option, total + score);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...surviving ?? []].sort(([, a], [, b]) => a - b).map(([option]) => option);
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/ui/tui/primitives/PromptLabel.tsx
|
|
82
|
+
/**
|
|
83
|
+
* PromptLabel — Compact inline label for input prompts.
|
|
84
|
+
*
|
|
85
|
+
* Renders: [!] message
|
|
86
|
+
* where [!] is black text on accent background.
|
|
87
|
+
*/
|
|
88
|
+
const PromptLabel = ({ message }) => {
|
|
89
|
+
if (!message) return null;
|
|
90
|
+
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, {
|
|
91
|
+
bold: true,
|
|
92
|
+
color: Colors.accent,
|
|
93
|
+
children: [" ", message]
|
|
94
|
+
}) });
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/ui/tui/primitives/ConfirmButton.tsx
|
|
98
|
+
/**
|
|
99
|
+
* ConfirmButton — the confirm row used to submit a selection.
|
|
100
|
+
*
|
|
101
|
+
* Pure render. Multi-select menus (PickerMenu mode="multi", GroupedPickerMenu)
|
|
102
|
+
* append this below their options as the final focusable row: the user toggles
|
|
103
|
+
* options with enter, then arrows down onto this row and presses enter to
|
|
104
|
+
* submit. This replaces the older "enter anywhere submits" pattern, which
|
|
105
|
+
* confused people who expected enter to toggle the focused item.
|
|
106
|
+
*
|
|
107
|
+
* Renders flat, mirroring the option rows — a focus triangle and the label,
|
|
108
|
+
* accent and bold when focused, dimmed otherwise — so it lines up under the
|
|
109
|
+
* options instead of sitting in a separate boxed target.
|
|
110
|
+
*/
|
|
111
|
+
const ConfirmButton = ({ label = "Confirm", focused, count }) => {
|
|
112
|
+
const text = count && count > 0 ? `${label} (${count})` : label;
|
|
113
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
114
|
+
color: focused ? Colors.accent : void 0,
|
|
115
|
+
bold: focused,
|
|
116
|
+
dimColor: !focused,
|
|
117
|
+
children: [
|
|
118
|
+
focused ? Icons.triangleSmallRight : " ",
|
|
119
|
+
" ",
|
|
120
|
+
text
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/ui/tui/hooks/useStdoutDimensions.ts
|
|
126
|
+
/**
|
|
127
|
+
* useStdoutDimensions — Returns [columns, rows] and re-renders on terminal resize.
|
|
128
|
+
*
|
|
129
|
+
* Ink's useStdout() does not subscribe to resize events, so layout only updates
|
|
130
|
+
* when something else causes a re-render. This hook listens to the stream's
|
|
131
|
+
* 'resize' event (Node TTY) and updates state so the component re-renders
|
|
132
|
+
* with the new dimensions.
|
|
133
|
+
*/
|
|
134
|
+
function useStdoutDimensions() {
|
|
135
|
+
const { stdout } = useStdout();
|
|
136
|
+
const [size, setSize] = useState(() => [stdout.columns || 80, stdout.rows || 24]);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
setSize([stdout.columns || 80, stdout.rows || 24]);
|
|
139
|
+
const stream = stdout;
|
|
140
|
+
if (typeof stream.on !== "function") return;
|
|
141
|
+
const onResize = () => {
|
|
142
|
+
const c = stdout.columns || 80;
|
|
143
|
+
const r = stdout.rows || 24;
|
|
144
|
+
if (c > 0 && r > 0) setSize([c, r]);
|
|
145
|
+
};
|
|
146
|
+
stream.on("resize", onResize);
|
|
147
|
+
return () => {
|
|
148
|
+
stream.off?.("resize", onResize);
|
|
149
|
+
};
|
|
150
|
+
}, [stdout]);
|
|
151
|
+
return size;
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/ui/tui/hooks/keyboard-hints-utils.ts
|
|
155
|
+
/** Default priorities by key type. */
|
|
156
|
+
const DEFAULT_PRIORITY = {
|
|
157
|
+
["upArrow"]: 0,
|
|
158
|
+
["downArrow"]: 0,
|
|
159
|
+
["leftArrow"]: 1,
|
|
160
|
+
["rightArrow"]: 1,
|
|
161
|
+
["space"]: 10,
|
|
162
|
+
["printable"]: 12,
|
|
163
|
+
["backspace"]: 13,
|
|
164
|
+
["escape"]: 20,
|
|
165
|
+
["return"]: 21
|
|
166
|
+
};
|
|
167
|
+
/** Get the default display priority for a key match. */
|
|
168
|
+
function getDefaultPriority(match) {
|
|
169
|
+
return DEFAULT_PRIORITY[Array.isArray(match) ? match[0] : match] ?? 15;
|
|
170
|
+
}
|
|
171
|
+
/** Check if a KeyMatchOrChar matches the given input string and key flags. */
|
|
172
|
+
function matchesKey(m, input, key) {
|
|
173
|
+
switch (m) {
|
|
174
|
+
case "upArrow": return !!key.upArrow;
|
|
175
|
+
case "downArrow": return !!key.downArrow;
|
|
176
|
+
case "leftArrow": return !!key.leftArrow;
|
|
177
|
+
case "rightArrow": return !!key.rightArrow;
|
|
178
|
+
case "return": return !!key.return;
|
|
179
|
+
case "escape": return !!key.escape;
|
|
180
|
+
case "space": return input === " ";
|
|
181
|
+
case "backspace": return !!key.backspace || !!key.delete;
|
|
182
|
+
case "printable": return input.length === 1 && input >= " " && input !== "" && Object.entries(key).every(([flag, on]) => !on || flag === "shift");
|
|
183
|
+
default: return input === m;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Serialize hints for comparison. */
|
|
187
|
+
function hintsKey(hints) {
|
|
188
|
+
return hints.map((h) => `${h.label}:${h.action}`).join("|");
|
|
189
|
+
}
|
|
190
|
+
/** Deduplicate hints by label+action and sort by priority. */
|
|
191
|
+
function deduplicateAndSort(hints) {
|
|
192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
193
|
+
const deduped = [];
|
|
194
|
+
for (const hint of hints) {
|
|
195
|
+
const k = `${hint.label}:${hint.action}`;
|
|
196
|
+
if (!seen.has(k)) {
|
|
197
|
+
seen.add(k);
|
|
198
|
+
deduped.push(hint);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
deduped.sort((a, b) => a.priority - b.priority);
|
|
202
|
+
return deduped;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/ui/tui/hooks/useKeyboardHints.tsx
|
|
206
|
+
/**
|
|
207
|
+
* KeyboardHintsProvider — Context for collecting and displaying keyboard hints.
|
|
208
|
+
*
|
|
209
|
+
* Input components register their hints via useKeyBindings. The provider
|
|
210
|
+
* flattens, deduplicates, and sorts them. The hints bar stays visible for as
|
|
211
|
+
* long as a screen has registered hints — it never auto-dismisses.
|
|
212
|
+
*/
|
|
213
|
+
const KeyboardHintsContext = createContext({
|
|
214
|
+
register: () => void 0,
|
|
215
|
+
unregister: () => void 0,
|
|
216
|
+
hints: []
|
|
217
|
+
});
|
|
218
|
+
const useKeyboardHintsContext = () => useContext(KeyboardHintsContext);
|
|
219
|
+
const KeyboardHintsProvider = ({ children }) => {
|
|
220
|
+
const registrationsRef = useRef(/* @__PURE__ */ new Map());
|
|
221
|
+
const [hints, setHints] = useState([]);
|
|
222
|
+
const prevHintsKeyRef = useRef("");
|
|
223
|
+
const recompute = useCallback(() => {
|
|
224
|
+
const all = [];
|
|
225
|
+
for (const h of registrationsRef.current.values()) all.push(...h);
|
|
226
|
+
const deduped = deduplicateAndSort(all);
|
|
227
|
+
const newKey = hintsKey(deduped);
|
|
228
|
+
if (newKey !== prevHintsKeyRef.current) {
|
|
229
|
+
prevHintsKeyRef.current = newKey;
|
|
230
|
+
setHints(deduped);
|
|
231
|
+
}
|
|
232
|
+
}, []);
|
|
233
|
+
const register = useCallback((id, h) => {
|
|
234
|
+
registrationsRef.current.set(id, h);
|
|
235
|
+
recompute();
|
|
236
|
+
}, [recompute]);
|
|
237
|
+
const unregister = useCallback((id) => {
|
|
238
|
+
registrationsRef.current.delete(id);
|
|
239
|
+
recompute();
|
|
240
|
+
}, [recompute]);
|
|
241
|
+
return /* @__PURE__ */ jsx(KeyboardHintsContext.Provider, {
|
|
242
|
+
value: {
|
|
243
|
+
register,
|
|
244
|
+
unregister,
|
|
245
|
+
hints
|
|
246
|
+
},
|
|
247
|
+
children
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/ui/tui/hooks/useKeyBindings.ts
|
|
252
|
+
/**
|
|
253
|
+
* useKeyBindings — Declarative keyboard input + automatic hint registration.
|
|
254
|
+
*
|
|
255
|
+
* Replaces raw `useInput` in input components. Define bindings as data;
|
|
256
|
+
* the hook wires up the Ink input handler AND registers hints in the
|
|
257
|
+
* KeyboardHintsProvider. One source of truth for keys and their labels.
|
|
258
|
+
*/
|
|
259
|
+
/**
|
|
260
|
+
* Declarative key bindings hook. Replaces `useInput` in input components.
|
|
261
|
+
* Registers hints automatically with the KeyboardHintsProvider.
|
|
262
|
+
*
|
|
263
|
+
* @param id Unique identifier for this component's hints registration
|
|
264
|
+
* @param bindings Array of key binding definitions
|
|
265
|
+
*/
|
|
266
|
+
function useKeyBindings(id, bindings) {
|
|
267
|
+
const ctx = useKeyboardHintsContext();
|
|
268
|
+
const hintsRef = useRef("");
|
|
269
|
+
const hints = bindings.map((b) => ({
|
|
270
|
+
label: b.label,
|
|
271
|
+
action: b.action,
|
|
272
|
+
priority: b.priority ?? getDefaultPriority(b.match)
|
|
273
|
+
}));
|
|
274
|
+
const serialized = hints.map((h) => `${h.label}:${h.action}:${h.priority}`).join("|");
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (serialized !== hintsRef.current) {
|
|
277
|
+
hintsRef.current = serialized;
|
|
278
|
+
ctx.register(id, hints);
|
|
279
|
+
}
|
|
280
|
+
return () => ctx.unregister(id);
|
|
281
|
+
}, [id, serialized]);
|
|
282
|
+
useInput((input, key) => {
|
|
283
|
+
for (const binding of bindings) if ((Array.isArray(binding.match) ? binding.match : [binding.match]).some((m) => matchesKey(m, input, key))) {
|
|
284
|
+
binding.handler(input, key);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/ui/tui/primitives/PickerMenu.tsx
|
|
291
|
+
/**
|
|
292
|
+
* PickerMenu — Single and multi select.
|
|
293
|
+
* Single mode: custom renderer with small triangle indicator; enter selects.
|
|
294
|
+
* Multi mode: checkbox glyphs toggled with enter, plus a focusable
|
|
295
|
+
* Confirm button below the options. The cursor moves onto the button and
|
|
296
|
+
* enter submits — see MultiPickerMenu for the rationale.
|
|
297
|
+
*
|
|
298
|
+
* Key bindings are declared via useKeyBindings, which auto-registers
|
|
299
|
+
* hints in the KeyboardHintsBar.
|
|
300
|
+
*/
|
|
301
|
+
/**
|
|
302
|
+
* Step through a column's options in `dir`, wrapping, until an enabled
|
|
303
|
+
* option is found. Returns `from` unchanged if the column is entirely
|
|
304
|
+
* disabled.
|
|
305
|
+
*/
|
|
306
|
+
function stepEnabled(options, rows, from, dir) {
|
|
307
|
+
const colStart = Math.floor(from / rows) * rows;
|
|
308
|
+
const colLen = Math.min(rows, options.length - colStart);
|
|
309
|
+
let row = from % rows;
|
|
310
|
+
for (let i = 0; i < colLen; i++) {
|
|
311
|
+
row = (row + dir + colLen) % colLen;
|
|
312
|
+
const idx = colStart + row;
|
|
313
|
+
if (!options[idx]?.disabled) return idx;
|
|
314
|
+
}
|
|
315
|
+
return from;
|
|
316
|
+
}
|
|
317
|
+
/** First enabled option in [start, end), for initial focus and for entering
|
|
318
|
+
* a page from the Confirm button. */
|
|
319
|
+
function firstEnabled(options, start = 0, end = options.length) {
|
|
320
|
+
for (let i = start; i < end; i++) if (!options[i]?.disabled) return i;
|
|
321
|
+
return start;
|
|
322
|
+
}
|
|
323
|
+
/** Last enabled option in [start, end), for wrapping from the button onto
|
|
324
|
+
* the bottom of the page. */
|
|
325
|
+
function lastEnabled(options, start = 0, end = options.length) {
|
|
326
|
+
for (let i = end - 1; i >= start; i--) if (!options[i]?.disabled) return i;
|
|
327
|
+
return end - 1;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Rows the surrounding screen or overlay consumes above and below a picker —
|
|
331
|
+
* border + padding, title, prompt text, and the keyboard-hints bar. A
|
|
332
|
+
* deliberately generous estimate: overshooting just shows a few fewer rows,
|
|
333
|
+
* whereas undershooting lets a long list overflow the viewport (the bug this
|
|
334
|
+
* windowing guards against). Mirrors GroupedPickerMenu's budgeting.
|
|
335
|
+
*/
|
|
336
|
+
const CHROME_OVERHEAD = 13;
|
|
337
|
+
/**
|
|
338
|
+
* Max visual rows a picker renders regardless of terminal height. Without a
|
|
339
|
+
* ceiling a tall terminal lets a long list fill the whole viewport, which
|
|
340
|
+
* reads as a wall of options; ~12 rows keeps the menu scannable and leaves
|
|
341
|
+
* known breathing room above and below.
|
|
342
|
+
*/
|
|
343
|
+
const MAX_LIST_ROWS = 12;
|
|
344
|
+
/** Extra rows a multi-select adds below its options: marginTop + Confirm button. */
|
|
345
|
+
const CONFIRM_CHROME = 3;
|
|
346
|
+
/** Lists shorter than this never page — tall rows would split 2-3 options into pages of one. */
|
|
347
|
+
const MIN_COUNT_TO_PAGE = 5;
|
|
348
|
+
/** Width the multi-select wraps option descriptions to (matches the render). */
|
|
349
|
+
const DESCRIPTION_WIDTH = 56;
|
|
350
|
+
/**
|
|
351
|
+
* Pages a single-column option list to the terminal height. The visible page
|
|
352
|
+
* is derived from the focused index — no scroll state — so ↑/↓ flip pages as
|
|
353
|
+
* focus crosses a page edge and n/p jump a whole page. Pages hold a fixed
|
|
354
|
+
* option count sized to the tallest row (`rowCost`), trading a sparser page
|
|
355
|
+
* on mixed-height lists for arithmetic-only paging. Engages only for
|
|
356
|
+
* single-column pickers — multi-column grids already compress vertically.
|
|
357
|
+
*/
|
|
358
|
+
function usePickerViewport(count, rowCost, chromeBelow, enabled, focused) {
|
|
359
|
+
const [, termRows] = useStdoutDimensions();
|
|
360
|
+
const budget = Math.max(5, Math.min(termRows - CHROME_OVERHEAD - chromeBelow, MAX_LIST_ROWS));
|
|
361
|
+
const needsScroll = enabled && count >= MIN_COUNT_TO_PAGE && count * rowCost > budget;
|
|
362
|
+
const perPage = needsScroll ? Math.max(1, Math.floor((budget - 2) / rowCost)) : count;
|
|
363
|
+
const pageCount = Math.max(1, Math.ceil(count / perPage));
|
|
364
|
+
const start = Math.floor(focused / perPage) * perPage;
|
|
365
|
+
const end = Math.min(start + perPage, count);
|
|
366
|
+
return {
|
|
367
|
+
needsScroll,
|
|
368
|
+
start,
|
|
369
|
+
end,
|
|
370
|
+
hiddenAbove: start,
|
|
371
|
+
hiddenBelow: count - end,
|
|
372
|
+
pageStep: (f, dir) => (Math.floor(f / perPage) + dir + pageCount) % pageCount * perPage
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/** Lists at least this long get filtering. Shorter ones fit on a page or two,
|
|
376
|
+
* where a filter row costs more rows than it saves. */
|
|
377
|
+
const FILTER_MIN_OPTIONS = 10;
|
|
378
|
+
const PickerMenu = ({ message, options, mode = "single", centered = false, columns = 1, optionMarginBottom = 0, filterable, onSelect }) => {
|
|
379
|
+
const canFilter = filterable ?? options.length >= FILTER_MIN_OPTIONS;
|
|
380
|
+
const [filter, setFilter] = useState("");
|
|
381
|
+
const fuse = useMemo(() => canFilter ? buildPickerIndex(options) : null, [options, canFilter]);
|
|
382
|
+
const visible = useMemo(() => fuse ? rankOptions(fuse, options, filter) ?? options : options, [
|
|
383
|
+
fuse,
|
|
384
|
+
options,
|
|
385
|
+
filter
|
|
386
|
+
]);
|
|
387
|
+
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
388
|
+
const shared = {
|
|
389
|
+
message,
|
|
390
|
+
options: visible,
|
|
391
|
+
centered,
|
|
392
|
+
optionMarginBottom,
|
|
393
|
+
onSelect,
|
|
394
|
+
filter: canFilter ? filter : null,
|
|
395
|
+
setFilter,
|
|
396
|
+
totalCount: options.length
|
|
397
|
+
};
|
|
398
|
+
const key = String(filter);
|
|
399
|
+
if (mode === "multi") return /* @__PURE__ */ jsx(MultiPickerMenu, {
|
|
400
|
+
...shared,
|
|
401
|
+
columns,
|
|
402
|
+
allOptions: options,
|
|
403
|
+
selected,
|
|
404
|
+
setSelected
|
|
405
|
+
}, key);
|
|
406
|
+
return /* @__PURE__ */ jsx(SinglePickerMenu, {
|
|
407
|
+
...shared,
|
|
408
|
+
columns
|
|
409
|
+
}, key);
|
|
410
|
+
};
|
|
411
|
+
/**
|
|
412
|
+
* Backspace and free typing. Append these last: `Printable` matches almost any
|
|
413
|
+
* key, and bindings resolve in order, so anything specific has to be registered
|
|
414
|
+
* first to keep working. Both share a label/action so the hints bar shows one.
|
|
415
|
+
*/
|
|
416
|
+
function filterBindings(filter, setFilter) {
|
|
417
|
+
return [{
|
|
418
|
+
match: "backspace",
|
|
419
|
+
label: "type",
|
|
420
|
+
action: "filter",
|
|
421
|
+
handler: () => setFilter(filter.slice(0, -1))
|
|
422
|
+
}, {
|
|
423
|
+
match: "printable",
|
|
424
|
+
label: "type",
|
|
425
|
+
action: "filter",
|
|
426
|
+
handler: (input) => setFilter(filter + input)
|
|
427
|
+
}];
|
|
428
|
+
}
|
|
429
|
+
/** The filter row above a filterable list: what's typed, and how much it left. */
|
|
430
|
+
const FilterRow = ({ filter, shown, total }) => /* @__PURE__ */ jsx(Box, {
|
|
431
|
+
paddingLeft: 1,
|
|
432
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
433
|
+
dimColor: true,
|
|
434
|
+
children: filter ? `Filter: ${filter} (${shown} of ${total})` : `Type to filter ${total} options`
|
|
435
|
+
})
|
|
436
|
+
});
|
|
437
|
+
/** Custom single-select with triangle indicator and accent highlight. */
|
|
438
|
+
const SinglePickerMenu = ({ message, options, centered = false, columns = 1, optionMarginBottom = 0, filter, setFilter, totalCount, onSelect }) => {
|
|
439
|
+
const [focused, setFocused] = useState(() => firstEnabled(options));
|
|
440
|
+
const rows = Math.ceil(options.length / columns);
|
|
441
|
+
const viewport = usePickerViewport(options.length, 1 + optionMarginBottom, filter !== null ? 1 : 0, columns === 1, focused);
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
if (focused >= options.length || options[focused]?.disabled) setFocused(firstEnabled(options));
|
|
444
|
+
}, [options, focused]);
|
|
445
|
+
const bindings = [
|
|
446
|
+
{
|
|
447
|
+
match: ["upArrow", "downArrow"],
|
|
448
|
+
label: "↑↓",
|
|
449
|
+
action: "navigate",
|
|
450
|
+
handler: (_input, key) => {
|
|
451
|
+
setFocused(stepEnabled(options, rows, focused, key.upArrow ? -1 : 1));
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
...viewport.needsScroll && filter === null ? [{
|
|
455
|
+
match: ["n", "p"],
|
|
456
|
+
label: "n/p",
|
|
457
|
+
action: "page",
|
|
458
|
+
handler: (input) => {
|
|
459
|
+
const target = viewport.pageStep(focused, input === "n" ? 1 : -1);
|
|
460
|
+
setFocused(options[target]?.disabled ? stepEnabled(options, rows, target, 1) : target);
|
|
461
|
+
}
|
|
462
|
+
}] : [],
|
|
463
|
+
{
|
|
464
|
+
match: "return",
|
|
465
|
+
label: "enter",
|
|
466
|
+
action: "select",
|
|
467
|
+
handler: () => {
|
|
468
|
+
const selected = options[focused];
|
|
469
|
+
if (selected && !selected.disabled) onSelect(selected.value);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
];
|
|
473
|
+
if (columns > 1) bindings.splice(1, 0, {
|
|
474
|
+
match: ["leftArrow", "rightArrow"],
|
|
475
|
+
label: "←→",
|
|
476
|
+
action: "navigate",
|
|
477
|
+
handler: (_input, key) => {
|
|
478
|
+
const col = Math.floor(focused / rows);
|
|
479
|
+
const row = focused % rows;
|
|
480
|
+
let next = focused;
|
|
481
|
+
if (key.leftArrow) {
|
|
482
|
+
const prevCol = col > 0 ? col - 1 : columns - 1;
|
|
483
|
+
next = Math.min(prevCol * rows + row, options.length - 1);
|
|
484
|
+
}
|
|
485
|
+
if (key.rightArrow) {
|
|
486
|
+
const nextCol = col < columns - 1 ? col + 1 : 0;
|
|
487
|
+
next = Math.min(nextCol * rows + row, options.length - 1);
|
|
488
|
+
}
|
|
489
|
+
if (options[next]?.disabled) next = stepEnabled(options, rows, next, 1);
|
|
490
|
+
setFocused(next);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
if (filter !== null) bindings.push(...filterBindings(filter, setFilter));
|
|
494
|
+
useKeyBindings("single-picker", bindings);
|
|
495
|
+
const columnArrays = [];
|
|
496
|
+
for (let c = 0; c < columns; c++) columnArrays.push(options.slice(c * rows, c * rows + rows));
|
|
497
|
+
const align = centered ? "center" : void 0;
|
|
498
|
+
const renderOption = (opt, flatIdx) => {
|
|
499
|
+
const isFocused = flatIdx === focused;
|
|
500
|
+
const base = opt.hint ? `${opt.label} (${opt.hint})` : opt.label;
|
|
501
|
+
const label = opt.indent ? ` ${base}` : base;
|
|
502
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
503
|
+
gap: 1,
|
|
504
|
+
marginBottom: optionMarginBottom,
|
|
505
|
+
children: [
|
|
506
|
+
/* @__PURE__ */ jsx(Text, {
|
|
507
|
+
color: isFocused ? Colors.accent : void 0,
|
|
508
|
+
dimColor: !isFocused,
|
|
509
|
+
children: isFocused && !opt.header ? Icons.triangleSmallRight : " "
|
|
510
|
+
}),
|
|
511
|
+
opt.icon && /* @__PURE__ */ jsx(Text, {
|
|
512
|
+
color: opt.icon.color,
|
|
513
|
+
children: opt.icon.glyph
|
|
514
|
+
}),
|
|
515
|
+
/* @__PURE__ */ jsx(Text, {
|
|
516
|
+
color: opt.header ? void 0 : opt.disabled ? Colors.muted : isFocused ? Colors.accent : void 0,
|
|
517
|
+
bold: opt.header || isFocused && !opt.disabled,
|
|
518
|
+
dimColor: !opt.header && (!isFocused || opt.disabled),
|
|
519
|
+
children: label
|
|
520
|
+
})
|
|
521
|
+
]
|
|
522
|
+
}, flatIdx);
|
|
523
|
+
};
|
|
524
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
525
|
+
flexDirection: "column",
|
|
526
|
+
alignItems: align,
|
|
527
|
+
children: [
|
|
528
|
+
/* @__PURE__ */ jsx(PromptLabel, { message }),
|
|
529
|
+
filter !== null && /* @__PURE__ */ jsx(FilterRow, {
|
|
530
|
+
filter,
|
|
531
|
+
shown: options.length,
|
|
532
|
+
total: totalCount
|
|
533
|
+
}),
|
|
534
|
+
filter && options.length === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
535
|
+
dimColor: true,
|
|
536
|
+
children: "No options match. Backspace to widen the filter."
|
|
537
|
+
}) : viewport.needsScroll ? /* @__PURE__ */ jsxs(Box, {
|
|
538
|
+
flexDirection: "column",
|
|
539
|
+
children: [
|
|
540
|
+
/* @__PURE__ */ jsx(Text, {
|
|
541
|
+
dimColor: true,
|
|
542
|
+
children: viewport.hiddenAbove > 0 ? `↑ ${viewport.hiddenAbove} more${filter === null ? " [P] for previous page" : ""}` : " "
|
|
543
|
+
}),
|
|
544
|
+
options.slice(viewport.start, viewport.end).map((opt, relIdx) => renderOption(opt, viewport.start + relIdx)),
|
|
545
|
+
/* @__PURE__ */ jsx(Text, {
|
|
546
|
+
dimColor: true,
|
|
547
|
+
children: viewport.hiddenBelow > 0 ? `↓ ${viewport.hiddenBelow} more${filter === null ? " [N] for next page" : ""}` : " "
|
|
548
|
+
})
|
|
549
|
+
]
|
|
550
|
+
}) : /* @__PURE__ */ jsx(Box, {
|
|
551
|
+
flexDirection: "row",
|
|
552
|
+
gap: 4,
|
|
553
|
+
children: columnArrays.map((colOpts, colIdx) => /* @__PURE__ */ jsx(Box, {
|
|
554
|
+
flexDirection: "column",
|
|
555
|
+
children: colOpts.map((opt, rowIdx) => renderOption(opt, colIdx * rows + rowIdx))
|
|
556
|
+
}, colIdx))
|
|
557
|
+
})
|
|
558
|
+
]
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
/**
|
|
562
|
+
* Custom multi-select with checkbox glyphs and accent highlight.
|
|
563
|
+
*
|
|
564
|
+
* Interaction model (shared with GroupedPickerMenu):
|
|
565
|
+
* - \u2191\u2193 move the cursor through the options AND onto the Confirm button,
|
|
566
|
+
* which lives just past the last option.
|
|
567
|
+
* - enter toggles the focused option (no more "space toggles but enter
|
|
568
|
+
* advances" split that tripped people up). Space is kept as an
|
|
569
|
+
* undocumented alias, but the hints bar advertises only enter.
|
|
570
|
+
* - moving onto the Confirm button and pressing enter submits the
|
|
571
|
+
* current selection.
|
|
572
|
+
*/
|
|
573
|
+
const MultiPickerMenu = ({ message, options, centered = false, columns = 1, optionMarginBottom = 0, filter, setFilter, totalCount, allOptions, selected, setSelected, onSelect }) => {
|
|
574
|
+
const [focused, setFocused] = useState(() => firstEnabled(options));
|
|
575
|
+
const [onButton, setOnButton] = useState(false);
|
|
576
|
+
const rows = Math.ceil(options.length / columns);
|
|
577
|
+
const rowCost = options.reduce((max, opt) => Math.max(max, 1 + optionMarginBottom + (opt.description ? wordWrap(opt.description, DESCRIPTION_WIDTH).length : 0)), 1);
|
|
578
|
+
const viewport = usePickerViewport(options.length, rowCost, CONFIRM_CHROME + (filter !== null ? 1 : 0), columns === 1, focused);
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
if (focused >= options.length || options[focused]?.disabled) setFocused(firstEnabled(options));
|
|
581
|
+
}, [options, focused]);
|
|
582
|
+
const confirm = () => {
|
|
583
|
+
onSelect(allOptions.filter((option) => selected.has(option.label)).map((option) => option.value));
|
|
584
|
+
};
|
|
585
|
+
const bindings = [
|
|
586
|
+
{
|
|
587
|
+
match: ["upArrow", "downArrow"],
|
|
588
|
+
label: "↑↓",
|
|
589
|
+
action: "navigate",
|
|
590
|
+
handler: (_input, key) => {
|
|
591
|
+
const dir = key.upArrow ? -1 : 1;
|
|
592
|
+
if (onButton) {
|
|
593
|
+
setOnButton(false);
|
|
594
|
+
setFocused(dir === -1 ? lastEnabled(options, viewport.start, viewport.end) : firstEnabled(options, viewport.start, viewport.end));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const col = Math.floor(focused / rows);
|
|
598
|
+
const row = focused % rows;
|
|
599
|
+
const colLen = Math.min(rows, options.length - col * rows);
|
|
600
|
+
let r = row + dir;
|
|
601
|
+
while (r >= 0 && r < colLen && options[col * rows + r]?.disabled) r += dir;
|
|
602
|
+
if (r >= 0 && r < colLen) setFocused(col * rows + r);
|
|
603
|
+
else setOnButton(true);
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
...viewport.needsScroll && filter === null ? [{
|
|
607
|
+
match: ["n", "p"],
|
|
608
|
+
label: "n/p",
|
|
609
|
+
action: "page",
|
|
610
|
+
handler: (input) => {
|
|
611
|
+
const target = viewport.pageStep(focused, input === "n" ? 1 : -1);
|
|
612
|
+
setOnButton(false);
|
|
613
|
+
setFocused(options[target]?.disabled ? stepEnabled(options, rows, target, 1) : target);
|
|
614
|
+
}
|
|
615
|
+
}] : [],
|
|
616
|
+
{
|
|
617
|
+
match: filter !== null ? "return" : ["space", "return"],
|
|
618
|
+
label: "enter",
|
|
619
|
+
action: "select",
|
|
620
|
+
handler: () => {
|
|
621
|
+
if (onButton) {
|
|
622
|
+
confirm();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const option = options[focused];
|
|
626
|
+
if (!option || option.disabled) return;
|
|
627
|
+
const label = option.label;
|
|
628
|
+
setSelected((prev) => {
|
|
629
|
+
const next = new Set(prev);
|
|
630
|
+
if (next.has(label)) {
|
|
631
|
+
next.delete(label);
|
|
632
|
+
return next;
|
|
633
|
+
}
|
|
634
|
+
if (option.exclusive) return new Set([label]);
|
|
635
|
+
for (const picked of next) if (allOptions.find((o) => o.label === picked)?.exclusive) next.delete(picked);
|
|
636
|
+
next.add(label);
|
|
637
|
+
return next;
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
];
|
|
642
|
+
if (columns > 1) bindings.splice(1, 0, {
|
|
643
|
+
match: ["leftArrow", "rightArrow"],
|
|
644
|
+
label: "←→",
|
|
645
|
+
action: "navigate",
|
|
646
|
+
handler: (_input, key) => {
|
|
647
|
+
if (onButton) return;
|
|
648
|
+
const col = Math.floor(focused / rows);
|
|
649
|
+
const row = focused % rows;
|
|
650
|
+
let next = focused;
|
|
651
|
+
if (key.leftArrow) {
|
|
652
|
+
const prevCol = col > 0 ? col - 1 : columns - 1;
|
|
653
|
+
next = Math.min(prevCol * rows + row, options.length - 1);
|
|
654
|
+
}
|
|
655
|
+
if (key.rightArrow) {
|
|
656
|
+
const nextCol = col < columns - 1 ? col + 1 : 0;
|
|
657
|
+
next = Math.min(nextCol * rows + row, options.length - 1);
|
|
658
|
+
}
|
|
659
|
+
if (options[next]?.disabled) next = stepEnabled(options, rows, next, 1);
|
|
660
|
+
setFocused(next);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
if (filter !== null) bindings.push(...filterBindings(filter, setFilter));
|
|
664
|
+
useKeyBindings("multi-picker", bindings);
|
|
665
|
+
const columnArrays = [];
|
|
666
|
+
for (let c = 0; c < columns; c++) columnArrays.push(options.slice(c * rows, c * rows + rows));
|
|
667
|
+
const renderOption = (opt, flatIdx) => {
|
|
668
|
+
const isFocused = !onButton && flatIdx === focused;
|
|
669
|
+
const isSelected = selected.has(opt.label);
|
|
670
|
+
const label = opt.hint ? `${opt.label} (${opt.hint})` : opt.label;
|
|
671
|
+
const checkbox = isSelected ? Icons.squareFilled : Icons.squareOpen;
|
|
672
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
673
|
+
flexDirection: "column",
|
|
674
|
+
marginBottom: optionMarginBottom,
|
|
675
|
+
children: [/* @__PURE__ */ jsxs(Box, {
|
|
676
|
+
gap: 1,
|
|
677
|
+
children: [
|
|
678
|
+
/* @__PURE__ */ jsx(Text, {
|
|
679
|
+
color: isSelected ? "white" : Colors.muted,
|
|
680
|
+
dimColor: !isFocused && !isSelected,
|
|
681
|
+
children: checkbox
|
|
682
|
+
}),
|
|
683
|
+
opt.icon && /* @__PURE__ */ jsx(Text, {
|
|
684
|
+
color: opt.icon.color,
|
|
685
|
+
children: opt.icon.glyph
|
|
686
|
+
}),
|
|
687
|
+
/* @__PURE__ */ jsx(Text, {
|
|
688
|
+
color: opt.disabled ? Colors.muted : isFocused ? Colors.accent : void 0,
|
|
689
|
+
bold: isFocused && !opt.disabled,
|
|
690
|
+
dimColor: !isFocused || opt.disabled,
|
|
691
|
+
children: label
|
|
692
|
+
})
|
|
693
|
+
]
|
|
694
|
+
}), opt.description && /* @__PURE__ */ jsx(Box, {
|
|
695
|
+
marginLeft: 4,
|
|
696
|
+
width: DESCRIPTION_WIDTH,
|
|
697
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
698
|
+
dimColor: true,
|
|
699
|
+
wrap: "wrap",
|
|
700
|
+
children: opt.description
|
|
701
|
+
})
|
|
702
|
+
})]
|
|
703
|
+
}, flatIdx);
|
|
704
|
+
};
|
|
705
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
706
|
+
flexDirection: "column",
|
|
707
|
+
alignItems: centered ? "center" : void 0,
|
|
708
|
+
children: [
|
|
709
|
+
/* @__PURE__ */ jsx(PromptLabel, { message }),
|
|
710
|
+
filter !== null && /* @__PURE__ */ jsx(FilterRow, {
|
|
711
|
+
filter,
|
|
712
|
+
shown: options.length,
|
|
713
|
+
total: totalCount
|
|
714
|
+
}),
|
|
715
|
+
filter && options.length === 0 && /* @__PURE__ */ jsx(Text, {
|
|
716
|
+
dimColor: true,
|
|
717
|
+
children: "No options match. Backspace to widen the filter."
|
|
718
|
+
}),
|
|
719
|
+
viewport.needsScroll ? /* @__PURE__ */ jsxs(Box, {
|
|
720
|
+
flexDirection: "column",
|
|
721
|
+
marginLeft: centered ? 0 : 2,
|
|
722
|
+
marginTop: message ? 1 : 0,
|
|
723
|
+
children: [
|
|
724
|
+
/* @__PURE__ */ jsx(Text, {
|
|
725
|
+
dimColor: true,
|
|
726
|
+
children: viewport.hiddenAbove > 0 ? `↑ ${viewport.hiddenAbove} more${filter === null ? " [P] for previous page" : ""}` : " "
|
|
727
|
+
}),
|
|
728
|
+
options.slice(viewport.start, viewport.end).map((opt, relIdx) => renderOption(opt, viewport.start + relIdx)),
|
|
729
|
+
/* @__PURE__ */ jsx(Text, {
|
|
730
|
+
dimColor: true,
|
|
731
|
+
children: viewport.hiddenBelow > 0 ? `↓ ${viewport.hiddenBelow} more${filter === null ? " [N] for next page" : ""}` : " "
|
|
732
|
+
})
|
|
733
|
+
]
|
|
734
|
+
}) : /* @__PURE__ */ jsx(Box, {
|
|
735
|
+
flexDirection: "row",
|
|
736
|
+
gap: 4,
|
|
737
|
+
marginLeft: centered ? 0 : 2,
|
|
738
|
+
marginTop: message ? 1 : 0,
|
|
739
|
+
children: columnArrays.map((colOpts, colIdx) => /* @__PURE__ */ jsx(Box, {
|
|
740
|
+
flexDirection: "column",
|
|
741
|
+
children: colOpts.map((opt, rowIdx) => renderOption(opt, colIdx * rows + rowIdx))
|
|
742
|
+
}, colIdx))
|
|
743
|
+
}),
|
|
744
|
+
/* @__PURE__ */ jsx(Box, {
|
|
745
|
+
marginTop: 1,
|
|
746
|
+
marginLeft: centered ? 0 : 2,
|
|
747
|
+
children: /* @__PURE__ */ jsx(ConfirmButton, {
|
|
748
|
+
focused: onButton,
|
|
749
|
+
count: selected.size
|
|
750
|
+
})
|
|
751
|
+
})
|
|
752
|
+
]
|
|
753
|
+
});
|
|
754
|
+
};
|
|
755
|
+
//#endregion
|
|
756
|
+
export { useStdoutDimensions as a, useKeyboardHintsContext as i, useKeyBindings as n, ConfirmButton as o, KeyboardHintsProvider as r, PromptLabel as s, PickerMenu as t };
|
|
757
|
+
|
|
758
|
+
//# sourceMappingURL=PickerMenu-dG5DQCZA.js.map
|