Creating a zoom mic status icon in the menu bar

Like many people lately, I spend half my day in Zoom meetings. And also like many people, I’m working from house noisy from kids running around. So my usual mode of operation on a call is to mute myself except when actually speaking.

On the Mac, Zoom provides a built in global keyboard shortcut (Command-Shift-A) so I can toggle mute/unmute no matter what app I’m using. But that’s awkward to remember and type. And there’s no way to tell when your mic is hot if Zoom isn’t active in the foreground.

So I set out to fix these two things:

  1. A simpler way to toggle mute/unmute in zoom, from any application.
  2. A menu bar icon indicating my mic status, so I know if the mic is hot even if the zoom meeting window is in the background.

And I figured it out! It’s held together by shoestrings and good intentions, but it’s reliable. I’ve written up my notes here.

The steps I followed were:

  1. Set Zoom’s mute/unmute keyboard shortcut as a global keyboard shortcut.
  2. Map Map “eject” key to command-shift-A, command-f13 sequence using Karabiner Elements.
  3. Use Anybar to put a red/green dot in the menu bar indicating mic status, using an AppleScript to set its status.
  4. Write a script to update the menubar icon.

1. Set Zoom’s mute/unmute keyboard shortcut as a global keyboard shortcut.

This is done in the Zoom settings, seen here:

image alt Zoom preference screenshot

2. Map “eject” key to command-shift-A, command-f13 sequence using Karabiner Elements

Karabiner-Elements is a program that can change key mapping on the Mac. You can do really simple things, like map caps-lock to control, or more complex things, like what I’m about to describe.

I chose to map eject to two key sequences: command-shift-A, command-f13. The first simply executes the native Zoom keyboard shortcut for mute, as described above. The second one is more interesting, which I’ll describe in a later step. 

Complex modifications in Karabiner are defined via json.

{
  "title": "Zoom audio toggle modification",
  "rules": [
    {
        "description": "Eject key triggers two keyboard shortcuts",
        "manipulators": [
            {
                "from": {
                    "consumer_key_code": "eject"
                },
                "to": [
                    {
                        "key_code": "a",
                        "modifiers" : ["command", "shift"]
                    },
                    { 
                        "key_code": "f13",
                        "modifiers" : ["command"]
                    }
                ],
                "type": "basic"
            }
        ]
    }]
}

To enable the above:

  1. Place the json file in ~/.config/karabiner/assets/complex_modifications.
  2. Goto Karabiner -> Preferences -> Complex Modifications -> Add rule
  3. Enable your custom rule.

The documentation for complex modifications in Karabiner Elements is a bit hard to come by / nonexistent. A few resources I used:

(You can actually just add a simple modification in Karabiner elements if you don’t care about the status bar indicator, and stop here.)

3. Use Anybar to put a red/green dot in the menu bar indicating mic status, using an AppleScript to set its status.

Anybar is a simple Mac utility that puts a colored dot in the menu bar, which you can control using the command line or AppleScript. I installed it using Homebrew

4. Write a script to update the menubar icon

This is what the second keyboard shortcut sequence, command-f13 is for. When that is triggered, I use an Alfred hotkey to run the script, which reads the current state of audio in Zoom and updates the menu bar accordingly (There are different ways to do this, but I use Alfred to manage my keyboard shortcuts.):

image alt Alfred screenshot

image alt Alfred screenshot

-- Manipulate and get the status of Zoom mute/unmute, and show that status using the app AnyBar
-- Uses AnyBar to show zoom audio status (https://github.com/tonsky/AnyBar)
-- 
-- Takes one argument:
--   - toggle_zoom: Toggles the mute status and updates AnyBar red/green to reflect new status
--   - update_bar:  Grabs the current mute status and updates AnyBar
--
-- Anybar colors:
--   - Green: mic on
--   - Red: mic muted
--   - Hidden: No zoom meeting in progress
--

property btnTitleMute : "Mute audio"
property btnTitleUnMute : "Unmute audio"

on is_running(appName)
	tell application "System Events" to (name of processes) contains appName
end is_running

on set_indicator(indicatorColor)
	tell application "AnyBar" to set image name to indicatorColor
	-- display notification indicatorColor
end set_indicator

-- Return true if zoom meeting is active
on is_zoom_meeting_active()
	-- Is zoom even running?
	if not is_running("zoom.us") then
		return false
	end if
	
	tell application "System Events"
		tell application process "zoom.us"
			if exists (menu bar item "Meeting" of menu bar 1) then
				return true
			end if
			return false
		end tell
	end tell
end is_zoom_meeting_active

-- Return true/false if mic is active or not
on get_zoom_meeting_mic_on()
	if is_zoom_meeting_active() then
		tell application "System Events"
			tell application process "zoom.us"
				if exists (menu item btnTitleMute of menu 1 of menu bar item "Meeting" of menu bar 1) then
					return true
				end if
			end tell
		end tell
	end if
	return false
end get_zoom_meeting_mic_on


-- Update the status bar
on update_status_bar()
	if get_zoom_meeting_mic_on() then
		set_indicator("green")
	else
		set_indicator("red")
	end if
end update_status_bar

-- Toggle the audio state in zoom
on toggle_zoom_audio_state()
	if is_zoom_meeting_active() then
		tell application "System Events"
			tell application process "zoom.us"
				if my get_zoom_meeting_mic_on() then
					-- If unmuted, mute
					click menu item btnTitleMute of menu 1 of menu bar item "Meeting" of menu bar 1
					my set_indicator("red")
				else
					-- If unmuted, mute
					click menu item btnTitleUnMute of menu 1 of menu bar item "Meeting" of menu bar 1
					my set_indicator("green")
				end if
			end tell
		end tell
	end if
end toggle_zoom_audio_state

-- Entry point for script
on run argv
	-- If there is no zoom meeting, quit anybar (no indicator), and quit processing
	if not is_zoom_meeting_active() then
		tell application "AnyBar" to quit
		return
	end if
	
	if (count of argv) > 0 then
		set mode to item 1 of argv
		
		if mode is "toggle_zoom" then
			-- Change state of audio zoom (triggered from keybard shortcut)
			toggle_zoom_audio_state()
		else if mode is "update_bar" then
			-- Just update the menu bar (called via cron)
			update_status_bar()
		end if
	end if
end run

(Note that this script has the ability to set the mute status. I ended up not using that, because it’s significantly slower than doing it via native Zoom keyboard shortcut.)

This actually works really well. But what if I just click the “mute” button in the zoom app? This script won’t be run, and the status bar won’t be updated, and therefore wrong.

I use launchd to run the script every 10 seconds. This also serves to remove the status icon once a video chat has concluded.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>EnvironmentVariables</key>
	<dict>
		<key>PATH</key>
		<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/munki:/usr/local/sbin</string>
	</dict>
	<key>Label</key>
	<string>Periodically check zoom status</string>
	<key>LowPriorityIO</key>
	<true/>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/bin/osascript</string>
		<string>/Users/james.sulak/src/zoom_toggle/zoom_audio.applescript</string>
		<string>update_bar</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>StartInterval</key>
	<integer>10</integer>
</dict>
</plist>

Place this in ~/Library/LaunchAgents/.