File Browser
A working cross-platform file browser: directory tree, image thumbnails, double-click to open with the OS default handler, right-click to reveal.
Exercises: fs.readDir, fs.readBinary (zero-copy), shell.openPath, os.homeDir, path.join.
Scaffold
bunx @tynd/cli create file-browser --framework react --runtime lite
cd file-browserBackend — minimal
No RPC needed — every operation is an OS call from the frontend.
backend/main.ts
import { app } from "@tynd/core";
app.start({
window: { title: "Files", width: 1100, height: 720, center: true },
});Frontend
src/App.tsx
import { useEffect, useState } from "react";
import { fs, os, path, shell } from "@tynd/core/client";
interface Entry {
name: string;
path: string;
isDir: boolean;
}
export default function App() {
const [cwd, setCwd] = useState<string>("");
const [entries, setEntries] = useState<Entry[]>([]);
useEffect(() => {
os.homeDir().then((h) => h && setCwd(h));
}, []);
useEffect(() => {
if (!cwd) return;
fs.readDir(cwd).then((items) => {
items.sort((a, b) =>
a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1,
);
setEntries(items);
});
}, [cwd]);
async function goUp() {
const parent = await path.dirname(cwd);
if (parent !== cwd) setCwd(parent);
}
async function onOpen(e: Entry) {
if (e.isDir) setCwd(e.path);
else await shell.openPath(e.path);
}
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<header style={{ display: "flex", gap: 8, padding: 8, borderBottom: "1px solid #ddd" }}>
<button onClick={goUp}>↑</button>
<code style={{ flex: 1, padding: "4px 8px", background: "#f4f4f4" }}>{cwd}</code>
</header>
<ul style={{ listStyle: "none", margin: 0, padding: 0, overflow: "auto" }}>
{entries.map((e) => (
<li
key={e.path}
onDoubleClick={() => onOpen(e)}
style={{
padding: "8px 12px",
display: "flex",
alignItems: "center",
gap: 12,
cursor: "pointer",
}}
>
{e.isDir ? "📁" : isImage(e.name) ? <Thumbnail path={e.path} /> : "📄"}
<span>{e.name}</span>
</li>
))}
</ul>
</div>
);
}
function isImage(name: string) {
return /\.(png|jpe?g|gif|webp|bmp)$/i.test(name);
}
function Thumbnail({ path }: { path: string }) {
const [url, setUrl] = useState<string | null>(null);
useEffect(() => {
let dead = false;
fs.readBinary(path).then((bytes) => {
if (dead) return;
const blob = new Blob([bytes]);
setUrl(URL.createObjectURL(blob));
});
return () => {
dead = true;
if (url) URL.revokeObjectURL(url);
};
}, [path]);
return url ? <img src={url} alt="" width={32} height={32} style={{ objectFit: "cover" }} /> : <span>🖼️</span>;
}Why this matters
fs.readBinarygoes through thetynd-bin://zero-copy channel — multi-MB thumbnails don’t stall the main thread.shell.openPathdelegates to the OS default handler — PDFs open in Preview/Acrobat, images in your image viewer, etc. No need to build your own viewer.- No backend RPC. Every operation is a direct OS call from the frontend. The backend is essentially a config file.
Build
tynd build~9 MB binary on lite. Launches to the user’s home directory.
Next ideas
- Watch the current directory —
fs.watch(cwd, { recursive: false }, reload)to pick up changes from other apps. - Search — recursive walk with
async function*yielding matches as they’re found. - Copy / move / delete —
fs.copy,fs.rename,fs.remove, wrapped with adialog.confirmfor destructive ops. - Reveal in file manager — use
process.exec("explorer", { args: [path] })on Windows,openon macOS,xdg-openon Linux. - Drag-drop to add files — frontend HTML5 drag-drop; the dropped paths are returned as native file paths.
Related
- fs API.
- Binary Data guide — why
fs.readBinaryis fast. - shell API.
- process API.
Last updated on