@n-seiji/nuthatch 0.1.5 → 0.1.7

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.
@@ -0,0 +1,1533 @@
1
+ import"./cli-qrxgkskr.js";
2
+ import {
3
+ rm2
4
+ } from "./cli-23z98dw1.js";
5
+ import {
6
+ root2
7
+ } from "./cli-ygt2w4e1.js";
8
+ import {
9
+ graphemes,
10
+ displayWidth,
11
+ padToWidth,
12
+ truncateToWidth,
13
+ truncateToWidthKeepingTail
14
+ } from "./cli-71jgs14h.js";
15
+ import {
16
+ render,
17
+ reportResult
18
+ } from "./cli-dbzx9nzg.js";
19
+ import {
20
+ loadRepoContext,
21
+ worktreeDirtyState
22
+ } from "./cli-7jg5qqs9.js";
23
+ import {
24
+ EXIT_CANCELLED,
25
+ ok
26
+ } from "./cli-2bhb9p0x.js";
27
+
28
+ // src/domain/candidates.ts
29
+ var buildPickCandidates = (worktrees, dirtyByPath, localBranches, remoteBranches) => {
30
+ const existingBranches = new Set(worktrees.map((worktree) => worktree.branch).filter((branch) => branch !== null));
31
+ const worktreeCandidates = worktrees.map((worktree) => ({
32
+ kind: "worktree",
33
+ worktree,
34
+ dirty: dirtyByPath.get(worktree.path) ?? null
35
+ }));
36
+ const localOnly = localBranches.filter((branch) => !existingBranches.has(branch));
37
+ const localSet = new Set(localBranches);
38
+ const remoteOnly = remoteBranches.filter((branch) => !existingBranches.has(branch) && !localSet.has(branch));
39
+ return [
40
+ ...worktreeCandidates,
41
+ ...localOnly.map((branch) => ({
42
+ kind: "creatable",
43
+ branch,
44
+ source: "local"
45
+ })),
46
+ ...remoteOnly.map((branch) => ({
47
+ kind: "creatable",
48
+ branch,
49
+ source: "remote"
50
+ }))
51
+ ];
52
+ };
53
+ var candidateBranchLabel = (candidate) => candidate.kind === "worktree" ? candidate.worktree.branch ?? "(detached)" : candidate.branch;
54
+ var candidateBranchName = (candidate) => candidate.kind === "worktree" ? candidate.worktree.branch : candidate.branch;
55
+
56
+ // src/commands/pick.ts
57
+ var pick = async (git, fs, options) => {
58
+ const context = await loadRepoContext(git, fs, options.cwd);
59
+ const [localBranches, remoteBranches, dirtyEntries] = await Promise.all([
60
+ git.listBranches(context.rootPath),
61
+ git.listRemoteBranches(context.rootPath),
62
+ Promise.all(context.worktrees.map(async (worktree) => [
63
+ worktree.path,
64
+ await worktreeDirtyState(git, fs, context.worktrees, worktree)
65
+ ]))
66
+ ]);
67
+ const dirtyByPath = new Map(dirtyEntries);
68
+ const candidates = buildPickCandidates(context.worktrees, dirtyByPath, localBranches, remoteBranches);
69
+ return ok({ data: { candidates } });
70
+ };
71
+
72
+ // src/ui/picker-render.ts
73
+ import { homedir } from "node:os";
74
+
75
+ // src/ui/ansi.ts
76
+ var ESC = "\x1B";
77
+
78
+ // src/ui/picker-frame.ts
79
+ var CURSOR_HOME = `${ESC}[H`;
80
+ var CLEAR_TO_END = `${ESC}[J`;
81
+ var SGR_RESET = `${ESC}[0m`;
82
+ var SGR_CODES = {
83
+ plain: "",
84
+ cyan: `${ESC}[36m`,
85
+ dim: `${ESC}[2m`,
86
+ "bold-dim": `${ESC}[1m${ESC}[2m`,
87
+ inverse: `${ESC}[7m`,
88
+ red: `${ESC}[31m`,
89
+ green: `${ESC}[32m`,
90
+ yellow: `${ESC}[33m`,
91
+ magenta: `${ESC}[35m`
92
+ };
93
+ var renderSpan = (span, colorEnabled) => {
94
+ const style = span.style ?? "plain";
95
+ if (!colorEnabled || style === "plain") {
96
+ return span.text;
97
+ }
98
+ return `${SGR_CODES[style]}${span.text}${SGR_RESET}`;
99
+ };
100
+ var lineWidth = (line) => line.reduce((total, span) => total + displayWidth(span.text), 0);
101
+ var renderLine = (line, colorEnabled) => line.map((span) => renderSpan(span, colorEnabled)).join("");
102
+ var padRenderedLine = (line, rendered, width) => {
103
+ const gap = Math.max(0, width - lineWidth(line));
104
+ return gap === 0 ? rendered : rendered + " ".repeat(gap);
105
+ };
106
+ var GUTTER = " ";
107
+ var buildSideBySide = (left, right, colorEnabled) => {
108
+ const leftWidth = Math.max(0, ...left.map((line) => lineWidth(line)));
109
+ const rowCount = Math.max(left.length, right.length);
110
+ const rows = [];
111
+ for (let index = 0;index < rowCount; index += 1) {
112
+ const leftLine = left[index] ?? [];
113
+ const rightLine = right[index] ?? [];
114
+ const renderedLeft = padRenderedLine(leftLine, renderLine(leftLine, colorEnabled), leftWidth);
115
+ const renderedRight = renderLine(rightLine, colorEnabled);
116
+ rows.push(index < left.length ? `${renderedLeft}${GUTTER}${renderedRight}` : renderedRight);
117
+ }
118
+ return rows;
119
+ };
120
+ var BOX_TOP_LEFT = "╭";
121
+ var BOX_TOP_RIGHT = "╮";
122
+ var BOX_BOTTOM_LEFT = "╰";
123
+ var BOX_BOTTOM_RIGHT = "╯";
124
+ var BOX_HORIZONTAL = "─";
125
+ var BOX_VERTICAL = "│";
126
+ var BOX_PADDING = 1;
127
+ var BOX_PADDING_TOTAL = BOX_PADDING + BOX_PADDING;
128
+ var BOX_BORDER_WIDTH = 2;
129
+ var flattenToGraphemes = (line) => line.flatMap((span) => graphemes(span.text).map((text) => ({ text, style: span.style })));
130
+ var toStyledSpan = (char) => char.style === undefined ? { text: char.text } : { text: char.text, style: char.style };
131
+ var groupIntoSpans = (chars) => {
132
+ const spans = [];
133
+ for (const char of chars) {
134
+ const last = spans.at(-1);
135
+ if (last !== undefined && last.style === char.style) {
136
+ spans[spans.length - 1] = toStyledSpan({
137
+ text: last.text + char.text,
138
+ style: last.style
139
+ });
140
+ } else {
141
+ spans.push(toStyledSpan(char));
142
+ }
143
+ }
144
+ return spans;
145
+ };
146
+ var wrapLineToWidth = (line, width) => {
147
+ if (width <= 0) {
148
+ return [line];
149
+ }
150
+ const rows = [];
151
+ let current = [];
152
+ let currentWidth = 0;
153
+ for (const char of flattenToGraphemes(line)) {
154
+ const charWidth = displayWidth(char.text);
155
+ if (currentWidth + charWidth > width && current.length > 0) {
156
+ rows.push(groupIntoSpans(current));
157
+ current = [];
158
+ currentWidth = 0;
159
+ }
160
+ current.push(char);
161
+ currentWidth += charWidth;
162
+ }
163
+ if (current.length > 0 || rows.length === 0) {
164
+ rows.push(groupIntoSpans(current));
165
+ }
166
+ return rows;
167
+ };
168
+ var wrapInBox = (content, width) => {
169
+ const innerWidth = Math.max(0, width - BOX_BORDER_WIDTH - BOX_PADDING_TOTAL);
170
+ const horizontal = BOX_HORIZONTAL.repeat(Math.max(0, width - BOX_BORDER_WIDTH));
171
+ const pad = " ".repeat(BOX_PADDING);
172
+ const top = [{ text: `${BOX_TOP_LEFT}${horizontal}${BOX_TOP_RIGHT}` }];
173
+ const bottom = [{ text: `${BOX_BOTTOM_LEFT}${horizontal}${BOX_BOTTOM_RIGHT}` }];
174
+ const boxRow = (row) => {
175
+ const gap = " ".repeat(Math.max(0, innerWidth - lineWidth(row)));
176
+ return [{ text: `${BOX_VERTICAL}${pad}` }, ...row, { text: `${gap}${pad}${BOX_VERTICAL}` }];
177
+ };
178
+ const middle = content.flatMap((line) => {
179
+ const rows = lineWidth(line) <= innerWidth ? [line] : wrapLineToWidth(line, innerWidth);
180
+ return rows.map((row) => boxRow(row));
181
+ });
182
+ return [top, ...middle, bottom];
183
+ };
184
+ var ELLIPSIS = "…";
185
+ var truncateLineToWidth = (line, width) => {
186
+ if (lineWidth(line) <= width) {
187
+ return line;
188
+ }
189
+ const budget = Math.max(0, width - displayWidth(ELLIPSIS));
190
+ const kept = [];
191
+ let usedWidth = 0;
192
+ for (const char of flattenToGraphemes(line)) {
193
+ const charWidth = displayWidth(char.text);
194
+ if (usedWidth + charWidth > budget) {
195
+ break;
196
+ }
197
+ kept.push(char);
198
+ usedWidth += charWidth;
199
+ }
200
+ return [...groupIntoSpans(kept), { text: ELLIPSIS }];
201
+ };
202
+ var buildFrame = (input) => {
203
+ const { left, right, stacked, colorEnabled } = input;
204
+ const lines = buildFrameLines(left, right, stacked, colorEnabled);
205
+ return `${CURSOR_HOME}${CLEAR_TO_END}${lines.join(`\r
206
+ `)}`;
207
+ };
208
+ var buildFrameLines = (left, right, stacked, colorEnabled) => {
209
+ if (right === null) {
210
+ return left.map((line) => renderLine(line, colorEnabled));
211
+ }
212
+ if (stacked) {
213
+ return [...left, ...right].map((line) => renderLine(line, colorEnabled));
214
+ }
215
+ return buildSideBySide(left, right, colorEnabled);
216
+ };
217
+
218
+ // src/ui/picker-layout.ts
219
+ var MAX_PATH_LENGTH = 40;
220
+ var MAX_BRANCH_COLUMN_WIDTH = 24;
221
+ var LEGEND_TEXT = "●=dirty ○=clean +=not created";
222
+ var WORKTREE_KIND_LABELS = {
223
+ root: "root",
224
+ managed: "managed",
225
+ external: "ext"
226
+ };
227
+ var CREATABLE_SOURCE_LABELS = {
228
+ local: "local",
229
+ remote: "remote"
230
+ };
231
+ var statusMarker = (candidate) => {
232
+ if (candidate.kind === "creatable") {
233
+ return "+";
234
+ }
235
+ if (candidate.dirty === null) {
236
+ return " ";
237
+ }
238
+ return candidate.dirty ? "●" : "○";
239
+ };
240
+ var candidateKindLabel = (candidate) => candidate.kind === "worktree" ? WORKTREE_KIND_LABELS[candidate.worktree.kind] : CREATABLE_SOURCE_LABELS[candidate.source];
241
+ var isWorktreeCandidate = (candidate) => candidate.kind === "worktree";
242
+ var isCreatableCandidate = (candidate) => candidate.kind === "creatable";
243
+ var WORKTREE_KIND_ORDER = {
244
+ root: 0,
245
+ managed: 1,
246
+ external: 2
247
+ };
248
+ var CREATABLE_SOURCE_ORDER = {
249
+ local: 0,
250
+ remote: 1
251
+ };
252
+ var compareByBranchLabel = (a, b) => candidateBranchLabel(a).localeCompare(candidateBranchLabel(b));
253
+ var detachedRank = (candidate) => candidate.worktree.branch === null ? 1 : 0;
254
+ var compareWorktreeCandidates = (a, b) => {
255
+ const kindDiff = WORKTREE_KIND_ORDER[a.worktree.kind] - WORKTREE_KIND_ORDER[b.worktree.kind];
256
+ if (kindDiff !== 0) {
257
+ return kindDiff;
258
+ }
259
+ const detachedDiff = detachedRank(a) - detachedRank(b);
260
+ return detachedDiff === 0 ? compareByBranchLabel(a, b) : detachedDiff;
261
+ };
262
+ var compareCreatableCandidates = (a, b) => {
263
+ const sourceDiff = CREATABLE_SOURCE_ORDER[a.source] - CREATABLE_SOURCE_ORDER[b.source];
264
+ return sourceDiff === 0 ? compareByBranchLabel(a, b) : sourceDiff;
265
+ };
266
+ var sortCandidatesForDisplay = (candidates) => [
267
+ ...candidates.filter((candidate) => isWorktreeCandidate(candidate)).toSorted((a, b) => compareWorktreeCandidates(a, b)),
268
+ ...candidates.filter((candidate) => isCreatableCandidate(candidate)).toSorted((a, b) => compareCreatableCandidates(a, b))
269
+ ];
270
+ var KIND_COLUMN_WIDTH = Math.max(...Object.values(WORKTREE_KIND_LABELS).map((label) => label.length), ...Object.values(CREATABLE_SOURCE_LABELS).map((label) => label.length));
271
+ var shortenPath = (path, homeDir, maxLength = MAX_PATH_LENGTH) => {
272
+ const withTilde = homeDir.length > 0 && (path === homeDir || path.startsWith(`${homeDir}/`)) ? `~${path.slice(homeDir.length)}` : path;
273
+ return truncateToWidthKeepingTail(withTilde, maxLength);
274
+ };
275
+ var candidatePathLabel = (candidate, homeDir, maxLength) => candidate.kind === "worktree" ? shortenPath(candidate.worktree.path, homeDir, maxLength) : "";
276
+ var rawBranchColumnWidth = (candidates) => candidates.reduce((max, candidate) => Math.max(max, displayWidth(candidateBranchLabel(candidate))), 0);
277
+ var branchColumnWidth = (candidates) => Math.min(MAX_BRANCH_COLUMN_WIDTH, rawBranchColumnWidth(candidates));
278
+ var padBranchLabel = (label, width) => padToWidth(label, width);
279
+ var toCandidateRow = (candidate, options) => ({
280
+ kind: "candidate",
281
+ index: options.index,
282
+ section: options.section,
283
+ statusMarker: statusMarker(candidate),
284
+ branchLabel: padBranchLabel(truncateToWidth(candidateBranchLabel(candidate), options.branchWidth), options.branchWidth),
285
+ kindLabel: candidateKindLabel(candidate).padEnd(KIND_COLUMN_WIDTH, " "),
286
+ pathLabel: candidatePathLabel(candidate, options.homeDir, options.pathMaxLength)
287
+ });
288
+ var sectionRows = (label, section, entries, rowOptions) => {
289
+ if (entries.length === 0) {
290
+ return [];
291
+ }
292
+ return [
293
+ { kind: "header", label },
294
+ ...entries.map((entry) => toCandidateRow(entry.candidate, { ...rowOptions, index: entry.index, section }))
295
+ ];
296
+ };
297
+ var buildDisplayRows = (candidates, homeDir, indexOffset = 0, columnWidths) => {
298
+ const { branchWidth, pathMaxLength } = columnWidths ?? {
299
+ branchWidth: branchColumnWidth(candidates),
300
+ pathMaxLength: MAX_PATH_LENGTH
301
+ };
302
+ const indexed = candidates.map((candidate, index) => ({
303
+ candidate,
304
+ index: index + indexOffset
305
+ }));
306
+ const worktreeEntries = indexed.filter((entry) => isWorktreeCandidate(entry.candidate));
307
+ const branchEntries = indexed.filter((entry) => isCreatableCandidate(entry.candidate));
308
+ const rowOptions = { branchWidth, pathMaxLength, homeDir };
309
+ return [
310
+ ...sectionRows("WORKTREES", "worktree", worktreeEntries, rowOptions),
311
+ ...sectionRows("BRANCHES — Enter creates a worktree", "branch", branchEntries, rowOptions)
312
+ ];
313
+ };
314
+
315
+ // src/domain/actions.ts
316
+ var availableActions = (candidate) => {
317
+ const actions = ["cd"];
318
+ if (candidate.kind !== "worktree") {
319
+ actions.push("switchRoot");
320
+ return actions;
321
+ }
322
+ if (candidate.worktree.branch === null) {
323
+ return actions;
324
+ }
325
+ if (candidate.worktree.kind === "managed" || candidate.worktree.kind === "external") {
326
+ actions.push("delete");
327
+ }
328
+ if (candidate.worktree.kind !== "root") {
329
+ actions.push("switchRoot");
330
+ }
331
+ return actions;
332
+ };
333
+ var requiresDeleteConfirmation = (candidate) => candidate.kind === "worktree" && (candidate.worktree.kind === "external" || candidate.worktree.prunable);
334
+ var requiresSwitchRootConfirmation = (candidate) => candidate.kind === "worktree" && candidate.worktree.kind === "external";
335
+
336
+ // src/ui/side-panel.ts
337
+ var ACTION_LABELS = {
338
+ cd: "cd into this worktree",
339
+ delete: "delete worktree",
340
+ switchRoot: "switch root here"
341
+ };
342
+ var ACTION_LETTERS = {
343
+ cd: "c",
344
+ delete: "d",
345
+ switchRoot: "r"
346
+ };
347
+ var SIDE_PANEL_WIDTH = 34;
348
+ var buildActionPanelRows = ({
349
+ candidate,
350
+ panelIndex,
351
+ error,
352
+ busy
353
+ }) => {
354
+ const actions = availableActions(candidate);
355
+ const rows = [
356
+ [{ text: "Actions for " }, { text: candidateBranchLabel(candidate), style: "cyan" }],
357
+ ...actions.map((action, actionIndex) => {
358
+ const marker = actionIndex === panelIndex ? "> " : " ";
359
+ const text = `${marker}[${ACTION_LETTERS[action]}] ${ACTION_LABELS[action]}`;
360
+ return [{ text, style: actionIndex === panelIndex ? "inverse" : "plain" }];
361
+ })
362
+ ];
363
+ if (busy) {
364
+ rows.push([{ text: "Working…", style: "dim" }]);
365
+ }
366
+ if (error !== null) {
367
+ rows.push([{ text: error, style: "red" }]);
368
+ }
369
+ return rows;
370
+ };
371
+ var CONFIRM_QUESTIONS = {
372
+ delete: "Delete worktree for",
373
+ switchRoot: "Switch root here for"
374
+ };
375
+ var buildConfirmPanelRows = ({
376
+ candidate,
377
+ action
378
+ }) => {
379
+ const rows = [
380
+ [
381
+ { text: `${CONFIRM_QUESTIONS[action]} ` },
382
+ { text: candidateBranchName(candidate) ?? "", style: "cyan" },
383
+ { text: "?" }
384
+ ]
385
+ ];
386
+ if (action === "switchRoot") {
387
+ rows.push([{ text: "This will put it into detached HEAD.", style: "dim" }]);
388
+ }
389
+ if (action === "delete" && candidate.kind === "worktree" && candidate.worktree.prunable) {
390
+ rows.push([{ text: "Prunable, not dirty-checked:", style: "dim" }], [{ text: "removes just its registration.", style: "dim" }]);
391
+ }
392
+ rows.push([{ text: "(y/N)", style: "dim" }]);
393
+ return rows;
394
+ };
395
+
396
+ // src/ui/picker-side-by-side.ts
397
+ var SELECTION_MARKER_WIDTH = 2;
398
+ var STATUS_MARKER_WIDTH = 1;
399
+ var STATUS_MARKER_GAP_WIDTH = 1;
400
+ var BRANCH_KIND_GUTTER_WIDTH = 2;
401
+ var KIND_PATH_GAP_WIDTH = 1;
402
+ var CANDIDATE_ROW_CHROME_WIDTH = SELECTION_MARKER_WIDTH + STATUS_MARKER_WIDTH + STATUS_MARKER_GAP_WIDTH + BRANCH_KIND_GUTTER_WIDTH + KIND_PATH_GAP_WIDTH;
403
+ var HALF_DIVISOR = 2;
404
+ var MIN_PATH_DISPLAY_LENGTH = Math.ceil(MAX_PATH_LENGTH / HALF_DIVISOR);
405
+ var MAX_CANDIDATE_ROW_WIDTH = CANDIDATE_ROW_CHROME_WIDTH + MAX_BRANCH_COLUMN_WIDTH + KIND_COLUMN_WIDTH + MAX_PATH_LENGTH;
406
+ var MIN_SIDE_BY_SIDE_WIDTH = CANDIDATE_ROW_CHROME_WIDTH + MAX_BRANCH_COLUMN_WIDTH + KIND_COLUMN_WIDTH + MIN_PATH_DISPLAY_LENGTH + displayWidth(GUTTER) + SIDE_PANEL_WIDTH;
407
+ var isNarrowTerminal = (columns) => columns < MIN_SIDE_BY_SIDE_WIDTH;
408
+ var LIST_FOOTER_HINT = "Tab/→/Ctrl+L actions · Ctrl+X delete · Ctrl+R switch root · ↑↓/Ctrl+P,N,K,J move · Enter cd · Esc cancel";
409
+ var PANEL_FOOTER_HINT = "↑↓/Ctrl+P,N,K,J move · Enter run · c/d/r shortcuts · Esc/Tab/←/Ctrl+H close";
410
+ var constrainRowColumnWidths = (rawBranchWidth, maxRowWidth) => {
411
+ const availableForBranchAndPath = Math.max(0, maxRowWidth - CANDIDATE_ROW_CHROME_WIDTH - KIND_COLUMN_WIDTH);
412
+ const branchWidth = Math.min(rawBranchWidth, availableForBranchAndPath);
413
+ const pathMaxLength = Math.max(0, Math.min(MAX_PATH_LENGTH, availableForBranchAndPath - branchWidth));
414
+ return { branchWidth, pathMaxLength };
415
+ };
416
+ var SHORT_LIST_FOOTER_HINT = "↑↓ move · Enter cd · Esc cancel";
417
+ var SHORT_PANEL_FOOTER_HINT = "↑↓ move · Enter run · Esc close";
418
+ var footerHintForWidth = (kind, width) => {
419
+ const full = kind === "list" ? LIST_FOOTER_HINT : PANEL_FOOTER_HINT;
420
+ const short = kind === "list" ? SHORT_LIST_FOOTER_HINT : SHORT_PANEL_FOOTER_HINT;
421
+ if (displayWidth(full) <= width) {
422
+ return full;
423
+ }
424
+ if (displayWidth(short) <= width) {
425
+ return short;
426
+ }
427
+ return truncateToWidth(short, width);
428
+ };
429
+
430
+ // src/ui/picker-viewport.ts
431
+ var DEFAULT_TERMINAL_HEIGHT = 24;
432
+ var MIN_VISIBLE_CANDIDATE_ROWS = 3;
433
+ var BASE_CHROME_ROWS = 3;
434
+ var SECTION_HEADER_ROWS = 2;
435
+ var HIDDEN_COUNT_INDICATOR_ROWS = 2;
436
+ var rowBudget = ({ terminalHeight, stackedPanelRows }) => {
437
+ const reserved = BASE_CHROME_ROWS + SECTION_HEADER_ROWS + HIDDEN_COUNT_INDICATOR_ROWS + stackedPanelRows;
438
+ return Math.max(MIN_VISIBLE_CANDIDATE_ROWS, terminalHeight - reserved);
439
+ };
440
+ var computeViewport = (total, selectedIndex, budget) => {
441
+ if (total <= budget) {
442
+ return { start: 0, end: total, hiddenAbove: 0, hiddenBelow: 0 };
443
+ }
444
+ const HALF_DIVISOR = 2;
445
+ const half = Math.floor(budget / HALF_DIVISOR);
446
+ const maxStart = total - budget;
447
+ const start = Math.min(maxStart, Math.max(0, selectedIndex - half));
448
+ const end = start + budget;
449
+ return { start, end, hiddenAbove: start, hiddenBelow: total - end };
450
+ };
451
+
452
+ // src/ui/picker-render.ts
453
+ var QUERY_PREFIX = "hop: ";
454
+ var queryLine = (query, maxWidth) => {
455
+ const hint = query.length === 0 ? " (type to filter)" : "";
456
+ const budget = Math.max(0, maxWidth - QUERY_PREFIX.length - hint.length);
457
+ return [
458
+ { text: QUERY_PREFIX },
459
+ { text: truncateToWidthKeepingTail(query, budget), style: "cyan" },
460
+ { text: hint, style: "dim" }
461
+ ];
462
+ };
463
+ var hiddenCountLine = (count, direction) => {
464
+ if (count === 0) {
465
+ return null;
466
+ }
467
+ const arrow = direction === "above" ? "↑ " : "↓ ";
468
+ const word = direction === "above" ? "above" : "below";
469
+ return [
470
+ {
471
+ text: `${arrow}${count} more ${word} (keep typing to narrow down)`,
472
+ style: "dim"
473
+ }
474
+ ];
475
+ };
476
+ var STATUS_MARKER_STYLES = {
477
+ "●": "yellow",
478
+ "○": "green"
479
+ };
480
+ var KIND_LABEL_STYLES = {
481
+ root: "magenta",
482
+ managed: "cyan",
483
+ ext: "yellow"
484
+ };
485
+ var candidateRowLine = (row, selected) => {
486
+ if (selected) {
487
+ const text = `❯ ${row.statusMarker} ${row.branchLabel} ${row.kindLabel} ${row.pathLabel}`;
488
+ return [{ text, style: "inverse" }];
489
+ }
490
+ if (row.section === "branch") {
491
+ return [
492
+ {
493
+ text: ` ${row.statusMarker} ${row.branchLabel} ${row.kindLabel} ${row.pathLabel}`,
494
+ style: "dim"
495
+ }
496
+ ];
497
+ }
498
+ return [
499
+ { text: " " },
500
+ { text: row.statusMarker, style: STATUS_MARKER_STYLES[row.statusMarker] ?? "plain" },
501
+ { text: ` ${row.branchLabel} ` },
502
+ { text: row.kindLabel, style: KIND_LABEL_STYLES[row.kindLabel.trim()] ?? "plain" },
503
+ { text: ` ${row.pathLabel}`, style: "dim" }
504
+ ];
505
+ };
506
+ var displayRowLine = (row, clampedIndex) => row.kind === "header" ? [{ text: row.label, style: "bold-dim" }] : candidateRowLine(row, row.index === clampedIndex);
507
+ var buildListLines = ({
508
+ query,
509
+ maxWidth,
510
+ rows,
511
+ clampedIndex,
512
+ hiddenAbove,
513
+ hiddenBelow,
514
+ footerHint
515
+ }) => {
516
+ const lines = [queryLine(query, maxWidth)];
517
+ if (rows.length === 0) {
518
+ lines.push([{ text: "No matches.", style: "dim" }]);
519
+ }
520
+ const above = hiddenCountLine(hiddenAbove, "above");
521
+ if (above !== null) {
522
+ lines.push(above);
523
+ }
524
+ for (const row of rows) {
525
+ lines.push(displayRowLine(row, clampedIndex));
526
+ }
527
+ const below = hiddenCountLine(hiddenBelow, "below");
528
+ if (below !== null) {
529
+ lines.push(below);
530
+ }
531
+ lines.push([{ text: `(${LEGEND_TEXT})`, style: "dim" }], [{ text: footerHint, style: "dim" }]);
532
+ return lines;
533
+ };
534
+ var buildSidePanelLines = (mode, panelIndex, busy) => {
535
+ if (mode.kind === "panel") {
536
+ return wrapInBox(buildActionPanelRows({
537
+ candidate: mode.candidate,
538
+ panelIndex,
539
+ error: mode.error,
540
+ busy
541
+ }), SIDE_PANEL_WIDTH);
542
+ }
543
+ if (mode.kind === "confirm") {
544
+ return wrapInBox(buildConfirmPanelRows({ candidate: mode.candidate, action: mode.action }), SIDE_PANEL_WIDTH);
545
+ }
546
+ return null;
547
+ };
548
+ var renderPickerFrame = ({
549
+ snapshot,
550
+ width,
551
+ height,
552
+ colorEnabled: color
553
+ }) => {
554
+ const { query, filtered, clampedIndex, mode, panelIndex, busy } = snapshot;
555
+ const narrow = isNarrowTerminal(width);
556
+ const right = buildSidePanelLines(mode, panelIndex, busy);
557
+ const stacked = right !== null && narrow;
558
+ const listColumnWidth = right !== null && !stacked ? width - GUTTER.length - SIDE_PANEL_WIDTH : width;
559
+ const stackedPanelRows = stacked && right !== null ? right.length : 0;
560
+ const budget = rowBudget({ terminalHeight: height, stackedPanelRows });
561
+ const viewport = computeViewport(filtered.length, clampedIndex, budget);
562
+ const visible = filtered.slice(viewport.start, viewport.end);
563
+ const columnWidths = constrainRowColumnWidths(rawBranchColumnWidth(visible), listColumnWidth);
564
+ const rows = buildDisplayRows(visible, homedir(), viewport.start, columnWidths);
565
+ const left = buildListLines({
566
+ query,
567
+ maxWidth: listColumnWidth,
568
+ rows,
569
+ clampedIndex,
570
+ hiddenAbove: viewport.hiddenAbove,
571
+ hiddenBelow: viewport.hiddenBelow,
572
+ footerHint: footerHintForWidth(right === null ? "list" : "panel", listColumnWidth)
573
+ }).map((line) => truncateLineToWidth(line, listColumnWidth));
574
+ return buildFrame({ left, right, stacked, colorEnabled: color });
575
+ };
576
+
577
+ // src/ui/picker-keys.ts
578
+ var isCtrlJByte = (input) => input === `
579
+ `;
580
+ var resolvePickerKeyAction = (input, key) => {
581
+ if (key.escape) {
582
+ return { type: "cancel", reason: "esc" };
583
+ }
584
+ if (key.ctrl && input === "c") {
585
+ return { type: "cancel", reason: "ctrlC" };
586
+ }
587
+ if (key.return) {
588
+ return { type: "select" };
589
+ }
590
+ if (key.tab || key.rightArrow || key.ctrl && (input === "l" || input === "f")) {
591
+ return { type: "openPanel" };
592
+ }
593
+ if (key.ctrl && input === "x") {
594
+ return { type: "deleteShortcut" };
595
+ }
596
+ if (key.ctrl && input === "r") {
597
+ return { type: "rootSwitchShortcut" };
598
+ }
599
+ if (key.upArrow || key.ctrl && (input === "p" || input === "k")) {
600
+ return { type: "up" };
601
+ }
602
+ if (key.downArrow || key.ctrl && (input === "n" || input === "j") || isCtrlJByte(input)) {
603
+ return { type: "down" };
604
+ }
605
+ if (key.ctrl && input === "u") {
606
+ return { type: "clear" };
607
+ }
608
+ if (key.backspace || key.delete) {
609
+ return { type: "backspace" };
610
+ }
611
+ if (input.length > 0 && !key.ctrl && !key.meta) {
612
+ return { type: "char", char: input };
613
+ }
614
+ return { type: "ignore" };
615
+ };
616
+ var PANEL_LETTER_SHORTCUTS = ["c", "d", "r"];
617
+ var resolvePanelKeyAction = (input, key) => {
618
+ if (key.escape || key.tab || key.leftArrow || key.backspace || key.ctrl && input === "h") {
619
+ return { type: "close" };
620
+ }
621
+ if (key.return) {
622
+ return { type: "confirm" };
623
+ }
624
+ if (key.upArrow || key.ctrl && (input === "p" || input === "k")) {
625
+ return { type: "up" };
626
+ }
627
+ if (key.downArrow || key.ctrl && (input === "n" || input === "j") || isCtrlJByte(input)) {
628
+ return { type: "down" };
629
+ }
630
+ if (!key.ctrl && !key.meta && isPanelLetterShortcut(input)) {
631
+ return { type: "letter", char: input };
632
+ }
633
+ return { type: "ignore" };
634
+ };
635
+ var isPanelLetterShortcut = (input) => PANEL_LETTER_SHORTCUTS.includes(input);
636
+ var resolveConfirmKeyAction = (input, key) => {
637
+ if (!key.ctrl && !key.meta && (input === "y" || input === "Y")) {
638
+ return { type: "yes" };
639
+ }
640
+ return { type: "no" };
641
+ };
642
+
643
+ // src/ui/picker-input.ts
644
+ var requiresConfirmation = (candidate, action) => {
645
+ if (action === "delete") {
646
+ return requiresDeleteConfirmation(candidate);
647
+ }
648
+ if (action === "switchRoot") {
649
+ return requiresSwitchRootConfirmation(candidate);
650
+ }
651
+ return false;
652
+ };
653
+ var handleConfirmInput = (input, key, confirmMode, ctx) => {
654
+ const confirmAction = resolveConfirmKeyAction(input, key);
655
+ if (confirmAction.type === "yes") {
656
+ ctx.runAction(confirmMode.candidate, confirmMode.action);
657
+ } else {
658
+ ctx.setMode({ kind: "list" });
659
+ }
660
+ };
661
+ var runOrConfirm = (candidate, action, ctx) => {
662
+ if ((action === "delete" || action === "switchRoot") && requiresConfirmation(candidate, action)) {
663
+ ctx.setMode({ kind: "confirm", action, candidate });
664
+ return;
665
+ }
666
+ ctx.runAction(candidate, action);
667
+ };
668
+ var handlePanelInput = (input, key, panelMode, ctx) => {
669
+ const actions = availableActions(panelMode.candidate);
670
+ const panelAction = resolvePanelKeyAction(input, key);
671
+ switch (panelAction.type) {
672
+ case "close": {
673
+ ctx.setMode({ kind: "list" });
674
+ break;
675
+ }
676
+ case "up": {
677
+ ctx.setPanelIndex((current) => Math.max(0, current - 1));
678
+ break;
679
+ }
680
+ case "down": {
681
+ ctx.setPanelIndex((current) => Math.min(actions.length - 1, current + 1));
682
+ break;
683
+ }
684
+ case "confirm": {
685
+ const chosen = actions[Math.min(ctx.panelIndex, actions.length - 1)];
686
+ if (chosen !== undefined) {
687
+ runOrConfirm(panelMode.candidate, chosen, ctx);
688
+ }
689
+ break;
690
+ }
691
+ case "letter": {
692
+ const chosen = actions.find((action) => ACTION_LETTERS[action] === panelAction.char);
693
+ if (chosen !== undefined) {
694
+ runOrConfirm(panelMode.candidate, chosen, ctx);
695
+ }
696
+ break;
697
+ }
698
+ case "ignore": {
699
+ break;
700
+ }
701
+ }
702
+ };
703
+ var handleListInput = (input, key, ctx) => {
704
+ const action = resolvePickerKeyAction(input, key);
705
+ switch (action.type) {
706
+ case "cancel": {
707
+ ctx.onCancel(action.reason);
708
+ break;
709
+ }
710
+ case "select": {
711
+ if (ctx.selectedCandidate !== undefined) {
712
+ ctx.runAction(ctx.selectedCandidate, "cd");
713
+ }
714
+ break;
715
+ }
716
+ case "up": {
717
+ ctx.setIndex((current) => Math.max(0, current - 1));
718
+ break;
719
+ }
720
+ case "down": {
721
+ ctx.setIndex((current) => Math.min(ctx.filteredLength - 1, current + 1));
722
+ break;
723
+ }
724
+ case "clear": {
725
+ ctx.setQuery(() => "");
726
+ ctx.setIndex(() => 0);
727
+ break;
728
+ }
729
+ case "backspace": {
730
+ ctx.setQuery((current) => current.slice(0, -1));
731
+ ctx.setIndex(() => 0);
732
+ break;
733
+ }
734
+ case "char": {
735
+ ctx.setQuery((current) => current + action.char);
736
+ ctx.setIndex(() => 0);
737
+ break;
738
+ }
739
+ case "openPanel": {
740
+ if (ctx.selectedCandidate !== undefined) {
741
+ ctx.setPanelIndex(() => 0);
742
+ ctx.setMode({
743
+ kind: "panel",
744
+ candidate: ctx.selectedCandidate,
745
+ error: null
746
+ });
747
+ }
748
+ break;
749
+ }
750
+ case "deleteShortcut": {
751
+ if (ctx.selectedCandidate !== undefined && availableActions(ctx.selectedCandidate).includes("delete")) {
752
+ ctx.setMode({
753
+ kind: "confirm",
754
+ action: "delete",
755
+ candidate: ctx.selectedCandidate
756
+ });
757
+ }
758
+ break;
759
+ }
760
+ case "rootSwitchShortcut": {
761
+ if (ctx.selectedCandidate !== undefined && availableActions(ctx.selectedCandidate).includes("switchRoot")) {
762
+ runOrConfirm(ctx.selectedCandidate, "switchRoot", ctx);
763
+ }
764
+ break;
765
+ }
766
+ case "ignore": {
767
+ break;
768
+ }
769
+ }
770
+ };
771
+
772
+ // src/ui/picker-store.ts
773
+ var matchesQuery = (candidate, query) => query.length === 0 || candidateBranchLabel(candidate).toLowerCase().includes(query.toLowerCase());
774
+ var panelErrorTransition = (candidate, error) => ({
775
+ mode: { kind: "panel", candidate, error },
776
+ panelIndex: 0
777
+ });
778
+ var createFilteredSelector = () => {
779
+ let cache = null;
780
+ return (state) => {
781
+ if (cache !== null && cache.candidates === state.candidates && cache.query === state.query) {
782
+ return cache.result;
783
+ }
784
+ const result = sortCandidatesForDisplay(state.candidates.filter((candidate) => matchesQuery(candidate, state.query)));
785
+ cache = { candidates: state.candidates, query: state.query, result };
786
+ return result;
787
+ };
788
+ };
789
+ var runMutation = async (callbacks, candidate, action) => {
790
+ if (action === "switchRoot") {
791
+ const result = await callbacks.switchRootHere(candidate);
792
+ if (!result.ok || result.path === undefined) {
793
+ return {
794
+ type: "patch",
795
+ patch: panelErrorTransition(candidate, result.message ?? "Failed to switch root.")
796
+ };
797
+ }
798
+ return { type: "exit", outcome: { type: "path", path: result.path } };
799
+ }
800
+ const result = await callbacks.deleteWorktree(candidate);
801
+ if (!result.ok) {
802
+ return {
803
+ type: "patch",
804
+ patch: panelErrorTransition(candidate, result.message ?? "Failed to delete worktree.")
805
+ };
806
+ }
807
+ const fresh = await callbacks.reloadCandidates();
808
+ return {
809
+ type: "patch",
810
+ patch: { candidates: fresh, mode: { kind: "list" }, index: 0 }
811
+ };
812
+ };
813
+ var createExitGuard = (onExit, onCancel) => {
814
+ let exited = false;
815
+ return {
816
+ isExited: () => exited,
817
+ guardedOnExit: (outcome) => {
818
+ exited = true;
819
+ onExit(outcome);
820
+ },
821
+ guardedOnCancel: (reason) => {
822
+ exited = true;
823
+ onCancel(reason);
824
+ }
825
+ };
826
+ };
827
+ var createPickerStore = (initialCandidates, callbacks, onExit, onCancel) => {
828
+ let state = {
829
+ candidates: initialCandidates,
830
+ query: "",
831
+ index: 0,
832
+ mode: { kind: "list" },
833
+ panelIndex: 0,
834
+ busy: false
835
+ };
836
+ const { isExited, guardedOnExit, guardedOnCancel } = createExitGuard(onExit, onCancel);
837
+ const listeners = new Set;
838
+ const getFiltered = createFilteredSelector();
839
+ let snapshotCache = null;
840
+ const notify = () => {
841
+ snapshotCache = null;
842
+ for (const listener of listeners) {
843
+ listener();
844
+ }
845
+ };
846
+ const setState = (patch) => {
847
+ state = { ...state, ...patch };
848
+ notify();
849
+ };
850
+ const getSnapshot = () => {
851
+ if (snapshotCache !== null) {
852
+ return snapshotCache;
853
+ }
854
+ const filtered = getFiltered(state);
855
+ const clampedIndex = Math.min(state.index, Math.max(filtered.length - 1, 0));
856
+ snapshotCache = {
857
+ query: state.query,
858
+ filtered,
859
+ clampedIndex,
860
+ mode: state.mode,
861
+ panelIndex: state.panelIndex,
862
+ busy: state.busy
863
+ };
864
+ return snapshotCache;
865
+ };
866
+ const runAction = (candidate, action) => {
867
+ if (action === "cd") {
868
+ guardedOnExit({ type: "cd", candidate });
869
+ return;
870
+ }
871
+ if (state.busy) {
872
+ return;
873
+ }
874
+ setState({ busy: true });
875
+ (async () => {
876
+ const outcome = await runMutation(callbacks, candidate, action);
877
+ if (outcome.type === "exit") {
878
+ guardedOnExit(outcome.outcome);
879
+ return;
880
+ }
881
+ setState({ ...outcome.patch, busy: false });
882
+ })();
883
+ };
884
+ const setIndex = (updater) => {
885
+ setState({ index: updater(state.index) });
886
+ };
887
+ const setQuery = (updater) => {
888
+ setState({ query: updater(state.query) });
889
+ };
890
+ const setPanelIndex = (updater) => {
891
+ setState({ panelIndex: updater(state.panelIndex) });
892
+ };
893
+ const setMode = (mode) => {
894
+ setState({ mode });
895
+ };
896
+ const handleInput = (input, key) => {
897
+ if (isExited() || state.busy) {
898
+ return;
899
+ }
900
+ const { mode, panelIndex } = state;
901
+ if (mode.kind === "confirm") {
902
+ handleConfirmInput(input, key, mode, { runAction, setMode });
903
+ return;
904
+ }
905
+ if (mode.kind === "panel") {
906
+ handlePanelInput(input, key, mode, {
907
+ panelIndex,
908
+ runAction,
909
+ setPanelIndex,
910
+ setMode
911
+ });
912
+ return;
913
+ }
914
+ const { filtered, clampedIndex } = getSnapshot();
915
+ handleListInput(input, key, {
916
+ selectedCandidate: filtered[clampedIndex],
917
+ filteredLength: filtered.length,
918
+ runAction,
919
+ onCancel: guardedOnCancel,
920
+ setIndex,
921
+ setQuery,
922
+ setPanelIndex,
923
+ setMode
924
+ });
925
+ };
926
+ const subscribe = (listener) => {
927
+ listeners.add(listener);
928
+ return () => {
929
+ listeners.delete(listener);
930
+ };
931
+ };
932
+ return { getSnapshot, subscribe, handleInput };
933
+ };
934
+
935
+ // src/ui/alt-screen.ts
936
+ var ENTER_ALT_SCREEN = `${ESC}[?1049h`;
937
+ var LEAVE_ALT_SCREEN = `${ESC}[?1049l`;
938
+ var HIDE_CURSOR = `${ESC}[?25l`;
939
+ var SHOW_CURSOR = `${ESC}[?25h`;
940
+ var enterAltScreen = (target) => {
941
+ if (!target.isTTY) {
942
+ return;
943
+ }
944
+ target.write(ENTER_ALT_SCREEN + HIDE_CURSOR);
945
+ };
946
+ var leaveAltScreen = (target) => {
947
+ if (!target.isTTY) {
948
+ return;
949
+ }
950
+ target.write(SHOW_CURSOR + LEAVE_ALT_SCREEN);
951
+ };
952
+
953
+ // src/ui/picker-key-event.ts
954
+ var NO_MODIFIERS = {
955
+ ctrl: false,
956
+ meta: false,
957
+ escape: false,
958
+ return: false,
959
+ tab: false,
960
+ upArrow: false,
961
+ downArrow: false,
962
+ leftArrow: false,
963
+ rightArrow: false,
964
+ backspace: false,
965
+ delete: false
966
+ };
967
+ var event = (input, key = {}) => ({
968
+ input,
969
+ key: { ...NO_MODIFIERS, ...key }
970
+ });
971
+ var withMeta = (keyEvent) => ({
972
+ input: keyEvent.input,
973
+ key: { ...keyEvent.key, meta: true }
974
+ });
975
+
976
+ // src/ui/picker-key-csi.ts
977
+ var ARROW_FINAL_BYTE_A = "A".codePointAt(0);
978
+ var ARROW_FINAL_BYTE_B = "B".codePointAt(0);
979
+ var ARROW_FINAL_BYTE_C = "C".codePointAt(0);
980
+ var ARROW_FINAL_BYTE_D = "D".codePointAt(0);
981
+ var arrowModifierKey = (finalByte) => {
982
+ if (finalByte === ARROW_FINAL_BYTE_A) {
983
+ return "upArrow";
984
+ }
985
+ if (finalByte === ARROW_FINAL_BYTE_B) {
986
+ return "downArrow";
987
+ }
988
+ if (finalByte === ARROW_FINAL_BYTE_C) {
989
+ return "rightArrow";
990
+ }
991
+ if (finalByte === ARROW_FINAL_BYTE_D) {
992
+ return "leftArrow";
993
+ }
994
+ return null;
995
+ };
996
+ var modifierBitsFromParam = (param) => {
997
+ const bits = param - 1;
998
+ const CTRL_BIT = 4;
999
+ const ALT_BIT = 2;
1000
+ return { ctrl: (bits & CTRL_BIT) !== 0, meta: (bits & ALT_BIT) !== 0 };
1001
+ };
1002
+ var CSI_FINAL_BYTE_MIN = 64;
1003
+ var CSI_FINAL_BYTE_MAX = 126;
1004
+ var isCsiFinalByte = (byte) => byte >= CSI_FINAL_BYTE_MIN && byte <= CSI_FINAL_BYTE_MAX;
1005
+ var CTRL_C_BYTE = 3;
1006
+ var BRACKETED_PASTE_START = "200~";
1007
+ var BRACKETED_PASTE_END = "201~";
1008
+ var modifiedArrowOrDelete = (paramText, finalByte) => {
1009
+ const MODIFIED_PATTERN = /^\d+;\d+$/u;
1010
+ const DELETE_PARAM = "3";
1011
+ if (finalByte === "~" && (paramText === DELETE_PARAM || paramText.startsWith(`${DELETE_PARAM};`))) {
1012
+ return event("", { delete: true });
1013
+ }
1014
+ const arrowKey = arrowModifierKey(finalByte.codePointAt(0));
1015
+ if (arrowKey === null || !(paramText === "" || MODIFIED_PATTERN.test(paramText))) {
1016
+ return null;
1017
+ }
1018
+ const [, modifierParam] = paramText.split(";");
1019
+ const mods = modifierParam === undefined ? {} : modifierBitsFromParam(Number(modifierParam));
1020
+ return event("", { [arrowKey]: true, ...mods });
1021
+ };
1022
+ var parseCsi = (buf) => {
1023
+ const CSI_PREFIX_LENGTH = 2;
1024
+ let index = CSI_PREFIX_LENGTH;
1025
+ let paramText = "";
1026
+ while (index < buf.length && !isCsiFinalByte(buf[index])) {
1027
+ const paramByte = buf[index];
1028
+ if (paramByte === CTRL_C_BYTE) {
1029
+ return {
1030
+ kind: "interrupted",
1031
+ consumed: index + 1,
1032
+ event: event("c", { ctrl: true })
1033
+ };
1034
+ }
1035
+ paramText += String.fromCodePoint(paramByte);
1036
+ index += 1;
1037
+ }
1038
+ if (index >= buf.length) {
1039
+ return { kind: "incomplete" };
1040
+ }
1041
+ const finalByte = String.fromCodePoint(buf[index]);
1042
+ const consumed = index + 1;
1043
+ const full = paramText + finalByte;
1044
+ if (full === BRACKETED_PASTE_START) {
1045
+ return { kind: "resolved", consumed, event: null, pasteStarted: true };
1046
+ }
1047
+ if (full === BRACKETED_PASTE_END) {
1048
+ return { kind: "resolved", consumed, event: null, pasteEnded: true };
1049
+ }
1050
+ return {
1051
+ kind: "resolved",
1052
+ consumed,
1053
+ event: modifiedArrowOrDelete(paramText, finalByte)
1054
+ };
1055
+ };
1056
+ var SS3_SEQUENCE_LENGTH = 3;
1057
+ var parseSs3 = (buf) => {
1058
+ if (buf.length < SS3_SEQUENCE_LENGTH) {
1059
+ return { kind: "incomplete" };
1060
+ }
1061
+ const finalByte = buf[2];
1062
+ if (finalByte === CTRL_C_BYTE) {
1063
+ return {
1064
+ kind: "interrupted",
1065
+ consumed: SS3_SEQUENCE_LENGTH,
1066
+ event: event("c", { ctrl: true })
1067
+ };
1068
+ }
1069
+ const arrowKey = arrowModifierKey(finalByte);
1070
+ return {
1071
+ kind: "resolved",
1072
+ consumed: SS3_SEQUENCE_LENGTH,
1073
+ event: arrowKey === null ? null : event("", { [arrowKey]: true })
1074
+ };
1075
+ };
1076
+
1077
+ // src/ui/picker-key-parser.ts
1078
+ var ESC2 = 27;
1079
+ var BACKSPACE_DEL = 127;
1080
+ var BACKSPACE_BS = 8;
1081
+ var TAB = 9;
1082
+ var LF = 10;
1083
+ var CR = 13;
1084
+ var CTRL_LETTER_START = 1;
1085
+ var CTRL_LETTER_END = 26;
1086
+ var CTRL_LETTER_OFFSET = "a".codePointAt(0);
1087
+ var ctrlLetterFromByte = (byte) => String.fromCodePoint(byte + CTRL_LETTER_OFFSET - 1);
1088
+ var ONE_BYTE_MAX = 127;
1089
+ var TWO_BYTE_MASK = 224;
1090
+ var TWO_BYTE_PREFIX = 192;
1091
+ var THREE_BYTE_MASK = 240;
1092
+ var THREE_BYTE_PREFIX = 224;
1093
+ var FOUR_BYTE_MASK = 248;
1094
+ var FOUR_BYTE_PREFIX = 240;
1095
+ var UTF8_ONE_BYTE_LENGTH = 1;
1096
+ var UTF8_TWO_BYTE_LENGTH = 2;
1097
+ var UTF8_THREE_BYTE_LENGTH = 3;
1098
+ var UTF8_FOUR_BYTE_LENGTH = 4;
1099
+ var utf8SequenceLength = (leadByte) => {
1100
+ if (leadByte <= ONE_BYTE_MAX) {
1101
+ return UTF8_ONE_BYTE_LENGTH;
1102
+ }
1103
+ if ((leadByte & TWO_BYTE_MASK) === TWO_BYTE_PREFIX) {
1104
+ return UTF8_TWO_BYTE_LENGTH;
1105
+ }
1106
+ if ((leadByte & THREE_BYTE_MASK) === THREE_BYTE_PREFIX) {
1107
+ return UTF8_THREE_BYTE_LENGTH;
1108
+ }
1109
+ if ((leadByte & FOUR_BYTE_MASK) === FOUR_BYTE_PREFIX) {
1110
+ return UTF8_FOUR_BYTE_LENGTH;
1111
+ }
1112
+ return UTF8_ONE_BYTE_LENGTH;
1113
+ };
1114
+ var decodeUtf8 = (bytes) => new TextDecoder("utf-8", { fatal: false }).decode(bytes);
1115
+ var CONTROL_CODEPOINT_MAX = 31;
1116
+ var DEL_CODEPOINT = 127;
1117
+ var C1_CONTROL_MIN = 128;
1118
+ var C1_CONTROL_MAX = 159;
1119
+ var isPrintableChar = (char) => {
1120
+ const codePoint = char.codePointAt(0) ?? 0;
1121
+ if (codePoint <= CONTROL_CODEPOINT_MAX || codePoint === DEL_CODEPOINT) {
1122
+ return false;
1123
+ }
1124
+ return codePoint < C1_CONTROL_MIN || codePoint > C1_CONTROL_MAX;
1125
+ };
1126
+ var pastedTextToEvents = (text) => [...text].filter((char) => isPrintableChar(char)).map((char) => event(char));
1127
+ var CSI_INTRODUCER_CODE = "[".codePointAt(0);
1128
+ var SS3_INTRODUCER_CODE = "O".codePointAt(0);
1129
+ var MAX_PENDING_SEQUENCE_BYTES = 32;
1130
+ var PASTE_END_MARKER = Buffer.concat([
1131
+ Buffer.from([ESC2]),
1132
+ Buffer.from(`[${BRACKETED_PASTE_END}`)
1133
+ ]);
1134
+
1135
+ class PickerKeyParser {
1136
+ buffer = Buffer.alloc(0);
1137
+ inPaste = false;
1138
+ hasPendingEscape() {
1139
+ return !this.inPaste && this.buffer.length === 1 && this.buffer[0] === ESC2;
1140
+ }
1141
+ flushPendingEscape() {
1142
+ if (!this.hasPendingEscape()) {
1143
+ return null;
1144
+ }
1145
+ this.buffer = Buffer.alloc(0);
1146
+ return event("", { escape: true });
1147
+ }
1148
+ feed(chunk) {
1149
+ this.buffer = Buffer.concat([this.buffer, chunk]);
1150
+ const results = [];
1151
+ let step = this.inPaste ? this.stepPaste() : this.step();
1152
+ while (step !== null) {
1153
+ results.push(...step);
1154
+ step = this.inPaste ? this.stepPaste() : this.step();
1155
+ }
1156
+ return results;
1157
+ }
1158
+ stepPaste() {
1159
+ if (this.buffer.length === 0) {
1160
+ return null;
1161
+ }
1162
+ const markerIndex = this.buffer.indexOf(PASTE_END_MARKER);
1163
+ if (markerIndex !== -1) {
1164
+ const text = decodeUtf8(this.buffer.subarray(0, markerIndex));
1165
+ this.buffer = this.buffer.subarray(markerIndex + PASTE_END_MARKER.length);
1166
+ this.inPaste = false;
1167
+ return pastedTextToEvents(text);
1168
+ }
1169
+ const maxOverlap = Math.min(PASTE_END_MARKER.length - 1, this.buffer.length);
1170
+ let overlap = 0;
1171
+ for (let candidate = maxOverlap;candidate > 0; candidate -= 1) {
1172
+ const tail = this.buffer.subarray(this.buffer.length - candidate);
1173
+ if (tail.equals(PASTE_END_MARKER.subarray(0, candidate))) {
1174
+ overlap = candidate;
1175
+ break;
1176
+ }
1177
+ }
1178
+ const safeLength = this.buffer.length - overlap;
1179
+ if (safeLength === 0) {
1180
+ return null;
1181
+ }
1182
+ const text = decodeUtf8(this.buffer.subarray(0, safeLength));
1183
+ this.buffer = this.buffer.subarray(safeLength);
1184
+ return text.length > 0 ? pastedTextToEvents(text) : null;
1185
+ }
1186
+ step() {
1187
+ if (this.buffer.length === 0) {
1188
+ return null;
1189
+ }
1190
+ const first = this.buffer[0];
1191
+ if (first === ESC2) {
1192
+ return this.stepEscape();
1193
+ }
1194
+ if (first === CR) {
1195
+ this.buffer = this.buffer.subarray(1);
1196
+ return [event("", { return: true })];
1197
+ }
1198
+ if (first === LF) {
1199
+ this.buffer = this.buffer.subarray(1);
1200
+ return [event(`
1201
+ `)];
1202
+ }
1203
+ if (first === TAB) {
1204
+ this.buffer = this.buffer.subarray(1);
1205
+ return [event("", { tab: true })];
1206
+ }
1207
+ if (first === BACKSPACE_DEL || first === BACKSPACE_BS) {
1208
+ this.buffer = this.buffer.subarray(1);
1209
+ return [event("", { backspace: true })];
1210
+ }
1211
+ if (first >= CTRL_LETTER_START && first <= CTRL_LETTER_END) {
1212
+ this.buffer = this.buffer.subarray(1);
1213
+ return [event(ctrlLetterFromByte(first), { ctrl: true })];
1214
+ }
1215
+ if (first <= CONTROL_CODEPOINT_MAX) {
1216
+ this.buffer = this.buffer.subarray(1);
1217
+ return [];
1218
+ }
1219
+ return this.stepUtf8Char();
1220
+ }
1221
+ stepEscape() {
1222
+ if (this.buffer.length === 1) {
1223
+ return null;
1224
+ }
1225
+ const second = this.buffer[1];
1226
+ if (second === CSI_INTRODUCER_CODE) {
1227
+ return this.applySequence(parseCsi(this.buffer));
1228
+ }
1229
+ if (second === SS3_INTRODUCER_CODE) {
1230
+ return this.applySequence(parseSs3(this.buffer));
1231
+ }
1232
+ this.buffer = this.buffer.subarray(1);
1233
+ const inner = this.stepUtf8Char();
1234
+ return inner === null ? null : inner.map((keyEvent) => withMeta(keyEvent));
1235
+ }
1236
+ applySequence(result) {
1237
+ if (result.kind === "incomplete") {
1238
+ if (this.buffer.length > MAX_PENDING_SEQUENCE_BYTES) {
1239
+ this.buffer = Buffer.alloc(0);
1240
+ }
1241
+ return null;
1242
+ }
1243
+ if (result.kind === "interrupted") {
1244
+ this.buffer = this.buffer.subarray(result.consumed);
1245
+ return [result.event];
1246
+ }
1247
+ this.buffer = this.buffer.subarray(result.consumed);
1248
+ if (result.pasteStarted === true) {
1249
+ this.inPaste = true;
1250
+ }
1251
+ if (result.pasteEnded === true) {
1252
+ this.inPaste = false;
1253
+ }
1254
+ return result.event === null ? [] : [result.event];
1255
+ }
1256
+ stepUtf8Char() {
1257
+ const leadByte = this.buffer[0];
1258
+ const length = utf8SequenceLength(leadByte);
1259
+ if (this.buffer.length < length) {
1260
+ return null;
1261
+ }
1262
+ const bytes = this.buffer.subarray(0, length);
1263
+ this.buffer = this.buffer.subarray(length);
1264
+ const char = decodeUtf8(bytes);
1265
+ return char.length > 0 ? [event(char)] : [];
1266
+ }
1267
+ }
1268
+
1269
+ // src/ui/terminal-session.ts
1270
+ var ENABLE_BRACKETED_PASTE = `${ESC}[?2004h`;
1271
+ var DISABLE_BRACKETED_PASTE = `${ESC}[?2004l`;
1272
+ var PENDING_ESCAPE_TIMEOUT_MS = 25;
1273
+ var EXIT_SIGTERM = 143;
1274
+ var EXIT_SIGHUP = 129;
1275
+ var defaultStreams = () => ({
1276
+ stdin: process.stdin,
1277
+ stderr: process.stderr
1278
+ });
1279
+ var runSafely = (step) => {
1280
+ try {
1281
+ step();
1282
+ } catch {}
1283
+ };
1284
+ var createCleanup = (deps) => {
1285
+ let hasCleanedUp = false;
1286
+ return () => {
1287
+ if (hasCleanedUp) {
1288
+ return;
1289
+ }
1290
+ hasCleanedUp = true;
1291
+ runSafely(deps.clearPendingEscapeTimer);
1292
+ if (deps.isInteractive) {
1293
+ runSafely(() => {
1294
+ deps.stdin.off("data", deps.handleData);
1295
+ });
1296
+ runSafely(() => {
1297
+ deps.stdin.setRawMode?.(false);
1298
+ });
1299
+ runSafely(() => {
1300
+ deps.stdin.pause();
1301
+ });
1302
+ runSafely(() => {
1303
+ deps.altScreenTarget.write(DISABLE_BRACKETED_PASTE);
1304
+ });
1305
+ }
1306
+ runSafely(() => {
1307
+ deps.stderr.off("resize", deps.handleResize);
1308
+ });
1309
+ runSafely(() => {
1310
+ leaveAltScreen(deps.altScreenTarget);
1311
+ });
1312
+ };
1313
+ };
1314
+ var installExitSignalHandler = (signal, code, cleanup) => {
1315
+ const handleSignal = () => {
1316
+ cleanup();
1317
+ process.exitCode = code;
1318
+ process.exit(code);
1319
+ };
1320
+ process.once(signal, handleSignal);
1321
+ return () => {
1322
+ process.removeListener(signal, handleSignal);
1323
+ };
1324
+ };
1325
+ var installExitSignalHandlers = (cleanup) => {
1326
+ const removeSigint = installExitSignalHandler("SIGINT", EXIT_CANCELLED, cleanup);
1327
+ const removeSigterm = installExitSignalHandler("SIGTERM", EXIT_SIGTERM, cleanup);
1328
+ const removeSighup = installExitSignalHandler("SIGHUP", EXIT_SIGHUP, cleanup);
1329
+ return () => {
1330
+ removeSigint();
1331
+ removeSigterm();
1332
+ removeSighup();
1333
+ };
1334
+ };
1335
+ var runTerminalSession = (createHandlers, streams = defaultStreams()) => new Promise((resolve) => {
1336
+ const { stdin, stderr } = streams;
1337
+ const altScreenTarget = {
1338
+ isTTY: stderr.isTTY === true,
1339
+ write: (data) => {
1340
+ stderr.write(data);
1341
+ }
1342
+ };
1343
+ const isInteractive = stdin.isTTY === true && altScreenTarget.isTTY;
1344
+ const parser = new PickerKeyParser;
1345
+ let pendingEscapeTimer = null;
1346
+ let renderScheduled = false;
1347
+ let finished = false;
1348
+ let handlers = null;
1349
+ const clearPendingEscapeTimer = () => {
1350
+ if (pendingEscapeTimer !== null) {
1351
+ clearTimeout(pendingEscapeTimer);
1352
+ pendingEscapeTimer = null;
1353
+ }
1354
+ };
1355
+ const paint = () => {
1356
+ renderScheduled = false;
1357
+ if (finished || handlers === null) {
1358
+ return;
1359
+ }
1360
+ altScreenTarget.write(handlers.buildFrame());
1361
+ };
1362
+ const requestRender = () => {
1363
+ if (finished || renderScheduled) {
1364
+ return;
1365
+ }
1366
+ renderScheduled = true;
1367
+ queueMicrotask(paint);
1368
+ };
1369
+ const finish = (result) => {
1370
+ if (finished) {
1371
+ return;
1372
+ }
1373
+ finished = true;
1374
+ cleanup();
1375
+ removeSignalListeners();
1376
+ resolve(result);
1377
+ };
1378
+ const scheduleEscapeFlush = () => {
1379
+ clearPendingEscapeTimer();
1380
+ pendingEscapeTimer = setTimeout(() => {
1381
+ pendingEscapeTimer = null;
1382
+ const flushed = parser.flushPendingEscape();
1383
+ if (flushed !== null) {
1384
+ handlers?.onKey(flushed);
1385
+ }
1386
+ }, PENDING_ESCAPE_TIMEOUT_MS);
1387
+ };
1388
+ const handleData = (chunk) => {
1389
+ for (const event of parser.feed(chunk)) {
1390
+ if (finished) {
1391
+ return;
1392
+ }
1393
+ handlers?.onKey(event);
1394
+ }
1395
+ if (finished) {
1396
+ return;
1397
+ }
1398
+ if (parser.hasPendingEscape()) {
1399
+ scheduleEscapeFlush();
1400
+ } else {
1401
+ clearPendingEscapeTimer();
1402
+ }
1403
+ };
1404
+ const handleResize = () => {
1405
+ requestRender();
1406
+ };
1407
+ const cleanup = createCleanup({
1408
+ stdin,
1409
+ stderr,
1410
+ altScreenTarget,
1411
+ isInteractive,
1412
+ handleData,
1413
+ handleResize,
1414
+ clearPendingEscapeTimer
1415
+ });
1416
+ const removeSignalListeners = installExitSignalHandlers(cleanup);
1417
+ process.once("exit", cleanup);
1418
+ enterAltScreen(altScreenTarget);
1419
+ if (isInteractive) {
1420
+ altScreenTarget.write(ENABLE_BRACKETED_PASTE);
1421
+ stdin.setRawMode?.(true);
1422
+ stdin.resume();
1423
+ stdin.on("data", handleData);
1424
+ }
1425
+ if (altScreenTarget.isTTY) {
1426
+ stderr.on("resize", handleResize);
1427
+ }
1428
+ handlers = createHandlers({ requestRender, finish });
1429
+ requestRender();
1430
+ });
1431
+
1432
+ // src/ui/picker.ts
1433
+ var DEFAULT_TERMINAL_WIDTH = 80;
1434
+ var terminalDimension = (value, fallback) => value !== undefined && value > 0 ? value : fallback;
1435
+ var colorEnabled = (stderr) => stderr.isTTY === true && (process.env["NO_COLOR"] ?? "") === "";
1436
+ var isCtrlC = (event) => event.key.ctrl && event.input === "c";
1437
+ var runPicker = (candidates, callbacks) => runTerminalSession(({ requestRender, finish }) => {
1438
+ const store = createPickerStore(candidates, callbacks, (outcome) => finish(outcome), (reason) => finish({ type: "cancelled", reason }));
1439
+ store.subscribe(requestRender);
1440
+ return {
1441
+ onKey: (event) => {
1442
+ if (isCtrlC(event)) {
1443
+ finish({ type: "cancelled", reason: "ctrlC" });
1444
+ return;
1445
+ }
1446
+ store.handleInput(event.input, event.key);
1447
+ },
1448
+ buildFrame: () => renderPickerFrame({
1449
+ snapshot: store.getSnapshot(),
1450
+ width: terminalDimension(process.stderr.columns, DEFAULT_TERMINAL_WIDTH),
1451
+ height: terminalDimension(process.stderr.rows, DEFAULT_TERMINAL_HEIGHT),
1452
+ colorEnabled: colorEnabled(process.stderr)
1453
+ })
1454
+ };
1455
+ });
1456
+
1457
+ // src/cli-pick.ts
1458
+ var loadPickCandidates = async (git, fs, json) => {
1459
+ const pickResult = await pick(git, fs, { cwd: process.cwd() });
1460
+ if (!pickResult.ok) {
1461
+ reportResult("pick", pickResult, json);
1462
+ return null;
1463
+ }
1464
+ return pickResult.data?.candidates ?? [];
1465
+ };
1466
+ var deleteWorktree = async (git, fs, candidate) => {
1467
+ const branch = candidateBranchName(candidate);
1468
+ if (branch === null) {
1469
+ return { ok: false, message: "This candidate has no branch to remove." };
1470
+ }
1471
+ const result = await rm2(git, fs, {
1472
+ cwd: process.cwd(),
1473
+ branch,
1474
+ ...candidate.kind === "worktree" ? { expectedPath: candidate.worktree.path } : {},
1475
+ allowPrunable: candidate.kind === "worktree" && candidate.worktree.prunable,
1476
+ force: false,
1477
+ ext: false
1478
+ });
1479
+ if (!result.ok) {
1480
+ return {
1481
+ ok: false,
1482
+ message: result.errorMessage ?? "Failed to remove worktree."
1483
+ };
1484
+ }
1485
+ return { ok: true };
1486
+ };
1487
+ var createPickerCallbacks = (git, fs, json, onSwitchedBranch) => ({
1488
+ deleteWorktree: (candidate) => deleteWorktree(git, fs, candidate),
1489
+ switchRootHere: async (candidate) => {
1490
+ const branch = candidateBranchName(candidate);
1491
+ if (branch === null) {
1492
+ return {
1493
+ ok: false,
1494
+ message: "This candidate has no branch to switch to."
1495
+ };
1496
+ }
1497
+ const result = await root2(git, fs, {
1498
+ cwd: process.cwd(),
1499
+ target: branch,
1500
+ allowExternalHolderSwap: candidate.kind === "worktree" && candidate.worktree.kind === "external",
1501
+ ...candidate.kind === "worktree" ? { expectedHolderPath: candidate.worktree.path } : {}
1502
+ });
1503
+ if (!result.ok) {
1504
+ return {
1505
+ ok: false,
1506
+ message: result.errorMessage ?? "Failed to switch root."
1507
+ };
1508
+ }
1509
+ onSwitchedBranch({
1510
+ branch,
1511
+ detachedHolder: result.data?.detachedHolder ?? null,
1512
+ warnings: result.warnings ?? []
1513
+ });
1514
+ return {
1515
+ ok: true,
1516
+ ...result.path === undefined ? {} : { path: result.path }
1517
+ };
1518
+ },
1519
+ reloadCandidates: async () => await loadPickCandidates(git, fs, json) ?? []
1520
+ });
1521
+ var runInteractivePicker = (candidates, callbacks) => runPicker(candidates, callbacks);
1522
+ var renderSwitchRootOutcome = (path, outcome, json) => {
1523
+ const branch = outcome?.branch ?? null;
1524
+ const detachedHolder = outcome?.detachedHolder ?? null;
1525
+ const warnings = outcome?.warnings ?? [];
1526
+ render("root", ok({ path, data: { branch, switched: true, detachedHolder }, warnings }), json);
1527
+ };
1528
+ export {
1529
+ createPickerCallbacks,
1530
+ loadPickCandidates,
1531
+ renderSwitchRootOutcome,
1532
+ runInteractivePicker
1533
+ };