archymedes-cli 3.0.0 → 3.2.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.
@@ -1,240 +0,0 @@
1
- import {
2
- SpringAnimator,
3
- paginator
4
- } from "./archymedes-qsrxyhb3.mjs";
5
- import {
6
- UNICODE_GLYPHS,
7
- clipTo,
8
- visibleWidth
9
- } from "./archymedes-y64yssbc.mjs";
10
-
11
- // packages/archymedes-cli/src/ui/chooser.ts
12
- var ALL_CHROME = { title: true, status: true, pagination: true, help: true };
13
- function chromeOf(chrome) {
14
- return { ...ALL_CHROME, ...chrome };
15
- }
16
- var INITIAL_CHOOSER_STATE = { selected: 0, query: "" };
17
- function filterItems(items, query) {
18
- const needle = query.trim().toLowerCase();
19
- if (!needle)
20
- return [...items];
21
- const rank = (item) => {
22
- const label = item.label.toLowerCase();
23
- if (label.startsWith(needle))
24
- return 0;
25
- if (new RegExp(`\\b${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(label))
26
- return 1;
27
- if (label.includes(needle))
28
- return 2;
29
- return item.description?.toLowerCase().includes(needle) ? 3 : 4;
30
- };
31
- return items.map((item, index) => ({ item, index, tier: item.pinned ? 5 : rank(item) })).filter((entry) => entry.tier < 4 || entry.item.pinned).sort((left, right) => left.tier - right.tier || left.index - right.index).map((entry) => entry.item);
32
- }
33
- function windowStart(selected, total, height) {
34
- const rows = Math.max(1, Math.floor(height));
35
- const count = Math.max(0, Math.floor(total));
36
- if (count <= rows)
37
- return 0;
38
- const cursor = Math.max(0, Math.min(Math.floor(selected), count - 1));
39
- return Math.max(0, Math.min(cursor - Math.floor(rows / 2), count - rows));
40
- }
41
- function terminalColumns(width, fallback = 80) {
42
- const value = width ?? fallback;
43
- return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : fallback;
44
- }
45
- function advanceChooser(state, items, input, options = {}) {
46
- const name = input.key.name;
47
- if (state.status !== undefined)
48
- return advanceChooser({ ...state, status: undefined }, items, input, options);
49
- const visible = filterItems(items, state.query);
50
- const last = Math.max(0, visible.length - 1);
51
- const page = options.page ?? 8;
52
- const clamp = (index) => Math.max(0, Math.min(last, index));
53
- state = { ...state, selected: clamp(state.selected) };
54
- const keep = state.chrome === undefined ? {} : { chrome: state.chrome };
55
- if (name === "escape" && options.filter && state.query !== "")
56
- return { state: { selected: 0, query: "", ...keep } };
57
- if (name === "escape" || input.key.ctrl && (name === "c" || name === "g"))
58
- return { state, done: {} };
59
- if (name === "return" || name === "enter") {
60
- return { state, done: visible.length > 0 ? { index: clamp(state.selected) } : {} };
61
- }
62
- if (name === "up" || input.key.ctrl && name === "p")
63
- return { state: { ...state, selected: clamp(state.selected - 1) } };
64
- if (name === "down" || input.key.ctrl && name === "n")
65
- return { state: { ...state, selected: clamp(state.selected + 1) } };
66
- if (name === "pageup")
67
- return { state: { ...state, selected: clamp(state.selected - page) } };
68
- if (name === "pagedown")
69
- return { state: { ...state, selected: clamp(state.selected + page) } };
70
- if (name === "home")
71
- return { state: { ...state, selected: 0 } };
72
- if (name === "end")
73
- return { state: { ...state, selected: last } };
74
- const digitsJump = !options.filter || state.query === "";
75
- if (digitsJump && input.str && /^[1-9]$/.test(input.str) && !input.key.ctrl && !input.key.meta) {
76
- const height = options.height ?? 10;
77
- const start = windowStart(state.selected, visible.length, height);
78
- const shown = Math.min(height, visible.length - start);
79
- const digit = Number(input.str);
80
- if (digit > shown)
81
- return { state };
82
- return { state: { ...state, selected: start + digit - 1 } };
83
- }
84
- if (options.filter) {
85
- if (name === "backspace")
86
- return { state: { selected: 0, query: state.query.slice(0, -1), ...keep } };
87
- if (input.key.ctrl && name === "u")
88
- return { state: { selected: 0, query: "", ...keep } };
89
- const char = input.str;
90
- if (char && char.length === 1 && char >= " " && !input.key.ctrl && !input.key.meta) {
91
- return { state: { selected: 0, query: state.query + char, ...keep } };
92
- }
93
- }
94
- return { state };
95
- }
96
- function renderChooser(state, items, options) {
97
- const { paint } = options;
98
- const glyphs = options.glyphs ?? UNICODE_GLYPHS;
99
- const clip = (text, width2) => clipTo(text, width2, glyphs);
100
- const height = options.height ?? 10;
101
- const width = terminalColumns(options.width);
102
- const visible = filterItems(items, state.query);
103
- const lines = [];
104
- const matched = visible.some((item) => !item.pinned);
105
- const chrome = chromeOf(state.chrome);
106
- if (options.title && chrome.title) {
107
- lines.push(paint.cyan(clip(` ${options.spinner ? `${options.spinner} ` : ""}${options.title}`, width)));
108
- }
109
- if (options.filter && state.query)
110
- lines.push(paint.dim(clip(` filter: ${state.query}${matched ? "" : " (no match)"}`, width)));
111
- else if (!matched)
112
- lines.push(paint.dim(clip(" (no match)", width)));
113
- const start = windowStart(state.selected, visible.length, height);
114
- const window = visible.slice(start, start + height);
115
- const labelWidth = Math.max(0, ...window.map((item) => visibleWidth(item.label)));
116
- let lastHeader;
117
- for (const [offset, item] of window.entries()) {
118
- const index = start + offset;
119
- if (item.header && item.header !== lastHeader)
120
- lines.push(paint.cyan(clip(` ${item.header}`, width)));
121
- lastHeader = item.header ?? lastHeader;
122
- const active = index === state.selected;
123
- const fadingOut = !active && index === options.transitionFrom;
124
- const number = offset < 9 ? `${offset + 1}.` : " ";
125
- const furniture = visibleWidth(` ${active ? ">" : " "} ${number} `) + 2;
126
- if (width < furniture + 1) {
127
- lines.push(active ? paint.green(clip(`${glyphs.prompt}${item.label}`, width)) : clip(item.label, width));
128
- continue;
129
- }
130
- const room = Math.max(0, width - furniture);
131
- const hasTail = Boolean(item.hint || item.description);
132
- const labelBudget = Math.min(labelWidth, hasTail ? Math.max(1, Math.floor(room * 0.6)) : room);
133
- const shownLabel = clip(item.label, labelBudget);
134
- const padded = shownLabel + " ".repeat(Math.max(0, labelBudget - visibleWidth(shownLabel)));
135
- const marker = active ? paint.green(glyphs.prompt) : fadingOut ? paint.dim(glyphs.prompt) : " ";
136
- const focus = Math.max(0, Math.min(1, options.focusProgress ?? 1));
137
- const graphemes = [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(padded)].map((part) => part.segment);
138
- const illuminated = Math.round(graphemes.length * focus);
139
- const label = active ? paint.cyan(graphemes.slice(0, illuminated).join("")) + graphemes.slice(illuminated).join("") : padded;
140
- const head = `${marker} ${paint.dim(number)} ${label} `;
141
- const tail = `${item.hint ? ` ${item.hint}` : ""}${item.description ? ` ${item.description}` : ""}`;
142
- lines.push(` ${head}${paint.dim(clip(tail, Math.max(0, room - visibleWidth(padded) - 2)))}`);
143
- }
144
- const position = visible.length > height && chrome.pagination ? ` ${paginator(state.selected, visible.length)}` : "";
145
- if (chrome.help) {
146
- const legend = options.legend !== undefined && options.filter && state.query !== "" ? options.legend.replace(/Esc\b[^·]*$/, "Esc clear filter") : options.legend;
147
- lines.push(paint.dim(clip(` ${legend ?? `${glyphs.arrowUp}${glyphs.arrowDown} move ${glyphs.middot} Enter choose ${glyphs.middot} ${options.filter ? `type to filter ${glyphs.middot} ` : ""}${options.filter ? "Esc clear/back" : "Esc back"}`}${position}`, width)));
148
- } else if (position) {
149
- lines.push(paint.dim(clip(position, width)));
150
- }
151
- if (state.status && chrome.status)
152
- lines.push(paint.green(clip(` ${state.status}`, width)));
153
- return lines.join(`
154
- `);
155
- }
156
- async function runChooser(keys, items, paint, options) {
157
- let state = { selected: Math.max(0, Math.min(options.initialIndex ?? 0, Math.max(0, items.length - 1))), query: "" };
158
- const liveOptions = () => {
159
- const size = options.getSize?.();
160
- const height = Math.max(1, Math.min(options.height ?? 10, size?.height ?? Number.POSITIVE_INFINITY));
161
- return { ...options, width: size?.width ?? options.width, height };
162
- };
163
- let list = items;
164
- let pendingDelete;
165
- paint(renderChooser(state, list, liveOptions()));
166
- let glide;
167
- const settleGlide = () => {
168
- glide?.stop();
169
- glide = undefined;
170
- };
171
- try {
172
- for await (const input of keys) {
173
- if (pendingDelete && options.onDelete) {
174
- const target = pendingDelete;
175
- pendingDelete = undefined;
176
- if (input.str === "y" || input.str === "Y") {
177
- const outcome = await options.onDelete(target);
178
- if (outcome.deleted) {
179
- list = list.filter((item) => item !== target);
180
- const remaining = filterItems(list, state.query).length;
181
- state = { ...state, selected: Math.max(0, Math.min(state.selected, remaining - 1)), status: `deleted "${target.label}"` };
182
- } else {
183
- state = { ...state, status: outcome.reason };
184
- }
185
- } else {
186
- state = { ...state, status: "kept" };
187
- }
188
- paint(renderChooser(state, list, liveOptions()));
189
- continue;
190
- }
191
- if (options.onDelete && (input.key.name === "delete" || input.key.ctrl && input.key.name === "d")) {
192
- const target = filterItems(list, state.query)[state.selected];
193
- if (target && !target.pinned) {
194
- pendingDelete = target;
195
- state = { ...state, status: `Delete "${target.label}"? y/n` };
196
- paint(renderChooser(state, list, liveOptions()));
197
- }
198
- continue;
199
- }
200
- const current = liveOptions();
201
- const previousSelected = state.selected;
202
- const step = advanceChooser(state, list, input, {
203
- filter: options.filter ?? false,
204
- page: options.page ?? current.height ?? 8,
205
- ...current.height === undefined ? {} : { height: current.height }
206
- });
207
- state = step.state;
208
- if (step.done) {
209
- settleGlide();
210
- if (step.done.index === undefined)
211
- return;
212
- return filterItems(list, state.query)[step.done.index]?.value;
213
- }
214
- settleGlide();
215
- const isSingleStep = (input.key.name === "up" || input.key.name === "down") && Math.abs(state.selected - previousSelected) === 1;
216
- const motion = options.motion ?? (process.env.ARCHYMEDES_NO_MOTION !== "1" && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb");
217
- if (!motion || !isSingleStep || previousSelected === state.selected) {
218
- paint(renderChooser(state, list, liveOptions()));
219
- continue;
220
- }
221
- paint(renderChooser(state, list, { ...liveOptions(), transitionFrom: previousSelected, focusProgress: 0 }));
222
- const animator = new SpringAnimator(0, (value) => {
223
- if (value < 0.95) {
224
- paint(renderChooser(state, list, { ...liveOptions(), transitionFrom: value < 0.5 ? previousSelected : undefined, focusProgress: value }));
225
- return;
226
- }
227
- animator.stop();
228
- glide = undefined;
229
- paint(renderChooser(state, list, liveOptions()));
230
- }, { intervalMs: 30 });
231
- glide = animator;
232
- animator.retarget(1);
233
- }
234
- } finally {
235
- settleGlide();
236
- }
237
- return;
238
- }
239
-
240
- export { INITIAL_CHOOSER_STATE, filterItems, windowStart, terminalColumns, renderChooser, runChooser };