How to listen to global hotkeys with Swift in a macOS app?
Asked Answered
K

7

55

I'm trying to have a handler in my Mac OS X app written in Swift for a global (system-wide) hotkey combo but I just cannot find proper documentation for it. I've read that I'd have to mess around in some legacy Carbon API for it, is there no better way? Can you show me some proof of concept Swift code? Thanks in advance!

Kriemhild answered 2/2, 2015 at 15:58 Comment(1)
What exactly have you tried? What do you mean by hot keys? Have you tried NSEvent global monitor or CGEventTapAnfractuous
M
14

Since Swift 2.0, you can now pass a function pointer to C APIs.

var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
gMyHotKeyID.id = UInt32(keyCode)

var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyPressed)

// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {(nextHanlder, theEvent, userData) -> OSStatus in
    var hkCom = EventHotKeyID()
    GetEventParameter(theEvent, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, sizeof(EventHotKeyID), nil, &hkCom)

    // Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)

// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode), UInt32(modifierKeys), gMyHotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef)
Mitosis answered 18/1, 2016 at 21:30 Comment(6)
Does not work in Swift 4 and without a more detailed explanation it’s impossible to implement.Mauritamauritania
Works well in Swift 4, I have converted my projects to Swift 4 and it works fine. As a developer, you should know that "doesn't work" is the worst possible description of an issue - what does "doesn't work" mean? It doesn't compile? What compile error are you getting? There's really not much to explain - each function used is documented by Apple.Mitosis
Good vibes only, Charlie! It would be great if you could share your Swift 4 solution as well. The solution you posted is incomplete what makes it hard for beginners to adapt. One thing that would be helpful to say is that importing Carbon is necessary. The next problem I faced is that String does not have a .fourCharCodeValue property. Also it would be great if you could provide an example for keyCode and modifierKeys.Mauritamauritania
@Mauritamauritania - sorry if it sounded hostile - was not meant to be at all. fourCharCodeValue converts a string to Int - it was used a lot back in the day - 4 ASCII chars convert to 32-bit integer - it makes it easier to identify in dumps than a random integer. You can find my implementation here: github.com/charlieMonroe/XUCore/blob/master/XUCore/additions/… - I'm currently retrieving keyCode from SRRecorderControl: gist.github.com/charlieMonroe/0923985c704695b252ed9bb879e7f99dMitosis
I have done my homework and made this solution to work with Swift 5. See here.Portraiture
I got error of "Cannot find 'EventHotKeyID' in scope" do we need import some third party framework?Hoban
P
15

The following code works for me for Swift 5.0.1. This solution is the combination of the solution from the accepted answer by Charlie Monroe and the recommendation by Rob Napier to use DDHotKey.

DDHotKey seems to work out of the box but it had one limitation that I had to change: the eventKind is hardcoded to kEventHotKeyReleased while I needed both kEventHotKeyPressed and kEventHotKeyReleased event types.

eventSpec.eventKind = kEventHotKeyReleased;

If you want to handle both Pressed and Released events, just add a second InstallEventHandler call which registers the other event kind.

This the complete example of the code that registers the "Command + R" key for the kEventHotKeyReleased type.

import Carbon

extension String {
  /// This converts string to UInt as a fourCharCode
  public var fourCharCodeValue: Int {
    var result: Int = 0
    if let data = self.data(using: String.Encoding.macOSRoman) {
      data.withUnsafeBytes({ (rawBytes) in
        let bytes = rawBytes.bindMemory(to: UInt8.self)
        for i in 0 ..< data.count {
          result = result << 8 + Int(bytes[i])
        }
      })
    }
    return result
  }
}

class HotkeySolution {
  static
  func getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags) -> UInt32 {
    let flags = cocoaFlags.rawValue
    var newFlags: Int = 0

    if ((flags & NSEvent.ModifierFlags.control.rawValue) > 0) {
      newFlags |= controlKey
    }

    if ((flags & NSEvent.ModifierFlags.command.rawValue) > 0) {
      newFlags |= cmdKey
    }

    if ((flags & NSEvent.ModifierFlags.shift.rawValue) > 0) {
      newFlags |= shiftKey;
    }

    if ((flags & NSEvent.ModifierFlags.option.rawValue) > 0) {
      newFlags |= optionKey
    }

    if ((flags & NSEvent.ModifierFlags.capsLock.rawValue) > 0) {
      newFlags |= alphaLock
    }

    return UInt32(newFlags);
  }

  static func register() {
    var hotKeyRef: EventHotKeyRef?
    let modifierFlags: UInt32 =
      getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags.command)

    let keyCode = kVK_ANSI_R
    var gMyHotKeyID = EventHotKeyID()

    gMyHotKeyID.id = UInt32(keyCode)

    // Not sure what "swat" vs "htk1" do.
    gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
    // gMyHotKeyID.signature = OSType("htk1".fourCharCodeValue)

    var eventType = EventTypeSpec()
    eventType.eventClass = OSType(kEventClassKeyboard)
    eventType.eventKind = OSType(kEventHotKeyReleased)

    // Install handler.
    InstallEventHandler(GetApplicationEventTarget(), {
      (nextHanlder, theEvent, userData) -> OSStatus in
      // var hkCom = EventHotKeyID()

      // GetEventParameter(theEvent,
      //                   EventParamName(kEventParamDirectObject),
      //                   EventParamType(typeEventHotKeyID),
      //                   nil,
      //                   MemoryLayout<EventHotKeyID>.size,
      //                   nil,
      //                   &hkCom)

      NSLog("Command + R Released!")

      return noErr
      /// Check that hkCom in indeed your hotkey ID and handle it.
    }, 1, &eventType, nil, nil)

    // Register hotkey.
    let status = RegisterEventHotKey(UInt32(keyCode),
                                     modifierFlags,
                                     gMyHotKeyID,
                                     GetApplicationEventTarget(),
                                     0,
                                     &hotKeyRef)
    assert(status == noErr)
  }
}
Portraiture answered 3/10, 2019 at 19:3 Comment(6)
This still works great with Swift 5.7 unlike other solutions like github.com/soffes/HotKey. Unfortunately, there is a complete black hole when it comes to official documentation on this matter (global hotkeys). Thank you very much!Henriettahenriette
Could you please share usage example?Ennui
Awesome solution! @Ennui you can simply register it in applicationDidFinishLaunching for instance. HotkeySolution.register()Calender
how to make this work for cmd + tab, I have tested this for different characters its workingFerule
Can u show the use case of your answer? your codes mostly commented do we need those commented codes or what?Hoban
Needs import Cocoa at the topOverstuff
M
14

Since Swift 2.0, you can now pass a function pointer to C APIs.

var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
gMyHotKeyID.id = UInt32(keyCode)

var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyPressed)

// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {(nextHanlder, theEvent, userData) -> OSStatus in
    var hkCom = EventHotKeyID()
    GetEventParameter(theEvent, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, sizeof(EventHotKeyID), nil, &hkCom)

    // Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)

// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode), UInt32(modifierKeys), gMyHotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef)
Mitosis answered 18/1, 2016 at 21:30 Comment(6)
Does not work in Swift 4 and without a more detailed explanation it’s impossible to implement.Mauritamauritania
Works well in Swift 4, I have converted my projects to Swift 4 and it works fine. As a developer, you should know that "doesn't work" is the worst possible description of an issue - what does "doesn't work" mean? It doesn't compile? What compile error are you getting? There's really not much to explain - each function used is documented by Apple.Mitosis
Good vibes only, Charlie! It would be great if you could share your Swift 4 solution as well. The solution you posted is incomplete what makes it hard for beginners to adapt. One thing that would be helpful to say is that importing Carbon is necessary. The next problem I faced is that String does not have a .fourCharCodeValue property. Also it would be great if you could provide an example for keyCode and modifierKeys.Mauritamauritania
@Mauritamauritania - sorry if it sounded hostile - was not meant to be at all. fourCharCodeValue converts a string to Int - it was used a lot back in the day - 4 ASCII chars convert to 32-bit integer - it makes it easier to identify in dumps than a random integer. You can find my implementation here: github.com/charlieMonroe/XUCore/blob/master/XUCore/additions/… - I'm currently retrieving keyCode from SRRecorderControl: gist.github.com/charlieMonroe/0923985c704695b252ed9bb879e7f99dMitosis
I have done my homework and made this solution to work with Swift 5. See here.Portraiture
I got error of "Cannot find 'EventHotKeyID' in scope" do we need import some third party framework?Hoban
H
12

I don't believe you can do this in 100% Swift today. You'll need to call InstallEventHandler() or CGEventTapCreate(), and both of those require a CFunctionPointer, which can't be created in Swift. Your best plan is to use established ObjC solutions such as DDHotKey and bridge to Swift.

You can try using NSEvent.addGlobalMonitorForEventsMatchingMask(handler:), but that only makes copies of events. You can't consume them. That means the hotkey will also be passed along to the currently active app, which can cause problems. Here's an example, but I recommend the ObjC approach; it's almost certainly going to work better.

let keycode = UInt16(kVK_ANSI_X)
let keymask: NSEventModifierFlags = .CommandKeyMask | .AlternateKeyMask | .ControlKeyMask

func handler(event: NSEvent!) {
    if event.keyCode == self.keycode &&
        event.modifierFlags & self.keymask == self.keymask {
            println("PRESSED")
    }
}

// ... to set it up ...
    let options = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionaryRef
    let trusted = AXIsProcessTrustedWithOptions(options)
    if (trusted) {
        NSEvent.addGlobalMonitorForEventsMatchingMask(.KeyDownMask, handler: self.handler)
    }

This also requires that accessibility services be approved for this app. It also doesn't capture events that are sent to your own application, so you have to either capture them with your responder chain, our use addLocalMointorForEventsMatchingMask(handler:) to add a local handler.

Hargett answered 22/4, 2015 at 14:46 Comment(2)
While it's usable, it is important to remember that the user is granting your app the ability to monitor every single key stroke, which is potentially dangerous, mainly if your app is not sandboxed and can be injected. The old Carbon API will do this just for the user-defined combinations, making them a "safer" solution.Mitosis
InstallEventHandler() works very well in Swift (at least since Swift 3). I'm using a Swift version of DDHotKeyCenterBumbailiff
L
10

A quick Swift 3 update for the setup:

let opts = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionary

guard AXIsProcessTrustedWithOptions(opts) == true else { return }

NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: self.handler)
Langer answered 9/11, 2016 at 14:42 Comment(3)
Note: The Carbon-based Hotkey API does not need access privileges (can't be on the Mac App Store), although this sure is simpler to implement.Wheen
There are apps like CopyLess 2 that listen for keyDown and don't require accessibility, do you know something about them? Maybe the apps from store don't require it?Skippie
Can u show the use case of your answer?Hoban
H
7

I maintain this Swift package that makes it easy to both add global keyboard shortcuts to your app and also let the user set their own.

import SwiftUI
import KeyboardShortcuts

// Declare the shortcut for strongly-typed access.
extension KeyboardShortcuts.Name {
    static let toggleUnicornMode = Self("toggleUnicornMode")
}

@main
struct YourApp: App {
    @StateObject private var appState = AppState()

    var body: some Scene {
        WindowGroup {
            // …
        }
        Settings {
            SettingsScreen()
        }
    }
}

@MainActor
final class AppState: ObservableObject {
    init() {
        // Register the listener.
        KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) { [self] in
            isUnicornMode.toggle()
        }
    }
}

// Present a view where the user can set the shortcut they want.
struct SettingsScreen: View {
    var body: some View {
        Form {
            HStack(alignment: .firstTextBaseline) {
                Text("Toggle Unicorn Mode:")
                KeyboardShortcuts.Recorder(for: .toggleUnicornMode)
            }
        }
    }
}

SwiftUI is used in this example, but it also supports Cocoa.

Hueyhuff answered 22/1, 2022 at 19:4 Comment(0)
O
2

Take a look at the HotKey Library. You can simply use Carthage to implement it into your own app. HotKey Library

Ology answered 1/10, 2017 at 10:1 Comment(0)
B
2

there is a pretty hacky, but also pretty simple workaround if your app has a Menu:

  • add a new MenuItem (maybe call it something like "Dummy for Hotkey")
  • in the attributes inspector, conveniently enter your hotkey in the Key Equivalent field
  • set Allowed when Hidden, Enabled and Hidden to true
  • link it with an IBAction to do whatever your hotkey is supposed to do

done!

Bestow answered 6/7, 2019 at 16:21 Comment(1)
Interesting approach, but it does not work for apps that live only in the menu bar: for the keyboard shortcut to be detected, the app must be activated somehow, for instance by clicking on it in the menu bar.Thoughtless

© 2022 - 2024 — McMap. All rights reserved.