UNPKG

osa-bridge

Version:

Cross-platform Apple Events bridge for Node & Electron applications

193 lines (192 loc) 6.47 kB
import os from "os"; import bindings from "bindings"; /** Internal registry */ const _registry = new Map(); // Platform detection and native module loading let native = null; let isSupported = false; let initializationError = null; /** * Safely load the native module with proper error handling */ async function loadNativeModule() { try { if (os.platform() !== "darwin") { initializationError = `Apple Events not supported on ${os.platform()}`; return; } // Use bindings to load the native module native = bindings("osa_bridge"); if (native && typeof native.addHandler === "function") { isSupported = true; // Mark dispatch as ready and set it up immediately native._dispatchReady = true; if (typeof native.setDispatch === "function") { native.setDispatch(_dispatch); } } else { throw new Error("Native module loaded but required functions not available"); } } catch (error) { initializationError = `Failed to load native module: ${error instanceof Error ? error.message : String(error)}`; // Don't console.warn here - let the application decide how to handle this } } // Initialize the module loadNativeModule().catch(() => { // Error already captured in initializationError }); /** Register a handler for (suite,event). Wild-cards allowed. */ export function on(suite, event, handler) { if (!isSupported) { // Store handlers even on unsupported platforms for future compatibility _registry.set(`${suite}${event}`, handler); return; } try { native.addHandler(suite, event); // install native hook if not present _registry.set(`${suite}${event}`, handler); } catch (error) { throw new Error(`Failed to register handler for ${suite}.${event}: ${error instanceof Error ? error.message : String(error)}`); } } /** Check if Apple Events are supported on this platform */ export function isAppleEventsSupported() { return isSupported; } /** Get current platform information */ export function getPlatformInfo() { return { platform: os.platform(), supported: isSupported, ...(initializationError && { error: initializationError }), architecture: os.arch(), nodeVersion: process.version, }; } /** Get all registered handlers (useful for debugging) */ export function getRegisteredHandlers() { return Array.from(_registry.keys()); } /** Remove a handler */ export function off(suite, event) { const key = `${suite}${event}`; return _registry.delete(key); } /** Remove all handlers */ export function removeAllHandlers() { _registry.clear(); } /** Called from native when an Apple event arrives */ const _dispatch = async (ae, done) => { const key = `${ae.suite}${ae.event}`; const h = _registry.get(key) || _registry.get(`${ae.suite}****`) || _registry.get(`****${ae.event}`); if (!h) { return done(`No handler registered for ${ae.suite}.${ae.event}`); } try { const result = await h(ae); done(null, result); } catch (e) { done(e instanceof Error ? e.message : String(e)); } }; // For Electron compatibility - ensure module works in both main and renderer processes if (typeof window !== "undefined" && window.process && window.process.type === "renderer") { console.warn("osa-bridge: Running in Electron renderer process. Apple Events only work in the main process."); } /** * Helper function to get a human-readable representation of an Apple Event * Useful for debugging and logging */ export function formatAEEvent(evt) { const parts = [`${evt.suite}.${evt.event}`]; if (evt.targetApp) { parts.push(`target: ${evt.targetApp}`); } if (evt.sourceApp) { parts.push(`source: ${evt.sourceApp}`); } if (evt.transactionID !== undefined) { parts.push(`txn: ${evt.transactionID}`); } const paramCount = Object.keys(evt.params).length; if (paramCount > 0) { parts.push(`params: ${paramCount}`); } return parts.join(", "); } /** * Helper function to extract commonly used parameter types * from Apple Event parameters */ export function extractCommonParams(params) { return { directObject: params["----"], // Direct object parameter subject: params["subj"], // Subject parameter result: params["----"], // Result parameter (same as direct object) data: params["data"], // Data parameter text: params["ctxt"], // Text parameter file: params["file"], // File parameter url: params["url "], // URL parameter (note the space) }; } /** * Type guard to check if a parameter is an object specifier */ export function isObjectSpecifier(param) { return (typeof param === "object" && param !== null && !Array.isArray(param) && "type" in param && param.type === "objectSpecifier"); } /** * Extract the human-readable representation from an object specifier * Falls back to building it from the components if not available */ export function getObjectSpecifierDescription(param) { if (!isObjectSpecifier(param)) { return String(param); } // Use the native-generated human-readable string if available if (param.humanReadable) { return param.humanReadable; } // Fallback: build description from components let description = `${param.class} ${param.keyData}`; if (param.container && isObjectSpecifier(param.container)) { description += ` of ${getObjectSpecifierDescription(param.container)}`; } return description; } /** * Extract all object specifiers from event parameters recursively */ export function extractObjectSpecifiers(params) { const specifiers = []; function findSpecifiers(param) { if (isObjectSpecifier(param)) { specifiers.push(param); if (param.container) { findSpecifiers(param.container); } } else if (Array.isArray(param)) { param.forEach(findSpecifiers); } else if (typeof param === "object" && param !== null) { Object.values(param).forEach(findSpecifiers); } } Object.values(params).forEach(findSpecifiers); return specifiers; }