@ohzw/worktree-command-tui 0.1.1 → 0.1.3
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 +91 -26
- package/dist/app.d.ts +1 -1
- package/dist/app.js +242 -114
- 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 +29 -3
- package/dist/components/Header.js +5 -1
- 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 +240 -33
- package/dist/components/WorktreeList.js +20 -40
- package/dist/core/command-runner.d.ts +11 -0
- package/dist/core/command-runner.js +59 -7
- package/dist/core/config-lifecycle.d.ts +25 -0
- package/dist/core/config-lifecycle.js +160 -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 +21 -0
- package/dist/core/github-metadata.js +153 -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 +59 -0
- package/dist/core/posix-process.d.ts +2 -2
- package/dist/core/posix-process.js +19 -4
- package/dist/core/process-control.d.ts +2 -2
- package/dist/core/process-control.js +5 -2
- 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/session-store.js +22 -7
- 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 +132 -0
- package/dist/main.js +6 -5
- package/dist/terminal/viewport.d.ts +15 -0
- package/dist/terminal/viewport.js +49 -0
- package/package.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const tagPriority = {
|
|
2
|
+
active: 0,
|
|
3
|
+
main: 1,
|
|
4
|
+
external: 2,
|
|
5
|
+
invalid: 3,
|
|
6
|
+
};
|
|
7
|
+
const ANSI_CSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/gu;
|
|
8
|
+
const ANSI_OSC_PATTERN = /\u001B\][^\u0007\u001B\u009C]*(?:\u0007|\u001B\\|\u009C)?/gu;
|
|
9
|
+
const ANSI_STRING_PATTERN = /\u001B[P^_X][\s\S]*?(?:\u001B\\|\u009C|$)/gu;
|
|
10
|
+
const C1_STRING_PATTERN = /[\u0090\u0098\u009D\u009E\u009F][^\u0007\u009C]*(?:\u0007|\u009C)?/gu;
|
|
11
|
+
export function sanitizeInlineText(value) {
|
|
12
|
+
return value
|
|
13
|
+
.replace(ANSI_OSC_PATTERN, '')
|
|
14
|
+
.replace(ANSI_STRING_PATTERN, '')
|
|
15
|
+
.replace(C1_STRING_PATTERN, '')
|
|
16
|
+
.replace(ANSI_CSI_PATTERN, '')
|
|
17
|
+
.replace(/[\r\n\t\u2028\u2029]+/g, ' ')
|
|
18
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, '')
|
|
19
|
+
.replace(/\p{Cf}/gu, '')
|
|
20
|
+
.replace(/\s+/g, ' ')
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
23
|
+
function hasTag(row, tag) {
|
|
24
|
+
return row.tags.includes(tag);
|
|
25
|
+
}
|
|
26
|
+
function hasConflicts(row) {
|
|
27
|
+
return (row.workingTree?.conflicts ?? 0) > 0;
|
|
28
|
+
}
|
|
29
|
+
export function projectWorktreeListRow(row, isSelected) {
|
|
30
|
+
let state = 'normal';
|
|
31
|
+
if (hasTag(row, 'active')) {
|
|
32
|
+
state = 'active';
|
|
33
|
+
}
|
|
34
|
+
else if (hasTag(row, 'invalid')) {
|
|
35
|
+
state = 'invalid';
|
|
36
|
+
}
|
|
37
|
+
else if (hasTag(row, 'external')) {
|
|
38
|
+
state = 'external';
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
state,
|
|
42
|
+
isSelected,
|
|
43
|
+
isMain: hasTag(row, 'main'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function getOrderedNonActiveTags(tags) {
|
|
47
|
+
return tags
|
|
48
|
+
.filter(tag => tag !== 'active')
|
|
49
|
+
.slice()
|
|
50
|
+
.sort((a, b) => {
|
|
51
|
+
const aPriority = tagPriority[a] ?? 10;
|
|
52
|
+
const bPriority = tagPriority[b] ?? 10;
|
|
53
|
+
if (aPriority === bPriority) {
|
|
54
|
+
return a.localeCompare(b);
|
|
55
|
+
}
|
|
56
|
+
return aPriority - bPriority;
|
|
57
|
+
})
|
|
58
|
+
.map(tag => ({ tag }));
|
|
59
|
+
}
|
|
60
|
+
export function projectAction(row, activePath) {
|
|
61
|
+
if (row.invalidReason) {
|
|
62
|
+
return { kind: 'blocked', severity: 'error' };
|
|
63
|
+
}
|
|
64
|
+
if (row.path === activePath) {
|
|
65
|
+
return { kind: 'active', severity: 'success' };
|
|
66
|
+
}
|
|
67
|
+
return { kind: 'startable', severity: hasConflicts(row) ? 'error' : 'info' };
|
|
68
|
+
}
|
|
69
|
+
export function projectNote(row) {
|
|
70
|
+
if (row.invalidReason) {
|
|
71
|
+
return { kind: 'invalid', severity: 'error', invalidReason: sanitizeInlineText(row.invalidReason) };
|
|
72
|
+
}
|
|
73
|
+
if (hasTag(row, 'external')) {
|
|
74
|
+
return { kind: 'external', severity: 'info' };
|
|
75
|
+
}
|
|
76
|
+
return { kind: 'ready', severity: hasConflicts(row) ? 'error' : 'info' };
|
|
77
|
+
}
|
|
78
|
+
export function projectUpstream(row) {
|
|
79
|
+
if (row.upstreamUnavailable) {
|
|
80
|
+
return { kind: 'unavailable' };
|
|
81
|
+
}
|
|
82
|
+
if (!row.upstream) {
|
|
83
|
+
return { kind: 'none' };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
kind: 'found',
|
|
87
|
+
branch: sanitizeInlineText(row.upstream.branch),
|
|
88
|
+
ahead: row.upstream.ahead,
|
|
89
|
+
behind: row.upstream.behind,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function projectWorkingTree(row) {
|
|
93
|
+
if (!row.workingTree) {
|
|
94
|
+
return { kind: 'unavailable' };
|
|
95
|
+
}
|
|
96
|
+
const { staged, unstaged, untracked, conflicts } = row.workingTree;
|
|
97
|
+
if (staged === 0 && unstaged === 0 && untracked === 0 && conflicts === 0) {
|
|
98
|
+
return { kind: 'clean' };
|
|
99
|
+
}
|
|
100
|
+
const parts = [];
|
|
101
|
+
if (staged > 0) {
|
|
102
|
+
parts.push({ kind: 'staged', count: staged });
|
|
103
|
+
}
|
|
104
|
+
if (unstaged > 0) {
|
|
105
|
+
parts.push({ kind: 'unstaged', count: unstaged });
|
|
106
|
+
}
|
|
107
|
+
if (untracked > 0) {
|
|
108
|
+
parts.push({ kind: 'untracked', count: untracked });
|
|
109
|
+
}
|
|
110
|
+
if (conflicts > 0) {
|
|
111
|
+
parts.push({ kind: 'conflicts', count: conflicts });
|
|
112
|
+
}
|
|
113
|
+
return { kind: 'dirty', parts };
|
|
114
|
+
}
|
|
115
|
+
export function projectPullRequest(row) {
|
|
116
|
+
if (!row.pullRequest || row.pullRequest.kind === 'none') {
|
|
117
|
+
return { kind: 'none' };
|
|
118
|
+
}
|
|
119
|
+
if (row.pullRequest.kind === 'unavailable') {
|
|
120
|
+
return { kind: 'unavailable' };
|
|
121
|
+
}
|
|
122
|
+
const isHistorical = row.pullRequest.state !== 'OPEN';
|
|
123
|
+
return {
|
|
124
|
+
kind: 'found',
|
|
125
|
+
number: row.pullRequest.number,
|
|
126
|
+
title: sanitizeInlineText(row.pullRequest.title),
|
|
127
|
+
state: row.pullRequest.state,
|
|
128
|
+
isDraft: row.pullRequest.isDraft,
|
|
129
|
+
baseBranch: sanitizeInlineText(row.pullRequest.baseBranch),
|
|
130
|
+
isHistorical,
|
|
131
|
+
};
|
|
132
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createConfigForRepo, parseInitArgs } from './core/init.js';
|
|
4
4
|
import { CONFIG_FILE_NAME, CONFIG_FILE_NAMES } from './core/config.js';
|
|
5
|
+
import { sanitizeInlineText } from './core/worktree-projection.js';
|
|
5
6
|
import { ThemeProvider } from '@inkjs/ui';
|
|
6
7
|
import { render } from 'ink';
|
|
7
8
|
import { APP_RENDER_OPTIONS } from './render-options.js';
|
|
@@ -37,7 +38,7 @@ async function handleInitCommand() {
|
|
|
37
38
|
parsed = parseInitArgs(args.slice(1));
|
|
38
39
|
}
|
|
39
40
|
catch (error) {
|
|
40
|
-
console.error(error.message);
|
|
41
|
+
console.error(sanitizeInlineText(error.message));
|
|
41
42
|
process.exit(1);
|
|
42
43
|
}
|
|
43
44
|
if (parsed.help) {
|
|
@@ -46,10 +47,10 @@ async function handleInitCommand() {
|
|
|
46
47
|
}
|
|
47
48
|
try {
|
|
48
49
|
const result = await createConfigForRepo({ cwd, force: parsed.force });
|
|
49
|
-
console.log(`Created ${result.path}`);
|
|
50
|
+
console.log(`Created ${sanitizeInlineText(result.path)}`);
|
|
50
51
|
}
|
|
51
52
|
catch (error) {
|
|
52
|
-
console.error(error.message);
|
|
53
|
+
console.error(sanitizeInlineText(error.message));
|
|
53
54
|
process.exit(1);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -62,7 +63,7 @@ if (args.includes('-h') || args.includes('--help')) {
|
|
|
62
63
|
process.exit(0);
|
|
63
64
|
}
|
|
64
65
|
if (subcommand !== undefined) {
|
|
65
|
-
console.error(`Unknown command: ${subcommand}`);
|
|
66
|
+
console.error(`Unknown command: ${sanitizeInlineText(subcommand)}`);
|
|
66
67
|
process.exit(1);
|
|
67
68
|
}
|
|
68
69
|
try {
|
|
@@ -90,6 +91,6 @@ try {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
catch (error) {
|
|
93
|
-
console.error(describeError(error));
|
|
94
|
+
console.error(sanitizeInlineText(describeError(error)));
|
|
94
95
|
process.exit(1);
|
|
95
96
|
}
|
|
@@ -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
|
+
}
|