@solaqua/gji 0.7.2 → 0.8.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/dist/gji-bundle.mjs +785 -238
- package/dist/worktree-picker.d.ts +14 -2
- package/dist/worktree-picker.js +723 -27
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +1 -1
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +1 -1
- package/package.json +1 -1
package/dist/worktree-picker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { env, platform, stdin, stdout } from "node:process";
|
|
2
|
+
import { isCancel, Prompt } from "@clack/core";
|
|
2
3
|
import { loadHistory } from "./history.js";
|
|
3
4
|
import { readWorktreeInfos, } from "./worktree-info.js";
|
|
4
5
|
export async function buildWorktreePromptEntries(sources) {
|
|
@@ -38,25 +39,25 @@ function isAmbiguousRepoOnlyQuery(matches, query) {
|
|
|
38
39
|
return (matches.filter((match) => match.source.repoName.toLowerCase() === query)
|
|
39
40
|
.length > 1);
|
|
40
41
|
}
|
|
41
|
-
export async function promptForSingleWorktree(message, worktrees) {
|
|
42
|
-
const choice = await
|
|
42
|
+
export async function promptForSingleWorktree(message, worktrees, io = {}) {
|
|
43
|
+
const choice = await runSearchablePrompt({
|
|
44
|
+
entries: worktrees.map(buildSearchableWorktreeEntry),
|
|
45
|
+
input: io.input,
|
|
43
46
|
message,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
value: worktree.path,
|
|
47
|
-
})),
|
|
48
|
-
maxItems: 12,
|
|
47
|
+
multiple: false,
|
|
48
|
+
output: io.output,
|
|
49
49
|
});
|
|
50
|
-
return
|
|
50
|
+
return typeof choice === "string" ? choice : null;
|
|
51
51
|
}
|
|
52
|
-
export async function promptForMultipleWorktrees(message, worktrees) {
|
|
53
|
-
const choice = await
|
|
52
|
+
export async function promptForMultipleWorktrees(message, worktrees, io = {}) {
|
|
53
|
+
const choice = await runSearchablePrompt({
|
|
54
|
+
entries: buildGroupedSearchableEntries(worktrees),
|
|
55
|
+
input: io.input,
|
|
54
56
|
message,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
selectableGroups: false,
|
|
57
|
+
multiple: true,
|
|
58
|
+
output: io.output,
|
|
58
59
|
});
|
|
59
|
-
return
|
|
60
|
+
return Array.isArray(choice) ? choice : null;
|
|
60
61
|
}
|
|
61
62
|
function compareQueryMatches(a, b) {
|
|
62
63
|
if (a.matchScore !== b.matchScore) {
|
|
@@ -71,16 +72,652 @@ function compareQueryMatches(a, b) {
|
|
|
71
72
|
a.source.worktree.path.localeCompare(b.source.worktree.path));
|
|
72
73
|
}
|
|
73
74
|
function groupPromptEntries(worktrees) {
|
|
74
|
-
const groups =
|
|
75
|
+
const groups = new Map();
|
|
75
76
|
for (const worktree of worktrees) {
|
|
76
77
|
const group = worktree.group === "recent" ? "Recent worktrees" : "Other worktrees";
|
|
77
|
-
groups
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const groupWorktrees = groups.get(group);
|
|
79
|
+
if (groupWorktrees === undefined) {
|
|
80
|
+
groups.set(group, [worktree]);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
groupWorktrees.push(worktree);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...groups.entries()];
|
|
87
|
+
}
|
|
88
|
+
function buildGroupedSearchableEntries(worktrees) {
|
|
89
|
+
const entries = [];
|
|
90
|
+
for (const [group, groupWorktrees] of groupPromptEntries(worktrees)) {
|
|
91
|
+
entries.push({
|
|
92
|
+
label: group,
|
|
93
|
+
searchText: group.toLowerCase(),
|
|
94
|
+
selectable: false,
|
|
81
95
|
});
|
|
96
|
+
for (const worktree of groupWorktrees) {
|
|
97
|
+
entries.push(buildSearchableWorktreeEntry(worktree));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
function buildSearchableWorktreeEntry(worktree) {
|
|
103
|
+
const metadata = worktree.metadata ?? null;
|
|
104
|
+
const branch = worktree.branch ?? "(detached)";
|
|
105
|
+
return {
|
|
106
|
+
detail: [worktree.repoName, branch, metadata, worktree.path]
|
|
107
|
+
.filter((part) => part !== null && part.length > 0)
|
|
108
|
+
.join(" · "),
|
|
109
|
+
label: worktree.label,
|
|
110
|
+
searchText: buildPromptSearchText(worktree),
|
|
111
|
+
value: worktree.path,
|
|
112
|
+
worktree: {
|
|
113
|
+
branch,
|
|
114
|
+
metadata,
|
|
115
|
+
path: worktree.path,
|
|
116
|
+
repoName: worktree.repoName,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function runSearchablePrompt(options) {
|
|
121
|
+
const input = options.input ?? stdin;
|
|
122
|
+
const output = options.output ?? stdout;
|
|
123
|
+
if (!input.isTTY) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const prompt = new SearchablePrompt(options, input, output);
|
|
127
|
+
const value = await prompt.run();
|
|
128
|
+
return isCancel(value) ? null : value;
|
|
129
|
+
}
|
|
130
|
+
class SearchablePrompt {
|
|
131
|
+
options;
|
|
132
|
+
output;
|
|
133
|
+
cursor = 0;
|
|
134
|
+
query = "";
|
|
135
|
+
searchActive = false;
|
|
136
|
+
selected = new Set();
|
|
137
|
+
prompt;
|
|
138
|
+
constructor(options, input, output) {
|
|
139
|
+
this.options = options;
|
|
140
|
+
this.output = output;
|
|
141
|
+
this.cursor = this.firstSelectableIndex();
|
|
142
|
+
const owner = this;
|
|
143
|
+
this.prompt = new WorktreeCorePrompt(this, {
|
|
144
|
+
input,
|
|
145
|
+
output,
|
|
146
|
+
render: function renderWorktreePrompt() {
|
|
147
|
+
return owner.render(this.state, this.error);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
this.syncValue();
|
|
151
|
+
}
|
|
152
|
+
run() {
|
|
153
|
+
return this.prompt.prompt();
|
|
154
|
+
}
|
|
155
|
+
handleKeypress(prompt, character, key) {
|
|
156
|
+
if (prompt.state === "error") {
|
|
157
|
+
prompt.state = "active";
|
|
158
|
+
prompt.error = "";
|
|
159
|
+
}
|
|
160
|
+
const action = resolvePromptAction(character, key);
|
|
161
|
+
if (action === "cancel") {
|
|
162
|
+
this.cancel(prompt);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (action === "escape") {
|
|
166
|
+
this.handleEscapeKey(prompt);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (action === "enter") {
|
|
170
|
+
this.submit(prompt);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (this.searchActive && this.handleSearchKey(character, key?.name)) {
|
|
174
|
+
this.syncValue();
|
|
175
|
+
renderPrompt(prompt);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (character === "/" && !this.searchActive) {
|
|
179
|
+
this.searchActive = true;
|
|
180
|
+
this.query = "";
|
|
181
|
+
this.cursor = this.firstSelectableIndex();
|
|
182
|
+
this.syncValue();
|
|
183
|
+
renderPrompt(prompt);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.handleNavigationKey(character, action);
|
|
187
|
+
this.syncValue();
|
|
188
|
+
renderPrompt(prompt);
|
|
189
|
+
}
|
|
190
|
+
handleEscapeKey(prompt) {
|
|
191
|
+
if (this.searchActive) {
|
|
192
|
+
this.searchActive = false;
|
|
193
|
+
this.query = "";
|
|
194
|
+
this.cursor = this.firstSelectableIndex();
|
|
195
|
+
this.syncValue();
|
|
196
|
+
renderPrompt(prompt);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.cancel(prompt);
|
|
200
|
+
}
|
|
201
|
+
cancel(prompt) {
|
|
202
|
+
prompt.state = "cancel";
|
|
203
|
+
renderPrompt(prompt);
|
|
204
|
+
closePrompt(prompt);
|
|
205
|
+
}
|
|
206
|
+
handleSearchKey(character, keyName) {
|
|
207
|
+
if (keyName === "space" || character === " ") {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (keyName === "backspace" || keyName === "delete") {
|
|
211
|
+
this.query = this.query.slice(0, -1);
|
|
212
|
+
this.cursor = this.firstSelectableIndex();
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (isPrintableSearchCharacter(character)) {
|
|
216
|
+
this.query += character;
|
|
217
|
+
this.cursor = this.firstSelectableIndex();
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
handleNavigationKey(character, action) {
|
|
223
|
+
switch (action) {
|
|
224
|
+
case "up":
|
|
225
|
+
this.moveCursor(-1);
|
|
226
|
+
return;
|
|
227
|
+
case "down":
|
|
228
|
+
this.moveCursor(1);
|
|
229
|
+
return;
|
|
230
|
+
case "space":
|
|
231
|
+
this.toggleSelection();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
switch (character) {
|
|
235
|
+
case "k":
|
|
236
|
+
this.moveCursor(-1);
|
|
237
|
+
return;
|
|
238
|
+
case "j":
|
|
239
|
+
this.moveCursor(1);
|
|
240
|
+
return;
|
|
241
|
+
case " ":
|
|
242
|
+
this.toggleSelection();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
moveCursor(direction) {
|
|
247
|
+
const entries = this.visibleEntries();
|
|
248
|
+
if (entries.length === 0)
|
|
249
|
+
return;
|
|
250
|
+
let nextCursor = this.cursor;
|
|
251
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
252
|
+
nextCursor = wrapIndex(nextCursor + direction, entries.length);
|
|
253
|
+
if (isSelectableEntry(entries[nextCursor])) {
|
|
254
|
+
this.cursor = nextCursor;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
submit(prompt) {
|
|
260
|
+
const entry = this.visibleEntries()[this.cursor];
|
|
261
|
+
if (!this.options.multiple) {
|
|
262
|
+
prompt.value = isSelectableEntry(entry) ? entry.value : null;
|
|
263
|
+
prompt.state = "submit";
|
|
264
|
+
renderPrompt(prompt);
|
|
265
|
+
closePrompt(prompt);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (this.selected.size === 0) {
|
|
269
|
+
prompt.error = "Please select at least one option.";
|
|
270
|
+
prompt.state = "error";
|
|
271
|
+
renderPrompt(prompt);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
prompt.value = [...this.selected];
|
|
275
|
+
prompt.state = "submit";
|
|
276
|
+
renderPrompt(prompt);
|
|
277
|
+
closePrompt(prompt);
|
|
278
|
+
}
|
|
279
|
+
toggleSelection() {
|
|
280
|
+
if (!this.options.multiple)
|
|
281
|
+
return;
|
|
282
|
+
const entry = this.visibleEntries()[this.cursor];
|
|
283
|
+
if (!isSelectableEntry(entry))
|
|
284
|
+
return;
|
|
285
|
+
if (this.selected.has(entry.value)) {
|
|
286
|
+
this.selected.delete(entry.value);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.selected.add(entry.value);
|
|
290
|
+
}
|
|
291
|
+
render(state, error) {
|
|
292
|
+
const entries = this.visibleEntries();
|
|
293
|
+
const visibleEntries = windowPromptEntries(entries, this.cursor, this.maxItems());
|
|
294
|
+
const glyphs = promptGlyphs();
|
|
295
|
+
const colors = promptColors();
|
|
296
|
+
const frame = promptFrameColor(state, colors);
|
|
297
|
+
const lines = [
|
|
298
|
+
colors.bar(glyphs.bar),
|
|
299
|
+
this.renderTitle(state, glyphs, colors),
|
|
300
|
+
`${frame(glyphs.bar)} ${this.renderSearchHint(colors)}`,
|
|
301
|
+
frame(glyphs.bar),
|
|
302
|
+
];
|
|
303
|
+
if (state === "error" && error.length > 0) {
|
|
304
|
+
lines.push(`${colors.errorBar(glyphs.bar)} ${colors.error(error)}`);
|
|
305
|
+
lines.push(frame(glyphs.bar));
|
|
306
|
+
}
|
|
307
|
+
lines.push(...this.renderEntries(entries, visibleEntries, glyphs, colors, frame));
|
|
308
|
+
lines.push(frame(glyphs.bar));
|
|
309
|
+
const preview = this.activePreview(entries, colors);
|
|
310
|
+
if (preview !== null) {
|
|
311
|
+
lines.push(`${frame(glyphs.bar)} ${preview}`);
|
|
312
|
+
lines.push(frame(glyphs.bar));
|
|
313
|
+
}
|
|
314
|
+
const visibleWorktreeCount = entries.filter(isSelectableEntry).length;
|
|
315
|
+
const footer = this.footerText(visibleWorktreeCount, colors);
|
|
316
|
+
if (footer.length > 0) {
|
|
317
|
+
lines.push(`${frame(glyphs.corner)} ${footer}`);
|
|
318
|
+
}
|
|
319
|
+
return `${lines.join("\n")}\n`;
|
|
320
|
+
}
|
|
321
|
+
renderTitle(state, glyphs, colors) {
|
|
322
|
+
const symbol = colors.symbol(promptSymbol(state, glyphs), state);
|
|
323
|
+
const maxTitleLength = Math.max(8, this.columns() - 3);
|
|
324
|
+
if (!this.searchActive) {
|
|
325
|
+
return `${symbol} ${middleEllipsize(this.options.message, maxTitleLength)}`;
|
|
326
|
+
}
|
|
327
|
+
const search = ` /${this.query}`;
|
|
328
|
+
const maxSearchLength = Math.min(terminalWidth(search), Math.floor(maxTitleLength / 2));
|
|
329
|
+
const message = middleEllipsize(this.options.message, Math.max(1, maxTitleLength - maxSearchLength));
|
|
330
|
+
const visibleSearch = middleEllipsize(search, Math.max(1, maxTitleLength - terminalWidth(message)));
|
|
331
|
+
return `${symbol} ${message}${colors.search(visibleSearch)}`;
|
|
332
|
+
}
|
|
333
|
+
renderSearchHint(colors) {
|
|
334
|
+
if (this.searchActive) {
|
|
335
|
+
return `${colors.hint("type to filter")} ${colors.hint("·")} ${colors.key("esc")} ${colors.hint("clears")}`;
|
|
336
|
+
}
|
|
337
|
+
return `${colors.hint("press ")}${colors.key("/")}${colors.hint(" to search")}`;
|
|
338
|
+
}
|
|
339
|
+
renderEntries(entries, visibleEntries, glyphs, colors, frame) {
|
|
340
|
+
if (entries.length === 0) {
|
|
341
|
+
return [`${frame(glyphs.bar)} ${colors.hint("No matching worktrees")}`];
|
|
342
|
+
}
|
|
343
|
+
return visibleEntries.map((entry) => entry === "ellipsis"
|
|
344
|
+
? `${frame(glyphs.bar)} ${colors.hint("...")}`
|
|
345
|
+
: this.renderEntry(entries, entry, glyphs, colors, frame));
|
|
346
|
+
}
|
|
347
|
+
renderEntry(entries, entry, glyphs, colors, frame) {
|
|
348
|
+
const active = entries[this.cursor] === entry;
|
|
349
|
+
const selected = isSelectableEntry(entry) && this.selected.has(entry.value);
|
|
350
|
+
const prefix = this.entryPrefix(entry, active, selected, glyphs);
|
|
351
|
+
const label = this.entryLabel(entry, prefix);
|
|
352
|
+
const line = isSelectableEntry(entry)
|
|
353
|
+
? active
|
|
354
|
+
? label
|
|
355
|
+
: colors.hint(label)
|
|
356
|
+
: colors.hint(label);
|
|
357
|
+
const marker = selected
|
|
358
|
+
? colors.selected(prefix)
|
|
359
|
+
: active
|
|
360
|
+
? this.options.multiple
|
|
361
|
+
? colors.option(prefix)
|
|
362
|
+
: colors.selected(prefix)
|
|
363
|
+
: colors.hint(prefix);
|
|
364
|
+
return `${frame(glyphs.bar)} ${marker} ${line}`;
|
|
365
|
+
}
|
|
366
|
+
activePreview(entries, colors) {
|
|
367
|
+
const entry = entries[this.cursor];
|
|
368
|
+
if (!isSelectableEntry(entry) || entry.detail === undefined)
|
|
369
|
+
return null;
|
|
370
|
+
const label = "current";
|
|
371
|
+
const separator = " ";
|
|
372
|
+
const width = this.previewWidth();
|
|
373
|
+
const prefixWidth = terminalWidth(label) + terminalWidth(separator);
|
|
374
|
+
if (width <= prefixWidth)
|
|
375
|
+
return colors.key(middleEllipsize(label, width));
|
|
376
|
+
const detail = middleEllipsize(entry.detail, width - prefixWidth);
|
|
377
|
+
return `${colors.key(label)}${colors.hint(separator)}${colors.hint(detail)}`;
|
|
378
|
+
}
|
|
379
|
+
entryLabel(entry, prefix) {
|
|
380
|
+
const width = this.labelWidth(prefix);
|
|
381
|
+
if (entry.worktree === undefined) {
|
|
382
|
+
return middleEllipsize(entry.label, width);
|
|
383
|
+
}
|
|
384
|
+
return fitPromptPieces([
|
|
385
|
+
{
|
|
386
|
+
ellipsize: middleEllipsize,
|
|
387
|
+
max: 18,
|
|
388
|
+
min: 6,
|
|
389
|
+
value: entry.worktree.repoName,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
ellipsize: middleEllipsize,
|
|
393
|
+
max: 32,
|
|
394
|
+
min: 8,
|
|
395
|
+
value: entry.worktree.branch,
|
|
396
|
+
},
|
|
397
|
+
...(entry.worktree.metadata === null
|
|
398
|
+
? []
|
|
399
|
+
: [
|
|
400
|
+
{
|
|
401
|
+
ellipsize: middleEllipsize,
|
|
402
|
+
max: 30,
|
|
403
|
+
min: 10,
|
|
404
|
+
value: entry.worktree.metadata,
|
|
405
|
+
},
|
|
406
|
+
]),
|
|
407
|
+
{
|
|
408
|
+
ellipsize: startEllipsize,
|
|
409
|
+
max: 36,
|
|
410
|
+
min: 12,
|
|
411
|
+
value: entry.worktree.path,
|
|
412
|
+
},
|
|
413
|
+
], width);
|
|
414
|
+
}
|
|
415
|
+
labelWidth(prefix) {
|
|
416
|
+
return Math.max(8, this.columns() - terminalWidth(prefix) - 4);
|
|
417
|
+
}
|
|
418
|
+
previewWidth() {
|
|
419
|
+
return Math.max(8, this.columns() - 3);
|
|
420
|
+
}
|
|
421
|
+
entryPrefix(entry, active, selected, glyphs) {
|
|
422
|
+
if (!isSelectableEntry(entry)) {
|
|
423
|
+
return active ? glyphs.active : " ";
|
|
424
|
+
}
|
|
425
|
+
if (!this.options.multiple) {
|
|
426
|
+
return active ? glyphs.active : glyphs.inactive;
|
|
427
|
+
}
|
|
428
|
+
if (active && selected)
|
|
429
|
+
return glyphs.checked;
|
|
430
|
+
if (active)
|
|
431
|
+
return glyphs.uncheckedActive;
|
|
432
|
+
return selected ? glyphs.checked : glyphs.unchecked;
|
|
433
|
+
}
|
|
434
|
+
footerText(visibleCount, colors) {
|
|
435
|
+
if (!this.options.multiple)
|
|
436
|
+
return "";
|
|
437
|
+
return [
|
|
438
|
+
`${colors.key("space")} ${colors.hint("select")}`,
|
|
439
|
+
`${colors.selected(String(this.selected.size))} ${colors.hint("selected")}`,
|
|
440
|
+
colors.hint(`${visibleCount} shown`),
|
|
441
|
+
].join(colors.hint(" · "));
|
|
442
|
+
}
|
|
443
|
+
firstSelectableIndex() {
|
|
444
|
+
const index = this.visibleEntries().findIndex(isSelectableEntry);
|
|
445
|
+
return index === -1 ? 0 : index;
|
|
446
|
+
}
|
|
447
|
+
visibleEntries() {
|
|
448
|
+
const query = normalizeQuery(this.query);
|
|
449
|
+
if (query === null) {
|
|
450
|
+
return this.options.entries;
|
|
451
|
+
}
|
|
452
|
+
return filterSearchablePromptEntries(this.options.entries, query);
|
|
453
|
+
}
|
|
454
|
+
maxItems() {
|
|
455
|
+
const rows = this.output.rows ?? 16;
|
|
456
|
+
return Math.max(5, Math.min(12, rows - 8));
|
|
457
|
+
}
|
|
458
|
+
columns() {
|
|
459
|
+
return Math.max(20, this.output.columns ?? 80);
|
|
460
|
+
}
|
|
461
|
+
syncValue() {
|
|
462
|
+
if (this.options.multiple) {
|
|
463
|
+
this.prompt.value = [...this.selected];
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const entry = this.visibleEntries()[this.cursor];
|
|
467
|
+
this.prompt.value = isSelectableEntry(entry) ? entry.value : null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
class WorktreeCorePrompt extends Prompt {
|
|
471
|
+
worktreePrompt;
|
|
472
|
+
constructor(worktreePrompt, options) {
|
|
473
|
+
super(options, false);
|
|
474
|
+
this.worktreePrompt = worktreePrompt;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/*
|
|
478
|
+
* @clack/core owns raw mode, frame clearing, and cursor restoration, but it
|
|
479
|
+
* does not expose a public pre-cancel keypress hook. Keep this private adapter
|
|
480
|
+
* small so slash-search can clear on Esc before Clack maps Esc to cancel.
|
|
481
|
+
*/
|
|
482
|
+
Object.defineProperty(WorktreeCorePrompt.prototype, "onKeypress", {
|
|
483
|
+
value: onWorktreeKeypress,
|
|
484
|
+
writable: true,
|
|
485
|
+
});
|
|
486
|
+
function renderPrompt(prompt) {
|
|
487
|
+
prompt.render();
|
|
488
|
+
}
|
|
489
|
+
function closePrompt(prompt) {
|
|
490
|
+
prompt.close();
|
|
491
|
+
}
|
|
492
|
+
function onWorktreeKeypress(character, key) {
|
|
493
|
+
this.worktreePrompt.handleKeypress(this, character, key);
|
|
494
|
+
}
|
|
495
|
+
function resolvePromptAction(character, key) {
|
|
496
|
+
if ((key?.ctrl && key.name === "c") || character === "\u0003") {
|
|
497
|
+
return "cancel";
|
|
498
|
+
}
|
|
499
|
+
switch (key?.name) {
|
|
500
|
+
case "escape":
|
|
501
|
+
return "escape";
|
|
502
|
+
case "return":
|
|
503
|
+
return "enter";
|
|
504
|
+
case "space":
|
|
505
|
+
return "space";
|
|
506
|
+
case "up":
|
|
507
|
+
case "left":
|
|
508
|
+
return "up";
|
|
509
|
+
case "down":
|
|
510
|
+
case "right":
|
|
511
|
+
return "down";
|
|
512
|
+
}
|
|
513
|
+
switch (character) {
|
|
514
|
+
case " ":
|
|
515
|
+
return "space";
|
|
516
|
+
case "k":
|
|
517
|
+
case "h":
|
|
518
|
+
return "up";
|
|
519
|
+
case "j":
|
|
520
|
+
case "l":
|
|
521
|
+
return "down";
|
|
522
|
+
}
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
function promptGlyphs() {
|
|
526
|
+
return supportsUnicode()
|
|
527
|
+
? {
|
|
528
|
+
active: "●",
|
|
529
|
+
bar: "│",
|
|
530
|
+
checked: "◼",
|
|
531
|
+
corner: "└",
|
|
532
|
+
inactive: "○",
|
|
533
|
+
pending: "◆",
|
|
534
|
+
submitted: "◇",
|
|
535
|
+
unchecked: "◻",
|
|
536
|
+
uncheckedActive: "◻",
|
|
537
|
+
}
|
|
538
|
+
: {
|
|
539
|
+
active: ">",
|
|
540
|
+
bar: "|",
|
|
541
|
+
checked: "[x]",
|
|
542
|
+
corner: "-",
|
|
543
|
+
inactive: " ",
|
|
544
|
+
pending: "*",
|
|
545
|
+
submitted: "o",
|
|
546
|
+
unchecked: "[ ]",
|
|
547
|
+
uncheckedActive: "[ ]",
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function promptColors() {
|
|
551
|
+
return {
|
|
552
|
+
activeBar: cyan,
|
|
553
|
+
bar: gray,
|
|
554
|
+
error: yellow,
|
|
555
|
+
errorBar: yellow,
|
|
556
|
+
hint: dim,
|
|
557
|
+
key: cyan,
|
|
558
|
+
option: cyan,
|
|
559
|
+
search: cyan,
|
|
560
|
+
selected: green,
|
|
561
|
+
symbol: (value, state) => {
|
|
562
|
+
if (state === "submit")
|
|
563
|
+
return green(value);
|
|
564
|
+
if (state === "error")
|
|
565
|
+
return yellow(value);
|
|
566
|
+
if (state === "cancel")
|
|
567
|
+
return red(value);
|
|
568
|
+
return cyan(value);
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function promptFrameColor(state, colors) {
|
|
573
|
+
if (state === "error")
|
|
574
|
+
return colors.errorBar;
|
|
575
|
+
if (state === "active" || state === "initial")
|
|
576
|
+
return colors.activeBar;
|
|
577
|
+
return colors.bar;
|
|
578
|
+
}
|
|
579
|
+
function promptSymbol(state, glyphs) {
|
|
580
|
+
return state === "submit" ? glyphs.submitted : glyphs.pending;
|
|
581
|
+
}
|
|
582
|
+
function cyan(value) {
|
|
583
|
+
return color("\u001b[36m", "\u001b[39m", value);
|
|
584
|
+
}
|
|
585
|
+
function dim(value) {
|
|
586
|
+
return color("\u001b[2m", "\u001b[22m", value);
|
|
587
|
+
}
|
|
588
|
+
function gray(value) {
|
|
589
|
+
return color("\u001b[90m", "\u001b[39m", value);
|
|
590
|
+
}
|
|
591
|
+
function green(value) {
|
|
592
|
+
return color("\u001b[32m", "\u001b[39m", value);
|
|
593
|
+
}
|
|
594
|
+
function red(value) {
|
|
595
|
+
return color("\u001b[31m", "\u001b[39m", value);
|
|
596
|
+
}
|
|
597
|
+
function yellow(value) {
|
|
598
|
+
return color("\u001b[33m", "\u001b[39m", value);
|
|
599
|
+
}
|
|
600
|
+
function color(open, close, value) {
|
|
601
|
+
if (env.NO_COLOR !== undefined)
|
|
602
|
+
return value;
|
|
603
|
+
return `${open}${value}${close}`;
|
|
604
|
+
}
|
|
605
|
+
function supportsUnicode() {
|
|
606
|
+
if (platform !== "win32") {
|
|
607
|
+
return env.TERM !== "linux";
|
|
608
|
+
}
|
|
609
|
+
return Boolean(env.CI ||
|
|
610
|
+
env.WT_SESSION ||
|
|
611
|
+
env.TERMINUS_SUBLIME ||
|
|
612
|
+
env.ConEmuTask === "{cmd::Cmder}" ||
|
|
613
|
+
env.TERM_PROGRAM === "Terminus-Sublime" ||
|
|
614
|
+
env.TERM_PROGRAM === "vscode" ||
|
|
615
|
+
env.TERM === "xterm-256color" ||
|
|
616
|
+
env.TERM === "alacritty" ||
|
|
617
|
+
env.TERMINAL_EMULATOR === "JetBrains-JediTerm");
|
|
618
|
+
}
|
|
619
|
+
function isPrintableSearchCharacter(character) {
|
|
620
|
+
return (typeof character === "string" &&
|
|
621
|
+
character.length === 1 &&
|
|
622
|
+
character >= " " &&
|
|
623
|
+
character !== "\u007f");
|
|
624
|
+
}
|
|
625
|
+
function wrapIndex(index, length) {
|
|
626
|
+
return (index + length) % length;
|
|
627
|
+
}
|
|
628
|
+
function windowPromptEntries(entries, cursor, maxItems) {
|
|
629
|
+
if (entries.length <= maxItems) {
|
|
630
|
+
return entries;
|
|
631
|
+
}
|
|
632
|
+
const activeIndex = Math.min(Math.max(cursor, 0), entries.length - 1);
|
|
633
|
+
const start = Math.max(0, Math.min(activeIndex - 2, entries.length - maxItems));
|
|
634
|
+
const window = entries.slice(start, start + maxItems);
|
|
635
|
+
if (start > 0) {
|
|
636
|
+
window[0] = "ellipsis";
|
|
637
|
+
}
|
|
638
|
+
if (start + maxItems < entries.length) {
|
|
639
|
+
window[window.length - 1] = "ellipsis";
|
|
640
|
+
}
|
|
641
|
+
return window;
|
|
642
|
+
}
|
|
643
|
+
function fitPromptPieces(pieces, width) {
|
|
644
|
+
const separator = " · ";
|
|
645
|
+
let visiblePieces = pieces.filter((piece) => piece.value.length > 0);
|
|
646
|
+
let available = width - terminalWidth(separator) * Math.max(0, visiblePieces.length - 1);
|
|
647
|
+
while (visiblePieces.length > 1 &&
|
|
648
|
+
available < minimumPieceLength(visiblePieces)) {
|
|
649
|
+
const removableIndex = visiblePieces.length === 4 ? 2 : visiblePieces.length - 2;
|
|
650
|
+
visiblePieces = visiblePieces.filter((_, index) => index !== removableIndex);
|
|
651
|
+
available =
|
|
652
|
+
width - terminalWidth(separator) * Math.max(0, visiblePieces.length - 1);
|
|
653
|
+
}
|
|
654
|
+
if (available <= 0 || available < minimumPieceLength(visiblePieces)) {
|
|
655
|
+
return middleEllipsize(visiblePieces.map((piece) => piece.value).join(separator), width);
|
|
656
|
+
}
|
|
657
|
+
const lengths = visiblePieces.map((piece) => Math.min(terminalWidth(piece.value), piece.max));
|
|
658
|
+
while (sum(lengths) > available) {
|
|
659
|
+
const index = largestShrinkablePieceIndex(visiblePieces, lengths);
|
|
660
|
+
if (index === -1)
|
|
661
|
+
break;
|
|
662
|
+
lengths[index] -= 1;
|
|
663
|
+
}
|
|
664
|
+
return visiblePieces
|
|
665
|
+
.map((piece, index) => piece.ellipsize(piece.value, lengths[index]))
|
|
666
|
+
.join(separator);
|
|
667
|
+
}
|
|
668
|
+
function minimumPieceLength(pieces) {
|
|
669
|
+
return sum(pieces.map((piece) => Math.min(terminalWidth(piece.value), piece.min)));
|
|
670
|
+
}
|
|
671
|
+
function largestShrinkablePieceIndex(pieces, lengths) {
|
|
672
|
+
let candidate = -1;
|
|
673
|
+
for (let index = 0; index < pieces.length; index += 1) {
|
|
674
|
+
if (lengths[index] <=
|
|
675
|
+
Math.min(terminalWidth(pieces[index].value), pieces[index].min)) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (candidate === -1 || lengths[index] > lengths[candidate]) {
|
|
679
|
+
candidate = index;
|
|
680
|
+
}
|
|
82
681
|
}
|
|
83
|
-
return
|
|
682
|
+
return candidate;
|
|
683
|
+
}
|
|
684
|
+
function sum(values) {
|
|
685
|
+
return values.reduce((total, value) => total + value, 0);
|
|
686
|
+
}
|
|
687
|
+
function filterSearchablePromptEntries(entries, query) {
|
|
688
|
+
const filtered = [];
|
|
689
|
+
let pendingGroup = null;
|
|
690
|
+
for (const entry of entries) {
|
|
691
|
+
if (!isSelectableEntry(entry)) {
|
|
692
|
+
pendingGroup = entry;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (!entry.searchText.includes(query)) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (pendingGroup !== null) {
|
|
699
|
+
filtered.push(pendingGroup);
|
|
700
|
+
pendingGroup = null;
|
|
701
|
+
}
|
|
702
|
+
filtered.push(entry);
|
|
703
|
+
}
|
|
704
|
+
return filtered;
|
|
705
|
+
}
|
|
706
|
+
function isSelectableEntry(entry) {
|
|
707
|
+
return (entry !== undefined &&
|
|
708
|
+
entry.selectable !== false &&
|
|
709
|
+
typeof entry.value === "string");
|
|
710
|
+
}
|
|
711
|
+
function buildPromptSearchText(worktree) {
|
|
712
|
+
return [
|
|
713
|
+
worktree.repoName,
|
|
714
|
+
worktree.branch ?? "detached",
|
|
715
|
+
worktree.path,
|
|
716
|
+
worktree.label,
|
|
717
|
+
`${worktree.repoName}/${worktree.branch ?? "detached"}`,
|
|
718
|
+
]
|
|
719
|
+
.join(" ")
|
|
720
|
+
.toLowerCase();
|
|
84
721
|
}
|
|
85
722
|
function buildWorktreePromptEntry(source, info, lastUsedTimestamp, now) {
|
|
86
723
|
const lastWorkedTimestamp = info.lastCommitTimestamp === null ? null : info.lastCommitTimestamp * 1000;
|
|
@@ -94,12 +731,13 @@ function buildWorktreePromptEntry(source, info, lastUsedTimestamp, now) {
|
|
|
94
731
|
const badges = buildStatusBadges(info);
|
|
95
732
|
const recency = formatPromptRecency(lastActivityTimestamp, lastActivityType, now);
|
|
96
733
|
const status = badges.length > 0 ? badges.map((badge) => `[${badge}]`).join(" ") : null;
|
|
734
|
+
const metadataParts = [status, recency].filter((part) => part !== null && part.length > 0);
|
|
735
|
+
const metadata = metadataParts.length === 0 ? null : metadataParts.join(" · ");
|
|
97
736
|
const path = middleEllipsize(source.worktree.path, 76);
|
|
98
737
|
const label = [
|
|
99
738
|
middleEllipsize(source.repoName, 22),
|
|
100
739
|
middleEllipsize(branch, 34),
|
|
101
|
-
|
|
102
|
-
recency,
|
|
740
|
+
metadata,
|
|
103
741
|
path,
|
|
104
742
|
]
|
|
105
743
|
.filter((part) => part !== null && part.length > 0)
|
|
@@ -109,6 +747,7 @@ function buildWorktreePromptEntry(source, info, lastUsedTimestamp, now) {
|
|
|
109
747
|
group: lastUsedTimestamp !== null ? "recent" : "other",
|
|
110
748
|
label,
|
|
111
749
|
lastActivityTimestamp,
|
|
750
|
+
metadata,
|
|
112
751
|
repoName: source.repoName,
|
|
113
752
|
};
|
|
114
753
|
}
|
|
@@ -215,14 +854,71 @@ function scoreWorktreeMatch(entry, query) {
|
|
|
215
854
|
return buildSearchText(entry.repoName, entry).includes(query) ? 1 : null;
|
|
216
855
|
}
|
|
217
856
|
function middleEllipsize(value, maxLength) {
|
|
218
|
-
if (value
|
|
857
|
+
if (terminalWidth(value) <= maxLength) {
|
|
219
858
|
return value;
|
|
220
859
|
}
|
|
221
860
|
if (maxLength <= 1) {
|
|
222
861
|
return "…";
|
|
223
862
|
}
|
|
224
863
|
const keep = maxLength - 1;
|
|
225
|
-
const start = Math.ceil(keep / 2);
|
|
226
|
-
const end = Math.floor(keep / 2);
|
|
227
|
-
return `${
|
|
864
|
+
const start = takeTerminalColumns(value, Math.ceil(keep / 2), "start");
|
|
865
|
+
const end = takeTerminalColumns(value, Math.floor(keep / 2), "end");
|
|
866
|
+
return `${start}…${end}`;
|
|
867
|
+
}
|
|
868
|
+
function startEllipsize(value, maxLength) {
|
|
869
|
+
if (terminalWidth(value) <= maxLength) {
|
|
870
|
+
return value;
|
|
871
|
+
}
|
|
872
|
+
if (maxLength <= 1) {
|
|
873
|
+
return "…";
|
|
874
|
+
}
|
|
875
|
+
return `…${takeTerminalColumns(value, maxLength - 1, "end")}`;
|
|
876
|
+
}
|
|
877
|
+
function takeTerminalColumns(value, maxWidth, direction) {
|
|
878
|
+
const characters = direction === "start" ? Array.from(value) : Array.from(value).reverse();
|
|
879
|
+
const kept = [];
|
|
880
|
+
let width = 0;
|
|
881
|
+
for (const character of characters) {
|
|
882
|
+
const characterWidth = terminalWidth(character);
|
|
883
|
+
if (width + characterWidth > maxWidth)
|
|
884
|
+
break;
|
|
885
|
+
kept.push(character);
|
|
886
|
+
width += characterWidth;
|
|
887
|
+
}
|
|
888
|
+
return direction === "start" ? kept.join("") : kept.reverse().join("");
|
|
889
|
+
}
|
|
890
|
+
function terminalWidth(value) {
|
|
891
|
+
let width = 0;
|
|
892
|
+
for (const character of Array.from(value)) {
|
|
893
|
+
width += characterTerminalWidth(character);
|
|
894
|
+
}
|
|
895
|
+
return width;
|
|
896
|
+
}
|
|
897
|
+
function characterTerminalWidth(character) {
|
|
898
|
+
const codePoint = character.codePointAt(0);
|
|
899
|
+
if (codePoint === undefined)
|
|
900
|
+
return 0;
|
|
901
|
+
if (isZeroWidthCodePoint(codePoint) || /\p{Mark}/u.test(character)) {
|
|
902
|
+
return 0;
|
|
903
|
+
}
|
|
904
|
+
return isWideCodePoint(codePoint) ? 2 : 1;
|
|
905
|
+
}
|
|
906
|
+
function isZeroWidthCodePoint(codePoint) {
|
|
907
|
+
return (codePoint === 0 ||
|
|
908
|
+
codePoint === 0x200d ||
|
|
909
|
+
(codePoint >= 0xfe00 && codePoint <= 0xfe0f));
|
|
910
|
+
}
|
|
911
|
+
function isWideCodePoint(codePoint) {
|
|
912
|
+
return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
|
913
|
+
codePoint === 0x2329 ||
|
|
914
|
+
codePoint === 0x232a ||
|
|
915
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
|
|
916
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
917
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
918
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
919
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
920
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
921
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
922
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
|
|
923
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd));
|
|
228
924
|
}
|