"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerSystemHandlers = registerSystemHandlers; const electron_1 = require("electron"); const os = __importStar(require("os")); const path = __importStar(require("path")); const fs = __importStar(require("fs")); const child_process_1 = require("child_process"); const axios_1 = __importDefault(require("axios")); const file_utils_1 = require("./file_utils"); // Helper to execute shell commands const runExec = (cmd) => { return new Promise((resolve) => { (0, child_process_1.exec)(cmd, (error, stdout, stderr) => { resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : undefined }); }); }); }; let recordingProcess = null; function registerSystemHandlers() { // 1. Window Control electron_1.ipcMain.handle('native:window-control', async (event, { action, value }) => { const win = electron_1.BrowserWindow.fromWebContents(event.sender); if (!win) return { success: false, error: 'No window found' }; switch (action) { case 'maximize': win.maximize(); break; case 'unmaximize': win.unmaximize(); break; case 'minimize': win.minimize(); break; case 'restore': win.restore(); break; case 'close': win.close(); break; case 'devtools': if (value) win.webContents.openDevTools(); else win.webContents.closeDevTools(); break; case 'kiosk': win.setKiosk(!!value); break; case 'fullscreen': win.setFullScreen(!!value); break; case 'reload': win.reload(); break; default: return { success: false, error: `Unknown action: ${action}` }; } return { success: true }; }); // 2. Set Wallpaper // Supports local path OR URL download. URL images are saved to a stable cache dir // so macOS can reference them persistently after reboot. // display: 'all' (default) | 'primary' (built-in) | 'external' (projector/second screen) // url_external: optional second URL for the external display only. electron_1.ipcMain.handle('native:set-wallpaper', async (event, { path: imagePath, url, url_external, display = 'all', api_key, account_id }) => { // Cache dir: ~/Library/Caches/OSIT/wallpaper on macOS, ~/.cache/osit/wallpaper on Linux. // Using a stable path means macOS keeps the reference across reboots. const wallpaper_cache_dir = os.platform() === 'darwin' ? path.join(os.homedir(), 'Library', 'Caches', 'OSIT', 'wallpaper') : path.join(os.homedir(), '.cache', 'osit', 'wallpaper'); async function download_wallpaper_image(image_url, basename) { if (!fs.existsSync(wallpaper_cache_dir)) { fs.mkdirSync(wallpaper_cache_dir, { recursive: true }); } // Infer extension from URL path, fall back to .jpg let ext = '.jpg'; try { const url_path = new URL(image_url).pathname; const inferred = path.extname(url_path).toLowerCase(); if (['.jpg', '.jpeg', '.png', '.webp'].includes(inferred)) { ext = inferred === '.jpeg' ? '.jpg' : inferred; } } catch { } const dest_path = path.join(wallpaper_cache_dir, basename + ext); const headers = {}; if (api_key) headers['x-aether-api-key'] = api_key; if (account_id) headers['x-account-id'] = account_id; try { const response = await (0, axios_1.default)({ method: 'get', url: image_url, responseType: 'stream', headers }); const content_type = (response.headers['content-type'] ?? ''); if (!content_type.startsWith('image/')) { response.data.destroy(); return { success: false, error: `URL did not return an image (Content-Type: ${content_type})` }; } const writer = fs.createWriteStream(dest_path); response.data.pipe(writer); await new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); const file_size = fs.statSync(dest_path).size; if (file_size === 0) { try { fs.unlinkSync(dest_path); } catch { } return { success: false, error: 'Wallpaper download incomplete (0 bytes)' }; } return { success: true, path: dest_path }; } catch (e) { return { success: false, error: `Wallpaper download failed: ${e.message}` }; } } // HARDENED: write AppleScript to a temp file, same pattern as native:run-osascript. // The old osascript -e approach breaks on paths with spaces or special characters. async function apply_mac_wallpaper(img_path, display_target) { const escaped = img_path.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); let script; if (display_target === 'primary') { script = `tell application "System Events"\n\ttell desktop 1\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`; } else if (display_target === 'external') { script = `tell application "System Events"\n\ttell desktop 2\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`; } else { script = `tell application "System Events"\n\ttell every desktop\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`; } const script_path = path.join(os.tmpdir(), `ae_wallpaper_${Date.now()}.scpt`); fs.writeFileSync(script_path, script, 'utf-8'); try { return await runExec(`osascript "${script_path}"`); } finally { try { fs.unlinkSync(script_path); } catch { } } } if (os.platform() === 'darwin') { // Resolve primary image path let primary_path = null; if (imagePath) { const clean = (0, file_utils_1.expandPath)(imagePath); if (!fs.existsSync(clean)) return { success: false, error: 'Image file not found' }; primary_path = clean; } else if (url) { const result = await download_wallpaper_image(url, 'wallpaper_primary'); if (!result.success || !result.path) return { success: false, error: result.error }; primary_path = result.path; } if (!primary_path && url_external && display === 'external') { const ext_result = await download_wallpaper_image(url_external, 'wallpaper_external'); if (!ext_result.success || !ext_result.path) return { success: false, error: ext_result.error }; return await apply_mac_wallpaper(ext_result.path, 'external'); } if (!primary_path) return { success: false, error: 'No image source provided' }; if (url_external) { // Different images for each display: set primary display first, then external const primary_result = await apply_mac_wallpaper(primary_path, 'primary'); if (!primary_result.success) return primary_result; const ext_result = await download_wallpaper_image(url_external, 'wallpaper_external'); if (!ext_result.success || !ext_result.path) return { success: false, error: ext_result.error }; return await apply_mac_wallpaper(ext_result.path, 'external'); } else { return await apply_mac_wallpaper(primary_path, display); } } if (os.platform() === 'linux') { // Dev test mode: never touch the desktop on Linux. Running gsettings during // development would reset the dev workstation monitors on every test cycle. // Return what would have run so the Svelte side can show a debug popup. const would_run = []; const cache_dir = wallpaper_cache_dir; if (!imagePath && !url && !url_external) { return { success: false, error: 'No image source provided' }; } if (imagePath) { const clean = (0, file_utils_1.expandPath)(imagePath); if (!fs.existsSync(clean)) return { success: false, error: 'Image file not found' }; } if (url) would_run.push(`download: ${url}\n → ${path.join(cache_dir, 'wallpaper_primary.jpg')}`); if (url_external) would_run.push(`download: ${url_external}\n → ${path.join(cache_dir, 'wallpaper_external.jpg')}`); if (imagePath) { would_run.push(`gsettings set org.gnome.desktop.background picture-uri "file://${(0, file_utils_1.expandPath)(imagePath)}"`); } else if (url) { would_run.push(`gsettings set org.gnome.desktop.background picture-uri "file://${path.join(cache_dir, 'wallpaper_primary.jpg')}"`); } if (url_external) { would_run.push(`(external display: gsettings has no per-display wallpaper support)`); } return { success: true, linux_test_mode: true, platform: 'linux', display, url: url ?? null, url_external: url_external ?? null, would_run }; } return { success: false, error: 'Platform not supported' }; }); // 3. Power Control electron_1.ipcMain.handle('native:power-control', async (event, { action }) => { let cmd = ''; const isMac = os.platform() === 'darwin'; const isLinux = os.platform() === 'linux'; if (action === 'shutdown') { if (isMac) cmd = 'shutdown -h now'; // Requires sudo/admin usually if (isLinux) cmd = 'shutdown -h now'; } else if (action === 'reboot') { if (isMac) cmd = 'shutdown -r now'; if (isLinux) cmd = 'reboot'; } else if (action === 'sleep') { if (isMac) cmd = 'pmset sleepnow'; if (isLinux) cmd = 'systemctl suspend'; } if (!cmd) return { success: false, error: 'Action not supported' }; // NOTE: These commands often require root. // For a kiosk, you might configure sudoers to allow this specific command without password. return await runExec(cmd); }); // 4. Open External (Browser) electron_1.ipcMain.handle('native:open-external', async (event, { url, app: appName }) => { if (appName === 'chrome') { if (os.platform() === 'darwin') { return await runExec(`open -a "Google Chrome" "${url}"`); } else if (os.platform() === 'linux') { return await runExec(`google-chrome "${url}"`); } } else if (appName === 'firefox') { if (os.platform() === 'darwin') { return await runExec(`open -a "Firefox" "${url}"`); } else if (os.platform() === 'linux') { return await runExec(`firefox "${url}"`); } } // Default system handler await electron_1.shell.openExternal(url); return { success: true }; }); // 5. Manage Recording (Aperture Wrapper) electron_1.ipcMain.handle('native:manage-recording', async (event, { action, options }) => { if (os.platform() !== 'darwin') return { success: false, error: 'Recording only supported on macOS' }; // Path to bundled aperture binary // In dev: ./resources/bin/aperture // In prod: process.resourcesPath/bin/aperture const binPath = electron_1.app.isPackaged ? path.join(process.resourcesPath, 'bin', 'aperture') : path.join(__dirname, '../../resources/bin/aperture'); // Adjust based on structure if (action === 'start') { if (recordingProcess) return { success: false, error: 'Recording already in progress' }; const { fps = 30, audioDeviceId, output } = options || {}; const cleanOutput = (0, file_utils_1.expandPath)(output || '~/tmp/recording.mp4'); const args = ['run', '--fps', fps, '--output', cleanOutput]; if (audioDeviceId) args.push('--audio-device-id', audioDeviceId); // Spawn process // Note: aperture is a CLI tool. We might need 'aperture' node package or the binary. // Assuming binary usage here. try { console.log(`Starting recording: ${binPath} ${args.join(' ')}`); recordingProcess = (0, child_process_1.spawn)(binPath, args); recordingProcess.on('error', (err) => { console.error('Recording error:', err); recordingProcess = null; }); recordingProcess.on('exit', (code) => { console.log(`Recording exited with code ${code}`); recordingProcess = null; }); return { success: true, pid: recordingProcess.pid }; } catch (e) { return { success: false, error: e.message }; } } else if (action === 'stop') { if (!recordingProcess) return { success: false, error: 'No recording in progress' }; recordingProcess.kill('SIGINT'); // Send interrupt to stop cleanly recordingProcess = null; return { success: true }; } else if (action === 'status') { return { success: true, isRecording: !!recordingProcess }; } return { success: false, error: 'Unknown action' }; }); // 6. Set Display Layout (Displayplacer) // Auto-detects connected displays via `displayplacer list` when no explicit configStr is given. // mirror: secondary display(s) mirror the primary. // extend: displays shown side-by-side; un-mirrors if currently mirrored. electron_1.ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => { if (os.platform() !== 'darwin') return { success: false, error: 'Display control only supported on macOS' }; // Try bundled binary first; fall back to common Homebrew/system locations. // Install on a dev/venue Mac via: brew install displayplacer const _bin_candidates = electron_1.app.isPackaged ? [path.join(process.resourcesPath, 'bin', 'displayplacer')] : [ path.join(__dirname, '../../resources/bin/displayplacer'), '/opt/homebrew/bin/displayplacer', // Apple Silicon Homebrew '/usr/local/bin/displayplacer', // Intel Homebrew ]; const binPath = _bin_candidates.find(p => fs.existsSync(p)) ?? _bin_candidates[0]; // Explicit config string always takes priority — allows manual override per device. if (configStr) { return await runExec(`"${binPath}" ${configStr}`); } // Auto-detect: `displayplacer list` emits a ready-to-run command line at the bottom. // We parse the quoted display strings from that line and modify them for the requested mode. const list_result = await runExec(`"${binPath}" list`); if (!list_result.success || !list_result.stdout) { return { success: false, error: `displayplacer list failed: ${list_result.error ?? 'no output'}` }; } // The command line looks like: displayplacer "id:xxx res:... origin:(0,0) ..." "id:yyy ..." const cmd_line = list_result.stdout.split('\n').find(l => l.trim().startsWith('displayplacer "')); if (!cmd_line) { return { success: false, error: 'Only one display connected or displayplacer list output unrecognised' }; } const display_strings = [...cmd_line.matchAll(/"([^"]+)"/g)].map(m => m[1]); if (display_strings.length < 2) { return { success: false, error: 'Only one display found; cannot change layout' }; } if (mode === 'mirror') { const primary_id_match = display_strings[0].match(/\bid:([^\s]+)/); if (!primary_id_match) { return { success: false, error: 'Could not parse primary display ID from displayplacer output' }; } const primary_id = primary_id_match[1]; // Primary display unchanged; secondary display(s) get mirror_of_display:. const mirror_args = display_strings.map((s, i) => { if (i === 0) return `"${s}"`; const without_existing_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim(); return `"${without_existing_mirror} mirror_of_display:${primary_id}"`; }).join(' '); return await runExec(`"${binPath}" ${mirror_args}`); } if (mode === 'extend') { const any_mirrored = display_strings.some(s => /\bmirror_of_display:\S+/.test(s)); if (!any_mirrored) { // Already extended — re-apply current layout to ensure it is active. return await runExec(`"${binPath}" ${display_strings.map(s => `"${s}"`).join(' ')}`); } // Remove mirror keys and compute side-by-side origins from each display's resolution. let x_offset = 0; const extend_args = display_strings.map((s) => { const without_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim(); const res_match = without_mirror.match(/\bres:(\d+)x\d+/); const width = res_match ? parseInt(res_match[1]) : 1920; const updated = without_mirror.replace(/\borigin:\([^)]+\)/, `origin:(${x_offset},0)`); x_offset += width; return `"${updated}"`; }).join(' '); return await runExec(`"${binPath}" ${extend_args}`); } return { success: false, error: `Unsupported display mode: ${mode}` }; }); // 7. Update App electron_1.ipcMain.handle('native:update-app', async (event, { source, url, path: localPath }) => { // 1. Determine Source File let updateFile = ''; const tempDir = os.tmpdir(); const destName = 'update_package.zip'; // Or .app, .AppImage const destPath = path.join(tempDir, destName); if (source === 'url' && url) { // Download try { const response = await (0, axios_1.default)({ method: 'get', url: url, responseType: 'stream' }); const writer = fs.createWriteStream(destPath); response.data.pipe(writer); await new Promise((resolve, reject) => { writer.on('finish', () => resolve(true)); writer.on('error', reject); }); updateFile = destPath; } catch (e) { return { success: false, error: `Download failed: ${e.message}` }; } } else if (source === 'file' && localPath) { const cleanLocal = (0, file_utils_1.expandPath)(localPath); if (fs.existsSync(cleanLocal)) { updateFile = cleanLocal; } else { return { success: false, error: 'Local update file not found' }; } } if (!updateFile) return { success: false, error: 'No update source provided' }; // 2. Install Logic (Stub) // Real implementation depends on OS and packaging format. // macOS: Mount DMG, copy .app to /Applications? Or Unzip .app? // Linux: chmod +x AppImage and move? console.log(`Ready to install update from: ${updateFile}`); // For now, just return success so the UI knows we "downloaded" it. return { success: true, message: 'Update downloaded/located. Installation logic requires packaging specifics.', downloadedPath: updateFile }; }); } //# sourceMappingURL=system_handlers.js.map