Root cause: run_cmd uses exec() which blocks until the child exits. The direct VLC binary forks its GUI process and exits — exec returns and the post_script begins. The old post_script polled for VLC focus (up to 10s) then sent Cmd+F, which fired mid-playback and stopped the video. Fix 1 — nohup + &: detaches VLC from exec immediately so run_cmd returns in ~0ms. This decouples the launcher flow from VLC's lifecycle. Fix 2 — --fullscreen flag: VLC opens fullscreen directly via CLI option. Eliminates the Cmd+F keystroke that was the proximate cause of the stop. Fix 3 — > /dev/null 2>&1: silences VLC's verbose logging to prevent exec's 1MB stdout buffer from overflowing and killing the process. post_script simplified to a single `tell application "VLC" activate` to bring VLC to the foreground after the 3s startup delay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
401 lines
13 KiB
TypeScript
401 lines
13 KiB
TypeScript
/**
|
||
* ae_launcher__default_launch_profiles.ts
|
||
*
|
||
* Built-in launch profiles for the Aether Events Launcher — the Svelte-side
|
||
* replacement for the legacy OSIT MasterKey Swift app.
|
||
*
|
||
* These are the last-resort defaults. Override priority (high → low):
|
||
* 1. event_file.cfg_json.display_override — per-file, display_mode only
|
||
* 2. event_device.data_json.launch_profiles[profile] — per-profile override, per device (API)
|
||
* 3. $events_loc.launcher.launch_profiles[profile] — local persistent override
|
||
* 4. DEFAULT_LAUNCH_PROFILES[profile/alias] — canonical built-ins + aliases
|
||
* 5. DEFAULT_LAUNCH_PROFILES['default'] — catch-all
|
||
*
|
||
* Keys are lowercase file extensions without the dot: "pptx", "key", "pdf", etc.
|
||
* The special key "default" catches any unrecognised extension.
|
||
*
|
||
* post_script formats:
|
||
* - Plain string → run as AppleScript via run_osascript() (macOS only)
|
||
* - "shell:..." prefix → run as shell command via run_cmd()
|
||
*
|
||
* Reserved for future use:
|
||
* - speed_factor: number — delay multiplier for slower machines (1.0 = normal)
|
||
*
|
||
* Special pseudo-extension:
|
||
* - url — web-based presentations. Handled by the launcher URL branch rather
|
||
* than a cache-to-temp open flow.
|
||
*/
|
||
|
||
export interface LaunchProfile {
|
||
/** Human-readable label for status messages */
|
||
app: string;
|
||
/** Display layout to set before opening. 'extend' only applied if external display found. */
|
||
display_mode: 'extend' | 'mirror' | 'none';
|
||
/**
|
||
* Shell command to open the file. {{path}} is replaced with the resolved temp path.
|
||
* If omitted, falls back to open_local_file_v2(path) — OS default handler.
|
||
*/
|
||
open_cmd?: string;
|
||
/**
|
||
* Script to run after the file opens and post_delay_ms has elapsed.
|
||
* Plain string → AppleScript (macOS). "shell:" prefix → shell command.
|
||
*/
|
||
post_script?: string;
|
||
/**
|
||
* Milliseconds to wait after open_cmd before running post_script.
|
||
* Default: 2000. Can be overridden per profile via launch_profiles[profile].post_delay_ms.
|
||
*/
|
||
post_delay_ms?: number;
|
||
|
||
// --- Reserved for future use — not yet implemented ---
|
||
// speed_factor?: number;
|
||
// url?: string;
|
||
}
|
||
|
||
/**
|
||
* macOS VLC profile — uses direct binary path for max reliability.
|
||
* Bypasses `open -a` argument-handling quirks that could lose file path or re-use existing process.
|
||
*
|
||
* WHY nohup + &:
|
||
* run_cmd uses exec() which blocks until the child process exits (or the 30s timeout fires).
|
||
* The direct VLC binary forks a GUI process then exits — exec returns early and the code
|
||
* proceeds to the post_script. The old post_script polled for VLC focus (up to 10s) then
|
||
* sent Cmd+F, which was firing exactly 10–15 seconds into playback and stopping the video.
|
||
* nohup + & detaches VLC immediately so exec returns in ~0ms, decoupling run_cmd from
|
||
* VLC's lifecycle entirely.
|
||
*
|
||
* WHY --fullscreen:
|
||
* Starting VLC fullscreen via flag avoids the need to send Cmd+F via AppleScript. The old
|
||
* keystroke approach was the proximate cause of the video stopping — Cmd+F may have hit the
|
||
* wrong VLC window, triggered a menu action, or paused playback during the fullscreen
|
||
* transition. Using the flag is simpler and more reliable.
|
||
*
|
||
* WHY > /dev/null 2>&1:
|
||
* VLC logs verbosely to stdout/stderr. exec() buffers output (1MB default). Without
|
||
* redirection the buffer could overflow and kill VLC mid-playback.
|
||
*/
|
||
function make_vlc_mirror_mac_profile(): LaunchProfile {
|
||
return {
|
||
app: 'VLC (macOS)',
|
||
display_mode: 'mirror',
|
||
open_cmd: 'nohup /Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause --fullscreen "{{path}}" > /dev/null 2>&1 &',
|
||
post_delay_ms: 3000,
|
||
// Activate VLC after it has had time to open. Fullscreen is already set by the flag
|
||
// above — this just ensures VLC is the frontmost app and the presenter sees it.
|
||
post_script: `tell application "VLC"
|
||
activate
|
||
end tell`
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Linux VLC profile — uses shell command for compatibility.
|
||
*/
|
||
function make_vlc_mirror_linux_profile(): LaunchProfile {
|
||
return {
|
||
app: 'VLC (Linux)',
|
||
display_mode: 'mirror',
|
||
// shell: prefix runs as bash command. Same flags as macOS: `--no-play-and-exit` keeps window open, `--play-and-pause` holds final frame.
|
||
open_cmd: 'shell:vlc --no-play-and-exit --play-and-pause "{{path}}"',
|
||
post_delay_ms: 1000
|
||
// No post_script on Linux — VLC opens fullscreen by default, no need to send F.
|
||
};
|
||
}
|
||
|
||
const POWERPOINT_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||
app: 'Microsoft PowerPoint',
|
||
display_mode: 'extend',
|
||
open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"',
|
||
post_delay_ms: 1000,
|
||
post_script: `repeat 20 times
|
||
tell application "Microsoft PowerPoint"
|
||
activate
|
||
end tell
|
||
delay 0.5
|
||
tell application "System Events"
|
||
if frontmost of process "Microsoft PowerPoint" is true then exit repeat
|
||
end tell
|
||
end repeat
|
||
delay 0.3
|
||
tell application "System Events"
|
||
tell process "Microsoft PowerPoint"
|
||
keystroke return using command down
|
||
end tell
|
||
end tell`
|
||
};
|
||
|
||
const KEYNOTE_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||
app: 'Keynote',
|
||
display_mode: 'extend',
|
||
open_cmd: 'open -a "Keynote" "{{path}}"',
|
||
post_delay_ms: 1000,
|
||
// Keynote uses `start (front document)` which requires the document to actually be loaded —
|
||
// polling frontmost is not enough here. Poll document count instead.
|
||
post_script: `tell application "Keynote"
|
||
activate
|
||
end tell
|
||
repeat 20 times
|
||
delay 0.5
|
||
tell application "Keynote"
|
||
if (count of documents) > 0 then exit repeat
|
||
end tell
|
||
end repeat
|
||
delay 0.3
|
||
tell application "Keynote"
|
||
start (front document)
|
||
end tell`
|
||
};
|
||
|
||
const LIBREOFFICE_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||
app: 'LibreOffice',
|
||
display_mode: 'extend',
|
||
open_cmd: 'open -a "LibreOffice" "{{path}}"',
|
||
post_delay_ms: 1000,
|
||
post_script: `repeat 20 times
|
||
tell application "LibreOffice"
|
||
activate
|
||
end tell
|
||
delay 0.5
|
||
tell application "System Events"
|
||
if frontmost of process "soffice" is true then exit repeat
|
||
end tell
|
||
end repeat
|
||
delay 0.3
|
||
tell application "System Events"
|
||
tell process "soffice"
|
||
key code 96
|
||
end tell
|
||
end tell`
|
||
};
|
||
|
||
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: 1000,
|
||
post_script: `repeat 20 times
|
||
tell application "Adobe Acrobat Reader DC"
|
||
activate
|
||
end tell
|
||
delay 0.5
|
||
tell application "System Events"
|
||
if frontmost of process "AdobeReader" is true then exit repeat
|
||
end tell
|
||
end repeat
|
||
delay 0.3
|
||
tell application "System Events"
|
||
tell process "AdobeReader"
|
||
keystroke "l" using command down
|
||
end tell
|
||
end tell`
|
||
};
|
||
|
||
const VLC_MIRROR_MAC_PROFILE: LaunchProfile = make_vlc_mirror_mac_profile();
|
||
const VLC_MIRROR_LINUX_PROFILE: LaunchProfile = make_vlc_mirror_linux_profile();
|
||
|
||
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: 1500,
|
||
post_script: `tell application "Microsoft Office PowerPoint"
|
||
activate
|
||
end tell
|
||
repeat 15 times
|
||
delay 0.5
|
||
tell application "System Events"
|
||
if frontmost of process "Microsoft Office PowerPoint" is true then exit repeat
|
||
end tell
|
||
end repeat
|
||
delay 0.3
|
||
tell application "System Events"
|
||
key code 96
|
||
end tell`
|
||
};
|
||
|
||
const LIBREOFFICE_WIN_EXTEND_PROFILE: LaunchProfile = {
|
||
app: 'LibreOffice (Windows)',
|
||
display_mode: 'extend',
|
||
open_cmd: 'open -a "LibreOffice" "{{path}}"',
|
||
post_delay_ms: 1500,
|
||
post_script: `repeat 20 times
|
||
tell application "LibreOffice"
|
||
activate
|
||
end tell
|
||
delay 0.5
|
||
tell application "System Events"
|
||
if frontmost of process "soffice" is true then exit repeat
|
||
end tell
|
||
end repeat
|
||
delay 0.3
|
||
tell application "System Events"
|
||
tell process "soffice"
|
||
key code 96
|
||
end tell
|
||
end tell`
|
||
};
|
||
|
||
const ACROBAT_WIN_MIRROR_PROFILE: LaunchProfile = {
|
||
app: 'Acrobat Reader (Windows)',
|
||
display_mode: 'mirror',
|
||
open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"',
|
||
post_delay_ms: 1500,
|
||
post_script: `repeat 20 times
|
||
tell application "Acrobat Reader Windows"
|
||
activate
|
||
end tell
|
||
delay 0.5
|
||
tell application "System Events"
|
||
if frontmost of process "Acrobat Reader Windows" is true then exit repeat
|
||
end tell
|
||
end repeat
|
||
delay 0.3
|
||
tell application "System Events"
|
||
key code 108 using control down
|
||
end tell`
|
||
};
|
||
|
||
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_mac',
|
||
aliases: [],
|
||
profile: VLC_MIRROR_MAC_PROFILE
|
||
},
|
||
{
|
||
name: 'vlc_mirror_linux',
|
||
aliases: [],
|
||
profile: VLC_MIRROR_LINUX_PROFILE
|
||
},
|
||
{
|
||
name: 'vlc_mirror',
|
||
aliases: ['mp4', 'mkv', 'mov', 'mpeg', 'avi', 'flv', 'ogg', 'ogv', 'mp3', 'm4v', 'm4a', 'webm', 'wmv', 'wav', 'aac', 'flac'],
|
||
profile: VLC_MIRROR_MAC_PROFILE // Default to macOS (primary deployment platform)
|
||
},
|
||
{
|
||
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.
|
||
*
|
||
* Falls back to 'default' if no specific profile exists.
|
||
*/
|
||
export function resolve_launch_profile(
|
||
extension: string,
|
||
display_override?: 'extend' | 'mirror' | 'none' | null,
|
||
device_profiles?: Record<string, Partial<LaunchProfile>> | null,
|
||
local_profiles?: Record<string, Partial<LaunchProfile>> | null
|
||
): LaunchProfile {
|
||
const ext = (extension || '').toLowerCase().replace(/^\./, '');
|
||
const canonical_profile_name = DEFAULT_LAUNCH_PROFILE_ALIASES[ext] ?? ext;
|
||
|
||
const built_in_profile =
|
||
DEFAULT_LAUNCH_PROFILE_LIBRARY[canonical_profile_name] ??
|
||
DEFAULT_LAUNCH_PROFILE_LIBRARY.os_default;
|
||
|
||
const local_profile =
|
||
local_profiles?.[canonical_profile_name] ??
|
||
local_profiles?.[ext] ??
|
||
local_profiles?.['default'] ??
|
||
null;
|
||
|
||
const device_profile =
|
||
device_profiles?.[canonical_profile_name] ??
|
||
device_profiles?.[ext] ??
|
||
device_profiles?.['default'] ??
|
||
null;
|
||
|
||
const profile = {
|
||
...built_in_profile,
|
||
...(local_profile ?? {}),
|
||
...(device_profile ?? {})
|
||
};
|
||
|
||
// Per-file display override wins over everything
|
||
if (display_override) {
|
||
profile.display_mode = display_override;
|
||
}
|
||
|
||
return profile;
|
||
}
|