[Swift] Macで文字列をキー入力したい

# はじめに

キーボードのキー入力イベントを使う場合、一つ一つのキーのダウン/アップイベントを起こす必要があり面倒なので、文字列で簡単に指定できる関数を作成した。

_MacでJPキーボード（JIS（日本語）キーボード）を使用していることが前提です。他のキーボードでは結果が異なるので注意してください。_

https://support.apple.com/ja-jp/guide/mac-help/mchlp2886/mac

# キーボードのキー入力イベント

なにが面倒なのか？


```swift:キー入力イベント
fileprivate func doKey(_ key: CGKeyCode, keyDown: Bool, modifierFlags: CGEventFlags) {
    let source = CGEventSource(stateID: .hidSystemState)
    let event = CGEvent(keyboardEventSource: source, virtualKey: key, keyDown: keyDown)
    event?.flags = modifierFlags
    event?.post(tap: .cghidEventTap)
}
func keyDown(_ key: CGKeyCode, modifierFlags: CGEventFlags = []) {
    doKey(key, keyDown: true, modifierFlags: modifierFlags)
}
func keyUp(_ key: CGKeyCode, modifierFlags: CGEventFlags = []) {
    doKey(key, keyDown: false, modifierFlags: modifierFlags)
}
func keyDownAndUp(_ key: CGKeyCode, modifierFlags: CGEventFlags = []) {
    keyDown(key, modifierFlags: modifierFlags)
    keyUp(key, modifierFlags: modifierFlags)
}
```

上のコードを見ていただければ分かると思うが、プログラムからキー入力するには、一つ一つのキーのダウン/アップイベントをポストする必要がある。
例えば、"Hello!"とキー入力するためには、以下のような使い方になる。

```swift:"Hello!"をキー入力する
keyDownAndUp( 4, modifierFlags: [.maskShift])  //H (shift+h)
keyDownAndUp(14)                               //e
keyDownAndUp(37)                               //l
keyDownAndUp(37)                               //l
keyDownAndUp(31)                               //o
keyDownAndUp(18, modifierFlags: [.maskShift])  //! (shift+1)
```

アルファベットや数字だけなら、シフトキーを使って大文字にするかどうかぐらいの制御のためまだ簡単である。面倒なのが記号文字であり、多くの記号文字はUSキーボードとJPキーボードでキーの配置が異なるため、指定するキーコードが分かりにくい。
例を示すと、`=`（イコール）をキー入力する場合、以下のコードでは`^`（ハット）が入力されるのだ。なぜなら、USキーボードの`=`の位置のキーは、JPキーボードでは`^`だからである。

```swift:USキーボード"="
let kVK_ANSI_Equal: CGKeyCode = 0x18 //=
keyDownAndUp(kVK_ANSI_Equal, modifierFlags: []) //^
```

では、JPキーボードで`@`をキー入力するにはどうすれば良いか？
答えは、JPキーボードで`@`の位置にあるUSキーボードのキーコードを指定する必要があり、それは`[`だ。

```swift:JPキーボード"@"
let kVK_ANSI_LeftBracket: CGKeyCode = 0x21 //[
keyDownAndUp(kVK_ANSI_LeftBracket, modifierFlags: []) //@
```

このように文字ごとにキーの位置とシフトキーの有無を確認する必要があって、これが面倒である。

そこで、文字列で簡単にキー入力できる関数に仕立てる。

# 文字列で簡単にキー入力を行う

イメージは↓こんな感じ。

```swift
sendKeys("Hello!")
```

これを実現するためには、Asciiコード（印字可能文字）の変換テーブルを準備する必要がある。

```swift
let asciiMap: [(keycode: CGKeyCode, flag: UInt16)] = [
    (0,      1),     // NUL  0 0x00
    (0,      1),     // SOH  1 0x01
    (0,      1),     // STX  2 0x02
    (0,      1),     // ETX  3 0x03
    (0,      1),     // EOT  4 0x04
    (0,      1),     // ENQ  5 0x05
    (0,      1),     // ACK  6 0x06
    (0,      1),     // BEL  7 0x07
    (0x33,   0),     // BS   8 0x08
    (0x30,   0),     // TAB  9 0x09
    (0x24,   0),     // LF  10 0x0A
    (0,      1),     // VT  11 0x0B
    (0,      1),     // FF  12 0x0C
    (0,      1),     // CR  13 0x0D
    (0,      1),     // SO  14 0x0E
    (0,      1),     // SI  15 0x0F
    (0,      1),     // DLE 16 0x10
    (0,      1),     // DC1 17 0x11
    (0,      1),     // DC2 18 0x12
    (0,      1),     // DC3 19 0x13
    (0,      1),     // DC4 20 0x14
    (0,      1),     // NAK 21 0x15
    (0,      1),     // SYN 22 0x16
    (0,      1),     // ETB 23 0x17
    (0,      1),     // CAN 24 0x18
    (0,      1),     // EM  25 0x19
    (0,      1),     // SUB 26 0x1A
    (0x35,   0),     // ESC 27 0x1B
    (0,      1),     // FS  28 0x1C
    (0,      1),     // GS  29 0x1D
    (0,      1),     // RS  30 0x1E
    (0,      1),     // US  31 0x1F
    (0x31,   0),     // Space 32 0x20
    (0x12, 128),     // !  33 0x21
    (0x13, 128),     // "  34 0x22
    (0x14, 128),     // #  35 0x23
    (0x15, 128),     // $  36 0x24
    (0x17, 128),     // %  37 0x25
    (0x16, 128),     // &  38 0x26
    (0x1A, 128),     // '  39 0x27
    (0x1C, 128),     // (  40 0x28
    (0x19, 128),     // )  41 0x29
    (0x27, 128),     // *  42 0x2A
    (0x29, 128),     // +  43 0x2B
    (0x2B,   0),     // ,  44 0x2C
    (0x1B,   0),     // -  45 0x2D
    (0x2F,   0),     // .  46 0x2E
    (0x2C,   0),     // /  47 0x2F
    (0x1D,   0),     // 0  48 0x30
    (0x12,   0),     // 1  49 0x31
    (0x13,   0),     // 2  50 0x32
    (0x14,   0),     // 3  51 0x33
    (0x15,   0),     // 4  52 0x34
    (0x17,   0),     // 5  53 0x35
    (0x16,   0),     // 6  54 0x36
    (0x1A,   0),     // 7  55 0x37
    (0x1C,   0),     // 8  56 0x38
    (0x19,   0),     // 9  57 0x39
    (0x27,   0),     // :  58 0x3A
    (0x29,   0),     // ;  59 0x3B
    (0x2B, 128),     // <  60 0x3C
    (0x1B, 128),     // =  61 0x3D
    (0x2F, 128),     // >  62 0x3E
    (0x2C, 128),     // ?  63 0x3F
    (0x21,   0),     // @  64 0x40
    (0x00, 128),     // A  65 0x41
    (0x0B, 128),     // B  66 0x42
    (0x08, 128),     // C  67 0x43
    (0x02, 128),     // D  68 0x44
    (0x0E, 128),     // E  69 0x45
    (0x03, 128),     // F  70 0x46
    (0x05, 128),     // G  71 0x47
    (0x04, 128),     // H  72 0x48
    (0x22, 128),     // I  73 0x49
    (0x26, 128),     // J  74 0x4A
    (0x28, 128),     // K  75 0x4B
    (0x25, 128),     // L  76 0x4C
    (0x2E, 128),     // M  77 0x4D
    (0x2D, 128),     // N  78 0x4E
    (0x1F, 128),     // O  79 0x4F
    (0x23, 128),     // P  80 0x50
    (0x0C, 128),     // Q  81 0x51
    (0x0F, 128),     // R  82 0x52
    (0x01, 128),     // S  83 0x53
    (0x11, 128),     // T  84 0x54
    (0x20, 128),     // U  85 0x55
    (0x09, 128),     // V  86 0x56
    (0x0D, 128),     // W  87 0x57
    (0x07, 128),     // X  88 0x58
    (0x10, 128),     // Y  89 0x59
    (0x06, 128),     // Z  90 0x5A
    (0x1E,   0),     // [  91 0x5B
    (0x5D,   0),     // \  92 0x5C
    (0x2A,   0),     // ]  93 0x5D
    (0x18,   0),     // ^  94 0x5E
    (0x5E, 128),     // _  95 0x5F
    (0x21, 128),     // `  96 0x60
    (0x00,   0),     // a  97 0x61
    (0x0B,   0),     // b  98 0x62
    (0x08,   0),     // c  99 0x63
    (0x02,   0),     // d 100 0x64
    (0x0E,   0),     // e 101 0x65
    (0x03,   0),     // f 102 0x66
    (0x05,   0),     // g 103 0x67
    (0x04,   0),     // h 104 0x68
    (0x22,   0),     // i 105 0x69
    (0x26,   0),     // j 106 0x6A
    (0x28,   0),     // k 107 0x6B
    (0x25,   0),     // l 108 0x6C
    (0x2E,   0),     // m 109 0x6D
    (0x2D,   0),     // n 110 0x6E
    (0x1F,   0),     // o 111 0x6F
    (0x23,   0),     // p 112 0x70
    (0x0C,   0),     // q 113 0x71
    (0x0F,   0),     // r 114 0x72
    (0x01,   0),     // s 115 0x73
    (0x11,   0),     // t 116 0x74
    (0x20,   0),     // u 117 0x75
    (0x09,   0),     // v 118 0x76
    (0x0D,   0),     // w 119 0x77
    (0x07,   0),     // x 120 0x78
    (0x10,   0),     // y 121 0x79
    (0x06,   0),     // z 122 0x7A
    (0x1E, 128),     // { 123 0x7B
    (0x5D, 128),     // | 124 0x7C
    (0x2A, 128),     // } 125 0x7D
    (0x18, 128),     // ~ 126 0x7E
    (0,      1),     // DEL 127 0x7F
]
```

左側の数値はJPキーボードに対応したキーコード。右側の数値はShiftキーが必要なら128、不要なら0。非印字文字は1としてある。非印字文字はキー入力できないのでエラーとする。
引数で受け取った文字列を1文字づつこのテーブルで変換しながらキー入力関数に渡すのだ。

```swift
func sendKeys(_ string: String) {
    for ch in string {
        if ch.isASCII, let asciiCode = ch.asciiValue {
            precondition(asciiCode < 0x80, String(format: "invalid ascii code (0x%x)", asciiCode))

            let (keycode, flag) = asciiMap[Int(asciiCode)]
            precondition(flag != 1, String(format: "unprintable ascii code (0x%x)", asciiCode))

            let withShift: CGEventFlags = (flag & 0x80 != 0) ? [.maskShift]  : []
            keyDownAndUp(keycode, modifierFlags: withShift)
        } else {
            precondition(false, String(format: "unsupported unicode (%s)", String(ch.unicodeScalars.map({String(format:"0x%04x ", Int($0.value))}).joined().dropLast(1)) ))
        }
    }
}
```

使い方は簡単で文字列を渡すだけ。最後の`\n`で`enter`が入力される（改行される）。

```swift
sendKeys("Hello!\n")
```

しかし、ただこれだけだと能がないので、`⌘F`のようなキーシーケンスも処理できるように拡張する。

# 文字列の拡張

使用できる修飾キーは次の4つとする。これを文字列に含めることで、例えば`⌘F`のようなキーシーケンスも処理できるようにする。

- ⌘ ：commandキー
- ⌃ ：controlキー
- ⌥ ：optionキー
- ⇧ ：shiftキー

```swift:検索"⌘F"
sendKeys("⌘F")  // or "⌘f"
```

もし`command+shift+F`としたければ、明示的にshiftの修飾キーを指定する必要がある。

```swift:検索"command+shift+F"
sendKeys("⌘⇧F")
```

XcodeのRefactor-Renameの`⇧⌥⌘R`（shift+option+command+R）も、このまま引数に指定するだけ。
リネーム対象を選択しておいて`⇧⌥⌘R`を入力すると、リネームのダイアログが開く。

```swift:XcodeのRefactor-Rename
sendKeys("⇧⌥⌘R")
```


拡張したsendKeysのコードを以下に示す。

```swift:拡張版sendKeys
func sendKeys(_ string: String) {
    var controlKeys = false
    var keycodes = [UInt16]()
    var modifierFlags: CGEventFlags = []
    for ch in string {
        if ch.isASCII, let asciiCode = ch.asciiValue {
            precondition(asciiCode < 0x80, String(format: "invalid ascii code (0x%x)", asciiCode))

            let (keycode, flag) = asciiMap[Int(asciiCode)]
            precondition(flag != 1, String(format: "unprintable ascii code (0x%x)", asciiCode))

            var withShift = modifierFlags
            if !(controlKeys && (0x41 ... 0x5A ~= asciiCode)) {
                if flag & 0x80 != 0 {
                    withShift.insert(.maskShift)
                } else {
                    withShift.remove(.maskShift)
                }
            }
            if !controlKeys {
                keyDownAndUp(keycode, modifierFlags: withShift)
            } else {
                keycodes.append(keycode)
                keyDown(keycode, modifierFlags: withShift)
            }
        } else {
            controlKeys = true
            precondition(ch.unicodeScalars.count == 1, String(format: "unkown unicode (%s)", String(ch.unicodeScalars.map({String(format:"0x%04x ", Int($0.value))}).joined().dropLast(1)) ))

            switch ch.unicodeScalars.first!.value {
            case 0x2318: //⌘ command
                modifierFlags.insert(.maskCommand)
            case 0x2303: //⌃ control
                modifierFlags.insert(.maskControl)
            case 0x2325: //⌥ option
                modifierFlags.insert(.maskAlternate)
            case 0x21e7: //⇧ shift
                modifierFlags.insert(.maskShift)
            default:
                precondition(false, String(format: "unsupported unicode (%s)", String(ch.unicodeScalars.map({String(format:"0x%04x ", Int($0.value))}).joined().dropLast(1)) ))
        }
    }
    if controlKeys {
        for keycode in keycodes.reversed() {
            keyUp(keycode, modifierFlags: modifierFlags)
        }
    }
}
```

# おわりに

今回指定できる文字列は、いわゆるアスキー文字だけです。"こんにちは"など、日本語は使えません。当然といえば当然のことで、もし、日本語をキー入力したければ、「かな」キーに相当する`0x68`をキー入力して**日本語入力モード**に切り替えておき、ローマ字入力なら"konnnichiha "（最後の文字はスペース）とキー入力するなど、日本語に変換する一連のキー操作自体をキーイベントで送り込む必要がある。


_ちなみに、キー入力のプログラムを起動すると、アクティブウィンドウの現在のカーソル位置に文字が打ち込まれるので、ソースコードを破壊する可能性がある。注意が必要だ。_ ↓こんな感じで回避。（Stringのextensionにしてみた）

![typing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/4077da4a-52e8-3882-0cbd-f9ee9b41ed07.gif)　
インテルMacbook Pro / macOS 11.6 / Xcode 13.1 / Swift 5.5.1 で動作確認済み


<br>
以下、参考記事。


https://qiita.com/Kyome/items/33143418d061fd7ec7d6

https://qiita.com/nak435/items/53d952147c3986afd7fc

https://qiita.com/nak435/items/d662f64c6e082dc2244c