Skip to content

Instantly share code, notes, and snippets.

@mikebronner
Last active June 17, 2026 13:35
Show Gist options
  • Select an option

  • Save mikebronner/dcd4b402f66403da0f450c35f718ef14 to your computer and use it in GitHub Desktop.

Select an option

Save mikebronner/dcd4b402f66403da0f450c35f718ef14 to your computer and use it in GitHub Desktop.
macOS push-to-pause: hold right Control to pause media (Music, Spotify, YouTube, anything in Now Playing), release to resume. Hammerspoon-based companion to Wispr Flow.

Push-to-Pause for macOS — a Wispr Flow companion

Hold a key to pause whatever's playing on your Mac — release it to resume right where you left off. A Hammerspoon config that hooks the macOS Now Playing system, so it works with Music, Spotify, Podcasts, Safari, Chrome, Arc, YouTube, Netflix, VLC — anything that shows up in Control Center's media widget.

Why this exists

Built as a companion to Wispr Flow, an AI dictation tool you trigger by holding a hotkey (right Control by default). Wispr Flow has a "duck" feature that lowers the volume of other media while you dictate — but lowering the volume isn't enough. You can still miss content in a podcast or video while you're talking, especially if you dictate frequently.

The goal here is for dictation to act as a clean interruption: media pauses the moment you start talking, and resumes the instant you stop. You miss nothing.

Same key, two behaviors, layered:

  • 🎤 Wispr Flow sees right Control held → starts listening to your voice
  • ⏸️ This script sees right Control held → pauses Now Playing
  • 🔓 Release the key → Wispr Flow stops listening, this script resumes playback

The script doesn't replace Wispr Flow's hotkey — it runs alongside it. The keypress passes through to both.

Behavior

  • 🎬 Hold right Control > 100ms while something is playing → pauses it
  • ▶️ Release → resumes
  • 🔇 Hold while nothing is playing → no-op (won't accidentally start playback)
  • 👆 Quick tap (< 100ms) → ignored, no flicker
  • 🔄 Right Control still passes through to whatever else is bound to it (Wispr Flow's push-to-talk, etc.)

Why Hammerspoon and not Shortcuts.app or Karabiner?

Shortcuts.app has no key-down/key-up event model — it triggers on chord press, not on hold/release. Can't express "while held" behavior at all.

Karabiner-Elements can detect hold/release, but its model is JSON-driven static rules — so dynamic logic (check if media is playing, remember state, conditionally pause) gets shelled out to separate scripts in a separate directory. Three files across two directories, more surface area to maintain.

Hammerspoon hooks flagsChanged events directly via Lua and keeps press detection, media check, state, and pause/resume logic in one ~45-line file in one place. Easier to share, easier to maintain, easier to extend.

Install

brew install --cask hammerspoon
brew install nowplaying-cli
mkdir -p ~/.hammerspoon
# Save init.lua from this gist to ~/.hammerspoon/init.lua

Then:

  1. 🚀 Launch Hammerspoon (it'll auto-load ~/.hammerspoon/init.lua)
  2. 🔐 Grant both permissions in System Settings → Privacy & Security:
    • Accessibility — macOS will prompt for this on first launch
    • Input MonitoringmacOS often will NOT prompt for this. You must add Hammerspoon manually. Without it, the eventtap silently fails and nothing happens.
  3. 🔄 From Hammerspoon's menu bar 🔨 icon → Reload Config
  4. 🚀 Recommended: 🔨 → Preferences → check Launch Hammerspoon at login

Test

Play something — Spotify, a YouTube tab, anything. Run this to confirm Now Playing sees it:

nowplaying-cli get playbackRate    # should print "1"
nowplaying-cli get title           # should print the song/video title

Then hold right Control past 100ms. Should pause. Release. Should resume.

Customize

In init.lua:

  • HOLD_THRESHOLD = 0.1 — tap-vs-hold threshold in seconds. Lower = more sensitive, higher = harder to trigger accidentally.
  • RIGHT_CONTROL_KEYCODE = 62 — change to a different modifier. Run hs.keycodes.map in the Hammerspoon Console (🔨 → Console) to see all keycodes. Common ones: right_command = 54, right_option = 61, right_shift = 60, fn = 63.

Troubleshooting

  • 🔇 Nothing happens when I hold the key. Almost always Input Monitoring permission. Verify Hammerspoon is checked in System Settings → Privacy & Security → Input Monitoring. Restart Hammerspoon after granting.
  • 🤐 nowplaying-cli get playbackRate returns nothing. The browser/app you're playing from isn't registering with Now Playing. Safari, recent Chrome, and recent Firefox all do. Older Chrome may need chrome://flags/#hardware-media-key-handling enabled.
  • 🔁 Hold pauses but release doesn't resume. Likely Hammerspoon was reloaded mid-hold and lost state. Just press and release again.
-- Wispr Flow companion: pause media while right Control is held.
-- Quick taps (< 100ms) are ignored so they don't flicker playback.
-- Pass-through is preserved so right Control still triggers Wispr Flow.
local NPC = "/opt/homebrew/bin/nowplaying-cli"
local HOLD_THRESHOLD = 0.1 -- seconds; only pause if held longer than this
local RIGHT_CONTROL_KEYCODE = 62
local pausedByUs = false
local rightControlHeld = false
local pendingPauseTimer = nil
local function isPlaying()
local rate = hs.execute(NPC .. " get playbackRate"):gsub("%s+", "")
return rate == "1"
end
wisprPauseWatcher = hs.eventtap.new(
{ hs.eventtap.event.types.flagsChanged },
function(event)
if event:getKeyCode() ~= RIGHT_CONTROL_KEYCODE then
return false
end
rightControlHeld = not rightControlHeld
if rightControlHeld then
pendingPauseTimer = hs.timer.doAfter(HOLD_THRESHOLD, function()
pendingPauseTimer = nil
if rightControlHeld and isPlaying() then
hs.execute(NPC .. " pause")
pausedByUs = true
end
end)
else
if pendingPauseTimer then
pendingPauseTimer:stop()
pendingPauseTimer = nil
end
if pausedByUs then
hs.execute(NPC .. " play")
pausedByUs = false
end
end
return false -- always pass through so Wispr Flow still triggers
end
)
wisprPauseWatcher:start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment