refactor(launcher): canonicalize default profiles

This commit is contained in:
Scott Idem
2026-05-13 12:15:13 -04:00
parent c79ae92be0
commit 1374f0728e
5 changed files with 210 additions and 286 deletions

View File

@@ -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.

View File

@@ -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 |

View File

@@ -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

View File

@@ -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<string, LaunchProfile> = {
// -------------------------------------------------------------------------
// 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<string, LaunchProfile> = Object.fromEntries(
DEFAULT_LAUNCH_PROFILE_DEFS.map(({ name, profile }) => [name, profile])
);
export const DEFAULT_LAUNCH_PROFILE_ALIASES: Record<string, string> = Object.fromEntries(
DEFAULT_LAUNCH_PROFILE_DEFS.flatMap(({ name, aliases }) =>
aliases.map((alias) => [alias, name])
)
);
export const DEFAULT_LAUNCH_PROFILES: Record<string, LaunchProfile> = 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<string, LaunchProfile> | 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 };

View File

@@ -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
});
}