@kitlangton/ghui 0.1.13 → 0.1.15
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 +72 -13
- package/src/ui/DetailsPane.tsx +7 -3
- package/src/ui/colors.ts +224 -19
- package/src/ui/modals.tsx +34 -10
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -11,7 +11,7 @@ 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, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
14
|
+
import { colors, filterThemeDefinitions, 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"
|
|
@@ -264,6 +264,10 @@ export const App = () => {
|
|
|
264
264
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
265
265
|
const [themeModal, setThemeModal] = useAtom(themeModalAtom)
|
|
266
266
|
setActiveTheme(themeId)
|
|
267
|
+
const themeIdRef = useRef(themeId)
|
|
268
|
+
const themeModalRef = useRef(themeModal)
|
|
269
|
+
themeIdRef.current = themeId
|
|
270
|
+
themeModalRef.current = themeModal
|
|
267
271
|
const [labelCache, setLabelCache] = useAtom(labelCacheAtom)
|
|
268
272
|
const [pullRequestOverrides, setPullRequestOverrides] = useAtom(pullRequestOverridesAtom)
|
|
269
273
|
const retryProgress = useAtomValue(retryProgressAtom)
|
|
@@ -559,12 +563,14 @@ export const App = () => {
|
|
|
559
563
|
setMergeModal(initialMergeModalState)
|
|
560
564
|
setThemeModal({
|
|
561
565
|
open: true,
|
|
566
|
+
query: "",
|
|
567
|
+
filterMode: false,
|
|
562
568
|
initialThemeId: themeId,
|
|
563
569
|
})
|
|
564
570
|
}
|
|
565
571
|
|
|
566
572
|
const closeThemeModal = (confirm: boolean) => {
|
|
567
|
-
const selectedTheme = themeDefinitions.find((theme) => theme.id ===
|
|
573
|
+
const selectedTheme = themeDefinitions.find((theme) => theme.id === themeIdRef.current)
|
|
568
574
|
if (!confirm) {
|
|
569
575
|
setThemeId(themeModal.initialThemeId)
|
|
570
576
|
} else if (selectedTheme) {
|
|
@@ -573,12 +579,42 @@ export const App = () => {
|
|
|
573
579
|
setThemeModal(initialThemeModalState)
|
|
574
580
|
}
|
|
575
581
|
|
|
582
|
+
const previewTheme = (id: ThemeId) => {
|
|
583
|
+
if (id === themeIdRef.current) return
|
|
584
|
+
themeIdRef.current = id
|
|
585
|
+
setThemeId(id)
|
|
586
|
+
}
|
|
587
|
+
|
|
576
588
|
const moveThemeSelection = (delta: number) => {
|
|
577
|
-
const
|
|
578
|
-
|
|
589
|
+
const filteredThemes = filterThemeDefinitions(themeModalRef.current.query)
|
|
590
|
+
if (filteredThemes.length === 0) return
|
|
591
|
+
const currentIndex = Math.max(0, filteredThemes.findIndex((theme) => theme.id === themeIdRef.current))
|
|
592
|
+
const selectedIndex = Math.max(0, Math.min(filteredThemes.length - 1, currentIndex + delta))
|
|
579
593
|
if (selectedIndex === currentIndex) return
|
|
580
|
-
const theme =
|
|
581
|
-
if (theme
|
|
594
|
+
const theme = filteredThemes[selectedIndex]
|
|
595
|
+
if (theme) previewTheme(theme.id)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const updateThemeQuery = (query: string, options: { readonly previewFirst?: boolean; readonly filterMode?: boolean } = {}) => {
|
|
599
|
+
const current = themeModalRef.current
|
|
600
|
+
const next = {
|
|
601
|
+
...current,
|
|
602
|
+
query,
|
|
603
|
+
filterMode: options.filterMode ?? current.filterMode,
|
|
604
|
+
}
|
|
605
|
+
if (next.query === current.query && next.filterMode === current.filterMode) return
|
|
606
|
+
|
|
607
|
+
themeModalRef.current = next
|
|
608
|
+
setThemeModal(next)
|
|
609
|
+
|
|
610
|
+
if (options.previewFirst && query.trim().length > 0) {
|
|
611
|
+
const firstTheme = filterThemeDefinitions(query)[0]
|
|
612
|
+
if (firstTheme) previewTheme(firstTheme.id)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const editThemeQuery = (transform: (query: string) => string) => {
|
|
617
|
+
updateThemeQuery(transform(themeModalRef.current.query), { previewFirst: true })
|
|
582
618
|
}
|
|
583
619
|
|
|
584
620
|
const openLabelModal = () => {
|
|
@@ -714,7 +750,7 @@ export const App = () => {
|
|
|
714
750
|
}
|
|
715
751
|
|
|
716
752
|
useKeyboard((key) => {
|
|
717
|
-
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
753
|
+
if ((key.name === "q" && !(themeModal.open && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
718
754
|
if (themeModal.open) {
|
|
719
755
|
closeThemeModal(false)
|
|
720
756
|
return
|
|
@@ -733,21 +769,42 @@ export const App = () => {
|
|
|
733
769
|
|
|
734
770
|
if (themeModal.open) {
|
|
735
771
|
if (key.name === "escape") {
|
|
772
|
+
if (themeModal.filterMode) {
|
|
773
|
+
updateThemeQuery("", { filterMode: false })
|
|
774
|
+
return
|
|
775
|
+
}
|
|
736
776
|
closeThemeModal(false)
|
|
737
777
|
return
|
|
738
778
|
}
|
|
779
|
+
if (key.name === "/") {
|
|
780
|
+
updateThemeQuery("", { filterMode: true })
|
|
781
|
+
return
|
|
782
|
+
}
|
|
739
783
|
if (key.name === "return" || key.name === "enter") {
|
|
784
|
+
if (themeModal.filterMode && filterThemeDefinitions(themeModal.query).length === 0) return
|
|
740
785
|
closeThemeModal(true)
|
|
741
786
|
return
|
|
742
787
|
}
|
|
743
|
-
if (key.name === "up" || key.name === "k") {
|
|
788
|
+
if (key.name === "up" || (!themeModal.filterMode && key.name === "k")) {
|
|
744
789
|
moveThemeSelection(-1)
|
|
745
790
|
return
|
|
746
791
|
}
|
|
747
|
-
if (key.name === "down" || key.name === "j") {
|
|
792
|
+
if (key.name === "down" || (!themeModal.filterMode && key.name === "j")) {
|
|
748
793
|
moveThemeSelection(1)
|
|
749
794
|
return
|
|
750
795
|
}
|
|
796
|
+
if (themeModal.filterMode && key.name === "backspace") {
|
|
797
|
+
editThemeQuery((query) => query.slice(0, -1))
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
if (themeModal.filterMode && key.ctrl && key.name === "u") {
|
|
801
|
+
updateThemeQuery("")
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
if (themeModal.filterMode && !key.ctrl && !key.meta && key.sequence.length === 1 && key.name !== "return") {
|
|
805
|
+
editThemeQuery((query) => query + key.sequence)
|
|
806
|
+
return
|
|
807
|
+
}
|
|
751
808
|
return
|
|
752
809
|
}
|
|
753
810
|
|
|
@@ -1229,7 +1286,7 @@ export const App = () => {
|
|
|
1229
1286
|
|
|
1230
1287
|
return (
|
|
1231
1288
|
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
1232
|
-
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.
|
|
1289
|
+
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.background}>
|
|
1233
1290
|
<PlainLine text={headerLine} fg={colors.muted} bold />
|
|
1234
1291
|
</box>
|
|
1235
1292
|
{isWideLayout && !detailFullView && !diffFullView && !isInitialLoading ? (
|
|
@@ -1263,6 +1320,7 @@ export const App = () => {
|
|
|
1263
1320
|
showChecks
|
|
1264
1321
|
placeholderContent={detailPlaceholderContent}
|
|
1265
1322
|
loadingIndicator={loadingIndicator}
|
|
1323
|
+
themeId={themeId}
|
|
1266
1324
|
/>
|
|
1267
1325
|
</scrollbox>
|
|
1268
1326
|
</box>
|
|
@@ -1279,7 +1337,7 @@ export const App = () => {
|
|
|
1279
1337
|
<>
|
|
1280
1338
|
<DetailHeader pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
1281
1339
|
<scrollbox flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
1282
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} />
|
|
1340
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
1283
1341
|
</scrollbox>
|
|
1284
1342
|
</>
|
|
1285
1343
|
) : (
|
|
@@ -1297,12 +1355,13 @@ export const App = () => {
|
|
|
1297
1355
|
paneWidth={contentWidth}
|
|
1298
1356
|
placeholderContent={detailPlaceholderContent}
|
|
1299
1357
|
loadingIndicator={loadingIndicator}
|
|
1358
|
+
themeId={themeId}
|
|
1300
1359
|
/>
|
|
1301
1360
|
</scrollbox>
|
|
1302
1361
|
</box>
|
|
1303
1362
|
) : (
|
|
1304
1363
|
<box height={wideBodyHeight} flexDirection="column">
|
|
1305
|
-
<DetailsPane pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} />
|
|
1364
|
+
<DetailsPane pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
1306
1365
|
<Divider width={contentWidth} />
|
|
1307
1366
|
<box flexGrow={1} flexDirection="column">
|
|
1308
1367
|
<scrollbox flexGrow={1}>
|
|
@@ -1319,7 +1378,7 @@ export const App = () => {
|
|
|
1319
1378
|
) : (
|
|
1320
1379
|
<Divider width={contentWidth} />
|
|
1321
1380
|
)}
|
|
1322
|
-
<box paddingLeft={1} paddingRight={1} backgroundColor={colors.
|
|
1381
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={colors.background}>
|
|
1323
1382
|
{footerNotice ? (
|
|
1324
1383
|
<PlainLine text={footerNotice} fg={colors.count} />
|
|
1325
1384
|
) : (
|
package/src/ui/DetailsPane.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core"
|
|
|
2
2
|
import { Fragment, useMemo } from "react"
|
|
3
3
|
import { formatRelativeDate } from "../date.js"
|
|
4
4
|
import type { CheckItem, PullRequestItem } from "../domain.js"
|
|
5
|
-
import { colors } from "./colors.js"
|
|
5
|
+
import { colors, type ThemeId } from "./colors.js"
|
|
6
6
|
import { diffStatText } from "./diff.js"
|
|
7
7
|
import { centerCell, Divider, fitCell, PlainLine, TextLine } from "./primitives.js"
|
|
8
8
|
import { labelColor, labelTextColor, reviewLabel, shortRepoName, statusColor } from "./pullRequests.js"
|
|
@@ -387,15 +387,17 @@ export const DetailBody = ({
|
|
|
387
387
|
contentWidth,
|
|
388
388
|
bodyLines = DETAIL_BODY_LINES,
|
|
389
389
|
loadingIndicator,
|
|
390
|
+
themeId,
|
|
390
391
|
}: {
|
|
391
392
|
pullRequest: PullRequestItem
|
|
392
393
|
contentWidth: number
|
|
393
394
|
bodyLines?: number
|
|
394
395
|
loadingIndicator: string
|
|
396
|
+
themeId: ThemeId
|
|
395
397
|
}) => {
|
|
396
398
|
const previewLines = useMemo(
|
|
397
399
|
() => bodyPreview(pullRequest.body, contentWidth, bodyLines),
|
|
398
|
-
[pullRequest.body, contentWidth, bodyLines],
|
|
400
|
+
[pullRequest.body, contentWidth, bodyLines, themeId],
|
|
399
401
|
)
|
|
400
402
|
|
|
401
403
|
if (!pullRequest.detailLoaded) {
|
|
@@ -486,6 +488,7 @@ export const DetailsPane = ({
|
|
|
486
488
|
showChecks = false,
|
|
487
489
|
placeholderContent,
|
|
488
490
|
loadingIndicator,
|
|
491
|
+
themeId,
|
|
489
492
|
}: {
|
|
490
493
|
pullRequest: PullRequestItem | null
|
|
491
494
|
contentWidth: number
|
|
@@ -494,6 +497,7 @@ export const DetailsPane = ({
|
|
|
494
497
|
showChecks?: boolean
|
|
495
498
|
placeholderContent: DetailPlaceholderContent
|
|
496
499
|
loadingIndicator: string
|
|
500
|
+
themeId: ThemeId
|
|
497
501
|
}) => {
|
|
498
502
|
const contentHeight = getDetailsPaneHeight({ pullRequest, contentWidth, bodyLines, paneWidth, showChecks })
|
|
499
503
|
|
|
@@ -502,7 +506,7 @@ export const DetailsPane = ({
|
|
|
502
506
|
{pullRequest ? (
|
|
503
507
|
<>
|
|
504
508
|
<DetailHeader pullRequest={pullRequest} contentWidth={contentWidth} paneWidth={paneWidth} showChecks={showChecks} />
|
|
505
|
-
<DetailBody pullRequest={pullRequest} contentWidth={contentWidth} bodyLines={bodyLines} loadingIndicator={loadingIndicator} />
|
|
509
|
+
<DetailBody pullRequest={pullRequest} contentWidth={contentWidth} bodyLines={bodyLines} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
506
510
|
</>
|
|
507
511
|
) : (
|
|
508
512
|
<>
|
package/src/ui/colors.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
export type ThemeId =
|
|
1
|
+
export type ThemeId =
|
|
2
|
+
| "ghui"
|
|
3
|
+
| "tokyo-night"
|
|
4
|
+
| "catppuccin"
|
|
5
|
+
| "rose-pine"
|
|
6
|
+
| "gruvbox"
|
|
7
|
+
| "nord"
|
|
8
|
+
| "dracula"
|
|
9
|
+
| "kanagawa"
|
|
10
|
+
| "one-dark"
|
|
11
|
+
| "monokai"
|
|
12
|
+
| "solarized-dark"
|
|
13
|
+
| "everforest"
|
|
14
|
+
| "opencode"
|
|
2
15
|
|
|
3
16
|
export interface ColorPalette {
|
|
4
17
|
readonly background: string
|
|
5
|
-
readonly panel: string
|
|
6
|
-
readonly footer: string
|
|
7
18
|
readonly modalBackground: string
|
|
8
19
|
readonly text: string
|
|
9
20
|
readonly muted: string
|
|
@@ -50,8 +61,6 @@ export interface ThemeDefinition {
|
|
|
50
61
|
|
|
51
62
|
const ghuiColors: ColorPalette = {
|
|
52
63
|
background: "#111018",
|
|
53
|
-
panel: "#161923",
|
|
54
|
-
footer: "#1d2430",
|
|
55
64
|
modalBackground: "#1a1a2e",
|
|
56
65
|
text: "#ede7da",
|
|
57
66
|
muted: "#9f9788",
|
|
@@ -91,8 +100,6 @@ const ghuiColors: ColorPalette = {
|
|
|
91
100
|
|
|
92
101
|
const tokyoNightColors: ColorPalette = {
|
|
93
102
|
background: "#1a1b26",
|
|
94
|
-
panel: "#16161e",
|
|
95
|
-
footer: "#24283b",
|
|
96
103
|
modalBackground: "#24283b",
|
|
97
104
|
text: "#c0caf5",
|
|
98
105
|
muted: "#787c99",
|
|
@@ -132,8 +139,6 @@ const tokyoNightColors: ColorPalette = {
|
|
|
132
139
|
|
|
133
140
|
const opencodeColors: ColorPalette = {
|
|
134
141
|
background: "#0a0a0a",
|
|
135
|
-
panel: "#141414",
|
|
136
|
-
footer: "#1e1e1e",
|
|
137
142
|
modalBackground: "#1e1e1e",
|
|
138
143
|
text: "#eeeeee",
|
|
139
144
|
muted: "#808080",
|
|
@@ -173,8 +178,6 @@ const opencodeColors: ColorPalette = {
|
|
|
173
178
|
|
|
174
179
|
const catppuccinColors: ColorPalette = {
|
|
175
180
|
background: "#1e1e2e",
|
|
176
|
-
panel: "#181825",
|
|
177
|
-
footer: "#313244",
|
|
178
181
|
modalBackground: "#313244",
|
|
179
182
|
text: "#cdd6f4",
|
|
180
183
|
muted: "#7f849c",
|
|
@@ -214,8 +217,6 @@ const catppuccinColors: ColorPalette = {
|
|
|
214
217
|
|
|
215
218
|
const rosePineColors: ColorPalette = {
|
|
216
219
|
background: "#191724",
|
|
217
|
-
panel: "#1f1d2e",
|
|
218
|
-
footer: "#26233a",
|
|
219
220
|
modalBackground: "#26233a",
|
|
220
221
|
text: "#e0def4",
|
|
221
222
|
muted: "#908caa",
|
|
@@ -255,8 +256,6 @@ const rosePineColors: ColorPalette = {
|
|
|
255
256
|
|
|
256
257
|
const gruvboxColors: ColorPalette = {
|
|
257
258
|
background: "#282828",
|
|
258
|
-
panel: "#1d2021",
|
|
259
|
-
footer: "#3c3836",
|
|
260
259
|
modalBackground: "#3c3836",
|
|
261
260
|
text: "#ebdbb2",
|
|
262
261
|
muted: "#928374",
|
|
@@ -296,8 +295,6 @@ const gruvboxColors: ColorPalette = {
|
|
|
296
295
|
|
|
297
296
|
const nordColors: ColorPalette = {
|
|
298
297
|
background: "#2e3440",
|
|
299
|
-
panel: "#242933",
|
|
300
|
-
footer: "#3b4252",
|
|
301
298
|
modalBackground: "#3b4252",
|
|
302
299
|
text: "#eceff4",
|
|
303
300
|
muted: "#8892a7",
|
|
@@ -337,8 +334,6 @@ const nordColors: ColorPalette = {
|
|
|
337
334
|
|
|
338
335
|
const draculaColors: ColorPalette = {
|
|
339
336
|
background: "#282a36",
|
|
340
|
-
panel: "#21222c",
|
|
341
|
-
footer: "#343746",
|
|
342
337
|
modalBackground: "#343746",
|
|
343
338
|
text: "#f8f8f2",
|
|
344
339
|
muted: "#8f94b8",
|
|
@@ -376,6 +371,201 @@ const draculaColors: ColorPalette = {
|
|
|
376
371
|
},
|
|
377
372
|
}
|
|
378
373
|
|
|
374
|
+
const kanagawaColors: ColorPalette = {
|
|
375
|
+
background: "#1f1f28",
|
|
376
|
+
modalBackground: "#2a2a37",
|
|
377
|
+
text: "#dcd7ba",
|
|
378
|
+
muted: "#727169",
|
|
379
|
+
separator: "#54546d",
|
|
380
|
+
accent: "#7e9cd8",
|
|
381
|
+
inlineCode: "#d27e99",
|
|
382
|
+
error: "#c34043",
|
|
383
|
+
selectedBg: "#363646",
|
|
384
|
+
selectedText: "#fff7d6",
|
|
385
|
+
count: "#ffa066",
|
|
386
|
+
status: {
|
|
387
|
+
draft: "#c0a36e",
|
|
388
|
+
approved: "#76946a",
|
|
389
|
+
changes: "#c34043",
|
|
390
|
+
review: "#7e9cd8",
|
|
391
|
+
none: "#727169",
|
|
392
|
+
passing: "#76946a",
|
|
393
|
+
pending: "#c0a36e",
|
|
394
|
+
failing: "#c34043",
|
|
395
|
+
},
|
|
396
|
+
repos: {
|
|
397
|
+
opencode: "#7e9cd8",
|
|
398
|
+
"effect-smol": "#98bb6c",
|
|
399
|
+
"opencode-console": "#957fb8",
|
|
400
|
+
opencontrol: "#ffa066",
|
|
401
|
+
default: "#7fb4ca",
|
|
402
|
+
},
|
|
403
|
+
diff: {
|
|
404
|
+
addedBg: "#253326",
|
|
405
|
+
removedBg: "#3a2528",
|
|
406
|
+
contextBg: "transparent",
|
|
407
|
+
lineNumberBg: "#16161d",
|
|
408
|
+
addedLineNumberBg: "#223025",
|
|
409
|
+
removedLineNumberBg: "#352326",
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const oneDarkColors: ColorPalette = {
|
|
414
|
+
background: "#282c34",
|
|
415
|
+
modalBackground: "#2c313c",
|
|
416
|
+
text: "#abb2bf",
|
|
417
|
+
muted: "#7f848e",
|
|
418
|
+
separator: "#4b5263",
|
|
419
|
+
accent: "#61afef",
|
|
420
|
+
inlineCode: "#c678dd",
|
|
421
|
+
error: "#e06c75",
|
|
422
|
+
selectedBg: "#3e4451",
|
|
423
|
+
selectedText: "#ffffff",
|
|
424
|
+
count: "#d19a66",
|
|
425
|
+
status: {
|
|
426
|
+
draft: "#e5c07b",
|
|
427
|
+
approved: "#98c379",
|
|
428
|
+
changes: "#e06c75",
|
|
429
|
+
review: "#61afef",
|
|
430
|
+
none: "#7f848e",
|
|
431
|
+
passing: "#98c379",
|
|
432
|
+
pending: "#e5c07b",
|
|
433
|
+
failing: "#e06c75",
|
|
434
|
+
},
|
|
435
|
+
repos: {
|
|
436
|
+
opencode: "#61afef",
|
|
437
|
+
"effect-smol": "#98c379",
|
|
438
|
+
"opencode-console": "#c678dd",
|
|
439
|
+
opencontrol: "#d19a66",
|
|
440
|
+
default: "#56b6c2",
|
|
441
|
+
},
|
|
442
|
+
diff: {
|
|
443
|
+
addedBg: "#27362b",
|
|
444
|
+
removedBg: "#3a282c",
|
|
445
|
+
contextBg: "transparent",
|
|
446
|
+
lineNumberBg: "#21252b",
|
|
447
|
+
addedLineNumberBg: "#243228",
|
|
448
|
+
removedLineNumberBg: "#35262a",
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const monokaiColors: ColorPalette = {
|
|
453
|
+
background: "#272822",
|
|
454
|
+
modalBackground: "#383830",
|
|
455
|
+
text: "#f8f8f2",
|
|
456
|
+
muted: "#90908a",
|
|
457
|
+
separator: "#5b5b50",
|
|
458
|
+
accent: "#66d9ef",
|
|
459
|
+
inlineCode: "#ae81ff",
|
|
460
|
+
error: "#f92672",
|
|
461
|
+
selectedBg: "#49483e",
|
|
462
|
+
selectedText: "#ffffff",
|
|
463
|
+
count: "#fd971f",
|
|
464
|
+
status: {
|
|
465
|
+
draft: "#e6db74",
|
|
466
|
+
approved: "#a6e22e",
|
|
467
|
+
changes: "#f92672",
|
|
468
|
+
review: "#66d9ef",
|
|
469
|
+
none: "#90908a",
|
|
470
|
+
passing: "#a6e22e",
|
|
471
|
+
pending: "#e6db74",
|
|
472
|
+
failing: "#f92672",
|
|
473
|
+
},
|
|
474
|
+
repos: {
|
|
475
|
+
opencode: "#66d9ef",
|
|
476
|
+
"effect-smol": "#a6e22e",
|
|
477
|
+
"opencode-console": "#ae81ff",
|
|
478
|
+
opencontrol: "#fd971f",
|
|
479
|
+
default: "#a1efe4",
|
|
480
|
+
},
|
|
481
|
+
diff: {
|
|
482
|
+
addedBg: "#2f3a22",
|
|
483
|
+
removedBg: "#3d2430",
|
|
484
|
+
contextBg: "transparent",
|
|
485
|
+
lineNumberBg: "#1f201b",
|
|
486
|
+
addedLineNumberBg: "#2b3620",
|
|
487
|
+
removedLineNumberBg: "#38222d",
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const solarizedDarkColors: ColorPalette = {
|
|
492
|
+
background: "#002b36",
|
|
493
|
+
modalBackground: "#123d48",
|
|
494
|
+
text: "#eee8d5",
|
|
495
|
+
muted: "#839496",
|
|
496
|
+
separator: "#586e75",
|
|
497
|
+
accent: "#268bd2",
|
|
498
|
+
inlineCode: "#2aa198",
|
|
499
|
+
error: "#dc322f",
|
|
500
|
+
selectedBg: "#174652",
|
|
501
|
+
selectedText: "#fdf6e3",
|
|
502
|
+
count: "#cb4b16",
|
|
503
|
+
status: {
|
|
504
|
+
draft: "#b58900",
|
|
505
|
+
approved: "#859900",
|
|
506
|
+
changes: "#dc322f",
|
|
507
|
+
review: "#268bd2",
|
|
508
|
+
none: "#839496",
|
|
509
|
+
passing: "#859900",
|
|
510
|
+
pending: "#b58900",
|
|
511
|
+
failing: "#dc322f",
|
|
512
|
+
},
|
|
513
|
+
repos: {
|
|
514
|
+
opencode: "#268bd2",
|
|
515
|
+
"effect-smol": "#859900",
|
|
516
|
+
"opencode-console": "#d33682",
|
|
517
|
+
opencontrol: "#cb4b16",
|
|
518
|
+
default: "#2aa198",
|
|
519
|
+
},
|
|
520
|
+
diff: {
|
|
521
|
+
addedBg: "#123c2e",
|
|
522
|
+
removedBg: "#3c262a",
|
|
523
|
+
contextBg: "transparent",
|
|
524
|
+
lineNumberBg: "#073642",
|
|
525
|
+
addedLineNumberBg: "#10372b",
|
|
526
|
+
removedLineNumberBg: "#362429",
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const everforestColors: ColorPalette = {
|
|
531
|
+
background: "#2d353b",
|
|
532
|
+
modalBackground: "#343f44",
|
|
533
|
+
text: "#d3c6aa",
|
|
534
|
+
muted: "#859289",
|
|
535
|
+
separator: "#56635f",
|
|
536
|
+
accent: "#7fbbb3",
|
|
537
|
+
inlineCode: "#d699b6",
|
|
538
|
+
error: "#e67e80",
|
|
539
|
+
selectedBg: "#465258",
|
|
540
|
+
selectedText: "#fff4d6",
|
|
541
|
+
count: "#e69875",
|
|
542
|
+
status: {
|
|
543
|
+
draft: "#dbbc7f",
|
|
544
|
+
approved: "#a7c080",
|
|
545
|
+
changes: "#e67e80",
|
|
546
|
+
review: "#7fbbb3",
|
|
547
|
+
none: "#859289",
|
|
548
|
+
passing: "#a7c080",
|
|
549
|
+
pending: "#dbbc7f",
|
|
550
|
+
failing: "#e67e80",
|
|
551
|
+
},
|
|
552
|
+
repos: {
|
|
553
|
+
opencode: "#7fbbb3",
|
|
554
|
+
"effect-smol": "#a7c080",
|
|
555
|
+
"opencode-console": "#d699b6",
|
|
556
|
+
opencontrol: "#e69875",
|
|
557
|
+
default: "#83c092",
|
|
558
|
+
},
|
|
559
|
+
diff: {
|
|
560
|
+
addedBg: "#33422f",
|
|
561
|
+
removedBg: "#463333",
|
|
562
|
+
contextBg: "transparent",
|
|
563
|
+
lineNumberBg: "#232a2e",
|
|
564
|
+
addedLineNumberBg: "#303d2d",
|
|
565
|
+
removedLineNumberBg: "#403030",
|
|
566
|
+
},
|
|
567
|
+
}
|
|
568
|
+
|
|
379
569
|
export const themeDefinitions: readonly ThemeDefinition[] = [
|
|
380
570
|
{ id: "ghui", name: "GHUI", description: "Warm parchment accents on a deep slate background", colors: ghuiColors },
|
|
381
571
|
{ id: "tokyo-night", name: "Tokyo Night", description: "Cool indigo surfaces with neon editor accents", colors: tokyoNightColors },
|
|
@@ -384,6 +574,11 @@ export const themeDefinitions: readonly ThemeDefinition[] = [
|
|
|
384
574
|
{ id: "gruvbox", name: "Gruvbox", description: "Retro warm earth tones with punchy semantic accents", colors: gruvboxColors },
|
|
385
575
|
{ id: "nord", name: "Nord", description: "Arctic blue-gray surfaces with frosty accents", colors: nordColors },
|
|
386
576
|
{ id: "dracula", name: "Dracula", description: "High-contrast purple, pink, cyan, and green", colors: draculaColors },
|
|
577
|
+
{ id: "kanagawa", name: "Kanagawa", description: "Ink-wash indigo, wave blues, and autumn accents", colors: kanagawaColors },
|
|
578
|
+
{ id: "one-dark", name: "One Dark", description: "Atom-style charcoal with clean blue and green accents", colors: oneDarkColors },
|
|
579
|
+
{ id: "monokai", name: "Monokai", description: "Classic dark olive with electric syntax colors", colors: monokaiColors },
|
|
580
|
+
{ id: "solarized-dark", name: "Solarized Dark", description: "Low-contrast blue-green base with calibrated accents", colors: solarizedDarkColors },
|
|
581
|
+
{ id: "everforest", name: "Everforest", description: "Soft green-gray forest tones with warm highlights", colors: everforestColors },
|
|
387
582
|
{ id: "opencode", name: "OpenCode", description: "Charcoal panels with peach, violet, and blue highlights", colors: opencodeColors },
|
|
388
583
|
] as const
|
|
389
584
|
|
|
@@ -393,6 +588,16 @@ export const colors: ColorPalette = { ...ghuiColors }
|
|
|
393
588
|
|
|
394
589
|
export const getThemeDefinition = (id: ThemeId) => themeDefinitions.find((theme) => theme.id === id) ?? themeDefinitions[0]!
|
|
395
590
|
|
|
591
|
+
export const filterThemeDefinitions = (query: string) => {
|
|
592
|
+
const normalized = query.trim().toLowerCase()
|
|
593
|
+
if (normalized.length === 0) return themeDefinitions
|
|
594
|
+
return themeDefinitions.filter((theme) =>
|
|
595
|
+
theme.id.includes(normalized) ||
|
|
596
|
+
theme.name.toLowerCase().includes(normalized) ||
|
|
597
|
+
theme.description.toLowerCase().includes(normalized),
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
396
601
|
export const setActiveTheme = (id: ThemeId) => {
|
|
397
602
|
if (activeTheme.id === id) return
|
|
398
603
|
activeTheme = getThemeDefinition(id)
|
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, themeDefinitions, type ThemeId } from "./colors.js"
|
|
4
|
+
import { colors, filterThemeDefinitions, 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
|
|
|
@@ -27,6 +27,8 @@ export interface MergeModalState {
|
|
|
27
27
|
|
|
28
28
|
export interface ThemeModalState {
|
|
29
29
|
readonly open: boolean
|
|
30
|
+
readonly query: string
|
|
31
|
+
readonly filterMode: boolean
|
|
30
32
|
readonly initialThemeId: ThemeId
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -52,6 +54,8 @@ export const initialMergeModalState: MergeModalState = {
|
|
|
52
54
|
|
|
53
55
|
export const initialThemeModalState: ThemeModalState = {
|
|
54
56
|
open: false,
|
|
57
|
+
query: "",
|
|
58
|
+
filterMode: false,
|
|
55
59
|
initialThemeId: "ghui",
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -276,21 +280,27 @@ export const ThemeModal = ({
|
|
|
276
280
|
const innerWidth = Math.max(16, modalWidth - 2)
|
|
277
281
|
const contentWidth = Math.max(14, innerWidth - 2)
|
|
278
282
|
const rowWidth = innerWidth
|
|
279
|
-
const
|
|
280
|
-
const
|
|
283
|
+
const filteredThemes = filterThemeDefinitions(state.query)
|
|
284
|
+
const maxVisible = Math.max(1, modalHeight - 8)
|
|
285
|
+
const activeIndex = filteredThemes.findIndex((theme) => theme.id === activeThemeId)
|
|
281
286
|
const selectedIndex = Math.max(0, activeIndex)
|
|
282
|
-
const selectedTheme =
|
|
287
|
+
const selectedTheme = filteredThemes[selectedIndex] ?? themeDefinitions.find((theme) => theme.id === activeThemeId) ?? themeDefinitions[0]!
|
|
283
288
|
const scrollStart = Math.min(
|
|
284
|
-
Math.max(0,
|
|
289
|
+
Math.max(0, filteredThemes.length - maxVisible),
|
|
285
290
|
Math.max(0, selectedIndex - maxVisible + 1),
|
|
286
291
|
)
|
|
287
|
-
const visibleThemes =
|
|
288
|
-
const countText = `${selectedIndex + 1}/${
|
|
292
|
+
const visibleThemes = filteredThemes.slice(scrollStart, scrollStart + maxVisible)
|
|
293
|
+
const countText = `${filteredThemes.length === 0 ? 0 : selectedIndex + 1}/${filteredThemes.length}`
|
|
289
294
|
const title = "Themes"
|
|
290
295
|
const headerGap = Math.max(1, contentWidth - title.length - countText.length)
|
|
296
|
+
const queryText = state.query.length > 0 ? state.query : "type to filter themes"
|
|
297
|
+
const queryPrefix = "/ "
|
|
298
|
+
const queryWidth = Math.max(1, contentWidth - queryPrefix.length)
|
|
299
|
+
const messageTopRows = Math.max(0, Math.floor((maxVisible - 1) / 2))
|
|
300
|
+
const messageBottomRows = Math.max(0, maxVisible - messageTopRows - 1)
|
|
291
301
|
|
|
292
302
|
return (
|
|
293
|
-
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[
|
|
303
|
+
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[3, modalHeight - 4]}>
|
|
294
304
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
295
305
|
<TextLine>
|
|
296
306
|
<span fg={colors.accent} attributes={TextAttributes.BOLD}>{title}</span>
|
|
@@ -301,9 +311,21 @@ export const ThemeModal = ({
|
|
|
301
311
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
302
312
|
<PlainLine text={fitCell(selectedTheme.description, contentWidth)} fg={colors.muted} />
|
|
303
313
|
</box>
|
|
314
|
+
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
315
|
+
<TextLine>
|
|
316
|
+
<span fg={colors.count}>{queryPrefix}</span>
|
|
317
|
+
<span fg={state.query.length > 0 ? colors.text : colors.muted}>{fitCell(queryText, queryWidth)}</span>
|
|
318
|
+
</TextLine>
|
|
319
|
+
</box>
|
|
304
320
|
<Divider width={innerWidth} />
|
|
305
321
|
<box height={maxVisible} flexDirection="column">
|
|
306
|
-
{visibleThemes.
|
|
322
|
+
{visibleThemes.length === 0 ? (
|
|
323
|
+
<>
|
|
324
|
+
{Array.from({ length: messageTopRows }, (_, index) => <box key={`top-${index}`} height={1} />)}
|
|
325
|
+
<PlainLine text={centerCell("No matching themes", rowWidth)} fg={colors.muted} />
|
|
326
|
+
{Array.from({ length: messageBottomRows }, (_, index) => <box key={`bottom-${index}`} height={1} />)}
|
|
327
|
+
</>
|
|
328
|
+
) : visibleThemes.map((theme, index) => {
|
|
307
329
|
const actualIndex = scrollStart + index
|
|
308
330
|
const isSelected = actualIndex === selectedIndex
|
|
309
331
|
const isActive = theme.id === activeThemeId
|
|
@@ -317,7 +339,7 @@ export const ThemeModal = ({
|
|
|
317
339
|
<span> </span>
|
|
318
340
|
<span>{fitCell(theme.name, nameWidth)}</span>
|
|
319
341
|
<span bg={theme.colors.background}> </span>
|
|
320
|
-
<span bg={theme.colors.
|
|
342
|
+
<span bg={theme.colors.modalBackground}> </span>
|
|
321
343
|
<span bg={theme.colors.accent}> </span>
|
|
322
344
|
<span bg={theme.colors.status.passing}> </span>
|
|
323
345
|
<span bg={theme.colors.status.failing}> </span>
|
|
@@ -331,6 +353,8 @@ export const ThemeModal = ({
|
|
|
331
353
|
<TextLine>
|
|
332
354
|
<span fg={colors.count}>↑↓</span>
|
|
333
355
|
<span fg={colors.muted}> preview </span>
|
|
356
|
+
<span fg={colors.count}>/</span>
|
|
357
|
+
<span fg={colors.muted}> filter </span>
|
|
334
358
|
<span fg={colors.count}>enter</span>
|
|
335
359
|
<span fg={colors.muted}> select </span>
|
|
336
360
|
<span fg={colors.count}>esc</span>
|