Macbook の内臓キーボードの無効/有効を自動で切り替える (macOS Sierra)

**2017/07/02 追記**

`kextunload` してもキーボードが無効化されなくなってしまっていました。
原因はわかりませんが、代替手段としては [tekezo/Karabiner-Elements: The next generation Karabiner for macOS Sierra](https://github.com/tekezo/Karabiner-Elements) を使用するのが良いようです。

**追記終了**

macOS Sierra にしたら，Karabiner が使用できなくなりました．それに伴い，外部キーボードを接続しても内臓キーボードが無効化されなくなりました．これを解決するため，内臓キーボードの有効化/無効化の方法を調べ，その自動化を試みた，という話です．

結論から言うと，自動化してみたはいいものの，キーボードの接続/取り外しから有効/無効の切り替えまでに最悪5秒程かかってしまい，あまり実用的ではありません．しかし，

- 外部キーボードの付け外しはそこまで頻繁に行わない
- 内臓キーボードの有効化をし忘れを防げる
    - 無効化したままスリープし，次回 wake 時に内臓キーボードが効かずに焦ることがなくなる

という点で考えれば使えるかも？自分はしばらく使ってみます．

**2016/10/29 追記**
他のMacbookでも利用したかったので Github にリポジトリを公開しました．
[tasuwo/toggleMacbookInternalKeyboard: This scripts detect an external keyboard connection and disable internal keyboard of macbook instead of karabiner](https://github.com/tasuwo/toggleMacbookInternalKeyboard)

## 内臓キーボードの無効化

内臓キーボードは，対応する [kext](https://developer.apple.com/library/content/documentation/Darwin/Conceptual/KEXTConcept/KEXTConceptIntro/introduction.html) をアンロードすることで無効にできるようです．

- KEXT (Kernel EXTension)
  - その他の通り，カーネルを拡張するためのモジュール
  - その多くはデバイスドライバ
  - その大体が `/System/Library/Extensions` にある

|コマンド|効果|
|---|---|
|`kextload`|kextのロード|
|`kextunload`|kextのアンロード|
|`kextstat`|ロードされているkextの確認|

以下のコマンドでロード/アンロード(有効化/無効化)が行えます．

```bash
# 内臓キーボードの無効化
$ sudo kextunload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext/
# 内臓キーボードの有効化
$ sudo kextload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext/
```

[How to Load & Unload Kernel Extensions in OS X](http://osxdaily.com/2015/06/24/load-unload-kernel-extensions-mac-os-x/)
[ASCII.jp：不要なドライバーをスッキリ整理、KEXT関連コマンドを知る (1/2)｜Apple Geeks](http://ascii.jp/elem/000/000/544/544198/)

## 外部キーボードの存在判定

あるスクリプトを実行すると，外部キーボードが接続されているか否かで内臓キーボードの有効/無効を適切に切り替えられると良いと考えました．

外部キーボードが接続されているかどうかの確認には，`lsusb` や `ioreg` などのコマンドが使用できそうでした(他にもっと良い方法がありそう...) 前者は重かったので，後者の[ioreg](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man8/ioreg.8.html)を使うことにしました．
以下の質問サイトの回答を拝借し，USB接続されているデバイス名一覧を表示 & 外部キーボード名を grep & 結果が空文字か判定することで，外部キーボードの接続有無を判定することにします．
[List USB devices on OSX command line - Ask Different](http://apple.stackexchange.com/questions/170105/list-usb-devices-on-osx-command-line)
最終的なスクリプトは以下です．

```bash
#!/bin/sh

IS_CONNECTED=`ioreg -p IOUSB -w0 | sed 's/[^o]*o //; s/@.*$//' | grep -v '^Root.*' | grep "USB Keyboard"` 
IS_LOADED=`kextstat -a | grep AppleUSBTCKeyboard`
if [ -n "${IS_CONNECTED}" ]; then
    if [ -n "${IS_LOADED}" ]; then
        `echo "your passowrd" | sudo -S kextunload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext/`
    fi
else
    if [ -z "${IS_LOADED}" ]; then
        `echo "your password" | sudo -S kextload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext/`
   fi
fi
```
キーボードの存在判定のために grep している部分は `"USB Keyboard"` としてますが，これは自分の使用しているキーボードが `ThinkPad Compact USB Keyboard with TrackPoint` であるためです．
パスワードを直接記述しているので，セキュリティ的な問題があります．また，シェルスクリプトに関しては初心者なので，見苦しいコードになっているかもしれません．

## 自動化

できれば，キーボード接続時/取り外し時に自動的に上記のスクリプトを実行してくれると良いです．これを実現するために `launchd` を活用しました．

[LaunchAgent に登録して、スクリプトを自動起動する - それマグで！](http://takuya-1st.hatenablog.jp/entry/2015/06/02/145548)
[Backups With Rsync and Launchd - Stuff… And Things…](http://stuff-things.net/2015/03/18/backups-with-rsync-and-launchd)
[Logging with launchd | Erik's Lab](http://erikslab.com/2011/02/04/logging-with-launchd/)
[LaunchDaemons (launchctl, launchd.plist) の使い方 - maruko2 Note.](http://www.maruko2.com/mw/LaunchDaemons_(launchctl,_launchd.plist)_%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9#Daemon_.E3.81.A8_Agent_.E3.81.AE.E9.81.95.E3.81.84)

先ほど書いたスクリプトを `toggleInternalKeyboard.sh` として `/usr/local/bin` 配下に配置し，以下のような plist ファイルを作成しました．

``` xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
          "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.example.toggleInternalKeyboard</string>
    <key>ProgramArguments</key>
    <array>
      <string>/usr/local/bin/toggleInternalKeyboard.sh</string>
    </array>
    <key>StartInterval</key>
    <integer>1</integer>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/var/log/toggleInternalKeyboard_output.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/toggleInternalKeyboard_err.log</string>
  </dict>
</plist>
```

`<key>Label</key>` の `<string>` 内の値で，デーモンの識別子を定義しています．
`ProgramArguments` で実行するスクリプトを指定しています．
`StartInterval` で1秒ごとにスクリプトを実行しています．
また，`StandardOutputPath`，`StandardErrorPath` で，ログの出力先を指定しています．

上記の plist を `/System/Library/LaunchDaemons/` 配下に配置します．
起動するには以下のコマンドを実行します．`-w` オプションを付加することで，OSブート時に自動的に起動するようにします．

``` shell
$ sudo launchctl load -w /Library/LaunchDaemons/com.example.toggleInternalKeyboard.plist
```

停止させたいときは unload します．

``` shell
$ sudo launchctl unload -w /Library/LaunchDaemons/com.example.toggleInternalKeyboard.plist
```

## 通知

ただでさえ反映までにラグがあるのに，通知も何もないとどのタイミングで切り替わったかがわかりません．切り替えた時に通知が来ると良い感じです．手軽に通知をとばすためには Apple Script を使用するのが良さそうでした．
例えば，以下のような単純な Apple Script でとりあえず通知を飛ばすことができるようです．

```applescript
display notification "test" with title "TEST"
```

Apple Script はコマンドラインから `osascript` コマンドを通して実行できます．しかし，自分の環境では以下のようなエラーが出力されました．

```bash
$ osascript -e 'display notification "test" with title "TEST"'
2016-10-11 21:36:45.910 osascript[77739:1943672] NSNotificationCenter connection invalid
```
~~友人の Yosemite の環境では問題がなかったので，Sierra が悪いのかもしれないですが，詳細は不明です．~~

**2016/10/27追記**
私は tmux を使用していたのですが，tmux を抜けたところ上記エラーが出力されなくなりました．@q4yuu さん，ありがとうございました．以下の記事は上記のエラーが解決しなかった場合について記述しています．今後修正するかもしれません．

仕方がないので，無効化した場合/有効化した場合のための二種類の通知用 `app` を用意することにしました．

```applescript
display notification "内部キーボードを有効化しました" with title "toggleInternalKeyboard"
```

```applescript
display notification "内部キーボードを無効化しました" with title "toggleInternalKeyboard"
```

`app` の出力は，Script Editor の File > Export... から， File Format を `Application` にして保存するとできます．これらをとりあえず `/usr/local/bin` に配置しました．
最終的なスクリプトは以下です．

```bash
#!/bin/sh

IS_CONNECTED=`ioreg -p IOUSB -w0 | sed 's/[^o]*o //; s/@.*$//' | grep -v '^Root.*' | grep "USB Keyboard"` 
IS_LOADED=`kextstat -a | grep AppleUSBTCKeyboard`
if [ -n "${IS_CONNECTED}" ]; then
    if [ -n "${IS_LOADED}" ]; then
        `echo "your password" | sudo -S kextunload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext/`
        `open /usr/local/bin/notifyDisableInternalKeyboard.app`
    fi
else
    if [ -z "${IS_LOADED}" ]; then
        `echo "your passowrd" | sudo -S kextload /System/Library/Extensions/AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext/`
        `open /usr/local/bin/notifyEnableInternalKeyboard.app`
   fi
fi
```

これで，外部キーボードの接続/取り外しに合わせて，内臓キーボードを有効/無効にできるようになりました．