@kitlangton/ghui 0.1.12 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitlangton/ghui",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Terminal UI for GitHub pull requests",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/App.tsx CHANGED
@@ -198,7 +198,7 @@ const copyPullRequestMetadata = async (pullRequest: PullRequestItem) => {
198
198
 
199
199
  const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "G" || key.name === "g" && key.shift
200
200
 
201
- const isShiftT = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "T" || key.name === "t" && key.shift
201
+ const isThemeKey = (key: { readonly name: string; readonly ctrl?: boolean; readonly meta?: boolean }) => !key.ctrl && !key.meta && key.name.toLowerCase() === "t"
202
202
 
203
203
  const getDetailPlaceholderContent = ({
204
204
  status,
@@ -278,8 +278,10 @@ export const App = () => {
278
278
  const getPullRequestDiff = useAtomSet(getPullRequestDiffAtom, { mode: "promise" })
279
279
  const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
280
280
  const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
281
- const contentWidth = Math.max(60, width ?? 100)
282
- const isWideLayout = (width ?? 100) >= 100
281
+ const terminalWidth = width ?? 100
282
+ const terminalHeight = height ?? 24
283
+ const contentWidth = Math.max(1, terminalWidth)
284
+ const isWideLayout = terminalWidth >= 100
283
285
  const splitGap = 1
284
286
  const sectionPadding = 1
285
287
  const leftPaneWidth = isWideLayout ? Math.max(44, Math.floor((contentWidth - splitGap) * 0.56)) : contentWidth
@@ -287,8 +289,8 @@ export const App = () => {
287
289
  const dividerJunctionAt = Math.max(1, leftPaneWidth)
288
290
  const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 3) : Math.max(24, contentWidth - sectionPadding * 2)
289
291
  const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
290
- const wideDetailLines = Math.max(8, (height ?? 24) - 8) // fill available vertical space
291
- const wideBodyHeight = Math.max(8, (height ?? 24) - 4)
292
+ const wideDetailLines = Math.max(8, terminalHeight - 8) // fill available vertical space
293
+ const wideBodyHeight = Math.max(8, terminalHeight - 4)
292
294
  const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
293
295
  const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
294
296
  const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -557,13 +559,12 @@ export const App = () => {
557
559
  setMergeModal(initialMergeModalState)
558
560
  setThemeModal({
559
561
  open: true,
560
- selectedIndex: Math.max(0, themeDefinitions.findIndex((theme) => theme.id === themeId)),
561
562
  initialThemeId: themeId,
562
563
  })
563
564
  }
564
565
 
565
566
  const closeThemeModal = (confirm: boolean) => {
566
- const selectedTheme = themeDefinitions[themeModal.selectedIndex]
567
+ const selectedTheme = themeDefinitions.find((theme) => theme.id === themeId)
567
568
  if (!confirm) {
568
569
  setThemeId(themeModal.initialThemeId)
569
570
  } else if (selectedTheme) {
@@ -573,11 +574,11 @@ export const App = () => {
573
574
  }
574
575
 
575
576
  const moveThemeSelection = (delta: number) => {
576
- const selectedIndex = Math.max(0, Math.min(themeDefinitions.length - 1, themeModal.selectedIndex + delta))
577
- if (selectedIndex === themeModal.selectedIndex) return
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
578
580
  const theme = themeDefinitions[selectedIndex]
579
581
  if (theme && theme.id !== themeId) setThemeId(theme.id)
580
- setThemeModal((current) => ({ ...current, selectedIndex }))
581
582
  }
582
583
 
583
584
  const openLabelModal = () => {
@@ -1032,7 +1033,7 @@ export const App = () => {
1032
1033
  }
1033
1034
  }
1034
1035
 
1035
- if (isShiftT(key)) {
1036
+ if (isThemeKey(key)) {
1036
1037
  openThemeModal()
1037
1038
  return
1038
1039
  }
@@ -1183,7 +1184,7 @@ export const App = () => {
1183
1184
  })
1184
1185
 
1185
1186
  const fullscreenContentWidth = Math.max(24, contentWidth - 2)
1186
- const fullscreenBodyLines = Math.max(8, (height ?? 24) - 8)
1187
+ const fullscreenBodyLines = Math.max(8, terminalHeight - 8)
1187
1188
  const wideFullscreenDetailScrollable = getDetailsPaneHeight({
1188
1189
  pullRequest: selectedPullRequest,
1189
1190
  contentWidth: fullscreenContentWidth,
@@ -1214,20 +1215,20 @@ export const App = () => {
1214
1215
 
1215
1216
  const longestLabelName = labelModal.availableLabels.reduce((max, label) => Math.max(max, label.name.length), 0)
1216
1217
  const labelModalWidth = Math.min(Math.max(42, longestLabelName + 16), 56, contentWidth - 4)
1217
- const labelModalHeight = Math.min(20, (height ?? 24) - 4)
1218
+ const labelModalHeight = Math.min(20, terminalHeight - 4)
1218
1219
  const labelModalLeft = Math.floor((contentWidth - labelModalWidth) / 2)
1219
- const labelModalTop = Math.floor(((height ?? 24) - labelModalHeight) / 2)
1220
+ const labelModalTop = Math.floor((terminalHeight - labelModalHeight) / 2)
1220
1221
  const mergeModalWidth = Math.min(68, Math.max(46, contentWidth - 12))
1221
- const mergeModalHeight = Math.min(16, (height ?? 24) - 4)
1222
+ const mergeModalHeight = Math.min(16, terminalHeight - 4)
1222
1223
  const mergeModalLeft = Math.floor((contentWidth - mergeModalWidth) / 2)
1223
- const mergeModalTop = Math.floor(((height ?? 24) - mergeModalHeight) / 2)
1224
+ const mergeModalTop = Math.floor((terminalHeight - mergeModalHeight) / 2)
1224
1225
  const themeModalWidth = Math.min(58, Math.max(38, contentWidth - 12))
1225
- const themeModalHeight = Math.min(10, (height ?? 24) - 4)
1226
+ const themeModalHeight = Math.min(16, terminalHeight - 4)
1226
1227
  const themeModalLeft = Math.floor((contentWidth - themeModalWidth) / 2)
1227
- const themeModalTop = Math.floor(((height ?? 24) - themeModalHeight) / 2)
1228
+ const themeModalTop = Math.floor((terminalHeight - themeModalHeight) / 2)
1228
1229
 
1229
1230
  return (
1230
- <box flexGrow={1} flexDirection="column" backgroundColor={colors.background}>
1231
+ <box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
1231
1232
  <box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.panel}>
1232
1233
  <PlainLine text={headerLine} fg={colors.muted} bold />
1233
1234
  </box>
@@ -1300,7 +1301,7 @@ export const App = () => {
1300
1301
  </scrollbox>
1301
1302
  </box>
1302
1303
  ) : (
1303
- <>
1304
+ <box height={wideBodyHeight} flexDirection="column">
1304
1305
  <DetailsPane pullRequest={selectedPullRequest} contentWidth={rightContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} />
1305
1306
  <Divider width={contentWidth} />
1306
1307
  <box flexGrow={1} flexDirection="column">
@@ -1310,7 +1311,7 @@ export const App = () => {
1310
1311
  </box>
1311
1312
  </scrollbox>
1312
1313
  </box>
1313
- </>
1314
+ </box>
1314
1315
  )}
1315
1316
 
1316
1317
  {isWideLayout && !detailFullView && !diffFullView && !isInitialLoading ? (
@@ -1318,7 +1319,7 @@ export const App = () => {
1318
1319
  ) : (
1319
1320
  <Divider width={contentWidth} />
1320
1321
  )}
1321
- <box paddingLeft={1} paddingRight={1} backgroundColor={colors.panel}>
1322
+ <box paddingLeft={1} paddingRight={1} backgroundColor={colors.footer}>
1322
1323
  {footerNotice ? (
1323
1324
  <PlainLine text={footerNotice} fg={colors.count} />
1324
1325
  ) : (
@@ -86,7 +86,7 @@ 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>
89
+ <span fg={colors.count}>t</span>
90
90
  <span fg={colors.muted}> theme </span>
91
91
  {showFilterClear ? (
92
92
  <>
package/src/ui/colors.ts CHANGED
@@ -1,8 +1,9 @@
1
- export type ThemeId = "ghui" | "tokyo-night" | "opencode"
1
+ export type ThemeId = "ghui" | "tokyo-night" | "catppuccin" | "rose-pine" | "gruvbox" | "nord" | "dracula" | "opencode"
2
2
 
3
3
  export interface ColorPalette {
4
4
  readonly background: string
5
5
  readonly panel: string
6
+ readonly footer: string
6
7
  readonly modalBackground: string
7
8
  readonly text: string
8
9
  readonly muted: string
@@ -50,6 +51,7 @@ export interface ThemeDefinition {
50
51
  const ghuiColors: ColorPalette = {
51
52
  background: "#111018",
52
53
  panel: "#161923",
54
+ footer: "#1d2430",
53
55
  modalBackground: "#1a1a2e",
54
56
  text: "#ede7da",
55
57
  muted: "#9f9788",
@@ -90,6 +92,7 @@ const ghuiColors: ColorPalette = {
90
92
  const tokyoNightColors: ColorPalette = {
91
93
  background: "#1a1b26",
92
94
  panel: "#16161e",
95
+ footer: "#24283b",
93
96
  modalBackground: "#24283b",
94
97
  text: "#c0caf5",
95
98
  muted: "#787c99",
@@ -130,6 +133,7 @@ const tokyoNightColors: ColorPalette = {
130
133
  const opencodeColors: ColorPalette = {
131
134
  background: "#0a0a0a",
132
135
  panel: "#141414",
136
+ footer: "#1e1e1e",
133
137
  modalBackground: "#1e1e1e",
134
138
  text: "#eeeeee",
135
139
  muted: "#808080",
@@ -167,9 +171,219 @@ const opencodeColors: ColorPalette = {
167
171
  },
168
172
  }
169
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
+
170
379
  export const themeDefinitions: readonly ThemeDefinition[] = [
171
380
  { id: "ghui", name: "GHUI", description: "Warm parchment accents on a deep slate background", colors: ghuiColors },
172
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 },
173
387
  { id: "opencode", name: "OpenCode", description: "Charcoal panels with peach, violet, and blue highlights", colors: opencodeColors },
174
388
  ] as const
175
389
 
@@ -180,6 +394,7 @@ export const colors: ColorPalette = { ...ghuiColors }
180
394
  export const getThemeDefinition = (id: ThemeId) => themeDefinitions.find((theme) => theme.id === id) ?? themeDefinitions[0]!
181
395
 
182
396
  export const setActiveTheme = (id: ThemeId) => {
397
+ if (activeTheme.id === id) return
183
398
  activeTheme = getThemeDefinition(id)
184
399
  Object.assign(colors, activeTheme.colors)
185
400
  }
package/src/ui/modals.tsx CHANGED
@@ -27,7 +27,6 @@ export interface MergeModalState {
27
27
 
28
28
  export interface ThemeModalState {
29
29
  readonly open: boolean
30
- readonly selectedIndex: number
31
30
  readonly initialThemeId: ThemeId
32
31
  }
33
32
 
@@ -53,7 +52,6 @@ export const initialMergeModalState: MergeModalState = {
53
52
 
54
53
  export const initialThemeModalState: ThemeModalState = {
55
54
  open: false,
56
- selectedIndex: 0,
57
55
  initialThemeId: "ghui",
58
56
  }
59
57
 
@@ -279,7 +277,8 @@ export const ThemeModal = ({
279
277
  const contentWidth = Math.max(14, innerWidth - 2)
280
278
  const rowWidth = innerWidth
281
279
  const maxVisible = Math.max(1, modalHeight - 7)
282
- const selectedIndex = Math.max(0, Math.min(state.selectedIndex, themeDefinitions.length - 1))
280
+ const activeIndex = themeDefinitions.findIndex((theme) => theme.id === activeThemeId)
281
+ const selectedIndex = Math.max(0, activeIndex)
283
282
  const selectedTheme = themeDefinitions[selectedIndex]!
284
283
  const scrollStart = Math.min(
285
284
  Math.max(0, themeDefinitions.length - maxVisible),