From e70fe5845958bd061aac5763238f019004df7a11 Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Sat, 7 Mar 2026 14:38:51 +0100 Subject: [PATCH] implement shims --- shims/electron/remote/app.js | 47 +++++ shims/electron/remote/clipboard.js | 46 +++++ shims/electron/remote/dialog.js | 46 +++++ shims/electron/remote/index.js | 39 ++++ shims/electron/remote/menu.js | 59 ++++++ shims/electron/remote/native-image.js | 23 +++ shims/electron/remote/notification.js | 34 ++++ shims/electron/remote/screen.js | 30 +++ shims/electron/remote/session.js | 21 ++ shims/electron/remote/shell.js | 20 ++ shims/electron/remote/system-preferences.js | 24 +++ shims/electron/remote/theme.js | 60 ++++++ shims/electron/remote/window.js | 215 ++++++++++++++++++++ 13 files changed, 664 insertions(+) create mode 100644 shims/electron/remote/app.js create mode 100644 shims/electron/remote/clipboard.js create mode 100644 shims/electron/remote/dialog.js create mode 100644 shims/electron/remote/index.js create mode 100644 shims/electron/remote/menu.js create mode 100644 shims/electron/remote/native-image.js create mode 100644 shims/electron/remote/notification.js create mode 100644 shims/electron/remote/screen.js create mode 100644 shims/electron/remote/session.js create mode 100644 shims/electron/remote/shell.js create mode 100644 shims/electron/remote/system-preferences.js create mode 100644 shims/electron/remote/theme.js create mode 100644 shims/electron/remote/window.js diff --git a/shims/electron/remote/app.js b/shims/electron/remote/app.js new file mode 100644 index 0000000..65dcfd6 --- /dev/null +++ b/shims/electron/remote/app.js @@ -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() {}, +}; diff --git a/shims/electron/remote/clipboard.js b/shims/electron/remote/clipboard.js new file mode 100644 index 0000000..8e604f0 --- /dev/null +++ b/shims/electron/remote/clipboard.js @@ -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(() => {}); + }, +}; diff --git a/shims/electron/remote/dialog.js b/shims/electron/remote/dialog.js new file mode 100644 index 0000000..d478246 --- /dev/null +++ b/shims/electron/remote/dialog.js @@ -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); + }, +}; diff --git a/shims/electron/remote/index.js b/shims/electron/remote/index.js new file mode 100644 index 0000000..0e8ad03 --- /dev/null +++ b/shims/electron/remote/index.js @@ -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(); + }, +}; diff --git a/shims/electron/remote/menu.js b/shims/electron/remote/menu.js new file mode 100644 index 0000000..5223cc1 --- /dev/null +++ b/shims/electron/remote/menu.js @@ -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 || ''; + } +} diff --git a/shims/electron/remote/native-image.js b/shims/electron/remote/native-image.js new file mode 100644 index 0000000..206853c --- /dev/null +++ b/shims/electron/remote/native-image.js @@ -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)); + }, +}; diff --git a/shims/electron/remote/notification.js b/shims/electron/remote/notification.js new file mode 100644 index 0000000..864a594 --- /dev/null +++ b/shims/electron/remote/notification.js @@ -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; + } +} diff --git a/shims/electron/remote/screen.js b/shims/electron/remote/screen.js new file mode 100644 index 0000000..323e9f2 --- /dev/null +++ b/shims/electron/remote/screen.js @@ -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() {}, +}; diff --git a/shims/electron/remote/session.js b/shims/electron/remote/session.js new file mode 100644 index 0000000..f621a82 --- /dev/null +++ b/shims/electron/remote/session.js @@ -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() {}, + }, +}; diff --git a/shims/electron/remote/shell.js b/shims/electron/remote/shell.js new file mode 100644 index 0000000..c3f0d95 --- /dev/null +++ b/shims/electron/remote/shell.js @@ -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); + }, +}; diff --git a/shims/electron/remote/system-preferences.js b/shims/electron/remote/system-preferences.js new file mode 100644 index 0000000..23f3615 --- /dev/null +++ b/shims/electron/remote/system-preferences.js @@ -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() {}, +}; diff --git a/shims/electron/remote/theme.js b/shims/electron/remote/theme.js new file mode 100644 index 0000000..179c766 --- /dev/null +++ b/shims/electron/remote/theme.js @@ -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; + }, +}; diff --git a/shims/electron/remote/window.js b/shims/electron/remote/window.js new file mode 100644 index 0000000..818b129 --- /dev/null +++ b/shims/electron/remote/window.js @@ -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, +};