@kitlangton/ghui 0.2.1 → 0.3.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 +1 -4
- package/bin/ghui.js +6 -1
- package/dist/index.js +9419 -0
- package/package.json +7 -4
- package/src/App.tsx +0 -2604
- package/src/appCommands.ts +0 -384
- package/src/commands.ts +0 -73
- package/src/config.ts +0 -28
- package/src/date.ts +0 -20
- package/src/domain.ts +0 -139
- package/src/errors.ts +0 -10
- package/src/index.tsx +0 -50
- package/src/keyboard/opentuiAdapter.ts +0 -43
- package/src/keymap/all.ts +0 -103
- package/src/keymap/closeModal.ts +0 -13
- package/src/keymap/commandPalette.ts +0 -16
- package/src/keymap/commentModal.ts +0 -73
- package/src/keymap/commentThreadModal.ts +0 -24
- package/src/keymap/detailView.ts +0 -30
- package/src/keymap/diffView.ts +0 -91
- package/src/keymap/filterMode.ts +0 -13
- package/src/keymap/helpers.ts +0 -24
- package/src/keymap/labelModal.ts +0 -16
- package/src/keymap/listNav.ts +0 -119
- package/src/keymap/mergeModal.ts +0 -23
- package/src/keymap/openRepositoryModal.ts +0 -13
- package/src/keymap/themeModal.ts +0 -49
- package/src/mergeActions.ts +0 -88
- package/src/observability.ts +0 -46
- package/src/pullRequestCache.ts +0 -19
- package/src/pullRequestViews.ts +0 -45
- package/src/services/BrowserOpener.ts +0 -22
- package/src/services/Clipboard.ts +0 -46
- package/src/services/CommandRunner.ts +0 -91
- package/src/services/GitHubService.ts +0 -741
- package/src/services/MockGitHubService.ts +0 -181
- package/src/themeStore.ts +0 -60
- package/src/ui/CommandPalette.tsx +0 -216
- package/src/ui/DetailsPane.tsx +0 -656
- package/src/ui/FooterHints.tsx +0 -90
- package/src/ui/LoadingLogo.tsx +0 -75
- package/src/ui/PullRequestDiffPane.tsx +0 -248
- package/src/ui/PullRequestList.tsx +0 -210
- package/src/ui/colors.ts +0 -814
- package/src/ui/commentEditor.ts +0 -126
- package/src/ui/comments.tsx +0 -143
- package/src/ui/diff.ts +0 -650
- package/src/ui/diffStats.tsx +0 -25
- package/src/ui/modals.tsx +0 -614
- package/src/ui/primitives.tsx +0 -205
- package/src/ui/pullRequests.ts +0 -106
- package/src/ui/singleLineInput.ts +0 -26
- package/src/ui/spinner.ts +0 -1
package/src/ui/modals.tsx
DELETED
|
@@ -1,614 +0,0 @@
|
|
|
1
|
-
import { Data } from "effect"
|
|
2
|
-
import type { PullRequestLabel, PullRequestMergeInfo, PullRequestReviewComment } from "../domain.js"
|
|
3
|
-
import { availableMergeActions } from "../mergeActions.js"
|
|
4
|
-
import { clampCursor, commentEditorLines, cursorLineIndexForLines } from "./commentEditor.js"
|
|
5
|
-
import { colors, filterThemeDefinitions, themeDefinitions, type ThemeId } from "./colors.js"
|
|
6
|
-
import { commentDisplayRows, CommentSegmentsLine, type CommentDisplayLine } from "./comments.js"
|
|
7
|
-
import { centerCell, Filler, fitCell, HintRow, PlainLine, StandardModal, standardModalDims, TextLine } from "./primitives.js"
|
|
8
|
-
import { labelColor, shortRepoName } from "./pullRequests.js"
|
|
9
|
-
|
|
10
|
-
export interface LabelModalState {
|
|
11
|
-
readonly repository: string | null
|
|
12
|
-
readonly query: string
|
|
13
|
-
readonly selectedIndex: number
|
|
14
|
-
readonly availableLabels: readonly PullRequestLabel[]
|
|
15
|
-
readonly loading: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface MergeModalState {
|
|
19
|
-
readonly repository: string | null
|
|
20
|
-
readonly number: number | null
|
|
21
|
-
readonly selectedIndex: number
|
|
22
|
-
readonly loading: boolean
|
|
23
|
-
readonly running: boolean
|
|
24
|
-
readonly info: PullRequestMergeInfo | null
|
|
25
|
-
readonly error: string | null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface CloseModalState {
|
|
29
|
-
readonly repository: string | null
|
|
30
|
-
readonly number: number | null
|
|
31
|
-
readonly title: string
|
|
32
|
-
readonly url: string | null
|
|
33
|
-
readonly running: boolean
|
|
34
|
-
readonly error: string | null
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface CommentModalState {
|
|
38
|
-
readonly body: string
|
|
39
|
-
readonly cursor: number
|
|
40
|
-
readonly error: string | null
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface CommentThreadModalState {
|
|
44
|
-
readonly scrollOffset: number
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface ThemeModalState {
|
|
48
|
-
readonly query: string
|
|
49
|
-
readonly filterMode: boolean
|
|
50
|
-
readonly initialThemeId: ThemeId
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface CommandPaletteState {
|
|
54
|
-
readonly query: string
|
|
55
|
-
readonly selectedIndex: number
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface OpenRepositoryModalState {
|
|
59
|
-
readonly query: string
|
|
60
|
-
readonly error: string | null
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export const filterLabels = (labels: readonly PullRequestLabel[], query: string) => {
|
|
64
|
-
const normalized = query.trim().toLowerCase()
|
|
65
|
-
if (normalized.length === 0) return labels
|
|
66
|
-
return labels.filter((label) => label.name.toLowerCase().includes(normalized))
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const initialLabelModalState: LabelModalState = {
|
|
70
|
-
repository: null,
|
|
71
|
-
query: "",
|
|
72
|
-
selectedIndex: 0,
|
|
73
|
-
availableLabels: [],
|
|
74
|
-
loading: false,
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const initialMergeModalState: MergeModalState = {
|
|
78
|
-
repository: null,
|
|
79
|
-
number: null,
|
|
80
|
-
selectedIndex: 0,
|
|
81
|
-
loading: false,
|
|
82
|
-
running: false,
|
|
83
|
-
info: null,
|
|
84
|
-
error: null,
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const initialCloseModalState: CloseModalState = {
|
|
88
|
-
repository: null,
|
|
89
|
-
number: null,
|
|
90
|
-
title: "",
|
|
91
|
-
url: null,
|
|
92
|
-
running: false,
|
|
93
|
-
error: null,
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export const initialCommentModalState: CommentModalState = {
|
|
97
|
-
body: "",
|
|
98
|
-
cursor: 0,
|
|
99
|
-
error: null,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export const initialCommentThreadModalState: CommentThreadModalState = {
|
|
103
|
-
scrollOffset: 0,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export const initialThemeModalState: ThemeModalState = {
|
|
107
|
-
query: "",
|
|
108
|
-
filterMode: false,
|
|
109
|
-
initialThemeId: "ghui",
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export const initialCommandPaletteState: CommandPaletteState = {
|
|
113
|
-
query: "",
|
|
114
|
-
selectedIndex: 0,
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export const initialOpenRepositoryModalState: OpenRepositoryModalState = {
|
|
118
|
-
query: "",
|
|
119
|
-
error: null,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export type Modal = Data.TaggedEnum<{
|
|
123
|
-
None: {}
|
|
124
|
-
Label: LabelModalState
|
|
125
|
-
Close: CloseModalState
|
|
126
|
-
Merge: MergeModalState
|
|
127
|
-
Comment: CommentModalState
|
|
128
|
-
CommentThread: CommentThreadModalState
|
|
129
|
-
Theme: ThemeModalState
|
|
130
|
-
CommandPalette: CommandPaletteState
|
|
131
|
-
OpenRepository: OpenRepositoryModalState
|
|
132
|
-
}>
|
|
133
|
-
|
|
134
|
-
export const Modal = Data.taggedEnum<Modal>()
|
|
135
|
-
export const initialModal: Modal = Modal.None()
|
|
136
|
-
|
|
137
|
-
export type ModalTag = Modal["_tag"]
|
|
138
|
-
export type ModalState<Tag extends Exclude<ModalTag, "None">> = Omit<Extract<Modal, { _tag: Tag }>, "_tag">
|
|
139
|
-
|
|
140
|
-
export const modalInitialStates = {
|
|
141
|
-
Label: initialLabelModalState,
|
|
142
|
-
Close: initialCloseModalState,
|
|
143
|
-
Merge: initialMergeModalState,
|
|
144
|
-
Comment: initialCommentModalState,
|
|
145
|
-
CommentThread: initialCommentThreadModalState,
|
|
146
|
-
Theme: initialThemeModalState,
|
|
147
|
-
CommandPalette: initialCommandPaletteState,
|
|
148
|
-
OpenRepository: initialOpenRepositoryModalState,
|
|
149
|
-
} as const satisfies { [Tag in Exclude<ModalTag, "None">]: ModalState<Tag> }
|
|
150
|
-
|
|
151
|
-
export const OpenRepositoryModal = ({
|
|
152
|
-
state,
|
|
153
|
-
modalWidth,
|
|
154
|
-
modalHeight,
|
|
155
|
-
offsetLeft,
|
|
156
|
-
offsetTop,
|
|
157
|
-
}: {
|
|
158
|
-
state: OpenRepositoryModalState
|
|
159
|
-
modalWidth: number
|
|
160
|
-
modalHeight: number
|
|
161
|
-
offsetLeft: number
|
|
162
|
-
offsetTop: number
|
|
163
|
-
}) => {
|
|
164
|
-
const { contentWidth } = standardModalDims(modalWidth, modalHeight)
|
|
165
|
-
const inputText = state.query.length > 0 ? state.query : "owner/name or GitHub URL"
|
|
166
|
-
|
|
167
|
-
return (
|
|
168
|
-
<StandardModal
|
|
169
|
-
left={offsetLeft}
|
|
170
|
-
top={offsetTop}
|
|
171
|
-
width={modalWidth}
|
|
172
|
-
height={modalHeight}
|
|
173
|
-
title="Open Repository"
|
|
174
|
-
headerRight={{ text: "owner/name" }}
|
|
175
|
-
subtitle={
|
|
176
|
-
<TextLine>
|
|
177
|
-
<span fg={colors.count}>› </span>
|
|
178
|
-
<span fg={state.query.length > 0 ? colors.text : colors.muted}>{fitCell(inputText, Math.max(1, contentWidth - 2))}</span>
|
|
179
|
-
</TextLine>
|
|
180
|
-
}
|
|
181
|
-
bodyPadding={1}
|
|
182
|
-
footer={<HintRow items={[{ key: "enter", label: "open" }, { key: "ctrl-u", label: "clear" }, { key: "ctrl-w", label: "word" }, { key: "esc", label: "cancel" }]} />}
|
|
183
|
-
>
|
|
184
|
-
{state.error ? (
|
|
185
|
-
<PlainLine text={fitCell(state.error, contentWidth)} fg={colors.error} />
|
|
186
|
-
) : (
|
|
187
|
-
<PlainLine text={fitCell("Switches to the selected repository view.", contentWidth)} fg={colors.muted} />
|
|
188
|
-
)}
|
|
189
|
-
</StandardModal>
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const mergeUnavailableReason = (info: PullRequestMergeInfo | null) => {
|
|
194
|
-
if (!info) return "Loading merge status from GitHub."
|
|
195
|
-
if (info.state !== "open") return "This pull request is not open."
|
|
196
|
-
if (info.isDraft) return "Draft pull requests cannot be merged."
|
|
197
|
-
if (info.mergeable === "conflicting") return "This branch has merge conflicts."
|
|
198
|
-
return "No merge actions are currently available."
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export const LabelModal = ({
|
|
202
|
-
state,
|
|
203
|
-
currentLabels,
|
|
204
|
-
modalWidth,
|
|
205
|
-
modalHeight,
|
|
206
|
-
offsetLeft,
|
|
207
|
-
offsetTop,
|
|
208
|
-
loadingIndicator,
|
|
209
|
-
}: {
|
|
210
|
-
state: LabelModalState
|
|
211
|
-
currentLabels: readonly PullRequestLabel[]
|
|
212
|
-
modalWidth: number
|
|
213
|
-
modalHeight: number
|
|
214
|
-
offsetLeft: number
|
|
215
|
-
offsetTop: number
|
|
216
|
-
loadingIndicator: string
|
|
217
|
-
}) => {
|
|
218
|
-
const { contentWidth, bodyHeight: maxVisible, rowWidth } = standardModalDims(modalWidth, modalHeight)
|
|
219
|
-
const currentNames = new Set(currentLabels.map((l) => l.name.toLowerCase()))
|
|
220
|
-
const filtered = filterLabels(state.availableLabels, state.query)
|
|
221
|
-
const labelMessageTopRows = Math.max(0, Math.floor((maxVisible - 1) / 2))
|
|
222
|
-
const labelMessageBottomRows = Math.max(0, maxVisible - labelMessageTopRows - 1)
|
|
223
|
-
const selectedIndex = filtered.length === 0 ? 0 : Math.max(0, Math.min(state.selectedIndex, filtered.length - 1))
|
|
224
|
-
const scrollStart = Math.min(
|
|
225
|
-
Math.max(0, filtered.length - maxVisible),
|
|
226
|
-
Math.max(0, selectedIndex - maxVisible + 1),
|
|
227
|
-
)
|
|
228
|
-
const visibleLabels = filtered.slice(scrollStart, scrollStart + maxVisible)
|
|
229
|
-
const title = state.repository ? `Labels ${shortRepoName(state.repository)}` : "Labels"
|
|
230
|
-
const countText = state.loading ? "loading" : `${filtered.length}/${state.availableLabels.length}`
|
|
231
|
-
const queryText = state.query.length > 0 ? state.query : "type to filter labels"
|
|
232
|
-
const queryPrefix = "/ "
|
|
233
|
-
const queryWidth = Math.max(1, contentWidth - queryPrefix.length)
|
|
234
|
-
|
|
235
|
-
return (
|
|
236
|
-
<StandardModal
|
|
237
|
-
left={offsetLeft}
|
|
238
|
-
top={offsetTop}
|
|
239
|
-
width={modalWidth}
|
|
240
|
-
height={modalHeight}
|
|
241
|
-
title={title}
|
|
242
|
-
headerRight={{ text: countText }}
|
|
243
|
-
subtitle={
|
|
244
|
-
<TextLine>
|
|
245
|
-
<span fg={colors.count}>{queryPrefix}</span>
|
|
246
|
-
<span fg={state.query.length > 0 ? colors.text : colors.muted}>{fitCell(queryText, queryWidth)}</span>
|
|
247
|
-
</TextLine>
|
|
248
|
-
}
|
|
249
|
-
footer={
|
|
250
|
-
<TextLine>
|
|
251
|
-
<span fg={colors.count}>↑↓</span>
|
|
252
|
-
<span fg={colors.muted}> move </span>
|
|
253
|
-
<span fg={colors.count}>esc</span>
|
|
254
|
-
<span fg={colors.muted}> close</span>
|
|
255
|
-
{filtered.length > maxVisible ? <span fg={colors.muted}> {selectedIndex + 1}/{filtered.length}</span> : null}
|
|
256
|
-
</TextLine>
|
|
257
|
-
}
|
|
258
|
-
>
|
|
259
|
-
{state.loading ? (
|
|
260
|
-
<>
|
|
261
|
-
<Filler rows={labelMessageTopRows} prefix="top" />
|
|
262
|
-
<PlainLine text={centerCell(`${loadingIndicator} Loading labels`, rowWidth)} fg={colors.muted} />
|
|
263
|
-
<Filler rows={labelMessageBottomRows} prefix="bottom" />
|
|
264
|
-
</>
|
|
265
|
-
) : visibleLabels.length === 0 ? (
|
|
266
|
-
<>
|
|
267
|
-
<Filler rows={labelMessageTopRows} prefix="top" />
|
|
268
|
-
<PlainLine text={centerCell(state.query.length > 0 ? "No matching labels" : "No labels found", rowWidth)} fg={colors.muted} />
|
|
269
|
-
<Filler rows={labelMessageBottomRows} prefix="bottom" />
|
|
270
|
-
</>
|
|
271
|
-
) : (
|
|
272
|
-
visibleLabels.map((label, index) => {
|
|
273
|
-
const actualIndex = scrollStart + index
|
|
274
|
-
const isActive = currentNames.has(label.name.toLowerCase())
|
|
275
|
-
const isSelected = actualIndex === selectedIndex
|
|
276
|
-
const marker = isActive ? "✓" : " "
|
|
277
|
-
const nameWidth = Math.max(1, rowWidth - 5)
|
|
278
|
-
return (
|
|
279
|
-
<box key={label.name} height={1}>
|
|
280
|
-
<TextLine bg={isSelected ? colors.selectedBg : undefined} fg={isSelected ? colors.selectedText : colors.text}>
|
|
281
|
-
<span fg={isActive ? colors.status.passing : colors.muted}>{marker}</span>
|
|
282
|
-
<span> </span>
|
|
283
|
-
<span bg={labelColor(label)}> </span>
|
|
284
|
-
<span> {fitCell(label.name, nameWidth)}</span>
|
|
285
|
-
</TextLine>
|
|
286
|
-
</box>
|
|
287
|
-
)
|
|
288
|
-
})
|
|
289
|
-
)}
|
|
290
|
-
</StandardModal>
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export const MergeModal = ({
|
|
295
|
-
state,
|
|
296
|
-
modalWidth,
|
|
297
|
-
modalHeight,
|
|
298
|
-
offsetLeft,
|
|
299
|
-
offsetTop,
|
|
300
|
-
loadingIndicator,
|
|
301
|
-
}: {
|
|
302
|
-
state: MergeModalState
|
|
303
|
-
modalWidth: number
|
|
304
|
-
modalHeight: number
|
|
305
|
-
offsetLeft: number
|
|
306
|
-
offsetTop: number
|
|
307
|
-
loadingIndicator: string
|
|
308
|
-
}) => {
|
|
309
|
-
const { contentWidth, bodyHeight: optionAreaHeight, rowWidth } = standardModalDims(modalWidth, modalHeight)
|
|
310
|
-
const options = availableMergeActions(state.info)
|
|
311
|
-
const selectedIndex = options.length === 0 ? 0 : Math.max(0, Math.min(state.selectedIndex, options.length - 1))
|
|
312
|
-
const title = state.info ? `Merge #${state.info.number}` : state.number ? `Merge #${state.number}` : "Merge"
|
|
313
|
-
const rightText = state.running ? `${loadingIndicator} running` : state.loading ? `${loadingIndicator} loading` : state.info?.autoMergeEnabled ? "auto on" : "manual"
|
|
314
|
-
const repo = state.info?.repository ?? state.repository
|
|
315
|
-
const statusLine = state.info
|
|
316
|
-
? `${shortRepoName(state.info.repository)} ${state.info.mergeable} ${state.info.reviewStatus} ${state.info.checkSummary ?? state.info.checkStatus}`
|
|
317
|
-
: repo ? shortRepoName(repo) : ""
|
|
318
|
-
const optionRows = Math.max(1, Math.floor(optionAreaHeight / 2))
|
|
319
|
-
const visibleOptions = options.slice(0, optionRows)
|
|
320
|
-
const loadingTopRows = Math.max(0, Math.floor((optionAreaHeight - 1) / 2))
|
|
321
|
-
const loadingBottomRows = Math.max(0, optionAreaHeight - loadingTopRows - 1)
|
|
322
|
-
|
|
323
|
-
return (
|
|
324
|
-
<StandardModal
|
|
325
|
-
left={offsetLeft}
|
|
326
|
-
top={offsetTop}
|
|
327
|
-
width={modalWidth}
|
|
328
|
-
height={modalHeight}
|
|
329
|
-
title={title}
|
|
330
|
-
headerRight={{ text: rightText, pending: state.running || state.loading }}
|
|
331
|
-
subtitle={<PlainLine text={fitCell(statusLine, contentWidth)} fg={colors.muted} />}
|
|
332
|
-
footer={<HintRow items={[{ key: "↑↓", label: "move" }, { key: "enter", label: "confirm" }, { key: "esc", label: "close" }]} />}
|
|
333
|
-
>
|
|
334
|
-
{state.loading ? (
|
|
335
|
-
<>
|
|
336
|
-
<Filler rows={loadingTopRows} prefix="top" />
|
|
337
|
-
<PlainLine text={centerCell(`${loadingIndicator} Loading merge status`, rowWidth)} fg={colors.muted} />
|
|
338
|
-
<Filler rows={loadingBottomRows} prefix="bottom" />
|
|
339
|
-
</>
|
|
340
|
-
) : state.error ? (
|
|
341
|
-
<PlainLine text={centerCell(state.error, rowWidth)} fg={colors.error} />
|
|
342
|
-
) : visibleOptions.length === 0 ? (
|
|
343
|
-
<PlainLine text={centerCell(mergeUnavailableReason(state.info), rowWidth)} fg={colors.muted} />
|
|
344
|
-
) : (
|
|
345
|
-
visibleOptions.map((option, index) => {
|
|
346
|
-
const isSelected = index === selectedIndex
|
|
347
|
-
const titleColor = option.danger ? colors.error : isSelected ? colors.selectedText : colors.text
|
|
348
|
-
const titleWidth = Math.max(1, rowWidth - 1)
|
|
349
|
-
const descriptionWidth = Math.max(1, rowWidth - 1)
|
|
350
|
-
|
|
351
|
-
return (
|
|
352
|
-
<box key={option.action} height={2} flexDirection="column">
|
|
353
|
-
<TextLine bg={isSelected ? colors.selectedBg : undefined}>
|
|
354
|
-
<span fg={titleColor}> {fitCell(option.title, titleWidth)}</span>
|
|
355
|
-
</TextLine>
|
|
356
|
-
<TextLine bg={isSelected ? colors.selectedBg : undefined}>
|
|
357
|
-
<span fg={colors.muted}> {fitCell(option.description, descriptionWidth)}</span>
|
|
358
|
-
</TextLine>
|
|
359
|
-
</box>
|
|
360
|
-
)
|
|
361
|
-
})
|
|
362
|
-
)}
|
|
363
|
-
</StandardModal>
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
export const CloseModal = ({
|
|
368
|
-
state,
|
|
369
|
-
modalWidth,
|
|
370
|
-
modalHeight,
|
|
371
|
-
offsetLeft,
|
|
372
|
-
offsetTop,
|
|
373
|
-
loadingIndicator,
|
|
374
|
-
}: {
|
|
375
|
-
state: CloseModalState
|
|
376
|
-
modalWidth: number
|
|
377
|
-
modalHeight: number
|
|
378
|
-
offsetLeft: number
|
|
379
|
-
offsetTop: number
|
|
380
|
-
loadingIndicator: string
|
|
381
|
-
}) => {
|
|
382
|
-
const { contentWidth, bodyHeight } = standardModalDims(modalWidth, modalHeight)
|
|
383
|
-
const title = state.number ? `Close #${state.number}` : "Close pull request"
|
|
384
|
-
const rightText = state.running ? `${loadingIndicator} closing` : "confirm"
|
|
385
|
-
const repo = state.repository ? shortRepoName(state.repository) : ""
|
|
386
|
-
const titleLines = [fitCell(repo, contentWidth), fitCell(state.title, contentWidth)]
|
|
387
|
-
const topRows = Math.max(0, Math.floor((bodyHeight - titleLines.length - 2) / 2))
|
|
388
|
-
const bottomRows = Math.max(0, bodyHeight - topRows - titleLines.length - 2)
|
|
389
|
-
|
|
390
|
-
return (
|
|
391
|
-
<StandardModal
|
|
392
|
-
left={offsetLeft}
|
|
393
|
-
top={offsetTop}
|
|
394
|
-
width={modalWidth}
|
|
395
|
-
height={modalHeight}
|
|
396
|
-
title={title}
|
|
397
|
-
titleFg={colors.error}
|
|
398
|
-
headerRight={{ text: rightText, pending: state.running }}
|
|
399
|
-
subtitle={<PlainLine text={fitCell("This will close the pull request without merging it.", contentWidth)} fg={colors.muted} />}
|
|
400
|
-
bodyPadding={1}
|
|
401
|
-
footer={<HintRow items={[{ key: "enter", label: "close" }, { key: "esc", label: "cancel" }]} />}
|
|
402
|
-
>
|
|
403
|
-
{state.error ? (
|
|
404
|
-
<PlainLine text={fitCell(state.error, contentWidth)} fg={colors.error} />
|
|
405
|
-
) : (
|
|
406
|
-
<>
|
|
407
|
-
<Filler rows={topRows} prefix="top" />
|
|
408
|
-
<PlainLine text={titleLines[0]!} fg={colors.muted} />
|
|
409
|
-
<PlainLine text={titleLines[1]!} fg={colors.text} bold />
|
|
410
|
-
<Filler rows={bottomRows} prefix="bottom" />
|
|
411
|
-
</>
|
|
412
|
-
)}
|
|
413
|
-
</StandardModal>
|
|
414
|
-
)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export const CommentModal = ({
|
|
418
|
-
state,
|
|
419
|
-
anchorLabel,
|
|
420
|
-
modalWidth,
|
|
421
|
-
modalHeight,
|
|
422
|
-
offsetLeft,
|
|
423
|
-
offsetTop,
|
|
424
|
-
}: {
|
|
425
|
-
state: CommentModalState
|
|
426
|
-
anchorLabel: string
|
|
427
|
-
modalWidth: number
|
|
428
|
-
modalHeight: number
|
|
429
|
-
offsetLeft: number
|
|
430
|
-
offsetTop: number
|
|
431
|
-
}) => {
|
|
432
|
-
const { contentWidth, bodyHeight } = standardModalDims(modalWidth, modalHeight)
|
|
433
|
-
const title = "Comment"
|
|
434
|
-
const editorHeight = Math.max(1, bodyHeight - (state.error ? 1 : 0))
|
|
435
|
-
const lineRanges = commentEditorLines(state.body)
|
|
436
|
-
const cursor = clampCursor(state.body, state.cursor)
|
|
437
|
-
const cursorLineIndex = cursorLineIndexForLines(lineRanges, cursor)
|
|
438
|
-
const visibleStart = Math.min(
|
|
439
|
-
Math.max(0, lineRanges.length - editorHeight),
|
|
440
|
-
Math.max(0, cursorLineIndex - editorHeight + 1),
|
|
441
|
-
)
|
|
442
|
-
const visibleLines = lineRanges.slice(visibleStart, visibleStart + editorHeight)
|
|
443
|
-
const renderEditorLine = (line: { readonly text: string; readonly start: number; readonly end: number }, index: number) => {
|
|
444
|
-
const lineIndex = visibleStart + index
|
|
445
|
-
const isCursorLine = lineIndex === cursorLineIndex
|
|
446
|
-
const cursorColumn = Math.max(0, Math.min(cursor - line.start, line.text.length))
|
|
447
|
-
const viewStart = isCursorLine ? Math.max(0, cursorColumn - contentWidth + 1) : 0
|
|
448
|
-
const visibleText = line.text.slice(viewStart, viewStart + contentWidth)
|
|
449
|
-
|
|
450
|
-
if (!isCursorLine) {
|
|
451
|
-
return <PlainLine key={lineIndex} text={fitCell(visibleText, contentWidth)} fg={state.body.length > 0 ? colors.text : colors.muted} />
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const cursorInView = cursorColumn - viewStart
|
|
455
|
-
const before = visibleText.slice(0, cursorInView)
|
|
456
|
-
const placeholder = state.body.length === 0 ? "Write a comment..." : ""
|
|
457
|
-
const cursorChar = placeholder ? placeholder[0] ?? " " : visibleText[cursorInView] ?? " "
|
|
458
|
-
const after = placeholder ? placeholder.slice(1) : visibleText.slice(cursorInView + 1)
|
|
459
|
-
|
|
460
|
-
return (
|
|
461
|
-
<TextLine key={lineIndex}>
|
|
462
|
-
{before ? <span fg={colors.text}>{before}</span> : null}
|
|
463
|
-
<span bg={colors.accent} fg={colors.background}>{cursorChar}</span>
|
|
464
|
-
{after ? <span fg={placeholder ? colors.muted : colors.text}>{after}</span> : null}
|
|
465
|
-
</TextLine>
|
|
466
|
-
)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return (
|
|
470
|
-
<StandardModal
|
|
471
|
-
left={offsetLeft}
|
|
472
|
-
top={offsetTop}
|
|
473
|
-
width={modalWidth}
|
|
474
|
-
height={modalHeight}
|
|
475
|
-
title={title}
|
|
476
|
-
headerRight={{ text: "enter save" }}
|
|
477
|
-
subtitle={<PlainLine text={fitCell(anchorLabel, contentWidth)} fg={colors.muted} />}
|
|
478
|
-
bodyPadding={1}
|
|
479
|
-
footer={<HintRow items={[{ key: "enter", label: "save" }, { key: "shift-enter", label: "newline" }, { key: "esc", label: "cancel" }]} />}
|
|
480
|
-
>
|
|
481
|
-
{state.error ? <PlainLine text={fitCell(state.error, contentWidth)} fg={colors.error} /> : null}
|
|
482
|
-
{visibleLines.map(renderEditorLine)}
|
|
483
|
-
</StandardModal>
|
|
484
|
-
)
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const commentThreadRows = (comments: readonly PullRequestReviewComment[], width: number): readonly CommentDisplayLine[] =>
|
|
488
|
-
comments.flatMap((comment) => commentDisplayRows({ item: comment, width }))
|
|
489
|
-
|
|
490
|
-
export const CommentThreadModal = ({
|
|
491
|
-
state,
|
|
492
|
-
anchorLabel,
|
|
493
|
-
comments,
|
|
494
|
-
modalWidth,
|
|
495
|
-
modalHeight,
|
|
496
|
-
offsetLeft,
|
|
497
|
-
offsetTop,
|
|
498
|
-
}: {
|
|
499
|
-
state: CommentThreadModalState
|
|
500
|
-
anchorLabel: string
|
|
501
|
-
comments: readonly PullRequestReviewComment[]
|
|
502
|
-
modalWidth: number
|
|
503
|
-
modalHeight: number
|
|
504
|
-
offsetLeft: number
|
|
505
|
-
offsetTop: number
|
|
506
|
-
}) => {
|
|
507
|
-
const { contentWidth, bodyHeight } = standardModalDims(modalWidth, modalHeight)
|
|
508
|
-
const title = "Thread"
|
|
509
|
-
const countText = comments.length === 1 ? "1 comment" : `${comments.length} comments`
|
|
510
|
-
const rows = commentThreadRows(comments, contentWidth)
|
|
511
|
-
const maxScroll = Math.max(0, rows.length - bodyHeight)
|
|
512
|
-
const scrollOffset = Math.max(0, Math.min(state.scrollOffset, maxScroll))
|
|
513
|
-
const visibleRows = rows.slice(scrollOffset, scrollOffset + bodyHeight)
|
|
514
|
-
|
|
515
|
-
return (
|
|
516
|
-
<StandardModal
|
|
517
|
-
left={offsetLeft}
|
|
518
|
-
top={offsetTop}
|
|
519
|
-
width={modalWidth}
|
|
520
|
-
height={modalHeight}
|
|
521
|
-
title={title}
|
|
522
|
-
headerRight={{ text: countText }}
|
|
523
|
-
subtitle={<PlainLine text={fitCell(anchorLabel, contentWidth)} fg={colors.muted} />}
|
|
524
|
-
bodyPadding={1}
|
|
525
|
-
footer={<HintRow items={[{ key: "↑↓", label: "scroll" }, { key: "a", label: "comment" }, { key: "esc", label: "close" }]} />}
|
|
526
|
-
>
|
|
527
|
-
{visibleRows.length === 0 ? (
|
|
528
|
-
<PlainLine text={fitCell("No comments on this line.", contentWidth)} fg={colors.muted} />
|
|
529
|
-
) : visibleRows.map((row) => <CommentSegmentsLine key={row.key} segments={row.segments} />)}
|
|
530
|
-
</StandardModal>
|
|
531
|
-
)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
export const ThemeModal = ({
|
|
535
|
-
state,
|
|
536
|
-
activeThemeId,
|
|
537
|
-
modalWidth,
|
|
538
|
-
modalHeight,
|
|
539
|
-
offsetLeft,
|
|
540
|
-
offsetTop,
|
|
541
|
-
}: {
|
|
542
|
-
state: ThemeModalState
|
|
543
|
-
activeThemeId: ThemeId
|
|
544
|
-
modalWidth: number
|
|
545
|
-
modalHeight: number
|
|
546
|
-
offsetLeft: number
|
|
547
|
-
offsetTop: number
|
|
548
|
-
}) => {
|
|
549
|
-
const { contentWidth, bodyHeight: maxVisible, rowWidth } = standardModalDims(modalWidth, modalHeight)
|
|
550
|
-
const filteredThemes = filterThemeDefinitions(state.query)
|
|
551
|
-
const activeIndex = filteredThemes.findIndex((theme) => theme.id === activeThemeId)
|
|
552
|
-
const selectedIndex = Math.max(0, activeIndex)
|
|
553
|
-
const selectedTheme = filteredThemes[selectedIndex] ?? themeDefinitions.find((theme) => theme.id === activeThemeId) ?? themeDefinitions[0]!
|
|
554
|
-
const scrollStart = Math.min(
|
|
555
|
-
Math.max(0, filteredThemes.length - maxVisible),
|
|
556
|
-
Math.max(0, selectedIndex - maxVisible + 1),
|
|
557
|
-
)
|
|
558
|
-
const visibleThemes = filteredThemes.slice(scrollStart, scrollStart + maxVisible)
|
|
559
|
-
const countText = `${filteredThemes.length === 0 ? 0 : selectedIndex + 1}/${filteredThemes.length}`
|
|
560
|
-
const subtitleText = state.filterMode ? (state.query.length > 0 ? state.query : "type to filter themes") : selectedTheme.description
|
|
561
|
-
const queryPrefix = "/ "
|
|
562
|
-
const subtitleWidth = Math.max(1, contentWidth - (state.filterMode ? queryPrefix.length : 0))
|
|
563
|
-
const messageTopRows = Math.max(0, Math.floor((maxVisible - 1) / 2))
|
|
564
|
-
const messageBottomRows = Math.max(0, maxVisible - messageTopRows - 1)
|
|
565
|
-
|
|
566
|
-
return (
|
|
567
|
-
<StandardModal
|
|
568
|
-
left={offsetLeft}
|
|
569
|
-
top={offsetTop}
|
|
570
|
-
width={modalWidth}
|
|
571
|
-
height={modalHeight}
|
|
572
|
-
title="Themes"
|
|
573
|
-
headerRight={{ text: countText }}
|
|
574
|
-
subtitle={state.filterMode ? (
|
|
575
|
-
<TextLine>
|
|
576
|
-
<span fg={colors.count}>{queryPrefix}</span>
|
|
577
|
-
<span fg={state.query.length > 0 ? colors.text : colors.muted}>{fitCell(subtitleText, subtitleWidth)}</span>
|
|
578
|
-
</TextLine>
|
|
579
|
-
) : (
|
|
580
|
-
<PlainLine text={fitCell(subtitleText, subtitleWidth)} fg={colors.muted} />
|
|
581
|
-
)}
|
|
582
|
-
footer={<HintRow items={[{ key: "↑↓", label: "preview" }, { key: "/", label: "filter" }, { key: "enter", label: "select" }, { key: "esc", label: "cancel" }]} />}
|
|
583
|
-
>
|
|
584
|
-
{visibleThemes.length === 0 ? (
|
|
585
|
-
<>
|
|
586
|
-
<Filler rows={messageTopRows} prefix="top" />
|
|
587
|
-
<PlainLine text={centerCell("No matching themes", rowWidth)} fg={colors.muted} />
|
|
588
|
-
<Filler rows={messageBottomRows} prefix="bottom" />
|
|
589
|
-
</>
|
|
590
|
-
) : visibleThemes.map((theme, index) => {
|
|
591
|
-
const actualIndex = scrollStart + index
|
|
592
|
-
const isSelected = actualIndex === selectedIndex
|
|
593
|
-
const isActive = theme.id === activeThemeId
|
|
594
|
-
const marker = isActive ? "✓" : " "
|
|
595
|
-
const swatchWidth = 6
|
|
596
|
-
const nameWidth = Math.max(1, rowWidth - swatchWidth - 3)
|
|
597
|
-
|
|
598
|
-
return (
|
|
599
|
-
<TextLine key={theme.id} bg={isSelected ? colors.selectedBg : undefined} fg={isSelected ? colors.selectedText : colors.text}>
|
|
600
|
-
<span fg={isActive ? colors.status.passing : colors.muted}>{marker}</span>
|
|
601
|
-
<span> </span>
|
|
602
|
-
<span>{fitCell(theme.name, nameWidth)}</span>
|
|
603
|
-
<span bg={theme.colors.background}> </span>
|
|
604
|
-
<span bg={theme.colors.modalBackground}> </span>
|
|
605
|
-
<span bg={theme.colors.accent}> </span>
|
|
606
|
-
<span bg={theme.colors.status.passing}> </span>
|
|
607
|
-
<span bg={theme.colors.status.failing}> </span>
|
|
608
|
-
<span bg={theme.colors.status.review}> </span>
|
|
609
|
-
</TextLine>
|
|
610
|
-
)
|
|
611
|
-
})}
|
|
612
|
-
</StandardModal>
|
|
613
|
-
)
|
|
614
|
-
}
|