@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.
Files changed (69) hide show
  1. package/bin/overview +10 -0
  2. package/dist/overview.js +10361 -0
  3. package/package.json +22 -15
  4. package/bunfig.toml +0 -7
  5. package/packages/core/__tests__/concurrency.test.ts +0 -111
  6. package/packages/core/__tests__/helpers.ts +0 -60
  7. package/packages/core/__tests__/integration/git-status.test.ts +0 -62
  8. package/packages/core/__tests__/integration/scanner.test.ts +0 -140
  9. package/packages/core/__tests__/ocn.test.ts +0 -164
  10. package/packages/core/package.json +0 -13
  11. package/packages/core/src/cache.ts +0 -31
  12. package/packages/core/src/concurrency.ts +0 -44
  13. package/packages/core/src/devpad.ts +0 -61
  14. package/packages/core/src/git-graph.ts +0 -54
  15. package/packages/core/src/git-stats.ts +0 -201
  16. package/packages/core/src/git-status.ts +0 -316
  17. package/packages/core/src/github.ts +0 -286
  18. package/packages/core/src/index.ts +0 -58
  19. package/packages/core/src/ocn.ts +0 -74
  20. package/packages/core/src/scanner.ts +0 -118
  21. package/packages/core/src/types.ts +0 -199
  22. package/packages/core/src/watcher.ts +0 -128
  23. package/packages/core/src/worktree.ts +0 -80
  24. package/packages/core/tsconfig.json +0 -5
  25. package/packages/render/bunfig.toml +0 -8
  26. package/packages/render/jsx-runtime.d.ts +0 -3
  27. package/packages/render/package.json +0 -18
  28. package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
  29. package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -304
  30. package/packages/render/src/components/git-graph.tsx +0 -127
  31. package/packages/render/src/components/help-overlay.tsx +0 -108
  32. package/packages/render/src/components/index.ts +0 -7
  33. package/packages/render/src/components/repo-list.tsx +0 -127
  34. package/packages/render/src/components/stats-panel.tsx +0 -116
  35. package/packages/render/src/components/status-badge.tsx +0 -70
  36. package/packages/render/src/components/status-bar.tsx +0 -56
  37. package/packages/render/src/components/widget-container.tsx +0 -286
  38. package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
  39. package/packages/render/src/components/widgets/branch-list.tsx +0 -93
  40. package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
  41. package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
  42. package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
  43. package/packages/render/src/components/widgets/file-changes.tsx +0 -78
  44. package/packages/render/src/components/widgets/git-status.tsx +0 -125
  45. package/packages/render/src/components/widgets/github-ci.tsx +0 -98
  46. package/packages/render/src/components/widgets/github-issues.tsx +0 -101
  47. package/packages/render/src/components/widgets/github-prs.tsx +0 -119
  48. package/packages/render/src/components/widgets/github-release.tsx +0 -73
  49. package/packages/render/src/components/widgets/index.ts +0 -12
  50. package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
  51. package/packages/render/src/components/widgets/registry.ts +0 -23
  52. package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
  53. package/packages/render/src/config/index.ts +0 -104
  54. package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
  55. package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
  56. package/packages/render/src/lib/actions.ts +0 -68
  57. package/packages/render/src/lib/fetch-context.ts +0 -102
  58. package/packages/render/src/lib/filter.ts +0 -94
  59. package/packages/render/src/lib/format.ts +0 -36
  60. package/packages/render/src/lib/use-devpad.ts +0 -167
  61. package/packages/render/src/lib/use-github.ts +0 -75
  62. package/packages/render/src/lib/widget-grid.ts +0 -204
  63. package/packages/render/src/lib/widget-state.ts +0 -96
  64. package/packages/render/src/overview.tsx +0 -16
  65. package/packages/render/src/screens/index.ts +0 -1
  66. package/packages/render/src/screens/main-screen.tsx +0 -410
  67. package/packages/render/src/theme/index.ts +0 -37
  68. package/packages/render/tsconfig.json +0 -9
  69. 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
- }