@ohzw/worktree-command-tui 0.1.0 → 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.
Files changed (49) hide show
  1. package/README.md +5 -0
  2. package/dist/app.d.ts +3 -2
  3. package/dist/app.js +458 -42
  4. package/dist/components/ActionPanel.d.ts +6 -2
  5. package/dist/components/ActionPanel.js +141 -82
  6. package/dist/components/ContextBar.d.ts +4 -1
  7. package/dist/components/ContextBar.js +37 -4
  8. package/dist/components/FloatingLogWindow.d.ts +7 -0
  9. package/dist/components/FloatingLogWindow.js +5 -0
  10. package/dist/components/HelpWindow.d.ts +7 -0
  11. package/dist/components/HelpWindow.js +29 -0
  12. package/dist/components/LogPanel.d.ts +22 -0
  13. package/dist/components/LogPanel.js +260 -0
  14. package/dist/components/WorktreeList.d.ts +3 -1
  15. package/dist/components/WorktreeList.js +25 -30
  16. package/dist/core/command-runner.d.ts +11 -0
  17. package/dist/core/command-runner.js +44 -0
  18. package/dist/core/config-lifecycle.d.ts +25 -0
  19. package/dist/core/config-lifecycle.js +143 -0
  20. package/dist/core/config.d.ts +2 -3
  21. package/dist/core/config.js +0 -48
  22. package/dist/core/git-metadata.d.ts +25 -0
  23. package/dist/core/git-metadata.js +84 -0
  24. package/dist/core/git-worktrees.d.ts +2 -1
  25. package/dist/core/git-worktrees.js +30 -11
  26. package/dist/core/github-metadata.d.ts +14 -0
  27. package/dist/core/github-metadata.js +137 -0
  28. package/dist/core/init.d.ts +3 -2
  29. package/dist/core/init.js +9 -57
  30. package/dist/core/log-reader.d.ts +7 -0
  31. package/dist/core/log-reader.js +43 -0
  32. package/dist/core/runtime-state.d.ts +42 -0
  33. package/dist/core/runtime-state.js +125 -0
  34. package/dist/core/runtime.d.ts +20 -33
  35. package/dist/core/runtime.js +116 -173
  36. package/dist/core/tui-interaction.d.ts +31 -0
  37. package/dist/core/tui-interaction.js +59 -0
  38. package/dist/core/worktree-projection.d.ts +76 -0
  39. package/dist/core/worktree-projection.js +124 -0
  40. package/dist/main.js +24 -2
  41. package/dist/render-options.d.ts +1 -0
  42. package/dist/render-options.js +1 -0
  43. package/dist/repro.d.ts +1 -0
  44. package/dist/repro.js +13 -0
  45. package/dist/terminal/viewport.d.ts +15 -0
  46. package/dist/terminal/viewport.js +49 -0
  47. package/dist/ui-theme.d.ts +3 -0
  48. package/dist/ui-theme.js +38 -0
  49. package/package.json +2 -1
@@ -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
+ }
package/dist/main.js CHANGED
@@ -2,10 +2,12 @@
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 { ThemeProvider } from '@inkjs/ui';
5
6
  import { render } from 'ink';
7
+ import { APP_RENDER_OPTIONS } from './render-options.js';
6
8
  import { App } from './app.js';
7
9
  import { buildActions, buildInitialModel } from './core/runtime.js';
8
- import { APP_RENDER_OPTIONS } from './render-options.js';
10
+ import { appTheme } from './ui-theme.js';
9
11
  const cwd = process.cwd();
10
12
  const args = process.argv.slice(2);
11
13
  const [, , subcommand] = process.argv;
@@ -65,7 +67,27 @@ if (subcommand !== undefined) {
65
67
  }
66
68
  try {
67
69
  const [initialModel, actions] = await Promise.all([buildInitialModel(cwd), buildActions(cwd)]);
68
- render(_jsx(App, { initialModel: initialModel, actions: actions }), APP_RENDER_OPTIONS);
70
+ const createApp = () => (_jsx(ThemeProvider, { theme: appTheme, children: _jsx(App, { initialModel: initialModel, actions: actions }) }));
71
+ const instance = render(createApp(), APP_RENDER_OPTIONS);
72
+ let repaintTimer;
73
+ const repaintAfterResize = () => {
74
+ // Give Ink/useWindowSize one tick to observe the new size, then force a fresh root render.
75
+ if (repaintTimer) {
76
+ clearTimeout(repaintTimer);
77
+ }
78
+ repaintTimer = setTimeout(() => {
79
+ instance.rerender(createApp());
80
+ }, 25);
81
+ };
82
+ if (process.stdout.isTTY) {
83
+ process.stdout.on('resize', repaintAfterResize);
84
+ void instance.waitUntilExit().finally(() => {
85
+ if (repaintTimer) {
86
+ clearTimeout(repaintTimer);
87
+ }
88
+ process.stdout.off('resize', repaintAfterResize);
89
+ });
90
+ }
69
91
  }
70
92
  catch (error) {
71
93
  console.error(describeError(error));
@@ -1,4 +1,5 @@
1
1
  export declare const APP_RENDER_OPTIONS: {
2
2
  readonly alternateScreen: true;
3
3
  readonly exitOnCtrlC: true;
4
+ readonly incrementalRendering: true;
4
5
  };
@@ -1,4 +1,5 @@
1
1
  export const APP_RENDER_OPTIONS = {
2
2
  alternateScreen: true,
3
3
  exitOnCtrlC: true,
4
+ incrementalRendering: true,
4
5
  };
@@ -0,0 +1 @@
1
+ export {};
package/dist/repro.js ADDED
@@ -0,0 +1,13 @@
1
+ const model = {
2
+ repoName: "reclaim-the-forest",
3
+ namespace: "rojo-serve",
4
+ rows: Array.from({ length: 10 }, (_, index) => ({
5
+ path: `/repo/.worktree/feat-${index}`,
6
+ shortPath: `.worktree/feat-${index}`,
7
+ branch: `feat/${index}`,
8
+ tags: (index === 0 ? [active] : []),
9
+ pullRequest: index === 0 ? { kind: found, number: 2125, title: Selection }
10
+ :
11
+ }))
12
+ };
13
+ export {};
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export declare const appTheme: {
2
+ components: Record<string, import("@inkjs/ui").ComponentTheme>;
3
+ };
@@ -0,0 +1,38 @@
1
+ import { defaultTheme, extendTheme } from '@inkjs/ui';
2
+ const variantColor = {
3
+ info: 'blue',
4
+ success: 'green',
5
+ error: 'red',
6
+ warning: 'yellow',
7
+ };
8
+ export const appTheme = extendTheme(defaultTheme, {
9
+ components: {
10
+ StatusMessage: {
11
+ styles: {
12
+ icon: ({ variant }) => ({
13
+ color: variantColor[variant],
14
+ bold: true,
15
+ }),
16
+ },
17
+ },
18
+ Alert: {
19
+ styles: {
20
+ container: ({ variant }) => ({
21
+ flexGrow: 0,
22
+ flexShrink: 0,
23
+ borderStyle: 'round',
24
+ borderColor: variantColor[variant],
25
+ gap: 1,
26
+ paddingX: 1,
27
+ }),
28
+ icon: ({ variant }) => ({
29
+ color: variantColor[variant],
30
+ bold: true,
31
+ }),
32
+ title: () => ({
33
+ bold: true,
34
+ }),
35
+ },
36
+ },
37
+ },
38
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohzw/worktree-command-tui",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A TUI for managing git worktrees",
5
5
  "private": false,
6
6
  "type": "module",
@@ -54,6 +54,7 @@
54
54
  "access": "public"
55
55
  },
56
56
  "dependencies": {
57
+ "@inkjs/ui": "^2.0.0",
57
58
  "ink": "^7.0.4",
58
59
  "react": "^19.2.0"
59
60
  },