@kitlangton/ghui 0.3.0 → 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/bin/ghui.js +6 -1
- package/dist/index.js +9419 -0
- package/package.json +7 -4
- package/src/App.tsx +0 -2779
- package/src/appCommands.ts +0 -409
- package/src/commands.ts +0 -73
- package/src/config.ts +0 -20
- package/src/date.ts +0 -20
- package/src/domain.ts +0 -149
- 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 -116
- package/src/keymap/changedFilesModal.ts +0 -23
- 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 -93
- 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/submitReviewModal.ts +0 -48
- 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 -43
- 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 -756
- package/src/services/MockGitHubService.ts +0 -182
- package/src/themeStore.ts +0 -60
- package/src/ui/CommandPalette.tsx +0 -176
- 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 -249
- package/src/ui/PullRequestList.tsx +0 -194
- package/src/ui/colors.ts +0 -821
- 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 -964
- package/src/ui/primitives.tsx +0 -350
- package/src/ui/pullRequests.ts +0 -106
- package/src/ui/singleLineInput.ts +0 -26
- package/src/ui/spinner.ts +0 -1
package/src/ui/FooterHints.tsx
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { Data } from "effect"
|
|
2
|
-
import { colors } from "./colors.js"
|
|
3
|
-
import { HintRow, type HintItem } from "./primitives.js"
|
|
4
|
-
|
|
5
|
-
export type RetryProgress = Data.TaggedEnum<{
|
|
6
|
-
Idle: {}
|
|
7
|
-
Retrying: { readonly attempt: number; readonly max: number }
|
|
8
|
-
}>
|
|
9
|
-
|
|
10
|
-
export const RetryProgress = Data.taggedEnum<RetryProgress>()
|
|
11
|
-
export const initialRetryProgress: RetryProgress = RetryProgress.Idle()
|
|
12
|
-
|
|
13
|
-
interface HintsContext {
|
|
14
|
-
readonly filterEditing: boolean
|
|
15
|
-
readonly showFilterClear: boolean
|
|
16
|
-
readonly detailFullView: boolean
|
|
17
|
-
readonly diffFullView: boolean
|
|
18
|
-
readonly diffRangeActive: 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 diffViewHints = (ctx: HintsContext): readonly HintItem[] => [
|
|
37
|
-
{ key: "esc", label: "back" },
|
|
38
|
-
{ key: "↑↓", label: ctx.diffRangeActive ? "range" : "line" },
|
|
39
|
-
{ key: "enter", label: ctx.diffRangeActive ? "comment" : "open" },
|
|
40
|
-
{ key: "v", label: ctx.diffRangeActive ? "clear" : "range" },
|
|
41
|
-
{ key: "n/p", label: "threads" },
|
|
42
|
-
{ key: "[]", label: "files" },
|
|
43
|
-
{ key: "V", label: "view" },
|
|
44
|
-
{ key: "w", label: "wrap" },
|
|
45
|
-
{ key: "r", label: "reload" },
|
|
46
|
-
{ key: "o", label: "open" },
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
const detailFullViewHints = (ctx: HintsContext): readonly HintItem[] => [
|
|
50
|
-
{ key: "esc", label: "back" },
|
|
51
|
-
{ key: "↑↓", label: "scroll" },
|
|
52
|
-
{ key: "r", label: ctx.hasError ? "retry" : "refresh" },
|
|
53
|
-
{ key: "t", label: "theme" },
|
|
54
|
-
{ key: "s", label: "state", when: ctx.hasSelection },
|
|
55
|
-
{ key: "d", label: "diff", when: ctx.hasSelection },
|
|
56
|
-
{ key: "l", label: "labels", when: ctx.hasSelection },
|
|
57
|
-
{ key: "m", label: "merge", when: ctx.hasSelection },
|
|
58
|
-
{ key: "x", label: "close", when: ctx.hasSelection && ctx.canCloseSelection },
|
|
59
|
-
{ key: "o", label: "open" },
|
|
60
|
-
{ key: "y", label: "copy" },
|
|
61
|
-
{ key: "q", label: "quit" },
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
const defaultHints = (ctx: HintsContext): readonly HintItem[] => {
|
|
65
|
-
const retrying = ctx.retryProgress._tag === "Retrying"
|
|
66
|
-
return [
|
|
67
|
-
{ key: "/", label: "filter" },
|
|
68
|
-
{ key: "t", label: "theme" },
|
|
69
|
-
{ key: "esc", label: "clear", when: ctx.showFilterClear },
|
|
70
|
-
{ 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 },
|
|
71
|
-
{ key: ctx.loadingIndicator, label: "loading", when: !retrying && ctx.isLoading, keyFg: colors.status.pending },
|
|
72
|
-
{ key: "r", label: "retry", when: ctx.hasError },
|
|
73
|
-
{ key: "d", label: "diff", when: ctx.hasSelection },
|
|
74
|
-
{ key: "l", label: "labels", when: ctx.hasSelection },
|
|
75
|
-
{ key: "m", label: "merge", when: ctx.hasSelection },
|
|
76
|
-
{ key: "x", label: "close", when: ctx.hasSelection && ctx.canCloseSelection },
|
|
77
|
-
{ key: "o", label: "open", when: ctx.hasSelection },
|
|
78
|
-
{ key: "y", label: "copy", when: ctx.hasSelection },
|
|
79
|
-
{ key: "ctrl-p", label: "commands" },
|
|
80
|
-
]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const footerHints = (ctx: HintsContext): readonly HintItem[] => {
|
|
84
|
-
if (ctx.filterEditing) return filterEditingHints
|
|
85
|
-
if (ctx.diffFullView) return diffViewHints(ctx)
|
|
86
|
-
if (ctx.detailFullView) return detailFullViewHints(ctx)
|
|
87
|
-
return defaultHints(ctx)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const FooterHints = (props: HintsContext) => <HintRow items={footerHints(props)} />
|
package/src/ui/LoadingLogo.tsx
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { TextAttributes } from "@opentui/core"
|
|
2
|
-
import { colors, mixHex } from "./colors.js"
|
|
3
|
-
import type { DetailPlaceholderContent } from "./DetailsPane.js"
|
|
4
|
-
import { centerCell, Filler, PlainLine, TextLine } from "./primitives.js"
|
|
5
|
-
import { SPINNER_FRAMES } from "./spinner.js"
|
|
6
|
-
|
|
7
|
-
type LoadingLogoContent = Pick<DetailPlaceholderContent, "hint">
|
|
8
|
-
|
|
9
|
-
const GHUI_LOGO = ["█▀▀▀ █ █ █ █ ▀█▀", "█ ▀█ █▀▀█ █ █ █ ", "▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀"] as const
|
|
10
|
-
|
|
11
|
-
const LEFT_WORD_WIDTH = 9
|
|
12
|
-
const LOGO_WIDTH = Math.max(...GHUI_LOGO.map((line) => line.length))
|
|
13
|
-
const LOGO_HEIGHT = GHUI_LOGO.length
|
|
14
|
-
const LOGO_BLOCK_HEIGHT = LOGO_HEIGHT + 2
|
|
15
|
-
|
|
16
|
-
const logoColor = (x: number) => (x < LEFT_WORD_WIDTH ? mixHex(colors.accent, colors.text, 0.14) : mixHex(colors.accent, colors.text, 0.52))
|
|
17
|
-
|
|
18
|
-
const LOGO_COLORS = Array.from({ length: LOGO_WIDTH }, (_, index) => logoColor(index))
|
|
19
|
-
const LOGO_ROWS = GHUI_LOGO.map((line) => Array.from(line.padEnd(LOGO_WIDTH, " "), (char, index) => ({ char, color: LOGO_COLORS[index]! })))
|
|
20
|
-
|
|
21
|
-
const LogoRow = ({ row, left }: { row: (typeof LOGO_ROWS)[number]; left: number }) => (
|
|
22
|
-
<TextLine>
|
|
23
|
-
<span fg={colors.muted}>{" ".repeat(left)}</span>
|
|
24
|
-
{row.map(({ char, color }, index) =>
|
|
25
|
-
char === " " ? (
|
|
26
|
-
<span key={index}> </span>
|
|
27
|
-
) : (
|
|
28
|
-
<span key={index} fg={color} attributes={TextAttributes.BOLD}>
|
|
29
|
-
{char}
|
|
30
|
-
</span>
|
|
31
|
-
),
|
|
32
|
-
)}
|
|
33
|
-
</TextLine>
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
export const LoadingLogo = ({ content, width, frame }: { content: LoadingLogoContent; width: number; frame: number }) => {
|
|
37
|
-
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]!
|
|
38
|
-
const logoLeft = Math.max(0, Math.floor((width - LOGO_WIDTH) / 2))
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<box flexDirection="column" width={width}>
|
|
42
|
-
{LOGO_ROWS.map((row, index) => (
|
|
43
|
-
<LogoRow key={index} row={row} left={logoLeft} />
|
|
44
|
-
))}
|
|
45
|
-
<box height={1} />
|
|
46
|
-
<PlainLine text={centerCell(`${spinner} ${content.hint}`, width)} fg={colors.muted} />
|
|
47
|
-
</box>
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const LoadingLogoPane = ({ content, width, height, frame }: { content: LoadingLogoContent; width: number; height: number; frame: number }) => {
|
|
52
|
-
if (width < LOGO_WIDTH + 2 || height < LOGO_BLOCK_HEIGHT) {
|
|
53
|
-
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]!
|
|
54
|
-
const topRows = Math.max(0, Math.floor((height - 1) / 2))
|
|
55
|
-
const bottomRows = Math.max(0, height - topRows - 1)
|
|
56
|
-
return (
|
|
57
|
-
<box height={height} flexDirection="column">
|
|
58
|
-
<Filler rows={topRows} prefix="loading-logo-compact-top" />
|
|
59
|
-
<PlainLine text={centerCell(`${spinner} ${content.hint}`, width)} fg={colors.muted} />
|
|
60
|
-
<Filler rows={bottomRows} prefix="loading-logo-compact-bottom" />
|
|
61
|
-
</box>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const topRows = Math.max(0, Math.floor((height - LOGO_BLOCK_HEIGHT) / 2))
|
|
66
|
-
const bottomRows = Math.max(0, height - topRows - LOGO_BLOCK_HEIGHT)
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<box height={height} flexDirection="column">
|
|
70
|
-
<Filler rows={topRows} prefix="loading-logo-top" />
|
|
71
|
-
<LoadingLogo content={content} width={width} frame={frame} />
|
|
72
|
-
<Filler rows={bottomRows} prefix="loading-logo-bottom" />
|
|
73
|
-
</box>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import type { DiffRenderable, MouseEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
-
import { useMemo, type Ref } from "react"
|
|
3
|
-
import type { DiffCommentSide, PullRequestItem, PullRequestReviewComment } from "../domain.js"
|
|
4
|
-
import { colors, lineNumberTextColor, type ThemeId } from "./colors.js"
|
|
5
|
-
import { CommentBodyLine, commentCountText, commentMetaSegments, CommentSegmentsLine } from "./comments.js"
|
|
6
|
-
import { createDiffSyntaxStyle, diffCommentAnchorLabel, diffCommentLineLabel, diffFileStats, diffFileStatsText, diffStatText, stackedDiffFileIndexAtLine, type DiffFileStats, type DiffView, type DiffWhitespaceMode, type DiffWrapMode, type PullRequestDiffState, type StackedDiffCommentAnchor, type StackedDiffFilePatch } from "./diff.js"
|
|
7
|
-
import { LoadingPane, StatusCard } from "./DetailsPane.js"
|
|
8
|
-
import { DiffStats } from "./diffStats.js"
|
|
9
|
-
import { Divider, fitCell, PaddedRow, PlainLine, TextLine } from "./primitives.js"
|
|
10
|
-
import { shortRepoName } from "./pullRequests.js"
|
|
11
|
-
|
|
12
|
-
const DiffPaneHeader = ({ pullRequest, paneWidth }: { pullRequest: PullRequestItem; paneWidth: number }) => {
|
|
13
|
-
const stats = diffStatText(pullRequest)
|
|
14
|
-
const headerWidth = Math.max(24, paneWidth - 2)
|
|
15
|
-
const leftHeader = `#${pullRequest.number} ${shortRepoName(pullRequest.repository)}`
|
|
16
|
-
const headerGap = Math.max(2, headerWidth - leftHeader.length - stats.length)
|
|
17
|
-
return (
|
|
18
|
-
<PaddedRow>
|
|
19
|
-
<TextLine>
|
|
20
|
-
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
21
|
-
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
22
|
-
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
23
|
-
<DiffStats pullRequest={pullRequest} />
|
|
24
|
-
</TextLine>
|
|
25
|
-
</PaddedRow>
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const FileStats = ({ stats }: { stats: DiffFileStats }) => {
|
|
30
|
-
return (
|
|
31
|
-
<>
|
|
32
|
-
{stats.additions > 0 ? <span fg={colors.status.passing}>{`+${stats.additions}`}</span> : null}
|
|
33
|
-
{stats.additions > 0 && stats.deletions > 0 ? <span fg={colors.muted}> </span> : null}
|
|
34
|
-
{stats.deletions > 0 ? <span fg={colors.status.failing}>{`-${stats.deletions}`}</span> : null}
|
|
35
|
-
</>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const FileHeader = ({
|
|
40
|
-
file,
|
|
41
|
-
index,
|
|
42
|
-
count,
|
|
43
|
-
width,
|
|
44
|
-
suffix = "",
|
|
45
|
-
suffixColor = colors.muted,
|
|
46
|
-
}: {
|
|
47
|
-
file: StackedDiffFilePatch["file"]
|
|
48
|
-
index: number
|
|
49
|
-
count: number
|
|
50
|
-
width: number
|
|
51
|
-
suffix?: string
|
|
52
|
-
suffixColor?: string
|
|
53
|
-
}) => {
|
|
54
|
-
const counter = `${index + 1}/${count}`
|
|
55
|
-
const stats = diffFileStats(file)
|
|
56
|
-
const statsText = diffFileStatsText(stats)
|
|
57
|
-
const nameWidth = Math.max(1, width - counter.length - statsText.length - suffix.length - 5)
|
|
58
|
-
return (
|
|
59
|
-
<TextLine>
|
|
60
|
-
<span fg={colors.muted}>{counter} </span>
|
|
61
|
-
<span fg={colors.text}>{fitCell(file.name, nameWidth)}</span>
|
|
62
|
-
{statsText ? <span fg={colors.muted}> </span> : null}
|
|
63
|
-
<FileStats stats={stats} />
|
|
64
|
-
{suffix ? <span fg={suffixColor}>{suffix}</span> : null}
|
|
65
|
-
</TextLine>
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const PullRequestDiffPane = ({
|
|
70
|
-
pullRequest,
|
|
71
|
-
diffState,
|
|
72
|
-
stackedFiles,
|
|
73
|
-
scrollTop,
|
|
74
|
-
view,
|
|
75
|
-
whitespaceMode,
|
|
76
|
-
wrapMode,
|
|
77
|
-
paneWidth,
|
|
78
|
-
height,
|
|
79
|
-
loadingIndicator,
|
|
80
|
-
scrollRef,
|
|
81
|
-
setDiffRef,
|
|
82
|
-
selectedCommentAnchor,
|
|
83
|
-
selectedCommentLabel,
|
|
84
|
-
selectedCommentThread,
|
|
85
|
-
onSelectCommentLine,
|
|
86
|
-
themeId,
|
|
87
|
-
}: {
|
|
88
|
-
pullRequest: PullRequestItem | null
|
|
89
|
-
diffState: PullRequestDiffState | undefined
|
|
90
|
-
stackedFiles: readonly StackedDiffFilePatch[]
|
|
91
|
-
scrollTop: number
|
|
92
|
-
view: DiffView
|
|
93
|
-
whitespaceMode: DiffWhitespaceMode
|
|
94
|
-
wrapMode: DiffWrapMode
|
|
95
|
-
paneWidth: number
|
|
96
|
-
height: number
|
|
97
|
-
loadingIndicator: string
|
|
98
|
-
scrollRef: Ref<ScrollBoxRenderable>
|
|
99
|
-
setDiffRef: (index: number, diff: DiffRenderable | null) => void
|
|
100
|
-
selectedCommentAnchor: StackedDiffCommentAnchor | null
|
|
101
|
-
selectedCommentLabel: string | null
|
|
102
|
-
selectedCommentThread: readonly PullRequestReviewComment[]
|
|
103
|
-
onSelectCommentLine: (renderLine: number, side: DiffCommentSide | null) => void
|
|
104
|
-
themeId: ThemeId
|
|
105
|
-
}) => {
|
|
106
|
-
const readyFiles = diffState?._tag === "Ready" ? diffState.files : []
|
|
107
|
-
const syntaxStyle = useMemo(() => createDiffSyntaxStyle(), [themeId])
|
|
108
|
-
|
|
109
|
-
if (!pullRequest) {
|
|
110
|
-
return <LoadingPane content={{ title: "No pull request selected", hint: "Press esc to go back" }} width={paneWidth} height={height} />
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!diffState || diffState._tag === "Loading") {
|
|
114
|
-
return (
|
|
115
|
-
<box height={height} flexDirection="column">
|
|
116
|
-
<DiffPaneHeader pullRequest={pullRequest} paneWidth={paneWidth} />
|
|
117
|
-
<Divider width={paneWidth} />
|
|
118
|
-
<LoadingPane content={{ title: `${loadingIndicator} Loading diff`, hint: "Fetching patch from GitHub" }} width={paneWidth} height={Math.max(1, height - 2)} />
|
|
119
|
-
</box>
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (diffState._tag === "Error") {
|
|
124
|
-
return (
|
|
125
|
-
<box height={height} flexDirection="column">
|
|
126
|
-
<PaddedRow>
|
|
127
|
-
<PlainLine text={`#${pullRequest.number} ${shortRepoName(pullRequest.repository)} diff`} fg={colors.count} bold />
|
|
128
|
-
</PaddedRow>
|
|
129
|
-
<Divider width={paneWidth} />
|
|
130
|
-
<StatusCard content={{ title: "Could not load diff", hint: diffState.error }} width={paneWidth} />
|
|
131
|
-
</box>
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (readyFiles.length === 0 || stackedFiles.length === 0) {
|
|
136
|
-
return <LoadingPane content={{ title: whitespaceMode === "ignore" ? "No non-whitespace diff" : "No diff", hint: whitespaceMode === "ignore" ? "Use the command palette to show whitespace changes" : "This PR has no patch contents" }} width={paneWidth} height={height} />
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const hasSelectedCommentAnchor = selectedCommentAnchor !== null
|
|
140
|
-
const commentPeek = hasSelectedCommentAnchor && selectedCommentThread.length > 0
|
|
141
|
-
? selectedCommentThread[selectedCommentThread.length - 1]!
|
|
142
|
-
: null
|
|
143
|
-
const commentPeekMeta = commentPeek && selectedCommentAnchor
|
|
144
|
-
? commentMetaSegments({
|
|
145
|
-
item: commentPeek,
|
|
146
|
-
markerLabel: diffCommentLineLabel(selectedCommentAnchor),
|
|
147
|
-
groups: [
|
|
148
|
-
[{ text: commentCountText(selectedCommentThread.length), fg: colors.muted }],
|
|
149
|
-
[{ text: "enter", fg: colors.text }, { text: " thread", fg: colors.muted }],
|
|
150
|
-
],
|
|
151
|
-
})
|
|
152
|
-
: []
|
|
153
|
-
const stickyScrollTop = Math.max(0, Math.floor(scrollTop))
|
|
154
|
-
const stickyArrayIndex = stackedDiffFileIndexAtLine(stackedFiles, stickyScrollTop)
|
|
155
|
-
const stickyFile = stickyArrayIndex >= 0 ? stackedFiles[stickyArrayIndex] : stackedFiles[0]
|
|
156
|
-
const incomingStickyFile = stickyArrayIndex >= 0 ? stackedFiles[stickyArrayIndex + 1] : undefined
|
|
157
|
-
const incomingHeaderDistance = incomingStickyFile ? incomingStickyFile.headerLine - stickyScrollTop : Number.POSITIVE_INFINITY
|
|
158
|
-
const incomingFile = incomingHeaderDistance === 1 ? incomingStickyFile : undefined
|
|
159
|
-
const stickyCommentLabelFor = (stackedFile: StackedDiffFilePatch | undefined) => {
|
|
160
|
-
if (!selectedCommentAnchor) return " no lines"
|
|
161
|
-
if (selectedCommentAnchor.fileIndex !== stackedFile?.index) return ""
|
|
162
|
-
return ` ${selectedCommentLabel ?? diffCommentAnchorLabel(selectedCommentAnchor)}`
|
|
163
|
-
}
|
|
164
|
-
const stickyCommentColor = selectedCommentAnchor?.side === "LEFT" ? colors.status.failing : colors.status.passing
|
|
165
|
-
const diffLineNumberFg = lineNumberTextColor(colors.diff.lineNumberBg, colors.text)
|
|
166
|
-
const handleDiffMouseDown = function (this: ScrollBoxRenderable, event: MouseEvent) {
|
|
167
|
-
if (event.button !== 0) return
|
|
168
|
-
const localY = event.y - this.viewport.y
|
|
169
|
-
if (localY < 0 || localY >= this.viewport.height) return
|
|
170
|
-
const localX = event.x - this.viewport.x
|
|
171
|
-
const side = view === "split"
|
|
172
|
-
? localX < Math.floor(paneWidth / 2) ? "LEFT" : "RIGHT"
|
|
173
|
-
: null
|
|
174
|
-
onSelectCommentLine(Math.max(0, Math.floor(this.scrollTop + localY)), side)
|
|
175
|
-
event.preventDefault()
|
|
176
|
-
event.stopPropagation()
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<box height={height} flexDirection="column">
|
|
181
|
-
<DiffPaneHeader pullRequest={pullRequest} paneWidth={paneWidth} />
|
|
182
|
-
<Divider width={paneWidth} />
|
|
183
|
-
<scrollbox ref={scrollRef} focusable={false} flexGrow={1} scrollY scrollX={false} onMouseDown={handleDiffMouseDown}>
|
|
184
|
-
{stackedFiles.map((stackedFile) => (
|
|
185
|
-
<box key={`${pullRequest.url}-${stackedFile.index}-${view}-${wrapMode}`} flexDirection="column" flexShrink={0}>
|
|
186
|
-
{stackedFile.index > 0 ? <Divider width={paneWidth} /> : null}
|
|
187
|
-
<PaddedRow>
|
|
188
|
-
<FileHeader file={stackedFile.file} index={stackedFile.index} count={readyFiles.length} width={paneWidth} />
|
|
189
|
-
</PaddedRow>
|
|
190
|
-
<Divider width={paneWidth} />
|
|
191
|
-
<diff
|
|
192
|
-
ref={(diff: DiffRenderable | null) => setDiffRef(stackedFile.index, diff)}
|
|
193
|
-
diff={stackedFile.file.patch}
|
|
194
|
-
view={view}
|
|
195
|
-
syncScroll
|
|
196
|
-
filetype={stackedFile.file.filetype ?? "text"}
|
|
197
|
-
syntaxStyle={syntaxStyle}
|
|
198
|
-
showLineNumbers
|
|
199
|
-
wrapMode={wrapMode}
|
|
200
|
-
addedBg={colors.diff.addedBg}
|
|
201
|
-
removedBg={colors.diff.removedBg}
|
|
202
|
-
contextBg={colors.diff.contextBg}
|
|
203
|
-
addedSignColor={colors.status.passing}
|
|
204
|
-
removedSignColor={colors.status.failing}
|
|
205
|
-
lineNumberFg={diffLineNumberFg}
|
|
206
|
-
lineNumberBg={colors.diff.lineNumberBg}
|
|
207
|
-
addedLineNumberBg={colors.diff.addedLineNumberBg}
|
|
208
|
-
removedLineNumberBg={colors.diff.removedLineNumberBg}
|
|
209
|
-
selectionBg={colors.selectedBg}
|
|
210
|
-
selectionFg={colors.selectedText}
|
|
211
|
-
height={stackedFile.diffHeight}
|
|
212
|
-
style={{ flexShrink: 0 }}
|
|
213
|
-
/>
|
|
214
|
-
</box>
|
|
215
|
-
))}
|
|
216
|
-
</scrollbox>
|
|
217
|
-
{stickyFile ? (
|
|
218
|
-
<box position="absolute" top={2} left={0} width={paneWidth} height={2} zIndex={10} flexDirection="column" backgroundColor={colors.background}>
|
|
219
|
-
{incomingFile ? (
|
|
220
|
-
<>
|
|
221
|
-
<Divider width={paneWidth} />
|
|
222
|
-
<PaddedRow backgroundColor={colors.background}>
|
|
223
|
-
<FileHeader file={incomingFile.file} index={incomingFile.index} count={readyFiles.length} width={paneWidth} suffix={stickyCommentLabelFor(incomingFile)} suffixColor={stickyCommentColor} />
|
|
224
|
-
</PaddedRow>
|
|
225
|
-
</>
|
|
226
|
-
) : (
|
|
227
|
-
<>
|
|
228
|
-
<PaddedRow backgroundColor={colors.background}>
|
|
229
|
-
<FileHeader file={stickyFile.file} index={stickyFile.index} count={readyFiles.length} width={paneWidth} suffix={stickyCommentLabelFor(stickyFile)} suffixColor={stickyCommentColor} />
|
|
230
|
-
</PaddedRow>
|
|
231
|
-
<Divider width={paneWidth} />
|
|
232
|
-
</>
|
|
233
|
-
)}
|
|
234
|
-
</box>
|
|
235
|
-
) : null}
|
|
236
|
-
{commentPeek ? (
|
|
237
|
-
<>
|
|
238
|
-
<Divider width={paneWidth} />
|
|
239
|
-
<PaddedRow>
|
|
240
|
-
<CommentSegmentsLine segments={commentPeekMeta} />
|
|
241
|
-
</PaddedRow>
|
|
242
|
-
<PaddedRow>
|
|
243
|
-
<CommentBodyLine body={commentPeek.body} width={Math.max(1, paneWidth - 2)} />
|
|
244
|
-
</PaddedRow>
|
|
245
|
-
</>
|
|
246
|
-
) : null}
|
|
247
|
-
</box>
|
|
248
|
-
)
|
|
249
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { TextAttributes } from "@opentui/core"
|
|
2
|
-
import type { LoadStatus, PullRequestItem } from "../domain.js"
|
|
3
|
-
import { daysOpen } from "../date.js"
|
|
4
|
-
import { colors } from "./colors.js"
|
|
5
|
-
import { fitCell, MatchedCell, PlainLine, SectionTitle, TextLine } from "./primitives.js"
|
|
6
|
-
import { pullRequestRowDisplay, repoColor, reviewIcon } from "./pullRequests.js"
|
|
7
|
-
|
|
8
|
-
export type PullRequestGroups = Array<[string, PullRequestItem[]]>
|
|
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
|
-
|
|
18
|
-
const GROUP_ICON = "◆"
|
|
19
|
-
|
|
20
|
-
const getRowLayout = (contentWidth: number, numberWidth: number, ageWidth: number) => {
|
|
21
|
-
const reviewWidth = 1
|
|
22
|
-
const checkWidth = 6
|
|
23
|
-
const fixedWidth = reviewWidth + 1 + numberWidth + 1 + checkWidth + ageWidth
|
|
24
|
-
const titleWidth = Math.max(8, contentWidth - fixedWidth)
|
|
25
|
-
return { reviewWidth, checkWidth, ageWidth, numberWidth, titleWidth }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const groupNumberWidth = (pullRequests: readonly PullRequestItem[]) => {
|
|
29
|
-
if (pullRequests.length === 0) return 4
|
|
30
|
-
const maxLen = Math.max(...pullRequests.map((pr) => String(pr.number).length))
|
|
31
|
-
return maxLen + 1
|
|
32
|
-
}
|
|
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
|
-
|
|
40
|
-
const GroupTitle = ({ label, color, filterText }: { label: string; color: string; filterText: string }) => (
|
|
41
|
-
<TextLine>
|
|
42
|
-
<span fg={color}>{GROUP_ICON} </span>
|
|
43
|
-
<span fg={color} attributes={TextAttributes.BOLD}><MatchedCell text={label} width={label.length} query={filterText} /></span>
|
|
44
|
-
</TextLine>
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
export const buildPullRequestListRows = ({
|
|
48
|
-
groups,
|
|
49
|
-
status,
|
|
50
|
-
error,
|
|
51
|
-
filterText,
|
|
52
|
-
showFilterBar,
|
|
53
|
-
loadedCount,
|
|
54
|
-
hasMore,
|
|
55
|
-
isLoadingMore,
|
|
56
|
-
loadingIndicator = "-",
|
|
57
|
-
}: {
|
|
58
|
-
readonly groups: PullRequestGroups
|
|
59
|
-
readonly status: LoadStatus
|
|
60
|
-
readonly error: string | null
|
|
61
|
-
readonly filterText: string
|
|
62
|
-
readonly showFilterBar: boolean
|
|
63
|
-
readonly loadedCount: number
|
|
64
|
-
readonly hasMore: boolean
|
|
65
|
-
readonly isLoadingMore: boolean
|
|
66
|
-
readonly loadingIndicator?: string
|
|
67
|
-
}): readonly PullRequestListRow[] => {
|
|
68
|
-
const itemCount = groups.reduce((count, [, pullRequests]) => count + pullRequests.length, 0)
|
|
69
|
-
const rows: PullRequestListRow[] = [{ _tag: "title" }]
|
|
70
|
-
if (showFilterBar) rows.push({ _tag: "filter" })
|
|
71
|
-
if (status === "loading" && itemCount === 0) rows.push({ _tag: "message", text: "- Loading pull requests...", color: colors.muted })
|
|
72
|
-
if (status === "error") rows.push({ _tag: "message", text: `- ${error ?? "Could not load pull requests."}`, color: colors.error })
|
|
73
|
-
if (status === "ready" && itemCount === 0) rows.push({ _tag: "message", text: filterText.length > 0 ? "- No matching pull requests." : "- No open pull requests.", color: colors.muted })
|
|
74
|
-
for (const [repository, pullRequests] of groups) {
|
|
75
|
-
rows.push({ _tag: "group", repository, pullRequests })
|
|
76
|
-
for (const pullRequest of pullRequests) rows.push({ _tag: "pull-request", pullRequest, groupPullRequests: pullRequests })
|
|
77
|
-
}
|
|
78
|
-
if (status === "ready" && itemCount > 0 && (hasMore || isLoadingMore)) {
|
|
79
|
-
rows.push({ _tag: "load-more", text: isLoadingMore ? `${loadingIndicator} Loading more pull requests... (${loadedCount} loaded)` : `- ${loadedCount} loaded, more available` })
|
|
80
|
-
}
|
|
81
|
-
return rows
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export const pullRequestListRowIndex = (rows: readonly PullRequestListRow[], url: string | null) => {
|
|
85
|
-
if (!url) return null
|
|
86
|
-
const index = rows.findIndex((row) => row._tag === "pull-request" && row.pullRequest.url === url)
|
|
87
|
-
return index >= 0 ? index : null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const PullRequestRow = ({
|
|
91
|
-
pullRequest,
|
|
92
|
-
selected,
|
|
93
|
-
contentWidth,
|
|
94
|
-
numWidth,
|
|
95
|
-
ageColWidth,
|
|
96
|
-
filterText,
|
|
97
|
-
onSelect,
|
|
98
|
-
}: {
|
|
99
|
-
pullRequest: PullRequestItem
|
|
100
|
-
selected: boolean
|
|
101
|
-
contentWidth: number
|
|
102
|
-
numWidth: number
|
|
103
|
-
ageColWidth: number
|
|
104
|
-
filterText: string
|
|
105
|
-
onSelect: () => void
|
|
106
|
-
}) => {
|
|
107
|
-
const ageText = `${daysOpen(pullRequest.createdAt)}d`
|
|
108
|
-
const { reviewWidth, checkWidth, ageWidth, numberWidth, titleWidth } = getRowLayout(contentWidth, numWidth, ageColWidth)
|
|
109
|
-
const rowWidth = reviewWidth + 1 + numberWidth + 1 + titleWidth + checkWidth + ageWidth
|
|
110
|
-
const fillerWidth = Math.max(0, contentWidth - rowWidth)
|
|
111
|
-
const display = pullRequestRowDisplay(pullRequest, selected)
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<box width={contentWidth} height={1} onMouseDown={onSelect}>
|
|
115
|
-
<TextLine width={contentWidth} fg={display.rowFg} bg={selected ? colors.selectedBg : undefined}>
|
|
116
|
-
<span fg={display.indicatorFg}>{fitCell(reviewIcon(pullRequest), reviewWidth)}</span>
|
|
117
|
-
<span> </span>
|
|
118
|
-
<span fg={display.numberFg}><MatchedCell text={`#${pullRequest.number}`} width={numberWidth} query={filterText} align="right" /></span>
|
|
119
|
-
<span> </span>
|
|
120
|
-
<span><MatchedCell text={pullRequest.title} width={titleWidth} query={filterText} /></span>
|
|
121
|
-
<span fg={display.checkFg}>{fitCell(display.checkText, checkWidth, "right")}</span>
|
|
122
|
-
<span fg={colors.muted}>{fitCell(ageText, ageWidth, "right")}</span>
|
|
123
|
-
{fillerWidth > 0 ? <span>{" ".repeat(fillerWidth)}</span> : null}
|
|
124
|
-
</TextLine>
|
|
125
|
-
</box>
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export const PullRequestList = ({
|
|
130
|
-
groups,
|
|
131
|
-
selectedUrl,
|
|
132
|
-
status,
|
|
133
|
-
error,
|
|
134
|
-
contentWidth,
|
|
135
|
-
filterText,
|
|
136
|
-
showFilterBar,
|
|
137
|
-
isFilterEditing,
|
|
138
|
-
loadedCount,
|
|
139
|
-
hasMore,
|
|
140
|
-
isLoadingMore,
|
|
141
|
-
loadingIndicator,
|
|
142
|
-
onSelectPullRequest,
|
|
143
|
-
}: {
|
|
144
|
-
groups: PullRequestGroups
|
|
145
|
-
selectedUrl: string | null
|
|
146
|
-
status: LoadStatus
|
|
147
|
-
error: string | null
|
|
148
|
-
contentWidth: number
|
|
149
|
-
filterText: string
|
|
150
|
-
showFilterBar: boolean
|
|
151
|
-
isFilterEditing: boolean
|
|
152
|
-
loadedCount: number
|
|
153
|
-
hasMore: boolean
|
|
154
|
-
isLoadingMore: boolean
|
|
155
|
-
loadingIndicator: string
|
|
156
|
-
onSelectPullRequest: (url: string) => void
|
|
157
|
-
}) => {
|
|
158
|
-
const rows = buildPullRequestListRows({ groups, status, error, filterText, showFilterBar, loadedCount, hasMore, isLoadingMore, loadingIndicator })
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<box width={contentWidth} flexDirection="column">
|
|
162
|
-
{rows.map((row, index) => {
|
|
163
|
-
if (row._tag === "title") return <SectionTitle key="title" title="PULL REQUESTS" />
|
|
164
|
-
if (row._tag === "filter") {
|
|
165
|
-
return (
|
|
166
|
-
<TextLine key="filter">
|
|
167
|
-
<span fg={colors.count}>/</span>
|
|
168
|
-
<span fg={colors.muted}> </span>
|
|
169
|
-
<span fg={isFilterEditing ? colors.text : colors.count}>{filterText.length > 0 ? filterText : "type to filter..."}</span>
|
|
170
|
-
</TextLine>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
if (row._tag === "message") return <PlainLine key={`message-${index}`} text={row.text} fg={row.color} />
|
|
174
|
-
if (row._tag === "load-more") return <PlainLine key="load-more" text={row.text} fg={colors.muted} />
|
|
175
|
-
if (row._tag === "group") return <GroupTitle key={`group-${row.repository}`} label={row.repository} color={repoColor(row.repository)} filterText={filterText} />
|
|
176
|
-
|
|
177
|
-
const numWidth = groupNumberWidth(row.groupPullRequests)
|
|
178
|
-
const ageColWidth = groupAgeWidth(row.groupPullRequests)
|
|
179
|
-
return (
|
|
180
|
-
<PullRequestRow
|
|
181
|
-
key={row.pullRequest.url}
|
|
182
|
-
pullRequest={row.pullRequest}
|
|
183
|
-
selected={row.pullRequest.url === selectedUrl}
|
|
184
|
-
contentWidth={contentWidth}
|
|
185
|
-
numWidth={numWidth}
|
|
186
|
-
ageColWidth={ageColWidth}
|
|
187
|
-
filterText={filterText}
|
|
188
|
-
onSelect={() => onSelectPullRequest(row.pullRequest.url)}
|
|
189
|
-
/>
|
|
190
|
-
)
|
|
191
|
-
})}
|
|
192
|
-
</box>
|
|
193
|
-
)
|
|
194
|
-
}
|