Karabinerのランチャーアイテムを編集するシェルスクリプト


## 動機
* Karabinerのvkopenurldef素晴らしすぎて泣ける
    * [Karabinerで多機能ランチャーを実現する - Qiita](http://qiita.com/catfist/items/ab43d06b6914418afb85)
* 簡単に追加できるようにしたい

## 使用ツール
### xmlstarlet
* コマンドラインでXML解析・編集

```bash
$ brew install xmlstarlet

$ type xmlstarlet
xmlstarlet is /usr/local/bin/xmlstarlet
$ type xml
xml is /usr/local/bin/xml
```

### GNU sed
* xmlstarletだとネストした要素を追加するのが超かったるいので
* メタキャラクタ`\n`を使いたいので

```bash
$ brew install gnu-sed

$ type gsed
gsed is /usr/local/bin/gsed
```

### MacVim

```bash
$ brew cask install macvim

$ type mvim
mvim is /usr/local/bin/mvim
```

### pygmentize
* [bash標準出力をpygmentizeでシンタックスハイライトする - Qiita](http://qiita.com/catfist/items/f696de9d8cb7ed718423)

## Karabiner launcherの構造

```private.xml
<?xml version="1.0"?>
<root>
  <item><!-- 関係ないitem -->
    <name>Esc alternative</name>
    <identifier>esc_alternative</identifier>
    <appendix>All Applications</appendix>
    <autogen>
      __SimultaneousKeyPresses__
      KeyCode::J, KeyCode::K,
      KeyCode::ESCAPE,
      Option::SIMULTANEOUSKEYPRESSES_RAW
    </autogen>
  </item>
  <item><!-- このitemがランチャーを定義 -->
    <name>launcher</name>
    <identifier>karabiner_launcher</identifier>
    <vkopenurldef>
      <name>KeyCode::VK_OPEN_URL_SHELL.toggle.Finder</name>
      <url type="shell">
        <![CDATA[ osascript ~/bin/osascript/toggleapp.scpt Finder ]]>
      </url>
    </vkopenurldef>
    <vkopenurldef>
      <name>KeyCode::VK_OPEN_URL_SHELL.doingtxt</name>
      <url type="shell">
        <![CDATA[ open -a "TaskPaper" ~/Dropbox/todo/doing.txt ]]>
      </url>
    </vkopenurldef>
    <autogen>
      __SimultaneousKeyPresses__
      KeyCode::F, KeyCode::D,
      KeyCode::VK_OPEN_URL_SHELL.toggle.Finder
    </autogen>
    <autogen>
      __SimultaneousKeyPresses__
      KeyCode::D, KeyCode::G,
      KeyCode::VK_OPEN_URL_SHELL.doingtxt,
      Option::SIMULTANEOUSKEYPRESSES_STRICT_KEY_ORDER
    </autogen>
  </item>
  <item>
    <name>remap to Delete</name>
    <identifier>remap.to_delete</identifier>
    <autogen>__SimultaneousKeyPresses__ KeyCode::D, KeyCode::L, KeyCode::DELETE</autogen>
  </item>
  <item><!-- 関係ないitem -->
    <name>Simultaneous Key Presses to Geneal Hotkey</name>
    <identifier>remap.simultaneouskeypresses_to_general</identifier>
    <appendix>C-V: Cmd+C</appendix>
    <autogen>__SimultaneousKeyPresses__ KeyCode::M, KeyCode::N, KeyCode::F2, VK_CONTROL, Option::SIMULTANEOUSKEYPRESSES_STRICT_KEY_ORDER</autogen>
  </item>
</root>
```

`<identifier>karabiner_launcher</identifier>`を持つ`<item>`内に、`<vkopenurldef>`と`<autogen>`を追加する必要がある。

## 完成したコード

```kled
#!/bin/bash
# @(#) add launcher item to private.xml for Karabiner
# readonly FILE=~/Documents/karabiner.xml
readonly CMDNAME="$(basename "$0")"
readonly FILE=~/Library/Application\ Support/Karabiner/private.xml
url='<url type="shell">'
defid=''
nametype=''
def=''
autogen=''
dflag=0
usage () {
  echo "Usage: $(basename "$0") -t <type> -i <id> [-h] [contents]" 1>&2
  echo "-t: url/file/shell" 1>&2
}
while getopts t:i:d:h OPT
do
  case $OPT in
    t)#type
      case "$OPTARG" in
        u*)
          url='<url>'
          nametype="WEB"
          ;;
        f*)
          url='<url type="file">'
          nametype="FILE"
          ;;
        s*)
          url='<url type="shell">'
          nametype="SHELL"
          ;;
        ?) echo "$(basename "$0"): Undifined url type. select from 'url/file/shell'" 1>&2; exit 1
          ;;
      esac
      ;;
    i)#identifier
      defid="$OPTARG"
      ;;
    d)#delete
      dflag=1
      darg="$OPTARG"
      ;;
    h)
      usage
      exit 0
      ;;
    ?)
      echo "$(basename "$0"): Undifined option." 1>&2
      usage
      exit 1
      ;;
  esac
done
shift $((OPTIND - 1))

# process
# delete option
if [ "$dflag" = 1 ];then
  dname="$(xmlstarlet sel -I -t -v '//item[identifier="karabiner_launcher"]/vkopenurldef/name' "$FILE" 2>/dev/null | grepeco -p "Select item to delete:" "$darg")"
  if [ -n "$dname" ];then
    {
      xmlstarlet sel -t -c "//item[identifier='karabiner_launcher']/vkopenurldef[name='$dname']" "$FILE" | xmlstarlet fo | gsed -e 1d -e "\$a\ " | gsed -e "1i\------------------------------"
      xmlstarlet sel -t -c "//item[identifier='karabiner_launcher']/autogen[contains(./text(),'$dname')]" "$FILE" | xmlstarlet fo | gsed -e 1d -e "\$a\------------------------------"
    } | pygmentize
    echo "$CMDNAME: Delete this launcher item? ['y'/n]" 1>&2
    read ANSWER
  else
    echo "$CMDNAME: ERROR: No hit." 1>&2
    exit 1
  fi
  if ! echo "$ANSWER" | grep -q '^n'; then
    xmlstarlet ed -L -d "//item[identifier='karabiner_launcher']/vkopenurldef[name='$dname']" "$FILE"
    xmlstarlet ed -L -d "//item[identifier='karabiner_launcher']/autogen[contains(./text(),'$dname')]" "$FILE"
    echo "$CMDNAME: Deleted." 1>&2
    /Applications/Karabiner.app/Contents/Library/bin/karabiner reloadxml
    exit 0
  else
    echo "$CMDNAME: Canceled." 1>&2
    exit 0
  fi
fi

if [ -z "$defid" ];then
  echo "$CMDNAME: Enter 'vkopenurldef' identifier." 1>&2
  echo -n "id: " 1>&2
  read defid
fi

if [ -z "$1" ];then
  echo "$CMDNAME: Enter 'vkopenurldef' url content." 1>&2
  echo -n "CDATA: " 1>&2
  read CDATA
else
  CDATA="$*"
fi

# add <vkopenurldef>
def="    <vkopenurldef>\n      <name>KeyCode::VK_OPEN_URL_$nametype.$defid<\/name>\n      $url\n        <\![CDATA[ $CDATA ]]>\n      <\/url>\n    <\/vkopenurldef>"
echo "$def" | gsed -e 's/\\n/\n/g' -e 's@\\!@!@g' -e 's@\\/@/@g' | pygmentize
echo "$CMDNAME: Add this item? ['y'/n]" 1>&2
read ANSWER
if ! echo "$ANSWER" | grep -q '^n'; then
  gsed -i "/<identifier>karabiner_launcher<\/identifier>/a\\$def" "$FILE"
else
  echo "$CMDNAME: Canceled." 1>&2
  exit 0
fi

# add <autogen>
autogen="    <autogen>\n      __SimultaneousKeyPresses__\n      KeyCode::, KeyCode::,\n      KeyCode::VK_OPEN_URL_$nametype.$defid\n    <\/autogen>"
gsed -i "/<identifier>karabiner_launcher<\/identifier>/,/<\/item>/s/\( *<\/item>\)/$autogen\n\1/" "$FILE"
mvim +"/^ *KeyCode::VK_OPEN_URL_$nametype.$defid" +-1 +"normal f," "$FILE"
```

## 使い方
* 追加

```bash
$ kled -t shell -i toggle.Notes osascript ~/bin/osascript/toggleapp.scpt Notes
    <vkopenurldef>
      <name>KeyCode::VK_OPEN_URL_SHELL.toggle.Notes</name>
      <url type="shell">
        <![CDATA[ osascript /Users/catfist/bin/osascript/toggleapp.scpt Notes ]]>
      </url>
    </vkopenurldef>
Add this item? ['y'/n]
# n以外を入力
# MacVimが起動して<autogen>部分の編集に入る
```

* 削除

```bash
$ kled -d Notes
------------------------------
<vkopenurldef>
  <name>KeyCode::VK_OPEN_URL_SHELL.toggle.Notes</name>
  <url type="shell"> osascript /Users/catfist/bin/osascript/toggleapp.scpt Notes </url>
</vkopenurldef>

<autogen> __SimultaneousKeyPresses__ KeyCode::N, KeyCode::T,KeyCode::VK_OPEN_URL_SHELL.toggle.Notes</autogen>
------------------------------
kled: Delete this launcher item? ['y'/n]
# n以外を入力
kled: Deleted.
```

## 補遺

```bash
# !をエスケープする
def="    <vkopenurldef>\n      <name>KeyCode::VK_OPEN_URL_SHELL.toggle.$appname<\/name>\n      <url type="shell">\n      <\![CDATA[ osascript ~\/bin\/osascript\/toggleapp.scpt $appname ]]>\n<\/url>\n    <\/vkopenurldef>"
```

```bash
# $をエスケープする
$ var=replaced
$ cat txt | gsed "/aaa/a\\$var"
aaaa
replaced
bbbb
cccc
ddff
gghh
```


## 参考
* [Linuxコマンド集 - 【 sed 】 文字列の置換、行の削除を行う：ITpro](http://itpro.nikkeibp.co.jp/article/COLUMN/20060227/230879/)
* [morituriのブログ : sed aコマンド、 iコマンドの動作（指定文字列の挿入、追加） 【Linuxコマンド】](http://blog.livedoor.jp/morituri/archives/51996552.html)