@kitlangton/ghui 0.1.19 → 0.1.21
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 +14 -3
- package/bin/ghui.js +64 -1
- package/package.json +18 -7
- package/src/App.tsx +1075 -653
- package/src/appCommands.ts +330 -0
- package/src/commands.ts +73 -0
- package/src/config.ts +10 -0
- package/src/domain.ts +29 -20
- package/src/errors.ts +10 -0
- package/src/index.tsx +30 -3
- package/src/mergeActions.ts +1 -6
- package/src/pullRequestCache.ts +19 -0
- package/src/pullRequestViews.ts +45 -0
- package/src/services/BrowserOpener.ts +22 -0
- package/src/services/Clipboard.ts +46 -0
- package/src/services/CommandRunner.ts +14 -6
- package/src/services/GitHubService.ts +290 -126
- package/src/services/MockGitHubService.ts +146 -0
- package/src/ui/CommandPalette.tsx +156 -0
- package/src/ui/DetailsPane.tsx +49 -63
- package/src/ui/FooterHints.tsx +84 -179
- package/src/ui/PullRequestDiffPane.tsx +29 -50
- package/src/ui/PullRequestList.tsx +99 -45
- package/src/ui/colors.ts +167 -1
- package/src/ui/diff.ts +34 -44
- package/src/ui/diffStats.tsx +25 -0
- package/src/ui/modals.tsx +263 -295
- package/src/ui/primitives.tsx +90 -0
- package/src/ui/pullRequests.ts +44 -12
- package/src/ui/singleLineInput.ts +25 -0
package/src/ui/FooterHints.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Data } from "effect"
|
|
2
2
|
import { colors } from "./colors.js"
|
|
3
|
-
import {
|
|
3
|
+
import { HintRow, type HintItem } from "./primitives.js"
|
|
4
4
|
|
|
5
5
|
export type RetryProgress = Data.TaggedEnum<{
|
|
6
6
|
Idle: {}
|
|
@@ -10,185 +10,90 @@ export type RetryProgress = Data.TaggedEnum<{
|
|
|
10
10
|
export const RetryProgress = Data.taggedEnum<RetryProgress>()
|
|
11
11
|
export const initialRetryProgress: RetryProgress = RetryProgress.Idle()
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
filterEditing
|
|
15
|
-
showFilterClear
|
|
16
|
-
detailFullView
|
|
17
|
-
diffFullView
|
|
18
|
-
diffCommentMode
|
|
19
|
-
hasSelection
|
|
20
|
-
canCloseSelection
|
|
21
|
-
hasError
|
|
22
|
-
isLoading
|
|
23
|
-
loadingIndicator
|
|
24
|
-
retryProgress
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<span fg={colors.muted}> apply </span>
|
|
47
|
-
<span fg={colors.count}>esc</span>
|
|
48
|
-
<span fg={colors.muted}> cancel </span>
|
|
49
|
-
<span fg={colors.count}>ctrl-u</span>
|
|
50
|
-
<span fg={colors.muted}> clear </span>
|
|
51
|
-
<span fg={colors.count}>ctrl-w</span>
|
|
52
|
-
<span fg={colors.muted}> word</span>
|
|
53
|
-
</TextLine>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
13
|
+
interface HintsContext {
|
|
14
|
+
readonly filterEditing: boolean
|
|
15
|
+
readonly showFilterClear: boolean
|
|
16
|
+
readonly detailFullView: boolean
|
|
17
|
+
readonly diffFullView: boolean
|
|
18
|
+
readonly diffCommentMode: boolean
|
|
19
|
+
readonly hasSelection: boolean
|
|
20
|
+
readonly canCloseSelection: boolean
|
|
21
|
+
readonly hasError: boolean
|
|
22
|
+
readonly isLoading: boolean
|
|
23
|
+
readonly loadingIndicator: string
|
|
24
|
+
readonly retryProgress: RetryProgress
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const filterEditingHints: readonly HintItem[] = [
|
|
28
|
+
{ key: "search", label: "typing" },
|
|
29
|
+
{ key: "↑↓", label: "move" },
|
|
30
|
+
{ key: "enter", label: "apply" },
|
|
31
|
+
{ key: "esc", label: "cancel" },
|
|
32
|
+
{ key: "ctrl-u", label: "clear" },
|
|
33
|
+
{ key: "ctrl-w", label: "word" },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const diffCommentModeHints: readonly HintItem[] = [
|
|
37
|
+
{ key: "↑↓", label: "line" },
|
|
38
|
+
{ key: "pgup/pgdn", label: "jump" },
|
|
39
|
+
{ key: "←→", label: "side" },
|
|
40
|
+
{ key: "enter", label: "open" },
|
|
41
|
+
{ key: "a", label: "comment" },
|
|
42
|
+
{ key: "c", label: "done" },
|
|
43
|
+
{ key: "[]", label: "files" },
|
|
44
|
+
{ key: "esc", label: "back" },
|
|
45
|
+
]
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<span fg={colors.count}>enter</span>
|
|
68
|
-
<span fg={colors.muted}> open </span>
|
|
69
|
-
<span fg={colors.count}>a</span>
|
|
70
|
-
<span fg={colors.muted}> comment </span>
|
|
71
|
-
<span fg={colors.count}>c</span>
|
|
72
|
-
<span fg={colors.muted}> done </span>
|
|
73
|
-
<span fg={colors.count}>[]</span>
|
|
74
|
-
<span fg={colors.muted}> files </span>
|
|
75
|
-
<span fg={colors.count}>esc</span>
|
|
76
|
-
<span fg={colors.muted}> back</span>
|
|
77
|
-
</TextLine>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
return (
|
|
81
|
-
<TextLine>
|
|
82
|
-
<span fg={colors.count}>esc</span>
|
|
83
|
-
<span fg={colors.muted}> back </span>
|
|
84
|
-
<span fg={colors.count}>v</span>
|
|
85
|
-
<span fg={colors.muted}> view </span>
|
|
86
|
-
<span fg={colors.count}>w</span>
|
|
87
|
-
<span fg={colors.muted}> wrap </span>
|
|
88
|
-
<span fg={colors.count}>c</span>
|
|
89
|
-
<span fg={colors.muted}> comment </span>
|
|
90
|
-
<span fg={colors.count}>[]</span>
|
|
91
|
-
<span fg={colors.muted}> files </span>
|
|
92
|
-
<span fg={colors.count}>r</span>
|
|
93
|
-
<span fg={colors.muted}> reload </span>
|
|
94
|
-
<span fg={colors.count}>o</span>
|
|
95
|
-
<span fg={colors.muted}> open </span>
|
|
96
|
-
<span fg={colors.count}>q</span>
|
|
97
|
-
<span fg={colors.muted}> quit</span>
|
|
98
|
-
</TextLine>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
47
|
+
const diffViewHints: readonly HintItem[] = [
|
|
48
|
+
{ key: "esc", label: "back" },
|
|
49
|
+
{ key: "v", label: "view" },
|
|
50
|
+
{ key: "w", label: "wrap" },
|
|
51
|
+
{ key: "c", label: "comment" },
|
|
52
|
+
{ key: "[]", label: "files" },
|
|
53
|
+
{ key: "r", label: "reload" },
|
|
54
|
+
{ key: "o", label: "open" },
|
|
55
|
+
{ key: "q", label: "quit" },
|
|
56
|
+
]
|
|
101
57
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<span fg={colors.muted}> state </span>
|
|
117
|
-
<span fg={colors.count}>d</span>
|
|
118
|
-
<span fg={colors.muted}> diff </span>
|
|
119
|
-
<span fg={colors.count}>l</span>
|
|
120
|
-
<span fg={colors.muted}> labels </span>
|
|
121
|
-
<span fg={colors.count}>m</span>
|
|
122
|
-
<span fg={colors.muted}> merge </span>
|
|
123
|
-
{canCloseSelection ? (
|
|
124
|
-
<>
|
|
125
|
-
<span fg={colors.count}>x</span>
|
|
126
|
-
<span fg={colors.muted}> close </span>
|
|
127
|
-
</>
|
|
128
|
-
) : null}
|
|
129
|
-
</>
|
|
130
|
-
) : null}
|
|
131
|
-
<span fg={colors.count}>o</span>
|
|
132
|
-
<span fg={colors.muted}> open </span>
|
|
133
|
-
<span fg={colors.count}>y</span>
|
|
134
|
-
<span fg={colors.muted}> copy </span>
|
|
135
|
-
<span fg={colors.count}>q</span>
|
|
136
|
-
<span fg={colors.muted}> quit</span>
|
|
137
|
-
</TextLine>
|
|
138
|
-
)
|
|
139
|
-
}
|
|
58
|
+
const detailFullViewHints = (ctx: HintsContext): readonly HintItem[] => [
|
|
59
|
+
{ key: "esc", label: "back" },
|
|
60
|
+
{ key: "↑↓", label: "scroll" },
|
|
61
|
+
{ key: "r", label: ctx.hasError ? "retry" : "refresh" },
|
|
62
|
+
{ key: "t", label: "theme" },
|
|
63
|
+
{ key: "s", label: "state", when: ctx.hasSelection },
|
|
64
|
+
{ key: "d", label: "diff", when: ctx.hasSelection },
|
|
65
|
+
{ key: "l", label: "labels", when: ctx.hasSelection },
|
|
66
|
+
{ key: "m", label: "merge", when: ctx.hasSelection },
|
|
67
|
+
{ key: "x", label: "close", when: ctx.hasSelection && ctx.canCloseSelection },
|
|
68
|
+
{ key: "o", label: "open" },
|
|
69
|
+
{ key: "y", label: "copy" },
|
|
70
|
+
{ key: "q", label: "quit" },
|
|
71
|
+
]
|
|
140
72
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<span fg={colors.muted}> {retryProgress.attempt}/{retryProgress.max} </span>
|
|
159
|
-
</>
|
|
160
|
-
) : isLoading ? (
|
|
161
|
-
<>
|
|
162
|
-
<span fg={colors.status.pending}>{loadingIndicator}</span>
|
|
163
|
-
<span fg={colors.muted}> loading </span>
|
|
164
|
-
</>
|
|
165
|
-
) : null}
|
|
166
|
-
<span fg={colors.count}>r</span>
|
|
167
|
-
<span fg={colors.muted}>{hasError ? " retry " : " refresh "}</span>
|
|
168
|
-
{hasSelection ? (
|
|
169
|
-
<>
|
|
170
|
-
<span fg={colors.count}>s</span>
|
|
171
|
-
<span fg={colors.muted}> state </span>
|
|
172
|
-
<span fg={colors.count}>d</span>
|
|
173
|
-
<span fg={colors.muted}> diff </span>
|
|
174
|
-
<span fg={colors.count}>l</span>
|
|
175
|
-
<span fg={colors.muted}> labels </span>
|
|
176
|
-
<span fg={colors.count}>m</span>
|
|
177
|
-
<span fg={colors.muted}> merge </span>
|
|
178
|
-
{canCloseSelection ? (
|
|
179
|
-
<>
|
|
180
|
-
<span fg={colors.count}>x</span>
|
|
181
|
-
<span fg={colors.muted}> close </span>
|
|
182
|
-
</>
|
|
183
|
-
) : null}
|
|
184
|
-
<span fg={colors.count}>o</span>
|
|
185
|
-
<span fg={colors.muted}> open </span>
|
|
186
|
-
<span fg={colors.count}>y</span>
|
|
187
|
-
<span fg={colors.muted}> copy </span>
|
|
188
|
-
</>
|
|
189
|
-
) : null}
|
|
190
|
-
<span fg={colors.count}>q</span>
|
|
191
|
-
<span fg={colors.muted}> quit</span>
|
|
192
|
-
</TextLine>
|
|
193
|
-
)
|
|
73
|
+
const defaultHints = (ctx: HintsContext): readonly HintItem[] => {
|
|
74
|
+
const retrying = ctx.retryProgress._tag === "Retrying"
|
|
75
|
+
return [
|
|
76
|
+
{ key: "/", label: "filter" },
|
|
77
|
+
{ key: "t", label: "theme" },
|
|
78
|
+
{ key: "esc", label: "clear", when: ctx.showFilterClear },
|
|
79
|
+
{ key: "retry", label: retrying ? `${(ctx.retryProgress as { attempt: number; max: number }).attempt}/${(ctx.retryProgress as { attempt: number; max: number }).max}` : "", when: retrying, keyFg: colors.status.pending },
|
|
80
|
+
{ key: ctx.loadingIndicator, label: "loading", when: !retrying && ctx.isLoading, keyFg: colors.status.pending },
|
|
81
|
+
{ key: "r", label: "retry", when: ctx.hasError },
|
|
82
|
+
{ key: "d", label: "diff", when: ctx.hasSelection },
|
|
83
|
+
{ key: "l", label: "labels", when: ctx.hasSelection },
|
|
84
|
+
{ key: "m", label: "merge", when: ctx.hasSelection },
|
|
85
|
+
{ key: "x", label: "close", when: ctx.hasSelection && ctx.canCloseSelection },
|
|
86
|
+
{ key: "o", label: "open", when: ctx.hasSelection },
|
|
87
|
+
{ key: "y", label: "copy", when: ctx.hasSelection },
|
|
88
|
+
{ key: "ctrl-p", label: "commands" },
|
|
89
|
+
]
|
|
194
90
|
}
|
|
91
|
+
|
|
92
|
+
const footerHints = (ctx: HintsContext): readonly HintItem[] => {
|
|
93
|
+
if (ctx.filterEditing) return filterEditingHints
|
|
94
|
+
if (ctx.diffFullView) return ctx.diffCommentMode ? diffCommentModeHints : diffViewHints
|
|
95
|
+
if (ctx.detailFullView) return detailFullViewHints(ctx)
|
|
96
|
+
return defaultHints(ctx)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const FooterHints = (props: HintsContext) => <HintRow items={footerHints(props)} />
|
|
@@ -4,26 +4,24 @@ import type { DiffCommentSide, PullRequestItem, PullRequestReviewComment } from
|
|
|
4
4
|
import { colors, type ThemeId } from "./colors.js"
|
|
5
5
|
import { createDiffSyntaxStyle, diffFileStats, diffFileStatsText, diffStatText, stackedDiffFileAtLine, type DiffFileStats, type DiffView, type DiffWrapMode, type PullRequestDiffState, type StackedDiffCommentAnchor, type StackedDiffFilePatch } from "./diff.js"
|
|
6
6
|
import { LoadingPane, StatusCard } from "./DetailsPane.js"
|
|
7
|
-
import {
|
|
7
|
+
import { DiffStats } from "./diffStats.js"
|
|
8
|
+
import { Divider, fitCell, PaddedRow, PlainLine, TextLine } from "./primitives.js"
|
|
8
9
|
import { shortRepoName } from "./pullRequests.js"
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
pullRequest.additions > 0 ? { key: "additions", text: `+${pullRequest.additions}`, color: colors.status.passing } : null,
|
|
16
|
-
pullRequest.deletions > 0 ? { key: "deletions", text: `-${pullRequest.deletions}`, color: colors.status.failing } : null,
|
|
17
|
-
{ key: "files", text: files, color: colors.muted },
|
|
18
|
-
]
|
|
19
|
-
const parts = rawParts.filter((part): part is Part => part !== null)
|
|
20
|
-
|
|
11
|
+
const DiffPaneHeader = ({ pullRequest, paneWidth }: { pullRequest: PullRequestItem; paneWidth: number }) => {
|
|
12
|
+
const stats = diffStatText(pullRequest)
|
|
13
|
+
const headerWidth = Math.max(24, paneWidth - 2)
|
|
14
|
+
const leftHeader = `#${pullRequest.number} ${shortRepoName(pullRequest.repository)}`
|
|
15
|
+
const headerGap = Math.max(2, headerWidth - leftHeader.length - stats.length)
|
|
21
16
|
return (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<span
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
<PaddedRow>
|
|
18
|
+
<TextLine>
|
|
19
|
+
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
20
|
+
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
21
|
+
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
22
|
+
<DiffStats pullRequest={pullRequest} />
|
|
23
|
+
</TextLine>
|
|
24
|
+
</PaddedRow>
|
|
27
25
|
)
|
|
28
26
|
}
|
|
29
27
|
|
|
@@ -109,22 +107,10 @@ export const PullRequestDiffPane = ({
|
|
|
109
107
|
return <LoadingPane content={{ title: "No pull request selected", hint: "Press esc to go back" }} width={paneWidth} height={height} />
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
const stats = diffStatText(pullRequest)
|
|
113
|
-
const headerWidth = Math.max(24, paneWidth - 2)
|
|
114
|
-
const leftHeader = `#${pullRequest.number} ${shortRepoName(pullRequest.repository)}`
|
|
115
|
-
const headerGap = Math.max(2, headerWidth - leftHeader.length - stats.length)
|
|
116
|
-
|
|
117
110
|
if (!diffState || diffState._tag === "Loading") {
|
|
118
111
|
return (
|
|
119
112
|
<box height={height} flexDirection="column">
|
|
120
|
-
<
|
|
121
|
-
<TextLine>
|
|
122
|
-
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
123
|
-
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
124
|
-
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
125
|
-
<DiffStats pullRequest={pullRequest} />
|
|
126
|
-
</TextLine>
|
|
127
|
-
</box>
|
|
113
|
+
<DiffPaneHeader pullRequest={pullRequest} paneWidth={paneWidth} />
|
|
128
114
|
<Divider width={paneWidth} />
|
|
129
115
|
<LoadingPane content={{ title: `${loadingIndicator} Loading diff`, hint: "Fetching patch from GitHub" }} width={paneWidth} height={Math.max(1, height - 2)} />
|
|
130
116
|
</box>
|
|
@@ -134,9 +120,9 @@ export const PullRequestDiffPane = ({
|
|
|
134
120
|
if (diffState._tag === "Error") {
|
|
135
121
|
return (
|
|
136
122
|
<box height={height} flexDirection="column">
|
|
137
|
-
<
|
|
123
|
+
<PaddedRow>
|
|
138
124
|
<PlainLine text={`#${pullRequest.number} ${shortRepoName(pullRequest.repository)} diff`} fg={colors.count} bold />
|
|
139
|
-
</
|
|
125
|
+
</PaddedRow>
|
|
140
126
|
<Divider width={paneWidth} />
|
|
141
127
|
<StatusCard content={{ title: "Could not load diff", hint: diffState.error }} width={paneWidth} />
|
|
142
128
|
</box>
|
|
@@ -184,22 +170,15 @@ export const PullRequestDiffPane = ({
|
|
|
184
170
|
|
|
185
171
|
return (
|
|
186
172
|
<box height={height} flexDirection="column">
|
|
187
|
-
<
|
|
188
|
-
<TextLine>
|
|
189
|
-
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
190
|
-
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
191
|
-
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
192
|
-
<DiffStats pullRequest={pullRequest} />
|
|
193
|
-
</TextLine>
|
|
194
|
-
</box>
|
|
173
|
+
<DiffPaneHeader pullRequest={pullRequest} paneWidth={paneWidth} />
|
|
195
174
|
<Divider width={paneWidth} />
|
|
196
175
|
<scrollbox ref={scrollRef} focused={!commentMode} flexGrow={1} scrollY scrollX={false} onMouseDown={handleDiffMouseDown}>
|
|
197
176
|
{stackedFiles.map((stackedFile) => (
|
|
198
177
|
<box key={`${pullRequest.url}-${stackedFile.index}-${view}-${wrapMode}`} flexDirection="column" flexShrink={0}>
|
|
199
178
|
{stackedFile.index > 0 ? <Divider width={paneWidth} /> : null}
|
|
200
|
-
<
|
|
179
|
+
<PaddedRow>
|
|
201
180
|
<FileHeader file={stackedFile.file} index={stackedFile.index} count={readyFiles.length} width={paneWidth} />
|
|
202
|
-
</
|
|
181
|
+
</PaddedRow>
|
|
203
182
|
<Divider width={paneWidth} />
|
|
204
183
|
<diff
|
|
205
184
|
ref={(diff: DiffRenderable | null) => setDiffRef(stackedFile.index, diff)}
|
|
@@ -232,15 +211,15 @@ export const PullRequestDiffPane = ({
|
|
|
232
211
|
{incomingFile ? (
|
|
233
212
|
<>
|
|
234
213
|
<Divider width={paneWidth} />
|
|
235
|
-
<
|
|
214
|
+
<PaddedRow backgroundColor={colors.background}>
|
|
236
215
|
<FileHeader file={incomingFile.file} index={incomingFile.index} count={readyFiles.length} width={paneWidth} suffix={stickyCommentLabelFor(incomingFile)} suffixColor={stickyCommentColor} />
|
|
237
|
-
</
|
|
216
|
+
</PaddedRow>
|
|
238
217
|
</>
|
|
239
218
|
) : (
|
|
240
219
|
<>
|
|
241
|
-
<
|
|
220
|
+
<PaddedRow backgroundColor={colors.background}>
|
|
242
221
|
<FileHeader file={stickyFile.file} index={stickyFile.index} count={readyFiles.length} width={paneWidth} suffix={stickyCommentLabelFor(stickyFile)} suffixColor={stickyCommentColor} />
|
|
243
|
-
</
|
|
222
|
+
</PaddedRow>
|
|
244
223
|
<Divider width={paneWidth} />
|
|
245
224
|
</>
|
|
246
225
|
)}
|
|
@@ -249,12 +228,12 @@ export const PullRequestDiffPane = ({
|
|
|
249
228
|
{commentPeek ? (
|
|
250
229
|
<>
|
|
251
230
|
<Divider width={paneWidth} />
|
|
252
|
-
<
|
|
231
|
+
<PaddedRow>
|
|
253
232
|
<PlainLine text={fitCell(commentPeekMeta, Math.max(1, paneWidth - 2))} fg={colors.count} />
|
|
254
|
-
</
|
|
255
|
-
<
|
|
233
|
+
</PaddedRow>
|
|
234
|
+
<PaddedRow>
|
|
256
235
|
<PlainLine text={fitCell(commentPeekBody, Math.max(1, paneWidth - 2))} fg={colors.text} />
|
|
257
|
-
</
|
|
236
|
+
</PaddedRow>
|
|
258
237
|
</>
|
|
259
238
|
) : null}
|
|
260
239
|
</box>
|
|
@@ -3,16 +3,23 @@ import type { LoadStatus, PullRequestItem } from "../domain.js"
|
|
|
3
3
|
import { daysOpen } from "../date.js"
|
|
4
4
|
import { colors } from "./colors.js"
|
|
5
5
|
import { fitCell, PlainLine, SectionTitle, TextLine } from "./primitives.js"
|
|
6
|
-
import {
|
|
6
|
+
import { pullRequestRowDisplay, repoColor, reviewIcon } from "./pullRequests.js"
|
|
7
7
|
|
|
8
8
|
export type PullRequestGroups = Array<[string, PullRequestItem[]]>
|
|
9
9
|
|
|
10
|
+
export type PullRequestListRow =
|
|
11
|
+
| { readonly _tag: "title" }
|
|
12
|
+
| { readonly _tag: "filter" }
|
|
13
|
+
| { readonly _tag: "message"; readonly text: string; readonly color: string }
|
|
14
|
+
| { readonly _tag: "group"; readonly repository: string; readonly pullRequests: readonly PullRequestItem[] }
|
|
15
|
+
| { readonly _tag: "pull-request"; readonly pullRequest: PullRequestItem; readonly groupPullRequests: readonly PullRequestItem[] }
|
|
16
|
+
| { readonly _tag: "load-more"; readonly text: string }
|
|
17
|
+
|
|
10
18
|
const GROUP_ICON = "◆"
|
|
11
19
|
|
|
12
|
-
const getRowLayout = (contentWidth: number, numberWidth
|
|
20
|
+
const getRowLayout = (contentWidth: number, numberWidth: number, ageWidth: number) => {
|
|
13
21
|
const reviewWidth = 1
|
|
14
22
|
const checkWidth = 6
|
|
15
|
-
const ageWidth = 4
|
|
16
23
|
const fixedWidth = reviewWidth + 1 + numberWidth + 1 + checkWidth + ageWidth
|
|
17
24
|
const titleWidth = Math.max(8, contentWidth - fixedWidth)
|
|
18
25
|
return { reviewWidth, checkWidth, ageWidth, numberWidth, titleWidth }
|
|
@@ -24,6 +31,12 @@ const groupNumberWidth = (pullRequests: readonly PullRequestItem[]) => {
|
|
|
24
31
|
return maxLen + 1
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
const groupAgeWidth = (pullRequests: readonly PullRequestItem[]) => {
|
|
35
|
+
if (pullRequests.length === 0) return 4
|
|
36
|
+
const maxLen = Math.max(...pullRequests.map((pr) => `${daysOpen(pr.createdAt)}d`.length))
|
|
37
|
+
return Math.max(4, maxLen + 1)
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
const MatchedCell = ({ text, width, query, align = "left" }: { text: string; width: number; query: string; align?: "left" | "right" }) => {
|
|
28
41
|
const fitted = fitCell(text, width, align)
|
|
29
42
|
const needle = query.trim().toLowerCase()
|
|
@@ -47,11 +60,53 @@ const GroupTitle = ({ label, color, filterText }: { label: string; color: string
|
|
|
47
60
|
</TextLine>
|
|
48
61
|
)
|
|
49
62
|
|
|
63
|
+
export const buildPullRequestListRows = ({
|
|
64
|
+
groups,
|
|
65
|
+
status,
|
|
66
|
+
error,
|
|
67
|
+
filterText,
|
|
68
|
+
showFilterBar,
|
|
69
|
+
loadedCount,
|
|
70
|
+
hasMore,
|
|
71
|
+
isLoadingMore,
|
|
72
|
+
}: {
|
|
73
|
+
readonly groups: PullRequestGroups
|
|
74
|
+
readonly status: LoadStatus
|
|
75
|
+
readonly error: string | null
|
|
76
|
+
readonly filterText: string
|
|
77
|
+
readonly showFilterBar: boolean
|
|
78
|
+
readonly loadedCount: number
|
|
79
|
+
readonly hasMore: boolean
|
|
80
|
+
readonly isLoadingMore: boolean
|
|
81
|
+
}): readonly PullRequestListRow[] => {
|
|
82
|
+
const itemCount = groups.reduce((count, [, pullRequests]) => count + pullRequests.length, 0)
|
|
83
|
+
const rows: PullRequestListRow[] = [{ _tag: "title" }]
|
|
84
|
+
if (showFilterBar) rows.push({ _tag: "filter" })
|
|
85
|
+
if (status === "loading" && itemCount === 0) rows.push({ _tag: "message", text: "- Loading pull requests...", color: colors.muted })
|
|
86
|
+
if (status === "error") rows.push({ _tag: "message", text: `- ${error ?? "Could not load pull requests."}`, color: colors.error })
|
|
87
|
+
if (status === "ready" && itemCount === 0) rows.push({ _tag: "message", text: filterText.length > 0 ? "- No matching pull requests." : "- No open pull requests.", color: colors.muted })
|
|
88
|
+
for (const [repository, pullRequests] of groups) {
|
|
89
|
+
rows.push({ _tag: "group", repository, pullRequests })
|
|
90
|
+
for (const pullRequest of pullRequests) rows.push({ _tag: "pull-request", pullRequest, groupPullRequests: pullRequests })
|
|
91
|
+
}
|
|
92
|
+
if (status === "ready" && itemCount > 0 && (hasMore || isLoadingMore)) {
|
|
93
|
+
rows.push({ _tag: "load-more", text: isLoadingMore ? `- Loading more pull requests... (${loadedCount} loaded)` : `- ${loadedCount} loaded, more available` })
|
|
94
|
+
}
|
|
95
|
+
return rows
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const pullRequestListRowIndex = (rows: readonly PullRequestListRow[], url: string | null) => {
|
|
99
|
+
if (!url) return null
|
|
100
|
+
const index = rows.findIndex((row) => row._tag === "pull-request" && row.pullRequest.url === url)
|
|
101
|
+
return index >= 0 ? index : null
|
|
102
|
+
}
|
|
103
|
+
|
|
50
104
|
const PullRequestRow = ({
|
|
51
105
|
pullRequest,
|
|
52
106
|
selected,
|
|
53
107
|
contentWidth,
|
|
54
108
|
numWidth,
|
|
109
|
+
ageColWidth,
|
|
55
110
|
filterText,
|
|
56
111
|
onSelect,
|
|
57
112
|
}: {
|
|
@@ -59,31 +114,25 @@ const PullRequestRow = ({
|
|
|
59
114
|
selected: boolean
|
|
60
115
|
contentWidth: number
|
|
61
116
|
numWidth: number
|
|
117
|
+
ageColWidth: number
|
|
62
118
|
filterText: string
|
|
63
119
|
onSelect: () => void
|
|
64
120
|
}) => {
|
|
65
|
-
const isClosed = pullRequest.state === "closed"
|
|
66
|
-
const isMerged = pullRequest.state === "merged"
|
|
67
|
-
const isFinal = isClosed || isMerged
|
|
68
|
-
const checkText = isMerged ? "merged" : isClosed ? "closed" : checkLabel(pullRequest)?.replace(/^checks\s+/, "") ?? ""
|
|
69
121
|
const ageText = `${daysOpen(pullRequest.createdAt)}d`
|
|
70
|
-
const { reviewWidth, checkWidth, ageWidth, numberWidth, titleWidth } = getRowLayout(contentWidth, numWidth)
|
|
122
|
+
const { reviewWidth, checkWidth, ageWidth, numberWidth, titleWidth } = getRowLayout(contentWidth, numWidth, ageColWidth)
|
|
71
123
|
const rowWidth = reviewWidth + 1 + numberWidth + 1 + titleWidth + checkWidth + ageWidth
|
|
72
124
|
const fillerWidth = Math.max(0, contentWidth - rowWidth)
|
|
73
|
-
const
|
|
74
|
-
const rowTextColor = selected ? colors.selectedText : isFinal ? colors.muted : colors.text
|
|
75
|
-
const numberColor = selected ? colors.accent : isFinal ? colors.muted : colors.count
|
|
76
|
-
const checkColor = isMerged ? colors.status.passing : isClosed ? colors.muted : statusColor(pullRequest.checkStatus)
|
|
125
|
+
const display = pullRequestRowDisplay(pullRequest, selected)
|
|
77
126
|
|
|
78
127
|
return (
|
|
79
128
|
<box width={contentWidth} height={1} onMouseDown={onSelect}>
|
|
80
|
-
<TextLine width={contentWidth} fg={
|
|
81
|
-
<span fg={
|
|
129
|
+
<TextLine width={contentWidth} fg={display.rowFg} bg={selected ? colors.selectedBg : undefined}>
|
|
130
|
+
<span fg={display.indicatorFg}>{fitCell(reviewIcon(pullRequest), reviewWidth)}</span>
|
|
82
131
|
<span> </span>
|
|
83
|
-
<span fg={
|
|
132
|
+
<span fg={display.numberFg}><MatchedCell text={`#${pullRequest.number}`} width={numberWidth} query={filterText} align="right" /></span>
|
|
84
133
|
<span> </span>
|
|
85
134
|
<span><MatchedCell text={pullRequest.title} width={titleWidth} query={filterText} /></span>
|
|
86
|
-
<span fg={
|
|
135
|
+
<span fg={display.checkFg}>{fitCell(display.checkText, checkWidth, "right")}</span>
|
|
87
136
|
<span fg={colors.muted}>{fitCell(ageText, ageWidth, "right")}</span>
|
|
88
137
|
{fillerWidth > 0 ? <span>{" ".repeat(fillerWidth)}</span> : null}
|
|
89
138
|
</TextLine>
|
|
@@ -100,6 +149,9 @@ export const PullRequestList = ({
|
|
|
100
149
|
filterText,
|
|
101
150
|
showFilterBar,
|
|
102
151
|
isFilterEditing,
|
|
152
|
+
loadedCount,
|
|
153
|
+
hasMore,
|
|
154
|
+
isLoadingMore,
|
|
103
155
|
onSelectPullRequest,
|
|
104
156
|
}: {
|
|
105
157
|
groups: PullRequestGroups
|
|
@@ -110,41 +162,43 @@ export const PullRequestList = ({
|
|
|
110
162
|
filterText: string
|
|
111
163
|
showFilterBar: boolean
|
|
112
164
|
isFilterEditing: boolean
|
|
165
|
+
loadedCount: number
|
|
166
|
+
hasMore: boolean
|
|
167
|
+
isLoadingMore: boolean
|
|
113
168
|
onSelectPullRequest: (url: string) => void
|
|
114
169
|
}) => {
|
|
115
|
-
const
|
|
116
|
-
const emptyText = filterText.length > 0 ? "- No matching pull requests." : "- No open pull requests."
|
|
170
|
+
const rows = buildPullRequestListRows({ groups, status, error, filterText, showFilterBar, loadedCount, hasMore, isLoadingMore })
|
|
117
171
|
|
|
118
172
|
return (
|
|
119
173
|
<box width={contentWidth} flexDirection="column">
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
174
|
+
{rows.map((row, index) => {
|
|
175
|
+
if (row._tag === "title") return <SectionTitle key="title" title="PULL REQUESTS" />
|
|
176
|
+
if (row._tag === "filter") {
|
|
177
|
+
return (
|
|
178
|
+
<TextLine key="filter">
|
|
179
|
+
<span fg={colors.count}>/</span>
|
|
180
|
+
<span fg={colors.muted}> </span>
|
|
181
|
+
<span fg={isFilterEditing ? colors.text : colors.count}>{filterText.length > 0 ? filterText : "type to filter..."}</span>
|
|
182
|
+
</TextLine>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if (row._tag === "message") return <PlainLine key={`message-${index}`} text={row.text} fg={row.color} />
|
|
186
|
+
if (row._tag === "load-more") return <PlainLine key="load-more" text={row.text} fg={colors.muted} />
|
|
187
|
+
if (row._tag === "group") return <GroupTitle key={`group-${row.repository}`} label={row.repository} color={repoColor(row.repository)} filterText={filterText} />
|
|
188
|
+
|
|
189
|
+
const numWidth = groupNumberWidth(row.groupPullRequests)
|
|
190
|
+
const ageColWidth = groupAgeWidth(row.groupPullRequests)
|
|
133
191
|
return (
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
onSelect={() => onSelectPullRequest(pullRequest.url)}
|
|
145
|
-
/>
|
|
146
|
-
))}
|
|
147
|
-
</box>
|
|
192
|
+
<PullRequestRow
|
|
193
|
+
key={row.pullRequest.url}
|
|
194
|
+
pullRequest={row.pullRequest}
|
|
195
|
+
selected={row.pullRequest.url === selectedUrl}
|
|
196
|
+
contentWidth={contentWidth}
|
|
197
|
+
numWidth={numWidth}
|
|
198
|
+
ageColWidth={ageColWidth}
|
|
199
|
+
filterText={filterText}
|
|
200
|
+
onSelect={() => onSelectPullRequest(row.pullRequest.url)}
|
|
201
|
+
/>
|
|
148
202
|
)
|
|
149
203
|
})}
|
|
150
204
|
</box>
|