diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e99c98 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Aether Native Launcher (Electron) + +The Aether Native Launcher is a specialized Electron-based shell for the Aether Platform. It provides a secure bridge between the SvelteKit web UI and the local operating system, enabling features restricted by browser sandboxing. + +## 🚀 Overview + +This application serves as the "Native Mode" runtime for Aether podiums and devices. It handles: +- **Local File Orchestration:** Managed cache for presentation files (PPTx, Keynote, PDF). +- **Automation:** Specialized AppleScript handlers for PowerPoint and Keynote. +- **Hardware Telemetry:** Direct access to CPU, RAM, and Network interface data. +- **Remote Control:** Slide navigation and application control via WebSocket intents. + +## ⚙️ Configuration + +The application requires a `seed_config.json` file to identify the device and connect to the Aether API. + +### 1. Seed Configuration +Create/edit `resources/seed_config.json`: +```json +{ + "event_device_id": "YOUR_DEVICE_ID", + "aether_api_key": "YOUR_API_KEY", + "primary_api_base_url": "https://api.yourdomain.com", + "backup_api_base_url": null +} +``` + +### 2. Environment Setup +```bash +npm install +npm run build +npm start +``` + +## 🌉 The Native Bridge (`aetherNative`) + +The bridge is exposed to the renderer via `contextBridge`. It can be accessed in the web UI via `window.aetherNative`. + +### Core Methods + +| Method | Description | +| --- | --- | +| `list_tools()` | Returns a JSON manifest of all available native functions. | +| `launch_presentation({path, app})` | Launches a presentation with auto-focus and slideshow start. | +| `control_presentation({app, action})` | Sends `next`, `prev`, `start`, or `stop` to active decks. | +| `open_folder(path)` | Opens a local directory in the OS file explorer. | +| `get_device_info()` | Returns hardware metadata (RAM, IPs, Hostname). | + +### Example Usage (UI Relay) +```typescript +import * as native from '$lib/electron/electron_relay'; + +// Launch a file from local cache +await native.launch_presentation({ + path: '[tmp]/my_deck.pptx', + app: 'powerpoint' +}); + +// Navigate slides +await native.control_presentation({ + app: 'powerpoint', + action: 'next' +}); +``` + +## 🛠️ Development + +- **Preload:** Logic defined in `src/preload/index.ts`. +- **Handlers:** OS-level logic in `src/main/shell_handlers.ts` and `src/main/file_handlers.ts`. +- **Types:** Shared TypeScript interfaces in `src/shared/types.ts`. diff --git a/aether_app_native_electron.code-workspace b/aether_app_native_electron.code-workspace index 38f0c02..9d076f9 100644 --- a/aether_app_native_electron.code-workspace +++ b/aether_app_native_electron.code-workspace @@ -8,6 +8,8 @@ "git.autofetch": true, "markdownlint.config": { "MD007": false, + "MD030": false, + "MD004": false, "MD033": false } } diff --git a/src/main/file_handlers.ts b/src/main/file_handlers.ts index f8a1c45..2392e88 100644 --- a/src/main/file_handlers.ts +++ b/src/main/file_handlers.ts @@ -90,15 +90,37 @@ export function registerFileHandlers() { }); } - if (os.platform() === 'darwin' && ext === 'key') { - console.log(`Native: Launching Keynote via AppleScript for ${target}`); - const script = `tell application "Keynote" to open POSIX file "${target}"`; - return new Promise((resolve) => { - exec(`osascript -e "${script.replace(/"/g, '\"')}"`, (err) => { - if (err) resolve({ success: false, error: err.message }); - else resolve({ success: true }); + if (os.platform() === 'darwin') { + let script = ''; + if (ext === 'key') { + script = ` + tell application "Keynote" + activate + open (POSIX file "${target}") + delay 1 + start (front document) + end tell + `; + } else if (ext === 'pptx' || ext === 'ppt') { + script = ` + tell application "Microsoft PowerPoint" + activate + open (POSIX file "${target}") + delay 1 + run slide show of active presentation + end tell + `; + } + + if (script) { + console.log(`Native: Launching ${ext} via AppleScript for ${target}`); + return new Promise((resolve) => { + exec(`osascript -e "${script.replace(/"/g, '\\\\"')}"`, (err) => { + if (err) resolve({ success: false, error: err.message }); + else resolve({ success: true }); + }); }); - }); + } } } @@ -109,4 +131,4 @@ export function registerFileHandlers() { return { success: false, error: error.message }; } }); -} +} \ No newline at end of file diff --git a/src/main/shell_handlers.ts b/src/main/shell_handlers.ts index 6e981a9..67427f1 100644 --- a/src/main/shell_handlers.ts +++ b/src/main/shell_handlers.ts @@ -79,10 +79,30 @@ export function registerShellHandlers() { } if (os.platform() === 'darwin') { + let script = ''; if (appType === 'keynote') { - const script = `tell application "Keynote" to open POSIX file "${cleanedPath}"`; - const escapedScript = script.replace(/"/g, '\\"'); + script = ` + tell application "Keynote" + activate + open (POSIX file "${cleanedPath}") + delay 1 + start (front document) + end tell + `; + } else if (appType === 'powerpoint') { + script = ` + tell application "Microsoft PowerPoint" + activate + open (POSIX file "${cleanedPath}") + delay 1 + run slide show of active presentation + end tell + `; + } + + if (script) { return new Promise((resolve) => { + const escapedScript = script.replace(/"/g, '\\"'); exec(`osascript -e "${escapedScript}"`, (err, stdout, stderr) => { if (err) resolve({ success: false, error: err.message }); else resolve({ success: true }); @@ -94,4 +114,98 @@ export function registerShellHandlers() { const error = await shell.openPath(cleanedPath); return { success: !error, error }; }); + + ipcMain.handle('native:control-presentation', async (event, { app, action }) => { + if (os.platform() !== 'darwin') return { success: false, error: 'Presentation control is only available on macOS' }; + + let script = ''; + if (app === 'powerpoint') { + switch (action) { + case 'next': script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation'; break; + case 'prev': script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation'; break; + case 'start': script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation'; break; + case 'stop': script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation'; break; + } + } else if (app === 'keynote') { + switch (action) { + case 'next': script = 'tell application "Keynote" to show next'; break; + case 'prev': script = 'tell application "Keynote" to show previous'; break; + case 'start': script = 'tell application "Keynote" to start (front document)'; break; + case 'stop': script = 'tell application "Keynote" to stop'; break; + } + } + + if (!script) return { success: false, error: `Unsupported app or action: ${app}/${action}` }; + + return new Promise((resolve) => { + exec(`osascript -e "${script.replace(/"/g, '\\"')}"`, (error, stdout, stderr) => { + resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null }); + }); + }); + }); + + ipcMain.handle('native:list-tools', async () => { + return [ + { + name: 'open_folder', + description: 'Opens a directory in the OS file explorer (Finder/Files/Explorer).', + params: { path: 'string' } + }, + { + name: 'run_cmd', + description: 'Executes an asynchronous shell command with a timeout.', + params: { cmd: 'string', timeout: 'number (optional)' } + }, + { + name: 'run_cmd_sync', + description: 'Executes a synchronous shell command.', + params: { cmd: 'string' } + }, + { + name: 'run_osascript', + description: 'Executes a raw AppleScript string (macOS only).', + params: { script: 'string' } + }, + { + name: 'kill_processes', + description: 'Forcefully terminates processes by name.', + params: { process_name_li: 'string[]' } + }, + { + name: 'open_local_file_v2', + description: 'Opens a local file using the default OS handler.', + params: { filePath: 'string' } + }, + { + name: 'launch_presentation', + description: 'Phase 5: Specialized launcher for PowerPoint, Keynote, and LibreOffice with auto-focus.', + params: { path: 'string', app: 'default|powerpoint|keynote' } + }, + { + name: 'control_presentation', + description: 'Phase 5: Remote navigation for active slideshows.', + params: { app: 'powerpoint|keynote', action: 'next|prev|start|stop' } + }, + { + name: 'check_cache', + description: 'Checks if a file exists in the local organized cache.', + params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number' } + }, + { + name: 'download_to_cache', + description: 'Downloads a file from the API directly into the native cache.', + params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string' } + }, + { + name: 'launch_from_cache', + description: 'Atomic operation: Copies file from cache to temp with original name and launches via specialized handler.', + params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string' } + }, + { + name: 'get_device_info', + description: 'Returns hardware and OS metadata (CPUs, RAM, IP addresses, Hostname).', + params: {} + } + ]; + }); } diff --git a/src/preload/index.ts b/src/preload/index.ts index df95263..c7ea667 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -17,4 +17,6 @@ contextBridge.exposeInMainWorld('aetherNative', { download_to_cache: (args: any) => ipcRenderer.invoke('native:download-to-cache', args), launch_from_cache: (args: any) => ipcRenderer.invoke('native:launch-from-cache', args), launch_presentation: (args: any) => ipcRenderer.invoke('native:launch-presentation', args), + control_presentation: (args: any) => ipcRenderer.invoke('native:control-presentation', args), + list_tools: () => ipcRenderer.invoke('native:list-tools'), }); diff --git a/src/shared/types.ts b/src/shared/types.ts index 8e3b170..46ec0bd 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -7,22 +7,34 @@ export interface SeedConfig { } export interface AetherNativeBridge { - getSeedConfig: () => Promise; - getDeviceConfig: () => Promise; - getJWT: () => Promise; - log: (message: string) => void; + get_seed_config: () => Promise; + get_device_config: () => Promise; + get_jwt: () => Promise; + get_device_info: () => Promise; + // Shell Handlers - openFolder: (path: string) => Promise<{success: boolean, error?: string}>; - runCommand: (cmd: string) => Promise<{success: boolean, stdout: string, stderr: string, error?: string}>; - launchFile: (path: string) => Promise<{success: boolean, error?: string}>; + open_folder: (path: string) => Promise<{success: boolean, error?: string}>; + run_cmd: (args: {cmd: string, timeout?: number}) => Promise<{success: boolean, stdout: string, stderr: string, error?: string}>; + run_cmd_sync: (args: {cmd: string}) => Promise<{success: boolean, stdout: string, error?: string, stderr?: string}>; + run_osascript: (script: string) => Promise<{success: boolean, stdout: string, stderr: string, error?: string}>; + kill_processes: (args: {process_name_li: string[]}) => Promise<{success: boolean, results: any[]}>; + open_local_file_v2: (path: string) => Promise<{success: boolean, error?: string}>; + // File/Cache Handlers - checkCache: (args: {cacheRoot: string, hash: string}) => Promise; - downloadToCache: (args: {url: string, cacheRoot: string, hash: string, apiKey: string}) => Promise<{success: boolean, path?: string, error?: string}>; - launchFromCache: (args: {cacheRoot: string, hash: string, tempRoot: string, filename: string}) => Promise<{success: boolean, error?: string}>; + check_cache: (args: {cache_root: string, hash: string, hash_prefix_length?: number}) => Promise; + download_to_cache: (args: {url: string, cache_root: string, hash: string, api_key: string, account_id?: string, hash_prefix_length?: number}) => Promise<{success: boolean, error?: string}>; + launch_from_cache: (args: {cache_root: string, hash: string, temp_root: string, filename: string, hash_prefix_length?: number}) => Promise<{success: boolean, error?: string}>; + + // Specialized Presentation Handlers (Phase 5) + launch_presentation: (args: {path: string, app?: string}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>; + control_presentation: (args: {app: 'powerpoint' | 'keynote', action: 'next' | 'prev' | 'start' | 'stop'}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>; + + // Self-Documentation + list_tools: () => Promise>; } declare global { interface Window { aetherNative: AetherNativeBridge; } -} \ No newline at end of file +}