@d3lm/pr-stats 0.2.0 → 0.2.1
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/README.md +1 -1
- package/dist/tui-app.mjs +203 -39
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ The custom theme joins the Theme row cycle as a sixth entry and persists across
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
The `preset` key names the active theme, one of `default`, `green`, `blue`, `purple`, `yellow`, and `custom`. The `base` key names the built-in theme the custom colors start from and defaults to `default`, and every other key sets one custom color on top of that base. The color keys are `bg`, `border`, `text`, `muted`, `dim`, `accent`, `selectedBg`, `inputBg`, `inputFocusedBg`, `warn`, `error`, `chartBar`, `chartLine`, `chartDim`, and `heat`. The `heat` key colors the weekly heatmap and takes four colors from cool to hot. A file that sets colors without a `preset` activates the custom theme they define, and a `custom` preset without any colors falls back to its `base` theme. A typo in a key or a color fails the start with a message naming the mistake, so a broken theme never renders.
|
|
79
|
+
The `preset` key names the active theme, one of `default`, `green`, `blue`, `purple`, `yellow`, and `custom`. The `base` key names the built-in theme the custom colors start from and defaults to `default`, and every other key sets one custom color on top of that base. The color keys are `bg`, `border`, `text`, `muted`, `dim`, `accent`, `selectedBg`, `inputBg`, `inputFocusedBg`, `warn`, `error`, `success`, `chartBar`, `chartLine`, `chartDim`, and `heat`. The `heat` key colors the weekly heatmap and takes four colors from cool to hot. A file that sets colors without a `preset` activates the custom theme they define, and a `custom` preset without any colors falls back to its `base` theme. A typo in a key or a color fails the start with a message naming the mistake, so a broken theme never renders.
|
|
80
80
|
|
|
81
81
|
## Development
|
|
82
82
|
|
package/dist/tui-app.mjs
CHANGED
|
@@ -136,7 +136,7 @@ import { createRoot } from "@opentui/react";
|
|
|
136
136
|
|
|
137
137
|
// tui/App.tsx
|
|
138
138
|
import { useKeyboard, useTerminalDimensions as useTerminalDimensions3 } from "@opentui/react";
|
|
139
|
-
import { useReducer, useRef as useRef5, useState as useState4 } from "react";
|
|
139
|
+
import { useEffect as useEffect6, useReducer, useRef as useRef5, useState as useState4 } from "react";
|
|
140
140
|
|
|
141
141
|
// settings.ts
|
|
142
142
|
import { readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
|
|
@@ -307,6 +307,9 @@ function loadSettings() {
|
|
|
307
307
|
if (settings.noCache !== void 0 && typeof settings.noCache !== "boolean") {
|
|
308
308
|
throw new CliError(`"noCache" in ${settingsFile()} must be true or false`);
|
|
309
309
|
}
|
|
310
|
+
if (settings.copyLinks !== void 0 && typeof settings.copyLinks !== "boolean") {
|
|
311
|
+
throw new CliError(`"copyLinks" in ${settingsFile()} must be true or false`);
|
|
312
|
+
}
|
|
310
313
|
current = settings;
|
|
311
314
|
return current;
|
|
312
315
|
}
|
|
@@ -319,6 +322,15 @@ function saveNoCache(on) {
|
|
|
319
322
|
`);
|
|
320
323
|
return true;
|
|
321
324
|
}
|
|
325
|
+
function saveCopyLinks(on) {
|
|
326
|
+
current = { ...current, copyLinks: on };
|
|
327
|
+
if (!cacheEnabled()) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
writeFileAtomic(settingsFile(), `${JSON.stringify(current, null, 2)}
|
|
331
|
+
`);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
322
334
|
function saveTheme(value2) {
|
|
323
335
|
current = { ...current };
|
|
324
336
|
if (value2 === void 0) {
|
|
@@ -362,6 +374,7 @@ var BASE = {
|
|
|
362
374
|
inputFocusedBg: "#303030",
|
|
363
375
|
warn: "#f0b689",
|
|
364
376
|
error: "#ff8080",
|
|
377
|
+
success: "#89f0ab",
|
|
365
378
|
chartBar: "#b98d63",
|
|
366
379
|
chartLine: "#c99a6f",
|
|
367
380
|
chartDim: "#6e563f",
|
|
@@ -542,9 +555,15 @@ function Footer({
|
|
|
542
555
|
editing,
|
|
543
556
|
tab,
|
|
544
557
|
views,
|
|
558
|
+
copyLinks: copyLinks2,
|
|
545
559
|
openError,
|
|
560
|
+
copyNotice,
|
|
546
561
|
stale
|
|
547
562
|
}) {
|
|
563
|
+
const notice = openError ?? copyNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
|
|
564
|
+
const check = openError === null && copyNotice !== null;
|
|
565
|
+
const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
|
|
566
|
+
const hints = truncated(hintsFor(modal, editing, tab, views, copyLinks2), width - 2 - noticeWidth);
|
|
548
567
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
549
568
|
/* @__PURE__ */ jsx("box", { height: 1, children: /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
|
|
550
569
|
/* @__PURE__ */ jsxs(
|
|
@@ -557,14 +576,23 @@ function Footer({
|
|
|
557
576
|
paddingRight: 1,
|
|
558
577
|
justifyContent: "space-between",
|
|
559
578
|
children: [
|
|
560
|
-
/* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children:
|
|
561
|
-
/* @__PURE__ */
|
|
579
|
+
/* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children: hints }),
|
|
580
|
+
/* @__PURE__ */ jsxs("text", { wrapMode: "none", children: [
|
|
581
|
+
check && /* @__PURE__ */ jsx("span", { fg: theme.success, children: "\u2714 " }),
|
|
582
|
+
/* @__PURE__ */ jsx("span", { fg: openError !== null ? theme.error : copyNotice !== null ? theme.muted : theme.warn, children: notice })
|
|
583
|
+
] })
|
|
562
584
|
]
|
|
563
585
|
}
|
|
564
586
|
)
|
|
565
587
|
] });
|
|
566
588
|
}
|
|
567
|
-
function
|
|
589
|
+
function truncated(text, limit2) {
|
|
590
|
+
if (text.length <= limit2) {
|
|
591
|
+
return text;
|
|
592
|
+
}
|
|
593
|
+
return limit2 <= 1 ? "" : `${text.slice(0, limit2 - 1).trimEnd()}\u2026`;
|
|
594
|
+
}
|
|
595
|
+
function hintsFor(modal, editing, tab, views, copyLinks2) {
|
|
568
596
|
if (modal === "options") {
|
|
569
597
|
return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit \xB7 \u2190/\u2192 toggle \xB7 s save \xB7 esc close \xB7 q quit";
|
|
570
598
|
}
|
|
@@ -580,10 +608,11 @@ function hintsFor(modal, editing, tab, views) {
|
|
|
580
608
|
if (scope2?.view === "list") {
|
|
581
609
|
return "\u2191/\u2193 select \xB7 enter open \xB7 \u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit";
|
|
582
610
|
}
|
|
611
|
+
const action = copyLinks2 ? "enter copy link" : "enter open";
|
|
583
612
|
if (scope2 !== null && repos2.length > 0) {
|
|
584
|
-
return scope2.repo === null ?
|
|
613
|
+
return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 g group by repo \xB7 esc back \xB7 o options \xB7 s settings \xB7 r reload \xB7 q quit` : `\u2191/\u2193 select \xB7 ${action} \xB7 esc back \xB7 1-5 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
585
614
|
}
|
|
586
|
-
return
|
|
615
|
+
return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 \u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
587
616
|
}
|
|
588
617
|
const scope = views === null ? null : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
|
|
589
618
|
const repos = views === null ? [] : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
|
|
@@ -996,7 +1025,8 @@ function lineLength(line) {
|
|
|
996
1025
|
return line.reduce((sum, span) => sum + span.text.length, 0);
|
|
997
1026
|
}
|
|
998
1027
|
function cardWidth(card) {
|
|
999
|
-
|
|
1028
|
+
const subtitleLength = typeof card.subtitle === "string" ? card.subtitle.length : lineLength(card.subtitle);
|
|
1029
|
+
return Math.max(card.title.length + 2 + subtitleLength, ...card.lines.map(lineLength));
|
|
1000
1030
|
}
|
|
1001
1031
|
function formatDuration(hours) {
|
|
1002
1032
|
if (hours < 1) {
|
|
@@ -1105,10 +1135,12 @@ function ChartLine({ line }) {
|
|
|
1105
1135
|
) });
|
|
1106
1136
|
}
|
|
1107
1137
|
function ChartCard({ card }) {
|
|
1138
|
+
const subtitle = typeof card.subtitle === "string" ? [{ text: card.subtitle, fg: theme.muted }] : card.subtitle;
|
|
1108
1139
|
return /* @__PURE__ */ jsxs3("box", { flexDirection: "column", children: [
|
|
1109
1140
|
/* @__PURE__ */ jsxs3("text", { wrapMode: "none", children: [
|
|
1110
1141
|
/* @__PURE__ */ jsx4("b", { fg: theme.text, children: card.title }),
|
|
1111
|
-
/* @__PURE__ */ jsx4("span", { fg: theme.muted, children:
|
|
1142
|
+
/* @__PURE__ */ jsx4("span", { fg: theme.muted, children: " " }),
|
|
1143
|
+
keyed(subtitle, spanKey).map(({ key, item }) => /* @__PURE__ */ jsx4("span", { fg: item.fg ?? theme.muted, children: item.text }, key))
|
|
1112
1144
|
] }),
|
|
1113
1145
|
/* @__PURE__ */ jsx4("box", { flexDirection: "column", marginTop: 1, children: keyed(card.lines, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) })
|
|
1114
1146
|
] });
|
|
@@ -1157,10 +1189,12 @@ function QueuePanel({
|
|
|
1157
1189
|
warning,
|
|
1158
1190
|
empty,
|
|
1159
1191
|
lists,
|
|
1160
|
-
cursor
|
|
1192
|
+
cursor,
|
|
1193
|
+
onRefClick
|
|
1161
1194
|
}) {
|
|
1162
1195
|
const scrollRef = useRef(null);
|
|
1163
1196
|
const { width } = useTerminalDimensions2();
|
|
1197
|
+
const lastDown = useRef(null);
|
|
1164
1198
|
useScrollbarSettle(scrollRef, empty === null);
|
|
1165
1199
|
useEffect2(() => {
|
|
1166
1200
|
scrollRef.current?.scrollChildIntoView(`queue-row-${cursor}`);
|
|
@@ -1203,18 +1237,41 @@ function QueuePanel({
|
|
|
1203
1237
|
const index = offsets[listIndex] + rowIndex;
|
|
1204
1238
|
const isSelected = index === cursor;
|
|
1205
1239
|
const bg = isSelected ? theme.selectedBg : void 0;
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1240
|
+
const refStart = 2 + row.lead.length + 2;
|
|
1241
|
+
return /* @__PURE__ */ jsxs5(
|
|
1242
|
+
"text",
|
|
1243
|
+
{
|
|
1244
|
+
id: `queue-row-${index}`,
|
|
1245
|
+
wrapMode: "none",
|
|
1246
|
+
onMouseDown: onRefClick === null ? void 0 : (event) => {
|
|
1247
|
+
lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
|
|
1248
|
+
},
|
|
1249
|
+
onMouseUp: onRefClick === null ? void 0 : (event) => {
|
|
1250
|
+
const down = lastDown.current;
|
|
1251
|
+
lastDown.current = null;
|
|
1252
|
+
if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
|
|
1256
|
+
if (local >= refStart && local < refStart + row.ref.length) {
|
|
1257
|
+
onRefClick(row);
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
children: [
|
|
1261
|
+
/* @__PURE__ */ jsx6("span", { fg: theme.accent, children: isSelected ? "\u25B8 " : " " }),
|
|
1262
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1263
|
+
row.lead,
|
|
1264
|
+
" "
|
|
1265
|
+
] }),
|
|
1266
|
+
onRefClick === null ? /* @__PURE__ */ jsx6("a", { href: row.url, fg: theme.accent, bg, children: row.ref }) : /* @__PURE__ */ jsx6("span", { fg: theme.accent, bg, children: row.ref }),
|
|
1267
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1268
|
+
" ",
|
|
1269
|
+
row.title
|
|
1270
|
+
] })
|
|
1271
|
+
]
|
|
1272
|
+
},
|
|
1273
|
+
row.url
|
|
1274
|
+
);
|
|
1218
1275
|
})
|
|
1219
1276
|
] }, list.title))
|
|
1220
1277
|
]
|
|
@@ -1273,7 +1330,8 @@ function QueueTab({
|
|
|
1273
1330
|
repoCursor,
|
|
1274
1331
|
rowCursor,
|
|
1275
1332
|
grouped,
|
|
1276
|
-
warning
|
|
1333
|
+
warning,
|
|
1334
|
+
onRefClick
|
|
1277
1335
|
}) {
|
|
1278
1336
|
if (scope.view === "list") {
|
|
1279
1337
|
return /* @__PURE__ */ jsx8(RepoList, { prompt, options: repos, cursor: Math.min(repoCursor, repos.length - 1) });
|
|
@@ -1288,7 +1346,8 @@ function QueueTab({
|
|
|
1288
1346
|
warning,
|
|
1289
1347
|
empty: view.empty,
|
|
1290
1348
|
lists: view.lists,
|
|
1291
|
-
cursor: Math.min(rowCursor, queueRows(view).length - 1)
|
|
1349
|
+
cursor: Math.min(rowCursor, queueRows(view).length - 1),
|
|
1350
|
+
onRefClick
|
|
1292
1351
|
}
|
|
1293
1352
|
);
|
|
1294
1353
|
}
|
|
@@ -1326,7 +1385,8 @@ function MainPanel({
|
|
|
1326
1385
|
scrollRefs,
|
|
1327
1386
|
error,
|
|
1328
1387
|
loading,
|
|
1329
|
-
load
|
|
1388
|
+
load,
|
|
1389
|
+
onRefClick
|
|
1330
1390
|
}) {
|
|
1331
1391
|
return /* @__PURE__ */ jsx8("box", { flexGrow: 1, flexDirection: "column", marginTop: 1, children: views === null ? /* @__PURE__ */ jsx8(Placeholder, { error, loading, load }) : browse.tab === 0 ? /* @__PURE__ */ jsx8(
|
|
1332
1392
|
QueueTab,
|
|
@@ -1338,7 +1398,8 @@ function MainPanel({
|
|
|
1338
1398
|
repoCursor: browse.repoCursors.pending,
|
|
1339
1399
|
rowCursor: browse.rowCursors.pending,
|
|
1340
1400
|
grouped: browse.grouped.pending,
|
|
1341
|
-
warning
|
|
1401
|
+
warning,
|
|
1402
|
+
onRefClick
|
|
1342
1403
|
}
|
|
1343
1404
|
) : browse.tab === 1 ? /* @__PURE__ */ jsx8(
|
|
1344
1405
|
QueueTab,
|
|
@@ -1350,7 +1411,8 @@ function MainPanel({
|
|
|
1350
1411
|
repoCursor: browse.repoCursors.open,
|
|
1351
1412
|
rowCursor: browse.rowCursors.open,
|
|
1352
1413
|
grouped: browse.grouped.open,
|
|
1353
|
-
warning
|
|
1414
|
+
warning,
|
|
1415
|
+
onRefClick
|
|
1354
1416
|
}
|
|
1355
1417
|
) : browse.tab === 2 ? /* @__PURE__ */ jsx8(
|
|
1356
1418
|
StatsTab,
|
|
@@ -2568,6 +2630,12 @@ var SETTINGS = [
|
|
|
2568
2630
|
label: "Clear cache",
|
|
2569
2631
|
hint: "deletes the cached PR data at this path, so the next reload refetches everything"
|
|
2570
2632
|
},
|
|
2633
|
+
{
|
|
2634
|
+
key: "copyLinks",
|
|
2635
|
+
section: "Links",
|
|
2636
|
+
label: "Copy instead of open",
|
|
2637
|
+
hint: "enter and a click on a PR reference copy its link to the clipboard instead of opening the browser"
|
|
2638
|
+
},
|
|
2571
2639
|
{
|
|
2572
2640
|
key: "themePreset",
|
|
2573
2641
|
section: "Theme",
|
|
@@ -2599,6 +2667,7 @@ var THEME_COLORS = [
|
|
|
2599
2667
|
{ key: "inputFocusedBg", hint: "background of the focused text input" },
|
|
2600
2668
|
{ key: "warn", hint: "notices like the reload reminder and confirm prompts" },
|
|
2601
2669
|
{ key: "error", hint: "error messages and failed loads" },
|
|
2670
|
+
{ key: "success", hint: "the checkmark on the copied-link notice" },
|
|
2602
2671
|
{ key: "chartBar", hint: "histogram and volume bars" },
|
|
2603
2672
|
{ key: "chartLine", hint: "trend lines and the scatter dots" },
|
|
2604
2673
|
{ key: "chartDim", hint: "de-emphasized chart parts like the over-target share" },
|
|
@@ -2630,6 +2699,7 @@ function SettingsModal({
|
|
|
2630
2699
|
selected,
|
|
2631
2700
|
cacheAction,
|
|
2632
2701
|
noCache: noCache2,
|
|
2702
|
+
copyLinks: copyLinks2,
|
|
2633
2703
|
preset
|
|
2634
2704
|
}) {
|
|
2635
2705
|
const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
|
|
@@ -2645,6 +2715,7 @@ function SettingsModal({
|
|
|
2645
2715
|
isSelected,
|
|
2646
2716
|
cacheAction,
|
|
2647
2717
|
noCache: noCache2,
|
|
2718
|
+
copyLinks: copyLinks2,
|
|
2648
2719
|
preset
|
|
2649
2720
|
}
|
|
2650
2721
|
) }, setting.key);
|
|
@@ -2658,6 +2729,7 @@ function SettingValue({
|
|
|
2658
2729
|
isSelected,
|
|
2659
2730
|
cacheAction,
|
|
2660
2731
|
noCache: noCache2,
|
|
2732
|
+
copyLinks: copyLinks2,
|
|
2661
2733
|
preset
|
|
2662
2734
|
}) {
|
|
2663
2735
|
switch (setting.key) {
|
|
@@ -2667,6 +2739,9 @@ function SettingValue({
|
|
|
2667
2739
|
case "clearCache": {
|
|
2668
2740
|
return /* @__PURE__ */ jsx11(PathValue, { path: cacheDir(), confirming: cacheAction === "confirm", isSelected });
|
|
2669
2741
|
}
|
|
2742
|
+
case "copyLinks": {
|
|
2743
|
+
return /* @__PURE__ */ jsx11(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
|
|
2744
|
+
}
|
|
2670
2745
|
case "themePreset": {
|
|
2671
2746
|
return /* @__PURE__ */ jsx11(ToggleValue, { value: preset, isSelected });
|
|
2672
2747
|
}
|
|
@@ -3986,9 +4061,9 @@ function buildSpreadCard(title, metrics, format) {
|
|
|
3986
4061
|
const span = Math.log1p(max) - Math.log1p(min);
|
|
3987
4062
|
const pos = (value2) => {
|
|
3988
4063
|
if (span === 0) {
|
|
3989
|
-
return
|
|
4064
|
+
return 1;
|
|
3990
4065
|
}
|
|
3991
|
-
return Math.round((Math.log1p(value2) - Math.log1p(min)) / span * (SPREAD_STRIP_WIDTH -
|
|
4066
|
+
return 1 + Math.round((Math.log1p(value2) - Math.log1p(min)) / span * (SPREAD_STRIP_WIDTH - 3));
|
|
3992
4067
|
};
|
|
3993
4068
|
const cells = blankCells(SPREAD_STRIP_WIDTH);
|
|
3994
4069
|
for (let i = 0; i < SPREAD_STRIP_WIDTH; i++) {
|
|
@@ -4012,7 +4087,13 @@ function buildSpreadCard(title, metrics, format) {
|
|
|
4012
4087
|
{ text: ` ${row.max}`, fg: theme.text }
|
|
4013
4088
|
];
|
|
4014
4089
|
});
|
|
4015
|
-
|
|
4090
|
+
const subtitle = [
|
|
4091
|
+
{ text: "\u2592", fg: theme.chartBar },
|
|
4092
|
+
{ text: " p25-p75, ", fg: theme.muted },
|
|
4093
|
+
{ text: "\u2588", fg: theme.accent },
|
|
4094
|
+
{ text: " p50, log scale", fg: theme.muted }
|
|
4095
|
+
];
|
|
4096
|
+
return { title, subtitle, lines };
|
|
4016
4097
|
}
|
|
4017
4098
|
|
|
4018
4099
|
// tui/views/charts/trend.ts
|
|
@@ -4648,6 +4729,12 @@ function handleSettingsModalKey(key, context) {
|
|
|
4648
4729
|
}
|
|
4649
4730
|
break;
|
|
4650
4731
|
}
|
|
4732
|
+
case "copyLinks": {
|
|
4733
|
+
const next = !context.copyLinks;
|
|
4734
|
+
context.setCopyLinks(next);
|
|
4735
|
+
context.dispatchUi({ type: "cacheActionReported", action: saveCopyLinks(next) ? "saved" : "notSaved" });
|
|
4736
|
+
break;
|
|
4737
|
+
}
|
|
4651
4738
|
case "themePreset": {
|
|
4652
4739
|
const next = cycleTheme(context.themeState, key.name === "left" ? -1 : 1);
|
|
4653
4740
|
applyThemeState(next);
|
|
@@ -4770,9 +4857,14 @@ function handleQueueKey(key, context) {
|
|
|
4770
4857
|
}
|
|
4771
4858
|
case "return": {
|
|
4772
4859
|
const cursor = context.browse.rowCursors[tab];
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4860
|
+
const row = rows[Math.min(cursor, rows.length - 1)];
|
|
4861
|
+
if (context.copyLinks) {
|
|
4862
|
+
context.copyRow(row);
|
|
4863
|
+
} else {
|
|
4864
|
+
context.openUrl(row.url, (message) => {
|
|
4865
|
+
context.dispatchUi({ type: "openErrorReported", message });
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4776
4868
|
break;
|
|
4777
4869
|
}
|
|
4778
4870
|
}
|
|
@@ -4828,7 +4920,7 @@ function handleStatsKey(key, context) {
|
|
|
4828
4920
|
}
|
|
4829
4921
|
}
|
|
4830
4922
|
function handleAppKey(key, context) {
|
|
4831
|
-
context.dispatchUi({ type: "
|
|
4923
|
+
context.dispatchUi({ type: "noticesDismissed" });
|
|
4832
4924
|
if (context.ui.editing) {
|
|
4833
4925
|
if (key.name === "escape") {
|
|
4834
4926
|
context.dispatchUi({ type: "editCancelled" });
|
|
@@ -4882,19 +4974,29 @@ var initialUiState = {
|
|
|
4882
4974
|
fieldError: null,
|
|
4883
4975
|
themeColorError: null,
|
|
4884
4976
|
cacheAction: null,
|
|
4885
|
-
openError: null
|
|
4977
|
+
openError: null,
|
|
4978
|
+
copyNotice: null
|
|
4886
4979
|
};
|
|
4887
4980
|
function cycled2(previous, delta, count2) {
|
|
4888
4981
|
return (previous + count2 + delta) % count2;
|
|
4889
4982
|
}
|
|
4890
4983
|
function uiReducer(state, action) {
|
|
4891
4984
|
switch (action.type) {
|
|
4892
|
-
case "
|
|
4893
|
-
|
|
4985
|
+
case "noticesDismissed": {
|
|
4986
|
+
if (state.openError === null && state.copyNotice === null) {
|
|
4987
|
+
return state;
|
|
4988
|
+
}
|
|
4989
|
+
return { ...state, openError: null, copyNotice: null };
|
|
4894
4990
|
}
|
|
4895
4991
|
case "openErrorReported": {
|
|
4896
4992
|
return { ...state, openError: action.message };
|
|
4897
4993
|
}
|
|
4994
|
+
case "copyReported": {
|
|
4995
|
+
return { ...state, openError: null, copyNotice: { text: action.message } };
|
|
4996
|
+
}
|
|
4997
|
+
case "copyNoticeExpired": {
|
|
4998
|
+
return { ...state, copyNotice: null };
|
|
4999
|
+
}
|
|
4898
5000
|
case "modalOpened": {
|
|
4899
5001
|
return { ...state, modal: action.modal, fieldError: null, themeColorError: null, cacheAction: null };
|
|
4900
5002
|
}
|
|
@@ -4988,14 +5090,50 @@ function openInBrowser(url, onError) {
|
|
|
4988
5090
|
child.unref();
|
|
4989
5091
|
}
|
|
4990
5092
|
|
|
5093
|
+
// tui/utils/clipboard.ts
|
|
5094
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
5095
|
+
function copyCommand() {
|
|
5096
|
+
switch (process.platform) {
|
|
5097
|
+
case "darwin": {
|
|
5098
|
+
return { command: "pbcopy", args: [] };
|
|
5099
|
+
}
|
|
5100
|
+
case "win32": {
|
|
5101
|
+
return { command: "clip", args: [] };
|
|
5102
|
+
}
|
|
5103
|
+
default: {
|
|
5104
|
+
if (process.env.WAYLAND_DISPLAY !== void 0 && process.env.WAYLAND_DISPLAY !== "") {
|
|
5105
|
+
return { command: "wl-copy", args: [] };
|
|
5106
|
+
}
|
|
5107
|
+
return { command: "xclip", args: ["-selection", "clipboard"] };
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
function copyToClipboard(text, onError) {
|
|
5112
|
+
const { command, args } = copyCommand();
|
|
5113
|
+
const child = spawn2(command, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
5114
|
+
child.on("error", (error) => {
|
|
5115
|
+
onError(`could not copy the link (${error.message})`);
|
|
5116
|
+
});
|
|
5117
|
+
child.on("exit", (code) => {
|
|
5118
|
+
if (code !== null && code !== 0) {
|
|
5119
|
+
onError(`could not copy the link (${command} exited with ${code})`);
|
|
5120
|
+
}
|
|
5121
|
+
});
|
|
5122
|
+
child.stdin.on("error", () => {
|
|
5123
|
+
});
|
|
5124
|
+
child.stdin.end(text);
|
|
5125
|
+
}
|
|
5126
|
+
|
|
4991
5127
|
// tui/App.tsx
|
|
4992
5128
|
import { jsx as jsx14, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
|
|
4993
5129
|
function App({
|
|
4994
5130
|
initial: initial2,
|
|
4995
5131
|
initialSaved = null,
|
|
4996
5132
|
initialNoCache = false,
|
|
5133
|
+
initialCopyLinks = false,
|
|
4997
5134
|
initialTheme = defaultThemeState(),
|
|
4998
5135
|
openUrl = openInBrowser,
|
|
5136
|
+
copyUrl = copyToClipboard,
|
|
4999
5137
|
onQuit
|
|
5000
5138
|
}) {
|
|
5001
5139
|
const { width } = useTerminalDimensions3();
|
|
@@ -5004,7 +5142,25 @@ function App({
|
|
|
5004
5142
|
const [options, setOptions] = useState4(initial2);
|
|
5005
5143
|
const [saved2, setSaved] = useState4(initialSaved);
|
|
5006
5144
|
const [noCache2, setNoCache] = useState4(initialNoCache);
|
|
5145
|
+
const [copyLinks2, setCopyLinks] = useState4(initialCopyLinks);
|
|
5007
5146
|
const [themeState, setThemeState] = useState4(initialTheme);
|
|
5147
|
+
const copyRow = (row) => {
|
|
5148
|
+
copyUrl(row.url, (message) => {
|
|
5149
|
+
dispatchUi({ type: "openErrorReported", message });
|
|
5150
|
+
});
|
|
5151
|
+
dispatchUi({ type: "copyReported", message: `copied ${row.ref} to the clipboard` });
|
|
5152
|
+
};
|
|
5153
|
+
useEffect6(() => {
|
|
5154
|
+
if (ui.copyNotice === null) {
|
|
5155
|
+
return void 0;
|
|
5156
|
+
}
|
|
5157
|
+
const timer = setTimeout(() => {
|
|
5158
|
+
dispatchUi({ type: "copyNoticeExpired" });
|
|
5159
|
+
}, 2500);
|
|
5160
|
+
return () => {
|
|
5161
|
+
clearTimeout(timer);
|
|
5162
|
+
};
|
|
5163
|
+
}, [ui.copyNotice]);
|
|
5008
5164
|
const reviewScrollRef = useRef5(null);
|
|
5009
5165
|
const sizeScrollRef = useRef5(null);
|
|
5010
5166
|
const commentScrollRef = useRef5(null);
|
|
@@ -5059,6 +5215,7 @@ function App({
|
|
|
5059
5215
|
ui,
|
|
5060
5216
|
browse,
|
|
5061
5217
|
noCache: noCache2,
|
|
5218
|
+
copyLinks: copyLinks2,
|
|
5062
5219
|
themeState,
|
|
5063
5220
|
options,
|
|
5064
5221
|
views,
|
|
@@ -5067,10 +5224,12 @@ function App({
|
|
|
5067
5224
|
setOptions,
|
|
5068
5225
|
setSaved,
|
|
5069
5226
|
setNoCache,
|
|
5227
|
+
setCopyLinks,
|
|
5070
5228
|
setThemeState,
|
|
5071
5229
|
quit: onQuit,
|
|
5072
5230
|
reload,
|
|
5073
5231
|
openUrl,
|
|
5232
|
+
copyRow,
|
|
5074
5233
|
beginEdit: (value2) => {
|
|
5075
5234
|
draftRef.current = value2;
|
|
5076
5235
|
dispatchUi({ type: "editStarted" });
|
|
@@ -5095,7 +5254,8 @@ function App({
|
|
|
5095
5254
|
scrollRefs: { review: reviewScrollRef, size: sizeScrollRef, comment: commentScrollRef },
|
|
5096
5255
|
error,
|
|
5097
5256
|
loading,
|
|
5098
|
-
load
|
|
5257
|
+
load,
|
|
5258
|
+
onRefClick: copyLinks2 ? copyRow : null
|
|
5099
5259
|
}
|
|
5100
5260
|
),
|
|
5101
5261
|
/* @__PURE__ */ jsx14(
|
|
@@ -5106,7 +5266,9 @@ function App({
|
|
|
5106
5266
|
editing: ui.editing,
|
|
5107
5267
|
tab: browse.tab,
|
|
5108
5268
|
views,
|
|
5269
|
+
copyLinks: copyLinks2,
|
|
5109
5270
|
openError: ui.openError,
|
|
5271
|
+
copyNotice: ui.copyNotice === null ? null : ui.copyNotice.text,
|
|
5110
5272
|
stale
|
|
5111
5273
|
}
|
|
5112
5274
|
),
|
|
@@ -5130,6 +5292,7 @@ function App({
|
|
|
5130
5292
|
selected: ui.selectedSetting,
|
|
5131
5293
|
cacheAction: ui.cacheAction,
|
|
5132
5294
|
noCache: noCache2,
|
|
5295
|
+
copyLinks: copyLinks2,
|
|
5133
5296
|
preset: themeState.preset
|
|
5134
5297
|
}
|
|
5135
5298
|
),
|
|
@@ -5180,7 +5343,7 @@ function bootstrap() {
|
|
|
5180
5343
|
validateField(field.key, value2);
|
|
5181
5344
|
}
|
|
5182
5345
|
}
|
|
5183
|
-
return { initial: initial2, saved: saved2, noCache: values["no-cache"], theme: theme3 };
|
|
5346
|
+
return { initial: initial2, saved: saved2, noCache: values["no-cache"], copyLinks: settings.copyLinks === true, theme: theme3 };
|
|
5184
5347
|
} catch (error) {
|
|
5185
5348
|
if (error instanceof CliError) {
|
|
5186
5349
|
fail(error.message);
|
|
@@ -5191,7 +5354,7 @@ function bootstrap() {
|
|
|
5191
5354
|
|
|
5192
5355
|
// tui/main.tsx
|
|
5193
5356
|
import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
|
|
5194
|
-
var { initial, saved, noCache, theme: theme2 } = bootstrap();
|
|
5357
|
+
var { initial, saved, noCache, copyLinks, theme: theme2 } = bootstrap();
|
|
5195
5358
|
var renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
5196
5359
|
for (const signal of ["SIGTERM", "SIGHUP"]) {
|
|
5197
5360
|
process.on(signal, () => {
|
|
@@ -5206,6 +5369,7 @@ createRoot(renderer).render(
|
|
|
5206
5369
|
initial,
|
|
5207
5370
|
initialSaved: saved,
|
|
5208
5371
|
initialNoCache: noCache,
|
|
5372
|
+
initialCopyLinks: copyLinks,
|
|
5209
5373
|
initialTheme: theme2,
|
|
5210
5374
|
onQuit: () => {
|
|
5211
5375
|
renderer.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3lm/pr-stats",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "GitHub PR stats in an interactive terminal UI, via the gh CLI or an access token",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Dominic Elm",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@types/node": "^26.3.0",
|
|
37
37
|
"@types/react": "^19.2.18",
|
|
38
38
|
"esbuild": "^0.28.2",
|
|
39
|
-
"typescript": "^
|
|
39
|
+
"typescript": "^6.0.3"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@opentui/core": "^0.5.8",
|