@kitlangton/ghui 0.2.1 → 0.3.2

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.
Files changed (53) hide show
  1. package/README.md +1 -4
  2. package/bin/ghui.js +6 -1
  3. package/dist/index.js +9419 -0
  4. package/package.json +7 -4
  5. package/src/App.tsx +0 -2604
  6. package/src/appCommands.ts +0 -384
  7. package/src/commands.ts +0 -73
  8. package/src/config.ts +0 -28
  9. package/src/date.ts +0 -20
  10. package/src/domain.ts +0 -139
  11. package/src/errors.ts +0 -10
  12. package/src/index.tsx +0 -50
  13. package/src/keyboard/opentuiAdapter.ts +0 -43
  14. package/src/keymap/all.ts +0 -103
  15. package/src/keymap/closeModal.ts +0 -13
  16. package/src/keymap/commandPalette.ts +0 -16
  17. package/src/keymap/commentModal.ts +0 -73
  18. package/src/keymap/commentThreadModal.ts +0 -24
  19. package/src/keymap/detailView.ts +0 -30
  20. package/src/keymap/diffView.ts +0 -91
  21. package/src/keymap/filterMode.ts +0 -13
  22. package/src/keymap/helpers.ts +0 -24
  23. package/src/keymap/labelModal.ts +0 -16
  24. package/src/keymap/listNav.ts +0 -119
  25. package/src/keymap/mergeModal.ts +0 -23
  26. package/src/keymap/openRepositoryModal.ts +0 -13
  27. package/src/keymap/themeModal.ts +0 -49
  28. package/src/mergeActions.ts +0 -88
  29. package/src/observability.ts +0 -46
  30. package/src/pullRequestCache.ts +0 -19
  31. package/src/pullRequestViews.ts +0 -45
  32. package/src/services/BrowserOpener.ts +0 -22
  33. package/src/services/Clipboard.ts +0 -46
  34. package/src/services/CommandRunner.ts +0 -91
  35. package/src/services/GitHubService.ts +0 -741
  36. package/src/services/MockGitHubService.ts +0 -181
  37. package/src/themeStore.ts +0 -60
  38. package/src/ui/CommandPalette.tsx +0 -216
  39. package/src/ui/DetailsPane.tsx +0 -656
  40. package/src/ui/FooterHints.tsx +0 -90
  41. package/src/ui/LoadingLogo.tsx +0 -75
  42. package/src/ui/PullRequestDiffPane.tsx +0 -248
  43. package/src/ui/PullRequestList.tsx +0 -210
  44. package/src/ui/colors.ts +0 -814
  45. package/src/ui/commentEditor.ts +0 -126
  46. package/src/ui/comments.tsx +0 -143
  47. package/src/ui/diff.ts +0 -650
  48. package/src/ui/diffStats.tsx +0 -25
  49. package/src/ui/modals.tsx +0 -614
  50. package/src/ui/primitives.tsx +0 -205
  51. package/src/ui/pullRequests.ts +0 -106
  52. package/src/ui/singleLineInput.ts +0 -26
  53. package/src/ui/spinner.ts +0 -1
@@ -1,205 +0,0 @@
1
- import { TextAttributes } from "@opentui/core"
2
- import type React from "react"
3
- import { colors } from "./colors.js"
4
-
5
- export const fitCell = (text: string, width: number, align: "left" | "right" = "left") => {
6
- const trimmed = text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text
7
- return align === "right" ? trimmed.padStart(width, " ") : trimmed.padEnd(width, " ")
8
- }
9
-
10
- export const trimCell = (text: string, width: number) => text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text
11
-
12
- export const centerCell = (text: string, width: number) => {
13
- const trimmed = text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text
14
- const left = Math.floor((width - trimmed.length) / 2)
15
- return `${" ".repeat(Math.max(0, left))}${trimmed}`.padEnd(width, " ")
16
- }
17
-
18
- export const PlainLine = ({ text, fg = colors.text, bold = false }: { text: string; fg?: string; bold?: boolean }) => (
19
- <box height={1}>
20
- {bold ? (
21
- <text wrapMode="none" truncate fg={fg} attributes={TextAttributes.BOLD}>
22
- {text}
23
- </text>
24
- ) : (
25
- <text wrapMode="none" truncate fg={fg}>
26
- {text}
27
- </text>
28
- )}
29
- </box>
30
- )
31
-
32
- export const TextLine = ({ children, fg = colors.text, bg, width }: { children: React.ReactNode; fg?: string; bg?: string | undefined; width?: number }) => (
33
- <box height={1} {...(width === undefined ? {} : { width })}>
34
- {bg ? (
35
- <text wrapMode="none" truncate fg={fg} bg={bg}>
36
- {children}
37
- </text>
38
- ) : (
39
- <text wrapMode="none" truncate fg={fg}>
40
- {children}
41
- </text>
42
- )}
43
- </box>
44
- )
45
-
46
- export const SectionTitle = ({ title }: { title: string }) => (
47
- <TextLine>
48
- <span fg={colors.accent} attributes={TextAttributes.BOLD}>
49
- {title}
50
- </span>
51
- </TextLine>
52
- )
53
-
54
- export const Filler = ({ rows, prefix }: { rows: number; prefix: string }) =>
55
- <>{Array.from({ length: rows }, (_, index) => <box key={`${prefix}-${index}`} height={1} />)}</>
56
-
57
- export const PaddedRow = ({ children, backgroundColor }: { children: React.ReactNode; backgroundColor?: string }) => (
58
- <box height={1} paddingLeft={1} paddingRight={1} {...(backgroundColor ? { backgroundColor } : {})}>{children}</box>
59
- )
60
-
61
- export const Divider = ({ width, junctionAt, junctionChar }: { width: number; junctionAt?: number; junctionChar?: string }) => {
62
- if (junctionAt === undefined || junctionChar === undefined || junctionAt < 0 || junctionAt >= width) {
63
- return <PlainLine text={"─".repeat(Math.max(1, width))} fg={colors.separator} />
64
- }
65
-
66
- return <PlainLine text={`${"─".repeat(junctionAt)}${junctionChar}${"─".repeat(Math.max(0, width - junctionAt - 1))}`} fg={colors.separator} />
67
- }
68
-
69
- export const SeparatorColumn = ({ height, junctionRows }: { height: number; junctionRows?: readonly number[] }) => {
70
- const junctions = new Set(junctionRows)
71
- return (
72
- <box width={1} height={height} flexDirection="column">
73
- {Array.from({ length: height }, (_, index) => (
74
- <PlainLine key={index} text={junctions.has(index) ? "├" : "│"} fg={colors.separator} />
75
- ))}
76
- </box>
77
- )
78
- }
79
-
80
- export type StandardModalDims = {
81
- readonly innerWidth: number
82
- readonly contentWidth: number
83
- readonly bodyHeight: number
84
- readonly rowWidth: number
85
- }
86
-
87
- export const standardModalDims = (modalWidth: number, modalHeight: number): StandardModalDims => {
88
- const innerWidth = Math.max(16, modalWidth - 2)
89
- const contentWidth = Math.max(14, innerWidth - 2)
90
- const bodyHeight = Math.max(1, modalHeight - 7)
91
- return { innerWidth, contentWidth, bodyHeight, rowWidth: innerWidth }
92
- }
93
-
94
- export type HintItem = {
95
- readonly key: string
96
- readonly label: string
97
- readonly when?: boolean
98
- readonly keyFg?: string
99
- }
100
-
101
- export const HintRow = ({ items }: { items: readonly HintItem[] }) => {
102
- const visible = items.filter((item) => item.when !== false)
103
- return (
104
- <TextLine>
105
- {visible.flatMap((item, index) => [
106
- <span key={`k${index}`} fg={item.keyFg ?? colors.count}>{item.key}</span>,
107
- <span key={`l${index}`} fg={colors.muted}>{` ${item.label}${index < visible.length - 1 ? " " : ""}`}</span>,
108
- ])}
109
- </TextLine>
110
- )
111
- }
112
-
113
- export const StandardModal = ({
114
- left,
115
- top,
116
- width,
117
- height,
118
- title,
119
- titleFg = colors.accent,
120
- headerRight,
121
- subtitle,
122
- footer,
123
- bodyPadding = 0,
124
- children,
125
- }: {
126
- left: number
127
- top: number
128
- width: number
129
- height: number
130
- title: string
131
- titleFg?: string
132
- headerRight?: { readonly text: string; readonly pending?: boolean }
133
- subtitle: React.ReactNode
134
- footer: React.ReactNode
135
- bodyPadding?: number
136
- children: React.ReactNode
137
- }) => {
138
- const { innerWidth, contentWidth, bodyHeight } = standardModalDims(width, height)
139
- const rightText = headerRight?.text ?? ""
140
- const headerGap = Math.max(1, contentWidth - title.length - rightText.length)
141
- return (
142
- <ModalFrame left={left} top={top} width={width} height={height} junctionRows={[2, height - 4]}>
143
- <PaddedRow>
144
- <TextLine>
145
- <span fg={titleFg} attributes={TextAttributes.BOLD}>{title}</span>
146
- {headerRight ? (
147
- <>
148
- <span fg={colors.muted}>{" ".repeat(headerGap)}</span>
149
- <span fg={headerRight.pending ? colors.status.pending : colors.muted}>{headerRight.text}</span>
150
- </>
151
- ) : null}
152
- </TextLine>
153
- </PaddedRow>
154
- <PaddedRow>{subtitle}</PaddedRow>
155
- <Divider width={innerWidth} />
156
- <box height={bodyHeight} flexDirection="column" paddingLeft={bodyPadding} paddingRight={bodyPadding}>{children}</box>
157
- <Divider width={innerWidth} />
158
- <PaddedRow>{footer}</PaddedRow>
159
- </ModalFrame>
160
- )
161
- }
162
-
163
- export const ModalFrame = ({
164
- children,
165
- left,
166
- top,
167
- width,
168
- height,
169
- junctionRows = [],
170
- topJunctionColumns = [],
171
- backgroundColor = colors.modalBackground,
172
- }: {
173
- children: React.ReactNode
174
- left: number
175
- top: number
176
- width: number
177
- height: number
178
- junctionRows?: readonly number[]
179
- topJunctionColumns?: readonly number[]
180
- backgroundColor?: string
181
- }) => {
182
- const innerWidth = Math.max(1, width - 2)
183
- const innerHeight = Math.max(1, height - 2)
184
- const junctions = new Set(junctionRows)
185
- const topJunctions = new Set(topJunctionColumns)
186
- const topBorder = Array.from({ length: innerWidth }, (_, index) => topJunctions.has(index) ? "┬" : "─").join("")
187
-
188
- return (
189
- <box position="absolute" left={left} top={top} width={width} height={height} flexDirection="column" backgroundColor={backgroundColor}>
190
- <PlainLine text={`┌${topBorder}┐`} fg={colors.separator} />
191
- <box height={innerHeight} flexDirection="row">
192
- <box width={1} height={innerHeight} flexDirection="column">
193
- {Array.from({ length: innerHeight }, (_, index) => <PlainLine key={index} text={junctions.has(index) ? "├" : "│"} fg={colors.separator} />)}
194
- </box>
195
- <box width={innerWidth} height={innerHeight} flexDirection="column">
196
- {children}
197
- </box>
198
- <box width={1} height={innerHeight} flexDirection="column">
199
- {Array.from({ length: innerHeight }, (_, index) => <PlainLine key={index} text={junctions.has(index) ? "┤" : "│"} fg={colors.separator} />)}
200
- </box>
201
- </box>
202
- <PlainLine text={`└${"─".repeat(innerWidth)}┘`} fg={colors.separator} />
203
- </box>
204
- )
205
- }
@@ -1,106 +0,0 @@
1
- import type { PullRequestItem, PullRequestLabel, ReviewStatus } from "../domain.js"
2
- import { colors } from "./colors.js"
3
-
4
- export const shortRepoName = (repository: string) => repository.split("/")[1] ?? repository
5
-
6
- export const repoColor = (repository: string) => colors.repos[shortRepoName(repository) as keyof typeof colors.repos] ?? colors.repos.default
7
-
8
- const REVIEW_LABEL: Partial<Record<ReviewStatus, string>> = {
9
- draft: "draft",
10
- approved: "approved",
11
- changes: "changes",
12
- review: "review",
13
- }
14
-
15
- export const reviewLabel = (pullRequest: PullRequestItem) => REVIEW_LABEL[pullRequest.reviewStatus] ?? null
16
-
17
- export const checkLabel = (pullRequest: PullRequestItem) => pullRequest.checkSummary
18
-
19
- export const statusColor = (status: PullRequestItem["reviewStatus"] | PullRequestItem["checkStatus"]) => colors.status[status]
20
-
21
- const REVIEW_ICON: Record<ReviewStatus, string> = {
22
- draft: "◌",
23
- approved: "✓",
24
- changes: "!",
25
- review: "◐",
26
- none: "·",
27
- }
28
-
29
- export const reviewIcon = (pullRequest: PullRequestItem) => {
30
- if (pullRequest.state === "merged") return "✓"
31
- if (pullRequest.state === "closed") return "×"
32
- if (pullRequest.autoMergeEnabled) return "↻"
33
- return REVIEW_ICON[pullRequest.reviewStatus]
34
- }
35
-
36
- export interface PullRequestRowDisplay {
37
- readonly indicatorFg: string
38
- readonly rowFg: string
39
- readonly numberFg: string
40
- readonly checkFg: string
41
- readonly checkText: string
42
- }
43
-
44
- export const pullRequestRowDisplay = (pullRequest: PullRequestItem, selected: boolean): PullRequestRowDisplay => {
45
- const isMerged = pullRequest.state === "merged"
46
- const isClosed = pullRequest.state === "closed"
47
- const isFinal = isMerged || isClosed
48
- const indicatorFg = isMerged ? colors.status.passing
49
- : isClosed ? colors.muted
50
- : pullRequest.autoMergeEnabled ? colors.accent
51
- : statusColor(pullRequest.reviewStatus)
52
- const checkFg = isMerged ? colors.status.passing : isClosed ? colors.muted : statusColor(pullRequest.checkStatus)
53
- const checkText = isMerged ? "merged" : isClosed ? "closed" : pullRequest.checkSummary?.replace(/^checks\s+/, "") ?? ""
54
- return {
55
- indicatorFg,
56
- rowFg: selected ? colors.selectedText : isFinal ? colors.muted : colors.text,
57
- numberFg: selected ? colors.accent : isFinal ? colors.muted : colors.count,
58
- checkFg,
59
- checkText,
60
- }
61
- }
62
-
63
- const fallbackLabelColor = (name: string) => {
64
- let hash = 0
65
- for (const char of name) {
66
- hash = (hash * 31 + char.charCodeAt(0)) >>> 0
67
- }
68
- const hue = hash % 360
69
- return `hsl(${hue} 55% 35%)`
70
- }
71
-
72
- export const labelColor = (label: PullRequestLabel) => label.color ?? fallbackLabelColor(label.name)
73
-
74
- export const labelTextColor = (color: string) => {
75
- if (color.startsWith("#") && color.length === 7) {
76
- const red = Number.parseInt(color.slice(1, 3), 16)
77
- const green = Number.parseInt(color.slice(3, 5), 16)
78
- const blue = Number.parseInt(color.slice(5, 7), 16)
79
- const luminance = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255
80
- return luminance > 0.6 ? "#111111" : "#f8fafc"
81
- }
82
- return "#f8fafc"
83
- }
84
-
85
- export const groupBy = <T,>(items: readonly T[], getKey: (item: T) => string, orderedKeys: readonly string[] = []) => {
86
- const groups = new Map<string, T[]>()
87
- for (const item of items) {
88
- const key = getKey(item)
89
- const existing = groups.get(key)
90
- if (existing) {
91
- existing.push(item)
92
- } else {
93
- groups.set(key, [item])
94
- }
95
- }
96
-
97
- const order = new Map(orderedKeys.map((key, index) => [key, index]))
98
- return [...groups.entries()].sort((left, right) => {
99
- const leftIndex = order.get(left[0])
100
- const rightIndex = order.get(right[0])
101
- if (leftIndex !== undefined && rightIndex !== undefined) return leftIndex - rightIndex
102
- if (leftIndex !== undefined) return -1
103
- if (rightIndex !== undefined) return 1
104
- return left[0].localeCompare(right[0])
105
- })
106
- }
@@ -1,26 +0,0 @@
1
- export interface SingleLineInputKey {
2
- readonly name: string
3
- readonly sequence: string
4
- readonly ctrl?: boolean
5
- readonly meta?: boolean
6
- }
7
-
8
- export const deleteLastWord = (value: string) => value.replace(/\s*\S+\s*$/, "")
9
-
10
- export const singleLineText = (text: string) => text.replace(/[\r\n]+/g, " ")
11
-
12
- export const printableKeyText = (key: Pick<SingleLineInputKey, "ctrl" | "meta" | "sequence">) => {
13
- if (key.ctrl || key.meta || key.sequence.length === 0) return null
14
- // oxlint-disable-next-line no-control-regex -- intentional: skip control characters
15
- return /^[^\u0000-\u001f\u007f]+$/.test(key.sequence) ? key.sequence : null
16
- }
17
-
18
- export const editSingleLineInput = (value: string, key: SingleLineInputKey) => {
19
- if (key.ctrl && key.name === "u") return ""
20
- if (key.ctrl && key.name === "w") return deleteLastWord(value)
21
- if (key.name === "backspace") return value.slice(0, -1)
22
- const text = printableKeyText(key)
23
- return text ? value + text : null
24
- }
25
-
26
- export const isSingleLineInputKey = (key: SingleLineInputKey) => editSingleLineInput("", key) !== null
package/src/ui/spinner.ts DELETED
@@ -1 +0,0 @@
1
- export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;