@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitlangton/ghui",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Terminal UI for GitHub pull requests",
5
5
  "type": "module",
6
6
  "license": "MIT",
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>("ghui").pipe(Atom.keepAlive)
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 - 8)
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 queryText = state.query.length > 0 ? state.query : "type to filter themes"
296
+ const subtitleText = state.filterMode ? (state.query.length > 0 ? state.query : "type to filter themes") : selectedTheme.description
297
297
  const queryPrefix = "/ "
298
- const queryWidth = Math.max(1, contentWidth - queryPrefix.length)
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={[3, modalHeight - 4]}>
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
- <PlainLine text={fitCell(selectedTheme.description, contentWidth)} fg={colors.muted} />
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>
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">