|
#!/usr/bin/osascript -l JavaScript |
|
|
|
ObjC.import('Foundation') |
|
ObjC.import('stdlib') |
|
|
|
const APP_NAME = 'zoom.us' |
|
const MENU_NAME = 'Meeting' |
|
const SCRIPT_NAME = 'Zoom JS' |
|
const AUDIO_ON = 'audio-on' |
|
const AUDIO_OFF = 'audio-off' |
|
const VIDEO_ON = 'video-on' |
|
const VIDEO_OFF = 'video-off' |
|
const PIPE_PATH = '/tmp/zoom_js' |
|
const LOG_PATH = '/tmp/zoom_js.log' |
|
|
|
const MENU_ITEM = { |
|
AUDIO: { ON: 'Unmute audio', OFF: 'Mute audio' }, |
|
VIDEO: { ON: 'Start video', OFF: 'Stop video' } |
|
} |
|
|
|
const SOUND = { |
|
[AUDIO_ON]: 'Purr', |
|
[AUDIO_OFF]: 'Pop', |
|
// [VIDEO_ON]: 'Funk', |
|
// [VIDEO_OFF]: 'Bottle' |
|
} |
|
|
|
function log(msg) { |
|
const app = Application.currentApplication() |
|
app.includeStandardAdditions = true |
|
app.doShellScript(`echo "[${SCRIPT_NAME}] ${msg}" >> ${LOG_PATH}`) |
|
} |
|
|
|
function makeSound(sound) { |
|
try { |
|
const app = Application.currentApplication() |
|
app.includeStandardAdditions = true |
|
app.doShellScript(`nohup afplay /System/Library/Sounds/${sound}.aiff > /dev/null 2>&1 &`) |
|
} catch (e) { |
|
log(`Failed to play sound: ${e}`) |
|
} |
|
} |
|
|
|
function waitForMenuItem(menuItem, timeoutMs = 1_000) { |
|
const start = Date.now() |
|
while (Date.now() - start < timeoutMs) { |
|
try { |
|
if (menuItem[0].exists() && menuItem[0].enabled()) { |
|
return true |
|
} |
|
} catch (error) { |
|
delay(0.1) // Wait for 100ms before checking again |
|
} |
|
} |
|
return false |
|
} |
|
|
|
function executeZoomCommand(command) { |
|
log(`Received command: ${command}`) |
|
|
|
try { |
|
const systemEvents = Application("System Events") |
|
|
|
const zoomProcs = systemEvents.processes.whose({ name: APP_NAME }) |
|
|
|
if (zoomProcs.length === 0) { |
|
log("Zoom is not running.") |
|
return |
|
} |
|
|
|
const zoomProc = zoomProcs[0] |
|
|
|
if (zoomProc.windows.length === 0) { |
|
log("Zoom has no open windows.") |
|
return |
|
} |
|
|
|
const meetingItems = zoomProc.menuBars[0].menuBarItems.whose({ name: MENU_NAME }) |
|
|
|
if (meetingItems.length === 0) { |
|
log("Meeting menu not found.") |
|
return |
|
} |
|
|
|
const meetingMenu = meetingItems[0] |
|
const meetingMenuContent = meetingMenu.menus[0] |
|
|
|
const menuContentItemFn = { |
|
[AUDIO_ON]: () => meetingMenuContent.menuItems.whose({ name: MENU_ITEM.AUDIO.ON }), |
|
[AUDIO_OFF]: () => meetingMenuContent.menuItems.whose({ name: MENU_ITEM.AUDIO.OFF }), |
|
[VIDEO_ON]: () => meetingMenuContent.menuItems.whose({ name: MENU_ITEM.VIDEO.ON }), |
|
[VIDEO_OFF]: () => meetingMenuContent.menuItems.whose({ name: MENU_ITEM.VIDEO.OFF }), |
|
}[command] |
|
|
|
if (!menuContentItemFn) { |
|
log(`Unknown command: ${command}`) |
|
return |
|
} |
|
|
|
const menuContentItem = menuContentItemFn() |
|
if (menuContentItem.length === 0) { |
|
log(`Waiting for menu item ${command} to become enabled...`); |
|
if (!waitForMenuItem(menuContentItem)) { |
|
log(`Menu item ${command} is still disabled after timeout.`); |
|
return; |
|
} |
|
} |
|
|
|
const menuItem = menuContentItem[0] |
|
|
|
|
|
if (menuItem.enabled()) { |
|
log(`Clicking menu item: ${command}`) |
|
menuItem.click() |
|
const sound = SOUND[command] |
|
if (sound) makeSound(sound) |
|
} else { |
|
log(`Menu item for ${command} is disabled.`) |
|
} |
|
} catch (error) { |
|
log(`Error executing command ${command}: ${error}`) |
|
} |
|
} |
|
|
|
function listenForCommands() { |
|
const app = Application.currentApplication() |
|
app.includeStandardAdditions = true |
|
|
|
// Ensure the named pipe exists |
|
app.doShellScript(`rm -f ${PIPE_PATH} && mkfifo ${PIPE_PATH}`) |
|
|
|
log("Listening for commands...") |
|
|
|
while (true) { |
|
try { |
|
const command = app.doShellScript(`cat ${PIPE_PATH}`).trim() |
|
if (command) { |
|
executeZoomCommand(command) |
|
} |
|
} catch (error) { |
|
log(`Error reading command: ${error}`) |
|
} |
|
} |
|
} |
|
|
|
// Run the command listener |
|
listenForCommands() |