@kitlangton/ghui 0.1.14 → 0.1.16
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 -12
- package/src/themeStore.ts +43 -0
- package/src/ui/colors.ts +226 -19
- package/src/ui/modals.tsx +34 -9
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -11,7 +11,8 @@ 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 {
|
|
14
|
+
import { loadStoredThemeId, saveStoredThemeId } from "./themeStore.js"
|
|
15
|
+
import { colors, filterThemeDefinitions, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
15
16
|
import { pullRequestDiffKey, splitPatchFiles, type PullRequestDiffState } from "./ui/diff.js"
|
|
16
17
|
import { DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailBodyHeight, getDetailHeaderHeight, getDetailJunctionRows, getDetailsPaneHeight, LoadingPane, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
17
18
|
import { FooterHints, type RetryProgress } from "./ui/FooterHints.js"
|
|
@@ -22,6 +23,7 @@ import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
|
22
23
|
import { PullRequestList } from "./ui/PullRequestList.js"
|
|
23
24
|
|
|
24
25
|
const githubRuntime = Atom.runtime(GitHubService.layer.pipe(Layer.provideMerge(Observability.layer)))
|
|
26
|
+
const initialThemeId = await Effect.runPromise(loadStoredThemeId)
|
|
25
27
|
|
|
26
28
|
type LoadStatus = "loading" | "ready" | "error"
|
|
27
29
|
|
|
@@ -81,7 +83,7 @@ const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>
|
|
|
81
83
|
|
|
82
84
|
const labelModalAtom = Atom.make(initialLabelModalState).pipe(Atom.keepAlive)
|
|
83
85
|
const mergeModalAtom = Atom.make(initialMergeModalState).pipe(Atom.keepAlive)
|
|
84
|
-
const themeIdAtom = Atom.make<ThemeId>(
|
|
86
|
+
const themeIdAtom = Atom.make<ThemeId>(initialThemeId).pipe(Atom.keepAlive)
|
|
85
87
|
const themeModalAtom = Atom.make(initialThemeModalState).pipe(Atom.keepAlive)
|
|
86
88
|
const labelCacheAtom = Atom.make<Record<string, readonly PullRequestLabel[]>>({}).pipe(Atom.keepAlive)
|
|
87
89
|
const pullRequestOverridesAtom = Atom.make<Record<string, PullRequestItem>>({}).pipe(Atom.keepAlive)
|
|
@@ -264,6 +266,10 @@ export const App = () => {
|
|
|
264
266
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
265
267
|
const [themeModal, setThemeModal] = useAtom(themeModalAtom)
|
|
266
268
|
setActiveTheme(themeId)
|
|
269
|
+
const themeIdRef = useRef(themeId)
|
|
270
|
+
const themeModalRef = useRef(themeModal)
|
|
271
|
+
themeIdRef.current = themeId
|
|
272
|
+
themeModalRef.current = themeModal
|
|
267
273
|
const [labelCache, setLabelCache] = useAtom(labelCacheAtom)
|
|
268
274
|
const [pullRequestOverrides, setPullRequestOverrides] = useAtom(pullRequestOverridesAtom)
|
|
269
275
|
const retryProgress = useAtomValue(retryProgressAtom)
|
|
@@ -559,26 +565,59 @@ export const App = () => {
|
|
|
559
565
|
setMergeModal(initialMergeModalState)
|
|
560
566
|
setThemeModal({
|
|
561
567
|
open: true,
|
|
568
|
+
query: "",
|
|
569
|
+
filterMode: false,
|
|
562
570
|
initialThemeId: themeId,
|
|
563
571
|
})
|
|
564
572
|
}
|
|
565
573
|
|
|
566
574
|
const closeThemeModal = (confirm: boolean) => {
|
|
567
|
-
const selectedTheme = themeDefinitions.find((theme) => theme.id ===
|
|
575
|
+
const selectedTheme = themeDefinitions.find((theme) => theme.id === themeIdRef.current)
|
|
568
576
|
if (!confirm) {
|
|
569
577
|
setThemeId(themeModal.initialThemeId)
|
|
570
578
|
} else if (selectedTheme) {
|
|
579
|
+
void Effect.runPromise(saveStoredThemeId(selectedTheme.id)).catch((error) => flashNotice(errorMessage(error)))
|
|
571
580
|
flashNotice(`Theme: ${selectedTheme.name}`)
|
|
572
581
|
}
|
|
573
582
|
setThemeModal(initialThemeModalState)
|
|
574
583
|
}
|
|
575
584
|
|
|
585
|
+
const previewTheme = (id: ThemeId) => {
|
|
586
|
+
if (id === themeIdRef.current) return
|
|
587
|
+
themeIdRef.current = id
|
|
588
|
+
setThemeId(id)
|
|
589
|
+
}
|
|
590
|
+
|
|
576
591
|
const moveThemeSelection = (delta: number) => {
|
|
577
|
-
const
|
|
578
|
-
|
|
592
|
+
const filteredThemes = filterThemeDefinitions(themeModalRef.current.query)
|
|
593
|
+
if (filteredThemes.length === 0) return
|
|
594
|
+
const currentIndex = Math.max(0, filteredThemes.findIndex((theme) => theme.id === themeIdRef.current))
|
|
595
|
+
const selectedIndex = Math.max(0, Math.min(filteredThemes.length - 1, currentIndex + delta))
|
|
579
596
|
if (selectedIndex === currentIndex) return
|
|
580
|
-
const theme =
|
|
581
|
-
if (theme
|
|
597
|
+
const theme = filteredThemes[selectedIndex]
|
|
598
|
+
if (theme) previewTheme(theme.id)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const updateThemeQuery = (query: string, options: { readonly previewFirst?: boolean; readonly filterMode?: boolean } = {}) => {
|
|
602
|
+
const current = themeModalRef.current
|
|
603
|
+
const next = {
|
|
604
|
+
...current,
|
|
605
|
+
query,
|
|
606
|
+
filterMode: options.filterMode ?? current.filterMode,
|
|
607
|
+
}
|
|
608
|
+
if (next.query === current.query && next.filterMode === current.filterMode) return
|
|
609
|
+
|
|
610
|
+
themeModalRef.current = next
|
|
611
|
+
setThemeModal(next)
|
|
612
|
+
|
|
613
|
+
if (options.previewFirst && query.trim().length > 0) {
|
|
614
|
+
const firstTheme = filterThemeDefinitions(query)[0]
|
|
615
|
+
if (firstTheme) previewTheme(firstTheme.id)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const editThemeQuery = (transform: (query: string) => string) => {
|
|
620
|
+
updateThemeQuery(transform(themeModalRef.current.query), { previewFirst: true })
|
|
582
621
|
}
|
|
583
622
|
|
|
584
623
|
const openLabelModal = () => {
|
|
@@ -714,7 +753,7 @@ export const App = () => {
|
|
|
714
753
|
}
|
|
715
754
|
|
|
716
755
|
useKeyboard((key) => {
|
|
717
|
-
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
756
|
+
if ((key.name === "q" && !(themeModal.open && themeModal.filterMode)) || (key.ctrl && key.name === "c")) {
|
|
718
757
|
if (themeModal.open) {
|
|
719
758
|
closeThemeModal(false)
|
|
720
759
|
return
|
|
@@ -733,21 +772,42 @@ export const App = () => {
|
|
|
733
772
|
|
|
734
773
|
if (themeModal.open) {
|
|
735
774
|
if (key.name === "escape") {
|
|
775
|
+
if (themeModal.filterMode) {
|
|
776
|
+
updateThemeQuery("", { filterMode: false })
|
|
777
|
+
return
|
|
778
|
+
}
|
|
736
779
|
closeThemeModal(false)
|
|
737
780
|
return
|
|
738
781
|
}
|
|
782
|
+
if (key.name === "/") {
|
|
783
|
+
updateThemeQuery("", { filterMode: true })
|
|
784
|
+
return
|
|
785
|
+
}
|
|
739
786
|
if (key.name === "return" || key.name === "enter") {
|
|
787
|
+
if (themeModal.filterMode && filterThemeDefinitions(themeModal.query).length === 0) return
|
|
740
788
|
closeThemeModal(true)
|
|
741
789
|
return
|
|
742
790
|
}
|
|
743
|
-
if (key.name === "up" || key.name === "k") {
|
|
791
|
+
if (key.name === "up" || (!themeModal.filterMode && key.name === "k")) {
|
|
744
792
|
moveThemeSelection(-1)
|
|
745
793
|
return
|
|
746
794
|
}
|
|
747
|
-
if (key.name === "down" || key.name === "j") {
|
|
795
|
+
if (key.name === "down" || (!themeModal.filterMode && key.name === "j")) {
|
|
748
796
|
moveThemeSelection(1)
|
|
749
797
|
return
|
|
750
798
|
}
|
|
799
|
+
if (themeModal.filterMode && key.name === "backspace") {
|
|
800
|
+
editThemeQuery((query) => query.slice(0, -1))
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
if (themeModal.filterMode && key.ctrl && key.name === "u") {
|
|
804
|
+
updateThemeQuery("")
|
|
805
|
+
return
|
|
806
|
+
}
|
|
807
|
+
if (themeModal.filterMode && !key.ctrl && !key.meta && key.sequence.length === 1 && key.name !== "return") {
|
|
808
|
+
editThemeQuery((query) => query + key.sequence)
|
|
809
|
+
return
|
|
810
|
+
}
|
|
751
811
|
return
|
|
752
812
|
}
|
|
753
813
|
|
|
@@ -1229,7 +1289,7 @@ export const App = () => {
|
|
|
1229
1289
|
|
|
1230
1290
|
return (
|
|
1231
1291
|
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
1232
|
-
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.
|
|
1292
|
+
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.background}>
|
|
1233
1293
|
<PlainLine text={headerLine} fg={colors.muted} bold />
|
|
1234
1294
|
</box>
|
|
1235
1295
|
{isWideLayout && !detailFullView && !diffFullView && !isInitialLoading ? (
|
|
@@ -1321,7 +1381,7 @@ export const App = () => {
|
|
|
1321
1381
|
) : (
|
|
1322
1382
|
<Divider width={contentWidth} />
|
|
1323
1383
|
)}
|
|
1324
|
-
<box paddingLeft={1} paddingRight={1} backgroundColor={colors.
|
|
1384
|
+
<box paddingLeft={1} paddingRight={1} backgroundColor={colors.background}>
|
|
1325
1385
|
{footerNotice ? (
|
|
1326
1386
|
<PlainLine text={footerNotice} fg={colors.count} />
|
|
1327
1387
|
) : (
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
import { Effect } from "effect"
|
|
5
|
+
import { isThemeId, type ThemeId } from "./ui/colors.js"
|
|
6
|
+
|
|
7
|
+
interface StoredConfig {
|
|
8
|
+
readonly theme?: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const configDirectory = () => {
|
|
12
|
+
if (process.env.GHUI_CONFIG_DIR) return process.env.GHUI_CONFIG_DIR
|
|
13
|
+
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, "ghui")
|
|
14
|
+
if (process.platform === "win32" && process.env.APPDATA) return join(process.env.APPDATA, "ghui")
|
|
15
|
+
return join(homedir(), ".config", "ghui")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const configPath = () => join(configDirectory(), "config.json")
|
|
19
|
+
|
|
20
|
+
const parseConfig = (text: string): StoredConfig => {
|
|
21
|
+
const value = JSON.parse(text) as unknown
|
|
22
|
+
return value && typeof value === "object" ? value : {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const loadStoredThemeId: Effect.Effect<ThemeId> = Effect.catchCause(Effect.tryPromise(async () => {
|
|
26
|
+
const file = Bun.file(configPath())
|
|
27
|
+
if (!(await file.exists())) return "ghui" satisfies ThemeId
|
|
28
|
+
|
|
29
|
+
const config = parseConfig(await file.text())
|
|
30
|
+
return isThemeId(config.theme) ? config.theme : "ghui"
|
|
31
|
+
}), () => Effect.succeed("ghui" satisfies ThemeId))
|
|
32
|
+
|
|
33
|
+
export const saveStoredThemeId = (theme: ThemeId): Effect.Effect<void> => Effect.tryPromise(async () => {
|
|
34
|
+
const path = configPath()
|
|
35
|
+
const file = Bun.file(path)
|
|
36
|
+
const config = await file.exists()
|
|
37
|
+
? parseConfig(await file.text())
|
|
38
|
+
: {}
|
|
39
|
+
if (config.theme === theme) return
|
|
40
|
+
|
|
41
|
+
await mkdir(dirname(path), { recursive: true })
|
|
42
|
+
await Bun.write(path, `${JSON.stringify({ ...config, theme }, null, "\t")}\n`)
|
|
43
|
+
})
|
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,18 @@ 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 isThemeId = (value: unknown): value is ThemeId => typeof value === "string" && themeDefinitions.some((theme) => theme.id === value)
|
|
592
|
+
|
|
593
|
+
export const filterThemeDefinitions = (query: string) => {
|
|
594
|
+
const normalized = query.trim().toLowerCase()
|
|
595
|
+
if (normalized.length === 0) return themeDefinitions
|
|
596
|
+
return themeDefinitions.filter((theme) =>
|
|
597
|
+
theme.id.includes(normalized) ||
|
|
598
|
+
theme.name.toLowerCase().includes(normalized) ||
|
|
599
|
+
theme.description.toLowerCase().includes(normalized),
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
396
603
|
export const setActiveTheme = (id: ThemeId) => {
|
|
397
604
|
if (activeTheme.id === id) return
|
|
398
605
|
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,18 +280,24 @@ 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
|
|
283
|
+
const filteredThemes = filterThemeDefinitions(state.query)
|
|
279
284
|
const maxVisible = Math.max(1, modalHeight - 7)
|
|
280
|
-
const activeIndex =
|
|
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 subtitleText = state.filterMode ? (state.query.length > 0 ? state.query : "type to filter themes") : selectedTheme.description
|
|
297
|
+
const queryPrefix = "/ "
|
|
298
|
+
const subtitleWidth = Math.max(1, contentWidth - (state.filterMode ? queryPrefix.length : 0))
|
|
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
303
|
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[2, modalHeight - 4]}>
|
|
@@ -299,11 +309,24 @@ export const ThemeModal = ({
|
|
|
299
309
|
</TextLine>
|
|
300
310
|
</box>
|
|
301
311
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
302
|
-
|
|
312
|
+
{state.filterMode ? (
|
|
313
|
+
<TextLine>
|
|
314
|
+
<span fg={colors.count}>{queryPrefix}</span>
|
|
315
|
+
<span fg={state.query.length > 0 ? colors.text : colors.muted}>{fitCell(subtitleText, subtitleWidth)}</span>
|
|
316
|
+
</TextLine>
|
|
317
|
+
) : (
|
|
318
|
+
<PlainLine text={fitCell(subtitleText, subtitleWidth)} fg={colors.muted} />
|
|
319
|
+
)}
|
|
303
320
|
</box>
|
|
304
321
|
<Divider width={innerWidth} />
|
|
305
322
|
<box height={maxVisible} flexDirection="column">
|
|
306
|
-
{visibleThemes.
|
|
323
|
+
{visibleThemes.length === 0 ? (
|
|
324
|
+
<>
|
|
325
|
+
{Array.from({ length: messageTopRows }, (_, index) => <box key={`top-${index}`} height={1} />)}
|
|
326
|
+
<PlainLine text={centerCell("No matching themes", rowWidth)} fg={colors.muted} />
|
|
327
|
+
{Array.from({ length: messageBottomRows }, (_, index) => <box key={`bottom-${index}`} height={1} />)}
|
|
328
|
+
</>
|
|
329
|
+
) : visibleThemes.map((theme, index) => {
|
|
307
330
|
const actualIndex = scrollStart + index
|
|
308
331
|
const isSelected = actualIndex === selectedIndex
|
|
309
332
|
const isActive = theme.id === activeThemeId
|
|
@@ -317,7 +340,7 @@ export const ThemeModal = ({
|
|
|
317
340
|
<span> </span>
|
|
318
341
|
<span>{fitCell(theme.name, nameWidth)}</span>
|
|
319
342
|
<span bg={theme.colors.background}> </span>
|
|
320
|
-
<span bg={theme.colors.
|
|
343
|
+
<span bg={theme.colors.modalBackground}> </span>
|
|
321
344
|
<span bg={theme.colors.accent}> </span>
|
|
322
345
|
<span bg={theme.colors.status.passing}> </span>
|
|
323
346
|
<span bg={theme.colors.status.failing}> </span>
|
|
@@ -331,6 +354,8 @@ export const ThemeModal = ({
|
|
|
331
354
|
<TextLine>
|
|
332
355
|
<span fg={colors.count}>↑↓</span>
|
|
333
356
|
<span fg={colors.muted}> preview </span>
|
|
357
|
+
<span fg={colors.count}>/</span>
|
|
358
|
+
<span fg={colors.muted}> filter </span>
|
|
334
359
|
<span fg={colors.count}>enter</span>
|
|
335
360
|
<span fg={colors.muted}> select </span>
|
|
336
361
|
<span fg={colors.count}>esc</span>
|