minor refactor, cleanup

This commit is contained in:
Nystik
2026-03-11 22:08:30 +01:00
parent 2b9ebf1fbd
commit 9789be6d70
38 changed files with 259 additions and 379 deletions

43
ARCHITECTURE.md Normal file
View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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");

View File

@@ -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}`,

View File

@@ -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;

View File

@@ -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=<id> - returns info for a specific vault
// GET /api/vault/info?vault=<id> - 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=<id> - remove a vault from disk
// DELETE /api/vault/remove?vault=<id> - remove a vault from disk
router.delete("/remove", async (req, res) => {
const vaultId = req.query.vault;
const vaultPath = config.getVaultPath(vaultId);

View File

@@ -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;

View File

@@ -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");
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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"));
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -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(() => {});
},
};

View File

@@ -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);
},
};

View File

@@ -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";

View File

@@ -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 || "";
}
}

View File

@@ -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: () => "",
};
},

View File

@@ -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;
}
}

View File

@@ -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,
},
};
},

View File

@@ -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() {},

View File

@@ -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);
},
};

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -1,6 +1,3 @@
// Shim for electron.webFrame
// Obsidian uses: getZoomLevel(), setZoomLevel()
let currentZoom = 0;
export const webFrame = {

View File

@@ -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);

View File

@@ -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);
},

View File

@@ -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(

View File

@@ -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++) {

View File

@@ -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<listener>
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);
}
}
}

View File

@@ -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

View File

@@ -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";

View File

@@ -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: () => {},

View File

@@ -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(

View File

@@ -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);