Search code examples
swiftmacosswiftuikeyboard-shortcuts

Simulating global keyboard presses


I'm building a swiftUI app on mac and I was wondering if there was a way to simulate keyboard shortcuts while the app is running, or in the background. My goal with this app is to call on shortcuts when my server sends me the corresponding gesture information. I was looking at using NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: self.handler) but I think that was more reprograming shortcuts rather than just calling on them.

For example if my server sends me a string that says "Paste", how would I call on the command paste shorcut no matter what screen i'm on, as long as my app is running. Any help or information would be appreciated!


Solution

  • I had this playground sitting on my Mac, so I figured I'd share it here in case it's helpful:

    import Cocoa
    
    let src = CGEventSource(stateID: .hidSystemState)
    
    let down = CGEvent(keyboardEventSource: src, virtualKey: 0x12, keyDown: true)!
    let up = CGEvent(keyboardEventSource: src, virtualKey: 0x12, keyDown: false)!
    
    sleep(5)
    
    ////down?.post(tap: .cghidEventTap)
    ////up?.post(tap: .cghidEventTap)
    ////down?.flags = .maskCommand
    //
    //let cmd_d = CGEvent(keyboardEventSource: src, virtualKey: 0x37, keyDown: true)!
    //let cmd_u = CGEvent(keyboardEventSource: src, virtualKey: 0x37, keyDown: false)!
    //let a_d = CGEvent(keyboardEventSource: src, virtualKey: 0x00, keyDown: true)!
    //let a_u = CGEvent(keyboardEventSource: src, virtualKey: 0x00, keyDown: false)!
    //let delete_d = CGEvent(keyboardEventSource: src, virtualKey: 0x33, keyDown: true)!
    //let delete_u = CGEvent(keyboardEventSource: src, virtualKey: 0x33, keyDown: false)!
    //
    //down.post(tap: .cghidEventTap)
    //up.post(tap: .cghidEventTap)
    //sleep(1)
    ////cmd_d.post(tap: .cghidEventTap)
    //a_d.flags = .maskCommand
    //a_u.flags = .maskCommand
    //a_d.post(tap: .cghidEventTap)
    //a_u.post(tap: .cghidEventTap)
    ////cmd_u.post(tap: .cghidEventTap)
    //sleep(1)
    //delete_d.post(tap: .cghidEventTap)
    //delete_u.post(tap: .cghidEventTap)
    
    enum KeyCode: UInt16 {
        // https://gist.github.com/swillits/df648e87016772c7f7e5dbed2b345066
    
        // Layout-independent Keys
        // eg.These key codes are always the same key on all layouts.
        case returnKey = 0x24
    //    case enter = 0x4C //0x24
        case tab = 0x30
        case space = 0x31
        case delete = 0x33
        case escape = 0x35
        case command = 0x37
        case shift = 0x38
        case capsLock = 0x39
        case option = 0x3A
        case control = 0x3B
        case rightShift = 0x3C
        case rightOption = 0x3D
        case rightControl = 0x3E
        case leftArrow = 0x7B
        case rightArrow = 0x7C
        case downArrow = 0x7D
        case upArrow = 0x7E
        case volumeUp = 0x48
        case volumeDown = 0x49
        case mute = 0x4A
        case help = 0x72
        case home = 0x73
        case pageUp = 0x74
        case forwardDelete = 0x75
        case end = 0x77
        case pageDown = 0x79
        case function = 0x3F
        case f1 = 0x7A
        case f2 = 0x78
        case f4 = 0x76
        case f5 = 0x60
        case f6 = 0x61
        case f7 = 0x62
        case f3 = 0x63
        case f8 = 0x64
        case f9 = 0x65
        case f10 = 0x6D
        case f11 = 0x67
        case f12 = 0x6F
        case f13 = 0x69
        case f14 = 0x6B
        case f15 = 0x71
        case f16 = 0x6A
        case f17 = 0x40
        case f18 = 0x4F
        case f19 = 0x50
        case f20 = 0x5A
    
        // US-ANSI Keyboard Positions
        // eg. These key codes are for the physical key (in any keyboard layout)
        // at the location of the named key in the US-ANSI layout.
        case a = 0x00
        case b = 0x0B
        case c = 0x08
        case d = 0x02
        case e = 0x0E
        case f = 0x03
        case g = 0x05
        case h = 0x04
        case i = 0x22
        case j = 0x26
        case k = 0x28
        case l = 0x25
        case m = 0x2E
        case n = 0x2D
        case o = 0x1F
        case p = 0x23
        case q = 0x0C
        case r = 0x0F
        case s = 0x01
        case t = 0x11
        case u = 0x20
        case v = 0x09
        case w = 0x0D
        case x = 0x07
        case y = 0x10
        case z = 0x06
    
        case zero = 0x1D
        case one = 0x12
        case two = 0x13
        case three = 0x14
        case four = 0x15
        case five = 0x17
        case six = 0x16
        case seven = 0x1A
        case eight = 0x1C
        case nine = 0x19
    
        case equals = 0x18
        case minus = 0x1B
        case semicolon = 0x29
        case apostrophe = 0x27
        case comma = 0x2B
        case period = 0x2F
        case forwardSlash = 0x2C
        case backslash = 0x2A
        case grave = 0x32
        case leftBracket = 0x21
        case rightBracket = 0x1E
    
        case keypadDecimal = 0x41
        case keypadMultiply = 0x43
        case keypadPlus = 0x45
        case keypadClear = 0x47
        case keypadDivide = 0x4B
        case keypadEnter = 0x4C
        case keypadMinus = 0x4E
        case keypadEquals = 0x51
        case keypad0 = 0x52
        case keypad1 = 0x53
        case keypad2 = 0x54
        case keypad3 = 0x55
        case keypad4 = 0x56
        case keypad5 = 0x57
        case keypad6 = 0x58
        case keypad7 = 0x59
        case keypad8 = 0x5B
        case keypad9 = 0x5C
    }
    func press(_ key: KeyCode, withModifiers modifiers: CGEventFlags = .init()) {
        let down = CGEvent(keyboardEventSource: src, virtualKey: key.rawValue, keyDown: true)!
        let up = CGEvent(keyboardEventSource: src, virtualKey: key.rawValue, keyDown: false)!
        down.flags = modifiers
        up.flags = modifiers
        down.post(tap: .cghidEventTap)
        up.post(tap: .cghidEventTap)
    }
    struct KeyPress {
        let key: KeyCode
        let modifiers: CGEventFlags
    }
    func press(_ key: KeyPress) {
        press(key.key, withModifiers: key.modifiers)
    }
    /// An enum representing possible typing rates.
    ///
    /// - allAtOnce: All the text should be typed at once.
    /// - consistent: The text should be typed at a reasonable speed, but with no variance in delay.
    /// - natural: The text should be typed so that it appears natural.
    /// - customConsistent: The text should be typed at a consistent speed, specified by the associated value.
    /// - customVarying: The text should be typed around a given speed, with 5 possible ranges of variation. Both the base speed and the maximum variance are specified by associated values.
    public enum Rate {
        /// All the text should be typed at once.
        case allAtOnce
        /// The text should be typed at a reasonable speed, but with no variance in delay.
        case consistent
        /// The text should be typed so that it appears natural.
        case natural
        /// The text should be typed at a specified consistent speed.
        /// - µsecondDelay: The delay between each key typed.
        case customConsistent(µsecondDelay: UInt32)
        /// The text should be typed around a specified given speed, with specified variation. The base delay should be the average delay time, and the max variance is the maximum distance from the average to the fastest/slowest possible delay.
        /// - µsecondBaseDelay: The base delay between each key typed.
        /// - maxVariance: The delay between each key typed.
        case customVarying(µsecondBaseDelay: UInt32, maxVariance: UInt32)
    }
    
    func type(_ text: [KeyPress], typing: Rate = .natural) {
        for character in text {
            press(character)
            switch typing {
            case .allAtOnce:
                usleep(0001000)
            case .consistent:
                usleep(0100000)
            case .natural:
                var sleepTime = UInt32.random(in: 0...4)
                sleepTime *= 10000
                usleep(0080000 + sleepTime)
            case .customConsistent(let µsecondDelay):
                usleep(µsecondDelay)
            case let .customVarying(µsecondBaseDelay, maxVariance):
                var sleepTime = UInt32.random(in: 0...4)
                let base = µsecondBaseDelay - maxVariance
                sleepTime *= (maxVariance / 2)
                let µsecondDelay = base + sleepTime
                usleep(µsecondDelay)
            }
        }
    }
    let lowercaseCharMap: [Character: KeyCode] = [
        "a": .a,
        "b": .b,
        "c": .c,
        "d": .d,
        "e": .e,
        "f": .f,
        "g": .g,
        "h": .h,
        "i": .i,
        "j": .j,
        "k": .k,
        "l": .l,
        "m": .m,
        "n": .n,
        "o": .o,
        "p": .p,
        "q": .q,
        "r": .r,
        "s": .s,
        "t": .t,
        "u": .u,
        "v": .v,
        "w": .w,
        "x": .x,
        "y": .y,
        "z": .z,
        "0": .zero,
        "1": .one,
        "2": .two,
        "3": .three,
        "4": .four,
        "5": .five,
        "6": .six,
        "7": .seven,
        "8": .eight,
        "9": .nine,
        "=": .equals,
        "-": .minus,
        ";": .semicolon,
        "'": .apostrophe,
        ",": .comma,
        ".": .period,
        "/": .forwardSlash,
        "\\": .backslash,
        "`": .grave,
        "[": .leftBracket,
        "]": .rightBracket,
        " ": .space
    ]
    let uppercaseCharMap: [Character: KeyCode] = [
        "A": .a,
        "B": .b,
        "C": .c,
        "D": .d,
        "E": .e,
        "F": .f,
        "G": .g,
        "H": .h,
        "I": .i,
        "J": .j,
        "K": .k,
        "L": .l,
        "M": .m,
        "N": .n,
        "O": .o,
        "P": .p,
        "Q": .q,
        "R": .r,
        "S": .s,
        "T": .t,
        "U": .u,
        "V": .v,
        "W": .w,
        "X": .x,
        "Y": .y,
        "Z": .z,
        ")": .zero,
        "!": .one,
        "@": .two,
        "#": .three,
        "$": .four,
        "%": .five,
        "^": .six,
        "&": .seven,
        "*": .eight,
        "(": .nine,
        "+": .equals,
        "_": .minus,
        ":": .semicolon,
        "\"": .apostrophe,
        "<": .comma,
        ">": .period,
        "?": .forwardSlash,
        "|": .backslash,
        "~": .grave,
        "{": .leftBracket,
        "}": .rightBracket,
    ]
    
    func type(_ text: String, typing: Rate = .natural) {
        type(str_to_kparr(text), typing: typing)
    }
    
    type("Hello, there")
    
    func str_to_kparr(_ str: String) -> [KeyPress] {
        str.map { char -> KeyPress in
            if let kc = lowercaseCharMap[char] {
                return KeyPress(key: kc, modifiers: .init())
            }
            if let kc = uppercaseCharMap[char] {
                return KeyPress(key: kc, modifiers: .maskShift)
            }
            return KeyPress(key: .three, modifiers: .maskShift)
        }
    }
    
    func +(lhs: String, rhs: [KeyPress]) -> [KeyPress] {
        return str_to_kparr(lhs) + rhs
    }
    func +(lhs: [KeyPress], rhs: String) -> [KeyPress] {
        return lhs + str_to_kparr(rhs)
    }
    
    type("! General Kenobi" + [.init(key: .a, modifiers: .maskCommand), .init(key: .delete, modifiers: .init()), .init(key: .space, modifiers: [.maskCommand])])
    
    extension KeyPress {
        static let returnKey = KeyPress(key: .returnKey, modifiers: .init())
        static let enter = Self.returnKey
        static let tab = KeyPress(key: .tab, modifiers: .init())
        static let space = KeyPress(key: .space, modifiers: .init())
        static let delete = KeyPress(key: .delete, modifiers: .init())
        static let escape = KeyPress(key: .escape, modifiers: .init())
        static let command = KeyPress(key: .command, modifiers: .init())
        static let shift = KeyPress(key: .shift, modifiers: .init())
        static let capsLock = KeyPress(key: .capsLock, modifiers: .init())
        static let option = KeyPress(key: .option, modifiers: .init())
        static let control = KeyPress(key: .control, modifiers: .init())
        static let rightShift = KeyPress(key: .rightShift, modifiers: .init())
        static let rightOption = KeyPress(key: .rightOption, modifiers: .init())
        static let rightControl = KeyPress(key: .rightControl, modifiers: .init())
        static let leftArrow = KeyPress(key: .leftArrow, modifiers: .init())
        static let rightArrow = KeyPress(key: .rightArrow, modifiers: .init())
        static let downArrow = KeyPress(key: .downArrow, modifiers: .init())
        static let upArrow = KeyPress(key: .upArrow, modifiers: .init())
        static let volumeUp = KeyPress(key: .volumeUp, modifiers: .init())
        static let volumeDown = KeyPress(key: .volumeDown, modifiers: .init())
        static let mute = KeyPress(key: .mute, modifiers: .init())
        static let help = KeyPress(key: .help, modifiers: .init())
        static let home = KeyPress(key: .home, modifiers: .init())
        static let pageUp = KeyPress(key: .pageUp, modifiers: .init())
        static let forwardDelete = KeyPress(key: .forwardDelete, modifiers: .init())
        static let end = KeyPress(key: .end, modifiers: .init())
        static let pageDown = KeyPress(key: .pageDown, modifiers: .init())
        static let function = KeyPress(key: .function, modifiers: .init())
        static let f1 = KeyPress(key: .f1, modifiers: .init())
        static let f2 = KeyPress(key: .f2, modifiers: .init())
        static let f4 = KeyPress(key: .f4, modifiers: .init())
        static let f5 = KeyPress(key: .f5, modifiers: .init())
        static let f6 = KeyPress(key: .f6, modifiers: .init())
        static let f7 = KeyPress(key: .f7, modifiers: .init())
        static let f3 = KeyPress(key: .f3, modifiers: .init())
        static let f8 = KeyPress(key: .f8, modifiers: .init())
        static let f9 = KeyPress(key: .f9, modifiers: .init())
        static let f10 = KeyPress(key: .f10, modifiers: .init())
        static let f11 = KeyPress(key: .f11, modifiers: .init())
        static let f12 = KeyPress(key: .f12, modifiers: .init())
        static let f13 = KeyPress(key: .f13, modifiers: .init())
        static let f14 = KeyPress(key: .f14, modifiers: .init())
        static let f15 = KeyPress(key: .f15, modifiers: .init())
        static let f16 = KeyPress(key: .f16, modifiers: .init())
        static let f17 = KeyPress(key: .f17, modifiers: .init())
        static let f18 = KeyPress(key: .f18, modifiers: .init())
        static let f19 = KeyPress(key: .f19, modifiers: .init())
        static let f20 = KeyPress(key: .f20, modifiers: .init())
    
            // US-ANSI Keyboard Positions
            // eg. These key codes are for the physical key (in any keyboard layout)
            // at the location of the named key in the US-ANSI layout.
        static let a = KeyPress(key: .a, modifiers: .init())
        static let b = KeyPress(key: .b, modifiers: .init())
        static let c = KeyPress(key: .c, modifiers: .init())
        static let d = KeyPress(key: .d, modifiers: .init())
        static let e = KeyPress(key: .e, modifiers: .init())
        static let f = KeyPress(key: .f, modifiers: .init())
        static let g = KeyPress(key: .g, modifiers: .init())
        static let h = KeyPress(key: .h, modifiers: .init())
        static let i = KeyPress(key: .i, modifiers: .init())
        static let j = KeyPress(key: .j, modifiers: .init())
        static let k = KeyPress(key: .k, modifiers: .init())
        static let l = KeyPress(key: .l, modifiers: .init())
        static let m = KeyPress(key: .m, modifiers: .init())
        static let n = KeyPress(key: .n, modifiers: .init())
        static let o = KeyPress(key: .o, modifiers: .init())
        static let p = KeyPress(key: .p, modifiers: .init())
        static let q = KeyPress(key: .q, modifiers: .init())
        static let r = KeyPress(key: .r, modifiers: .init())
        static let s = KeyPress(key: .s, modifiers: .init())
        static let t = KeyPress(key: .t, modifiers: .init())
        static let u = KeyPress(key: .u, modifiers: .init())
        static let v = KeyPress(key: .v, modifiers: .init())
        static let w = KeyPress(key: .w, modifiers: .init())
        static let x = KeyPress(key: .x, modifiers: .init())
        static let y = KeyPress(key: .y, modifiers: .init())
        static let z = KeyPress(key: .z, modifiers: .init())
        static let A = KeyPress(key: .a, modifiers: .maskShift)
        static let B = KeyPress(key: .b, modifiers: .maskShift)
        static let C = KeyPress(key: .c, modifiers: .maskShift)
        static let D = KeyPress(key: .d, modifiers: .maskShift)
        static let E = KeyPress(key: .e, modifiers: .maskShift)
        static let F = KeyPress(key: .f, modifiers: .maskShift)
        static let G = KeyPress(key: .g, modifiers: .maskShift)
        static let H = KeyPress(key: .h, modifiers: .maskShift)
        static let I = KeyPress(key: .i, modifiers: .maskShift)
        static let J = KeyPress(key: .j, modifiers: .maskShift)
        static let K = KeyPress(key: .k, modifiers: .maskShift)
        static let L = KeyPress(key: .l, modifiers: .maskShift)
        static let M = KeyPress(key: .m, modifiers: .maskShift)
        static let N = KeyPress(key: .n, modifiers: .maskShift)
        static let O = KeyPress(key: .o, modifiers: .maskShift)
        static let P = KeyPress(key: .p, modifiers: .maskShift)
        static let Q = KeyPress(key: .q, modifiers: .maskShift)
        static let R = KeyPress(key: .r, modifiers: .maskShift)
        static let S = KeyPress(key: .s, modifiers: .maskShift)
        static let T = KeyPress(key: .t, modifiers: .maskShift)
        static let U = KeyPress(key: .u, modifiers: .maskShift)
        static let V = KeyPress(key: .v, modifiers: .maskShift)
        static let W = KeyPress(key: .w, modifiers: .maskShift)
        static let X = KeyPress(key: .x, modifiers: .maskShift)
        static let Y = KeyPress(key: .y, modifiers: .maskShift)
        static let Z = KeyPress(key: .z, modifiers: .maskShift)
    
        static let zero = KeyPress(key: .zero, modifiers: .init())
        static let one = KeyPress(key: .one, modifiers: .init())
        static let two = KeyPress(key: .two, modifiers: .init())
        static let three = KeyPress(key: .three, modifiers: .init())
        static let four = KeyPress(key: .four, modifiers: .init())
        static let five = KeyPress(key: .five, modifiers: .init())
        static let six = KeyPress(key: .six, modifiers: .init())
        static let seven = KeyPress(key: .seven, modifiers: .init())
        static let eight = KeyPress(key: .eight, modifiers: .init())
        static let nine = KeyPress(key: .nine, modifiers: .init())
        static let leftParenthesis = KeyPress(key: .zero, modifiers: .maskShift)
        static let exclamationPoint = KeyPress(key: .one, modifiers: .maskShift)
        static let atSign = KeyPress(key: .two, modifiers: .maskShift)
        static let numberSign = KeyPress(key: .three, modifiers: .maskShift)
        static let dollarSign = KeyPress(key: .four, modifiers: .maskShift)
        static let percent = KeyPress(key: .five, modifiers: .maskShift)
        static let caret = KeyPress(key: .six, modifiers: .maskShift)
        static let ampersand = KeyPress(key: .seven, modifiers: .maskShift)
        static let asterisk = KeyPress(key: .eight, modifiers: .maskShift)
        static let rightParenthesis = KeyPress(key: .nine, modifiers: .maskShift)
    
        static let equals = KeyPress(key: .equals, modifiers: .init())
        static let minus = KeyPress(key: .minus, modifiers: .init())
        static let semicolon = KeyPress(key: .semicolon, modifiers: .init())
        static let apostrophe = KeyPress(key: .apostrophe, modifiers: .init())
        static let comma = KeyPress(key: .comma, modifiers: .init())
        static let period = KeyPress(key: .period, modifiers: .init())
        static let forwardSlash = KeyPress(key: .forwardSlash, modifiers: .init())
        static let backslash = KeyPress(key: .backslash, modifiers: .init())
        static let grave = KeyPress(key: .grave, modifiers: .init())
        static let leftBracket = KeyPress(key: .leftBracket, modifiers: .init())
        static let rightBracket = KeyPress(key: .rightBracket, modifiers: .init())
        static let plus = KeyPress(key: .equals, modifiers: .maskShift)
        static let underscore = KeyPress(key: .minus, modifiers: .maskShift)
        static let colon = KeyPress(key: .semicolon, modifiers: .maskShift)
        static let quotationMark = KeyPress(key: .apostrophe, modifiers: .maskShift)
        static let lessThan = KeyPress(key: .comma, modifiers: .maskShift)
        static let greaterThan = KeyPress(key: .period, modifiers: .maskShift)
        static let questionMark = KeyPress(key: .forwardSlash, modifiers: .maskShift)
        static let pipe = KeyPress(key: .backslash, modifiers: .maskShift)
        static let tilde = KeyPress(key: .grave, modifiers: .maskShift)
        static let leftBrace = KeyPress(key: .leftBracket, modifiers: .maskShift)
        static let rightBrace = KeyPress(key: .rightBracket, modifiers: .maskShift)
    
        static let keypadDecimal = KeyPress(key: .keypadDecimal, modifiers: .init())
        static let keypadMultiply = KeyPress(key: .keypadMultiply, modifiers: .init())
        static let keypadPlus = KeyPress(key: .keypadPlus, modifiers: .init())
        static let keypadClear = KeyPress(key: .keypadClear, modifiers: .init())
        static let keypadDivide = KeyPress(key: .keypadDivide, modifiers: .init())
        static let keypadEnter = KeyPress(key: .keypadEnter, modifiers: .init())
        static let keypadMinus = KeyPress(key: .keypadMinus, modifiers: .init())
        static let keypadEquals = KeyPress(key: .keypadEquals, modifiers: .init())
        static let keypad0 = KeyPress(key: .keypad0, modifiers: .init())
        static let keypad1 = KeyPress(key: .keypad1, modifiers: .init())
        static let keypad2 = KeyPress(key: .keypad2, modifiers: .init())
        static let keypad3 = KeyPress(key: .keypad3, modifiers: .init())
        static let keypad4 = KeyPress(key: .keypad4, modifiers: .init())
        static let keypad5 = KeyPress(key: .keypad5, modifiers: .init())
        static let keypad6 = KeyPress(key: .keypad6, modifiers: .init())
        static let keypad7 = KeyPress(key: .keypad7, modifiers: .init())
        static let keypad8 = KeyPress(key: .keypad8, modifiers: .init())
        static let keypad9 = KeyPress(key: .keypad9, modifiers: .init())
    }
    
    extension KeyPress {
        func withModifiers(_ modifiers: CGEventFlags) -> KeyPress {
            return KeyPress(key: self.key, modifiers: modifiers)
        }
        func appendingModifiers(_ modifiers: CGEventFlags) -> KeyPress {
            return self.withModifiers(self.modifiers.union(modifiers))
        }
    }
    
    sleep(2)
    type([KeyPress.space.withModifiers([.maskCommand])] + "My name is Inigo Montoya. You killed my father. Prepare to die" + [KeyPress.space.withModifiers([.maskCommand]), KeyPress.tab.withModifiers(.maskCommand), .command])
    
    func +(lhs: String, rhs: KeyPress) -> [KeyPress] {
        return lhs + [rhs]
    }
    func +(lhs: KeyPress, rhs: String) -> [KeyPress] {
        return [lhs] + rhs
    }
    
    type([KeyPress.tab.withModifiers(.maskCommand), .command, KeyPress.space.withModifiers([.maskCommand])] + "")
    

    A few notes:

    • I don't know if CGEventSource(stateID: .hidSystemState) is right, or if it should be a different state. Check the documentation.
    • Same for down.post(tap: .cghidEventTap) (docs)
    • Note that you need to post a keyDown: true event and a keydown: false event.
    • Obviously you can't type special characters like , so this code prints # instead
    • Otherwise, you can pass a string to type(_:typing:) and it will type it out.
    • Look through this code before you run it — it's designed to run with Spotlight bound to command-space (otherwise it might type somewhere important) and will type a # at the end in your second open application
    • The typing rate enum is from my Typer library, which uses AppleScript to type (another option for you), but does not support modifiers.

    You could also use AppleScript to type, if you're only on a Mac. I believe this would do it:

    tell app "System Events" to keystroke "v" using command down
    tell app "System Events" to keystroke "v" using {shift down, command down}
    

    Take a look at my Typer library to see how I compile and run AppleScript.