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

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