Skip to content

Commit b9ab849

Browse files
author
shmuel hizmi
committed
master
1 parent 67d243a commit b9ab849

3 files changed

Lines changed: 104 additions & 35 deletions

File tree

bun.lock

Lines changed: 17 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/** @jsxImportSource @opentui/react */
2+
import { useState, useEffect, type ReactNode } from "react";
3+
import { readFile } from "node:fs/promises";
4+
5+
interface LocalFileViewerProps {
6+
readonly filePath: string;
7+
}
8+
9+
export const LocalFileViewer = ({ filePath }: LocalFileViewerProps): ReactNode => {
10+
const [content, setContent] = useState<string | null>(null);
11+
const [error, setError] = useState<string | null>(null);
12+
const name = filePath.split("/").pop() ?? filePath;
13+
14+
useEffect(() => {
15+
let cancelled = false;
16+
setContent(null);
17+
setError(null);
18+
readFile(filePath, "utf-8")
19+
.then((text) => { if (!cancelled) setContent(text); })
20+
.catch((err) => { if (!cancelled) setError(String(err)); });
21+
return () => { cancelled = true; };
22+
}, [filePath]);
23+
24+
if (error) {
25+
return (
26+
<box flexGrow={1} justifyContent="center" alignItems="center">
27+
<text fg="#ff453a">Failed to read: {name}</text>
28+
</box>
29+
);
30+
}
31+
32+
if (content === null) {
33+
return (
34+
<box flexGrow={1} justifyContent="center" alignItems="center">
35+
<text fg="#636366">Loading {name}...</text>
36+
</box>
37+
);
38+
}
39+
40+
const lines = content.split("\n");
41+
const gutterWidth = String(lines.length).length + 1;
42+
43+
return (
44+
<box flexGrow={1} flexDirection="column">
45+
<box height={1} paddingX={1} backgroundColor="#2c2c2e">
46+
<text fg="#98989d">{name}</text>
47+
</box>
48+
<scrollbox flexGrow={1}>
49+
{lines.map((line, i) => (
50+
<text key={i}>
51+
<span fg="#48484a">{String(i + 1).padStart(gutterWidth, " ")} </span>
52+
<span fg="#e5e5ea">{line || " "}</span>
53+
</text>
54+
))}
55+
</scrollbox>
56+
</box>
57+
);
58+
};

packages/wmux-client-terminal/src/components/WmuxApp.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Sidebar } from "./Sidebar";
55
import { StatusBar } from "./StatusBar";
66
import { PrefixProvider, useTUIContext } from "./FocusContext";
77
import { SearchOverlay } from "./SearchOverlay";
8+
import { LocalFileViewer } from "./LocalFileViewer";
89
import type { CategoryInfo } from "../types";
910

1011
const SIDEBAR_WIDTH = 30;
@@ -30,9 +31,11 @@ interface SidebarNavigationItem {
3031
const buildAllNavigationItems = (
3132
categories: ReadonlyArray<CategoryInfo>,
3233
): readonly SidebarNavigationItem[] =>
33-
categories
34-
.filter((c) => c.type !== "files")
35-
.flatMap((c) => c.tabs.map((tab) => ({ categoryName: c.name, tabId: tab.id })));
34+
categories.flatMap((c) =>
35+
c.type === "files"
36+
? (c.openFiles ?? []).map((file) => ({ categoryName: c.name, tabId: `file::${file.path}` }))
37+
: c.tabs.map((tab) => ({ categoryName: c.name, tabId: tab.id })),
38+
);
3639

3740
const navigateItem = (
3841
items: readonly SidebarNavigationItem[],
@@ -105,12 +108,24 @@ export const WmuxApp = (props: {
105108
return map;
106109
}, [children]);
107110

111+
const selectFirstInCategory = useCallback((cat: CategoryInfo) => {
112+
if (cat.type === "files") {
113+
const firstOpen = (cat.openFiles ?? [])[0];
114+
if (firstOpen) { selectTab(`file::${firstOpen.path}`); return; }
115+
const firstFile = (cat.fileEntries ?? []).find((e) => !e.isDir);
116+
if (firstFile) { selectTab(`file::${firstFile.path}`); openFile(firstFile.path); }
117+
return;
118+
}
119+
if (cat.tabs.length > 0) selectTab(cat.tabs[0]!.id);
120+
}, [selectTab, openFile]);
121+
108122
const handleNavigate = useCallback((delta: number) => {
109123
const target = navigateItem(allNavItems, activeTabId, delta);
110124
if (!target) return;
111125
if (target.categoryName !== activeCategory) selectCategory(target.categoryName);
112126
selectTab(target.tabId);
113-
}, [allNavItems, activeTabId, activeCategory, selectCategory, selectTab]);
127+
if (target.tabId.startsWith("file::")) openFile(target.tabId.slice(6));
128+
}, [allNavItems, activeTabId, activeCategory, selectCategory, selectTab, openFile]);
114129

115130
useKeyboard((key) => {
116131
// ── Search overlay handles its own keys ──────────────
@@ -147,9 +162,7 @@ export const WmuxApp = (props: {
147162
if (idx < categories.length) {
148163
const cat = categories[idx]!;
149164
selectCategory(cat.name);
150-
if (cat.type !== "files" && cat.tabs.length > 0) {
151-
selectTab(cat.tabs[0]!.id);
152-
}
165+
selectFirstInCategory(cat);
153166
}
154167
return;
155168
}
@@ -160,19 +173,15 @@ export const WmuxApp = (props: {
160173
const nextIdx = (catIdx + 1) % categories.length;
161174
const nextCat = categories[nextIdx]!;
162175
selectCategory(nextCat.name);
163-
if (nextCat.type !== "files" && nextCat.tabs.length > 0) {
164-
selectTab(nextCat.tabs[0]!.id);
165-
}
176+
selectFirstInCategory(nextCat);
166177
return;
167178
}
168179
if (key.name === "[") {
169180
const catIdx = categories.findIndex((c) => c.name === activeCategory);
170181
const prevIdx = (catIdx - 1 + categories.length) % categories.length;
171182
const prevCat = categories[prevIdx]!;
172183
selectCategory(prevCat.name);
173-
if (prevCat.type !== "files" && prevCat.tabs.length > 0) {
174-
selectTab(prevCat.tabs[0]!.id);
175-
}
184+
selectFirstInCategory(prevCat);
176185
return;
177186
}
178187

@@ -274,12 +283,14 @@ export const WmuxApp = (props: {
274283
onOpenFile={openFile}
275284
onClose={handleSearchClose}
276285
/>
286+
) : activeChild ? (
287+
activeChild
288+
) : activeTabId.startsWith("file::") ? (
289+
<LocalFileViewer filePath={activeTabId.slice(6)} />
277290
) : (
278-
activeChild ?? (
279-
<box flexGrow={1} justifyContent="center" alignItems="center">
280-
<text fg="#636366">No active tab</text>
281-
</box>
282-
)
291+
<box flexGrow={1} justifyContent="center" alignItems="center">
292+
<text fg="#636366">No active tab</text>
293+
</box>
283294
)}
284295
</box>
285296
</box>

0 commit comments

Comments
 (0)