diff --git a/documentation/MODULE__AE_Events_PressMgmt_Launcher.md b/documentation/MODULE__AE_Events_PressMgmt_Launcher.md index d17abff9..d4b1d19f 100644 --- a/documentation/MODULE__AE_Events_PressMgmt_Launcher.md +++ b/documentation/MODULE__AE_Events_PressMgmt_Launcher.md @@ -37,7 +37,7 @@ platform is flexible enough to handle the full range. ### Object Hierarchy -``` +```text Event ├── Event File (walk-in/out, hold slides for the whole event) ├── Location (physical room — assigned to Sessions, not the other way around) @@ -314,11 +314,30 @@ The Electron app zero-configs itself: **Configurable launch behavior:** The file-open behavior is driven by a Launch Profile, not just a command string. Profiles are stored per file extension in `event_device.data_json.launch_profiles` (device-level config) or -`event.launcher.launch_profiles` (event-level fallback). A profile can choose the app, -display mode, open command, and post-open automation. The resolved native template uses -`{{path}}` as the file path placeholder; AppleScript or `shell:` prefixed commands are both -supported. No Electron rebuild is required to change how files open — edit config in Aether -and it applies immediately. See `PROJECT__AE_Events_Launcher_Native_integration.md` Section 8. +`event.launcher.launch_profiles` (event-level fallback). The built-in Svelte defaults are the +final fallback and are documented below. A profile can choose the app, display mode, open +command, and post-open automation. The resolved native template uses `{{path}}` as the file +path placeholder; AppleScript or `shell:` prefixed commands are both supported. No Electron +rebuild is required to change how files open — edit config in Aether and it applies +immediately. See `PROJECT__AE_Events_Launcher_Native_integration.md` Section 8. + +### Built-In Default Launch Profiles + +These are the initial built-in defaults shipped with the Launcher. They are the Svelte-side +fallbacks used when neither device config nor event config defines a profile for the file +extension. Each canonical profile can have multiple extension aliases. + +| Profile name | Extension aliases | Default app | Display mode | Notes | +|---|---|---|---|---| +| `powerpoint_mac_extend` | `pptx`, `ppt` | Microsoft PowerPoint for macOS | `extend` | Open in the presentation app and extend to an external display if one is present. | +| `keynote_mac_extend` | `key` | Keynote | `extend` | Keynote slideshow on the external display if available. | +| `libreoffice_mac_extend` | `odp` | LibreOffice for macOS | `extend` | LibreOffice Impress for OpenDocument presentations. | +| `acrobat_mac_mirror` | `pdf` | Adobe Acrobat for macOS | `mirror` | PDF handout / deck view uses mirrored display. | +| `vlc_mirror` | `mp4`, `mkv`, `mp3`, `m4v`, `m4a`, `webm`, `wav`, `aac`, `flac`, `mov`, `mpeg`, `avi`, `flv`, `ogg`, `ogv`, `wmv` | VLC for macOS | `mirror` | Media playback is mirrored so the room sees the same output as the operator. | +| `powerpoint_win_extend` | `pptxwin`, `pptwin` | PowerPoint for Windows (Parallels) | `extend` | Windows PowerPoint profile for Parallels-based rooms. | +| `libreoffice_win_extend` | `odpwin` | LibreOffice for Windows | `extend` | Windows LibreOffice profile for Parallels-based rooms. | +| `acrobat_win_mirror` | `pdfwin` | Adobe Acrobat for Windows (Parallels) | `mirror` | Windows PDF profile for mirrored display rooms. | +| `url_web` | `url` | Browser / Event File web presentation | `extend` | Web-based presentations are handled as Event File URLs rather than cached local files. | Versioning is handled automatically: when a presenter uploads an updated file, the new hash is cached separately and the old one remains intact. diff --git a/documentation/PROJECT__AE_Events_Launcher_Native_integration.md b/documentation/PROJECT__AE_Events_Launcher_Native_integration.md index 15c7a862..175849bc 100644 --- a/documentation/PROJECT__AE_Events_Launcher_Native_integration.md +++ b/documentation/PROJECT__AE_Events_Launcher_Native_integration.md @@ -41,7 +41,8 @@ The integration is built on a decoupled three-layer communication model to ensur - **Role:** Provides a clean, typed API for Svelte components. - **Responsibilities:** - Mapping `camelCase` UI triggers to `snake_case` IPC calls. - - Resolving a Launch Profile to a single `native_template` string before crossing IPC. + - Resolving an extension alias to a canonical Launch Profile, then to a single + `native_template` string before crossing IPC. The reason for this split is simple: Launch Profiles are policy, while Native Templates are executable strings. Keeping that distinction explicit prevents the bridge from mixing config @@ -199,6 +200,11 @@ resolved a template yet, it should stop before IPC and surface a missing-profile This keeps all fallback logic in Svelte, where it can be edited without rebuilding Electron. The native layer should not invent or guess a default launch path. +The built-in defaults are organized as canonical profile names plus extension aliases. That +lets multiple file types share one profile without repeating the same app/script details. +URL-based presentations remain a special pseudo-extension handled separately from the cache +open flow. + ### Native Template Formats | Format | Example | diff --git a/documentation/TODO__Agents.md b/documentation/TODO__Agents.md index e3785742..a7f647d7 100644 --- a/documentation/TODO__Agents.md +++ b/documentation/TODO__Agents.md @@ -22,11 +22,15 @@ guessing defaults. - [x] `get_launch_profile()` in `launcher_file_cont.svelte` reads from device config then event config; resolves to a `native_template` string and passes it to `launch_from_cache` **Svelte-side migration — remaining before May 26:** -- [ ] **[Launcher] Built-in Svelte default profiles** — move the "known good" pptx/key/pdf +- [ ] **[Launcher] Built-in Svelte default profiles** — move the built-in presentation/media policy objects into a Svelte constants file (e.g. `ae_launcher__default_launch_profiles.ts`). - Priority: `get_launch_profile()` already checks device config and event config; add a 3rd - fallback to these Svelte defaults before returning `null`. Keep the fallback in Svelte, not - in Electron. + Use canonical profile names plus extension aliases so the media family does not repeat the + same VLC config for every file type. Cover the core macOS set (`pptx`, `ppt`, `key`, `odp`, + `pdf`), the media set (`mp4`, `mkv`, `mp3`, and related media types), the Windows / + Parallels variants (`pptxwin`, `pptwin`, `odpwin`, `pdfwin`), and the URL/web-based + presentation path. Priority: `get_launch_profile()` already checks device config and event + config; add a 3rd fallback to these Svelte defaults before returning `null`. Keep the + fallback in Svelte, not in Electron. - [ ] **[Launcher] Composable open flow** — refactor `handle_open_file()` to use `copy_from_cache_to_temp` + `run_osascript` / `run_cmd` directly instead of the all-in-one `launch_from_cache`. Finer error handling at each step (verify copy succeeded before diff --git a/src/lib/ae_events/ae_launcher__default_launch_profiles.ts b/src/lib/ae_events/ae_launcher__default_launch_profiles.ts index 87e81168..c0d3d4d5 100644 --- a/src/lib/ae_events/ae_launcher__default_launch_profiles.ts +++ b/src/lib/ae_events/ae_launcher__default_launch_profiles.ts @@ -8,7 +8,7 @@ * 1. event_file.cfg_json.display_override — per-file, display_mode only * 2. event_device.data_json.launch_profiles[ext] — full profile, per device (API) * 3. $events_loc.launcher.launch_profiles[ext] — local persistent override - * 4. DEFAULT_LAUNCH_PROFILES[ext] — these built-ins + * 4. DEFAULT_LAUNCH_PROFILES[ext] — extension aliases to canonical built-ins * 5. DEFAULT_LAUNCH_PROFILES['default'] — catch-all * * Keys are lowercase file extensions without the dot: "pptx", "key", "pdf", etc. @@ -18,9 +18,12 @@ * - Plain string → run as AppleScript via run_osascript() (macOS only) * - "shell:..." prefix → run as shell command via run_cmd() * - * Reserved for future use (not yet read anywhere): + * Reserved for future use: * - speed_factor: number — delay multiplier for slower machines (1.0 = normal) - * - url: string — for URL-type presentations (e.g. Google Slides) + * + * Special pseudo-extension: + * - url — web-based presentations. Handled by the launcher URL branch rather + * than a cache-to-temp open flow. */ export interface LaunchProfile { @@ -46,18 +49,29 @@ export interface LaunchProfile { // url?: string; } -export const DEFAULT_LAUNCH_PROFILES: Record = { - - // ------------------------------------------------------------------------- - // macOS presentation formats - // ------------------------------------------------------------------------- - - pptx: { - app: 'Microsoft PowerPoint', - display_mode: 'extend', - open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"', +function make_vlc_mirror_profile(): LaunchProfile { + return { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', post_delay_ms: 2000, - post_script: `tell application "Microsoft PowerPoint" + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }; +} + +const POWERPOINT_MAC_EXTEND_PROFILE: LaunchProfile = { + app: 'Microsoft PowerPoint', + display_mode: 'extend', + open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "Microsoft PowerPoint" activate end tell tell application "System Events" @@ -65,40 +79,25 @@ tell application "System Events" keystroke return using command down end tell end tell` - }, +}; - ppt: { - app: 'Microsoft PowerPoint', - display_mode: 'extend', - open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "Microsoft PowerPoint" - activate -end tell -tell application "System Events" - tell process "Microsoft PowerPoint" - keystroke return using command down - end tell -end tell` - }, - - key: { - app: 'Keynote', - display_mode: 'extend', - open_cmd: 'open -a "Keynote" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "Keynote" +const KEYNOTE_MAC_EXTEND_PROFILE: LaunchProfile = { + app: 'Keynote', + display_mode: 'extend', + open_cmd: 'open -a "Keynote" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "Keynote" activate start (front document) end tell` - }, +}; - odp: { - app: 'LibreOffice', - display_mode: 'extend', - open_cmd: 'open -a "LibreOffice" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "LibreOffice" +const LIBREOFFICE_MAC_EXTEND_PROFILE: LaunchProfile = { + app: 'LibreOffice', + display_mode: 'extend', + open_cmd: 'open -a "LibreOffice" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "LibreOffice" activate end tell tell application "System Events" @@ -106,14 +105,14 @@ tell application "System Events" key code 96 end tell end tell` - }, +}; - pdf: { - app: 'Adobe Acrobat Reader DC', - display_mode: 'mirror', - open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "Adobe Acrobat Reader DC" +const ACROBAT_MAC_MIRROR_PROFILE: LaunchProfile = { + app: 'Adobe Acrobat Reader DC', + display_mode: 'mirror', + open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "Adobe Acrobat Reader DC" activate end tell tell application "System Events" @@ -121,198 +120,29 @@ tell application "System Events" keystroke "l" using command down end tell end tell` - }, +}; - // ------------------------------------------------------------------------- - // Media (VLC) — mirror display - // ------------------------------------------------------------------------- +const VLC_MIRROR_PROFILE: LaunchProfile = make_vlc_mirror_profile(); - mp4: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - mkv: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - mov: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - mpeg: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - avi: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - flv: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - ogg: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - ogv: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - mp3: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - wmv: { - app: 'VLC', - display_mode: 'mirror', - open_cmd: 'open -a "VLC" "{{path}}"', - post_delay_ms: 2000, - post_script: `tell application "VLC" - activate -end tell -tell application "System Events" - tell process "VLC" - keystroke "f" using command down - end tell -end tell` - }, - - // ------------------------------------------------------------------------- - // Windows / Parallels variants — longer post_delay_ms (Parallels needs more time) - // ------------------------------------------------------------------------- - - pptxwin: { - app: 'Microsoft Office PowerPoint (Windows)', - display_mode: 'extend', - open_cmd: 'open -a "Microsoft Office PowerPoint" "{{path}}"', - post_delay_ms: 3000, - post_script: `tell application "Microsoft Office PowerPoint" +const POWERPOINT_WIN_EXTEND_PROFILE: LaunchProfile = { + app: 'Microsoft Office PowerPoint (Windows)', + display_mode: 'extend', + open_cmd: 'open -a "Microsoft Office PowerPoint" "{{path}}"', + post_delay_ms: 3000, + post_script: `tell application "Microsoft Office PowerPoint" activate end tell tell application "System Events" key code 96 end tell` - }, +}; - pptwin: { - app: 'Microsoft Office PowerPoint (Windows)', - display_mode: 'extend', - open_cmd: 'open -a "Microsoft Office PowerPoint" "{{path}}"', - post_delay_ms: 3000, - post_script: `tell application "Microsoft Office PowerPoint" - activate -end tell -tell application "System Events" - key code 96 -end tell` - }, - - odpwin: { - app: 'LibreOffice (Windows)', - display_mode: 'extend', - open_cmd: 'open -a "LibreOffice" "{{path}}"', - post_delay_ms: 3000, - post_script: `tell application "LibreOffice" +const LIBREOFFICE_WIN_EXTEND_PROFILE: LaunchProfile = { + app: 'LibreOffice (Windows)', + display_mode: 'extend', + open_cmd: 'open -a "LibreOffice" "{{path}}"', + post_delay_ms: 3000, + post_script: `tell application "LibreOffice" activate end tell tell application "System Events" @@ -320,46 +150,110 @@ tell application "System Events" key code 96 end tell end tell` - }, +}; - pdfwin: { - app: 'Acrobat Reader (Windows)', - display_mode: 'mirror', - open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"', - post_delay_ms: 3000, - post_script: `tell application "Acrobat Reader Windows" +const ACROBAT_WIN_MIRROR_PROFILE: LaunchProfile = { + app: 'Acrobat Reader (Windows)', + display_mode: 'mirror', + open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"', + post_delay_ms: 3000, + post_script: `tell application "Acrobat Reader Windows" activate end tell tell application "System Events" key code 108 using control down end tell` - }, - - // ------------------------------------------------------------------------- - // Catch-all — OS default handler, no display change - // Works on macOS (open) and Linux (xdg-open via open_local_file_v2) - // ------------------------------------------------------------------------- - - default: { - app: 'OS Default', - display_mode: 'none' - // No open_cmd — execution falls through to open_local_file_v2(path) - // No post_script - }, - - // ------------------------------------------------------------------------- - // URL-type files: event_file.filename IS the URL (https://...) - // Opened via native.open_external({ url, app: 'chrome' }) — no local file involved. - // display_mode 'extend' is the default for URL presentations (e.g. Google Slides). - // ------------------------------------------------------------------------- - - url: { - app: 'Chrome', - display_mode: 'mirror' - // No open_cmd or post_script — URL branch in handle_open_file() handles this - } }; +const URL_WEB_PROFILE: LaunchProfile = { + app: 'Chrome', + display_mode: 'extend', + // No open_cmd or post_script — URL branch in handle_open_file() handles this +}; + +const DEFAULT_OS_PROFILE: LaunchProfile = { + app: 'OS Default', + display_mode: 'none', + // No open_cmd — execution falls through to open_local_file_v2(path) + // No post_script +}; + +type DefaultLaunchProfileDefinition = { + name: string; + aliases: string[]; + profile: LaunchProfile; +}; + +export const DEFAULT_LAUNCH_PROFILE_DEFS: DefaultLaunchProfileDefinition[] = [ + { + name: 'powerpoint_mac_extend', + aliases: ['pptx', 'ppt'], + profile: POWERPOINT_MAC_EXTEND_PROFILE + }, + { + name: 'keynote_mac_extend', + aliases: ['key'], + profile: KEYNOTE_MAC_EXTEND_PROFILE + }, + { + name: 'libreoffice_mac_extend', + aliases: ['odp'], + profile: LIBREOFFICE_MAC_EXTEND_PROFILE + }, + { + name: 'acrobat_mac_mirror', + aliases: ['pdf'], + profile: ACROBAT_MAC_MIRROR_PROFILE + }, + { + name: 'vlc_mirror', + aliases: ['mp4', 'mkv', 'mov', 'mpeg', 'avi', 'flv', 'ogg', 'ogv', 'mp3', 'm4v', 'm4a', 'webm', 'wmv', 'wav', 'aac', 'flac'], + profile: VLC_MIRROR_PROFILE + }, + { + name: 'powerpoint_win_extend', + aliases: ['pptxwin', 'pptwin'], + profile: POWERPOINT_WIN_EXTEND_PROFILE + }, + { + name: 'libreoffice_win_extend', + aliases: ['odpwin'], + profile: LIBREOFFICE_WIN_EXTEND_PROFILE + }, + { + name: 'acrobat_win_mirror', + aliases: ['pdfwin'], + profile: ACROBAT_WIN_MIRROR_PROFILE + }, + { + name: 'url_web', + aliases: ['url'], + profile: URL_WEB_PROFILE + }, + { + name: 'os_default', + aliases: ['default'], + profile: DEFAULT_OS_PROFILE + } +]; + +export const DEFAULT_LAUNCH_PROFILE_LIBRARY: Record = Object.fromEntries( + DEFAULT_LAUNCH_PROFILE_DEFS.map(({ name, profile }) => [name, profile]) +); + +export const DEFAULT_LAUNCH_PROFILE_ALIASES: Record = Object.fromEntries( + DEFAULT_LAUNCH_PROFILE_DEFS.flatMap(({ name, aliases }) => + aliases.map((alias) => [alias, name]) + ) +); + +export const DEFAULT_LAUNCH_PROFILES: Record = Object.fromEntries( + DEFAULT_LAUNCH_PROFILE_DEFS.flatMap(({ name, aliases, profile }) => [ + [name, { ...profile }], + ...aliases.map((alias) => [alias, { ...profile }]) + ]) +); + /** * Returns a shallow copy of the built-in profile for the given extension, * with a display_override applied if provided. @@ -373,15 +267,16 @@ export function resolve_launch_profile( local_profiles?: Record | null ): LaunchProfile { const ext = (extension || '').toLowerCase().replace(/^\./, ''); + const default_profile_name = DEFAULT_LAUNCH_PROFILE_ALIASES[ext] ?? ext; - // Priority: device config → local config → built-ins → default + // Priority: device config → local config → canonical built-ins → default const source = device_profiles?.[ext] ?? device_profiles?.['default'] ?? local_profiles?.[ext] ?? local_profiles?.['default'] ?? - DEFAULT_LAUNCH_PROFILES[ext] ?? - DEFAULT_LAUNCH_PROFILES['default']; + DEFAULT_LAUNCH_PROFILES[default_profile_name] ?? + DEFAULT_LAUNCH_PROFILE_LIBRARY.os_default; const profile = { ...source }; diff --git a/src/lib/electron/electron_relay.ts b/src/lib/electron/electron_relay.ts index 0f5f5d2f..5b849d4c 100644 --- a/src/lib/electron/electron_relay.ts +++ b/src/lib/electron/electron_relay.ts @@ -65,7 +65,7 @@ export async function launch_from_cache({ temp_root, filename, hash_prefix_length = 2, - script_template = null + native_template = null }: { cache_root: string; hash: string; @@ -73,17 +73,17 @@ export async function launch_from_cache({ filename: string; hash_prefix_length?: number; /** - * Optional data-driven launch script. If provided, Electron runs this instead of - * its hardcoded extension-based logic — no app rebuild needed for script changes. + * Resolved native launch template. If provided, Electron executes this string + * after the file is copied to temp. * * Two formats: * - AppleScript: multi-line string with {{path}} placeholder (macOS only) * - Shell command: prefix with "shell:" → e.g. "shell:open \"{{path}}\"" * * Configure via event_device.data_json.launch_profiles or $events_loc.launcher.launch_profiles. - * If null, Electron falls through to its built-in hardcoded defaults. + * If null, Electron should treat that as a missing profile error. */ - script_template?: string | null; + native_template?: string | null; }) { if (!native) return { success: false, error: 'Native bridge not available' }; @@ -93,7 +93,7 @@ export async function launch_from_cache({ temp_root, filename, hash_prefix_length, - script_template + native_template }); }