@ohzw/worktree-command-tui 0.1.1 → 0.1.2
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/README.md +5 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +235 -113
- package/dist/components/ActionPanel.d.ts +4 -2
- package/dist/components/ActionPanel.js +87 -135
- package/dist/components/ContextBar.d.ts +4 -1
- package/dist/components/ContextBar.js +27 -3
- package/dist/components/HelpWindow.d.ts +7 -0
- package/dist/components/HelpWindow.js +29 -0
- package/dist/components/LogPanel.d.ts +10 -3
- package/dist/components/LogPanel.js +239 -33
- package/dist/components/WorktreeList.js +20 -40
- package/dist/core/command-runner.d.ts +11 -0
- package/dist/core/command-runner.js +44 -0
- package/dist/core/config-lifecycle.d.ts +25 -0
- package/dist/core/config-lifecycle.js +143 -0
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +0 -48
- package/dist/core/git-metadata.d.ts +25 -0
- package/dist/core/git-metadata.js +84 -0
- package/dist/core/git-worktrees.d.ts +2 -1
- package/dist/core/git-worktrees.js +30 -11
- package/dist/core/github-metadata.d.ts +14 -0
- package/dist/core/github-metadata.js +137 -0
- package/dist/core/init.d.ts +3 -2
- package/dist/core/init.js +9 -57
- package/dist/core/log-reader.d.ts +7 -0
- package/dist/core/log-reader.js +43 -0
- package/dist/core/runtime-state.d.ts +42 -0
- package/dist/core/runtime-state.js +125 -0
- package/dist/core/runtime.d.ts +19 -39
- package/dist/core/runtime.js +112 -216
- package/dist/core/tui-interaction.d.ts +31 -0
- package/dist/core/tui-interaction.js +59 -0
- package/dist/core/worktree-projection.d.ts +76 -0
- package/dist/core/worktree-projection.js +124 -0
- package/dist/terminal/viewport.d.ts +15 -0
- package/dist/terminal/viewport.js +49 -0
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function getNextSelectedPath(rows, currentPath) {
|
|
2
|
+
if (rows.length === 0) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
if (currentPath && rows.some(row => row.path === currentPath)) {
|
|
6
|
+
return currentPath;
|
|
7
|
+
}
|
|
8
|
+
return rows[0].path;
|
|
9
|
+
}
|
|
10
|
+
export function getSelectedIndex(rows, selectedPath) {
|
|
11
|
+
if (selectedPath === null) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
const foundIndex = rows.findIndex(row => row.path === selectedPath);
|
|
15
|
+
return foundIndex >= 0 ? foundIndex : 0;
|
|
16
|
+
}
|
|
17
|
+
export function clampSelectionIndex(nextIndex, rowCount) {
|
|
18
|
+
if (rowCount <= 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return Math.min(Math.max(nextIndex, 0), rowCount - 1);
|
|
22
|
+
}
|
|
23
|
+
export function decideEnterInteraction(selected, activePath) {
|
|
24
|
+
if (selected === undefined) {
|
|
25
|
+
return { kind: 'ignore' };
|
|
26
|
+
}
|
|
27
|
+
if (selected.invalidReason) {
|
|
28
|
+
return {
|
|
29
|
+
kind: 'set-status',
|
|
30
|
+
status: { kind: 'error', message: selected.invalidReason },
|
|
31
|
+
suppressesBackgroundRefreshes: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (selected.path === activePath) {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'set-status',
|
|
37
|
+
status: { kind: 'idle', message: 'already active' },
|
|
38
|
+
suppressesBackgroundRefreshes: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
kind: 'start',
|
|
43
|
+
path: selected.path,
|
|
44
|
+
status: { kind: 'starting', message: `Starting ${selected.branch}...` },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function decideSetupInteraction(selected, setupAvailable) {
|
|
48
|
+
if (selected === undefined || !setupAvailable) {
|
|
49
|
+
return { kind: 'ignore' };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
kind: 'setup',
|
|
53
|
+
path: selected.path,
|
|
54
|
+
status: { kind: 'setting-up', message: `Running setup for ${selected.branch}...` },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function shouldApplyAsyncResult(state) {
|
|
58
|
+
return state.blocksInput || (state.generation === state.currentGeneration && !state.userActionInFlight);
|
|
59
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AppRow, RowTag } from './runtime.js';
|
|
2
|
+
export type ProjectionSeverity = 'success' | 'error' | 'info';
|
|
3
|
+
export type ProjectionTag = RowTag | string;
|
|
4
|
+
export type WorktreeListRowState = 'active' | 'invalid' | 'external' | 'normal';
|
|
5
|
+
export interface WorktreeListRowProjection {
|
|
6
|
+
state: WorktreeListRowState;
|
|
7
|
+
isSelected: boolean;
|
|
8
|
+
isMain: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface TagProjection {
|
|
11
|
+
tag: ProjectionTag;
|
|
12
|
+
}
|
|
13
|
+
export type ActionProjection = {
|
|
14
|
+
kind: 'blocked';
|
|
15
|
+
severity: 'error';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'active';
|
|
18
|
+
severity: 'success';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'startable';
|
|
21
|
+
severity: 'error' | 'info';
|
|
22
|
+
};
|
|
23
|
+
export type NoteProjection = {
|
|
24
|
+
kind: 'invalid';
|
|
25
|
+
severity: 'error';
|
|
26
|
+
invalidReason: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'external';
|
|
29
|
+
severity: 'info';
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'ready';
|
|
32
|
+
severity: 'error' | 'info';
|
|
33
|
+
};
|
|
34
|
+
export type UpstreamProjection = {
|
|
35
|
+
kind: 'unavailable';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'none';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'found';
|
|
40
|
+
branch: string;
|
|
41
|
+
ahead: number;
|
|
42
|
+
behind: number;
|
|
43
|
+
};
|
|
44
|
+
export type WorkingTreePartKind = 'staged' | 'unstaged' | 'untracked' | 'conflicts';
|
|
45
|
+
export type WorkingTreeProjection = {
|
|
46
|
+
kind: 'unavailable';
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'clean';
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'dirty';
|
|
51
|
+
parts: Array<{
|
|
52
|
+
kind: WorkingTreePartKind;
|
|
53
|
+
count: number;
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
56
|
+
export type PullRequestProjection = {
|
|
57
|
+
kind: 'none';
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'unavailable';
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'found';
|
|
62
|
+
number: number;
|
|
63
|
+
title: string;
|
|
64
|
+
state: 'OPEN' | 'CLOSED' | 'MERGED';
|
|
65
|
+
isDraft: boolean;
|
|
66
|
+
baseBranch: string;
|
|
67
|
+
isHistorical: boolean;
|
|
68
|
+
};
|
|
69
|
+
export declare function sanitizeInlineText(value: string): string;
|
|
70
|
+
export declare function projectWorktreeListRow(row: AppRow, isSelected: boolean): WorktreeListRowProjection;
|
|
71
|
+
export declare function getOrderedNonActiveTags(tags: readonly string[]): TagProjection[];
|
|
72
|
+
export declare function projectAction(row: AppRow, activePath: string | null): ActionProjection;
|
|
73
|
+
export declare function projectNote(row: AppRow): NoteProjection;
|
|
74
|
+
export declare function projectUpstream(row: AppRow): UpstreamProjection;
|
|
75
|
+
export declare function projectWorkingTree(row: AppRow): WorkingTreeProjection;
|
|
76
|
+
export declare function projectPullRequest(row: AppRow): PullRequestProjection;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const tagPriority = {
|
|
2
|
+
active: 0,
|
|
3
|
+
main: 1,
|
|
4
|
+
external: 2,
|
|
5
|
+
invalid: 3,
|
|
6
|
+
};
|
|
7
|
+
export function sanitizeInlineText(value) {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/[\r\n\t\u2028\u2029]+/g, ' ')
|
|
10
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, '')
|
|
11
|
+
.replace(/\p{Cf}/gu, '')
|
|
12
|
+
.replace(/\s+/g, ' ')
|
|
13
|
+
.trim();
|
|
14
|
+
}
|
|
15
|
+
function hasTag(row, tag) {
|
|
16
|
+
return row.tags.includes(tag);
|
|
17
|
+
}
|
|
18
|
+
function hasConflicts(row) {
|
|
19
|
+
return (row.workingTree?.conflicts ?? 0) > 0;
|
|
20
|
+
}
|
|
21
|
+
export function projectWorktreeListRow(row, isSelected) {
|
|
22
|
+
let state = 'normal';
|
|
23
|
+
if (hasTag(row, 'active')) {
|
|
24
|
+
state = 'active';
|
|
25
|
+
}
|
|
26
|
+
else if (hasTag(row, 'invalid')) {
|
|
27
|
+
state = 'invalid';
|
|
28
|
+
}
|
|
29
|
+
else if (hasTag(row, 'external')) {
|
|
30
|
+
state = 'external';
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
state,
|
|
34
|
+
isSelected,
|
|
35
|
+
isMain: hasTag(row, 'main'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function getOrderedNonActiveTags(tags) {
|
|
39
|
+
return tags
|
|
40
|
+
.filter(tag => tag !== 'active')
|
|
41
|
+
.slice()
|
|
42
|
+
.sort((a, b) => {
|
|
43
|
+
const aPriority = tagPriority[a] ?? 10;
|
|
44
|
+
const bPriority = tagPriority[b] ?? 10;
|
|
45
|
+
if (aPriority === bPriority) {
|
|
46
|
+
return a.localeCompare(b);
|
|
47
|
+
}
|
|
48
|
+
return aPriority - bPriority;
|
|
49
|
+
})
|
|
50
|
+
.map(tag => ({ tag }));
|
|
51
|
+
}
|
|
52
|
+
export function projectAction(row, activePath) {
|
|
53
|
+
if (row.invalidReason) {
|
|
54
|
+
return { kind: 'blocked', severity: 'error' };
|
|
55
|
+
}
|
|
56
|
+
if (row.path === activePath) {
|
|
57
|
+
return { kind: 'active', severity: 'success' };
|
|
58
|
+
}
|
|
59
|
+
return { kind: 'startable', severity: hasConflicts(row) ? 'error' : 'info' };
|
|
60
|
+
}
|
|
61
|
+
export function projectNote(row) {
|
|
62
|
+
if (row.invalidReason) {
|
|
63
|
+
return { kind: 'invalid', severity: 'error', invalidReason: row.invalidReason };
|
|
64
|
+
}
|
|
65
|
+
if (hasTag(row, 'external')) {
|
|
66
|
+
return { kind: 'external', severity: 'info' };
|
|
67
|
+
}
|
|
68
|
+
return { kind: 'ready', severity: hasConflicts(row) ? 'error' : 'info' };
|
|
69
|
+
}
|
|
70
|
+
export function projectUpstream(row) {
|
|
71
|
+
if (row.upstreamUnavailable) {
|
|
72
|
+
return { kind: 'unavailable' };
|
|
73
|
+
}
|
|
74
|
+
if (!row.upstream) {
|
|
75
|
+
return { kind: 'none' };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
kind: 'found',
|
|
79
|
+
branch: sanitizeInlineText(row.upstream.branch),
|
|
80
|
+
ahead: row.upstream.ahead,
|
|
81
|
+
behind: row.upstream.behind,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function projectWorkingTree(row) {
|
|
85
|
+
if (!row.workingTree) {
|
|
86
|
+
return { kind: 'unavailable' };
|
|
87
|
+
}
|
|
88
|
+
const { staged, unstaged, untracked, conflicts } = row.workingTree;
|
|
89
|
+
if (staged === 0 && unstaged === 0 && untracked === 0 && conflicts === 0) {
|
|
90
|
+
return { kind: 'clean' };
|
|
91
|
+
}
|
|
92
|
+
const parts = [];
|
|
93
|
+
if (staged > 0) {
|
|
94
|
+
parts.push({ kind: 'staged', count: staged });
|
|
95
|
+
}
|
|
96
|
+
if (unstaged > 0) {
|
|
97
|
+
parts.push({ kind: 'unstaged', count: unstaged });
|
|
98
|
+
}
|
|
99
|
+
if (untracked > 0) {
|
|
100
|
+
parts.push({ kind: 'untracked', count: untracked });
|
|
101
|
+
}
|
|
102
|
+
if (conflicts > 0) {
|
|
103
|
+
parts.push({ kind: 'conflicts', count: conflicts });
|
|
104
|
+
}
|
|
105
|
+
return { kind: 'dirty', parts };
|
|
106
|
+
}
|
|
107
|
+
export function projectPullRequest(row) {
|
|
108
|
+
if (!row.pullRequest || row.pullRequest.kind === 'none') {
|
|
109
|
+
return { kind: 'none' };
|
|
110
|
+
}
|
|
111
|
+
if (row.pullRequest.kind === 'unavailable') {
|
|
112
|
+
return { kind: 'unavailable' };
|
|
113
|
+
}
|
|
114
|
+
const isHistorical = row.pullRequest.state !== 'OPEN';
|
|
115
|
+
return {
|
|
116
|
+
kind: 'found',
|
|
117
|
+
number: row.pullRequest.number,
|
|
118
|
+
title: sanitizeInlineText(row.pullRequest.title),
|
|
119
|
+
state: row.pullRequest.state,
|
|
120
|
+
isDraft: row.pullRequest.isDraft,
|
|
121
|
+
baseBranch: sanitizeInlineText(row.pullRequest.baseBranch),
|
|
122
|
+
isHistorical,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ViewportSlice<T> = {
|
|
2
|
+
visibleItems: T[];
|
|
3
|
+
startIndex: number;
|
|
4
|
+
viewportHeight: number;
|
|
5
|
+
scrollOffset: number;
|
|
6
|
+
maxScrollOffset: number;
|
|
7
|
+
};
|
|
8
|
+
export type TailViewportSlice<T> = ViewportSlice<T> & {
|
|
9
|
+
topScrollOffset: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function normalizeViewportHeight(viewportHeight: number): number;
|
|
12
|
+
export declare function clampScrollOffset(totalItems: number, viewportHeight: number, scrollOffset: number): number;
|
|
13
|
+
export declare function sliceListViewport<T>(items: readonly T[], viewportHeight: number, scrollOffset: number): ViewportSlice<T>;
|
|
14
|
+
export declare function sliceTailViewport<T>(items: readonly T[], viewportHeight: number, scrollOffset: number): TailViewportSlice<T>;
|
|
15
|
+
export declare function getScrollbarThumbRows(totalLines: number, viewportHeight: number, scrollOffset: number): Set<number>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function normalizeViewportHeight(viewportHeight) {
|
|
2
|
+
return Math.max(1, Math.trunc(viewportHeight));
|
|
3
|
+
}
|
|
4
|
+
export function clampScrollOffset(totalItems, viewportHeight, scrollOffset) {
|
|
5
|
+
const normalizedViewportHeight = normalizeViewportHeight(viewportHeight);
|
|
6
|
+
const maxScrollOffset = Math.max(0, totalItems - normalizedViewportHeight);
|
|
7
|
+
return Math.min(Math.max(Math.trunc(scrollOffset), 0), maxScrollOffset);
|
|
8
|
+
}
|
|
9
|
+
export function sliceListViewport(items, viewportHeight, scrollOffset) {
|
|
10
|
+
const normalizedViewportHeight = normalizeViewportHeight(viewportHeight);
|
|
11
|
+
const maxScrollOffset = Math.max(0, items.length - normalizedViewportHeight);
|
|
12
|
+
const effectiveScrollOffset = Math.min(Math.max(Math.trunc(scrollOffset), 0), maxScrollOffset);
|
|
13
|
+
return {
|
|
14
|
+
visibleItems: items.slice(effectiveScrollOffset, effectiveScrollOffset + normalizedViewportHeight),
|
|
15
|
+
startIndex: effectiveScrollOffset,
|
|
16
|
+
viewportHeight: normalizedViewportHeight,
|
|
17
|
+
scrollOffset: effectiveScrollOffset,
|
|
18
|
+
maxScrollOffset,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function sliceTailViewport(items, viewportHeight, scrollOffset) {
|
|
22
|
+
const normalizedViewportHeight = normalizeViewportHeight(viewportHeight);
|
|
23
|
+
const maxScrollOffset = Math.max(0, items.length - normalizedViewportHeight);
|
|
24
|
+
const effectiveScrollOffset = Math.min(Math.max(Math.trunc(scrollOffset), 0), maxScrollOffset);
|
|
25
|
+
const topScrollOffset = maxScrollOffset - effectiveScrollOffset;
|
|
26
|
+
return {
|
|
27
|
+
visibleItems: items.slice(topScrollOffset, topScrollOffset + normalizedViewportHeight),
|
|
28
|
+
startIndex: topScrollOffset,
|
|
29
|
+
viewportHeight: normalizedViewportHeight,
|
|
30
|
+
scrollOffset: effectiveScrollOffset,
|
|
31
|
+
maxScrollOffset,
|
|
32
|
+
topScrollOffset,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function getScrollbarThumbRows(totalLines, viewportHeight, scrollOffset) {
|
|
36
|
+
const normalizedViewportHeight = normalizeViewportHeight(viewportHeight);
|
|
37
|
+
if (totalLines <= normalizedViewportHeight) {
|
|
38
|
+
return new Set();
|
|
39
|
+
}
|
|
40
|
+
const thumbSize = Math.max(1, Math.floor((normalizedViewportHeight / totalLines) * normalizedViewportHeight));
|
|
41
|
+
const maxScrollOffset = Math.max(1, totalLines - normalizedViewportHeight);
|
|
42
|
+
const effectiveScrollOffset = Math.min(Math.max(Math.trunc(scrollOffset), 0), maxScrollOffset);
|
|
43
|
+
const thumbStart = Math.round((effectiveScrollOffset / maxScrollOffset) * (normalizedViewportHeight - thumbSize));
|
|
44
|
+
const rows = new Set();
|
|
45
|
+
for (let index = 0; index < thumbSize; index += 1) {
|
|
46
|
+
rows.add(thumbStart + index);
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|