@f0rbit/overview 0.1.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/bin/overview +10 -0
- package/dist/overview.js +10361 -0
- package/package.json +22 -15
- package/bunfig.toml +0 -7
- package/packages/core/__tests__/concurrency.test.ts +0 -111
- package/packages/core/__tests__/helpers.ts +0 -60
- package/packages/core/__tests__/integration/git-status.test.ts +0 -62
- package/packages/core/__tests__/integration/scanner.test.ts +0 -140
- package/packages/core/__tests__/ocn.test.ts +0 -164
- package/packages/core/package.json +0 -13
- package/packages/core/src/cache.ts +0 -31
- package/packages/core/src/concurrency.ts +0 -44
- package/packages/core/src/devpad.ts +0 -61
- package/packages/core/src/git-graph.ts +0 -54
- package/packages/core/src/git-stats.ts +0 -201
- package/packages/core/src/git-status.ts +0 -316
- package/packages/core/src/github.ts +0 -286
- package/packages/core/src/index.ts +0 -58
- package/packages/core/src/ocn.ts +0 -74
- package/packages/core/src/scanner.ts +0 -118
- package/packages/core/src/types.ts +0 -199
- package/packages/core/src/watcher.ts +0 -128
- package/packages/core/src/worktree.ts +0 -80
- package/packages/core/tsconfig.json +0 -5
- package/packages/render/bunfig.toml +0 -8
- package/packages/render/jsx-runtime.d.ts +0 -3
- package/packages/render/package.json +0 -18
- package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
- package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -304
- package/packages/render/src/components/git-graph.tsx +0 -127
- package/packages/render/src/components/help-overlay.tsx +0 -108
- package/packages/render/src/components/index.ts +0 -7
- package/packages/render/src/components/repo-list.tsx +0 -127
- package/packages/render/src/components/stats-panel.tsx +0 -116
- package/packages/render/src/components/status-badge.tsx +0 -70
- package/packages/render/src/components/status-bar.tsx +0 -56
- package/packages/render/src/components/widget-container.tsx +0 -286
- package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
- package/packages/render/src/components/widgets/branch-list.tsx +0 -93
- package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
- package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
- package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
- package/packages/render/src/components/widgets/file-changes.tsx +0 -78
- package/packages/render/src/components/widgets/git-status.tsx +0 -125
- package/packages/render/src/components/widgets/github-ci.tsx +0 -98
- package/packages/render/src/components/widgets/github-issues.tsx +0 -101
- package/packages/render/src/components/widgets/github-prs.tsx +0 -119
- package/packages/render/src/components/widgets/github-release.tsx +0 -73
- package/packages/render/src/components/widgets/index.ts +0 -12
- package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
- package/packages/render/src/components/widgets/registry.ts +0 -23
- package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
- package/packages/render/src/config/index.ts +0 -104
- package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
- package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
- package/packages/render/src/lib/actions.ts +0 -68
- package/packages/render/src/lib/fetch-context.ts +0 -102
- package/packages/render/src/lib/filter.ts +0 -94
- package/packages/render/src/lib/format.ts +0 -36
- package/packages/render/src/lib/use-devpad.ts +0 -167
- package/packages/render/src/lib/use-github.ts +0 -75
- package/packages/render/src/lib/widget-grid.ts +0 -204
- package/packages/render/src/lib/widget-state.ts +0 -96
- package/packages/render/src/overview.tsx +0 -16
- package/packages/render/src/screens/index.ts +0 -1
- package/packages/render/src/screens/main-screen.tsx +0 -410
- package/packages/render/src/theme/index.ts +0 -37
- package/packages/render/tsconfig.json +0 -9
- package/tsconfig.json +0 -23
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { testRender } from "@opentui/solid";
|
|
3
|
-
import { createSignal } from "solid-js";
|
|
4
|
-
import type { ScrollBoxRenderable, Renderable } from "@opentui/core";
|
|
5
|
-
import type { WidgetConfig, RepoStatus, WidgetId } from "@overview/core";
|
|
6
|
-
|
|
7
|
-
import { WidgetContainer } from "../widget-container";
|
|
8
|
-
import "../widgets/index";
|
|
9
|
-
|
|
10
|
-
function mockRepoStatus(): RepoStatus {
|
|
11
|
-
return {
|
|
12
|
-
path: "/tmp/test-repo",
|
|
13
|
-
name: "test-repo",
|
|
14
|
-
display_path: "test-repo",
|
|
15
|
-
current_branch: "main",
|
|
16
|
-
head_commit: "abc1234",
|
|
17
|
-
head_message: "test commit",
|
|
18
|
-
head_time: Date.now() / 1000,
|
|
19
|
-
remote_url: null,
|
|
20
|
-
ahead: 2,
|
|
21
|
-
behind: 0,
|
|
22
|
-
modified_count: 3,
|
|
23
|
-
staged_count: 1,
|
|
24
|
-
untracked_count: 1,
|
|
25
|
-
conflict_count: 0,
|
|
26
|
-
changes: [],
|
|
27
|
-
stashes: [],
|
|
28
|
-
stash_count: 0,
|
|
29
|
-
branches: [
|
|
30
|
-
{
|
|
31
|
-
name: "main",
|
|
32
|
-
is_current: true,
|
|
33
|
-
upstream: "origin/main",
|
|
34
|
-
ahead: 2,
|
|
35
|
-
behind: 0,
|
|
36
|
-
last_commit_time: Date.now() / 1000,
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
local_branch_count: 1,
|
|
40
|
-
remote_branch_count: 1,
|
|
41
|
-
tags: ["v1.0"],
|
|
42
|
-
total_commits: 50,
|
|
43
|
-
repo_size_bytes: 1024000,
|
|
44
|
-
contributor_count: 3,
|
|
45
|
-
recent_commits: [
|
|
46
|
-
{
|
|
47
|
-
hash: "abc1234",
|
|
48
|
-
message: "test commit",
|
|
49
|
-
author: "test",
|
|
50
|
-
time: Date.now() / 1000,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
is_clean: false,
|
|
54
|
-
health: "ahead" as const,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const ALL_WIDGET_IDS: WidgetId[] = [
|
|
59
|
-
"git-status",
|
|
60
|
-
"devpad-milestones",
|
|
61
|
-
"github-prs",
|
|
62
|
-
"devpad-tasks",
|
|
63
|
-
"file-changes",
|
|
64
|
-
"repo-meta",
|
|
65
|
-
"github-ci",
|
|
66
|
-
"branch-list",
|
|
67
|
-
"commit-activity",
|
|
68
|
-
"github-release",
|
|
69
|
-
"github-issues",
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
function defaultTestWidgetConfigs(): WidgetConfig[] {
|
|
73
|
-
return ALL_WIDGET_IDS.map((id, i) => ({
|
|
74
|
-
id,
|
|
75
|
-
enabled: true,
|
|
76
|
-
collapsed: false,
|
|
77
|
-
priority: i,
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
describe("widget container (integration)", () => {
|
|
82
|
-
test("renders all enabled widgets in grid layout", async () => {
|
|
83
|
-
const { renderOnce, captureCharFrame } = await testRender(
|
|
84
|
-
() => (
|
|
85
|
-
<box flexDirection="column" width={80} height={50}>
|
|
86
|
-
<WidgetContainer
|
|
87
|
-
status={mockRepoStatus()}
|
|
88
|
-
repoName="test-repo"
|
|
89
|
-
loading={false}
|
|
90
|
-
focused={true}
|
|
91
|
-
height={50}
|
|
92
|
-
availableWidth={80}
|
|
93
|
-
widgetConfigs={defaultTestWidgetConfigs()}
|
|
94
|
-
/>
|
|
95
|
-
</box>
|
|
96
|
-
),
|
|
97
|
-
{ width: 80, height: 50 },
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
await renderOnce();
|
|
101
|
-
const frame = captureCharFrame();
|
|
102
|
-
|
|
103
|
-
// Border characters present (scrollbar may replace right-side corners ╮/╯)
|
|
104
|
-
expect(frame).toContain("╭");
|
|
105
|
-
expect(frame).toContain("╰");
|
|
106
|
-
expect(frame).toContain("─");
|
|
107
|
-
|
|
108
|
-
// Widget labels visible (first widget + a few others)
|
|
109
|
-
expect(frame).toContain("Git Status");
|
|
110
|
-
expect(frame).toContain("Branches");
|
|
111
|
-
expect(frame).toContain("Repo Meta");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("focused widget has highlight marker", async () => {
|
|
115
|
-
const { renderOnce, captureCharFrame } = await testRender(
|
|
116
|
-
() => (
|
|
117
|
-
<box flexDirection="column" width={80} height={50}>
|
|
118
|
-
<WidgetContainer
|
|
119
|
-
status={mockRepoStatus()}
|
|
120
|
-
repoName="test-repo"
|
|
121
|
-
loading={false}
|
|
122
|
-
focused={true}
|
|
123
|
-
height={50}
|
|
124
|
-
availableWidth={80}
|
|
125
|
-
widgetConfigs={defaultTestWidgetConfigs()}
|
|
126
|
-
/>
|
|
127
|
-
</box>
|
|
128
|
-
),
|
|
129
|
-
{ width: 80, height: 50 },
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
await renderOnce();
|
|
133
|
-
const frame = captureCharFrame();
|
|
134
|
-
|
|
135
|
-
// The focused widget (first by priority = git-status) shows ▸ marker
|
|
136
|
-
expect(frame).toContain("▸");
|
|
137
|
-
expect(frame).toContain("▸ Git Status");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("j/k navigation changes focused widget", async () => {
|
|
141
|
-
const { renderOnce, captureCharFrame, mockInput } = await testRender(
|
|
142
|
-
() => (
|
|
143
|
-
<box flexDirection="column" width={80} height={50}>
|
|
144
|
-
<WidgetContainer
|
|
145
|
-
status={mockRepoStatus()}
|
|
146
|
-
repoName="test-repo"
|
|
147
|
-
loading={false}
|
|
148
|
-
focused={true}
|
|
149
|
-
height={50}
|
|
150
|
-
availableWidth={80}
|
|
151
|
-
widgetConfigs={defaultTestWidgetConfigs()}
|
|
152
|
-
/>
|
|
153
|
-
</box>
|
|
154
|
-
),
|
|
155
|
-
{ width: 80, height: 50 },
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
await renderOnce();
|
|
159
|
-
const frame_before = captureCharFrame();
|
|
160
|
-
expect(frame_before).toContain("▸ Git Status");
|
|
161
|
-
|
|
162
|
-
// Press j to move focus down
|
|
163
|
-
mockInput.pressKey("j");
|
|
164
|
-
await renderOnce();
|
|
165
|
-
const frame_after_j = captureCharFrame();
|
|
166
|
-
|
|
167
|
-
// Git Status should no longer have the ▸ marker
|
|
168
|
-
expect(frame_after_j).not.toContain("▸ Git Status");
|
|
169
|
-
// The next widget visually is Repo Meta (same row, next column)
|
|
170
|
-
expect(frame_after_j).toContain("▸ Repo Meta");
|
|
171
|
-
|
|
172
|
-
// Press k to go back
|
|
173
|
-
mockInput.pressKey("k");
|
|
174
|
-
await renderOnce();
|
|
175
|
-
const frame_after_k = captureCharFrame();
|
|
176
|
-
|
|
177
|
-
expect(frame_after_k).toContain("▸ Git Status");
|
|
178
|
-
expect(frame_after_k).not.toContain("▸ Repo Meta");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("collapsed widget shows collapsed indicator", async () => {
|
|
182
|
-
const configs = defaultTestWidgetConfigs();
|
|
183
|
-
// Collapse the first widget
|
|
184
|
-
configs[0] = { ...configs[0]!, collapsed: true };
|
|
185
|
-
|
|
186
|
-
const { renderOnce, captureCharFrame } = await testRender(
|
|
187
|
-
() => (
|
|
188
|
-
<box flexDirection="column" width={80} height={50}>
|
|
189
|
-
<WidgetContainer
|
|
190
|
-
status={mockRepoStatus()}
|
|
191
|
-
repoName="test-repo"
|
|
192
|
-
loading={false}
|
|
193
|
-
focused={true}
|
|
194
|
-
height={50}
|
|
195
|
-
availableWidth={80}
|
|
196
|
-
widgetConfigs={configs}
|
|
197
|
-
/>
|
|
198
|
-
</box>
|
|
199
|
-
),
|
|
200
|
-
{ width: 80, height: 50 },
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
await renderOnce();
|
|
204
|
-
const frame = captureCharFrame();
|
|
205
|
-
|
|
206
|
-
expect(frame).toContain("[>]");
|
|
207
|
-
expect(frame).toContain("collapsed");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("scroll-to-focused reaches bottom widgets in nested layout", async () => {
|
|
211
|
-
const { renderOnce, captureCharFrame, mockInput } = await testRender(
|
|
212
|
-
() => (
|
|
213
|
-
<box flexDirection="column" width="100%" height="100%">
|
|
214
|
-
<box height={1}>
|
|
215
|
-
<text content="header" />
|
|
216
|
-
</box>
|
|
217
|
-
<box flexDirection="row" flexGrow={1}>
|
|
218
|
-
<box width={40}>
|
|
219
|
-
<text content="left" />
|
|
220
|
-
</box>
|
|
221
|
-
<box flexDirection="column" flexGrow={1}>
|
|
222
|
-
<box height="50%">
|
|
223
|
-
<text content="graph" />
|
|
224
|
-
</box>
|
|
225
|
-
<WidgetContainer
|
|
226
|
-
status={mockRepoStatus()}
|
|
227
|
-
repoName="test-repo"
|
|
228
|
-
loading={false}
|
|
229
|
-
focused={true}
|
|
230
|
-
height="50%"
|
|
231
|
-
availableWidth={80}
|
|
232
|
-
widgetConfigs={defaultTestWidgetConfigs()}
|
|
233
|
-
/>
|
|
234
|
-
</box>
|
|
235
|
-
</box>
|
|
236
|
-
<box height={1}>
|
|
237
|
-
<text content="status" />
|
|
238
|
-
</box>
|
|
239
|
-
</box>
|
|
240
|
-
),
|
|
241
|
-
{ width: 120, height: 40 },
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
await renderOnce();
|
|
245
|
-
|
|
246
|
-
// Navigate to the last widget by pressing j many times
|
|
247
|
-
for (let i = 0; i < 15; i++) {
|
|
248
|
-
mockInput.pressKey("j");
|
|
249
|
-
await renderOnce();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const frame = captureCharFrame();
|
|
253
|
-
|
|
254
|
-
// The ▸ marker should always be visible (scroll-to-focused keeps it in view)
|
|
255
|
-
expect(frame).toContain("▸");
|
|
256
|
-
|
|
257
|
-
// The last widget (by priority order: github-issues, label "GitHub Issues") should be visible
|
|
258
|
-
// or at least the focus marker should be present proving scroll worked
|
|
259
|
-
const lines = frame.split("\n");
|
|
260
|
-
const marker_line = lines.find((l: string) => l.includes("▸"));
|
|
261
|
-
expect(marker_line).toBeDefined();
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("c key toggles widget collapse", async () => {
|
|
265
|
-
const [updated_configs, setUpdatedConfigs] = createSignal<WidgetConfig[] | null>(null);
|
|
266
|
-
|
|
267
|
-
const { renderOnce, mockInput } = await testRender(
|
|
268
|
-
() => (
|
|
269
|
-
<box flexDirection="column" width={80} height={50}>
|
|
270
|
-
<WidgetContainer
|
|
271
|
-
status={mockRepoStatus()}
|
|
272
|
-
repoName="test-repo"
|
|
273
|
-
loading={false}
|
|
274
|
-
focused={true}
|
|
275
|
-
height={50}
|
|
276
|
-
availableWidth={80}
|
|
277
|
-
widgetConfigs={defaultTestWidgetConfigs()}
|
|
278
|
-
onWidgetConfigChange={(configs) => setUpdatedConfigs(configs)}
|
|
279
|
-
/>
|
|
280
|
-
</box>
|
|
281
|
-
),
|
|
282
|
-
{ width: 80, height: 50 },
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
await renderOnce();
|
|
286
|
-
|
|
287
|
-
// Press c to toggle collapse on the focused (first) widget
|
|
288
|
-
mockInput.pressKey("c");
|
|
289
|
-
await renderOnce();
|
|
290
|
-
|
|
291
|
-
const result = updated_configs();
|
|
292
|
-
expect(result).not.toBeNull();
|
|
293
|
-
|
|
294
|
-
// First widget (git-status) should now be collapsed
|
|
295
|
-
const git_status_config = result!.find((c) => c.id === "git-status");
|
|
296
|
-
expect(git_status_config).toBeDefined();
|
|
297
|
-
expect(git_status_config!.collapsed).toBe(true);
|
|
298
|
-
|
|
299
|
-
// Other widgets should remain uncollapsed
|
|
300
|
-
const other = result!.find((c) => c.id === "github-issues");
|
|
301
|
-
expect(other).toBeDefined();
|
|
302
|
-
expect(other!.collapsed).toBe(false);
|
|
303
|
-
});
|
|
304
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { Show, For } from "solid-js";
|
|
2
|
-
import type { GitGraphOutput } from "@overview/core";
|
|
3
|
-
import { theme } from "../theme";
|
|
4
|
-
|
|
5
|
-
interface GitGraphProps {
|
|
6
|
-
graph: GitGraphOutput | null;
|
|
7
|
-
repoName: string;
|
|
8
|
-
loading: boolean;
|
|
9
|
-
focused: boolean;
|
|
10
|
-
height: number | `${number}%` | "auto";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface Segment {
|
|
14
|
-
text: string;
|
|
15
|
-
color: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Graph characters: * | / \ _ (edges and merge points)
|
|
19
|
-
const GRAPH_CHARS = /^[*|/\\_\s.]+/;
|
|
20
|
-
// Short hash: 7+ hex chars
|
|
21
|
-
const HASH_RE = /^[0-9a-f]{7,12}/;
|
|
22
|
-
// Ref decoration: (HEAD -> main, origin/main, tag: v1.0)
|
|
23
|
-
const REF_RE = /^\([^)]+\)/;
|
|
24
|
-
|
|
25
|
-
function parseGraphLine(line: string): Segment[] {
|
|
26
|
-
const segments: Segment[] = [];
|
|
27
|
-
|
|
28
|
-
// 1. Graph prefix (lines, merge points)
|
|
29
|
-
const graphMatch = line.match(GRAPH_CHARS);
|
|
30
|
-
let rest = line;
|
|
31
|
-
|
|
32
|
-
if (graphMatch) {
|
|
33
|
-
const graphPart = graphMatch[0];
|
|
34
|
-
// Color the * merge points differently from the | / \ lines
|
|
35
|
-
if (graphPart.includes("*")) {
|
|
36
|
-
const starIdx = graphPart.indexOf("*");
|
|
37
|
-
if (starIdx > 0) {
|
|
38
|
-
segments.push({ text: graphPart.slice(0, starIdx), color: theme.fg_dim });
|
|
39
|
-
}
|
|
40
|
-
segments.push({ text: "*", color: theme.green });
|
|
41
|
-
const after = graphPart.slice(starIdx + 1);
|
|
42
|
-
if (after) {
|
|
43
|
-
segments.push({ text: after, color: theme.fg_dim });
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
46
|
-
segments.push({ text: graphPart, color: theme.fg_dim });
|
|
47
|
-
}
|
|
48
|
-
rest = line.slice(graphPart.length);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!rest) return segments;
|
|
52
|
-
|
|
53
|
-
// 2. Hash
|
|
54
|
-
const hashMatch = rest.match(HASH_RE);
|
|
55
|
-
if (hashMatch) {
|
|
56
|
-
segments.push({ text: hashMatch[0], color: theme.yellow });
|
|
57
|
-
rest = rest.slice(hashMatch[0].length);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!rest) return segments;
|
|
61
|
-
|
|
62
|
-
// 3. Space before ref
|
|
63
|
-
if (rest.startsWith(" ")) {
|
|
64
|
-
segments.push({ text: " ", color: theme.fg });
|
|
65
|
-
rest = rest.slice(1);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 4. Ref decoration
|
|
69
|
-
const refMatch = rest.match(REF_RE);
|
|
70
|
-
if (refMatch) {
|
|
71
|
-
segments.push({ text: refMatch[0], color: theme.cyan });
|
|
72
|
-
rest = rest.slice(refMatch[0].length);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// 5. Commit message (everything remaining)
|
|
76
|
-
if (rest) {
|
|
77
|
-
segments.push({ text: rest, color: theme.fg });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return segments;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function GraphLine(props: { line: string }) {
|
|
84
|
-
const segments = () => parseGraphLine(props.line);
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<box flexDirection="row" height={1}>
|
|
88
|
-
<For each={segments()}>
|
|
89
|
-
{(seg) => <text fg={seg.color} content={seg.text} />}
|
|
90
|
-
</For>
|
|
91
|
-
</box>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function GitGraph(props: GitGraphProps) {
|
|
96
|
-
return (
|
|
97
|
-
<box
|
|
98
|
-
borderStyle="rounded"
|
|
99
|
-
borderColor={props.focused ? theme.border_highlight : theme.border}
|
|
100
|
-
title={`git graph: ${props.repoName}`}
|
|
101
|
-
titleAlignment="left"
|
|
102
|
-
flexDirection="column"
|
|
103
|
-
flexGrow={1}
|
|
104
|
-
height={props.height}
|
|
105
|
-
>
|
|
106
|
-
<Show
|
|
107
|
-
when={!props.loading && props.graph && props.graph.lines.length > 0}
|
|
108
|
-
fallback={
|
|
109
|
-
<text fg={theme.fg_dim} content={props.loading ? "loading..." : "(no commits)"} />
|
|
110
|
-
}
|
|
111
|
-
>
|
|
112
|
-
<scrollbox
|
|
113
|
-
focused={props.focused}
|
|
114
|
-
viewportCulling={true}
|
|
115
|
-
flexGrow={1}
|
|
116
|
-
>
|
|
117
|
-
<For each={props.graph!.lines}>
|
|
118
|
-
{(line) => <GraphLine line={line} />}
|
|
119
|
-
</For>
|
|
120
|
-
</scrollbox>
|
|
121
|
-
<box height={1}>
|
|
122
|
-
<text fg={theme.fg_dim} content={`(${props.graph!.total_lines} commits)`} />
|
|
123
|
-
</box>
|
|
124
|
-
</Show>
|
|
125
|
-
</box>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { Show, For } from "solid-js";
|
|
2
|
-
import { useKeyboard } from "@opentui/solid";
|
|
3
|
-
import { theme } from "../theme";
|
|
4
|
-
|
|
5
|
-
interface HelpOverlayProps {
|
|
6
|
-
visible: boolean;
|
|
7
|
-
onClose: () => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const SECTIONS = [
|
|
11
|
-
{
|
|
12
|
-
title: "Navigation",
|
|
13
|
-
items: [
|
|
14
|
-
{ key: "j / ↓", desc: "Move down" },
|
|
15
|
-
{ key: "k / ↑", desc: "Move up" },
|
|
16
|
-
{ key: "g", desc: "First repo" },
|
|
17
|
-
{ key: "G", desc: "Last repo" },
|
|
18
|
-
{ key: "Enter", desc: "Expand/collapse or enter detail mode" },
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
title: "Actions",
|
|
23
|
-
items: [
|
|
24
|
-
{ key: "g (in detail)", desc: "Launch ggi" },
|
|
25
|
-
{ key: "o", desc: "Open in $EDITOR" },
|
|
26
|
-
{ key: "t", desc: "Open tmux session" },
|
|
27
|
-
{ key: "r", desc: "Refresh selected repo" },
|
|
28
|
-
{ key: "R", desc: "Full rescan" },
|
|
29
|
-
],
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
title: "View",
|
|
33
|
-
items: [
|
|
34
|
-
{ key: "h / l", desc: "Switch panel focus" },
|
|
35
|
-
{ key: "Tab", desc: "Cycle panel focus" },
|
|
36
|
-
{ key: "f", desc: "Cycle filter (all/dirty/clean/ahead/behind)" },
|
|
37
|
-
{ key: "s", desc: "Cycle sort (name/status/last-commit)" },
|
|
38
|
-
{ key: "/", desc: "Search repos" },
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
title: "Widgets (stats panel focused)",
|
|
43
|
-
items: [
|
|
44
|
-
{ key: "j / k", desc: "Navigate between widgets" },
|
|
45
|
-
{ key: "c", desc: "Collapse/expand focused widget" },
|
|
46
|
-
{ key: "C", desc: "Collapse/expand all widgets" },
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
title: "General",
|
|
51
|
-
items: [
|
|
52
|
-
{ key: "q", desc: "Quit / back" },
|
|
53
|
-
{ key: "?", desc: "Toggle help" },
|
|
54
|
-
{ key: "Esc", desc: "Cancel / close" },
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
] as const;
|
|
58
|
-
|
|
59
|
-
function Section(props: { title: string; items: ReadonlyArray<{ key: string; desc: string }> }) {
|
|
60
|
-
return (
|
|
61
|
-
<box flexDirection="column">
|
|
62
|
-
<text fg={theme.yellow} content={props.title} />
|
|
63
|
-
<For each={props.items}>
|
|
64
|
-
{(item) => (
|
|
65
|
-
<box flexDirection="row" height={1}>
|
|
66
|
-
<text fg={theme.blue} content={item.key.padEnd(16)} />
|
|
67
|
-
<text fg={theme.fg} content={item.desc} />
|
|
68
|
-
</box>
|
|
69
|
-
)}
|
|
70
|
-
</For>
|
|
71
|
-
</box>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function HelpOverlay(props: HelpOverlayProps) {
|
|
76
|
-
useKeyboard((key) => {
|
|
77
|
-
if (!props.visible) return;
|
|
78
|
-
|
|
79
|
-
if (key.name === "q" || key.name === "escape" || key.raw === "?") {
|
|
80
|
-
props.onClose();
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<Show when={props.visible}>
|
|
86
|
-
<box
|
|
87
|
-
position="absolute"
|
|
88
|
-
width="60%"
|
|
89
|
-
height="80%"
|
|
90
|
-
left="20%"
|
|
91
|
-
top="10%"
|
|
92
|
-
backgroundColor={theme.bg_dark}
|
|
93
|
-
borderStyle="rounded"
|
|
94
|
-
borderColor={theme.blue}
|
|
95
|
-
title="Help"
|
|
96
|
-
titleAlignment="center"
|
|
97
|
-
padding={2}
|
|
98
|
-
flexDirection="column"
|
|
99
|
-
gap={1}
|
|
100
|
-
zIndex={100}
|
|
101
|
-
>
|
|
102
|
-
<For each={SECTIONS}>
|
|
103
|
-
{(section) => <Section title={section.title} items={section.items} />}
|
|
104
|
-
</For>
|
|
105
|
-
</box>
|
|
106
|
-
</Show>
|
|
107
|
-
);
|
|
108
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { GitGraph } from "./git-graph";
|
|
2
|
-
export { RepoList } from "./repo-list";
|
|
3
|
-
export { WidgetContainer } from "./widget-container";
|
|
4
|
-
export { StatusBar } from "./status-bar";
|
|
5
|
-
export type { AppMode } from "./status-bar";
|
|
6
|
-
export { StatusBadge } from "./status-badge";
|
|
7
|
-
export { HelpOverlay } from "./help-overlay";
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { createSignal, createMemo, createEffect, For } from "solid-js";
|
|
2
|
-
import { useKeyboard } from "@opentui/solid";
|
|
3
|
-
import type { RepoNode } from "@overview/core";
|
|
4
|
-
import { theme } from "../theme";
|
|
5
|
-
import { StatusBadge } from "./status-badge";
|
|
6
|
-
|
|
7
|
-
interface RepoListProps {
|
|
8
|
-
repos: RepoNode[];
|
|
9
|
-
focused: boolean;
|
|
10
|
-
onSelect: (node: RepoNode) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface FlatNode {
|
|
14
|
-
node: RepoNode;
|
|
15
|
-
depth: number;
|
|
16
|
-
is_last: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function flattenTree(nodes: RepoNode[], depth: number = 0): FlatNode[] {
|
|
20
|
-
return nodes.flatMap((node, i) => {
|
|
21
|
-
const entry: FlatNode = { node, depth, is_last: i === nodes.length - 1 };
|
|
22
|
-
if (node.type === "directory" && node.expanded && node.children.length > 0) {
|
|
23
|
-
return [entry, ...flattenTree(node.children, depth + 1)];
|
|
24
|
-
}
|
|
25
|
-
return [entry];
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function connector(is_last: boolean): string {
|
|
30
|
-
return is_last ? "└──" : "├──";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function icon(node: RepoNode): string {
|
|
34
|
-
if (node.type === "directory") return node.expanded ? "▾ " : "▸ ";
|
|
35
|
-
if (node.type === "worktree") return "⊞ ";
|
|
36
|
-
return " ";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function RepoList(props: RepoListProps) {
|
|
40
|
-
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
41
|
-
|
|
42
|
-
const visible = createMemo(() => flattenTree(props.repos));
|
|
43
|
-
|
|
44
|
-
const clampIndex = (idx: number) => Math.max(0, Math.min(idx, visible().length - 1));
|
|
45
|
-
|
|
46
|
-
createEffect(() => {
|
|
47
|
-
const items = visible();
|
|
48
|
-
if (items.length === 0) return;
|
|
49
|
-
const idx = clampIndex(selectedIndex());
|
|
50
|
-
if (idx !== selectedIndex()) setSelectedIndex(idx);
|
|
51
|
-
const item = items[idx];
|
|
52
|
-
if (item) props.onSelect(item.node);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
useKeyboard((key) => {
|
|
56
|
-
if (!props.focused) return;
|
|
57
|
-
const items = visible();
|
|
58
|
-
if (items.length === 0) return;
|
|
59
|
-
|
|
60
|
-
switch (key.name) {
|
|
61
|
-
case "j":
|
|
62
|
-
case "down": {
|
|
63
|
-
const next = clampIndex(selectedIndex() + 1);
|
|
64
|
-
setSelectedIndex(next);
|
|
65
|
-
const item = items[next];
|
|
66
|
-
if (item) props.onSelect(item.node);
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
case "k":
|
|
70
|
-
case "up": {
|
|
71
|
-
const next = clampIndex(selectedIndex() - 1);
|
|
72
|
-
setSelectedIndex(next);
|
|
73
|
-
const item = items[next];
|
|
74
|
-
if (item) props.onSelect(item.node);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
case "g": {
|
|
78
|
-
setSelectedIndex(0);
|
|
79
|
-
const item = items[0];
|
|
80
|
-
if (item) props.onSelect(item.node);
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
case "G": {
|
|
84
|
-
const last = items.length - 1;
|
|
85
|
-
setSelectedIndex(last);
|
|
86
|
-
const item = items[last];
|
|
87
|
-
if (item) props.onSelect(item.node);
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
case "return": {
|
|
91
|
-
const item = items[selectedIndex()];
|
|
92
|
-
if (item && item.node.type === "directory") {
|
|
93
|
-
item.node.expanded = !item.node.expanded;
|
|
94
|
-
setSelectedIndex(clampIndex(selectedIndex()));
|
|
95
|
-
}
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<box flexDirection="column" width="100%" height="100%">
|
|
103
|
-
<For each={visible()}>
|
|
104
|
-
{(entry, i) => {
|
|
105
|
-
const selected = () => i() === selectedIndex();
|
|
106
|
-
const indent = " ".repeat(entry.depth);
|
|
107
|
-
const conn = connector(entry.is_last);
|
|
108
|
-
const prefix = icon(entry.node);
|
|
109
|
-
const label = `${indent}${conn} ${prefix}${entry.node.name}`;
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<box
|
|
113
|
-
flexDirection="row"
|
|
114
|
-
width="100%"
|
|
115
|
-
height={1}
|
|
116
|
-
backgroundColor={selected() ? theme.selection : undefined}
|
|
117
|
-
>
|
|
118
|
-
<text fg={selected() ? theme.fg : theme.fg_dark} content={label} />
|
|
119
|
-
<box flexGrow={1} />
|
|
120
|
-
<StatusBadge status={entry.node.status} />
|
|
121
|
-
</box>
|
|
122
|
-
);
|
|
123
|
-
}}
|
|
124
|
-
</For>
|
|
125
|
-
</box>
|
|
126
|
-
);
|
|
127
|
-
}
|