KeyboardShortcuts

repository·main·Indexed 25 days ago

https://github.com/sindresorhus/keyboardshortcuts

A Swift library for defining, recording, and listening for keyboard shortcuts in macOS applications. It provides a unified way to implement customizable global and in-app shortcuts using SwiftUI and AppKit, featuring a built-in recorder for user assignments, support for repeating key events on macOS 13+, and utilities for symbolic representation of modifier flags.

Tokens
2.7K
Snippets
9
Records
11
Agent score
33%

What's inside KeyboardShortcuts

  1. Migrate from Magnet to KeyboardShortcuts

    main

    When migrating from Magnet, note that Magnet fires on key-down by default. To maintain the same behavior in KeyboardShortcuts, filter your event loop for .keyDown.

    Value Migration

    If you persisted KeyCombo values manually, convert them using a helper that provides the carbon key code and modifiers.

    import KeyboardShortcuts
    
    func migrateLegacyShortcut(
    	newName: KeyboardShortcuts.Name,
    	readLegacyCarbonValue: () -> (Int, Int)?
    ) {
    	guard KeyboardShortcuts.getShortcut(for: newName) == nil else {
    		return
    	}
    
    	guard let (carbonKeyCode, carbonModifiers) = readLegacyCarbonValue() else {
    		return
    	}
    
    	KeyboardShortcuts.setShortcut(.init(carbonKeyCode: carbonKeyCode, carbonModifiers: carbonModifiers), for: newName)
    }
  2. Implement the KeyboardShortcuts pattern

    main

    To use KeyboardShortcuts, follow these three steps:

    1. Define a name: Extend KeyboardShortcuts.Name with a static property.
    2. Add a recorder: Use KeyboardShortcuts.Recorder or KeyboardShortcuts.RecorderCocoa in your SwiftUI settings view to allow users to set the shortcut.
    3. Listen for events: Use KeyboardShortcuts.events(for:) within a Task or async context to respond to key presses. Note that you can filter by eventType (e.g., .keyUp or .keyDown).
    import SwiftUI
    import KeyboardShortcuts
    
    // 1. Define a name
    extension KeyboardShortcuts.Name {
    	static let toggleMainWindow = Self("toggleMainWindow")
    }
    
    // 2. Add a recorder to your settings view
    struct SettingsView: View {
    	var body: some View {
    		KeyboardShortcuts.Recorder("Toggle Main Window:", name: .toggleMainWindow)
    	}
    }
    
    // 3. Listen for events (must be inside a Task or async context)
    Task {
    	for await eventType in KeyboardShortcuts.events(for: .toggleMainWindow) where eventType == .keyUp {
    		toggleMainWindow()
    	}
    }
  3. Migrate from ShortcutRecorder to KeyboardShortcuts

    main

    To migrate from ShortcutRecorder, replace your recorderControl.bind(...) logic with a SwiftUI KeyboardShortcuts.Recorder.

    Value Migration

    If your project stores archived shortcut values (common in ShortcutRecorder), keep your existing decoding logic to extract the carbon key code and modifiers, then use the migration pattern used for Magnet to save them into KeyboardShortcuts.

  4. Register and use keyboard shortcuts in SwiftUI

    main

    To implement customizable global shortcuts in SwiftUI, follow these three steps:

    1. Register a name: Extend KeyboardShortcuts.Name to create a unique identifier.
    2. Provide a UI for recording: Use KeyboardShortcuts.Recorder in your view to allow users to assign a shortcut. This automatically handles UserDefaults storage and system conflict warnings.
    3. Listen for the event: Use KeyboardShortcuts.onKeyUp(for:) or KeyboardShortcuts.onKeyDown(for:) to trigger actions.
    import SwiftUI
    import KeyboardShortcuts
    
    // 1. Register
    extension KeyboardShortcuts.Name {
        static let toggleUnicornMode = Self("toggleUnicornMode")
    }
    
    // 2. UI
    struct SettingsScreen: View {
        var body: some View {
            Form {
                KeyboardShortcuts.Recorder("Toggle Unicorn Mode:", name: .toggleUnicornMode)
            }
        }
    }
    
    // 3. Listen
    @MainActor
    @Observable
    final class AppState {
        init() {
            KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) {
                // Action here
            }
        }
    }
    import SwiftUI
    import KeyboardShortcuts
    
    extension KeyboardShortcuts.Name {
        static let toggleUnicornMode = Self("toggleUnicornMode")
    }
    
    struct SettingsScreen: View {
        var body: some View {
            Form {
                KeyboardShortcuts.Recorder("Toggle Unicorn Mode:", name: .toggleUnicornMode)
            }
        }
    }
    
    @MainActor
    @Observable
    final class AppState {
        init() {
            KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) {
                isUnicornMode.toggle()
            }
        }
    }
  5. Follow the recommended migration rollout sequence

    main

    To ensure a smooth transition for users when migrating from another hotkey package, follow this sequence:

    1. Check for existing values: Only perform migration if KeyboardShortcuts.getShortcut(for:) == nil to avoid overwriting current user preferences.
    2. Write new values: Use KeyboardShortcuts.setShortcut to save the migrated data.
    3. Cleanup storage: Remove the old stored value (e.g., from UserDefaults) only after a successful conversion.
    4. Remove dependency: Finally, remove the old hotkey package dependency from your project.
  6. Migrate from MASShortcut to KeyboardShortcuts

    main

    When migrating from MASShortcut, note that MASShortcut's binder fires on key-up. To maintain the same behavior in KeyboardShortcuts, filter your event loop for .keyUp.

    Value Migration

    If you stored MASShortcut objects in UserDefaults, you can migrate them to KeyboardShortcuts by extracting the keyCode and carbonFlags.

    import MASShortcut
    import KeyboardShortcuts
    
    func migrateMASShortcutValue(oldDefaultsKey: String, newName: KeyboardShortcuts.Name) {
    	guard
    		KeyboardShortcuts.getShortcut(for: newName) == nil,
    		let legacyShortcut = UserDefaults.standard.object(forKey: oldDefaultsKey) as? MASShortcut
    	else {
    		return
    	}
    
    	KeyboardShortcuts.setShortcut(
    		.init(
    			carbonKeyCode: Int(legacyShortcut.keyCode),
    			carbonModifiers: Int(legacyShortcut.carbonFlags)
    		),
    		for: newName
    	)
    
    	UserDefaults.standard.removeObject(forKey: oldDefaultsKey)
    }
  7. Implement in-app (non-global) keyboard shortcuts

    main

    To use the package for shortcuts that only work when your app is focused (not global), use KeyboardShortcuts.Recorder with a Binding<KeyboardShortcuts.Shortcut?>. You must manage the persistence (e.g., via @AppStorage) yourself.

    import SwiftUI
    import KeyboardShortcuts
    
    struct ContentView: View {
        @State private var shortcut: KeyboardShortcuts.Shortcut?
    
        var body: some View {
            VStack {
                KeyboardShortcuts.Recorder("Record shortcut", shortcut: $shortcut)
                Button("Perform Action") {
                    performAction()
                }
                .keyboardShortcut(shortcut?.toSwiftUI)
            }
        }
    }
  8. Listen to hard-coded global shortcuts

    main

    If you need to listen to a specific, non-user-customizable global shortcut, you can listen to a KeyboardShortcuts.Shortcut directly using KeyboardShortcuts.events(for:).

    import KeyboardShortcuts
    
    let shortcut = KeyboardShortcuts.Shortcut(.a, modifiers: [.command])
    
    Task {
        for await eventType in KeyboardShortcuts.events(for: shortcut) where eventType == .keyUp {
            // Do something.
        }
    }
  9. Get symbolic representation of modifier flags

    main

    You can convert NSEvent.ModifierFlags or a KeyboardShortcuts.Shortcut's modifiers into a symbolic string representation (e.g., "⇧⌘") using the ks_symbolicRepresentation property.

    import KeyboardShortcuts
    
    let modifiers = NSEvent.ModifierFlags([.command, .shift])
    print(modifiers.ks_symbolicRepresentation)
    //=> "⇧⌘"
    
    if let shortcut = KeyboardShortcuts.getShortcut(for: .toggleUnicornMode) {
        print(shortcut.modifiers.ks_symbolicRepresentation)
        //=> "⌘⌥"
    }
    import KeyboardShortcuts
    
    let modifiers = NSEvent.ModifierFlags([.command, .shift])
    print(modifiers.ks_symbolicRepresentation)
    //=> "⇧⌘"
    
    // Also works with shortcuts:
    if let shortcut = KeyboardShortcuts.getShortcut(for: .toggleUnicornMode) {
        print(shortcut.modifiers.ks_symbolicRepresentation)
        //=> "⌘⌥"
    }
  10. Handle repeating key events (macOS 13+)

    main

    To perform repeated actions while a shortcut is held down, use KeyboardShortcuts.repeatingKeyDownEvents(for:). This emits once on the initial press and then repeats according to the system's key repeat settings.

    import KeyboardShortcuts
    
    Task {
        for await _ in KeyboardShortcuts.repeatingKeyDownEvents(for: .moveSelectionDown) {
            // Move to the next item.
        }
    }
  11. Use KeyboardShortcuts.RecorderCocoa for AppKit/Cocoa

    main

    If you are using AppKit instead of SwiftUI, use KeyboardShortcuts.RecorderCocoa to provide a shortcut recording interface in your NSViewController.

    import AppKit
    import KeyboardShortcuts
    
    final class SettingsViewController: NSViewController {
        override func loadView() {
            view = NSView()
    
            let recorder = KeyboardShortcuts.RecorderCocoa(for: .toggleUnicornMode)
            view.addSubview(recorder)
        }
    }