implement shims

This commit is contained in:
Nystik
2026-03-07 14:38:51 +01:00
parent 8b43493d87
commit e70fe58459
13 changed files with 664 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
// 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',
};
return paths[name] || '/';
},
getVersion() {
return '1.8.9';
},
getName() {
return 'Obsidian';
},
getLocale() {
return navigator.language || 'en-US';
},
isPackaged: true,
quit() {
console.log('[shim:app] quit (stub)');
},
relaunch() {
window.location.reload();
},
whenReady() {
return Promise.resolve();
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,46 @@
// 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 '';
},
writeText(text) {
navigator.clipboard.writeText(text).catch((e) => {
console.warn('[shim:clipboard] writeText failed:', e);
});
},
readHTML() {
return '';
},
writeHTML(html) {
// TODO: use clipboard API with text/html mime type
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)');
},
has(format) {
return false;
},
read(format) {
return '';
},
clear() {
navigator.clipboard.writeText('').catch(() => {});
},
};

View File

@@ -0,0 +1,46 @@
// 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);
return { canceled: true, filePaths: [] };
},
async showSaveDialog(browserWindow, options) {
// TODO: implement custom modal UI
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) {
options = browserWindow;
}
console.log('[shim:dialog] showMessageBox:', options);
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 : ''));
return { response: 0, checkboxChecked: false };
}
const result = confirm(message + (detail ? '\n\n' + detail : '') + '\n\n[OK] = "' + buttons[0] + '", [Cancel] = "' + buttons[1] + '"');
return {
response: result ? 0 : 1,
checkboxChecked: false,
};
},
showErrorBox(title, content) {
console.error('[shim:dialog] Error:', title, content);
alert(title + '\n\n' + content);
},
};

View File

@@ -0,0 +1,39 @@
// @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';
import { menuShim, menuItemShim } from './menu.js';
import { appShim } from './app.js';
import { windowShim, webContentsShim } from './window.js';
import { themeShim } from './theme.js';
import { sessionShim } from './session.js';
import { systemPreferencesShim } from './system-preferences.js';
import { screenShim } from './screen.js';
import { nativeImageShim } from './native-image.js';
import { notificationShim } from './notification.js';
export const remoteShim = {
clipboard: clipboardShim,
shell: shellShim,
dialog: dialogShim,
Menu: menuShim,
MenuItem: menuItemShim,
app: appShim,
BrowserWindow: windowShim,
nativeTheme: themeShim,
session: sessionShim,
systemPreferences: systemPreferencesShim,
screen: screenShim,
nativeImage: nativeImageShim,
Notification: notificationShim,
getCurrentWindow() {
return windowShim._current();
},
getCurrentWebContents() {
return webContentsShim._current();
},
};

View File

@@ -0,0 +1,59 @@
// Shim for remote.Menu and remote.MenuItem
// Obsidian uses: Menu.buildFromTemplate, Menu.popup, Menu.setApplicationMenu
export class menuShim {
constructor() {
this.items = [];
}
static buildFromTemplate(template) {
const menu = new menuShim();
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)');
}
static getApplicationMenu() {
return null;
}
popup(options) {
// TODO: implement custom HTML context menu rendered at mouse position
console.log('[shim:Menu] popup (stub)', options);
}
append(menuItem) {
this.items.push(menuItem);
}
insert(pos, menuItem) {
this.items.splice(pos, 0, menuItem);
}
closePopup() {
// TODO: hide custom context menu
}
}
export class menuItemShim {
constructor(options = {}) {
this.label = options.label || '';
this.type = options.type || 'normal';
this.click = options.click || null;
this.role = options.role || null;
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 : []
)
: null;
this.id = options.id || '';
}
}

View File

@@ -0,0 +1,23 @@
// Shim for remote.nativeImage
// Minimal stub - Obsidian's renderer-side usage is limited
export const nativeImageShim = {
createFromBuffer(buffer) {
return {
isEmpty: () => !buffer || buffer.length === 0,
getSize: () => ({ width: 0, height: 0 }),
toPNG: () => buffer || new Uint8Array(0),
toJPEG: (quality) => buffer || new Uint8Array(0),
toDataURL: () => '',
};
},
createFromPath(filePath) {
// TODO: could fetch from server and create image
return nativeImageShim.createFromBuffer(new Uint8Array(0));
},
createEmpty() {
return nativeImageShim.createFromBuffer(new Uint8Array(0));
},
};

View File

@@ -0,0 +1,34 @@
// Shim for remote.Notification
// Maps to browser Notification API
export class notificationShim {
constructor(options = {}) {
this.title = options.title || '';
this.body = options.body || '';
this.silent = options.silent || false;
this._handlers = {};
}
show() {
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') {
Notification.requestPermission().then((perm) => {
if (perm === 'granted') {
new Notification(this.title, { body: this.body, silent: this.silent });
}
});
}
}
close() {}
on(event, handler) {
this._handlers[event] = handler;
return this;
}
static isSupported() {
return 'Notification' in window;
}
}

View File

@@ -0,0 +1,30 @@
// 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 },
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 },
};
},
getAllDisplays() {
return [screenShim.getPrimaryDisplay()];
},
getDisplayNearestPoint(point) {
return screenShim.getPrimaryDisplay();
},
getCursorScreenPoint() {
return { x: 0, y: 0 };
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,21 @@
// Shim for remote.session
// Mostly no-op; Obsidian's use is minimal
export const sessionShim = {
defaultSession: {
clearCache() {
return Promise.resolve();
},
clearStorageData() {
return Promise.resolve();
},
setSpellCheckerLanguages(langs) {},
getSpellCheckerLanguages() { return []; },
on() {},
once() {},
removeListener() {},
},
};

View File

@@ -0,0 +1,20 @@
// Shim for remote.shell
// Obsidian uses: openExternal, openPath, showItemInFolder
export const shellShim = {
openExternal(url) {
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('');
},
showItemInFolder(filePath) {
// No OS file manager in browser context
console.log('[shim:shell] showItemInFolder (stub):', filePath);
},
};

View File

@@ -0,0 +1,24 @@
// Shim for remote.systemPreferences
// No-op with safe defaults
export const systemPreferencesShim = {
getAccentColor() {
return '0078d4'; // Default Windows accent blue
},
isAeroGlassEnabled() {
return false;
},
getMediaAccessStatus(mediaType) {
return 'granted';
},
askForMediaAccess(mediaType) {
return Promise.resolve(true);
},
on() {},
once() {},
removeListener() {},
};

View File

@@ -0,0 +1,60 @@
// Shim for remote.nativeTheme
// Obsidian uses: shouldUseDarkColors, on('updated', cb)
const listeners = [];
const darkQuery = typeof window !== 'undefined'
? window.matchMedia('(prefers-color-scheme: dark)')
: null;
if (darkQuery?.addEventListener) {
darkQuery.addEventListener('change', () => {
for (const fn of listeners) {
fn();
}
});
}
export const themeShim = {
get shouldUseDarkColors() {
return darkQuery ? darkQuery.matches : true;
},
get themeSource() {
return 'system';
},
set themeSource(val) {
// No-op in browser; theme is controlled by OS
},
on(event, callback) {
if (event === 'updated') {
listeners.push(callback);
}
return themeShim;
},
once(event, callback) {
if (event === 'updated') {
const wrapped = () => {
const idx = listeners.indexOf(wrapped);
if (idx >= 0) listeners.splice(idx, 1);
callback();
};
listeners.push(wrapped);
}
return themeShim;
},
removeListener(event, callback) {
const idx = listeners.indexOf(callback);
if (idx >= 0) listeners.splice(idx, 1);
return themeShim;
},
removeAllListeners() {
listeners.length = 0;
return themeShim;
},
};

View File

@@ -0,0 +1,215 @@
// 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,
isMinimized: false,
isFullScreen: false,
isAlwaysOnTop: false,
bounds: { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight },
focusTime: Date.now(),
};
const currentWindow = {
isMaximized: () => currentWindowState.isMaximized,
isMinimized: () => currentWindowState.isMinimized,
isFullScreen: () => !!document.fullscreenElement,
isAlwaysOnTop: () => currentWindowState.isAlwaysOnTop,
isFocused: () => document.hasFocus(),
isVisible: () => true,
isDestroyed: () => false,
minimize() {
console.log("[shim:window] minimize (stub)");
},
maximize() {
currentWindowState.isMaximized = true;
},
unmaximize() {
currentWindowState.isMaximized = false;
},
restore() {
currentWindowState.isMinimized = false;
},
close() {
console.log("[shim:window] close (stub)");
},
focus() {
window.focus();
},
show() {},
hide() {},
setTitle(title) {
currentWindowState.title = title;
document.title = title;
},
getTitle() {
return currentWindowState.title;
},
setAlwaysOnTop(flag) {
currentWindowState.isAlwaysOnTop = flag;
},
setFullScreen(flag) {
if (flag) {
document.documentElement.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
},
getBounds() {
return {
x: window.screenX,
y: window.screenY,
width: window.innerWidth,
height: window.innerHeight,
};
},
setBounds(bounds) {
// Cannot resize browser window from JS
console.log("[shim:window] setBounds (stub):", bounds);
},
setSize(width, height) {},
setPosition(x, y) {},
center() {},
setTrafficLightPosition() {},
setWindowButtonPosition() {},
get webContents() {
return webContentsShim._current();
},
get menuBarVisible() {
return false;
},
set menuBarVisible(v) {},
get loaded() {
return true;
},
set loaded(v) {},
get focusTime() {
return currentWindowState.focusTime;
},
set focusTime(v) {
currentWindowState.focusTime = v;
},
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);
return currentWindow;
},
once(event, handler) {
if (event === "focus")
window.addEventListener("focus", handler, { once: true });
return currentWindow;
},
removeListener() {
return currentWindow;
},
removeAllListeners() {
return currentWindow;
},
};
const currentWebContents = {
_zoomLevel: 0,
get zoomLevel() {
return this._zoomLevel;
},
set zoomLevel(v) {
this._zoomLevel = v;
},
executeJavaScript(code) {
try {
return Promise.resolve(eval(code));
} catch (e) {
return Promise.reject(e);
}
},
getZoomFactor() {
return Math.pow(1.2, this._zoomLevel);
},
getZoomLevel() {
return this._zoomLevel;
},
setZoomLevel(v) {
this._zoomLevel = v;
},
isDevToolsOpened() {
return false;
},
openDevTools() {},
setWindowOpenHandler(handler) {
this._windowOpenHandler = handler;
},
capturePage(rect) {
// TODO: could use html2canvas
console.log("[shim:webContents] capturePage (stub)");
return Promise.resolve({
toPNG: () => new Uint8Array(0),
toJPEG: () => new Uint8Array(0),
});
},
undo() {},
redo() {},
pasteAndMatchStyle() {},
setSpellCheckerLanguages(langs) {},
on(event, handler) {
return currentWebContents;
},
once(event, handler) {
return currentWebContents;
},
removeListener() {
return currentWebContents;
},
get isSecured() {
return true;
},
set isSecured(v) {},
};
export const windowShim = {
_current: () => currentWindow,
getFocusedWindow() {
return currentWindow;
},
};
export const webContentsShim = {
_current: () => currentWebContents,
};