From 9789be6d70f724f0d1a1994c25055835300a420e Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Wed, 11 Mar 2026 22:08:30 +0100 Subject: [PATCH] minor refactor, cleanup --- ARCHITECTURE.md | 43 ++++++++++++++++++++ Dockerfile | 11 ++---- README.md | 10 +++++ scripts/patch-obsidian.js | 1 - server/index.js | 7 ---- server/routes/fs.js | 44 +++++++++++---------- server/routes/vault.js | 8 ++-- shims/btime.js | 3 +- shims/crypto/create-hash.js | 42 +++++++++----------- shims/crypto/index.js | 9 ++--- shims/crypto/random-bytes.js | 14 +++---- shims/crypto/scrypt.js | 23 +++++------ shims/electron/index.js | 5 --- shims/electron/ipc-renderer.js | 30 -------------- shims/electron/remote/app.js | 26 ++++++------ shims/electron/remote/clipboard.js | 20 ++++------ shims/electron/remote/dialog.js | 40 ++++++++++--------- shims/electron/remote/index.js | 3 -- shims/electron/remote/menu.js | 22 +++++------ shims/electron/remote/native-image.js | 5 +-- shims/electron/remote/notification.js | 23 ++++++----- shims/electron/remote/screen.js | 22 ++++++++--- shims/electron/remote/session.js | 7 ++-- shims/electron/remote/shell.js | 13 ++---- shims/electron/remote/system-preferences.js | 7 +--- shims/electron/remote/theme.js | 18 ++++----- shims/electron/remote/window.js | 7 ---- shims/electron/web-frame.js | 3 -- shims/fs/index.js | 28 ++++--------- shims/fs/promises.js | 24 ++--------- shims/fs/sync.js | 9 +---- shims/fs/transport.js | 18 +-------- shims/fs/watch.js | 23 +++++------ shims/loader.js | 30 +------------- shims/path.js | 2 +- shims/process.js | 17 ++++---- shims/ui/vault-manager.js | 8 +--- shims/url.js | 13 +++--- 38 files changed, 259 insertions(+), 379 deletions(-) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..fa927a0 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,43 @@ +# Architecture + +This document covers how the shim layer is structured. + +## Loading + +The index file is patched to run the shim loader first. It replaces the module system and makes a blocking HTTP request to fetch the vault's directory tree into memory. The request has to be blocking because Obsidian makes synchronous filesystem calls during page load, before the event loop is running, so the cache has to already be populated. + +## Shims + +| Module | Implementation | +| -------------------- | --------------------------------------------------------------------------------- | +| `fs` / `original-fs` | HTTP transport + client-side metadata/content caches | +| `electron` | ipcRenderer dispatcher, webFrame stubs | +| `@electron/remote` | Partial: clipboard (browser API), shell, dialog, Menu, BrowserWindow, nativeTheme | +| `path` | path-browserify | +| `crypto` | Web Crypto (randomBytes, createHash, scrypt) | +| `url` | Browser URL API wrapper | +| `process` | Platform/version stubs | + +Unknown modules return an empty proxy and log a warning. The shim exposes two console helpers, one showing everything that has been accessed and one showing what is missing. + +## Filesystem + +On page load the server returns the full directory tree, which gets cached in memory with paths, sizes, and modification times. Sync filesystem calls hit the cache rather than the network. File contents are cached after first read and written through immediately on writes. + +Sync calls use synchronous XHR, to ensure blocking behavior. Async calls use fetch. Everything goes through a transport layer that handles vault ID injection, base64 encoding for binary files, and mapping HTTP error codes back to Node errno values. + +## IPC + +IPC is faked with a synchronous dispatcher that maps channel names to handlers. + +## Vaults + +Any subdirectory under the vault root is treated as a vault. The active vault is selected via a URL parameter. A custom vault manager modal replaces Obsidian's native startup screen. + +## Plugins + +Obsidian evals plugin code with its own require that checks its internal module map first, then falls back to the window-level require, which is our shim. Plugins that use the filesystem, path utilities, or crypto get our implementations without any changes. Plugins that need child processes or native addons won't work. + +## Server + +A simple Express server that handles filesystem operations, vault management, and static file serving. diff --git a/Dockerfile b/Dockerfile index e16eea9..f4a6d81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,3 @@ -# Stage 1: Build shims and extract/patch Obsidian FROM node:20-slim AS build RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -10,20 +9,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /build -# Install dependencies first (layer caching) COPY package.json package-lock.json ./ RUN npm ci --ignore-scripts -# Copy source COPY build.js ./ COPY shims/ ./shims/ COPY scripts/ ./scripts/ COPY server/ ./server/ -# Build shim-loader bundle RUN npm run build:shims -# Download and extract Obsidian + ARG OBSIDIAN_VERSION=1.8.9 RUN curl -fSL "https://github.com/obsidianmd/obsidian-releases/releases/download/v${OBSIDIAN_VERSION}/obsidian_${OBSIDIAN_VERSION}_amd64.deb" \ -o /tmp/obsidian.deb \ @@ -33,7 +29,7 @@ RUN curl -fSL "https://github.com/obsidianmd/obsidian-releases/releases/download && tar -xf /tmp/obsidian-deb/data.tar.xz -C /tmp/obsidian-pkg \ && rm -rf /tmp/obsidian.deb /tmp/obsidian-deb -# Extract asar + RUN npx --yes @electron/asar extract \ /tmp/obsidian-pkg/opt/Obsidian/resources/obsidian.asar \ /build/obsidian-app \ @@ -42,10 +38,9 @@ RUN npx --yes @electron/asar extract \ # Patch index.html RUN node scripts/patch-obsidian.js /build/obsidian-app -# Copy built shim-loader into the obsidian app directory RUN cp dist/shim-loader.js /build/obsidian-app/shim-loader.js -# Stage 2: Production image +# Production image FROM node:20-slim WORKDIR /app diff --git a/README.md b/README.md index b4a55dc..8055c1d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # Ignis An Electron shim and server bridge for running Obsidian in a browser. + +## How it works + +Ignis replaces the electron backend of Obsidian with a browser-compatible 'shim' that intercepts calls to Node.js and Electron APIs and routes them to a server. + +An in-memory metadata cache is built on page load so that sync filesystem calls (`existsSync`, `statSync`, etc.) work without round-tripping to the server every time. Async reads and writes go over HTTP. IPC channels like `ipcRenderer.sendSync("vault")` are faked with a dispatcher that returns what Obsidian expects. Native stuff like clipboard, menus, and dialogs have minimal stubs. + +## Status + +Ignis is in an experimental state. Basic functionality works but no guarantee of stability or feature completeness. See [ARCHITECTURE.md](ARCHITECTURE.md) for details. diff --git a/scripts/patch-obsidian.js b/scripts/patch-obsidian.js index c4d8b8e..7b7c303 100644 --- a/scripts/patch-obsidian.js +++ b/scripts/patch-obsidian.js @@ -2,7 +2,6 @@ // Patches the extracted Obsidian asar for browser use: // 1. Removes Content-Security-Policy meta tag // 2. Injects shim-loader.js script (non-deferred, before all other scripts) -// Patches both index.html and starter.html. const fs = require("fs"); const path = require("path"); diff --git a/server/index.js b/server/index.js index 6936a94..352f804 100644 --- a/server/index.js +++ b/server/index.js @@ -7,7 +7,6 @@ const app = express(); app.use(express.json({ limit: "50mb" })); -// --- Request logging --- app.use((req, res, next) => { const start = Date.now(); const origEnd = res.end; @@ -29,7 +28,6 @@ app.use((req, res, next) => { next(); }); -// --- Routes --- const fsRoutes = require("./routes/fs"); const vaultRoutes = require("./routes/vault"); @@ -51,15 +49,10 @@ app.use("/vault-files", (req, res, next) => { express.static(vaultPath)(req, res, next); }); -// --- Static serving --- -// dist/ has shim-loader.js + patched index.html (dev mode). -// In Docker, these live inside the obsidian assets dir instead. app.use(express.static(path.join(__dirname, "..", "dist"))); -// Serve obsidian assets (app.js, app.css, libs, fonts, etc.) app.use(express.static(config.obsidianAssetsPath)); -// --- Start --- const server = app.listen(config.port, () => { console.log( `[obsidian-bridge] Server running on http://localhost:${config.port}`, diff --git a/server/routes/fs.js b/server/routes/fs.js index 49f4f1c..6602707 100644 --- a/server/routes/fs.js +++ b/server/routes/fs.js @@ -17,8 +17,7 @@ function getVaultRoot(req, res) { } // Resolve a client-provided path to an absolute path within a vault. -// Strips leading slashes so paths from the client are always treated as -// relative to the vault root. Rejects path traversal attempts. +// Strips leading slashes so paths from the client are always treated as relative to the vault root. function resolveVaultPath(vaultRoot, relativePath) { const cleaned = (relativePath || "").replace(/^\/+/, ""); const resolved = path.resolve(vaultRoot, cleaned); @@ -46,6 +45,19 @@ function guardPath(req, res) { return resolved; } +// Same as guardPath but reads path from req.body (POST routes) +function guardBodyPath(req, res) { + const vaultRoot = getVaultRoot(req, res); + if (!vaultRoot) return null; + const resolved = resolveVaultPath(vaultRoot, req.body?.path); + if (!resolved) { + res.status(403).json({ error: "Invalid path" }); + return null; + } + req._vaultRoot = vaultRoot; + return resolved; +} + // GET /api/fs/stat?path=... router.get("/stat", async (req, res) => { const resolved = guardPath(req, res); @@ -70,7 +82,7 @@ router.get("/readdir", async (req, res) => { const resolved = guardPath(req, res); if (!resolved) return; try { - // Check if path is a file - return ENOTDIR instead of crashing + // Check if path is a file. return ENOTDIR instead of crashing const stat = await fs.promises.stat(resolved); if (!stat.isDirectory()) { return res @@ -123,10 +135,8 @@ router.get("/readFile", async (req, res) => { // POST /api/fs/writeFile { path, content, encoding?, vault? } router.post("/writeFile", async (req, res) => { - const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return; - const resolved = resolveVaultPath(vaultRoot, req.body?.path); - if (!resolved) return res.status(403).json({ error: "Invalid path" }); + const resolved = guardBodyPath(req, res); + if (!resolved) return; try { // Ensure parent directory exists const dir = path.dirname(resolved); @@ -151,10 +161,8 @@ router.post("/writeFile", async (req, res) => { // POST /api/fs/appendFile { path, content, vault? } router.post("/appendFile", async (req, res) => { - const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return; - const resolved = resolveVaultPath(vaultRoot, req.body?.path); - if (!resolved) return res.status(403).json({ error: "Invalid path" }); + const resolved = guardBodyPath(req, res); + if (!resolved) return; try { await fs.promises.appendFile(resolved, req.body.content, "utf-8"); res.json({ ok: true }); @@ -165,10 +173,8 @@ router.post("/appendFile", async (req, res) => { // POST /api/fs/mkdir { path, recursive?, vault? } router.post("/mkdir", async (req, res) => { - const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return; - const resolved = resolveVaultPath(vaultRoot, req.body?.path); - if (!resolved) return res.status(403).json({ error: "Invalid path" }); + const resolved = guardBodyPath(req, res); + if (!resolved) return; try { await fs.promises.mkdir(resolved, { recursive: !!req.body.recursive }); res.json({ ok: true }); @@ -277,10 +283,8 @@ router.get("/realpath", async (req, res) => { // POST /api/fs/utimes { path, atime, mtime, vault? } router.post("/utimes", async (req, res) => { - const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return; - const resolved = resolveVaultPath(vaultRoot, req.body?.path); - if (!resolved) return res.status(403).json({ error: "Invalid path" }); + const resolved = guardBodyPath(req, res); + if (!resolved) return; try { await fs.promises.utimes( resolved, @@ -293,7 +297,7 @@ router.post("/utimes", async (req, res) => { } }); -// GET /api/fs/tree?path=...&vault=... - returns full recursive file tree with metadata +// GET /api/fs/tree?path=...&vault=... returns full recursive file tree with metadata router.get("/tree", async (req, res) => { const vaultRoot = getVaultRoot(req, res); if (!vaultRoot) return; diff --git a/server/routes/vault.js b/server/routes/vault.js index 804d105..a63ee05 100644 --- a/server/routes/vault.js +++ b/server/routes/vault.js @@ -5,7 +5,7 @@ const path = require("path"); const router = express.Router(); -// GET /api/vault/list - returns all discovered vaults (re-scans on each call) +// GET /api/vault/list - returns all discovered vaults (re-scans on each call) router.get("/list", (req, res) => { config.refreshVaults(); const list = Object.entries(config.vaults).map(([id, vaultPath]) => ({ @@ -16,7 +16,7 @@ router.get("/list", (req, res) => { res.json(list); }); -// GET /api/vault/info?vault= - returns info for a specific vault +// GET /api/vault/info?vault= - returns info for a specific vault router.get("/info", (req, res) => { const vaultId = req.query.vault || config.defaultVaultId; const vaultPath = config.getVaultPath(vaultId); @@ -32,7 +32,7 @@ router.get("/info", (req, res) => { }); }); -// POST /api/vault/create { name } - create a new vault in VAULT_ROOT +// POST /api/vault/create { name } - create a new vault in VAULT_ROOT router.post("/create", async (req, res) => { const name = req.body?.name; if (!name || /[\/\\:*?"<>|]/.test(name)) { @@ -54,7 +54,7 @@ router.post("/create", async (req, res) => { } }); -// DELETE /api/vault/remove?vault= - remove a vault from disk +// DELETE /api/vault/remove?vault= - remove a vault from disk router.delete("/remove", async (req, res) => { const vaultId = req.query.vault; const vaultPath = config.getVaultPath(vaultId); diff --git a/shims/btime.js b/shims/btime.js index 4e8582b..a4627a1 100644 --- a/shims/btime.js +++ b/shims/btime.js @@ -1,5 +1,4 @@ -// Shim for the btime native module (file birth time) // Obsidian wraps this in try/catch: try{this.btime=window.require("btime")}catch(e){} -// Returning null causes graceful degradation - mtime is used instead. +// Returning null causes graceful degradation. mtime is used instead. export const btimeShim = null; diff --git a/shims/crypto/create-hash.js b/shims/crypto/create-hash.js index 2b8c75a..808ac31 100644 --- a/shims/crypto/create-hash.js +++ b/shims/crypto/create-hash.js @@ -1,20 +1,21 @@ -// Shim for crypto.createHash -// Obsidian uses createHash('SHA256') for signature verification (main process only) -// and possibly for content hashing in the renderer. -// Uses SubtleCrypto where possible. - export function createHash(algorithm) { - const alg = algorithm.toUpperCase().replace('-', ''); - const subtleAlg = alg === 'SHA256' ? 'SHA-256' : alg === 'SHA1' ? 'SHA-1' : alg === 'SHA512' ? 'SHA-512' : alg; + const alg = algorithm.toUpperCase().replace("-", ""); + const subtleAlg = + alg === "SHA256" + ? "SHA-256" + : alg === "SHA1" + ? "SHA-1" + : alg === "SHA512" + ? "SHA-512" + : alg; let inputData = new Uint8Array(0); return { update(data) { - if (typeof data === 'string') { + if (typeof data === "string") { data = new TextEncoder().encode(data); } - // Concatenate const merged = new Uint8Array(inputData.length + data.length); merged.set(inputData); merged.set(data, inputData.length); @@ -22,27 +23,23 @@ export function createHash(algorithm) { return this; }, - // Note: digest is sync in Node but we may need async. - // For now provide sync hex/base64 via a simple JS implementation. - // TODO: evaluate if any sync call sites exist; if not, make this async. digest(encoding) { - // Fallback: simple sync hash (for SHA-256 only) - // This is a placeholder - swap in a proper sync implementation if needed - console.warn('[shim:crypto] createHash.digest - using placeholder'); + console.warn("[shim:crypto] createHash.digest - using placeholder"); const hash = simpleHash(inputData); - if (encoding === 'hex') return hash; - if (encoding === 'base64') return btoa(hash); + if (encoding === "hex") return hash; + if (encoding === "base64") return btoa(hash); return hash; }, - // Async alternative for contexts that can await async digestAsync(encoding) { const hashBuffer = await crypto.subtle.digest(subtleAlg, inputData); const hashArray = new Uint8Array(hashBuffer); - if (encoding === 'hex') { - return Array.from(hashArray).map(b => b.toString(16).padStart(2, '0')).join(''); + if (encoding === "hex") { + return Array.from(hashArray) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); } - if (encoding === 'base64') { + if (encoding === "base64") { return btoa(String.fromCharCode(...hashArray)); } return hashArray; @@ -50,11 +47,10 @@ export function createHash(algorithm) { }; } -// Very basic placeholder hash - not cryptographic, just for bootstrapping function simpleHash(data) { let hash = 0; for (let i = 0; i < data.length; i++) { hash = ((hash << 5) - hash + data[i]) | 0; } - return Math.abs(hash).toString(16).padStart(8, '0'); + return Math.abs(hash).toString(16).padStart(8, "0"); } diff --git a/shims/crypto/index.js b/shims/crypto/index.js index 08ff284..8155c7f 100644 --- a/shims/crypto/index.js +++ b/shims/crypto/index.js @@ -1,9 +1,6 @@ -// Crypto shim -// Obsidian uses: scrypt, randomBytes, createHash - -import { randomBytes } from './random-bytes.js'; -import { createHash } from './create-hash.js'; -import { scrypt } from './scrypt.js'; +import { randomBytes } from "./random-bytes.js"; +import { createHash } from "./create-hash.js"; +import { scrypt } from "./scrypt.js"; export const cryptoShim = { randomBytes, diff --git a/shims/crypto/random-bytes.js b/shims/crypto/random-bytes.js index 87ba782..5d4aded 100644 --- a/shims/crypto/random-bytes.js +++ b/shims/crypto/random-bytes.js @@ -1,16 +1,14 @@ -// Shim for crypto.randomBytes -// Uses Web Crypto API under the hood - export function randomBytes(size) { const buf = new Uint8Array(size); crypto.getRandomValues(buf); - // Add Buffer-like convenience methods - buf.toString = function(encoding) { - if (encoding === 'hex') { - return Array.from(this).map(b => b.toString(16).padStart(2, '0')).join(''); + buf.toString = function (encoding) { + if (encoding === "hex") { + return Array.from(this) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); } - if (encoding === 'base64') { + if (encoding === "base64") { return btoa(String.fromCharCode(...this)); } return new TextDecoder().decode(this); diff --git a/shims/crypto/scrypt.js b/shims/crypto/scrypt.js index 0eb066f..b0eca8c 100644 --- a/shims/crypto/scrypt.js +++ b/shims/crypto/scrypt.js @@ -1,12 +1,5 @@ -// Shim for crypto.scrypt -// Delegates to window.scrypt which is already loaded by Obsidian's own scrypt.js - export function scrypt(password, salt, keylen, options, callback) { - // Node signature: scrypt(password, salt, keylen, options, callback) - // Obsidian's app.js checks for window.require("crypto") and uses it if available, - // otherwise falls back to window.scrypt - so this shim just delegates to the latter. - - if (typeof options === 'function') { + if (typeof options === "function") { callback = options; options = {}; } @@ -16,14 +9,18 @@ export function scrypt(password, salt, keylen, options, callback) { const p = options?.p || 1; if (window.scrypt && window.scrypt.scrypt) { - // Use the browser scrypt library already loaded by Obsidian - const pwBytes = typeof password === 'string' ? new TextEncoder().encode(password) : password; - const saltBytes = typeof salt === 'string' ? new TextEncoder().encode(salt) : salt; + const pwBytes = + typeof password === "string" + ? new TextEncoder().encode(password) + : password; + const saltBytes = + typeof salt === "string" ? new TextEncoder().encode(salt) : salt; - window.scrypt.scrypt(pwBytes, saltBytes, N, r, p, keylen) + window.scrypt + .scrypt(pwBytes, saltBytes, N, r, p, keylen) .then((result) => callback(null, new Uint8Array(result))) .catch((err) => callback(err)); } else { - callback(new Error('scrypt not available')); + callback(new Error("scrypt not available")); } } diff --git a/shims/electron/index.js b/shims/electron/index.js index 08ac557..03b28db 100644 --- a/shims/electron/index.js +++ b/shims/electron/index.js @@ -1,6 +1,3 @@ -// Electron module shim -// Returned when Obsidian calls: window.require('electron') - import { ipcRenderer } from "./ipc-renderer.js"; import { webFrame } from "./web-frame.js"; import { remoteShim } from "./remote/index.js"; @@ -10,14 +7,12 @@ export const electronShim = { webFrame, remote: remoteShim, - // electron.webUtils - used for drag/drop file path extraction (desktop only) webUtils: { getPathForFile(file) { return ""; }, }, - // electron.deprecate - used by Obsidian to mark deprecated APIs deprecate: { function(fn, name) { return fn; diff --git a/shims/electron/ipc-renderer.js b/shims/electron/ipc-renderer.js index d9899fa..0c75327 100644 --- a/shims/electron/ipc-renderer.js +++ b/shims/electron/ipc-renderer.js @@ -1,28 +1,7 @@ -// Shim for electron.ipcRenderer -// Obsidian uses: .send(), .sendSync(), .on(), .once() -// -// sendSync channels discovered in app.js: -// vault → {id, path} - critical for startup -// version → string - app version -// is-dev → boolean - dev mode flag -// file-url → string - base URL prefix for vault assets -// disable-update → boolean - whether updates are disabled -// update → string - update status -// disable-gpu → boolean - GPU acceleration toggle -// frame → void - window frame style -// set-icon → void - custom vault icon -// get-icon → null|object - get custom vault icon -// relaunch → void - restart app -// starter → void - open vault chooser -// help → void - open help -// sandbox → void - open sandbox vault -// copy-asar → boolean - install update - import { showVaultManager } from "../ui/vault-manager.js"; const listeners = new Map(); -// Sync channel handlers - must return values synchronously const syncHandlers = { vault: () => window.__vaultConfig || { id: "default-vault", path: "/" }, version: () => "1.8.9", @@ -51,7 +30,6 @@ const syncHandlers = { "copy-asar": () => false, "check-update": () => null, "vault-list": () => { - // Starter expects an object keyed by ID: {id: {path, ts, name}} const result = {}; for (const v of window.__vaultList || []) { result[v.id] = { @@ -66,14 +44,12 @@ const syncHandlers = { const id = (vaultPath || "").replace(/^\/+/, ""); const vault = (window.__vaultList || []).find((v) => v.id === id); if (!vault && id) { - // New vault created by starter - create it on the server const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/vault/create", false); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(JSON.stringify({ name: id })); if (xhr.status >= 400) return "Failed to create vault"; } - // Navigate - use parent if in iframe, otherwise current window const target = window.parent !== window ? window.parent : window; target.location.href = "/?vault=" + encodeURIComponent(id); return true; @@ -90,7 +66,6 @@ const syncHandlers = { return xhr.status < 400; }, "vault-move": (oldPath, newPath) => { - // Not supported in web context return "Moving vaults is not supported in the web version"; }, "vault-message": () => null, @@ -105,10 +80,6 @@ export const ipcRenderer = { send(channel, ...args) { console.log("[shim:ipcRenderer] send:", channel, args); - // context-menu: Obsidian sends this and waits (up to 1s) for a response. - // In Electron, the main process returns spell-check info + edit flags. - // We reply immediately with a response object so Obsidian proceeds to - // build and show its HTML context menu without delay. if (channel === "context-menu") { queueMicrotask(() => ipcRenderer._emit("context-menu", { @@ -163,7 +134,6 @@ export const ipcRenderer = { return ipcRenderer; }, - // Internal: emit an event to registered listeners (used by ws bridge) _emit(channel, ...args) { const arr = listeners.get(channel); if (arr) { diff --git a/shims/electron/remote/app.js b/shims/electron/remote/app.js index 65dcfd6..0c1faee 100644 --- a/shims/electron/remote/app.js +++ b/shims/electron/remote/app.js @@ -1,36 +1,32 @@ -// Shim for remote.app -// Obsidian uses: getPath, getVersion, getName, quit, isPackaged, getLocale - export const appShim = { getPath(name) { - // Return web-friendly paths; config lives server-side in the vault's .obsidian/ dir const paths = { - userData: '/.obsidian', - home: '/', - documents: '/documents', - desktop: '/desktop', - temp: '/tmp', - appData: '/.obsidian', + userData: "/.obsidian", + home: "/", + documents: "/documents", + desktop: "/desktop", + temp: "/tmp", + appData: "/.obsidian", }; - return paths[name] || '/'; + return paths[name] || "/"; }, getVersion() { - return '1.8.9'; + return "1.8.9"; }, getName() { - return 'Obsidian'; + return "Obsidian"; }, getLocale() { - return navigator.language || 'en-US'; + return navigator.language || "en-US"; }, isPackaged: true, quit() { - console.log('[shim:app] quit (stub)'); + console.log("[shim:app] quit (stub)"); }, relaunch() { diff --git a/shims/electron/remote/clipboard.js b/shims/electron/remote/clipboard.js index 8e604f0..a20b0af 100644 --- a/shims/electron/remote/clipboard.js +++ b/shims/electron/remote/clipboard.js @@ -1,35 +1,29 @@ -// Shim for remote.clipboard -// Obsidian uses: readText, writeText, readImage, writeImage, readHTML, writeHTML - export const clipboardShim = { readText() { - // navigator.clipboard.readText() is async; return empty for sync calls // TODO: maintain a local mirror updated via async reads - return ''; + return ""; }, writeText(text) { navigator.clipboard.writeText(text).catch((e) => { - console.warn('[shim:clipboard] writeText failed:', e); + console.warn("[shim:clipboard] writeText failed:", e); }); }, readHTML() { - return ''; + return ""; }, writeHTML(html) { - // TODO: use clipboard API with text/html mime type - console.log('[shim:clipboard] writeHTML (stub)'); + console.log("[shim:clipboard] writeHTML (stub)"); }, readImage() { - // TODO: implement if needed return { isEmpty: () => true, toPNG: () => new Uint8Array(0) }; }, writeImage(image) { - console.log('[shim:clipboard] writeImage (stub)'); + console.log("[shim:clipboard] writeImage (stub)"); }, has(format) { @@ -37,10 +31,10 @@ export const clipboardShim = { }, read(format) { - return ''; + return ""; }, clear() { - navigator.clipboard.writeText('').catch(() => {}); + navigator.clipboard.writeText("").catch(() => {}); }, }; diff --git a/shims/electron/remote/dialog.js b/shims/electron/remote/dialog.js index d478246..1b39bf8 100644 --- a/shims/electron/remote/dialog.js +++ b/shims/electron/remote/dialog.js @@ -1,38 +1,40 @@ -// Shim for remote.dialog -// Obsidian uses: showOpenDialog, showSaveDialog, showMessageBox, showErrorBox - export const dialogShim = { async showOpenDialog(browserWindow, options) { - // TODO: implement custom modal UI with server-side file listing - console.log('[shim:dialog] showOpenDialog (stub):', options); + // TODO: implement custom modal with server-side file listing + console.log("[shim:dialog] showOpenDialog (stub):", options); return { canceled: true, filePaths: [] }; }, async showSaveDialog(browserWindow, options) { - // TODO: implement custom modal UI - console.log('[shim:dialog] showSaveDialog (stub):', options); + // TODO: implement custom modal + console.log("[shim:dialog] showSaveDialog (stub):", options); return { canceled: true, filePath: undefined }; }, async showMessageBox(browserWindow, options) { - // TODO: implement custom modal matching Electron's return format - // For now, use browser confirm/alert as rough approximation - if (typeof browserWindow === 'object' && !options) { + if (typeof browserWindow === "object" && !options) { options = browserWindow; } - console.log('[shim:dialog] showMessageBox:', options); + console.log("[shim:dialog] showMessageBox:", options); - const message = options.message || ''; - const detail = options.detail || ''; - const buttons = options.buttons || ['OK']; + const message = options.message || ""; + const detail = options.detail || ""; + const buttons = options.buttons || ["OK"]; - // Simple fallback: use confirm for 2-button, alert for 1-button if (buttons.length <= 1) { - alert(message + (detail ? '\n\n' + detail : '')); + alert(message + (detail ? "\n\n" + detail : "")); return { response: 0, checkboxChecked: false }; } - const result = confirm(message + (detail ? '\n\n' + detail : '') + '\n\n[OK] = "' + buttons[0] + '", [Cancel] = "' + buttons[1] + '"'); + const result = confirm( + message + + (detail ? "\n\n" + detail : "") + + '\n\n[OK] = "' + + buttons[0] + + '", [Cancel] = "' + + buttons[1] + + '"', + ); return { response: result ? 0 : 1, checkboxChecked: false, @@ -40,7 +42,7 @@ export const dialogShim = { }, showErrorBox(title, content) { - console.error('[shim:dialog] Error:', title, content); - alert(title + '\n\n' + content); + console.error("[shim:dialog] Error:", title, content); + alert(title + "\n\n" + content); }, }; diff --git a/shims/electron/remote/index.js b/shims/electron/remote/index.js index c2f7c2f..0118fdd 100644 --- a/shims/electron/remote/index.js +++ b/shims/electron/remote/index.js @@ -1,6 +1,3 @@ -// @electron/remote shim -// Returned when Obsidian calls: window.require('@electron/remote') - import { clipboardShim } from "./clipboard.js"; import { shellShim } from "./shell.js"; import { dialogShim } from "./dialog.js"; diff --git a/shims/electron/remote/menu.js b/shims/electron/remote/menu.js index 5223cc1..306128f 100644 --- a/shims/electron/remote/menu.js +++ b/shims/electron/remote/menu.js @@ -1,6 +1,3 @@ -// Shim for remote.Menu and remote.MenuItem -// Obsidian uses: Menu.buildFromTemplate, Menu.popup, Menu.setApplicationMenu - export class menuShim { constructor() { this.items = []; @@ -8,13 +5,12 @@ export class menuShim { static buildFromTemplate(template) { const menu = new menuShim(); - menu.items = (template || []).map(item => new menuItemShim(item)); + menu.items = (template || []).map((item) => new menuItemShim(item)); return menu; } static setApplicationMenu(menu) { - // No native menu bar in browser - no-op - console.log('[shim:Menu] setApplicationMenu (stub)'); + console.log("[shim:Menu] setApplicationMenu (stub)"); } static getApplicationMenu() { @@ -22,8 +18,8 @@ export class menuShim { } popup(options) { - // TODO: implement custom HTML context menu rendered at mouse position - console.log('[shim:Menu] popup (stub)', options); + // TODO: render custom HTML context menu at mouse position + console.log("[shim:Menu] popup (stub)", options); } append(menuItem) { @@ -41,19 +37,19 @@ export class menuShim { export class menuItemShim { constructor(options = {}) { - this.label = options.label || ''; - this.type = options.type || 'normal'; + this.label = options.label || ""; + this.type = options.type || "normal"; this.click = options.click || null; this.role = options.role || null; - this.accelerator = options.accelerator || ''; + this.accelerator = options.accelerator || ""; this.enabled = options.enabled !== false; this.visible = options.visible !== false; this.checked = !!options.checked; this.submenu = options.submenu ? menuShim.buildFromTemplate( - Array.isArray(options.submenu) ? options.submenu : [] + Array.isArray(options.submenu) ? options.submenu : [], ) : null; - this.id = options.id || ''; + this.id = options.id || ""; } } diff --git a/shims/electron/remote/native-image.js b/shims/electron/remote/native-image.js index 206853c..92a4873 100644 --- a/shims/electron/remote/native-image.js +++ b/shims/electron/remote/native-image.js @@ -1,6 +1,3 @@ -// Shim for remote.nativeImage -// Minimal stub - Obsidian's renderer-side usage is limited - export const nativeImageShim = { createFromBuffer(buffer) { return { @@ -8,7 +5,7 @@ export const nativeImageShim = { getSize: () => ({ width: 0, height: 0 }), toPNG: () => buffer || new Uint8Array(0), toJPEG: (quality) => buffer || new Uint8Array(0), - toDataURL: () => '', + toDataURL: () => "", }; }, diff --git a/shims/electron/remote/notification.js b/shims/electron/remote/notification.js index 864a594..ec11de2 100644 --- a/shims/electron/remote/notification.js +++ b/shims/electron/remote/notification.js @@ -1,21 +1,24 @@ -// Shim for remote.Notification -// Maps to browser Notification API - export class notificationShim { constructor(options = {}) { - this.title = options.title || ''; - this.body = options.body || ''; + this.title = options.title || ""; + this.body = options.body || ""; this.silent = options.silent || false; this._handlers = {}; } show() { - if ('Notification' in window && Notification.permission === 'granted') { + if ("Notification" in window && Notification.permission === "granted") { new Notification(this.title, { body: this.body, silent: this.silent }); - } else if ('Notification' in window && Notification.permission !== 'denied') { + } else if ( + "Notification" in window && + Notification.permission !== "denied" + ) { Notification.requestPermission().then((perm) => { - if (perm === 'granted') { - new Notification(this.title, { body: this.body, silent: this.silent }); + if (perm === "granted") { + new Notification(this.title, { + body: this.body, + silent: this.silent, + }); } }); } @@ -29,6 +32,6 @@ export class notificationShim { } static isSupported() { - return 'Notification' in window; + return "Notification" in window; } } diff --git a/shims/electron/remote/screen.js b/shims/electron/remote/screen.js index 323e9f2..16a4ef5 100644 --- a/shims/electron/remote/screen.js +++ b/shims/electron/remote/screen.js @@ -1,14 +1,24 @@ -// Shim for remote.screen -// Obsidian uses screen for display/monitor info - export const screenShim = { getPrimaryDisplay() { return { - workAreaSize: { width: window.screen.availWidth, height: window.screen.availHeight }, + workAreaSize: { + width: window.screen.availWidth, + height: window.screen.availHeight, + }, size: { width: window.screen.width, height: window.screen.height }, scaleFactor: window.devicePixelRatio || 1, - bounds: { x: 0, y: 0, width: window.screen.width, height: window.screen.height }, - workArea: { x: 0, y: 0, width: window.screen.availWidth, height: window.screen.availHeight }, + bounds: { + x: 0, + y: 0, + width: window.screen.width, + height: window.screen.height, + }, + workArea: { + x: 0, + y: 0, + width: window.screen.availWidth, + height: window.screen.availHeight, + }, }; }, diff --git a/shims/electron/remote/session.js b/shims/electron/remote/session.js index f621a82..d21e3e5 100644 --- a/shims/electron/remote/session.js +++ b/shims/electron/remote/session.js @@ -1,6 +1,3 @@ -// Shim for remote.session -// Mostly no-op; Obsidian's use is minimal - export const sessionShim = { defaultSession: { clearCache() { @@ -12,7 +9,9 @@ export const sessionShim = { }, setSpellCheckerLanguages(langs) {}, - getSpellCheckerLanguages() { return []; }, + getSpellCheckerLanguages() { + return []; + }, on() {}, once() {}, diff --git a/shims/electron/remote/shell.js b/shims/electron/remote/shell.js index c3f0d95..52cd479 100644 --- a/shims/electron/remote/shell.js +++ b/shims/electron/remote/shell.js @@ -1,20 +1,15 @@ -// Shim for remote.shell -// Obsidian uses: openExternal, openPath, showItemInFolder - export const shellShim = { openExternal(url) { - window.open(url, '_blank'); + window.open(url, "_blank"); return Promise.resolve(); }, openPath(filePath) { - // TODO: could trigger a server-side download or preview - console.log('[shim:shell] openPath (stub):', filePath); - return Promise.resolve(''); + console.log("[shim:shell] openPath (stub):", filePath); + return Promise.resolve(""); }, showItemInFolder(filePath) { - // No OS file manager in browser context - console.log('[shim:shell] showItemInFolder (stub):', filePath); + console.log("[shim:shell] showItemInFolder (stub):", filePath); }, }; diff --git a/shims/electron/remote/system-preferences.js b/shims/electron/remote/system-preferences.js index 23f3615..90fe568 100644 --- a/shims/electron/remote/system-preferences.js +++ b/shims/electron/remote/system-preferences.js @@ -1,9 +1,6 @@ -// Shim for remote.systemPreferences -// No-op with safe defaults - export const systemPreferencesShim = { getAccentColor() { - return '0078d4'; // Default Windows accent blue + return "0078d4"; // Default Windows accent blue }, isAeroGlassEnabled() { @@ -11,7 +8,7 @@ export const systemPreferencesShim = { }, getMediaAccessStatus(mediaType) { - return 'granted'; + return "granted"; }, askForMediaAccess(mediaType) { diff --git a/shims/electron/remote/theme.js b/shims/electron/remote/theme.js index 179c766..4c6c031 100644 --- a/shims/electron/remote/theme.js +++ b/shims/electron/remote/theme.js @@ -1,14 +1,12 @@ -// Shim for remote.nativeTheme -// Obsidian uses: shouldUseDarkColors, on('updated', cb) - const listeners = []; -const darkQuery = typeof window !== 'undefined' - ? window.matchMedia('(prefers-color-scheme: dark)') - : null; +const darkQuery = + typeof window !== "undefined" + ? window.matchMedia("(prefers-color-scheme: dark)") + : null; if (darkQuery?.addEventListener) { - darkQuery.addEventListener('change', () => { + darkQuery.addEventListener("change", () => { for (const fn of listeners) { fn(); } @@ -21,7 +19,7 @@ export const themeShim = { }, get themeSource() { - return 'system'; + return "system"; }, set themeSource(val) { @@ -29,14 +27,14 @@ export const themeShim = { }, on(event, callback) { - if (event === 'updated') { + if (event === "updated") { listeners.push(callback); } return themeShim; }, once(event, callback) { - if (event === 'updated') { + if (event === "updated") { const wrapped = () => { const idx = listeners.indexOf(wrapped); if (idx >= 0) listeners.splice(idx, 1); diff --git a/shims/electron/remote/window.js b/shims/electron/remote/window.js index 3bcd41d..88a86d8 100644 --- a/shims/electron/remote/window.js +++ b/shims/electron/remote/window.js @@ -1,8 +1,3 @@ -// Shim for remote.getCurrentWindow() / remote.BrowserWindow -// Obsidian uses: isMaximized, isMinimized, isFullScreen, minimize, maximize, -// unmaximize, close, setTitle, setAlwaysOnTop, isAlwaysOnTop, -// getBounds, setBounds, show, focus, setFullScreen, etc. - const currentWindowState = { title: "Obsidian", isMaximized: false, @@ -80,7 +75,6 @@ const currentWindow = { }, setBounds(bounds) { - // Cannot resize browser window from JS console.log("[shim:window] setBounds (stub):", bounds); }, @@ -113,7 +107,6 @@ const currentWindow = { }, on(event, handler) { - // Map some Electron window events to browser equivalents if (event === "focus") window.addEventListener("focus", handler); else if (event === "blur") window.addEventListener("blur", handler); else if (event === "resize") window.addEventListener("resize", handler); diff --git a/shims/electron/web-frame.js b/shims/electron/web-frame.js index 0767ea7..b47addc 100644 --- a/shims/electron/web-frame.js +++ b/shims/electron/web-frame.js @@ -1,6 +1,3 @@ -// Shim for electron.webFrame -// Obsidian uses: getZoomLevel(), setZoomLevel() - let currentZoom = 0; export const webFrame = { diff --git a/shims/fs/index.js b/shims/fs/index.js index 4bad27d..eb51cc4 100644 --- a/shims/fs/index.js +++ b/shims/fs/index.js @@ -1,17 +1,10 @@ -// Filesystem shim - the core piece -// Returned for both require('original-fs') and require('fs') -// -// Strategy: metadata cache + on-demand content fetch + write-through -// Server sync mechanism (REST vs WebSocket) is TBD - abstracted behind -// the transport layer in ./transport.js - -import { MetadataCache } from './metadata-cache.js'; -import { ContentCache } from './content-cache.js'; -import { transport } from './transport.js'; -import { createFsPromises } from './promises.js'; -import { createFsSync } from './sync.js'; -import { createFsWatch } from './watch.js'; -import { constants } from './constants.js'; +import { MetadataCache } from "./metadata-cache.js"; +import { ContentCache } from "./content-cache.js"; +import { transport } from "./transport.js"; +import { createFsPromises } from "./promises.js"; +import { createFsSync } from "./sync.js"; +import { createFsWatch } from "./watch.js"; +import { constants } from "./constants.js"; const metadataCache = new MetadataCache(); const contentCache = new ContentCache(); @@ -21,10 +14,8 @@ const fsSync = createFsSync(metadataCache, contentCache, transport); const fsWatch = createFsWatch(transport); export const fsShim = { - // Async promise-based API (this.fsPromises = this.fs.promises) promises: fsPromises, - // Sync methods existsSync: fsSync.existsSync, readFileSync: fsSync.readFileSync, writeFileSync: fsSync.writeFileSync, @@ -33,17 +24,12 @@ export const fsShim = { statSync: fsSync.statSync, readdirSync: fsSync.readdirSync, - // Watch watch: fsWatch.watch, - - // Constants constants, - // Internal: for initialization _metadataCache: metadataCache, _contentCache: contentCache, - // Initialize the caches by fetching the full tree from server async _init(basePath) { const tree = await transport.fetchTree(basePath); metadataCache.populate(tree); diff --git a/shims/fs/promises.js b/shims/fs/promises.js index c825bd1..516c247 100644 --- a/shims/fs/promises.js +++ b/shims/fs/promises.js @@ -1,10 +1,6 @@ -// Async fs.promises implementation -// Maps to transport layer (REST/WebSocket/hybrid - TBD) - export function createFsPromises(metadataCache, contentCache, transport) { return { async stat(path) { - // Try cache first, fall back to server const cached = metadataCache.toStat(path); if (cached) return cached; @@ -14,17 +10,15 @@ export function createFsPromises(metadataCache, contentCache, transport) { }, async lstat(path) { - // No symlinks in our context - same as stat + // No symlinks in our context return this.stat(path); }, async readdir(path) { - // If metadata cache knows this is a file, return empty (ENOTDIR) const meta = metadataCache.get(path); if (meta && meta.type === "file") { return []; } - // If path not in cache at all (and not root), it doesn't exist if (!meta && path && path !== "/" && path !== ".") { const e = new Error( `ENOENT: no such file or directory, scandir '${path}'`, @@ -32,7 +26,6 @@ export function createFsPromises(metadataCache, contentCache, transport) { e.code = "ENOENT"; throw e; } - // Serve from metadata cache const entries = metadataCache.readdir(path); return entries.map((e) => e.name); }, @@ -41,14 +34,12 @@ export function createFsPromises(metadataCache, contentCache, transport) { if (typeof encoding === "object") encoding = encoding?.encoding; const wantText = encoding === "utf8" || encoding === "utf-8"; - // Short-circuit: reading a directory is an error const meta = metadataCache.get(path); if (meta && meta.type === "directory") { const e = new Error("EISDIR: illegal operation on a directory, read"); e.code = "EISDIR"; throw e; } - // Short-circuit: file not in metadata cache → doesn't exist if (!meta && path) { const e = new Error( `ENOENT: no such file or directory, open '${path}'`, @@ -57,7 +48,6 @@ export function createFsPromises(metadataCache, contentCache, transport) { throw e; } - // Check content cache const cached = contentCache.get(path); if (cached !== null) { if (wantText) { @@ -65,14 +55,13 @@ export function createFsPromises(metadataCache, contentCache, transport) { ? cached : new TextDecoder().decode(cached); } - // Binary mode: ensure we return a proper Uint8Array with .buffer + // binary. ensure we return a proper Uint8Array with .buffer if (typeof cached === "string") { return new TextEncoder().encode(cached); } return cached; } - // Fetch from server const data = await transport.readFile(path, encoding); contentCache.set(path, data); return data; @@ -81,7 +70,6 @@ export function createFsPromises(metadataCache, contentCache, transport) { async writeFile(path, data, encoding) { if (typeof encoding === "object") encoding = encoding?.encoding; - // Update caches optimistically contentCache.set(path, data); const size = typeof data === "string" ? data.length : data.byteLength || 0; @@ -92,9 +80,7 @@ export function createFsPromises(metadataCache, contentCache, transport) { ctime: metadataCache.get(path)?.ctime || Date.now(), }); - // Send to server const result = await transport.writeFile(path, data, encoding); - // Update metadata with server-confirmed values if (result.mtime) { metadataCache.set(path, { type: "file", @@ -108,7 +94,7 @@ export function createFsPromises(metadataCache, contentCache, transport) { async appendFile(path, data, encoding) { contentCache.invalidate(path); await transport.appendFile(path, data); - // Refresh metadata + const meta = await transport.stat(path); metadataCache.set(path, meta); }, @@ -120,13 +106,11 @@ export function createFsPromises(metadataCache, contentCache, transport) { }, async rename(oldPath, newPath) { - // Move content cache entry const content = contentCache.get(oldPath); if (content !== null) { contentCache.set(newPath, content); contentCache.delete(oldPath); } - // Move metadata metadataCache.rename(oldPath, newPath); await transport.rename(oldPath, newPath); @@ -154,7 +138,6 @@ export function createFsPromises(metadataCache, contentCache, transport) { async copyFile(src, dest) { await transport.copyFile(src, dest); - // Refresh metadata for dest const meta = await transport.stat(dest); metadataCache.set(dest, meta); }, @@ -169,7 +152,6 @@ export function createFsPromises(metadataCache, contentCache, transport) { }, async realpath(path) { - // Empty path = vault root, return the vault base path if (!path || path === "/" || path === ".") return "/"; return transport.realpath(path); }, diff --git a/shims/fs/sync.js b/shims/fs/sync.js index f98165a..e7a9562 100644 --- a/shims/fs/sync.js +++ b/shims/fs/sync.js @@ -1,6 +1,3 @@ -// Synchronous fs method implementations -// Served from caches where possible, sync XHR fallback for uncached content. - export function createFsSync(metadataCache, contentCache, transport) { return { existsSync(path) { @@ -32,7 +29,6 @@ export function createFsSync(metadataCache, contentCache, transport) { readFileSync(path, encoding) { if (typeof encoding === "object") encoding = encoding?.encoding; - // Short-circuit: reading a directory is an error const meta = metadataCache.get(path); if (meta && meta.type === "directory") { const e = new Error("EISDIR: illegal operation on a directory, read"); @@ -40,7 +36,6 @@ export function createFsSync(metadataCache, contentCache, transport) { throw e; } - // Try content cache first const cached = contentCache.get(path); if (cached !== null) { if (encoding === "utf8" || encoding === "utf-8") { @@ -51,7 +46,6 @@ export function createFsSync(metadataCache, contentCache, transport) { return cached; } - // Fallback: synchronous XHR console.warn("[shim:fs] readFileSync cache miss, using sync XHR:", path); const data = transport.readFileSync(path, encoding); contentCache.set(path, data); @@ -61,7 +55,6 @@ export function createFsSync(metadataCache, contentCache, transport) { writeFileSync(path, data, encoding) { if (typeof encoding === "object") encoding = encoding?.encoding; - // Write to cache immediately (sync return) contentCache.set(path, data); const size = typeof data === "string" ? data.length : data.byteLength || 0; @@ -86,7 +79,7 @@ export function createFsSync(metadataCache, contentCache, transport) { contentCache.delete(path); metadataCache.delete(path); - // Fire-and-forget - suppress ENOENT (file already gone, e.g. .OBSIDIANTEST race) + // Fire-and-forget - suppress ENOENT (file already gone) transport.unlink(path).catch((e) => { if (e.code !== "ENOENT") { console.error( diff --git a/shims/fs/transport.js b/shims/fs/transport.js index 1ffa084..1babd33 100644 --- a/shims/fs/transport.js +++ b/shims/fs/transport.js @@ -1,16 +1,9 @@ -// Transport abstraction layer -// Decouples the fs shim from the sync mechanism (REST, WebSocket, or hybrid). -// Currently implements a REST-based transport. This can be swapped or extended -// once the sync strategy is finalized. - const API_BASE = "/api/fs"; -// Strip leading slashes from paths before sending to server function normPath(p) { return (p || "").replace(/^\/+/, ""); } -// Convert a Uint8Array to base64 without blowing the stack function uint8ToBase64(bytes) { let binary = ""; const chunk = 8192; @@ -56,12 +49,10 @@ async function requestJson(method, endpoint, params = {}) { return res.json(); } -// Synchronous XHR - used only as fallback for sync fs calls on uncached content. -// Blocking but functional. Should be rare after pre-warming. function requestSync(method, endpoint, params = {}) { const url = new URL(API_BASE + endpoint, window.location.origin); - if (method === "GET") { + if (method === "GET" || method === "DELETE") { if (vaultId()) url.searchParams.set("vault", vaultId()); for (const [key, val] of Object.entries(params)) { url.searchParams.set(key, val); @@ -71,7 +62,7 @@ function requestSync(method, endpoint, params = {}) { const xhr = new XMLHttpRequest(); xhr.open(method, url.toString(), false); // synchronous - if (method !== "GET") { + if (method !== "GET" && method !== "DELETE") { xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(JSON.stringify({ vault: vaultId(), ...params })); } else { @@ -95,8 +86,6 @@ function requestSync(method, endpoint, params = {}) { } export const transport = { - // --- Async methods (used by fs.promises) --- - async fetchTree(basePath) { return requestJson("GET", "/tree", basePath ? { path: basePath } : {}); }, @@ -190,8 +179,6 @@ export const transport = { }); }, - // --- Sync methods (fallback) --- - readFileSync(path, encoding) { const xhr = requestSync("GET", "/readFile", { path: normPath(path), @@ -200,7 +187,6 @@ export const transport = { if (encoding === "utf8" || encoding === "utf-8") { return xhr.responseText; } - // Binary: return as Uint8Array const binary = xhr.responseText; const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { diff --git a/shims/fs/watch.js b/shims/fs/watch.js index 5e23798..ace86a8 100644 --- a/shims/fs/watch.js +++ b/shims/fs/watch.js @@ -1,14 +1,9 @@ -// File watching shim -// Translates fs.watch() calls into WebSocket subscriptions. -// The server pushes file-change events; this module dispatches them -// to registered watch listeners. - export function createFsWatch(transport) { const watchers = new Map(); // path -> Set return { watch(path, options, listener) { - if (typeof options === 'function') { + if (typeof options === "function") { listener = options; options = {}; } @@ -32,22 +27,28 @@ export function createFsWatch(transport) { } } }, - on() { return this; }, - once() { return this; }, - removeListener() { return this; }, + on() { + return this; + }, + once() { + return this; + }, + removeListener() { + return this; + }, }; }, // Internal: called when transport receives a file-change event _dispatch(eventType, filePath) { for (const [watchPath, listeners] of watchers) { - if (filePath === watchPath || filePath.startsWith(watchPath + '/')) { + if (filePath === watchPath || filePath.startsWith(watchPath + "/")) { const relativeName = filePath.slice(watchPath.length + 1) || filePath; for (const fn of listeners) { try { fn(eventType, relativeName); } catch (e) { - console.error('[shim:fs:watch] Listener error:', e); + console.error("[shim:fs:watch] Listener error:", e); } } } diff --git a/shims/loader.js b/shims/loader.js index 558885f..29ef564 100644 --- a/shims/loader.js +++ b/shims/loader.js @@ -1,17 +1,11 @@ -// shim-loader.js -// Loaded before app.js. Defines window.require() and window.process -// to intercept all Electron/Node API calls from Obsidian's renderer code. - import { electronShim } from "./electron/index.js"; import { remoteShim } from "./electron/remote/index.js"; import { fsShim } from "./fs/index.js"; import { pathShim } from "./path.js"; import { urlShim } from "./url.js"; import { cryptoShim } from "./crypto/index.js"; -import { btimeShim } from "./btime.js"; import { processShim } from "./process.js"; -// Debug mode: wrap shims in Proxy to log all property accesses const DEBUG = true; const _accessLog = new Map(); // "module.property" -> count @@ -28,7 +22,7 @@ function wrapWithProxy(obj, name) { const key = `${name}.${prop}`; _accessLog.set(key, (_accessLog.get(key) || 0) + 1); if (!(prop in target)) { - console.warn(`[shim:MISS] ${key} - property not found on shim`); + console.warn(`[shim:MISS] ${key} - property not found on shim`); } } return target[prop]; @@ -36,7 +30,6 @@ function wrapWithProxy(obj, name) { }); } -// Expose access log for debugging in console: window.__shimLog() window.__shimLog = function () { const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]); console.table(sorted.map(([k, v]) => ({ api: k, calls: v }))); @@ -60,7 +53,6 @@ const rawRegistry = { path: pathShim, url: urlShim, crypto: cryptoShim, - btime: btimeShim, }; const shimRegistry = {}; @@ -68,7 +60,6 @@ for (const [name, shim] of Object.entries(rawRegistry)) { shimRegistry[name] = wrapWithProxy(shim, name); } -// Modules that should throw on require (native modules that don't exist in browser) const throwOnRequire = new Set(["btime", "get-fonts", "vibrancy-win"]); window.require = function (moduleName) { @@ -84,9 +75,7 @@ window.require = function (moduleName) { window.process = processShim; -// Provide a global Buffer if needed if (typeof window.Buffer === "undefined") { - // TODO: evaluate if a full Buffer polyfill is needed or if Uint8Array suffices window.Buffer = { from: function (data, encoding) { if (typeof data === "string") { @@ -113,22 +102,10 @@ if (typeof window.Buffer === "undefined") { }; } -// Prevent app.js from closing the window (browser blocks this anyway, but suppress the error) -// In an iframe (starter modal), close the modal overlay instead. -const _origClose = window.close; window.close = function () { - if (window.parent !== window) { - const modal = window.parent.document.getElementById("ignis-starter-modal"); - if (modal) modal.remove(); - return; - } console.log("[obsidian-bridge] window.close() blocked"); }; -// Suppress the browser's native context menu without breaking Obsidian's. -// Problem: preventDefault() blocks the browser menu but also sets -// event.defaultPrevented=true, which Obsidian checks to bail out. -// Solution: call preventDefault() then shadow defaultPrevented to return false. window.addEventListener( "contextmenu", (e) => { @@ -138,11 +115,9 @@ window.addEventListener( true, ); -// Read vault ID from URL query param (?vault=my-notes) const _urlParams = new URLSearchParams(window.location.search); window.__currentVaultId = _urlParams.get("vault") || ""; -// Fetch vault config from server synchronously (before metadata cache) (function initVaultConfig() { try { const vaultParam = window.__currentVaultId @@ -165,7 +140,6 @@ window.__currentVaultId = _urlParams.get("vault") || ""; } })(); -// Fetch vault list for IPC handlers (function initVaultList() { try { const xhr = new XMLHttpRequest(); @@ -179,8 +153,6 @@ window.__currentVaultId = _urlParams.get("vault") || ""; } })(); -// Pre-populate fs metadata cache synchronously before app.js runs. -// This ensures existsSync() works for the vault path during startup. (function initMetadataCache() { try { const vaultParam = window.__currentVaultId diff --git a/shims/path.js b/shims/path.js index 3774a5e..776c32d 100644 --- a/shims/path.js +++ b/shims/path.js @@ -1,4 +1,4 @@ -// Path shim - delegates to path-browserify (bundled via esbuild alias) +// Path shim. delegates to path-browserify (bundled via esbuild alias) // Configured for posix mode since vault paths are normalized to forward slashes. import pathBrowserify from "path"; diff --git a/shims/process.js b/shims/process.js index e2d1e9c..6d27036 100644 --- a/shims/process.js +++ b/shims/process.js @@ -1,19 +1,16 @@ -// Shim for window.process -// Obsidian checks process.platform, process.versions.electron, etc. - export const processShim = { - platform: 'linux', + platform: "linux", versions: { - electron: '28.0.0', - node: '18.18.0', - chrome: '120.0.0.0', + electron: "28.0.0", + node: "18.18.0", + chrome: "120.0.0.0", }, env: {}, - cwd: () => '/', + cwd: () => "/", nextTick: (fn, ...args) => setTimeout(() => fn(...args), 0), argv: [], - type: 'renderer', - resourcesPath: '/', + type: "renderer", + resourcesPath: "/", stdout: { write: (s) => console.log(s) }, stderr: { write: (s) => console.error(s) }, on: () => {}, diff --git a/shims/ui/vault-manager.js b/shims/ui/vault-manager.js index 721f497..132e697 100644 --- a/shims/ui/vault-manager.js +++ b/shims/ui/vault-manager.js @@ -1,4 +1,4 @@ -// Custom vault manager modal - vanilla JS (will migrate to Svelte later) +// Custom vault manager modal. will migrate to Svelte later // Shows list of vaults, create new, delete, switch. export function showVaultManager() { @@ -74,11 +74,7 @@ export function showVaultManager() { "color:var(--text-muted);border-radius:4px;padding:2px 8px;font-size:12px;cursor:pointer;"; del.addEventListener("click", (e) => { e.stopPropagation(); - if ( - !confirm( - 'Delete vault "' + v.name + '"? This removes all files.', - ) - ) + if (!confirm('Delete vault "' + v.name + '"? This removes all files.')) return; const xhr2 = new XMLHttpRequest(); xhr2.open( diff --git a/shims/url.js b/shims/url.js index 9bc0d34..272e307 100644 --- a/shims/url.js +++ b/shims/url.js @@ -1,22 +1,19 @@ -// URL shim -// Obsidian uses: pathToFileURL, fileURLToPath, URL, URLSearchParams - export const urlShim = { URL: globalThis.URL, URLSearchParams: globalThis.URLSearchParams, pathToFileURL(p) { // Return an object with .href matching Node's url.pathToFileURL behavior - const encoded = encodeURI(p.replace(/\\/g, '/')); - const href = 'file:///' + encoded.replace(/^\/+/, ''); + const encoded = encodeURI(p.replace(/\\/g, "/")); + const href = "file:///" + encoded.replace(/^\/+/, ""); return { href, toString: () => href }; }, fileURLToPath(url) { - let str = typeof url === 'string' ? url : url.href || url.toString(); - if (str.startsWith('file:///')) { + let str = typeof url === "string" ? url : url.href || url.toString(); + if (str.startsWith("file:///")) { str = str.slice(8); - } else if (str.startsWith('file://')) { + } else if (str.startsWith("file://")) { str = str.slice(7); } return decodeURI(str);