system wide shortcut for Mac OS X

Question:

So I was asked to port some internal helper applications to Mac OS X 10.7.

Works all quite welll as the platform dependent code is minimal anyhow, but one application needs a system wide shortcut to function (i.e. RegisterHotkey functionality) and I can’t find any documentation on how I’d do this on a Mac.

The program is using a PyQt gui with Python 3.2. and the corresponding code for windows is basically:

def register_hotkey(self):
    hwnd = int(self.winId())
    modifiers, key = self._get_hotkey()
    user32.RegisterHotKey(hwnd, self._MESSAGE_ID, modifiers, key)

and then to receive the hotkey events:

def winEvent(self, msg):
    if msg.message == w32.WM_HOTKEY:
        self.handle_hotkey()
        return True, id(msg)
    return False, id(msg)

Note that I don’t need a python variant, I can easily write a simple c extension – so C/objective-c solutions are welcome as well.

Asked By: Voo

||

Answers:

Using the power of google, I found this snippet of code, which allows the registration of global hotkeys for Mac OS X

You’ll need to add the Carbon framework, and probably a bridged cast for ARC when passing the Objective-C self pointer to the C function.

At a minimum, you’ll also need to:

#import <Carbon/Carbon.h>

The keycodes can be seen on this page explaining the virtual key codes.

Answered By: Anya Shenanigans

I recently coded up an extension to quodlibet capturing multimedia keys (since absorbed into quodlibet itself); for your setup the same process applies.

I used the Quartz CGEventTapCreate hook and event loop, and the Cocoa AppKit framework to decipher key codes to achieve this.

The following code registers a python callback which is passed global key presses, and starts the event loop:

import Quartz
from AppKit import NSKeyUp, NSSystemDefined, NSEvent

# Set up a tap, with type of tap, location, options and event mask
tap = Quartz.CGEventTapCreate(
    Quartz.kCGSessionEventTap, # Session level is enough for our needs
    Quartz.kCGHeadInsertEventTap, # Insert wherever, we do not filter
    Quartz.kCGEventTapOptionListenOnly, # Listening is enough
    Quartz.CGEventMaskBit(NSSystemDefined), # NSSystemDefined for media keys
    keyboardTapCallback,
    None
)

runLoopSource = Quartz.CFMachPortCreateRunLoopSource(None, tap, 0)
Quartz.CFRunLoopAddSource(
    Quartz.CFRunLoopGetCurrent(),
    runLoopSource,
    Quartz.kCFRunLoopDefaultMode
)
# Enable the tap
Quartz.CGEventTapEnable(tap, True)
# and run! This won't return until we exit or are terminated.
Quartz.CFRunLoopRun()

I defined a tap for system defined keys only (media keys); you’ll have to specify a different event mask (CGEventMaskBit with one or more Event Types); e.g. Quartz.CGEventMaskBit(Quartz.kCGEventKeyUp) for key up events.

The callback should have the following signature (it implements the CGEventTapCallBack method from the Quartz API:

def keyboardTapCallback(proxy, type_, event, refcon):
    # Convert the Quartz CGEvent into something more useful
    keyEvent = NSEvent.eventWithCGEvent_(event)

I converted the Quartz event into a NSEvent, because all the information I could find on Mac multimedia keys was referring to that class.

In principle you can achieve the same thing with the AppKit APIs too, but then your Python application is treated as a Mac Application (visible in the Dock with an icon and everything), while I wanted this to be kept in the background altogether.

Answered By: Martijn Pieters

Why has nobody ever mentioned the hammerspoon, which supports custom global system shortcuts, which can invoke shell command or launch UI application like Safari, PHOTOSHOP.

The following is an example written by me demonstrating how to invoke shell function with global hotkeys.
https://gist.github.com/BigSully/0e59ab97f148bc167ea19dbd42ebef4b

Use hs.execute to execute shell command, either non-interactive or interactive.

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "P", function()
  local output = hs.execute("toggleProxy", true)
  hs.alert.show(output)
end)

or
Use hs.application.launchOrFocus to launch application
hs.application.launchOrFocus(“Safari”)

Answered By: Iceberg

Let me get to the good part first: I’ve written a small Python module to make this easy: QuickMacHotKey.

Using it looks like this:

@quickHotKey(virtualKey=kVK_ANSI_X,
             modifierMask=mask(cmdKey, controlKey, optionKey))
def handler() -> None:
    print("handled ⌘⌃⌥X")

You can get it with pip install quickmachotkey.

Now for the history and details as to why this is necessary:

Unfortunately the accepted answer is using a documented, public API that requires obnoxious security prompts and full accessibility access in order to use. Higher level APIs like addGlobalMonitorForEventsMatchingMask:handler: also require that sort of accessibility access. As the documentation for that method puts it:

Key-related events may only be monitored if accessibility is enabled or if your application is trusted for accessibility access (see AXIsProcessTrusted).

The way to register global hotkeys without triggering accessibility prompts is still to use the ancient Carbon API RegisterEventHotKey, and that’s not a hyperlink because there’s no official documentation any more; you just have to know this exists by having worked with earlier versions of macOS. The best documentation tends to be other open source projects muddling through this discovery themselves. It’s just a leftover function from a framework that is so deprecated that almost all of it has been removed; this one, specific function has just never been replaced because there’s still no alternative, despite the vast majority of the framework being gone now.

This API used to be available to Python via the built-in Carbon module, but that was removed in Python 3 because it was wrapping a framework that was, as I said, going away. With the introduction of 64-bit macs, big chunks of that framework were never even ported, and 32-bit executables on macOS have not been supported for many years now.

So, what my module is doing is simply using a PyObjC API which is, itself, deprecated, with no replacement, although its maintainer is somewhat more responsive than Apple, so I’d expect that my concerns will actually be addressed before the framework release that fully breaks it comes out :).

Answered By: Glyph
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.