@kitlangton/ghui 0.1.11 → 0.1.13
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/package.json +1 -1
- package/src/App.tsx +170 -41
- package/src/ui/FooterHints.tsx +2 -0
- package/src/ui/PullRequestDiffPane.tsx +12 -9
- package/src/ui/colors.ts +374 -2
- package/src/ui/diff.ts +13 -13
- package/src/ui/modals.tsx +94 -1
- package/src/ui/primitives.tsx +1 -1
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -11,12 +11,12 @@ import { formatShortDate, formatTimestamp } from "./date.js"
|
|
|
11
11
|
import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
|
|
12
12
|
import { Observability } from "./observability.js"
|
|
13
13
|
import { GitHubService } from "./services/GitHubService.js"
|
|
14
|
-
import { colors } from "./ui/colors.js"
|
|
14
|
+
import { colors, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
15
15
|
import { pullRequestDiffKey, splitPatchFiles, type PullRequestDiffState } from "./ui/diff.js"
|
|
16
16
|
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailBodyHeight, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
17
17
|
import { FooterHints, type RetryProgress } from "./ui/FooterHints.js"
|
|
18
18
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
19
|
-
import { initialLabelModalState, initialMergeModalState, LabelModal, MergeModal } from "./ui/modals.js"
|
|
19
|
+
import { initialLabelModalState, initialMergeModalState, initialThemeModalState, LabelModal, MergeModal, ThemeModal } from "./ui/modals.js"
|
|
20
20
|
import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
21
21
|
import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
22
22
|
import { PullRequestList } from "./ui/PullRequestList.js"
|
|
@@ -81,6 +81,8 @@ const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>
|
|
|
81
81
|
|
|
82
82
|
const labelModalAtom = Atom.make(initialLabelModalState).pipe(Atom.keepAlive)
|
|
83
83
|
const mergeModalAtom = Atom.make(initialMergeModalState).pipe(Atom.keepAlive)
|
|
84
|
+
const themeIdAtom = Atom.make<ThemeId>("ghui").pipe(Atom.keepAlive)
|
|
85
|
+
const themeModalAtom = Atom.make(initialThemeModalState).pipe(Atom.keepAlive)
|
|
84
86
|
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
85
87
|
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
86
88
|
const usernameAtom = githubRuntime.atom(
|
|
@@ -118,6 +120,64 @@ const deleteLastWord = (value: string) => value.replace(/\s*\S+\s*$/, "")
|
|
|
118
120
|
|
|
119
121
|
const errorMessage = (error: unknown) => error instanceof Error ? error.message : String(error)
|
|
120
122
|
|
|
123
|
+
const clipboardCommands = (): readonly (readonly string[])[] => {
|
|
124
|
+
if (process.platform === "darwin") return [["pbcopy"]]
|
|
125
|
+
if (process.platform === "linux") {
|
|
126
|
+
return [
|
|
127
|
+
...(process.env.WAYLAND_DISPLAY ? [["wl-copy"]] : []),
|
|
128
|
+
["xclip", "-selection", "clipboard"],
|
|
129
|
+
["xsel", "--clipboard", "--input"],
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
return []
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const copyToClipboard = async (text: string) => {
|
|
136
|
+
const commands = clipboardCommands()
|
|
137
|
+
let lastError = ""
|
|
138
|
+
|
|
139
|
+
for (const command of commands) {
|
|
140
|
+
let proc: Bun.Subprocess<"pipe", "ignore", "pipe">
|
|
141
|
+
try {
|
|
142
|
+
proc = Bun.spawn({
|
|
143
|
+
cmd: [...command],
|
|
144
|
+
stdin: "pipe",
|
|
145
|
+
stdout: "ignore",
|
|
146
|
+
stderr: "pipe",
|
|
147
|
+
})
|
|
148
|
+
} catch (error) {
|
|
149
|
+
lastError = errorMessage(error)
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
proc.stdin.write(text)
|
|
154
|
+
proc.stdin.end()
|
|
155
|
+
|
|
156
|
+
const exitCode = await proc.exited
|
|
157
|
+
if (exitCode === 0) return
|
|
158
|
+
|
|
159
|
+
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
160
|
+
lastError = stderr.trim()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const installHint = process.platform === "linux" ? " Install wl-clipboard, xclip, or xsel." : ""
|
|
164
|
+
throw new Error(lastError || `Clipboard is not available.${installHint}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const openPullRequestInBrowser = async (pullRequest: PullRequestItem) => {
|
|
168
|
+
const proc = Bun.spawn({
|
|
169
|
+
cmd: ["gh", "pr", "view", String(pullRequest.number), "--repo", pullRequest.repository, "--web"],
|
|
170
|
+
stdout: "ignore",
|
|
171
|
+
stderr: "pipe",
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const exitCode = await proc.exited
|
|
175
|
+
if (exitCode === 0) return
|
|
176
|
+
|
|
177
|
+
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
178
|
+
throw new Error(stderr.trim() || "Could not open PR in browser")
|
|
179
|
+
}
|
|
180
|
+
|
|
121
181
|
const copyPullRequestMetadata = async (pullRequest: PullRequestItem) => {
|
|
122
182
|
const lines = [
|
|
123
183
|
pullRequest.title,
|
|
@@ -133,29 +193,13 @@ const copyPullRequestMetadata = async (pullRequest: PullRequestItem) => {
|
|
|
133
193
|
lines.push(pullRequest.checkSummary)
|
|
134
194
|
}
|
|
135
195
|
|
|
136
|
-
|
|
137
|
-
cmd: ["pbcopy"],
|
|
138
|
-
stdin: "pipe",
|
|
139
|
-
stdout: "ignore",
|
|
140
|
-
stderr: "pipe",
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
if (!proc.stdin) {
|
|
144
|
-
throw new Error("Clipboard is not available")
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
proc.stdin.write(lines.join("\n"))
|
|
148
|
-
proc.stdin.end()
|
|
149
|
-
|
|
150
|
-
const exitCode = await proc.exited
|
|
151
|
-
if (exitCode !== 0) {
|
|
152
|
-
const stderr = await Bun.readableStreamToText(proc.stderr)
|
|
153
|
-
throw new Error(stderr.trim() || "Could not copy PR metadata")
|
|
154
|
-
}
|
|
196
|
+
await copyToClipboard(lines.join("\n"))
|
|
155
197
|
}
|
|
156
198
|
|
|
157
199
|
const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "G" || key.name === "g" && key.shift
|
|
158
200
|
|
|
201
|
+
const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
|
|
202
|
+
|
|
159
203
|
const getDetailPlaceholderContent = ({
|
|
160
204
|
status,
|
|
161
205
|
retryProgress,
|
|
@@ -217,6 +261,9 @@ export const App = () => {
|
|
|
217
261
|
const [pullRequestDiffCache, setPullRequestDiffCache] = useAtom(pullRequestDiffCacheAtom)
|
|
218
262
|
const [labelModal, setLabelModal] = useAtom(labelModalAtom)
|
|
219
263
|
const [mergeModal, setMergeModal] = useAtom(mergeModalAtom)
|
|
264
|
+
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
265
|
+
const [themeModal, setThemeModal] = useAtom(themeModalAtom)
|
|
266
|
+
setActiveTheme(themeId)
|
|
220
267
|
const [labelCache, setLabelCache] = useAtom(labelCacheAtom)
|
|
221
268
|
const [pullRequestOverrides, setPullRequestOverrides] = useAtom(pullRequestOverridesAtom)
|
|
222
269
|
const retryProgress = useAtomValue(retryProgressAtom)
|
|
@@ -231,8 +278,10 @@ export const App = () => {
|
|
|
231
278
|
const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
|
|
232
279
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
233
280
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
234
|
-
const
|
|
235
|
-
const
|
|
281
|
+
const terminalWidth = width ?? 100
|
|
282
|
+
const terminalHeight = height ?? 24
|
|
283
|
+
const contentWidth = Math.max(1, terminalWidth)
|
|
284
|
+
const isWideLayout = terminalWidth >= 100
|
|
236
285
|
const splitGap = 1
|
|
237
286
|
const sectionPadding = 1
|
|
238
287
|
const leftPaneWidth = isWideLayout ? Math.max(44, Math.floor((contentWidth - splitGap) * 0.56)) : contentWidth
|
|
@@ -240,8 +289,8 @@ export const App = () => {
|
|
|
240
289
|
const dividerJunctionAt = Math.max(1, leftPaneWidth)
|
|
241
290
|
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 3) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
242
291
|
const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
|
|
243
|
-
const wideDetailLines = Math.max(8,
|
|
244
|
-
const wideBodyHeight = Math.max(8,
|
|
292
|
+
const wideDetailLines = Math.max(8, terminalHeight - 8) // fill available vertical space
|
|
293
|
+
const wideBodyHeight = Math.max(8, terminalHeight - 4)
|
|
245
294
|
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
246
295
|
const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
247
296
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
@@ -266,6 +315,10 @@ export const App = () => {
|
|
|
266
315
|
}, 2500)
|
|
267
316
|
}
|
|
268
317
|
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
renderer.setBackgroundColor(colors.background)
|
|
320
|
+
}, [renderer, themeId])
|
|
321
|
+
|
|
269
322
|
useEffect(() => () => {
|
|
270
323
|
if (noticeTimeoutRef.current !== null) {
|
|
271
324
|
clearTimeout(noticeTimeoutRef.current)
|
|
@@ -495,9 +548,43 @@ export const App = () => {
|
|
|
495
548
|
loadPullRequestDiff(selectedPullRequest)
|
|
496
549
|
}
|
|
497
550
|
|
|
551
|
+
const openSelectedPullRequestInBrowser = (pullRequest: PullRequestItem) => {
|
|
552
|
+
void openPullRequestInBrowser(pullRequest)
|
|
553
|
+
.then(() => flashNotice(`Opened #${pullRequest.number} in browser`))
|
|
554
|
+
.catch((error) => flashNotice(errorMessage(error)))
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const openThemeModal = () => {
|
|
558
|
+
setLabelModal(initialLabelModalState)
|
|
559
|
+
setMergeModal(initialMergeModalState)
|
|
560
|
+
setThemeModal({
|
|
561
|
+
open: true,
|
|
562
|
+
initialThemeId: themeId,
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const closeThemeModal = (confirm: boolean) => {
|
|
567
|
+
const selectedTheme = themeDefinitions.find((theme) => theme.id === themeId)
|
|
568
|
+
if (!confirm) {
|
|
569
|
+
setThemeId(themeModal.initialThemeId)
|
|
570
|
+
} else if (selectedTheme) {
|
|
571
|
+
flashNotice(`Theme: ${selectedTheme.name}`)
|
|
572
|
+
}
|
|
573
|
+
setThemeModal(initialThemeModalState)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const moveThemeSelection = (delta: number) => {
|
|
577
|
+
const currentIndex = Math.max(0, themeDefinitions.findIndex((theme) => theme.id === themeId))
|
|
578
|
+
const selectedIndex = Math.max(0, Math.min(themeDefinitions.length - 1, currentIndex + delta))
|
|
579
|
+
if (selectedIndex === currentIndex) return
|
|
580
|
+
const theme = themeDefinitions[selectedIndex]
|
|
581
|
+
if (theme && theme.id !== themeId) setThemeId(theme.id)
|
|
582
|
+
}
|
|
583
|
+
|
|
498
584
|
const openLabelModal = () => {
|
|
499
585
|
if (!selectedPullRequest) return
|
|
500
586
|
setMergeModal(initialMergeModalState)
|
|
587
|
+
setThemeModal(initialThemeModalState)
|
|
501
588
|
const repository = selectedPullRequest.repository
|
|
502
589
|
const cachedLabels = labelCache[repository]
|
|
503
590
|
if (cachedLabels) {
|
|
@@ -526,6 +613,7 @@ export const App = () => {
|
|
|
526
613
|
|
|
527
614
|
const openMergeModal = () => {
|
|
528
615
|
if (!selectedPullRequest) return
|
|
616
|
+
setThemeModal(initialThemeModalState)
|
|
529
617
|
const repository = selectedPullRequest.repository
|
|
530
618
|
const number = selectedPullRequest.number
|
|
531
619
|
const seededInfo = mergeInfoFromPullRequest(selectedPullRequest)
|
|
@@ -627,6 +715,10 @@ export const App = () => {
|
|
|
627
715
|
|
|
628
716
|
useKeyboard((key) => {
|
|
629
717
|
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
718
|
+
if (themeModal.open) {
|
|
719
|
+
closeThemeModal(false)
|
|
720
|
+
return
|
|
721
|
+
}
|
|
630
722
|
if (mergeModal.open) {
|
|
631
723
|
setMergeModal(initialMergeModalState)
|
|
632
724
|
return
|
|
@@ -639,6 +731,26 @@ export const App = () => {
|
|
|
639
731
|
return
|
|
640
732
|
}
|
|
641
733
|
|
|
734
|
+
if (themeModal.open) {
|
|
735
|
+
if (key.name === "escape") {
|
|
736
|
+
closeThemeModal(false)
|
|
737
|
+
return
|
|
738
|
+
}
|
|
739
|
+
if (key.name === "return" || key.name === "enter") {
|
|
740
|
+
closeThemeModal(true)
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
if (key.name === "up" || key.name === "k") {
|
|
744
|
+
moveThemeSelection(-1)
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
if (key.name === "down" || key.name === "j") {
|
|
748
|
+
moveThemeSelection(1)
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
|
|
642
754
|
if (mergeModal.open) {
|
|
643
755
|
const options = availableMergeActions(mergeModal.info)
|
|
644
756
|
if (key.name === "escape") {
|
|
@@ -803,8 +915,7 @@ export const App = () => {
|
|
|
803
915
|
return
|
|
804
916
|
}
|
|
805
917
|
if (key.name === "o" && selectedPullRequest) {
|
|
806
|
-
|
|
807
|
-
flashNotice(`Opened #${selectedPullRequest.number} in browser`)
|
|
918
|
+
openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
808
919
|
return
|
|
809
920
|
}
|
|
810
921
|
return
|
|
@@ -881,8 +992,7 @@ export const App = () => {
|
|
|
881
992
|
return
|
|
882
993
|
}
|
|
883
994
|
if (key.name === "o" && selectedPullRequest) {
|
|
884
|
-
|
|
885
|
-
flashNotice(`Opened #${selectedPullRequest.number} in browser`)
|
|
995
|
+
openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
886
996
|
return
|
|
887
997
|
}
|
|
888
998
|
if (key.name === "y" && selectedPullRequest) {
|
|
@@ -923,6 +1033,11 @@ export const App = () => {
|
|
|
923
1033
|
}
|
|
924
1034
|
}
|
|
925
1035
|
|
|
1036
|
+
if (isThemeKey(key)) {
|
|
1037
|
+
openThemeModal()
|
|
1038
|
+
return
|
|
1039
|
+
}
|
|
1040
|
+
|
|
926
1041
|
if (key.name === "/") {
|
|
927
1042
|
setFilterDraft(filterQuery)
|
|
928
1043
|
setFilterMode(true)
|
|
@@ -1037,8 +1152,7 @@ export const App = () => {
|
|
|
1037
1152
|
return
|
|
1038
1153
|
}
|
|
1039
1154
|
if (key.name === "o" && selectedPullRequest) {
|
|
1040
|
-
|
|
1041
|
-
flashNotice(`Opened #${selectedPullRequest.number} in browser`)
|
|
1155
|
+
openSelectedPullRequestInBrowser(selectedPullRequest)
|
|
1042
1156
|
return
|
|
1043
1157
|
}
|
|
1044
1158
|
if ((key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
@@ -1070,7 +1184,7 @@ export const App = () => {
|
|
|
1070
1184
|
})
|
|
1071
1185
|
|
|
1072
1186
|
const fullscreenContentWidth = Math.max(24, contentWidth - 2)
|
|
1073
|
-
const fullscreenBodyLines = Math.max(8,
|
|
1187
|
+
const fullscreenBodyLines = Math.max(8, terminalHeight - 8)
|
|
1074
1188
|
const wideFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
1075
1189
|
pullRequest: selectedPullRequest,
|
|
1076
1190
|
contentWidth: fullscreenContentWidth,
|
|
@@ -1101,17 +1215,21 @@ export const App = () => {
|
|
|
1101
1215
|
|
|
1102
1216
|
const longestLabelName = labelModal.availableLabels.reduce((max, label) => Math.max(max, label.name.length), 0)
|
|
1103
1217
|
const labelModalWidth = Math.min(Math.max(42, longestLabelName + 16), 56, contentWidth - 4)
|
|
1104
|
-
const labelModalHeight = Math.min(20,
|
|
1218
|
+
const labelModalHeight = Math.min(20, terminalHeight - 4)
|
|
1105
1219
|
const labelModalLeft = Math.floor((contentWidth - labelModalWidth) / 2)
|
|
1106
|
-
const labelModalTop = Math.floor((
|
|
1220
|
+
const labelModalTop = Math.floor((terminalHeight - labelModalHeight) / 2)
|
|
1107
1221
|
const mergeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
|
|
1108
|
-
const mergeModalHeight = Math.min(16,
|
|
1222
|
+
const mergeModalHeight = Math.min(16, terminalHeight - 4)
|
|
1109
1223
|
const mergeModalLeft = Math.floor((contentWidth - mergeModalWidth) / 2)
|
|
1110
|
-
const mergeModalTop = Math.floor((
|
|
1224
|
+
const mergeModalTop = Math.floor((terminalHeight - mergeModalHeight) / 2)
|
|
1225
|
+
const themeModalWidth = Math.min(58, Math.max(38, contentWidth - 12))
|
|
1226
|
+
const themeModalHeight = Math.min(16, terminalHeight - 4)
|
|
1227
|
+
const themeModalLeft = Math.floor((contentWidth - themeModalWidth) / 2)
|
|
1228
|
+
const themeModalTop = Math.floor((terminalHeight - themeModalHeight) / 2)
|
|
1111
1229
|
|
|
1112
1230
|
return (
|
|
1113
|
-
<box
|
|
1114
|
-
<box paddingLeft={1} paddingRight={1} flexDirection="column">
|
|
1231
|
+
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
1232
|
+
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.panel}>
|
|
1115
1233
|
<PlainLine text={headerLine} fg={colors.muted} bold />
|
|
1116
1234
|
</box>
|
|
1117
1235
|
{isWideLayout && !detailFullView && !diffFullView && !isInitialLoading ? (
|
|
@@ -1132,6 +1250,7 @@ export const App = () => {
|
|
|
1132
1250
|
height={wideBodyHeight}
|
|
1133
1251
|
loadingIndicator={loadingIndicator}
|
|
1134
1252
|
scrollRef={diffScrollRef}
|
|
1253
|
+
themeId={themeId}
|
|
1135
1254
|
/>
|
|
1136
1255
|
) : isWideLayout && detailFullView ? (
|
|
1137
1256
|
<box flexGrow={1} flexDirection="column">
|
|
@@ -1182,7 +1301,7 @@ export const App = () => {
|
|
|
1182
1301
|
</scrollbox>
|
|
1183
1302
|
</box>
|
|
1184
1303
|
) : (
|
|
1185
|
-
|
|
1304
|
+
<box height={wideBodyHeight} flexDirection="column">
|
|
1186
1305
|
<DetailsPane pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} />
|
|
1187
1306
|
<Divider width={contentWidth} />
|
|
1188
1307
|
<box flexGrow={1} flexDirection="column">
|
|
@@ -1192,7 +1311,7 @@ export const App = () => {
|
|
|
1192
1311
|
</box>
|
|
1193
1312
|
</scrollbox>
|
|
1194
1313
|
</box>
|
|
1195
|
-
|
|
1314
|
+
</box>
|
|
1196
1315
|
)}
|
|
1197
1316
|
|
|
1198
1317
|
{isWideLayout && !detailFullView && !diffFullView && !isInitialLoading ? (
|
|
@@ -1200,7 +1319,7 @@ export const App = () => {
|
|
|
1200
1319
|
) : (
|
|
1201
1320
|
<Divider width={contentWidth} />
|
|
1202
1321
|
)}
|
|
1203
|
-
<box paddingLeft={1} paddingRight={1}>
|
|
1322
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={colors.footer}>
|
|
1204
1323
|
{footerNotice ? (
|
|
1205
1324
|
<PlainLine text={footerNotice} fg={colors.count} />
|
|
1206
1325
|
) : (
|
|
@@ -1238,6 +1357,16 @@ export const App = () => {
|
|
|
1238
1357
|
loadingIndicator={loadingIndicator}
|
|
1239
1358
|
/>
|
|
1240
1359
|
) : null}
|
|
1360
|
+
{themeModal.open ? (
|
|
1361
|
+
<ThemeModal
|
|
1362
|
+
state={themeModal}
|
|
1363
|
+
activeThemeId={themeId}
|
|
1364
|
+
modalWidth={themeModalWidth}
|
|
1365
|
+
modalHeight={themeModalHeight}
|
|
1366
|
+
offsetLeft={themeModalLeft}
|
|
1367
|
+
offsetTop={themeModalTop}
|
|
1368
|
+
/>
|
|
1369
|
+
) : null}
|
|
1241
1370
|
</box>
|
|
1242
1371
|
)
|
|
1243
1372
|
}
|
package/src/ui/FooterHints.tsx
CHANGED
|
@@ -86,6 +86,8 @@ export const FooterHints = ({
|
|
|
86
86
|
<TextLine>
|
|
87
87
|
<span fg={colors.count}>/</span>
|
|
88
88
|
<span fg={colors.muted}> filter </span>
|
|
89
|
+
<span fg={colors.count}>t</span>
|
|
90
|
+
<span fg={colors.muted}> theme </span>
|
|
89
91
|
{showFilterClear ? (
|
|
90
92
|
<>
|
|
91
93
|
<span fg={colors.count}>esc</span>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { useMemo, type Ref } from "react"
|
|
3
3
|
import type { PullRequestItem } from "../domain.js"
|
|
4
|
-
import { colors } from "./colors.js"
|
|
5
|
-
import {
|
|
4
|
+
import { colors, type ThemeId } from "./colors.js"
|
|
5
|
+
import { createDiffSyntaxStyle, diffStatText, patchRenderableLineCount, type PullRequestDiffState } from "./diff.js"
|
|
6
6
|
import { LoadingPane, StatusCard } from "./DetailsPane.js"
|
|
7
7
|
import { Divider, fitCell, PlainLine, TextLine } from "./primitives.js"
|
|
8
8
|
import { shortRepoName } from "./pullRequests.js"
|
|
@@ -37,6 +37,7 @@ export const PullRequestDiffPane = ({
|
|
|
37
37
|
height,
|
|
38
38
|
loadingIndicator,
|
|
39
39
|
scrollRef,
|
|
40
|
+
themeId,
|
|
40
41
|
}: {
|
|
41
42
|
pullRequest: PullRequestItem | null
|
|
42
43
|
diffState: PullRequestDiffState | undefined
|
|
@@ -47,6 +48,7 @@ export const PullRequestDiffPane = ({
|
|
|
47
48
|
height: number
|
|
48
49
|
loadingIndicator: string
|
|
49
50
|
scrollRef: Ref<ScrollBoxRenderable>
|
|
51
|
+
themeId: ThemeId
|
|
50
52
|
}) => {
|
|
51
53
|
const readyFiles = diffState?.status === "ready" ? diffState.files : []
|
|
52
54
|
const safeIndex = readyFiles.length > 0 ? Math.max(0, Math.min(fileIndex, readyFiles.length - 1)) : 0
|
|
@@ -55,6 +57,7 @@ export const PullRequestDiffPane = ({
|
|
|
55
57
|
() => file ? patchRenderableLineCount(file.patch, view, wrapMode, paneWidth) : 1,
|
|
56
58
|
[file?.patch, view, wrapMode, paneWidth],
|
|
57
59
|
)
|
|
60
|
+
const syntaxStyle = useMemo(() => createDiffSyntaxStyle(), [themeId])
|
|
58
61
|
|
|
59
62
|
if (!pullRequest) {
|
|
60
63
|
return <LoadingPane content={{ title: "No pull request selected", hint: "Press esc to go back" }} width={paneWidth} height={height} />
|
|
@@ -125,18 +128,18 @@ export const PullRequestDiffPane = ({
|
|
|
125
128
|
view={view}
|
|
126
129
|
syncScroll
|
|
127
130
|
filetype={file.filetype ?? "text"}
|
|
128
|
-
syntaxStyle={
|
|
131
|
+
syntaxStyle={syntaxStyle}
|
|
129
132
|
showLineNumbers
|
|
130
133
|
wrapMode={wrapMode}
|
|
131
|
-
addedBg=
|
|
132
|
-
removedBg=
|
|
133
|
-
contextBg=
|
|
134
|
+
addedBg={colors.diff.addedBg}
|
|
135
|
+
removedBg={colors.diff.removedBg}
|
|
136
|
+
contextBg={colors.diff.contextBg}
|
|
134
137
|
addedSignColor={colors.status.passing}
|
|
135
138
|
removedSignColor={colors.status.failing}
|
|
136
139
|
lineNumberFg={colors.muted}
|
|
137
|
-
lineNumberBg=
|
|
138
|
-
addedLineNumberBg=
|
|
139
|
-
removedLineNumberBg=
|
|
140
|
+
lineNumberBg={colors.diff.lineNumberBg}
|
|
141
|
+
addedLineNumberBg={colors.diff.addedLineNumberBg}
|
|
142
|
+
removedLineNumberBg={colors.diff.removedLineNumberBg}
|
|
140
143
|
selectionBg={colors.selectedBg}
|
|
141
144
|
selectionFg={colors.selectedText}
|
|
142
145
|
height={diffHeight}
|
package/src/ui/colors.ts
CHANGED
|
@@ -1,4 +1,58 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ThemeId = "ghui" | "tokyo-night" | "catppuccin" | "rose-pine" | "gruvbox" | "nord" | "dracula" | "opencode"
|
|
2
|
+
|
|
3
|
+
export interface ColorPalette {
|
|
4
|
+
readonly background: string
|
|
5
|
+
readonly panel: string
|
|
6
|
+
readonly footer: string
|
|
7
|
+
readonly modalBackground: string
|
|
8
|
+
readonly text: string
|
|
9
|
+
readonly muted: string
|
|
10
|
+
readonly separator: string
|
|
11
|
+
readonly accent: string
|
|
12
|
+
readonly inlineCode: string
|
|
13
|
+
readonly error: string
|
|
14
|
+
readonly selectedBg: string
|
|
15
|
+
readonly selectedText: string
|
|
16
|
+
readonly count: string
|
|
17
|
+
readonly status: {
|
|
18
|
+
readonly draft: string
|
|
19
|
+
readonly approved: string
|
|
20
|
+
readonly changes: string
|
|
21
|
+
readonly review: string
|
|
22
|
+
readonly none: string
|
|
23
|
+
readonly passing: string
|
|
24
|
+
readonly pending: string
|
|
25
|
+
readonly failing: string
|
|
26
|
+
}
|
|
27
|
+
readonly repos: {
|
|
28
|
+
readonly opencode: string
|
|
29
|
+
readonly "effect-smol": string
|
|
30
|
+
readonly "opencode-console": string
|
|
31
|
+
readonly opencontrol: string
|
|
32
|
+
readonly default: string
|
|
33
|
+
}
|
|
34
|
+
readonly diff: {
|
|
35
|
+
readonly addedBg: string
|
|
36
|
+
readonly removedBg: string
|
|
37
|
+
readonly contextBg: string
|
|
38
|
+
readonly lineNumberBg: string
|
|
39
|
+
readonly addedLineNumberBg: string
|
|
40
|
+
readonly removedLineNumberBg: string
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ThemeDefinition {
|
|
45
|
+
readonly id: ThemeId
|
|
46
|
+
readonly name: string
|
|
47
|
+
readonly description: string
|
|
48
|
+
readonly colors: ColorPalette
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ghuiColors: ColorPalette = {
|
|
52
|
+
background: "#111018",
|
|
53
|
+
panel: "#161923",
|
|
54
|
+
footer: "#1d2430",
|
|
55
|
+
modalBackground: "#1a1a2e",
|
|
2
56
|
text: "#ede7da",
|
|
3
57
|
muted: "#9f9788",
|
|
4
58
|
separator: "#6f685d",
|
|
@@ -25,4 +79,322 @@ export const colors = {
|
|
|
25
79
|
opencontrol: "#f59e0b",
|
|
26
80
|
default: "#93c5fd",
|
|
27
81
|
},
|
|
28
|
-
|
|
82
|
+
diff: {
|
|
83
|
+
addedBg: "#17351f",
|
|
84
|
+
removedBg: "#3a1e22",
|
|
85
|
+
contextBg: "transparent",
|
|
86
|
+
lineNumberBg: "#151515",
|
|
87
|
+
addedLineNumberBg: "#12301a",
|
|
88
|
+
removedLineNumberBg: "#35171b",
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tokyoNightColors: ColorPalette = {
|
|
93
|
+
background: "#1a1b26",
|
|
94
|
+
panel: "#16161e",
|
|
95
|
+
footer: "#24283b",
|
|
96
|
+
modalBackground: "#24283b",
|
|
97
|
+
text: "#c0caf5",
|
|
98
|
+
muted: "#787c99",
|
|
99
|
+
separator: "#3b4261",
|
|
100
|
+
accent: "#7aa2f7",
|
|
101
|
+
inlineCode: "#bb9af7",
|
|
102
|
+
error: "#f7768e",
|
|
103
|
+
selectedBg: "#283457",
|
|
104
|
+
selectedText: "#ffffff",
|
|
105
|
+
count: "#ff9e64",
|
|
106
|
+
status: {
|
|
107
|
+
draft: "#e0af68",
|
|
108
|
+
approved: "#9ece6a",
|
|
109
|
+
changes: "#f7768e",
|
|
110
|
+
review: "#7dcfff",
|
|
111
|
+
none: "#787c99",
|
|
112
|
+
passing: "#9ece6a",
|
|
113
|
+
pending: "#e0af68",
|
|
114
|
+
failing: "#f7768e",
|
|
115
|
+
},
|
|
116
|
+
repos: {
|
|
117
|
+
opencode: "#7aa2f7",
|
|
118
|
+
"effect-smol": "#9ece6a",
|
|
119
|
+
"opencode-console": "#bb9af7",
|
|
120
|
+
opencontrol: "#ff9e64",
|
|
121
|
+
default: "#7dcfff",
|
|
122
|
+
},
|
|
123
|
+
diff: {
|
|
124
|
+
addedBg: "#203326",
|
|
125
|
+
removedBg: "#3a222c",
|
|
126
|
+
contextBg: "transparent",
|
|
127
|
+
lineNumberBg: "#16161e",
|
|
128
|
+
addedLineNumberBg: "#1b2f23",
|
|
129
|
+
removedLineNumberBg: "#33202a",
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const opencodeColors: ColorPalette = {
|
|
134
|
+
background: "#0a0a0a",
|
|
135
|
+
panel: "#141414",
|
|
136
|
+
footer: "#1e1e1e",
|
|
137
|
+
modalBackground: "#1e1e1e",
|
|
138
|
+
text: "#eeeeee",
|
|
139
|
+
muted: "#808080",
|
|
140
|
+
separator: "#484848",
|
|
141
|
+
accent: "#fab283",
|
|
142
|
+
inlineCode: "#7fd88f",
|
|
143
|
+
error: "#e06c75",
|
|
144
|
+
selectedBg: "#323232",
|
|
145
|
+
selectedText: "#eeeeee",
|
|
146
|
+
count: "#fab283",
|
|
147
|
+
status: {
|
|
148
|
+
draft: "#f5a742",
|
|
149
|
+
approved: "#7fd88f",
|
|
150
|
+
changes: "#e06c75",
|
|
151
|
+
review: "#5c9cf5",
|
|
152
|
+
none: "#808080",
|
|
153
|
+
passing: "#7fd88f",
|
|
154
|
+
pending: "#f5a742",
|
|
155
|
+
failing: "#e06c75",
|
|
156
|
+
},
|
|
157
|
+
repos: {
|
|
158
|
+
opencode: "#fab283",
|
|
159
|
+
"effect-smol": "#7fd88f",
|
|
160
|
+
"opencode-console": "#9d7cd8",
|
|
161
|
+
opencontrol: "#f5a742",
|
|
162
|
+
default: "#5c9cf5",
|
|
163
|
+
},
|
|
164
|
+
diff: {
|
|
165
|
+
addedBg: "#20303b",
|
|
166
|
+
removedBg: "#37222c",
|
|
167
|
+
contextBg: "transparent",
|
|
168
|
+
lineNumberBg: "#141414",
|
|
169
|
+
addedLineNumberBg: "#1b2b34",
|
|
170
|
+
removedLineNumberBg: "#2d1f26",
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const catppuccinColors: ColorPalette = {
|
|
175
|
+
background: "#1e1e2e",
|
|
176
|
+
panel: "#181825",
|
|
177
|
+
footer: "#313244",
|
|
178
|
+
modalBackground: "#313244",
|
|
179
|
+
text: "#cdd6f4",
|
|
180
|
+
muted: "#7f849c",
|
|
181
|
+
separator: "#45475a",
|
|
182
|
+
accent: "#cba6f7",
|
|
183
|
+
inlineCode: "#f5c2e7",
|
|
184
|
+
error: "#f38ba8",
|
|
185
|
+
selectedBg: "#45475a",
|
|
186
|
+
selectedText: "#f5e0dc",
|
|
187
|
+
count: "#fab387",
|
|
188
|
+
status: {
|
|
189
|
+
draft: "#f9e2af",
|
|
190
|
+
approved: "#a6e3a1",
|
|
191
|
+
changes: "#f38ba8",
|
|
192
|
+
review: "#89b4fa",
|
|
193
|
+
none: "#7f849c",
|
|
194
|
+
passing: "#a6e3a1",
|
|
195
|
+
pending: "#f9e2af",
|
|
196
|
+
failing: "#f38ba8",
|
|
197
|
+
},
|
|
198
|
+
repos: {
|
|
199
|
+
opencode: "#89b4fa",
|
|
200
|
+
"effect-smol": "#a6e3a1",
|
|
201
|
+
"opencode-console": "#f5c2e7",
|
|
202
|
+
opencontrol: "#fab387",
|
|
203
|
+
default: "#74c7ec",
|
|
204
|
+
},
|
|
205
|
+
diff: {
|
|
206
|
+
addedBg: "#243927",
|
|
207
|
+
removedBg: "#3b2532",
|
|
208
|
+
contextBg: "transparent",
|
|
209
|
+
lineNumberBg: "#181825",
|
|
210
|
+
addedLineNumberBg: "#203524",
|
|
211
|
+
removedLineNumberBg: "#36232f",
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const rosePineColors: ColorPalette = {
|
|
216
|
+
background: "#191724",
|
|
217
|
+
panel: "#1f1d2e",
|
|
218
|
+
footer: "#26233a",
|
|
219
|
+
modalBackground: "#26233a",
|
|
220
|
+
text: "#e0def4",
|
|
221
|
+
muted: "#908caa",
|
|
222
|
+
separator: "#524f67",
|
|
223
|
+
accent: "#c4a7e7",
|
|
224
|
+
inlineCode: "#f6c177",
|
|
225
|
+
error: "#eb6f92",
|
|
226
|
+
selectedBg: "#403d52",
|
|
227
|
+
selectedText: "#f6f1ff",
|
|
228
|
+
count: "#ebbcba",
|
|
229
|
+
status: {
|
|
230
|
+
draft: "#f6c177",
|
|
231
|
+
approved: "#9ccfd8",
|
|
232
|
+
changes: "#eb6f92",
|
|
233
|
+
review: "#31748f",
|
|
234
|
+
none: "#908caa",
|
|
235
|
+
passing: "#9ccfd8",
|
|
236
|
+
pending: "#f6c177",
|
|
237
|
+
failing: "#eb6f92",
|
|
238
|
+
},
|
|
239
|
+
repos: {
|
|
240
|
+
opencode: "#31748f",
|
|
241
|
+
"effect-smol": "#9ccfd8",
|
|
242
|
+
"opencode-console": "#c4a7e7",
|
|
243
|
+
opencontrol: "#f6c177",
|
|
244
|
+
default: "#ebbcba",
|
|
245
|
+
},
|
|
246
|
+
diff: {
|
|
247
|
+
addedBg: "#23343a",
|
|
248
|
+
removedBg: "#3a2534",
|
|
249
|
+
contextBg: "transparent",
|
|
250
|
+
lineNumberBg: "#1f1d2e",
|
|
251
|
+
addedLineNumberBg: "#203137",
|
|
252
|
+
removedLineNumberBg: "#352330",
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const gruvboxColors: ColorPalette = {
|
|
257
|
+
background: "#282828",
|
|
258
|
+
panel: "#1d2021",
|
|
259
|
+
footer: "#3c3836",
|
|
260
|
+
modalBackground: "#3c3836",
|
|
261
|
+
text: "#ebdbb2",
|
|
262
|
+
muted: "#928374",
|
|
263
|
+
separator: "#665c54",
|
|
264
|
+
accent: "#fabd2f",
|
|
265
|
+
inlineCode: "#d3869b",
|
|
266
|
+
error: "#fb4934",
|
|
267
|
+
selectedBg: "#504945",
|
|
268
|
+
selectedText: "#fbf1c7",
|
|
269
|
+
count: "#fe8019",
|
|
270
|
+
status: {
|
|
271
|
+
draft: "#fabd2f",
|
|
272
|
+
approved: "#b8bb26",
|
|
273
|
+
changes: "#fb4934",
|
|
274
|
+
review: "#83a598",
|
|
275
|
+
none: "#928374",
|
|
276
|
+
passing: "#b8bb26",
|
|
277
|
+
pending: "#fabd2f",
|
|
278
|
+
failing: "#fb4934",
|
|
279
|
+
},
|
|
280
|
+
repos: {
|
|
281
|
+
opencode: "#83a598",
|
|
282
|
+
"effect-smol": "#b8bb26",
|
|
283
|
+
"opencode-console": "#d3869b",
|
|
284
|
+
opencontrol: "#fe8019",
|
|
285
|
+
default: "#8ec07c",
|
|
286
|
+
},
|
|
287
|
+
diff: {
|
|
288
|
+
addedBg: "#32361f",
|
|
289
|
+
removedBg: "#3c2927",
|
|
290
|
+
contextBg: "transparent",
|
|
291
|
+
lineNumberBg: "#1d2021",
|
|
292
|
+
addedLineNumberBg: "#2f331e",
|
|
293
|
+
removedLineNumberBg: "#382726",
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const nordColors: ColorPalette = {
|
|
298
|
+
background: "#2e3440",
|
|
299
|
+
panel: "#242933",
|
|
300
|
+
footer: "#3b4252",
|
|
301
|
+
modalBackground: "#3b4252",
|
|
302
|
+
text: "#eceff4",
|
|
303
|
+
muted: "#8892a7",
|
|
304
|
+
separator: "#4c566a",
|
|
305
|
+
accent: "#88c0d0",
|
|
306
|
+
inlineCode: "#b48ead",
|
|
307
|
+
error: "#bf616a",
|
|
308
|
+
selectedBg: "#434c5e",
|
|
309
|
+
selectedText: "#eceff4",
|
|
310
|
+
count: "#ebcb8b",
|
|
311
|
+
status: {
|
|
312
|
+
draft: "#ebcb8b",
|
|
313
|
+
approved: "#a3be8c",
|
|
314
|
+
changes: "#bf616a",
|
|
315
|
+
review: "#81a1c1",
|
|
316
|
+
none: "#8892a7",
|
|
317
|
+
passing: "#a3be8c",
|
|
318
|
+
pending: "#ebcb8b",
|
|
319
|
+
failing: "#bf616a",
|
|
320
|
+
},
|
|
321
|
+
repos: {
|
|
322
|
+
opencode: "#81a1c1",
|
|
323
|
+
"effect-smol": "#a3be8c",
|
|
324
|
+
"opencode-console": "#b48ead",
|
|
325
|
+
opencontrol: "#d08770",
|
|
326
|
+
default: "#88c0d0",
|
|
327
|
+
},
|
|
328
|
+
diff: {
|
|
329
|
+
addedBg: "#334033",
|
|
330
|
+
removedBg: "#433238",
|
|
331
|
+
contextBg: "transparent",
|
|
332
|
+
lineNumberBg: "#242933",
|
|
333
|
+
addedLineNumberBg: "#303d31",
|
|
334
|
+
removedLineNumberBg: "#3f3036",
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const draculaColors: ColorPalette = {
|
|
339
|
+
background: "#282a36",
|
|
340
|
+
panel: "#21222c",
|
|
341
|
+
footer: "#343746",
|
|
342
|
+
modalBackground: "#343746",
|
|
343
|
+
text: "#f8f8f2",
|
|
344
|
+
muted: "#8f94b8",
|
|
345
|
+
separator: "#4f5268",
|
|
346
|
+
accent: "#bd93f9",
|
|
347
|
+
inlineCode: "#ff79c6",
|
|
348
|
+
error: "#ff5555",
|
|
349
|
+
selectedBg: "#44475a",
|
|
350
|
+
selectedText: "#f8f8f2",
|
|
351
|
+
count: "#ffb86c",
|
|
352
|
+
status: {
|
|
353
|
+
draft: "#f1fa8c",
|
|
354
|
+
approved: "#50fa7b",
|
|
355
|
+
changes: "#ff5555",
|
|
356
|
+
review: "#8be9fd",
|
|
357
|
+
none: "#8f94b8",
|
|
358
|
+
passing: "#50fa7b",
|
|
359
|
+
pending: "#f1fa8c",
|
|
360
|
+
failing: "#ff5555",
|
|
361
|
+
},
|
|
362
|
+
repos: {
|
|
363
|
+
opencode: "#8be9fd",
|
|
364
|
+
"effect-smol": "#50fa7b",
|
|
365
|
+
"opencode-console": "#ff79c6",
|
|
366
|
+
opencontrol: "#ffb86c",
|
|
367
|
+
default: "#bd93f9",
|
|
368
|
+
},
|
|
369
|
+
diff: {
|
|
370
|
+
addedBg: "#203a29",
|
|
371
|
+
removedBg: "#43272f",
|
|
372
|
+
contextBg: "transparent",
|
|
373
|
+
lineNumberBg: "#21222c",
|
|
374
|
+
addedLineNumberBg: "#1d3627",
|
|
375
|
+
removedLineNumberBg: "#3d252c",
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const themeDefinitions: readonly ThemeDefinition[] = [
|
|
380
|
+
{ id: "ghui", name: "GHUI", description: "Warm parchment accents on a deep slate background", colors: ghuiColors },
|
|
381
|
+
{ id: "tokyo-night", name: "Tokyo Night", description: "Cool indigo surfaces with neon editor accents", colors: tokyoNightColors },
|
|
382
|
+
{ id: "catppuccin", name: "Catppuccin", description: "Mocha lavender, peach, and soft pastel contrast", colors: catppuccinColors },
|
|
383
|
+
{ id: "rose-pine", name: "Rose Pine", description: "Muted rose, pine, and gold on dusky violet", colors: rosePineColors },
|
|
384
|
+
{ id: "gruvbox", name: "Gruvbox", description: "Retro warm earth tones with punchy semantic accents", colors: gruvboxColors },
|
|
385
|
+
{ id: "nord", name: "Nord", description: "Arctic blue-gray surfaces with frosty accents", colors: nordColors },
|
|
386
|
+
{ id: "dracula", name: "Dracula", description: "High-contrast purple, pink, cyan, and green", colors: draculaColors },
|
|
387
|
+
{ id: "opencode", name: "OpenCode", description: "Charcoal panels with peach, violet, and blue highlights", colors: opencodeColors },
|
|
388
|
+
] as const
|
|
389
|
+
|
|
390
|
+
let activeTheme = themeDefinitions[0]!
|
|
391
|
+
|
|
392
|
+
export const colors: ColorPalette = { ...ghuiColors }
|
|
393
|
+
|
|
394
|
+
export const getThemeDefinition = (id: ThemeId) => themeDefinitions.find((theme) => theme.id === id) ?? themeDefinitions[0]!
|
|
395
|
+
|
|
396
|
+
export const setActiveTheme = (id: ThemeId) => {
|
|
397
|
+
if (activeTheme.id === id) return
|
|
398
|
+
activeTheme = getThemeDefinition(id)
|
|
399
|
+
Object.assign(colors, activeTheme.colors)
|
|
400
|
+
}
|
package/src/ui/diff.ts
CHANGED
|
@@ -13,21 +13,21 @@ export type PullRequestDiffState =
|
|
|
13
13
|
| { readonly status: "ready"; readonly patch: string; readonly files: readonly DiffFilePatch[] }
|
|
14
14
|
| { readonly status: "error"; readonly error: string }
|
|
15
15
|
|
|
16
|
-
export const
|
|
17
|
-
keyword: { fg: parseColor(
|
|
18
|
-
"keyword.import": { fg: parseColor(
|
|
19
|
-
string: { fg: parseColor(
|
|
16
|
+
export const createDiffSyntaxStyle = () => SyntaxStyle.fromStyles({
|
|
17
|
+
keyword: { fg: parseColor(colors.accent), bold: true },
|
|
18
|
+
"keyword.import": { fg: parseColor(colors.accent), bold: true },
|
|
19
|
+
string: { fg: parseColor(colors.inlineCode) },
|
|
20
20
|
comment: { fg: parseColor(colors.muted), italic: true },
|
|
21
|
-
number: { fg: parseColor(
|
|
22
|
-
boolean: { fg: parseColor(
|
|
23
|
-
constant: { fg: parseColor(
|
|
24
|
-
function: { fg: parseColor(
|
|
25
|
-
"function.call": { fg: parseColor(
|
|
26
|
-
constructor: { fg: parseColor(
|
|
27
|
-
type: { fg: parseColor(
|
|
28
|
-
operator: { fg: parseColor(
|
|
21
|
+
number: { fg: parseColor(colors.status.review) },
|
|
22
|
+
boolean: { fg: parseColor(colors.status.review) },
|
|
23
|
+
constant: { fg: parseColor(colors.status.review) },
|
|
24
|
+
function: { fg: parseColor(colors.status.passing) },
|
|
25
|
+
"function.call": { fg: parseColor(colors.status.passing) },
|
|
26
|
+
constructor: { fg: parseColor(colors.status.draft) },
|
|
27
|
+
type: { fg: parseColor(colors.status.draft) },
|
|
28
|
+
operator: { fg: parseColor(colors.status.failing) },
|
|
29
29
|
variable: { fg: parseColor(colors.text) },
|
|
30
|
-
property: { fg: parseColor(
|
|
30
|
+
property: { fg: parseColor(colors.status.review) },
|
|
31
31
|
bracket: { fg: parseColor(colors.text) },
|
|
32
32
|
punctuation: { fg: parseColor(colors.text) },
|
|
33
33
|
default: { fg: parseColor(colors.text) },
|
package/src/ui/modals.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TextAttributes } from "@opentui/core"
|
|
2
2
|
import type { PullRequestLabel, PullRequestMergeInfo } from "../domain.js"
|
|
3
3
|
import { availableMergeActions } from "../mergeActions.js"
|
|
4
|
-
import { colors } from "./colors.js"
|
|
4
|
+
import { colors, themeDefinitions, type ThemeId } from "./colors.js"
|
|
5
5
|
import { centerCell, Divider, fitCell, ModalFrame, PlainLine, TextLine } from "./primitives.js"
|
|
6
6
|
import { labelColor, shortRepoName } from "./pullRequests.js"
|
|
7
7
|
|
|
@@ -25,6 +25,11 @@ export interface MergeModalState {
|
|
|
25
25
|
readonly error: string | null
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface ThemeModalState {
|
|
29
|
+
readonly open: boolean
|
|
30
|
+
readonly initialThemeId: ThemeId
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export const initialLabelModalState: LabelModalState = {
|
|
29
34
|
open: false,
|
|
30
35
|
repository: null,
|
|
@@ -45,6 +50,11 @@ export const initialMergeModalState: MergeModalState = {
|
|
|
45
50
|
error: null,
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
export const initialThemeModalState: ThemeModalState = {
|
|
54
|
+
open: false,
|
|
55
|
+
initialThemeId: "ghui",
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
const mergeUnavailableReason = (info: PullRequestMergeInfo | null) => {
|
|
49
59
|
if (!info) return "Loading merge status from GitHub."
|
|
50
60
|
if (info.state !== "open") return "This pull request is not open."
|
|
@@ -247,3 +257,86 @@ export const MergeModal = ({
|
|
|
247
257
|
</ModalFrame>
|
|
248
258
|
)
|
|
249
259
|
}
|
|
260
|
+
|
|
261
|
+
export const ThemeModal = ({
|
|
262
|
+
state,
|
|
263
|
+
activeThemeId,
|
|
264
|
+
modalWidth,
|
|
265
|
+
modalHeight,
|
|
266
|
+
offsetLeft,
|
|
267
|
+
offsetTop,
|
|
268
|
+
}: {
|
|
269
|
+
state: ThemeModalState
|
|
270
|
+
activeThemeId: ThemeId
|
|
271
|
+
modalWidth: number
|
|
272
|
+
modalHeight: number
|
|
273
|
+
offsetLeft: number
|
|
274
|
+
offsetTop: number
|
|
275
|
+
}) => {
|
|
276
|
+
const innerWidth = Math.max(16, modalWidth - 2)
|
|
277
|
+
const contentWidth = Math.max(14, innerWidth - 2)
|
|
278
|
+
const rowWidth = innerWidth
|
|
279
|
+
const maxVisible = Math.max(1, modalHeight - 7)
|
|
280
|
+
const activeIndex = themeDefinitions.findIndex((theme) => theme.id === activeThemeId)
|
|
281
|
+
const selectedIndex = Math.max(0, activeIndex)
|
|
282
|
+
const selectedTheme = themeDefinitions[selectedIndex]!
|
|
283
|
+
const scrollStart = Math.min(
|
|
284
|
+
Math.max(0, themeDefinitions.length - maxVisible),
|
|
285
|
+
Math.max(0, selectedIndex - maxVisible + 1),
|
|
286
|
+
)
|
|
287
|
+
const visibleThemes = themeDefinitions.slice(scrollStart, scrollStart + maxVisible)
|
|
288
|
+
const countText = `${selectedIndex + 1}/${themeDefinitions.length}`
|
|
289
|
+
const title = "Themes"
|
|
290
|
+
const headerGap = Math.max(1, contentWidth - title.length - countText.length)
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[2, modalHeight - 4]}>
|
|
294
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
295
|
+
<TextLine>
|
|
296
|
+
<span fg={colors.accent} attributes={TextAttributes.BOLD}>{title}</span>
|
|
297
|
+
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
298
|
+
<span fg={colors.muted}>{countText}</span>
|
|
299
|
+
</TextLine>
|
|
300
|
+
</box>
|
|
301
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
302
|
+
<PlainLine text={fitCell(selectedTheme.description, contentWidth)} fg={colors.muted} />
|
|
303
|
+
</box>
|
|
304
|
+
<Divider width={innerWidth} />
|
|
305
|
+
<box height={maxVisible} flexDirection="column">
|
|
306
|
+
{visibleThemes.map((theme, index) => {
|
|
307
|
+
const actualIndex = scrollStart + index
|
|
308
|
+
const isSelected = actualIndex === selectedIndex
|
|
309
|
+
const isActive = theme.id === activeThemeId
|
|
310
|
+
const marker = isActive ? "✓" : " "
|
|
311
|
+
const swatchWidth = 6
|
|
312
|
+
const nameWidth = Math.max(1, rowWidth - swatchWidth - 3)
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<TextLine key={theme.id} bg={isSelected ? colors.selectedBg : undefined} fg={isSelected ? colors.selectedText : colors.text}>
|
|
316
|
+
<span fg={isActive ? colors.status.passing : colors.muted}>{marker}</span>
|
|
317
|
+
<span> </span>
|
|
318
|
+
<span>{fitCell(theme.name, nameWidth)}</span>
|
|
319
|
+
<span bg={theme.colors.background}> </span>
|
|
320
|
+
<span bg={theme.colors.panel}> </span>
|
|
321
|
+
<span bg={theme.colors.accent}> </span>
|
|
322
|
+
<span bg={theme.colors.status.passing}> </span>
|
|
323
|
+
<span bg={theme.colors.status.failing}> </span>
|
|
324
|
+
<span bg={theme.colors.status.review}> </span>
|
|
325
|
+
</TextLine>
|
|
326
|
+
)
|
|
327
|
+
})}
|
|
328
|
+
</box>
|
|
329
|
+
<Divider width={innerWidth} />
|
|
330
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
331
|
+
<TextLine>
|
|
332
|
+
<span fg={colors.count}>↑↓</span>
|
|
333
|
+
<span fg={colors.muted}> preview </span>
|
|
334
|
+
<span fg={colors.count}>enter</span>
|
|
335
|
+
<span fg={colors.muted}> select </span>
|
|
336
|
+
<span fg={colors.count}>esc</span>
|
|
337
|
+
<span fg={colors.muted}> cancel</span>
|
|
338
|
+
</TextLine>
|
|
339
|
+
</box>
|
|
340
|
+
</ModalFrame>
|
|
341
|
+
)
|
|
342
|
+
}
|