@d3lm/pr-stats 0.2.0 → 0.2.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.
- package/README.md +8 -6
- package/dist/tui-app.mjs +673 -276
- package/package.json +29 -21
package/dist/tui-app.mjs
CHANGED
|
@@ -28,9 +28,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
mod
|
|
29
29
|
));
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// node_modules/.pnpm/asciichart@1.5.25/node_modules/asciichart/asciichart.js
|
|
32
32
|
var require_asciichart = __commonJS({
|
|
33
|
-
"
|
|
33
|
+
"node_modules/.pnpm/asciichart@1.5.25/node_modules/asciichart/asciichart.js"(exports) {
|
|
34
34
|
"use strict";
|
|
35
35
|
(function(exports2) {
|
|
36
36
|
exports2.black = "\x1B[30m";
|
|
@@ -130,23 +130,23 @@ var require_asciichart = __commonJS({
|
|
|
130
130
|
}
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
// tui/main.tsx
|
|
133
|
+
// src/tui/main.tsx
|
|
134
134
|
import { createCliRenderer } from "@opentui/core";
|
|
135
135
|
import { createRoot } from "@opentui/react";
|
|
136
136
|
|
|
137
|
-
// tui/App.tsx
|
|
137
|
+
// src/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
|
-
// settings.ts
|
|
141
|
+
// src/settings.ts
|
|
142
142
|
import { readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
|
|
143
143
|
import { join as join2 } from "node:path";
|
|
144
144
|
|
|
145
|
-
// cache.ts
|
|
145
|
+
// src/cache.ts
|
|
146
146
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
147
147
|
import { homedir } from "node:os";
|
|
148
148
|
import { dirname, join } from "node:path";
|
|
149
|
-
var VERSION =
|
|
149
|
+
var VERSION = 2;
|
|
150
150
|
var enabled = false;
|
|
151
151
|
function configureCache(on) {
|
|
152
152
|
enabled = on;
|
|
@@ -263,7 +263,7 @@ var PrCache = class {
|
|
|
263
263
|
}
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
-
// utils.ts
|
|
266
|
+
// src/utils.ts
|
|
267
267
|
var CliError = class extends Error {
|
|
268
268
|
};
|
|
269
269
|
function fail(message) {
|
|
@@ -278,7 +278,7 @@ function percentile(sorted, percent) {
|
|
|
278
278
|
return sorted[Math.max(0, index)];
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
// settings.ts
|
|
281
|
+
// src/settings.ts
|
|
282
282
|
function settingsFile() {
|
|
283
283
|
return join2(cacheDir(), "settings.json");
|
|
284
284
|
}
|
|
@@ -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) {
|
|
@@ -349,7 +361,7 @@ function applySettings(values, explicit) {
|
|
|
349
361
|
return settings;
|
|
350
362
|
}
|
|
351
363
|
|
|
352
|
-
// tui/theme.ts
|
|
364
|
+
// src/tui/theme.ts
|
|
353
365
|
var BASE = {
|
|
354
366
|
bg: "#1e1e1e",
|
|
355
367
|
border: "#363636",
|
|
@@ -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",
|
|
@@ -534,17 +547,24 @@ function themeColorText(key) {
|
|
|
534
547
|
return Array.isArray(value2) ? value2.join(" ") : value2;
|
|
535
548
|
}
|
|
536
549
|
|
|
537
|
-
// tui/components/Footer.tsx
|
|
550
|
+
// src/tui/components/Footer.tsx
|
|
538
551
|
import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
|
|
539
552
|
function Footer({
|
|
540
553
|
width,
|
|
541
554
|
modal,
|
|
542
555
|
editing,
|
|
543
556
|
tab,
|
|
557
|
+
authoredTab,
|
|
544
558
|
views,
|
|
559
|
+
copyLinks: copyLinks2,
|
|
545
560
|
openError,
|
|
561
|
+
copyNotice,
|
|
546
562
|
stale
|
|
547
563
|
}) {
|
|
564
|
+
const notice = openError ?? copyNotice ?? (stale ? "options changed \xB7 press r to reload" : "");
|
|
565
|
+
const check = openError === null && copyNotice !== null;
|
|
566
|
+
const noticeWidth = notice === "" ? 0 : notice.length + (check ? 2 : 0) + 2;
|
|
567
|
+
const hints = truncated(hintsFor(modal, editing, tab, authoredTab, views, copyLinks2), width - 2 - noticeWidth);
|
|
548
568
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
549
569
|
/* @__PURE__ */ jsx("box", { height: 1, children: /* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
|
|
550
570
|
/* @__PURE__ */ jsxs(
|
|
@@ -557,14 +577,23 @@ function Footer({
|
|
|
557
577
|
paddingRight: 1,
|
|
558
578
|
justifyContent: "space-between",
|
|
559
579
|
children: [
|
|
560
|
-
/* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children:
|
|
561
|
-
/* @__PURE__ */
|
|
580
|
+
/* @__PURE__ */ jsx("text", { wrapMode: "none", fg: theme.dim, children: hints }),
|
|
581
|
+
/* @__PURE__ */ jsxs("text", { wrapMode: "none", children: [
|
|
582
|
+
check && /* @__PURE__ */ jsx("span", { fg: theme.success, children: "\u2714 " }),
|
|
583
|
+
/* @__PURE__ */ jsx("span", { fg: openError !== null ? theme.error : copyNotice !== null ? theme.muted : theme.warn, children: notice })
|
|
584
|
+
] })
|
|
562
585
|
]
|
|
563
586
|
}
|
|
564
587
|
)
|
|
565
588
|
] });
|
|
566
589
|
}
|
|
567
|
-
function
|
|
590
|
+
function truncated(text, limit2) {
|
|
591
|
+
if (text.length <= limit2) {
|
|
592
|
+
return text;
|
|
593
|
+
}
|
|
594
|
+
return limit2 <= 1 ? "" : `${text.slice(0, limit2 - 1).trimEnd()}\u2026`;
|
|
595
|
+
}
|
|
596
|
+
function hintsFor(modal, editing, tab, authoredTab, views, copyLinks2) {
|
|
568
597
|
if (modal === "options") {
|
|
569
598
|
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
599
|
}
|
|
@@ -574,29 +603,31 @@ function hintsFor(modal, editing, tab, views) {
|
|
|
574
603
|
if (modal === "theme") {
|
|
575
604
|
return editing ? "enter apply \xB7 esc cancel" : "\u2191/\u2193 select \xB7 enter edit hex \xB7 esc back \xB7 q quit";
|
|
576
605
|
}
|
|
577
|
-
|
|
606
|
+
const toggle = tab === 1 ? authoredTab === "open" ? "t merged stats \xB7 " : "t open PRs \xB7 " : "";
|
|
607
|
+
if (tab === 0 || tab === 1 && authoredTab === "open") {
|
|
578
608
|
const scope2 = views === null ? null : tab === 0 ? views.pendingScope : views.openScope;
|
|
579
609
|
const repos2 = views === null ? [] : tab === 0 ? views.pendingRepos : views.openRepos;
|
|
580
610
|
if (scope2?.view === "list") {
|
|
581
|
-
return
|
|
611
|
+
return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
582
612
|
}
|
|
613
|
+
const action = copyLinks2 ? "enter copy link" : "enter open";
|
|
583
614
|
if (scope2 !== null && repos2.length > 0) {
|
|
584
|
-
return scope2.repo === null ?
|
|
615
|
+
return scope2.repo === null ? `\u2191/\u2193 select \xB7 ${action} \xB7 ${toggle}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 ${toggle}esc back \xB7 1-5 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
585
616
|
}
|
|
586
|
-
return
|
|
617
|
+
return `\u2191/\u2193 select \xB7 ${copyLinks2 ? "enter copy link" : "enter open in browser"} \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
587
618
|
}
|
|
588
|
-
const scope = views === null ? null : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
|
|
589
|
-
const repos = views === null ? [] : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
|
|
619
|
+
const scope = views === null ? null : tab === 1 ? views.mergedScope : tab === 2 ? views.reviewScope : tab === 3 ? views.sizeScope : views.commentScope;
|
|
620
|
+
const repos = views === null ? [] : tab === 1 ? views.mergedRepos : tab === 2 ? views.reviewRepos : tab === 3 ? views.sizeRepos : views.commentRepos;
|
|
590
621
|
if (scope?.view === "list") {
|
|
591
|
-
return
|
|
622
|
+
return `\u2191/\u2193 select \xB7 enter open \xB7 ${toggle}\u2190/\u2192 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
592
623
|
}
|
|
593
624
|
if (scope !== null && repos.length > 0) {
|
|
594
|
-
return
|
|
625
|
+
return `${toggle}esc back \xB7 j/k scroll \xB7 1-5 tabs \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
595
626
|
}
|
|
596
|
-
return
|
|
627
|
+
return `${toggle}1-5 tabs \xB7 j/k scroll \xB7 o options \xB7 s settings \xB7 r reload \xB7 R refetch \xB7 q quit`;
|
|
597
628
|
}
|
|
598
629
|
|
|
599
|
-
// tui/components/Spinner.tsx
|
|
630
|
+
// src/tui/components/Spinner.tsx
|
|
600
631
|
import { useEffect, useState } from "react";
|
|
601
632
|
import { jsx as jsx2 } from "@opentui/react/jsx-runtime";
|
|
602
633
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -614,7 +645,7 @@ function Spinner() {
|
|
|
614
645
|
return /* @__PURE__ */ jsx2("text", { wrapMode: "none", fg: theme.accent, children: SPINNER_FRAMES[frame] });
|
|
615
646
|
}
|
|
616
647
|
|
|
617
|
-
// tui/components/Header.tsx
|
|
648
|
+
// src/tui/components/Header.tsx
|
|
618
649
|
import { jsx as jsx3, jsxs as jsxs2 } from "@opentui/react/jsx-runtime";
|
|
619
650
|
function Header({
|
|
620
651
|
options,
|
|
@@ -648,7 +679,7 @@ function timeModeLabel(options) {
|
|
|
648
679
|
return options.workHours === "0-24" ? `Mon-Fri all hours ${tz}` : `Mon-Fri ${options.workHours} ${tz}`;
|
|
649
680
|
}
|
|
650
681
|
|
|
651
|
-
// time.ts
|
|
682
|
+
// src/time.ts
|
|
652
683
|
function wallFormatter(tz) {
|
|
653
684
|
return new Intl.DateTimeFormat("en-US", {
|
|
654
685
|
timeZone: tz,
|
|
@@ -739,7 +770,7 @@ function durationHours(start, end) {
|
|
|
739
770
|
return businessMsBetween(start, end) / 36e5;
|
|
740
771
|
}
|
|
741
772
|
|
|
742
|
-
// compute.ts
|
|
773
|
+
// src/compute.ts
|
|
743
774
|
function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Date() } = {}) {
|
|
744
775
|
const reviewed = [];
|
|
745
776
|
const pending = [];
|
|
@@ -789,6 +820,23 @@ function computeSizeStats(sizes, { sizeTarget } = {}) {
|
|
|
789
820
|
const misses = sizes.filter((size) => !meetsTarget(size)).toSorted((a, b) => b.total - a.total);
|
|
790
821
|
return { metrics, timelineTotals, met, misses, targetLabel };
|
|
791
822
|
}
|
|
823
|
+
function computeMergeStats(sizes) {
|
|
824
|
+
const merged = [];
|
|
825
|
+
const closed = [];
|
|
826
|
+
const open = [];
|
|
827
|
+
for (const entry of sizes) {
|
|
828
|
+
if (entry.mergedAt !== null) {
|
|
829
|
+
merged.push({ entry, mergedAt: entry.mergedAt, hours: durationHours(entry.pr.createdAt, entry.mergedAt) });
|
|
830
|
+
} else if (entry.pr.state === "open") {
|
|
831
|
+
open.push(entry);
|
|
832
|
+
} else if (entry.closedAt !== null) {
|
|
833
|
+
closed.push({ entry, closedAt: entry.closedAt, hours: durationHours(entry.pr.createdAt, entry.closedAt) });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
merged.sort((a, b) => b.mergedAt.getTime() - a.mergedAt.getTime());
|
|
837
|
+
closed.sort((a, b) => b.closedAt.getTime() - a.closedAt.getTime());
|
|
838
|
+
return { merged, closed, open, allHours: merged.map((result) => result.hours) };
|
|
839
|
+
}
|
|
792
840
|
function computeCommentStats(sizes) {
|
|
793
841
|
const metrics = [
|
|
794
842
|
{ label: "discussion comments", values: sizes.map((size) => size.comments.discussion) },
|
|
@@ -801,7 +849,7 @@ function computeCommentStats(sizes) {
|
|
|
801
849
|
return { metrics, totals, uncommented, top };
|
|
802
850
|
}
|
|
803
851
|
|
|
804
|
-
// report.ts
|
|
852
|
+
// src/report.ts
|
|
805
853
|
var BUCKETS = [];
|
|
806
854
|
function initBuckets() {
|
|
807
855
|
BUCKETS = makeBuckets();
|
|
@@ -882,7 +930,7 @@ function formatCount(value2) {
|
|
|
882
930
|
return `${scaled >= 10 ? Math.round(scaled) : scaled.toFixed(1)}k`;
|
|
883
931
|
}
|
|
884
932
|
|
|
885
|
-
// tui/views/rows.ts
|
|
933
|
+
// src/tui/views/rows.ts
|
|
886
934
|
function durationLead(result) {
|
|
887
935
|
return `${formatHoursOnly(result.hours).padStart(8)}${weeksSuffix(result.hours)}`;
|
|
888
936
|
}
|
|
@@ -898,7 +946,7 @@ function toPrRows(entries, leads) {
|
|
|
898
946
|
});
|
|
899
947
|
}
|
|
900
948
|
|
|
901
|
-
// tui/views/queue.ts
|
|
949
|
+
// src/tui/views/queue.ts
|
|
902
950
|
function queueRows(view) {
|
|
903
951
|
return view.lists.flatMap((list) => list.rows);
|
|
904
952
|
}
|
|
@@ -957,10 +1005,10 @@ function rowsOf(group) {
|
|
|
957
1005
|
);
|
|
958
1006
|
}
|
|
959
1007
|
|
|
960
|
-
// tui/components/ChartsPanel.tsx
|
|
1008
|
+
// src/tui/components/ChartsPanel.tsx
|
|
961
1009
|
import { useTerminalDimensions } from "@opentui/react";
|
|
962
1010
|
|
|
963
|
-
// tui/hooks/scrollbar.ts
|
|
1011
|
+
// src/tui/hooks/scrollbar.ts
|
|
964
1012
|
import { CliRenderEvents } from "@opentui/core";
|
|
965
1013
|
import { useRenderer } from "@opentui/react";
|
|
966
1014
|
import { useLayoutEffect } from "react";
|
|
@@ -991,12 +1039,13 @@ function useScrollbarSettle(scrollRef, mounted = true) {
|
|
|
991
1039
|
}, [scrollRef, renderer2, mounted]);
|
|
992
1040
|
}
|
|
993
1041
|
|
|
994
|
-
// tui/views/charts/model.ts
|
|
1042
|
+
// src/tui/views/charts/model.ts
|
|
995
1043
|
function lineLength(line) {
|
|
996
1044
|
return line.reduce((sum, span) => sum + span.text.length, 0);
|
|
997
1045
|
}
|
|
998
1046
|
function cardWidth(card) {
|
|
999
|
-
|
|
1047
|
+
const subtitleLength = typeof card.subtitle === "string" ? card.subtitle.length : lineLength(card.subtitle);
|
|
1048
|
+
return Math.max(card.title.length + 2 + subtitleLength, ...card.lines.map(lineLength));
|
|
1000
1049
|
}
|
|
1001
1050
|
function formatDuration(hours) {
|
|
1002
1051
|
if (hours < 1) {
|
|
@@ -1011,7 +1060,7 @@ function compact(value2) {
|
|
|
1011
1060
|
return String(Math.round(value2 * 10) / 10);
|
|
1012
1061
|
}
|
|
1013
1062
|
|
|
1014
|
-
// tui/components/ChartsPanel.tsx
|
|
1063
|
+
// src/tui/components/ChartsPanel.tsx
|
|
1015
1064
|
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "@opentui/react/jsx-runtime";
|
|
1016
1065
|
var COLUMN_GAP = 4;
|
|
1017
1066
|
function ChartsPanel({
|
|
@@ -1105,16 +1154,18 @@ function ChartLine({ line }) {
|
|
|
1105
1154
|
) });
|
|
1106
1155
|
}
|
|
1107
1156
|
function ChartCard({ card }) {
|
|
1157
|
+
const subtitle = typeof card.subtitle === "string" ? [{ text: card.subtitle, fg: theme.muted }] : card.subtitle;
|
|
1108
1158
|
return /* @__PURE__ */ jsxs3("box", { flexDirection: "column", children: [
|
|
1109
1159
|
/* @__PURE__ */ jsxs3("text", { wrapMode: "none", children: [
|
|
1110
1160
|
/* @__PURE__ */ jsx4("b", { fg: theme.text, children: card.title }),
|
|
1111
|
-
/* @__PURE__ */ jsx4("span", { fg: theme.muted, children:
|
|
1161
|
+
/* @__PURE__ */ jsx4("span", { fg: theme.muted, children: " " }),
|
|
1162
|
+
keyed(subtitle, spanKey).map(({ key, item }) => /* @__PURE__ */ jsx4("span", { fg: item.fg ?? theme.muted, children: item.text }, key))
|
|
1112
1163
|
] }),
|
|
1113
1164
|
/* @__PURE__ */ jsx4("box", { flexDirection: "column", marginTop: 1, children: keyed(card.lines, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) })
|
|
1114
1165
|
] });
|
|
1115
1166
|
}
|
|
1116
1167
|
|
|
1117
|
-
// tui/components/Placeholder.tsx
|
|
1168
|
+
// src/tui/components/Placeholder.tsx
|
|
1118
1169
|
import { jsx as jsx5, jsxs as jsxs4 } from "@opentui/react/jsx-runtime";
|
|
1119
1170
|
var BAR_WIDTH = 40;
|
|
1120
1171
|
var PARTIAL_BLOCKS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
|
|
@@ -1148,7 +1199,7 @@ function loadLabel(load) {
|
|
|
1148
1199
|
return load.phase === "search" ? "searching PRs..." : "fetching PR details";
|
|
1149
1200
|
}
|
|
1150
1201
|
|
|
1151
|
-
// tui/components/QueuePanel.tsx
|
|
1202
|
+
// src/tui/components/QueuePanel.tsx
|
|
1152
1203
|
import { useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
|
|
1153
1204
|
import { useEffect as useEffect2, useRef } from "react";
|
|
1154
1205
|
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "@opentui/react/jsx-runtime";
|
|
@@ -1157,10 +1208,12 @@ function QueuePanel({
|
|
|
1157
1208
|
warning,
|
|
1158
1209
|
empty,
|
|
1159
1210
|
lists,
|
|
1160
|
-
cursor
|
|
1211
|
+
cursor,
|
|
1212
|
+
onRefClick
|
|
1161
1213
|
}) {
|
|
1162
1214
|
const scrollRef = useRef(null);
|
|
1163
1215
|
const { width } = useTerminalDimensions2();
|
|
1216
|
+
const lastDown = useRef(null);
|
|
1164
1217
|
useScrollbarSettle(scrollRef, empty === null);
|
|
1165
1218
|
useEffect2(() => {
|
|
1166
1219
|
scrollRef.current?.scrollChildIntoView(`queue-row-${cursor}`);
|
|
@@ -1203,18 +1256,41 @@ function QueuePanel({
|
|
|
1203
1256
|
const index = offsets[listIndex] + rowIndex;
|
|
1204
1257
|
const isSelected = index === cursor;
|
|
1205
1258
|
const bg = isSelected ? theme.selectedBg : void 0;
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1259
|
+
const refStart = 2 + row.lead.length + 2;
|
|
1260
|
+
return /* @__PURE__ */ jsxs5(
|
|
1261
|
+
"text",
|
|
1262
|
+
{
|
|
1263
|
+
id: `queue-row-${index}`,
|
|
1264
|
+
wrapMode: "none",
|
|
1265
|
+
onMouseDown: onRefClick === null ? void 0 : (event) => {
|
|
1266
|
+
lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
|
|
1267
|
+
},
|
|
1268
|
+
onMouseUp: onRefClick === null ? void 0 : (event) => {
|
|
1269
|
+
const down = lastDown.current;
|
|
1270
|
+
lastDown.current = null;
|
|
1271
|
+
if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
|
|
1275
|
+
if (local >= refStart && local < refStart + row.ref.length) {
|
|
1276
|
+
onRefClick(row);
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
children: [
|
|
1280
|
+
/* @__PURE__ */ jsx6("span", { fg: theme.accent, children: isSelected ? "\u25B8 " : " " }),
|
|
1281
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1282
|
+
row.lead,
|
|
1283
|
+
" "
|
|
1284
|
+
] }),
|
|
1285
|
+
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 }),
|
|
1286
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1287
|
+
" ",
|
|
1288
|
+
row.title
|
|
1289
|
+
] })
|
|
1290
|
+
]
|
|
1291
|
+
},
|
|
1292
|
+
row.url
|
|
1293
|
+
);
|
|
1218
1294
|
})
|
|
1219
1295
|
] }, list.title))
|
|
1220
1296
|
]
|
|
@@ -1223,7 +1299,7 @@ function QueuePanel({
|
|
|
1223
1299
|
] });
|
|
1224
1300
|
}
|
|
1225
1301
|
|
|
1226
|
-
// tui/components/RepoList.tsx
|
|
1302
|
+
// src/tui/components/RepoList.tsx
|
|
1227
1303
|
import { useEffect as useEffect3, useRef as useRef2 } from "react";
|
|
1228
1304
|
import { jsx as jsx7, jsxs as jsxs6 } from "@opentui/react/jsx-runtime";
|
|
1229
1305
|
function RepoList({
|
|
@@ -1263,8 +1339,123 @@ function RepoList({
|
|
|
1263
1339
|
);
|
|
1264
1340
|
}
|
|
1265
1341
|
|
|
1266
|
-
// tui/
|
|
1267
|
-
|
|
1342
|
+
// src/tui/state/browse.ts
|
|
1343
|
+
var TABS = ["1 Awaiting you", "2 Your PRs", "3 Review time", "4 PR size", "5 Comments"];
|
|
1344
|
+
var initialBrowseState = {
|
|
1345
|
+
tab: 0,
|
|
1346
|
+
authoredTab: "open",
|
|
1347
|
+
scopes: {
|
|
1348
|
+
pending: { view: "list" },
|
|
1349
|
+
open: { view: "list" },
|
|
1350
|
+
review: { view: "list" },
|
|
1351
|
+
size: { view: "list" },
|
|
1352
|
+
comment: { view: "list" },
|
|
1353
|
+
merged: { view: "list" }
|
|
1354
|
+
},
|
|
1355
|
+
repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0, merged: 0 },
|
|
1356
|
+
rowCursors: { pending: 0, open: 0 },
|
|
1357
|
+
grouped: { pending: false, open: false }
|
|
1358
|
+
};
|
|
1359
|
+
function dropVanishedRepo(scope, repos) {
|
|
1360
|
+
if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
|
|
1361
|
+
return { view: "list" };
|
|
1362
|
+
}
|
|
1363
|
+
return scope;
|
|
1364
|
+
}
|
|
1365
|
+
function cycled(previous, delta, count2) {
|
|
1366
|
+
return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
|
|
1367
|
+
}
|
|
1368
|
+
function browseReducer(state, action) {
|
|
1369
|
+
switch (action.type) {
|
|
1370
|
+
case "tabSelected": {
|
|
1371
|
+
return { ...state, tab: action.tab };
|
|
1372
|
+
}
|
|
1373
|
+
case "tabCycled": {
|
|
1374
|
+
return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
|
|
1375
|
+
}
|
|
1376
|
+
case "subTabToggled": {
|
|
1377
|
+
return { ...state, authoredTab: state.authoredTab === "open" ? "merged" : "open" };
|
|
1378
|
+
}
|
|
1379
|
+
case "repoCursorMoved": {
|
|
1380
|
+
return {
|
|
1381
|
+
...state,
|
|
1382
|
+
repoCursors: {
|
|
1383
|
+
...state.repoCursors,
|
|
1384
|
+
[action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
case "rowCursorMoved": {
|
|
1389
|
+
return {
|
|
1390
|
+
...state,
|
|
1391
|
+
rowCursors: {
|
|
1392
|
+
...state.rowCursors,
|
|
1393
|
+
[action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
case "repoOpened": {
|
|
1398
|
+
return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
|
|
1399
|
+
}
|
|
1400
|
+
case "pickerReturned": {
|
|
1401
|
+
return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
|
|
1402
|
+
}
|
|
1403
|
+
case "groupingToggled": {
|
|
1404
|
+
return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
|
|
1405
|
+
}
|
|
1406
|
+
case "dataLoaded": {
|
|
1407
|
+
return {
|
|
1408
|
+
...state,
|
|
1409
|
+
scopes: {
|
|
1410
|
+
pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
|
|
1411
|
+
open: dropVanishedRepo(state.scopes.open, action.repos.open),
|
|
1412
|
+
review: dropVanishedRepo(state.scopes.review, action.repos.review),
|
|
1413
|
+
size: dropVanishedRepo(state.scopes.size, action.repos.size),
|
|
1414
|
+
comment: dropVanishedRepo(state.scopes.comment, action.repos.comment),
|
|
1415
|
+
merged: dropVanishedRepo(state.scopes.merged, action.repos.merged)
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return state;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/tui/components/TabBar.tsx
|
|
1424
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "@opentui/react/jsx-runtime";
|
|
1425
|
+
function TabBar({ tab }) {
|
|
1426
|
+
return /* @__PURE__ */ jsx8("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginTop: 1, columnGap: 1, children: TABS.map((label, i) => /* @__PURE__ */ jsx8(
|
|
1427
|
+
"text",
|
|
1428
|
+
{
|
|
1429
|
+
wrapMode: "none",
|
|
1430
|
+
fg: i === tab ? theme.accent : theme.muted,
|
|
1431
|
+
bg: i === tab ? theme.selectedBg : void 0,
|
|
1432
|
+
children: ` ${label} `
|
|
1433
|
+
},
|
|
1434
|
+
label
|
|
1435
|
+
)) });
|
|
1436
|
+
}
|
|
1437
|
+
var SUB_TABS = [
|
|
1438
|
+
{ key: "open", label: "Open" },
|
|
1439
|
+
{ key: "merged", label: "Merged & closed" }
|
|
1440
|
+
];
|
|
1441
|
+
function SubTabBar({ active }) {
|
|
1442
|
+
return /* @__PURE__ */ jsxs7("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginBottom: 1, columnGap: 1, children: [
|
|
1443
|
+
SUB_TABS.map(({ key, label }) => /* @__PURE__ */ jsx8(
|
|
1444
|
+
"text",
|
|
1445
|
+
{
|
|
1446
|
+
wrapMode: "none",
|
|
1447
|
+
fg: key === active ? theme.accent : theme.muted,
|
|
1448
|
+
bg: key === active ? theme.selectedBg : void 0,
|
|
1449
|
+
children: ` ${label} `
|
|
1450
|
+
},
|
|
1451
|
+
key
|
|
1452
|
+
)),
|
|
1453
|
+
/* @__PURE__ */ jsx8("text", { wrapMode: "none", fg: theme.dim, children: "t switches" })
|
|
1454
|
+
] });
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/tui/components/MainPanel.tsx
|
|
1458
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "@opentui/react/jsx-runtime";
|
|
1268
1459
|
function QueueTab({
|
|
1269
1460
|
prompt,
|
|
1270
1461
|
repos,
|
|
@@ -1273,22 +1464,24 @@ function QueueTab({
|
|
|
1273
1464
|
repoCursor,
|
|
1274
1465
|
rowCursor,
|
|
1275
1466
|
grouped,
|
|
1276
|
-
warning
|
|
1467
|
+
warning,
|
|
1468
|
+
onRefClick
|
|
1277
1469
|
}) {
|
|
1278
1470
|
if (scope.view === "list") {
|
|
1279
|
-
return /* @__PURE__ */
|
|
1471
|
+
return /* @__PURE__ */ jsx9(RepoList, { prompt, options: repos, cursor: Math.min(repoCursor, repos.length - 1) });
|
|
1280
1472
|
}
|
|
1281
1473
|
if (view === null) {
|
|
1282
1474
|
return null;
|
|
1283
1475
|
}
|
|
1284
|
-
return /* @__PURE__ */
|
|
1476
|
+
return /* @__PURE__ */ jsx9(
|
|
1285
1477
|
QueuePanel,
|
|
1286
1478
|
{
|
|
1287
1479
|
heading: repos.length > 0 ? scope.repo ?? (grouped ? "All repos \xB7 grouped by repo" : "All repos") : null,
|
|
1288
1480
|
warning,
|
|
1289
1481
|
empty: view.empty,
|
|
1290
1482
|
lists: view.lists,
|
|
1291
|
-
cursor: Math.min(rowCursor, queueRows(view).length - 1)
|
|
1483
|
+
cursor: Math.min(rowCursor, queueRows(view).length - 1),
|
|
1484
|
+
onRefClick
|
|
1292
1485
|
}
|
|
1293
1486
|
);
|
|
1294
1487
|
}
|
|
@@ -1302,12 +1495,12 @@ function StatsTab({
|
|
|
1302
1495
|
warning
|
|
1303
1496
|
}) {
|
|
1304
1497
|
if (scope.view === "list") {
|
|
1305
|
-
return /* @__PURE__ */
|
|
1498
|
+
return /* @__PURE__ */ jsx9(RepoList, { options: repos, cursor: Math.min(cursor, repos.length - 1) });
|
|
1306
1499
|
}
|
|
1307
1500
|
if (view === null) {
|
|
1308
1501
|
return null;
|
|
1309
1502
|
}
|
|
1310
|
-
return /* @__PURE__ */
|
|
1503
|
+
return /* @__PURE__ */ jsx9(
|
|
1311
1504
|
ChartsPanel,
|
|
1312
1505
|
{
|
|
1313
1506
|
scrollRef,
|
|
@@ -1326,9 +1519,10 @@ function MainPanel({
|
|
|
1326
1519
|
scrollRefs,
|
|
1327
1520
|
error,
|
|
1328
1521
|
loading,
|
|
1329
|
-
load
|
|
1522
|
+
load,
|
|
1523
|
+
onRefClick
|
|
1330
1524
|
}) {
|
|
1331
|
-
return /* @__PURE__ */
|
|
1525
|
+
return /* @__PURE__ */ jsx9("box", { flexGrow: 1, flexDirection: "column", marginTop: 1, children: views === null ? /* @__PURE__ */ jsx9(Placeholder, { error, loading, load }) : browse.tab === 0 ? /* @__PURE__ */ jsx9(
|
|
1332
1526
|
QueueTab,
|
|
1333
1527
|
{
|
|
1334
1528
|
prompt: "Select a repository and press enter to open its review queue.",
|
|
@@ -1338,21 +1532,37 @@ function MainPanel({
|
|
|
1338
1532
|
repoCursor: browse.repoCursors.pending,
|
|
1339
1533
|
rowCursor: browse.rowCursors.pending,
|
|
1340
1534
|
grouped: browse.grouped.pending,
|
|
1341
|
-
warning
|
|
1342
|
-
|
|
1343
|
-
) : browse.tab === 1 ? /* @__PURE__ */ jsx8(
|
|
1344
|
-
QueueTab,
|
|
1345
|
-
{
|
|
1346
|
-
prompt: "Select a repository and press enter to list its open PRs.",
|
|
1347
|
-
repos: views.openRepos,
|
|
1348
|
-
scope: views.openScope,
|
|
1349
|
-
view: views.open,
|
|
1350
|
-
repoCursor: browse.repoCursors.open,
|
|
1351
|
-
rowCursor: browse.rowCursors.open,
|
|
1352
|
-
grouped: browse.grouped.open,
|
|
1353
|
-
warning
|
|
1535
|
+
warning,
|
|
1536
|
+
onRefClick
|
|
1354
1537
|
}
|
|
1355
|
-
) : browse.tab ===
|
|
1538
|
+
) : browse.tab === 1 ? /* @__PURE__ */ jsxs8("box", { flexGrow: 1, flexDirection: "column", children: [
|
|
1539
|
+
/* @__PURE__ */ jsx9(SubTabBar, { active: browse.authoredTab }),
|
|
1540
|
+
browse.authoredTab === "open" ? /* @__PURE__ */ jsx9(
|
|
1541
|
+
QueueTab,
|
|
1542
|
+
{
|
|
1543
|
+
prompt: "Select a repository and press enter to list its open PRs.",
|
|
1544
|
+
repos: views.openRepos,
|
|
1545
|
+
scope: views.openScope,
|
|
1546
|
+
view: views.open,
|
|
1547
|
+
repoCursor: browse.repoCursors.open,
|
|
1548
|
+
rowCursor: browse.rowCursors.open,
|
|
1549
|
+
grouped: browse.grouped.open,
|
|
1550
|
+
warning,
|
|
1551
|
+
onRefClick
|
|
1552
|
+
}
|
|
1553
|
+
) : /* @__PURE__ */ jsx9(
|
|
1554
|
+
StatsTab,
|
|
1555
|
+
{
|
|
1556
|
+
repos: views.mergedRepos,
|
|
1557
|
+
scope: views.mergedScope,
|
|
1558
|
+
view: views.merged,
|
|
1559
|
+
cursor: browse.repoCursors.merged,
|
|
1560
|
+
scrollRef: scrollRefs.merged,
|
|
1561
|
+
focused,
|
|
1562
|
+
warning
|
|
1563
|
+
}
|
|
1564
|
+
)
|
|
1565
|
+
] }) : browse.tab === 2 ? /* @__PURE__ */ jsx9(
|
|
1356
1566
|
StatsTab,
|
|
1357
1567
|
{
|
|
1358
1568
|
repos: views.reviewRepos,
|
|
@@ -1363,7 +1573,7 @@ function MainPanel({
|
|
|
1363
1573
|
focused,
|
|
1364
1574
|
warning
|
|
1365
1575
|
}
|
|
1366
|
-
) : browse.tab === 3 ? /* @__PURE__ */
|
|
1576
|
+
) : browse.tab === 3 ? /* @__PURE__ */ jsx9(
|
|
1367
1577
|
StatsTab,
|
|
1368
1578
|
{
|
|
1369
1579
|
repos: views.sizeRepos,
|
|
@@ -1374,7 +1584,7 @@ function MainPanel({
|
|
|
1374
1584
|
focused,
|
|
1375
1585
|
warning
|
|
1376
1586
|
}
|
|
1377
|
-
) : /* @__PURE__ */
|
|
1587
|
+
) : /* @__PURE__ */ jsx9(
|
|
1378
1588
|
StatsTab,
|
|
1379
1589
|
{
|
|
1380
1590
|
repos: views.commentRepos,
|
|
@@ -1388,7 +1598,7 @@ function MainPanel({
|
|
|
1388
1598
|
) });
|
|
1389
1599
|
}
|
|
1390
1600
|
|
|
1391
|
-
//
|
|
1601
|
+
// node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/utilities.js
|
|
1392
1602
|
function stringReplaceAll(string, substring, postfix) {
|
|
1393
1603
|
let index = string.indexOf(substring);
|
|
1394
1604
|
if (index === -1) {
|
|
@@ -1418,7 +1628,7 @@ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
|
|
1418
1628
|
return returnValue;
|
|
1419
1629
|
}
|
|
1420
1630
|
|
|
1421
|
-
//
|
|
1631
|
+
// node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
1422
1632
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
1423
1633
|
var ANSI_UNDERLINE_OFFSET = 20;
|
|
1424
1634
|
var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
|
|
@@ -1640,7 +1850,7 @@ function assembleStyles() {
|
|
|
1640
1850
|
var ansiStyles = assembleStyles();
|
|
1641
1851
|
var ansi_styles_default = ansiStyles;
|
|
1642
1852
|
|
|
1643
|
-
//
|
|
1853
|
+
// node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/vendor/supports-color/index.js
|
|
1644
1854
|
import process2 from "node:process";
|
|
1645
1855
|
import os from "node:os";
|
|
1646
1856
|
import tty from "node:tty";
|
|
@@ -1782,7 +1992,7 @@ var supportsColor = {
|
|
|
1782
1992
|
};
|
|
1783
1993
|
var supports_color_default = supportsColor;
|
|
1784
1994
|
|
|
1785
|
-
//
|
|
1995
|
+
// node_modules/.pnpm/chalk@6.0.0/node_modules/chalk/source/index.js
|
|
1786
1996
|
var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
|
|
1787
1997
|
var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
|
|
1788
1998
|
var STYLER = /* @__PURE__ */ Symbol("STYLER");
|
|
@@ -1950,7 +2160,7 @@ var chalk = createChalk();
|
|
|
1950
2160
|
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
1951
2161
|
var source_default = chalk;
|
|
1952
2162
|
|
|
1953
|
-
// flags.ts
|
|
2163
|
+
// src/flags.ts
|
|
1954
2164
|
import { parseArgs } from "node:util";
|
|
1955
2165
|
var flag = source_default.green;
|
|
1956
2166
|
var value = source_default.cyan;
|
|
@@ -2211,7 +2421,7 @@ function resolveTimezone(input) {
|
|
|
2211
2421
|
return tz;
|
|
2212
2422
|
}
|
|
2213
2423
|
|
|
2214
|
-
// tui/state/options.ts
|
|
2424
|
+
// src/tui/state/options.ts
|
|
2215
2425
|
var FIELDS = [
|
|
2216
2426
|
{
|
|
2217
2427
|
key: "since",
|
|
@@ -2383,14 +2593,14 @@ function targetLabelOf(target) {
|
|
|
2383
2593
|
return target + (/^[\d.]+$/.test(target) ? "h" : "");
|
|
2384
2594
|
}
|
|
2385
2595
|
|
|
2386
|
-
// tui/components/ModalFrame.tsx
|
|
2387
|
-
import { jsx as
|
|
2596
|
+
// src/tui/components/ModalFrame.tsx
|
|
2597
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "@opentui/react/jsx-runtime";
|
|
2388
2598
|
var MODAL_WIDTH = 64;
|
|
2389
2599
|
function ModalFrame({ title, children }) {
|
|
2390
2600
|
const tabWidth = title.length + 4;
|
|
2391
2601
|
const tabTopRow = `\u250C${"\u2500".repeat(tabWidth - 2)}\u2510`;
|
|
2392
2602
|
const tabTitleSuffix = ` \u2514${"\u2500".repeat(MODAL_WIDTH - tabWidth - 1)}\u2510`;
|
|
2393
|
-
return /* @__PURE__ */
|
|
2603
|
+
return /* @__PURE__ */ jsx10(
|
|
2394
2604
|
"box",
|
|
2395
2605
|
{
|
|
2396
2606
|
position: "absolute",
|
|
@@ -2401,14 +2611,14 @@ function ModalFrame({ title, children }) {
|
|
|
2401
2611
|
alignItems: "center",
|
|
2402
2612
|
justifyContent: "center",
|
|
2403
2613
|
zIndex: 10,
|
|
2404
|
-
children: /* @__PURE__ */
|
|
2405
|
-
/* @__PURE__ */
|
|
2406
|
-
/* @__PURE__ */
|
|
2407
|
-
/* @__PURE__ */
|
|
2408
|
-
/* @__PURE__ */
|
|
2409
|
-
/* @__PURE__ */
|
|
2614
|
+
children: /* @__PURE__ */ jsxs9("box", { flexDirection: "column", width: MODAL_WIDTH, children: [
|
|
2615
|
+
/* @__PURE__ */ jsx10("text", { alignSelf: "flex-start", wrapMode: "none", fg: theme.border, bg: theme.bg, children: tabTopRow }),
|
|
2616
|
+
/* @__PURE__ */ jsxs9("text", { wrapMode: "none", bg: theme.bg, children: [
|
|
2617
|
+
/* @__PURE__ */ jsx10("span", { fg: theme.border, children: "\u2502 " }),
|
|
2618
|
+
/* @__PURE__ */ jsx10("span", { fg: theme.muted, children: title }),
|
|
2619
|
+
/* @__PURE__ */ jsx10("span", { fg: theme.border, children: tabTitleSuffix })
|
|
2410
2620
|
] }),
|
|
2411
|
-
/* @__PURE__ */
|
|
2621
|
+
/* @__PURE__ */ jsx10(
|
|
2412
2622
|
"box",
|
|
2413
2623
|
{
|
|
2414
2624
|
border: ["left", "right", "bottom"],
|
|
@@ -2425,7 +2635,7 @@ function ModalFrame({ title, children }) {
|
|
|
2425
2635
|
);
|
|
2426
2636
|
}
|
|
2427
2637
|
function ModalRow({ label, isSelected, children }) {
|
|
2428
|
-
return /* @__PURE__ */
|
|
2638
|
+
return /* @__PURE__ */ jsxs9(
|
|
2429
2639
|
"box",
|
|
2430
2640
|
{
|
|
2431
2641
|
flexDirection: "row",
|
|
@@ -2434,17 +2644,17 @@ function ModalRow({ label, isSelected, children }) {
|
|
|
2434
2644
|
paddingRight: 2,
|
|
2435
2645
|
backgroundColor: isSelected ? theme.selectedBg : void 0,
|
|
2436
2646
|
children: [
|
|
2437
|
-
isSelected ? /* @__PURE__ */
|
|
2438
|
-
/* @__PURE__ */
|
|
2439
|
-
/* @__PURE__ */
|
|
2647
|
+
isSelected ? /* @__PURE__ */ jsx10("text", { position: "absolute", left: -1, wrapMode: "none", fg: theme.accent, bg: theme.bg, children: "\u2503" }) : null,
|
|
2648
|
+
/* @__PURE__ */ jsx10("text", { wrapMode: "none", fg: theme.text, children: label }),
|
|
2649
|
+
/* @__PURE__ */ jsx10("box", { flexGrow: 1 }),
|
|
2440
2650
|
children
|
|
2441
2651
|
]
|
|
2442
2652
|
}
|
|
2443
2653
|
);
|
|
2444
2654
|
}
|
|
2445
2655
|
|
|
2446
|
-
// tui/components/OptionsModal.tsx
|
|
2447
|
-
import { jsx as
|
|
2656
|
+
// src/tui/components/OptionsModal.tsx
|
|
2657
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "@opentui/react/jsx-runtime";
|
|
2448
2658
|
var EMPTY_PLACEHOLDERS = {
|
|
2449
2659
|
repos: "(all accessible)",
|
|
2450
2660
|
user: "(authenticated user)",
|
|
@@ -2466,12 +2676,12 @@ function OptionsModal({
|
|
|
2466
2676
|
onSubmit
|
|
2467
2677
|
}) {
|
|
2468
2678
|
const savedState = savedLine(options, saved2);
|
|
2469
|
-
return /* @__PURE__ */
|
|
2470
|
-
SECTIONS.map((section) => /* @__PURE__ */
|
|
2471
|
-
/* @__PURE__ */
|
|
2679
|
+
return /* @__PURE__ */ jsxs10(ModalFrame, { title: "Options", children: [
|
|
2680
|
+
SECTIONS.map((section) => /* @__PURE__ */ jsxs10("box", { flexDirection: "column", marginBottom: 1, children: [
|
|
2681
|
+
/* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
|
|
2472
2682
|
section.fields.map((field) => {
|
|
2473
2683
|
const index = FIELDS.indexOf(field);
|
|
2474
|
-
return /* @__PURE__ */
|
|
2684
|
+
return /* @__PURE__ */ jsx11(
|
|
2475
2685
|
FieldRow,
|
|
2476
2686
|
{
|
|
2477
2687
|
field,
|
|
@@ -2485,8 +2695,8 @@ function OptionsModal({
|
|
|
2485
2695
|
);
|
|
2486
2696
|
})
|
|
2487
2697
|
] }, section.title)),
|
|
2488
|
-
/* @__PURE__ */
|
|
2489
|
-
/* @__PURE__ */
|
|
2698
|
+
/* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: savedState.color, marginLeft: 2, marginRight: 2, marginBottom: 1, children: savedState.text }),
|
|
2699
|
+
/* @__PURE__ */ jsx11(
|
|
2490
2700
|
"text",
|
|
2491
2701
|
{
|
|
2492
2702
|
wrapMode: "word",
|
|
@@ -2527,7 +2737,7 @@ function FieldRow({
|
|
|
2527
2737
|
}) {
|
|
2528
2738
|
const value2 = displayValue(field.key, options[field.key]);
|
|
2529
2739
|
const valueColor = value2.isPlaceholder ? isSelected ? theme.muted : theme.dim : theme.muted;
|
|
2530
|
-
return /* @__PURE__ */
|
|
2740
|
+
return /* @__PURE__ */ jsx11(ModalRow, { label: field.label, isSelected, children: isEditing ? /* @__PURE__ */ jsx11(
|
|
2531
2741
|
"input",
|
|
2532
2742
|
{
|
|
2533
2743
|
width: 32,
|
|
@@ -2544,17 +2754,17 @@ function FieldRow({
|
|
|
2544
2754
|
textColor: theme.text,
|
|
2545
2755
|
cursorColor: theme.accent
|
|
2546
2756
|
}
|
|
2547
|
-
) : isSelected && field.kind === "toggle" ? /* @__PURE__ */
|
|
2548
|
-
/* @__PURE__ */
|
|
2549
|
-
/* @__PURE__ */
|
|
2550
|
-
/* @__PURE__ */
|
|
2551
|
-
] }) : isSelected ? /* @__PURE__ */
|
|
2757
|
+
) : isSelected && field.kind === "toggle" ? /* @__PURE__ */ jsxs10("text", { wrapMode: "none", children: [
|
|
2758
|
+
/* @__PURE__ */ jsx11("span", { fg: theme.muted, children: "\u2039 " }),
|
|
2759
|
+
/* @__PURE__ */ jsx11("b", { fg: theme.text, children: value2.text }),
|
|
2760
|
+
/* @__PURE__ */ jsx11("span", { fg: theme.muted, children: " \u203A" })
|
|
2761
|
+
] }) : isSelected ? /* @__PURE__ */ jsx11("text", { wrapMode: "none", children: /* @__PURE__ */ jsx11("b", { fg: value2.isPlaceholder ? theme.muted : theme.text, children: value2.text }) }) : /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: valueColor, children: value2.text }) });
|
|
2552
2762
|
}
|
|
2553
2763
|
|
|
2554
|
-
// tui/components/SettingsModal.tsx
|
|
2764
|
+
// src/tui/components/SettingsModal.tsx
|
|
2555
2765
|
import { homedir as homedir2 } from "node:os";
|
|
2556
2766
|
|
|
2557
|
-
// tui/state/settings.ts
|
|
2767
|
+
// src/tui/state/settings.ts
|
|
2558
2768
|
var SETTINGS = [
|
|
2559
2769
|
{
|
|
2560
2770
|
key: "noCache",
|
|
@@ -2568,6 +2778,12 @@ var SETTINGS = [
|
|
|
2568
2778
|
label: "Clear cache",
|
|
2569
2779
|
hint: "deletes the cached PR data at this path, so the next reload refetches everything"
|
|
2570
2780
|
},
|
|
2781
|
+
{
|
|
2782
|
+
key: "copyLinks",
|
|
2783
|
+
section: "Links",
|
|
2784
|
+
label: "Copy instead of open",
|
|
2785
|
+
hint: "enter and a click on a PR reference copy its link to the clipboard instead of opening the browser"
|
|
2786
|
+
},
|
|
2571
2787
|
{
|
|
2572
2788
|
key: "themePreset",
|
|
2573
2789
|
section: "Theme",
|
|
@@ -2599,6 +2815,7 @@ var THEME_COLORS = [
|
|
|
2599
2815
|
{ key: "inputFocusedBg", hint: "background of the focused text input" },
|
|
2600
2816
|
{ key: "warn", hint: "notices like the reload reminder and confirm prompts" },
|
|
2601
2817
|
{ key: "error", hint: "error messages and failed loads" },
|
|
2818
|
+
{ key: "success", hint: "the checkmark on the copied-link notice" },
|
|
2602
2819
|
{ key: "chartBar", hint: "histogram and volume bars" },
|
|
2603
2820
|
{ key: "chartLine", hint: "trend lines and the scatter dots" },
|
|
2604
2821
|
{ key: "chartDim", hint: "de-emphasized chart parts like the over-target share" },
|
|
@@ -2615,8 +2832,8 @@ var CACHE_MESSAGES = {
|
|
|
2615
2832
|
resetDisabled: { text: "the cache is disabled for this session \xB7 nothing to reset" }
|
|
2616
2833
|
};
|
|
2617
2834
|
|
|
2618
|
-
// tui/components/SettingsModal.tsx
|
|
2619
|
-
import { jsx as
|
|
2835
|
+
// src/tui/components/SettingsModal.tsx
|
|
2836
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
|
|
2620
2837
|
var SECTIONS2 = [];
|
|
2621
2838
|
for (const setting of SETTINGS) {
|
|
2622
2839
|
const last = SECTIONS2.at(-1);
|
|
@@ -2630,27 +2847,29 @@ function SettingsModal({
|
|
|
2630
2847
|
selected,
|
|
2631
2848
|
cacheAction,
|
|
2632
2849
|
noCache: noCache2,
|
|
2850
|
+
copyLinks: copyLinks2,
|
|
2633
2851
|
preset
|
|
2634
2852
|
}) {
|
|
2635
2853
|
const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
|
|
2636
|
-
return /* @__PURE__ */
|
|
2637
|
-
SECTIONS2.map((section) => /* @__PURE__ */
|
|
2638
|
-
/* @__PURE__ */
|
|
2854
|
+
return /* @__PURE__ */ jsxs11(ModalFrame, { title: "Settings", children: [
|
|
2855
|
+
SECTIONS2.map((section) => /* @__PURE__ */ jsxs11("box", { flexDirection: "column", marginBottom: 1, children: [
|
|
2856
|
+
/* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.accent, marginLeft: 2, children: section.title }),
|
|
2639
2857
|
section.settings.map((setting) => {
|
|
2640
2858
|
const isSelected = SETTINGS.indexOf(setting) === selected;
|
|
2641
|
-
return /* @__PURE__ */
|
|
2859
|
+
return /* @__PURE__ */ jsx12(ModalRow, { label: setting.label, isSelected, children: /* @__PURE__ */ jsx12(
|
|
2642
2860
|
SettingValue,
|
|
2643
2861
|
{
|
|
2644
2862
|
setting,
|
|
2645
2863
|
isSelected,
|
|
2646
2864
|
cacheAction,
|
|
2647
2865
|
noCache: noCache2,
|
|
2866
|
+
copyLinks: copyLinks2,
|
|
2648
2867
|
preset
|
|
2649
2868
|
}
|
|
2650
2869
|
) }, setting.key);
|
|
2651
2870
|
})
|
|
2652
2871
|
] }, section.title)),
|
|
2653
|
-
/* @__PURE__ */
|
|
2872
|
+
/* @__PURE__ */ jsx12("text", { wrapMode: "word", height: 2, fg: message?.warn ? theme.warn : theme.muted, marginLeft: 2, marginRight: 2, children: message?.text ?? SETTINGS[selected].hint })
|
|
2654
2873
|
] });
|
|
2655
2874
|
}
|
|
2656
2875
|
function SettingValue({
|
|
@@ -2658,23 +2877,27 @@ function SettingValue({
|
|
|
2658
2877
|
isSelected,
|
|
2659
2878
|
cacheAction,
|
|
2660
2879
|
noCache: noCache2,
|
|
2880
|
+
copyLinks: copyLinks2,
|
|
2661
2881
|
preset
|
|
2662
2882
|
}) {
|
|
2663
2883
|
switch (setting.key) {
|
|
2664
2884
|
case "noCache": {
|
|
2665
|
-
return /* @__PURE__ */
|
|
2885
|
+
return /* @__PURE__ */ jsx12(ToggleValue, { value: noCache2 ? "yes" : "no", isSelected });
|
|
2666
2886
|
}
|
|
2667
2887
|
case "clearCache": {
|
|
2668
|
-
return /* @__PURE__ */
|
|
2888
|
+
return /* @__PURE__ */ jsx12(PathValue, { path: cacheDir(), confirming: cacheAction === "confirm", isSelected });
|
|
2889
|
+
}
|
|
2890
|
+
case "copyLinks": {
|
|
2891
|
+
return /* @__PURE__ */ jsx12(ToggleValue, { value: copyLinks2 ? "yes" : "no", isSelected });
|
|
2669
2892
|
}
|
|
2670
2893
|
case "themePreset": {
|
|
2671
|
-
return /* @__PURE__ */
|
|
2894
|
+
return /* @__PURE__ */ jsx12(ToggleValue, { value: preset, isSelected });
|
|
2672
2895
|
}
|
|
2673
2896
|
case "themeColors": {
|
|
2674
|
-
return /* @__PURE__ */
|
|
2897
|
+
return /* @__PURE__ */ jsx12("text", { wrapMode: "none", children: ["chartDim", "chartBar", "chartLine", "accent"].map((key) => /* @__PURE__ */ jsx12("span", { fg: theme[key], children: "\u2588\u2588" }, key)) });
|
|
2675
2898
|
}
|
|
2676
2899
|
case "resetSettings": {
|
|
2677
|
-
return /* @__PURE__ */
|
|
2900
|
+
return /* @__PURE__ */ jsx12(PathValue, { path: settingsFile(), confirming: cacheAction === "resetConfirm", isSelected });
|
|
2678
2901
|
}
|
|
2679
2902
|
default: {
|
|
2680
2903
|
return null;
|
|
@@ -2683,113 +2906,23 @@ function SettingValue({
|
|
|
2683
2906
|
}
|
|
2684
2907
|
function ToggleValue({ value: value2, isSelected }) {
|
|
2685
2908
|
if (isSelected) {
|
|
2686
|
-
return /* @__PURE__ */
|
|
2687
|
-
/* @__PURE__ */
|
|
2688
|
-
/* @__PURE__ */
|
|
2689
|
-
/* @__PURE__ */
|
|
2909
|
+
return /* @__PURE__ */ jsxs11("text", { wrapMode: "none", children: [
|
|
2910
|
+
/* @__PURE__ */ jsx12("span", { fg: theme.muted, children: "\u2039 " }),
|
|
2911
|
+
/* @__PURE__ */ jsx12("b", { fg: theme.text, children: value2 }),
|
|
2912
|
+
/* @__PURE__ */ jsx12("span", { fg: theme.muted, children: " \u203A" })
|
|
2690
2913
|
] });
|
|
2691
2914
|
}
|
|
2692
|
-
return /* @__PURE__ */
|
|
2915
|
+
return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: theme.muted, children: value2 });
|
|
2693
2916
|
}
|
|
2694
2917
|
function PathValue({ path, confirming, isSelected }) {
|
|
2695
2918
|
if (confirming) {
|
|
2696
|
-
return /* @__PURE__ */
|
|
2697
|
-
}
|
|
2698
|
-
return /* @__PURE__ */ jsx11("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
// tui/state/browse.ts
|
|
2702
|
-
var TABS = ["1 Awaiting you", "2 Your open PRs", "3 Review time", "4 PR size", "5 Comments"];
|
|
2703
|
-
var initialBrowseState = {
|
|
2704
|
-
tab: 0,
|
|
2705
|
-
scopes: {
|
|
2706
|
-
pending: { view: "list" },
|
|
2707
|
-
open: { view: "list" },
|
|
2708
|
-
review: { view: "list" },
|
|
2709
|
-
size: { view: "list" },
|
|
2710
|
-
comment: { view: "list" }
|
|
2711
|
-
},
|
|
2712
|
-
repoCursors: { pending: 0, open: 0, review: 0, size: 0, comment: 0 },
|
|
2713
|
-
rowCursors: { pending: 0, open: 0 },
|
|
2714
|
-
grouped: { pending: false, open: false }
|
|
2715
|
-
};
|
|
2716
|
-
function dropVanishedRepo(scope, repos) {
|
|
2717
|
-
if (scope.view === "detail" && scope.repo !== null && !repos.some((option) => option.repo === scope.repo)) {
|
|
2718
|
-
return { view: "list" };
|
|
2719
|
-
}
|
|
2720
|
-
return scope;
|
|
2721
|
-
}
|
|
2722
|
-
function cycled(previous, delta, count2) {
|
|
2723
|
-
return (Math.min(previous, count2 - 1) + count2 + delta) % count2;
|
|
2724
|
-
}
|
|
2725
|
-
function browseReducer(state, action) {
|
|
2726
|
-
switch (action.type) {
|
|
2727
|
-
case "tabSelected": {
|
|
2728
|
-
return { ...state, tab: action.tab };
|
|
2729
|
-
}
|
|
2730
|
-
case "tabCycled": {
|
|
2731
|
-
return { ...state, tab: (state.tab + TABS.length + action.delta) % TABS.length };
|
|
2732
|
-
}
|
|
2733
|
-
case "repoCursorMoved": {
|
|
2734
|
-
return {
|
|
2735
|
-
...state,
|
|
2736
|
-
repoCursors: {
|
|
2737
|
-
...state.repoCursors,
|
|
2738
|
-
[action.tab]: cycled(state.repoCursors[action.tab], action.delta, action.count)
|
|
2739
|
-
}
|
|
2740
|
-
};
|
|
2741
|
-
}
|
|
2742
|
-
case "rowCursorMoved": {
|
|
2743
|
-
return {
|
|
2744
|
-
...state,
|
|
2745
|
-
rowCursors: {
|
|
2746
|
-
...state.rowCursors,
|
|
2747
|
-
[action.tab]: cycled(state.rowCursors[action.tab], action.delta, action.count)
|
|
2748
|
-
}
|
|
2749
|
-
};
|
|
2750
|
-
}
|
|
2751
|
-
case "repoOpened": {
|
|
2752
|
-
return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "detail", repo: action.repo } } };
|
|
2753
|
-
}
|
|
2754
|
-
case "pickerReturned": {
|
|
2755
|
-
return { ...state, scopes: { ...state.scopes, [action.tab]: { view: "list" } } };
|
|
2756
|
-
}
|
|
2757
|
-
case "groupingToggled": {
|
|
2758
|
-
return { ...state, grouped: { ...state.grouped, [action.tab]: !state.grouped[action.tab] } };
|
|
2759
|
-
}
|
|
2760
|
-
case "dataLoaded": {
|
|
2761
|
-
return {
|
|
2762
|
-
...state,
|
|
2763
|
-
scopes: {
|
|
2764
|
-
pending: dropVanishedRepo(state.scopes.pending, action.repos.pending),
|
|
2765
|
-
open: dropVanishedRepo(state.scopes.open, action.repos.open),
|
|
2766
|
-
review: dropVanishedRepo(state.scopes.review, action.repos.review),
|
|
2767
|
-
size: dropVanishedRepo(state.scopes.size, action.repos.size),
|
|
2768
|
-
comment: dropVanishedRepo(state.scopes.comment, action.repos.comment)
|
|
2769
|
-
}
|
|
2770
|
-
};
|
|
2771
|
-
}
|
|
2919
|
+
return /* @__PURE__ */ jsx12("text", { wrapMode: "none", children: /* @__PURE__ */ jsx12("b", { fg: theme.warn, children: "enter to confirm" }) });
|
|
2772
2920
|
}
|
|
2773
|
-
return
|
|
2921
|
+
return /* @__PURE__ */ jsx12("text", { wrapMode: "none", fg: isSelected ? theme.text : theme.muted, children: path.replace(homedir2(), "~") });
|
|
2774
2922
|
}
|
|
2775
2923
|
|
|
2776
|
-
// tui/components/
|
|
2777
|
-
import { jsx as
|
|
2778
|
-
function TabBar({ tab }) {
|
|
2779
|
-
return /* @__PURE__ */ jsx12("box", { flexDirection: "row", height: 1, paddingLeft: 1, marginTop: 1, columnGap: 1, children: TABS.map((label, i) => /* @__PURE__ */ jsx12(
|
|
2780
|
-
"text",
|
|
2781
|
-
{
|
|
2782
|
-
wrapMode: "none",
|
|
2783
|
-
fg: i === tab ? theme.accent : theme.muted,
|
|
2784
|
-
bg: i === tab ? theme.selectedBg : void 0,
|
|
2785
|
-
children: ` ${label} `
|
|
2786
|
-
},
|
|
2787
|
-
label
|
|
2788
|
-
)) });
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
// tui/components/ThemeModal.tsx
|
|
2792
|
-
import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs10 } from "@opentui/react/jsx-runtime";
|
|
2924
|
+
// src/tui/components/ThemeModal.tsx
|
|
2925
|
+
import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs12 } from "@opentui/react/jsx-runtime";
|
|
2793
2926
|
function ThemeModal({
|
|
2794
2927
|
selected,
|
|
2795
2928
|
editing,
|
|
@@ -2802,7 +2935,7 @@ function ThemeModal({
|
|
|
2802
2935
|
const spec = THEME_COLORS[selected];
|
|
2803
2936
|
const message = cacheAction === null ? null : CACHE_MESSAGES[cacheAction];
|
|
2804
2937
|
const hint = spec.key in overrides ? `${spec.hint} \xB7 custom color, an empty value restores the theme` : spec.hint;
|
|
2805
|
-
return /* @__PURE__ */
|
|
2938
|
+
return /* @__PURE__ */ jsxs12(ModalFrame, { title: "Theme colors", children: [
|
|
2806
2939
|
/* @__PURE__ */ jsx13("box", { flexDirection: "column", marginBottom: 1, children: THEME_COLORS.map((color, index) => /* @__PURE__ */ jsx13(
|
|
2807
2940
|
ColorRow,
|
|
2808
2941
|
{
|
|
@@ -2845,8 +2978,8 @@ function ColorRow({
|
|
|
2845
2978
|
textColor: theme.text,
|
|
2846
2979
|
cursorColor: theme.accent
|
|
2847
2980
|
}
|
|
2848
|
-
) : /* @__PURE__ */
|
|
2849
|
-
Array.isArray(swatch) ? /* @__PURE__ */
|
|
2981
|
+
) : /* @__PURE__ */ jsxs12("text", { wrapMode: "none", children: [
|
|
2982
|
+
Array.isArray(swatch) ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
|
|
2850
2983
|
/* @__PURE__ */ jsx13("span", { fg: swatch[0], children: "\u2588" }),
|
|
2851
2984
|
/* @__PURE__ */ jsx13("span", { fg: swatch[1], children: "\u2588" }),
|
|
2852
2985
|
/* @__PURE__ */ jsx13("span", { fg: swatch[2], children: "\u2588" }),
|
|
@@ -2857,7 +2990,7 @@ function ColorRow({
|
|
|
2857
2990
|
] }) });
|
|
2858
2991
|
}
|
|
2859
2992
|
|
|
2860
|
-
// tui/hooks/useDeferredLoading.ts
|
|
2993
|
+
// src/tui/hooks/useDeferredLoading.ts
|
|
2861
2994
|
import { useEffect as useEffect4, useRef as useRef3, useState as useState2 } from "react";
|
|
2862
2995
|
function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } = {}) {
|
|
2863
2996
|
const [visible, setVisible] = useState2(isLoading && showDelay === 0);
|
|
@@ -2885,10 +3018,10 @@ function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } =
|
|
|
2885
3018
|
return visible;
|
|
2886
3019
|
}
|
|
2887
3020
|
|
|
2888
|
-
// tui/hooks/useLoader.ts
|
|
3021
|
+
// src/tui/hooks/useLoader.ts
|
|
2889
3022
|
import { useEffect as useEffect5, useRef as useRef4, useState as useState3 } from "react";
|
|
2890
3023
|
|
|
2891
|
-
// github.ts
|
|
3024
|
+
// src/github.ts
|
|
2892
3025
|
import { execFile } from "node:child_process";
|
|
2893
3026
|
import { createHash } from "node:crypto";
|
|
2894
3027
|
import { statSync } from "node:fs";
|
|
@@ -3132,6 +3265,8 @@ async function fetchPrSizes(prs) {
|
|
|
3132
3265
|
additions
|
|
3133
3266
|
deletions
|
|
3134
3267
|
changedFiles
|
|
3268
|
+
mergedAt
|
|
3269
|
+
closedAt
|
|
3135
3270
|
comments {
|
|
3136
3271
|
totalCount
|
|
3137
3272
|
}
|
|
@@ -3150,7 +3285,7 @@ async function fetchPrSizes(prs) {
|
|
|
3150
3285
|
return prs.map((pr, i) => data[`pr${i}`]?.pullRequest ?? null);
|
|
3151
3286
|
}
|
|
3152
3287
|
|
|
3153
|
-
// data.ts
|
|
3288
|
+
// src/data.ts
|
|
3154
3289
|
var BATCH_SIZE = 25;
|
|
3155
3290
|
var MAX_CONCURRENT_BATCHES = 4;
|
|
3156
3291
|
function createLimiter(maxConcurrent) {
|
|
@@ -3332,6 +3467,8 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
|
|
|
3332
3467
|
additions: details.additions,
|
|
3333
3468
|
deletions: details.deletions,
|
|
3334
3469
|
total: details.additions + details.deletions,
|
|
3470
|
+
mergedAt: details.mergedAt === null ? null : new Date(details.mergedAt),
|
|
3471
|
+
closedAt: details.closedAt === null ? null : new Date(details.closedAt),
|
|
3335
3472
|
comments: { discussion, review, total: discussion + review }
|
|
3336
3473
|
});
|
|
3337
3474
|
}
|
|
@@ -3339,7 +3476,7 @@ async function fetchSizeRaw(prs, onProgress, options = {}) {
|
|
|
3339
3476
|
return { sizes, cacheHits };
|
|
3340
3477
|
}
|
|
3341
3478
|
|
|
3342
|
-
// tui/data/load.ts
|
|
3479
|
+
// src/tui/data/load.ts
|
|
3343
3480
|
function reviveRawData(data) {
|
|
3344
3481
|
return {
|
|
3345
3482
|
...data,
|
|
@@ -3355,7 +3492,12 @@ function reviveRawData(data) {
|
|
|
3355
3492
|
return { ...result, pr };
|
|
3356
3493
|
}),
|
|
3357
3494
|
sizes: data.sizes.map((entry) => {
|
|
3358
|
-
return {
|
|
3495
|
+
return {
|
|
3496
|
+
...entry,
|
|
3497
|
+
pr: { ...entry.pr, createdAt: new Date(entry.pr.createdAt) },
|
|
3498
|
+
mergedAt: entry.mergedAt === null ? null : new Date(entry.mergedAt),
|
|
3499
|
+
closedAt: entry.closedAt === null ? null : new Date(entry.closedAt)
|
|
3500
|
+
};
|
|
3359
3501
|
})
|
|
3360
3502
|
};
|
|
3361
3503
|
}
|
|
@@ -3455,7 +3597,7 @@ async function loadData(options, onPhase, { bypassCache = false } = {}) {
|
|
|
3455
3597
|
return data;
|
|
3456
3598
|
}
|
|
3457
3599
|
|
|
3458
|
-
// tui/hooks/useLoader.ts
|
|
3600
|
+
// src/tui/hooks/useLoader.ts
|
|
3459
3601
|
function useLoader(options, noCache2, onLoaded) {
|
|
3460
3602
|
const [startupSnapshot] = useState3(() => noCache2 ? null : loadSnapshot(options));
|
|
3461
3603
|
const [raw, setRaw] = useState3(startupSnapshot);
|
|
@@ -3526,10 +3668,10 @@ function useLoader(options, noCache2, onLoaded) {
|
|
|
3526
3668
|
};
|
|
3527
3669
|
}
|
|
3528
3670
|
|
|
3529
|
-
// tui/hooks/useViewModel.ts
|
|
3671
|
+
// src/tui/hooks/useViewModel.ts
|
|
3530
3672
|
import { useMemo } from "react";
|
|
3531
3673
|
|
|
3532
|
-
// tui/views/repos.ts
|
|
3674
|
+
// src/tui/views/repos.ts
|
|
3533
3675
|
function reviewDetail(counts) {
|
|
3534
3676
|
return `${counts.reviewed} reviewed` + (counts.pending > 0 ? `, ${counts.pending} pending` : "");
|
|
3535
3677
|
}
|
|
@@ -3623,6 +3765,38 @@ function buildOpenRepoOptions(raw) {
|
|
|
3623
3765
|
})
|
|
3624
3766
|
];
|
|
3625
3767
|
}
|
|
3768
|
+
function mergedDetail(counts) {
|
|
3769
|
+
return `${counts.merged} merged` + (counts.closed > 0 ? `, ${counts.closed} closed unmerged` : "");
|
|
3770
|
+
}
|
|
3771
|
+
function buildMergedRepoOptions(raw) {
|
|
3772
|
+
const countsByRepo = /* @__PURE__ */ new Map();
|
|
3773
|
+
for (const size of raw.sizes) {
|
|
3774
|
+
const counts = countsByRepo.get(size.pr.repo) ?? { merged: 0, closed: 0 };
|
|
3775
|
+
if (size.mergedAt !== null) {
|
|
3776
|
+
counts.merged += 1;
|
|
3777
|
+
} else if (size.pr.state !== "open") {
|
|
3778
|
+
counts.closed += 1;
|
|
3779
|
+
}
|
|
3780
|
+
countsByRepo.set(size.pr.repo, counts);
|
|
3781
|
+
}
|
|
3782
|
+
if (countsByRepo.size < 2) {
|
|
3783
|
+
return [];
|
|
3784
|
+
}
|
|
3785
|
+
const entries = [...countsByRepo.entries()].toSorted(
|
|
3786
|
+
(a, b) => b[1].merged - a[1].merged || b[1].closed - a[1].closed || a[0].localeCompare(b[0])
|
|
3787
|
+
);
|
|
3788
|
+
const totals = { merged: 0, closed: 0 };
|
|
3789
|
+
for (const [, counts] of entries) {
|
|
3790
|
+
totals.merged += counts.merged;
|
|
3791
|
+
totals.closed += counts.closed;
|
|
3792
|
+
}
|
|
3793
|
+
return [
|
|
3794
|
+
{ repo: null, label: "All repos", detail: mergedDetail(totals) },
|
|
3795
|
+
...entries.map(([repo, counts]) => {
|
|
3796
|
+
return { repo, label: repo, detail: mergedDetail(counts) };
|
|
3797
|
+
})
|
|
3798
|
+
];
|
|
3799
|
+
}
|
|
3626
3800
|
function commentDetail(comments, prs) {
|
|
3627
3801
|
return `${comments} ${comments === 1 ? "comment" : "comments"} on ${prs} ${prs === 1 ? "PR" : "PRs"}`;
|
|
3628
3802
|
}
|
|
@@ -3653,7 +3827,7 @@ function buildCommentRepoOptions(raw) {
|
|
|
3653
3827
|
];
|
|
3654
3828
|
}
|
|
3655
3829
|
|
|
3656
|
-
// tui/views/charts/draw.ts
|
|
3830
|
+
// src/tui/views/charts/draw.ts
|
|
3657
3831
|
function blankCells(width) {
|
|
3658
3832
|
return Array.from({ length: width }, () => {
|
|
3659
3833
|
return { ch: " " };
|
|
@@ -3706,7 +3880,7 @@ function hbar(fraction, width, color) {
|
|
|
3706
3880
|
return line;
|
|
3707
3881
|
}
|
|
3708
3882
|
|
|
3709
|
-
// tui/views/charts/distribution.ts
|
|
3883
|
+
// src/tui/views/charts/distribution.ts
|
|
3710
3884
|
var DURATION_TICKS = [5 / 60, 0.25, 0.5, 1, 2, 4, 8, 24, 48, 96, 168, 336, 720, 2160];
|
|
3711
3885
|
var COUNT_TICKS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4, 25e3, 5e4, 1e5];
|
|
3712
3886
|
function buildDistribution({ values, width, format, ticks, flat }) {
|
|
@@ -3785,7 +3959,7 @@ function buildDistribution({ values, width, format, ticks, flat }) {
|
|
|
3785
3959
|
};
|
|
3786
3960
|
}
|
|
3787
3961
|
|
|
3788
|
-
// tui/views/charts/gauge.ts
|
|
3962
|
+
// src/tui/views/charts/gauge.ts
|
|
3789
3963
|
var GAUGE_BAR_WIDTH = 24;
|
|
3790
3964
|
function buildGaugeCard({ title, subtitle, rows }) {
|
|
3791
3965
|
const total = rows.reduce((sum, row) => sum + row.count, 0);
|
|
@@ -3808,7 +3982,7 @@ function buildGaugeCard({ title, subtitle, rows }) {
|
|
|
3808
3982
|
return { title, subtitle, lines };
|
|
3809
3983
|
}
|
|
3810
3984
|
|
|
3811
|
-
// tui/views/charts/heatmap.ts
|
|
3985
|
+
// src/tui/views/charts/heatmap.ts
|
|
3812
3986
|
var WEEKDAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
3813
3987
|
function heatColor(count2) {
|
|
3814
3988
|
if (count2 === 1) {
|
|
@@ -3874,7 +4048,7 @@ function buildHeatmapCard({ title, subtitle, grid, columns, legend }) {
|
|
|
3874
4048
|
return { title, subtitle, lines };
|
|
3875
4049
|
}
|
|
3876
4050
|
|
|
3877
|
-
// tui/views/charts/histogram.ts
|
|
4051
|
+
// src/tui/views/charts/histogram.ts
|
|
3878
4052
|
var HIST_BAR_WIDTH = 36;
|
|
3879
4053
|
function axisStep(maxCount) {
|
|
3880
4054
|
for (const step of [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1e3, 2e3, 5e3]) {
|
|
@@ -3926,7 +4100,7 @@ function buildHistogramCard({ title, subtitle, values, buckets, format }) {
|
|
|
3926
4100
|
return { title, subtitle, lines };
|
|
3927
4101
|
}
|
|
3928
4102
|
|
|
3929
|
-
// tui/views/charts/scatter.ts
|
|
4103
|
+
// src/tui/views/charts/scatter.ts
|
|
3930
4104
|
var SCATTER_WIDTH = 36;
|
|
3931
4105
|
var SCATTER_HEIGHT = 8;
|
|
3932
4106
|
var DOT_BITS = [
|
|
@@ -3976,7 +4150,7 @@ function buildScatterCard({ title, subtitle, points, formatX, formatY }) {
|
|
|
3976
4150
|
return { title, subtitle, lines };
|
|
3977
4151
|
}
|
|
3978
4152
|
|
|
3979
|
-
// tui/views/charts/spread.ts
|
|
4153
|
+
// src/tui/views/charts/spread.ts
|
|
3980
4154
|
var SPREAD_STRIP_WIDTH = 32;
|
|
3981
4155
|
function buildSpreadCard(title, metrics, format) {
|
|
3982
4156
|
const rows = metrics.map(({ label, values }) => {
|
|
@@ -3986,9 +4160,9 @@ function buildSpreadCard(title, metrics, format) {
|
|
|
3986
4160
|
const span = Math.log1p(max) - Math.log1p(min);
|
|
3987
4161
|
const pos = (value2) => {
|
|
3988
4162
|
if (span === 0) {
|
|
3989
|
-
return
|
|
4163
|
+
return 1;
|
|
3990
4164
|
}
|
|
3991
|
-
return Math.round((Math.log1p(value2) - Math.log1p(min)) / span * (SPREAD_STRIP_WIDTH -
|
|
4165
|
+
return 1 + Math.round((Math.log1p(value2) - Math.log1p(min)) / span * (SPREAD_STRIP_WIDTH - 3));
|
|
3992
4166
|
};
|
|
3993
4167
|
const cells = blankCells(SPREAD_STRIP_WIDTH);
|
|
3994
4168
|
for (let i = 0; i < SPREAD_STRIP_WIDTH; i++) {
|
|
@@ -4012,13 +4186,19 @@ function buildSpreadCard(title, metrics, format) {
|
|
|
4012
4186
|
{ text: ` ${row.max}`, fg: theme.text }
|
|
4013
4187
|
];
|
|
4014
4188
|
});
|
|
4015
|
-
|
|
4189
|
+
const subtitle = [
|
|
4190
|
+
{ text: "\u2592", fg: theme.chartBar },
|
|
4191
|
+
{ text: " p25-p75, ", fg: theme.muted },
|
|
4192
|
+
{ text: "\u2588", fg: theme.accent },
|
|
4193
|
+
{ text: " p50, log scale", fg: theme.muted }
|
|
4194
|
+
];
|
|
4195
|
+
return { title, subtitle, lines };
|
|
4016
4196
|
}
|
|
4017
4197
|
|
|
4018
|
-
// tui/views/charts/trend.ts
|
|
4198
|
+
// src/tui/views/charts/trend.ts
|
|
4019
4199
|
var asciichart = __toESM(require_asciichart(), 1);
|
|
4020
4200
|
|
|
4021
|
-
// tui/views/charts/weeks.ts
|
|
4201
|
+
// src/tui/views/charts/weeks.ts
|
|
4022
4202
|
var DAY_MS = 864e5;
|
|
4023
4203
|
var WEEK_MS = 7 * DAY_MS;
|
|
4024
4204
|
function mondayOf(dayUtcMs) {
|
|
@@ -4042,7 +4222,7 @@ function weekAxisRow(width, prefix, points, columnOf, mondayOfPoint) {
|
|
|
4042
4222
|
return mergeCells(cells);
|
|
4043
4223
|
}
|
|
4044
4224
|
|
|
4045
|
-
// tui/views/charts/trend.ts
|
|
4225
|
+
// src/tui/views/charts/trend.ts
|
|
4046
4226
|
var TREND_POINTS = 40;
|
|
4047
4227
|
var TREND_HEIGHT = 6;
|
|
4048
4228
|
function axisSplit(line) {
|
|
@@ -4136,7 +4316,7 @@ function buildTrendCard({ title, entries, format, floor }) {
|
|
|
4136
4316
|
return { title, subtitle, lines };
|
|
4137
4317
|
}
|
|
4138
4318
|
|
|
4139
|
-
// tui/views/charts/volume.ts
|
|
4319
|
+
// src/tui/views/charts/volume.ts
|
|
4140
4320
|
var VOLUME_ROWS = 4;
|
|
4141
4321
|
var VOLUME_MAX_BARS = 18;
|
|
4142
4322
|
var V_PARTIALS = [" ", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
@@ -4192,7 +4372,7 @@ function buildVolumeCard(title, dates, weights) {
|
|
|
4192
4372
|
return { title, subtitle, lines };
|
|
4193
4373
|
}
|
|
4194
4374
|
|
|
4195
|
-
// tui/views/stats.ts
|
|
4375
|
+
// src/tui/views/stats.ts
|
|
4196
4376
|
function countCell(count2, label, dimWhenZero = false) {
|
|
4197
4377
|
const dim2 = dimWhenZero && count2 === 0;
|
|
4198
4378
|
return [
|
|
@@ -4497,11 +4677,122 @@ function buildCommentView(raw, repo = null, width = 100) {
|
|
|
4497
4677
|
}
|
|
4498
4678
|
return { empty: null, ...base, strip, headline, left, right, distribution, lists };
|
|
4499
4679
|
}
|
|
4680
|
+
function buildMergedView(raw, repo = null, width = 100) {
|
|
4681
|
+
const base = {
|
|
4682
|
+
strip: [],
|
|
4683
|
+
headline: null,
|
|
4684
|
+
distributionTitle: "Time to merge distribution",
|
|
4685
|
+
noCharts: "No merged PRs to chart.",
|
|
4686
|
+
left: [],
|
|
4687
|
+
right: [],
|
|
4688
|
+
distribution: null,
|
|
4689
|
+
lists: []
|
|
4690
|
+
};
|
|
4691
|
+
if (raw.authoredTotal === 0) {
|
|
4692
|
+
return { empty: "No authored PRs found.", ...base };
|
|
4693
|
+
}
|
|
4694
|
+
const sizes = repo === null ? raw.sizes : raw.sizes.filter((size) => size.pr.repo === repo);
|
|
4695
|
+
if (sizes.length === 0) {
|
|
4696
|
+
return { empty: "No accessible authored PRs to analyze.", ...base };
|
|
4697
|
+
}
|
|
4698
|
+
const stats = computeMergeStats(sizes);
|
|
4699
|
+
const strip = [
|
|
4700
|
+
countCell(sizes.length, "PRs created"),
|
|
4701
|
+
countCell(stats.merged.length, "merged"),
|
|
4702
|
+
countCell(stats.closed.length, "closed unmerged", true),
|
|
4703
|
+
countCell(stats.open.length, "still open", true)
|
|
4704
|
+
];
|
|
4705
|
+
if (repo === null) {
|
|
4706
|
+
strip.push([{ text: `${raw.authoredTotal - raw.sizes.length} inaccessible (excluded)`, fg: theme.dim }]);
|
|
4707
|
+
}
|
|
4708
|
+
const lists = [];
|
|
4709
|
+
if (stats.merged.length > 0) {
|
|
4710
|
+
const top = stats.merged.slice(0, 5);
|
|
4711
|
+
lists.push({
|
|
4712
|
+
title: "Recently merged PRs",
|
|
4713
|
+
rows: toPrRows(
|
|
4714
|
+
top.map((result) => result.entry),
|
|
4715
|
+
top.map((result) => `${durationLead(result)} to merge`)
|
|
4716
|
+
)
|
|
4717
|
+
});
|
|
4718
|
+
}
|
|
4719
|
+
if (stats.closed.length > 0) {
|
|
4720
|
+
const top = stats.closed.slice(0, 5);
|
|
4721
|
+
lists.push({
|
|
4722
|
+
title: "Closed without merging",
|
|
4723
|
+
rows: toPrRows(
|
|
4724
|
+
top.map((result) => result.entry),
|
|
4725
|
+
top.map((result) => `${durationLead(result)} to close`)
|
|
4726
|
+
)
|
|
4727
|
+
});
|
|
4728
|
+
}
|
|
4729
|
+
if (stats.merged.length === 0) {
|
|
4730
|
+
return { empty: null, ...base, strip, lists };
|
|
4731
|
+
}
|
|
4732
|
+
const sorted = [...stats.allHours].toSorted((a, b) => a - b);
|
|
4733
|
+
const headline = [
|
|
4734
|
+
{ text: "p50 ", fg: theme.muted },
|
|
4735
|
+
{ text: formatDuration(percentile(sorted, 50)), fg: theme.accent },
|
|
4736
|
+
{ text: " p90 ", fg: theme.muted },
|
|
4737
|
+
{ text: formatDuration(percentile(sorted, 90)), fg: theme.accent },
|
|
4738
|
+
{ text: ` ${stats.merged.length} of ${sizes.length} PRs merged`, fg: theme.muted }
|
|
4739
|
+
];
|
|
4740
|
+
const mergeDates = stats.merged.map((result) => result.mergedAt);
|
|
4741
|
+
const created = sizes.map((size) => size.pr.createdAt);
|
|
4742
|
+
const left = [
|
|
4743
|
+
buildHistogramCard({
|
|
4744
|
+
title: "Time to merge",
|
|
4745
|
+
subtitle: "elapsed time, created \u2192 merged",
|
|
4746
|
+
values: stats.allHours,
|
|
4747
|
+
buckets: currentBuckets(),
|
|
4748
|
+
format: formatDuration
|
|
4749
|
+
}),
|
|
4750
|
+
buildHeatmapCard({
|
|
4751
|
+
title: "When your PRs merge",
|
|
4752
|
+
subtitle: "PRs merged, weekday \xD7 hour, local time",
|
|
4753
|
+
grid: mergeDates,
|
|
4754
|
+
columns: [
|
|
4755
|
+
{ label: "merged", dates: mergeDates },
|
|
4756
|
+
{ label: "created", dates: created, muted: true }
|
|
4757
|
+
],
|
|
4758
|
+
legend: "PRs merged in that hour"
|
|
4759
|
+
}),
|
|
4760
|
+
buildGaugeCard({
|
|
4761
|
+
title: "Outcomes",
|
|
4762
|
+
subtitle: "where your authored PRs ended up",
|
|
4763
|
+
rows: [
|
|
4764
|
+
{ label: "merged", count: stats.merged.length, color: theme.accent },
|
|
4765
|
+
{ label: "closed unmerged", count: stats.closed.length, color: theme.warn },
|
|
4766
|
+
...stats.open.length > 0 ? [{ label: "still open", count: stats.open.length, color: theme.chartDim }] : []
|
|
4767
|
+
]
|
|
4768
|
+
})
|
|
4769
|
+
];
|
|
4770
|
+
const right = [
|
|
4771
|
+
buildTrendCard({
|
|
4772
|
+
title: "Time to merge trend",
|
|
4773
|
+
entries: stats.merged.map((result) => {
|
|
4774
|
+
return { date: result.mergedAt, value: result.hours };
|
|
4775
|
+
}),
|
|
4776
|
+
format: formatDuration,
|
|
4777
|
+
floor: 1 / 60
|
|
4778
|
+
}),
|
|
4779
|
+
buildVolumeCard("PRs merged per week", mergeDates),
|
|
4780
|
+
buildVolumeCard("PRs created per week", created)
|
|
4781
|
+
];
|
|
4782
|
+
const distribution = buildDistribution({
|
|
4783
|
+
values: stats.allHours,
|
|
4784
|
+
width: Math.max(width - 3, 40),
|
|
4785
|
+
format: formatDuration,
|
|
4786
|
+
ticks: DURATION_TICKS,
|
|
4787
|
+
flat: (n, value2) => `all ${n} merged ${n === 1 ? "PR" : "PRs"} took ${value2}`
|
|
4788
|
+
});
|
|
4789
|
+
return { empty: null, ...base, strip, headline, left, right, distribution, lists };
|
|
4790
|
+
}
|
|
4500
4791
|
function count(value2) {
|
|
4501
4792
|
return formatCount(Math.round(value2));
|
|
4502
4793
|
}
|
|
4503
4794
|
|
|
4504
|
-
// tui/hooks/useViewModel.ts
|
|
4795
|
+
// src/tui/hooks/useViewModel.ts
|
|
4505
4796
|
function resolveScope(scope, repos) {
|
|
4506
4797
|
if (repos.length === 0) {
|
|
4507
4798
|
return { view: "detail", repo: null };
|
|
@@ -4524,11 +4815,13 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
|
|
|
4524
4815
|
const sizeTarget = options.sizeTarget === "" ? void 0 : parseSizeTarget(options.sizeTarget);
|
|
4525
4816
|
const pendingRepos = buildPendingRepoOptions(raw);
|
|
4526
4817
|
const openRepos = buildOpenRepoOptions(raw);
|
|
4818
|
+
const mergedRepos = buildMergedRepoOptions(raw);
|
|
4527
4819
|
const reviewRepos = buildReviewRepoOptions(raw);
|
|
4528
4820
|
const sizeRepos = buildSizeRepoOptions(raw);
|
|
4529
4821
|
const commentRepos = buildCommentRepoOptions(raw);
|
|
4530
4822
|
const pendingScope = resolveScope(scopes.pending, pendingRepos);
|
|
4531
4823
|
const openScope = resolveScope(scopes.open, openRepos);
|
|
4824
|
+
const mergedScope = resolveScope(scopes.merged, mergedRepos);
|
|
4532
4825
|
const reviewScope = resolveScope(scopes.review, reviewRepos);
|
|
4533
4826
|
const sizeScope = resolveScope(scopes.size, sizeRepos);
|
|
4534
4827
|
const commentScope = resolveScope(scopes.comment, commentRepos);
|
|
@@ -4536,16 +4829,19 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
|
|
|
4536
4829
|
return {
|
|
4537
4830
|
pendingRepos,
|
|
4538
4831
|
openRepos,
|
|
4832
|
+
mergedRepos,
|
|
4539
4833
|
reviewRepos,
|
|
4540
4834
|
sizeRepos,
|
|
4541
4835
|
commentRepos,
|
|
4542
4836
|
pendingScope,
|
|
4543
4837
|
openScope,
|
|
4838
|
+
mergedScope,
|
|
4544
4839
|
reviewScope,
|
|
4545
4840
|
sizeScope,
|
|
4546
4841
|
commentScope,
|
|
4547
4842
|
pending: pendingScope.view === "detail" ? buildPendingReviewView(raw, pendingScope.repo, grouping.pending) : null,
|
|
4548
4843
|
open: openScope.view === "detail" ? buildOpenAuthoredView(raw, openScope.repo, grouping.open) : null,
|
|
4844
|
+
merged: mergedScope.view === "detail" ? buildMergedView(raw, mergedScope.repo, width) : null,
|
|
4549
4845
|
review,
|
|
4550
4846
|
size: sizeScope.view === "detail" ? buildSizeView(raw, sizeTarget, sizeScope.repo, width) : null,
|
|
4551
4847
|
comments: commentScope.view === "detail" ? buildCommentView(raw, commentScope.repo, width) : null
|
|
@@ -4560,6 +4856,7 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
|
|
|
4560
4856
|
width,
|
|
4561
4857
|
scopes.pending,
|
|
4562
4858
|
scopes.open,
|
|
4859
|
+
scopes.merged,
|
|
4563
4860
|
scopes.review,
|
|
4564
4861
|
scopes.size,
|
|
4565
4862
|
scopes.comment,
|
|
@@ -4569,7 +4866,7 @@ function useViewModel(raw, options, width, scopes, grouping, themeEpoch) {
|
|
|
4569
4866
|
]);
|
|
4570
4867
|
}
|
|
4571
4868
|
|
|
4572
|
-
// tui/keymap.ts
|
|
4869
|
+
// src/tui/keymap.ts
|
|
4573
4870
|
function handleOptionsModalKey(key, context) {
|
|
4574
4871
|
switch (key.name) {
|
|
4575
4872
|
case "escape": {
|
|
@@ -4648,6 +4945,12 @@ function handleSettingsModalKey(key, context) {
|
|
|
4648
4945
|
}
|
|
4649
4946
|
break;
|
|
4650
4947
|
}
|
|
4948
|
+
case "copyLinks": {
|
|
4949
|
+
const next = !context.copyLinks;
|
|
4950
|
+
context.setCopyLinks(next);
|
|
4951
|
+
context.dispatchUi({ type: "cacheActionReported", action: saveCopyLinks(next) ? "saved" : "notSaved" });
|
|
4952
|
+
break;
|
|
4953
|
+
}
|
|
4651
4954
|
case "themePreset": {
|
|
4652
4955
|
const next = cycleTheme(context.themeState, key.name === "left" ? -1 : 1);
|
|
4653
4956
|
applyThemeState(next);
|
|
@@ -4770,15 +5073,27 @@ function handleQueueKey(key, context) {
|
|
|
4770
5073
|
}
|
|
4771
5074
|
case "return": {
|
|
4772
5075
|
const cursor = context.browse.rowCursors[tab];
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
5076
|
+
const row = rows[Math.min(cursor, rows.length - 1)];
|
|
5077
|
+
if (context.copyLinks) {
|
|
5078
|
+
context.copyRow(row);
|
|
5079
|
+
} else {
|
|
5080
|
+
context.openUrl(row.url, (message) => {
|
|
5081
|
+
context.dispatchUi({ type: "openErrorReported", message });
|
|
5082
|
+
});
|
|
5083
|
+
}
|
|
4776
5084
|
break;
|
|
4777
5085
|
}
|
|
4778
5086
|
}
|
|
4779
5087
|
}
|
|
4780
5088
|
function statsTabOf(context) {
|
|
4781
5089
|
const { views } = context;
|
|
5090
|
+
if (context.browse.tab === 1) {
|
|
5091
|
+
return {
|
|
5092
|
+
key: "merged",
|
|
5093
|
+
repos: views?.mergedRepos ?? [],
|
|
5094
|
+
scope: views?.mergedScope ?? null
|
|
5095
|
+
};
|
|
5096
|
+
}
|
|
4782
5097
|
if (context.browse.tab === 2) {
|
|
4783
5098
|
return {
|
|
4784
5099
|
key: "review",
|
|
@@ -4828,7 +5143,7 @@ function handleStatsKey(key, context) {
|
|
|
4828
5143
|
}
|
|
4829
5144
|
}
|
|
4830
5145
|
function handleAppKey(key, context) {
|
|
4831
|
-
context.dispatchUi({ type: "
|
|
5146
|
+
context.dispatchUi({ type: "noticesDismissed" });
|
|
4832
5147
|
if (context.ui.editing) {
|
|
4833
5148
|
if (key.name === "escape") {
|
|
4834
5149
|
context.dispatchUi({ type: "editCancelled" });
|
|
@@ -4865,14 +5180,16 @@ function handleAppKey(key, context) {
|
|
|
4865
5180
|
context.dispatchBrowse({ type: "tabCycled", delta: -1 });
|
|
4866
5181
|
} else if (key.name === "right" || key.name === "tab") {
|
|
4867
5182
|
context.dispatchBrowse({ type: "tabCycled", delta: 1 });
|
|
4868
|
-
} else if (context.browse.tab
|
|
5183
|
+
} else if (context.browse.tab === 1 && key.name === "t") {
|
|
5184
|
+
context.dispatchBrowse({ type: "subTabToggled" });
|
|
5185
|
+
} else if (context.browse.tab === 0 || context.browse.tab === 1 && context.browse.authoredTab === "open") {
|
|
4869
5186
|
handleQueueKey(key, context);
|
|
4870
5187
|
} else {
|
|
4871
5188
|
handleStatsKey(key, context);
|
|
4872
5189
|
}
|
|
4873
5190
|
}
|
|
4874
5191
|
|
|
4875
|
-
// tui/state/ui.ts
|
|
5192
|
+
// src/tui/state/ui.ts
|
|
4876
5193
|
var initialUiState = {
|
|
4877
5194
|
modal: null,
|
|
4878
5195
|
editing: false,
|
|
@@ -4882,19 +5199,29 @@ var initialUiState = {
|
|
|
4882
5199
|
fieldError: null,
|
|
4883
5200
|
themeColorError: null,
|
|
4884
5201
|
cacheAction: null,
|
|
4885
|
-
openError: null
|
|
5202
|
+
openError: null,
|
|
5203
|
+
copyNotice: null
|
|
4886
5204
|
};
|
|
4887
5205
|
function cycled2(previous, delta, count2) {
|
|
4888
5206
|
return (previous + count2 + delta) % count2;
|
|
4889
5207
|
}
|
|
4890
5208
|
function uiReducer(state, action) {
|
|
4891
5209
|
switch (action.type) {
|
|
4892
|
-
case "
|
|
4893
|
-
|
|
5210
|
+
case "noticesDismissed": {
|
|
5211
|
+
if (state.openError === null && state.copyNotice === null) {
|
|
5212
|
+
return state;
|
|
5213
|
+
}
|
|
5214
|
+
return { ...state, openError: null, copyNotice: null };
|
|
4894
5215
|
}
|
|
4895
5216
|
case "openErrorReported": {
|
|
4896
5217
|
return { ...state, openError: action.message };
|
|
4897
5218
|
}
|
|
5219
|
+
case "copyReported": {
|
|
5220
|
+
return { ...state, openError: null, copyNotice: { text: action.message } };
|
|
5221
|
+
}
|
|
5222
|
+
case "copyNoticeExpired": {
|
|
5223
|
+
return { ...state, copyNotice: null };
|
|
5224
|
+
}
|
|
4898
5225
|
case "modalOpened": {
|
|
4899
5226
|
return { ...state, modal: action.modal, fieldError: null, themeColorError: null, cacheAction: null };
|
|
4900
5227
|
}
|
|
@@ -4959,7 +5286,7 @@ function uiReducer(state, action) {
|
|
|
4959
5286
|
return state;
|
|
4960
5287
|
}
|
|
4961
5288
|
|
|
4962
|
-
// tui/utils/browser.ts
|
|
5289
|
+
// src/tui/utils/browser.ts
|
|
4963
5290
|
import { spawn } from "node:child_process";
|
|
4964
5291
|
function openCommand(url) {
|
|
4965
5292
|
switch (process.platform) {
|
|
@@ -4988,14 +5315,50 @@ function openInBrowser(url, onError) {
|
|
|
4988
5315
|
child.unref();
|
|
4989
5316
|
}
|
|
4990
5317
|
|
|
4991
|
-
// tui/
|
|
4992
|
-
import {
|
|
5318
|
+
// src/tui/utils/clipboard.ts
|
|
5319
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
5320
|
+
function copyCommand() {
|
|
5321
|
+
switch (process.platform) {
|
|
5322
|
+
case "darwin": {
|
|
5323
|
+
return { command: "pbcopy", args: [] };
|
|
5324
|
+
}
|
|
5325
|
+
case "win32": {
|
|
5326
|
+
return { command: "clip", args: [] };
|
|
5327
|
+
}
|
|
5328
|
+
default: {
|
|
5329
|
+
if (process.env.WAYLAND_DISPLAY !== void 0 && process.env.WAYLAND_DISPLAY !== "") {
|
|
5330
|
+
return { command: "wl-copy", args: [] };
|
|
5331
|
+
}
|
|
5332
|
+
return { command: "xclip", args: ["-selection", "clipboard"] };
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
function copyToClipboard(text, onError) {
|
|
5337
|
+
const { command, args } = copyCommand();
|
|
5338
|
+
const child = spawn2(command, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
5339
|
+
child.on("error", (error) => {
|
|
5340
|
+
onError(`could not copy the link (${error.message})`);
|
|
5341
|
+
});
|
|
5342
|
+
child.on("exit", (code) => {
|
|
5343
|
+
if (code !== null && code !== 0) {
|
|
5344
|
+
onError(`could not copy the link (${command} exited with ${code})`);
|
|
5345
|
+
}
|
|
5346
|
+
});
|
|
5347
|
+
child.stdin.on("error", () => {
|
|
5348
|
+
});
|
|
5349
|
+
child.stdin.end(text);
|
|
5350
|
+
}
|
|
5351
|
+
|
|
5352
|
+
// src/tui/App.tsx
|
|
5353
|
+
import { jsx as jsx14, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
|
|
4993
5354
|
function App({
|
|
4994
5355
|
initial: initial2,
|
|
4995
5356
|
initialSaved = null,
|
|
4996
5357
|
initialNoCache = false,
|
|
5358
|
+
initialCopyLinks = false,
|
|
4997
5359
|
initialTheme = defaultThemeState(),
|
|
4998
5360
|
openUrl = openInBrowser,
|
|
5361
|
+
copyUrl = copyToClipboard,
|
|
4999
5362
|
onQuit
|
|
5000
5363
|
}) {
|
|
5001
5364
|
const { width } = useTerminalDimensions3();
|
|
@@ -5004,10 +5367,29 @@ function App({
|
|
|
5004
5367
|
const [options, setOptions] = useState4(initial2);
|
|
5005
5368
|
const [saved2, setSaved] = useState4(initialSaved);
|
|
5006
5369
|
const [noCache2, setNoCache] = useState4(initialNoCache);
|
|
5370
|
+
const [copyLinks2, setCopyLinks] = useState4(initialCopyLinks);
|
|
5007
5371
|
const [themeState, setThemeState] = useState4(initialTheme);
|
|
5372
|
+
const copyRow = (row) => {
|
|
5373
|
+
copyUrl(row.url, (message) => {
|
|
5374
|
+
dispatchUi({ type: "openErrorReported", message });
|
|
5375
|
+
});
|
|
5376
|
+
dispatchUi({ type: "copyReported", message: `copied ${row.ref} to the clipboard` });
|
|
5377
|
+
};
|
|
5378
|
+
useEffect6(() => {
|
|
5379
|
+
if (ui.copyNotice === null) {
|
|
5380
|
+
return void 0;
|
|
5381
|
+
}
|
|
5382
|
+
const timer = setTimeout(() => {
|
|
5383
|
+
dispatchUi({ type: "copyNoticeExpired" });
|
|
5384
|
+
}, 2500);
|
|
5385
|
+
return () => {
|
|
5386
|
+
clearTimeout(timer);
|
|
5387
|
+
};
|
|
5388
|
+
}, [ui.copyNotice]);
|
|
5008
5389
|
const reviewScrollRef = useRef5(null);
|
|
5009
5390
|
const sizeScrollRef = useRef5(null);
|
|
5010
5391
|
const commentScrollRef = useRef5(null);
|
|
5392
|
+
const mergedScrollRef = useRef5(null);
|
|
5011
5393
|
const { raw, isSnapshot, loading, load, error, stale, reload } = useLoader(options, noCache2, (data) => {
|
|
5012
5394
|
dispatchBrowse({
|
|
5013
5395
|
type: "dataLoaded",
|
|
@@ -5016,7 +5398,8 @@ function App({
|
|
|
5016
5398
|
open: buildOpenRepoOptions(data),
|
|
5017
5399
|
review: buildReviewRepoOptions(data),
|
|
5018
5400
|
size: buildSizeRepoOptions(data),
|
|
5019
|
-
comment: buildCommentRepoOptions(data)
|
|
5401
|
+
comment: buildCommentRepoOptions(data),
|
|
5402
|
+
merged: buildMergedRepoOptions(data)
|
|
5020
5403
|
}
|
|
5021
5404
|
});
|
|
5022
5405
|
});
|
|
@@ -5059,6 +5442,7 @@ function App({
|
|
|
5059
5442
|
ui,
|
|
5060
5443
|
browse,
|
|
5061
5444
|
noCache: noCache2,
|
|
5445
|
+
copyLinks: copyLinks2,
|
|
5062
5446
|
themeState,
|
|
5063
5447
|
options,
|
|
5064
5448
|
views,
|
|
@@ -5067,22 +5451,24 @@ function App({
|
|
|
5067
5451
|
setOptions,
|
|
5068
5452
|
setSaved,
|
|
5069
5453
|
setNoCache,
|
|
5454
|
+
setCopyLinks,
|
|
5070
5455
|
setThemeState,
|
|
5071
5456
|
quit: onQuit,
|
|
5072
5457
|
reload,
|
|
5073
5458
|
openUrl,
|
|
5459
|
+
copyRow,
|
|
5074
5460
|
beginEdit: (value2) => {
|
|
5075
5461
|
draftRef.current = value2;
|
|
5076
5462
|
dispatchUi({ type: "editStarted" });
|
|
5077
5463
|
},
|
|
5078
5464
|
scrollBy: (forTab, delta) => {
|
|
5079
|
-
const ref = forTab === 2 ? reviewScrollRef : forTab === 3 ? sizeScrollRef : commentScrollRef;
|
|
5465
|
+
const ref = forTab === 1 ? mergedScrollRef : forTab === 2 ? reviewScrollRef : forTab === 3 ? sizeScrollRef : commentScrollRef;
|
|
5080
5466
|
ref.current?.scrollBy(delta);
|
|
5081
5467
|
}
|
|
5082
5468
|
});
|
|
5083
5469
|
});
|
|
5084
5470
|
const capWarning = raw?.searchCapped ? "Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow since or repos." : null;
|
|
5085
|
-
return /* @__PURE__ */
|
|
5471
|
+
return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
|
|
5086
5472
|
/* @__PURE__ */ jsx14(Header, { options, raw, error, spinning: showLoad }),
|
|
5087
5473
|
/* @__PURE__ */ jsx14(TabBar, { tab: browse.tab }),
|
|
5088
5474
|
/* @__PURE__ */ jsx14(
|
|
@@ -5092,10 +5478,16 @@ function App({
|
|
|
5092
5478
|
browse,
|
|
5093
5479
|
warning: capWarning,
|
|
5094
5480
|
focused: ui.modal === null,
|
|
5095
|
-
scrollRefs: {
|
|
5481
|
+
scrollRefs: {
|
|
5482
|
+
review: reviewScrollRef,
|
|
5483
|
+
size: sizeScrollRef,
|
|
5484
|
+
comment: commentScrollRef,
|
|
5485
|
+
merged: mergedScrollRef
|
|
5486
|
+
},
|
|
5096
5487
|
error,
|
|
5097
5488
|
loading,
|
|
5098
|
-
load
|
|
5489
|
+
load,
|
|
5490
|
+
onRefClick: copyLinks2 ? copyRow : null
|
|
5099
5491
|
}
|
|
5100
5492
|
),
|
|
5101
5493
|
/* @__PURE__ */ jsx14(
|
|
@@ -5105,8 +5497,11 @@ function App({
|
|
|
5105
5497
|
modal: ui.modal,
|
|
5106
5498
|
editing: ui.editing,
|
|
5107
5499
|
tab: browse.tab,
|
|
5500
|
+
authoredTab: browse.authoredTab,
|
|
5108
5501
|
views,
|
|
5502
|
+
copyLinks: copyLinks2,
|
|
5109
5503
|
openError: ui.openError,
|
|
5504
|
+
copyNotice: ui.copyNotice === null ? null : ui.copyNotice.text,
|
|
5110
5505
|
stale
|
|
5111
5506
|
}
|
|
5112
5507
|
),
|
|
@@ -5130,6 +5525,7 @@ function App({
|
|
|
5130
5525
|
selected: ui.selectedSetting,
|
|
5131
5526
|
cacheAction: ui.cacheAction,
|
|
5132
5527
|
noCache: noCache2,
|
|
5528
|
+
copyLinks: copyLinks2,
|
|
5133
5529
|
preset: themeState.preset
|
|
5134
5530
|
}
|
|
5135
5531
|
),
|
|
@@ -5150,7 +5546,7 @@ function App({
|
|
|
5150
5546
|
] });
|
|
5151
5547
|
}
|
|
5152
5548
|
|
|
5153
|
-
// tui/bootstrap.ts
|
|
5549
|
+
// src/tui/bootstrap.ts
|
|
5154
5550
|
function bootstrap() {
|
|
5155
5551
|
const { values, explicit } = parseCliArgs();
|
|
5156
5552
|
if (values.help) {
|
|
@@ -5180,7 +5576,7 @@ function bootstrap() {
|
|
|
5180
5576
|
validateField(field.key, value2);
|
|
5181
5577
|
}
|
|
5182
5578
|
}
|
|
5183
|
-
return { initial: initial2, saved: saved2, noCache: values["no-cache"], theme: theme3 };
|
|
5579
|
+
return { initial: initial2, saved: saved2, noCache: values["no-cache"], copyLinks: settings.copyLinks === true, theme: theme3 };
|
|
5184
5580
|
} catch (error) {
|
|
5185
5581
|
if (error instanceof CliError) {
|
|
5186
5582
|
fail(error.message);
|
|
@@ -5189,9 +5585,9 @@ function bootstrap() {
|
|
|
5189
5585
|
}
|
|
5190
5586
|
}
|
|
5191
5587
|
|
|
5192
|
-
// tui/main.tsx
|
|
5588
|
+
// src/tui/main.tsx
|
|
5193
5589
|
import { jsx as jsx15 } from "@opentui/react/jsx-runtime";
|
|
5194
|
-
var { initial, saved, noCache, theme: theme2 } = bootstrap();
|
|
5590
|
+
var { initial, saved, noCache, copyLinks, theme: theme2 } = bootstrap();
|
|
5195
5591
|
var renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
5196
5592
|
for (const signal of ["SIGTERM", "SIGHUP"]) {
|
|
5197
5593
|
process.on(signal, () => {
|
|
@@ -5206,6 +5602,7 @@ createRoot(renderer).render(
|
|
|
5206
5602
|
initial,
|
|
5207
5603
|
initialSaved: saved,
|
|
5208
5604
|
initialNoCache: noCache,
|
|
5605
|
+
initialCopyLinks: copyLinks,
|
|
5209
5606
|
initialTheme: theme2,
|
|
5210
5607
|
onQuit: () => {
|
|
5211
5608
|
renderer.destroy();
|