@kitlangton/ghui 0.1.5 → 0.1.7
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 +112 -36
- package/src/index.tsx +8 -1
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseColor, SyntaxStyle, TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
|
-
import { useKeyboard, useTerminalDimensions } from "@opentui/react"
|
|
3
|
+
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
4
4
|
import { Cause, Effect, Schedule } from "effect"
|
|
5
5
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
6
6
|
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
@@ -435,23 +435,104 @@ const pullRequestDiffKey = (pullRequest: PullRequestItem) => `${pullRequest.repo
|
|
|
435
435
|
|
|
436
436
|
const diffStatText = (pullRequest: PullRequestItem) => {
|
|
437
437
|
const files = pullRequest.changedFiles === 1 ? "1 file" : `${pullRequest.changedFiles} files`
|
|
438
|
-
return
|
|
438
|
+
return [
|
|
439
|
+
pullRequest.additions > 0 ? `+${pullRequest.additions}` : null,
|
|
440
|
+
pullRequest.deletions > 0 ? `-${pullRequest.deletions}` : null,
|
|
441
|
+
files,
|
|
442
|
+
].filter((part): part is string => part !== null).join(" ")
|
|
439
443
|
}
|
|
440
444
|
|
|
441
|
-
const
|
|
445
|
+
const DiffStats = ({ pullRequest }: { pullRequest: PullRequestItem }) => {
|
|
446
|
+
const files = pullRequest.changedFiles === 1 ? "1 file" : `${pullRequest.changedFiles} files`
|
|
447
|
+
type Part = { key: string; text: string; color: string }
|
|
448
|
+
const rawParts: Array<Part | null> = [
|
|
449
|
+
pullRequest.additions > 0 ? { key: "additions", text: `+${pullRequest.additions}`, color: colors.status.passing } : null,
|
|
450
|
+
pullRequest.deletions > 0 ? { key: "deletions", text: `-${pullRequest.deletions}`, color: colors.status.failing } : null,
|
|
451
|
+
{ key: "files", text: files, color: colors.muted },
|
|
452
|
+
]
|
|
453
|
+
const parts = rawParts.filter((part): part is Part => part !== null)
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<>
|
|
457
|
+
{parts.map((part, index) => (
|
|
458
|
+
<Fragment key={part.key}>
|
|
459
|
+
{index > 0 ? <span fg={colors.muted}> </span> : null}
|
|
460
|
+
<span fg={part.color}>{part.text}</span>
|
|
461
|
+
</Fragment>
|
|
462
|
+
))}
|
|
463
|
+
</>
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const estimatedWrappedLineCount = (text: string, width: number, wrapMode: "none" | "word") => {
|
|
468
|
+
if (wrapMode === "none") return 1
|
|
469
|
+
return Math.max(1, Math.ceil(Bun.stringWidth(text) / Math.max(1, width)))
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const patchLineNumberGutterWidth = (lines: readonly string[]) => {
|
|
473
|
+
let maxLineNumber = 1
|
|
474
|
+
let hasSigns = false
|
|
475
|
+
let oldLine = 0
|
|
476
|
+
let newLine = 0
|
|
477
|
+
|
|
478
|
+
for (const line of lines) {
|
|
479
|
+
const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/)
|
|
480
|
+
if (hunk) {
|
|
481
|
+
oldLine = Number(hunk[1])
|
|
482
|
+
newLine = Number(hunk[2])
|
|
483
|
+
maxLineNumber = Math.max(maxLineNumber, oldLine, newLine)
|
|
484
|
+
continue
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const firstChar = line[0]
|
|
488
|
+
if (firstChar === "-") {
|
|
489
|
+
hasSigns = true
|
|
490
|
+
maxLineNumber = Math.max(maxLineNumber, oldLine)
|
|
491
|
+
oldLine++
|
|
492
|
+
} else if (firstChar === "+") {
|
|
493
|
+
hasSigns = true
|
|
494
|
+
maxLineNumber = Math.max(maxLineNumber, newLine)
|
|
495
|
+
newLine++
|
|
496
|
+
} else if (firstChar === " ") {
|
|
497
|
+
maxLineNumber = Math.max(maxLineNumber, oldLine, newLine)
|
|
498
|
+
oldLine++
|
|
499
|
+
newLine++
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const digits = Math.floor(Math.log10(maxLineNumber)) + 1
|
|
504
|
+
return Math.max(3, digits + 2) + (hasSigns ? 2 : 0)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const patchRenderableLineCount = (patch: string, view: "unified" | "split", wrapMode: "none" | "word", width: number) => {
|
|
508
|
+
const lines = patch.split("\n")
|
|
509
|
+
const lineNumberGutterWidth = patchLineNumberGutterWidth(lines)
|
|
510
|
+
const splitPaneWidth = Math.max(1, Math.floor(width / 2) - lineNumberGutterWidth)
|
|
511
|
+
const unifiedPaneWidth = Math.max(1, width - lineNumberGutterWidth)
|
|
512
|
+
const contentWidth = view === "split" ? splitPaneWidth : unifiedPaneWidth
|
|
442
513
|
let count = 0
|
|
443
514
|
let inHunk = false
|
|
444
|
-
let deletions =
|
|
445
|
-
let additions =
|
|
515
|
+
let deletions: number[] = []
|
|
516
|
+
let additions: number[] = []
|
|
446
517
|
|
|
447
518
|
const flushChangeBlock = () => {
|
|
448
|
-
if (deletions === 0 && additions === 0) return
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
519
|
+
if (deletions.length === 0 && additions.length === 0) return
|
|
520
|
+
if (view === "split") {
|
|
521
|
+
const rows = Math.max(deletions.length, additions.length)
|
|
522
|
+
for (let index = 0; index < rows; index++) {
|
|
523
|
+
const deletionCount = index < deletions.length ? deletions[index]! : 1
|
|
524
|
+
const additionCount = index < additions.length ? additions[index]! : 1
|
|
525
|
+
count += Math.max(deletionCount, additionCount)
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
for (const deletion of deletions) count += deletion
|
|
529
|
+
for (const addition of additions) count += addition
|
|
530
|
+
}
|
|
531
|
+
deletions = []
|
|
532
|
+
additions = []
|
|
452
533
|
}
|
|
453
534
|
|
|
454
|
-
for (const line of
|
|
535
|
+
for (const line of lines) {
|
|
455
536
|
if (line.startsWith("@@")) {
|
|
456
537
|
flushChangeBlock()
|
|
457
538
|
inHunk = true
|
|
@@ -464,18 +545,18 @@ const patchRenderableLineCount = (patch: string, view: "unified" | "split") => {
|
|
|
464
545
|
if (firstChar === "\\") continue
|
|
465
546
|
|
|
466
547
|
if (firstChar === "-") {
|
|
467
|
-
deletions
|
|
548
|
+
deletions.push(estimatedWrappedLineCount(line.slice(1), contentWidth, wrapMode))
|
|
468
549
|
continue
|
|
469
550
|
}
|
|
470
551
|
|
|
471
552
|
if (firstChar === "+") {
|
|
472
|
-
additions
|
|
553
|
+
additions.push(estimatedWrappedLineCount(line.slice(1), contentWidth, wrapMode))
|
|
473
554
|
continue
|
|
474
555
|
}
|
|
475
556
|
|
|
476
557
|
if (firstChar === " ") {
|
|
477
558
|
flushChangeBlock()
|
|
478
|
-
count
|
|
559
|
+
count += estimatedWrappedLineCount(line.slice(1), contentWidth, wrapMode)
|
|
479
560
|
}
|
|
480
561
|
}
|
|
481
562
|
|
|
@@ -1036,7 +1117,7 @@ const DetailHeader = ({
|
|
|
1036
1117
|
const review = reviewLabel(pullRequest)
|
|
1037
1118
|
const checks = pullRequest.checkSummary?.replace(/^checks\s+/, "")
|
|
1038
1119
|
const statusParts = [review, checks].filter((part): part is string => Boolean(part))
|
|
1039
|
-
const rightSide = statusParts.length > 0 ? `${statusParts.join(" ")}
|
|
1120
|
+
const rightSide = statusParts.length > 0 ? `${statusParts.join(" ")} ${opened}` : opened
|
|
1040
1121
|
const leftWidth = 1 + number.length + 1 + repo.length
|
|
1041
1122
|
const gap = Math.max(2, contentWidth - leftWidth - rightSide.length)
|
|
1042
1123
|
|
|
@@ -1048,7 +1129,7 @@ const DetailHeader = ({
|
|
|
1048
1129
|
{review ? <span fg={statusColor(pullRequest.reviewStatus)}>{review}</span> : null}
|
|
1049
1130
|
{review && checks ? <span fg={colors.muted}> </span> : null}
|
|
1050
1131
|
{checks ? <span fg={statusColor(pullRequest.checkStatus)}>{checks}</span> : null}
|
|
1051
|
-
{statusParts.length > 0 ? <span fg={colors.muted}>
|
|
1132
|
+
{statusParts.length > 0 ? <span fg={colors.muted}> </span> : null}
|
|
1052
1133
|
<span fg={colors.muted}>{opened}</span>
|
|
1053
1134
|
</TextLine>
|
|
1054
1135
|
)
|
|
@@ -1070,10 +1151,7 @@ const DetailHeader = ({
|
|
|
1070
1151
|
{showStats ? (
|
|
1071
1152
|
<>
|
|
1072
1153
|
<span fg={colors.muted}>{" ".repeat(statsGap)}</span>
|
|
1073
|
-
<
|
|
1074
|
-
<span fg={colors.muted}> </span>
|
|
1075
|
-
<span fg={colors.status.failing}>-{pullRequest.deletions}</span>
|
|
1076
|
-
<span fg={colors.muted}> {pullRequest.changedFiles === 1 ? "1 file" : `${pullRequest.changedFiles} files`}</span>
|
|
1154
|
+
<DiffStats pullRequest={pullRequest} />
|
|
1077
1155
|
</>
|
|
1078
1156
|
) : null}
|
|
1079
1157
|
</TextLine>
|
|
@@ -1241,6 +1319,14 @@ const PullRequestDiffPane = ({
|
|
|
1241
1319
|
loadingIndicator: string
|
|
1242
1320
|
scrollRef: React.Ref<ScrollBoxRenderable>
|
|
1243
1321
|
}) => {
|
|
1322
|
+
const readyFiles = diffState?.status === "ready" ? diffState.files : []
|
|
1323
|
+
const safeIndex = readyFiles.length > 0 ? Math.max(0, Math.min(fileIndex, readyFiles.length - 1)) : 0
|
|
1324
|
+
const file = readyFiles[safeIndex] ?? null
|
|
1325
|
+
const diffHeight = useMemo(
|
|
1326
|
+
() => file ? patchRenderableLineCount(file.patch, view, wrapMode, paneWidth) : 1,
|
|
1327
|
+
[file?.patch, view, wrapMode, paneWidth],
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1244
1330
|
if (!pullRequest) {
|
|
1245
1331
|
return <LoadingPane content={{ title: "No pull request selected", hint: "Press esc to go back" }} width={paneWidth} height={height} />
|
|
1246
1332
|
}
|
|
@@ -1258,10 +1344,7 @@ const PullRequestDiffPane = ({
|
|
|
1258
1344
|
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
1259
1345
|
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
1260
1346
|
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
1261
|
-
<
|
|
1262
|
-
<span fg={colors.muted}> </span>
|
|
1263
|
-
<span fg={colors.status.failing}>-{pullRequest.deletions}</span>
|
|
1264
|
-
<span fg={colors.muted}> {pullRequest.changedFiles === 1 ? "1 file" : `${pullRequest.changedFiles} files`}</span>
|
|
1347
|
+
<DiffStats pullRequest={pullRequest} />
|
|
1265
1348
|
</TextLine>
|
|
1266
1349
|
</box>
|
|
1267
1350
|
<Divider width={paneWidth} />
|
|
@@ -1282,15 +1365,12 @@ const PullRequestDiffPane = ({
|
|
|
1282
1365
|
)
|
|
1283
1366
|
}
|
|
1284
1367
|
|
|
1285
|
-
if (
|
|
1368
|
+
if (readyFiles.length === 0 || !file) {
|
|
1286
1369
|
return <LoadingPane content={{ title: "No diff", hint: "This PR has no patch contents" }} width={paneWidth} height={height} />
|
|
1287
1370
|
}
|
|
1288
1371
|
|
|
1289
|
-
const
|
|
1290
|
-
const file = diffState.files[safeIndex]!
|
|
1291
|
-
const fileCounter = `${safeIndex + 1}/${diffState.files.length}`
|
|
1372
|
+
const fileCounter = `${safeIndex + 1}/${readyFiles.length}`
|
|
1292
1373
|
const fileNameWidth = Math.max(8, headerWidth - fileCounter.length - 2)
|
|
1293
|
-
const diffHeight = patchRenderableLineCount(file.patch, view)
|
|
1294
1374
|
|
|
1295
1375
|
return (
|
|
1296
1376
|
<box height={height} flexDirection="column">
|
|
@@ -1299,10 +1379,7 @@ const PullRequestDiffPane = ({
|
|
|
1299
1379
|
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
1300
1380
|
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
1301
1381
|
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
1302
|
-
<
|
|
1303
|
-
<span fg={colors.muted}> </span>
|
|
1304
|
-
<span fg={colors.status.failing}>-{pullRequest.deletions}</span>
|
|
1305
|
-
<span fg={colors.muted}> {pullRequest.changedFiles === 1 ? "1 file" : `${pullRequest.changedFiles} files`}</span>
|
|
1382
|
+
<DiffStats pullRequest={pullRequest} />
|
|
1306
1383
|
</TextLine>
|
|
1307
1384
|
</box>
|
|
1308
1385
|
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
@@ -1451,6 +1528,7 @@ const LabelModal = ({
|
|
|
1451
1528
|
}
|
|
1452
1529
|
|
|
1453
1530
|
export const App = () => {
|
|
1531
|
+
const renderer = useRenderer()
|
|
1454
1532
|
const { width, height } = useTerminalDimensions()
|
|
1455
1533
|
const pullRequestResult = useAtomValue(pullRequestsAtom)
|
|
1456
1534
|
const refreshPullRequestsAtom = useAtomRefresh(pullRequestsAtom)
|
|
@@ -1743,10 +1821,8 @@ export const App = () => {
|
|
|
1743
1821
|
setLabelModal(initialLabelModalState)
|
|
1744
1822
|
return
|
|
1745
1823
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
}
|
|
1749
|
-
process.exit(0)
|
|
1824
|
+
renderer.destroy()
|
|
1825
|
+
return
|
|
1750
1826
|
}
|
|
1751
1827
|
|
|
1752
1828
|
// Label modal takes priority over everything else
|
package/src/index.tsx
CHANGED
|
@@ -5,7 +5,14 @@ import { RegistryProvider } from "@effect/atom-react"
|
|
|
5
5
|
import { createRoot } from "@opentui/react"
|
|
6
6
|
import { App } from "./App.js"
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
process.env.OTUI_USE_ALTERNATE_SCREEN = "true"
|
|
9
|
+
|
|
10
|
+
const renderer = await createCliRenderer({
|
|
11
|
+
exitOnCtrlC: false,
|
|
12
|
+
screenMode: "alternate-screen",
|
|
13
|
+
externalOutputMode: "passthrough",
|
|
14
|
+
onDestroy: () => process.exit(0),
|
|
15
|
+
})
|
|
9
16
|
|
|
10
17
|
createRoot(renderer).render(
|
|
11
18
|
<RegistryProvider>
|