きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

続き: Karabiner-Elements と Swift で background のアプリに特定のキーストロークを送る

ターミナルやエディタでコーディングしつつ、参考書を表示しているKindleのページ送りをしたかったのが動機です。 表題で言っている background のアプリとは最前面(foremost)ではないウィンドウで動作しているアプリを指しています。

lightbulbcat.hatenablog.com

上の記事で試したのは、指定したアプリ向けにキーストロークを送る AppleScript を Karabiner-Elements で特定のキーにより実行する、というものでした。しかし AppleScript の限界なのか background のアプリにキーストロークを送ることはできず、実際には一瞬ターゲットのアプリを foremost に持ってきてキーを送り foremost を元のアプリに戻すという処理しか書けませんでした。

Swift を使えばターゲットのアプリを activate することなく特定のアプリにキーストロークを送れるようだったので、使用感向上のため実装してみました。

Swift スクリプトの用意

アプリにキーストロークを送る send-key-to-app.swift を用意します。 AppleScript よりこちらの方が好きです。

import Foundation
import AppKit

let args = CommandLine.arguments
if args.count != 3 {
  print("\(args[0]) [APP_NAME] [KEY_CODE]")
  print("APP_NAME: should be an executable file name(not a path)")
  print("KEY_CODE: a number such as 123(LEFT), 124(RIGHT), ...")
  exit(1)
}

let appName = args[1]
let keyCode = CGKeyCode(args[2]) ?? 0

let src = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)    

func sendKeyStroke(pid: pid_t, keyCode: CGKeyCode) {
  let keyUp = CGEvent(keyboardEventSource: src, virtualKey: keyCode, keyDown: true)
  keyUp?.postToPid(pid)
  let keyDown = CGEvent(keyboardEventSource: src, virtualKey: keyCode, keyDown: false)
  keyDown?.postToPid(pid)
}

func getPidByName(executableFileName: String) -> Optional<pid_t> {
  let apps = NSWorkspace.shared.runningApplications
  for a in apps {
    print(a.bundleIdentifier)
    if a.executableURL?.lastPathComponent == executableFileName {
      return a.processIdentifier
    }
  }
  return nil
}

if let pid = getPidByName(executableFileName: appName) {
  sendKeyStroke(pid: pid, keyCode: keyCode)
} else {
  print("executable '\(appName)' not found in user processes")
}

始めて Swift を書きましたが、Optional 型と if let のコードが気持ちいいですね。 値を wrap した Optional の扱い方はちょっと Rust っぽいです。

Karabiner-Elements に設定する前に以下のように動作確認をしておきます。

swift send-key-to-app.swift Kindle 124

これで background で動いている Kindle のページが進めば成功です。 AppleScript のものと比べるとウィンドウ切り替えのチカチカ感が減ってとても快適です。

Karabiner-Elements の設定

冒頭で紹介した以前の記事と shell_command 以外は同じです。 適当に Command + 1 でページ送り、Command + 2 でページバックするようにしています。

{
  "profiles": [
    {
      "complex_modifications": {
        "rules": [
          {
            "description": "Kindleのページ移動",
            "manipulators": [
              {
                "from": {
                  "key_code": "1",
                  "modifiers": {
                    "mandatory": ["command"],
                    "optional": ["any"]
                  }
                },
                "to": [{"shell_command": "swift /path/to/send-key-to-app.swift Kindle 123"}],
                "type": "basic"
              },
              {
                "from": {
                  "key_code": "2",
                  "modifiers": {
                    "mandatory": ["command"],
                    "optional": ["any"]
                  }
                },
                "to": [{"shell_command": "swift /path/to/send-key-to-app.swift Kindle 124"}],
                "type": "basic"
              }
            ]
          },

更新したら「Restart Karabiner-Elements」をすれば早速反映されます。

この使い方でもいいのですが、現状ページ送りごとに毎回 swift インタプリターが実行されているので、気になる場合はビルドすると良さそうですね。

余裕があったので Swift をビルドする

swift package build --type executable でコマンド用のプロジェクトが簡単に作成できたので作っておきました。

https://github.com/asa-taka/send-key-to-app

心なしかページ遷移を繰り返してもiMacのファンが鳴らなくなった気がします。