@kitlangton/ghui 0.1.15 → 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 +4 -1
- package/src/themeStore.ts +43 -0
- package/src/ui/colors.ts +2 -0
- package/src/ui/modals.tsx +12 -11
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -11,6 +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 { loadStoredThemeId, saveStoredThemeId } from "./themeStore.js"
|
|
14
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"
|
|
@@ -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)
|
|
@@ -574,6 +576,7 @@ export const App = () => {
|
|
|
574
576
|
if (!confirm) {
|
|
575
577
|
setThemeId(themeModal.initialThemeId)
|
|
576
578
|
} else if (selectedTheme) {
|
|
579
|
+
void Effect.runPromise(saveStoredThemeId(selectedTheme.id)).catch((error) => flashNotice(errorMessage(error)))
|
|
577
580
|
flashNotice(`Theme: ${selectedTheme.name}`)
|
|
578
581
|
}
|
|
579
582
|
setThemeModal(initialThemeModalState)
|
|
@@ -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
|
@@ -588,6 +588,8 @@ export const colors: ColorPalette = { ...ghuiColors }
|
|
|
588
588
|
|
|
589
589
|
export const getThemeDefinition = (id: ThemeId) => themeDefinitions.find((theme) => theme.id === id) ?? themeDefinitions[0]!
|
|
590
590
|
|
|
591
|
+
export const isThemeId = (value: unknown): value is ThemeId => typeof value === "string" && themeDefinitions.some((theme) => theme.id === value)
|
|
592
|
+
|
|
591
593
|
export const filterThemeDefinitions = (query: string) => {
|
|
592
594
|
const normalized = query.trim().toLowerCase()
|
|
593
595
|
if (normalized.length === 0) return themeDefinitions
|
package/src/ui/modals.tsx
CHANGED
|
@@ -281,7 +281,7 @@ export const ThemeModal = ({
|
|
|
281
281
|
const contentWidth = Math.max(14, innerWidth - 2)
|
|
282
282
|
const rowWidth = innerWidth
|
|
283
283
|
const filteredThemes = filterThemeDefinitions(state.query)
|
|
284
|
-
const maxVisible = Math.max(1, modalHeight -
|
|
284
|
+
const maxVisible = Math.max(1, modalHeight - 7)
|
|
285
285
|
const activeIndex = filteredThemes.findIndex((theme) => theme.id === activeThemeId)
|
|
286
286
|
const selectedIndex = Math.max(0, activeIndex)
|
|
287
287
|
const selectedTheme = filteredThemes[selectedIndex] ?? themeDefinitions.find((theme) => theme.id === activeThemeId) ?? themeDefinitions[0]!
|
|
@@ -293,14 +293,14 @@ export const ThemeModal = ({
|
|
|
293
293
|
const countText = `${filteredThemes.length === 0 ? 0 : selectedIndex + 1}/${filteredThemes.length}`
|
|
294
294
|
const title = "Themes"
|
|
295
295
|
const headerGap = Math.max(1, contentWidth - title.length - countText.length)
|
|
296
|
-
const
|
|
296
|
+
const subtitleText = state.filterMode ? (state.query.length > 0 ? state.query : "type to filter themes") : selectedTheme.description
|
|
297
297
|
const queryPrefix = "/ "
|
|
298
|
-
const
|
|
298
|
+
const subtitleWidth = Math.max(1, contentWidth - (state.filterMode ? queryPrefix.length : 0))
|
|
299
299
|
const messageTopRows = Math.max(0, Math.floor((maxVisible - 1) / 2))
|
|
300
300
|
const messageBottomRows = Math.max(0, maxVisible - messageTopRows - 1)
|
|
301
301
|
|
|
302
302
|
return (
|
|
303
|
-
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[
|
|
303
|
+
<ModalFrame left={offsetLeft} top={offsetTop} width={modalWidth} height={modalHeight} junctionRows={[2, modalHeight - 4]}>
|
|
304
304
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
305
305
|
<TextLine>
|
|
306
306
|
<span fg={colors.accent} attributes={TextAttributes.BOLD}>{title}</span>
|
|
@@ -309,13 +309,14 @@ export const ThemeModal = ({
|
|
|
309
309
|
</TextLine>
|
|
310
310
|
</box>
|
|
311
311
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
)}
|
|
319
320
|
</box>
|
|
320
321
|
<Divider width={innerWidth} />
|
|
321
322
|
<box height={maxVisible} flexDirection="column">
|