474 lines
22 KiB
JavaScript
474 lines
22 KiB
JavaScript
"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' };
|
|
const binPath = electron_1.app.isPackaged
|
|
? path.join(process.resourcesPath, 'bin', 'displayplacer')
|
|
: path.join(__dirname, '../../resources/bin/displayplacer');
|
|
// 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:<primary_id>.
|
|
const mirror_args = display_strings.map((s, i) => {
|
|
if (i === 0)
|
|
return `"${s}"`;
|
|
const without_existing_mirror = s.replace(/\s*mirror:\S+/g, '').trim();
|
|
return `"${without_existing_mirror} mirror:${primary_id}"`;
|
|
}).join(' ');
|
|
return await runExec(`"${binPath}" ${mirror_args}`);
|
|
}
|
|
if (mode === 'extend') {
|
|
const any_mirrored = display_strings.some(s => /\bmirror:\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:\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
|