diff --git a/index.html b/index.html
index eea3bd7..5edd686 100644
--- a/index.html
+++ b/index.html
@@ -1,24 +1,18 @@
-
+
-
-
- Miller code editor
-
-
- Miller code editor - welcome!
-
-
- N/A
-
-
-
-
-
-
+
+
+ Miller code editor
+
+
+
+ Miller code editor - welcome!
+
+
+
+
+
+
diff --git a/src/app/foldernav.ts b/src/app/foldernav.ts
index efbbd4a..7effbad 100644
--- a/src/app/foldernav.ts
+++ b/src/app/foldernav.ts
@@ -1,7 +1,94 @@
import van from "vanjs-core";
const v = van.tags;
+import type { FolderTree } from "../types/global";
-document.getElementById("openFolder").addEventListener("click", async () => {
- const folderPath = await window.electronAPI.openFolder();
- document.getElementById("currentFolder").innerText = folderPath;
-});
+const folderTreeState = van.state(null);
+
+async function openFolder() {
+ const folderTree = await window.electronAPI.openFolder();
+ if (!folderTree) return;
+ folderTreeState.val = folderTree;
+}
+
+const FolderTreeView = () => {
+ if (!folderTreeState.val) {
+ return v.div(
+ { style: "text-align: center; margin-top: 25px;" },
+ v.p("No folder selected!"),
+ v.p(
+ v.button(
+ {
+ onclick: async () => {
+ const folderTree =
+ await window.electronAPI.openFolder();
+ if (!folderTree) return;
+ folderTreeState.val = folderTree;
+ },
+ },
+ "Open Folder",
+ ),
+ ),
+ );
+ }
+ return v.div(
+ v.span(
+ { style: "font-weight: bold; margin-right: 1em;" },
+ folderTreeState.val?.name ?? "No folder",
+ ),
+ v.button(
+ {
+ onclick: openFolder,
+ title: "Refresh current folder",
+ style: "margin-right: 0.5em;",
+ },
+ "⟳",
+ ),
+ v.button(
+ {
+ onclick: openFolder,
+ title: "Open another folder",
+ style: "margin-right: 0.5em;",
+ },
+ "📁",
+ ),
+ folderTreeState.val.children?.map(FsItemView) || [],
+ );
+};
+
+const FsItemView = (tree: FolderTree): HTMLElement | string => {
+ if (tree.type === "file") return v.p(tree.name);
+ return v.details(v.summary(tree.name), tree.children?.map(FsItemView));
+
+ // if (tree.type === "file") return v.div(" ", tree.name);
+ // // Use a state to track open/close instead of
+ // const isOpen = van.state(0);
+ // return v.div(
+ // {
+ // style: "cursor: pointer; user-select: none; font-weight: bold;",
+ // onclick: () => isOpen.val++,
+ // },
+ // () => (isOpen.val ? "▼ " : "▶ "),
+ // tree.name,
+ // );
+ // // return v.div(
+ // // v.span(
+ // // {
+ // // style: "cursor: pointer; user-select: none; font-weight: bold;",
+ // // onclick: () => {
+ // // console.log("opening");
+ // // isOpen.val++;
+ // // },
+ // // },
+ // // () => (isOpen.val ? "▼ " : "▶ "),
+ // // tree.name,
+ // // ),
+ // // " ",
+ // // isOpen.val,
+ // // );
+};
+
+// Mount the folder tree view reactively to the nav
+const nav = document.querySelector("aside nav");
+if (nav) {
+ van.add(nav, () => FolderTreeView());
+}
diff --git a/src/app/index.css b/src/app/index.css
index a9ccef8..3465153 100644
--- a/src/app/index.css
+++ b/src/app/index.css
@@ -33,7 +33,6 @@ aside {
}
/* Editor grid stuff */
-
#editorGrid {
display: grid;
height: minmax(0, 100%);
@@ -52,3 +51,16 @@ aside {
}
/* Navbar styling */
+nav details {
+ margin-bottom: 0px;
+}
+nav details[open] summary,
+nav details summary,
+nav div p {
+ margin-bottom: 5px;
+ line-height: 1.2;
+}
+nav details[open] :not(summary) {
+ margin-left: 16px;
+}
+/* closed detail summary */
diff --git a/src/main/fileOperations.ts b/src/main/fileOperations.ts
index f3b1cb3..491db0c 100644
--- a/src/main/fileOperations.ts
+++ b/src/main/fileOperations.ts
@@ -3,26 +3,57 @@
import { dialog, BrowserWindow } from "electron";
import fs from "fs";
+const fsp = fs.promises;
import path from "path";
-export async function handleOpenFolder(mainWindow: BrowserWindow) {
+type FolderTree = {
+ name: string;
+ path: string;
+ type: "directory" | "file";
+ children?: FolderTree[];
+};
+
+async function readTree(dirPath: string): Promise {
+ const stats = await fsp.stat(dirPath);
+ if (!stats.isDirectory()) return [];
+ const names = await fsp.readdir(dirPath);
+ const children = await Promise.all(
+ names.map(async (name) => {
+ const fullPath = path.join(dirPath, name);
+ const stat = await fsp.stat(fullPath);
+ if (stat.isDirectory()) {
+ return {
+ name,
+ path: fullPath,
+ type: "directory" as const,
+ children: await readTree(fullPath),
+ };
+ } else {
+ return {
+ name,
+ path: fullPath,
+ type: "file" as const,
+ };
+ }
+ })
+ );
+ return children;
+}
+
+export async function handleOpenFolder(
+ mainWindow: BrowserWindow,
+): Promise {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
});
if (!result.canceled && result.filePaths.length > 0) {
const folderPath = result.filePaths[0];
- return folderPath;
+ return {
+ name: path.basename(folderPath),
+ path: folderPath,
+ type: "directory",
+ children: await readTree(folderPath),
+ };
}
return null;
}
-
-export function readFolderContents(folderPath: string): string[] {
- try {
- return fs
- .readdirSync(folderPath)
- .map((file) => path.join(folderPath, file));
- } catch (err) {
- console.error("Error reading folder:", err);
- return [];
- }
-}
diff --git a/src/main/main.ts b/src/main/main.ts
index d7710ec..0420e0c 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -1,5 +1,5 @@
import { app, BrowserWindow, ipcMain } from "electron";
-import { handleOpenFolder, readFolderContents } from "./fileOperations";
+import { handleOpenFolder } from "./fileOperations";
import path from "node:path";
import started from "electron-squirrel-startup";
@@ -45,13 +45,13 @@ app.whenReady().then(() => {
return await handleOpenFolder(senderWindow);
});
createWindow();
- app.on("activate", function () {
- if (BrowserWindow.getAllWindows().length === 0) createWindow();
- });
+ if (process.platform === "darwin") {
+ app.on("activate", function () {
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
+ });
+ }
});
-app.on("ready", createWindow);
-
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
diff --git a/src/preload.ts b/src/preload.ts
index 4ce1923..97a4d39 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -2,7 +2,9 @@
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
import { contextBridge, ipcRenderer } from "electron";
+import type { FolderTree } from "./types/global";
contextBridge.exposeInMainWorld("electronAPI", {
- openFolder: () => ipcRenderer.invoke("dialog:openFolder"),
+ openFolder: () =>
+ ipcRenderer.invoke("dialog:openFolder") as Promise,
});
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
index 76fe0c9..0940b40 100644
--- a/src/types/global.d.ts
+++ b/src/types/global.d.ts
@@ -1,9 +1,21 @@
// src/types/global.d.ts
+// FolderTree type for folder structure
+type FolderTree = {
+ name: string;
+ path: string;
+ type: "directory" | "file";
+ children?: FolderTree[];
+};
+
// Extend the Window interface to include electronAPI
-interface Window {
- electronAPI: {
- openFolder: () => Promise;
- // Add other methods as needed
- };
+declare global {
+ interface Window {
+ electronAPI: {
+ openFolder: () => Promise;
+ // Add other methods as needed
+ };
+ }
}
+
+export { FolderTree };
diff --git a/tsconfig.json b/tsconfig.json
index 74434b2..4de3018 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,5 +11,9 @@
"outDir": "dist",
"moduleResolution": "node",
"resolveJsonModule": true
- }
+ },
+ "include": [
+ "src/types/global.d.ts",
+ "src/**/*.ts"
+ ]
}