Hammerspoon が面白い

この記事は

Hammerspoon というMacの自動化を行えるツールを使って、key binding を設定する方法を紹介するものです。

追記情報

Date 変更概要 リンク
20170308 複数アプリ対応 blacklist方式(最終的にこれになった) 改:Hammerspoonでホットキーのキーリマップ(blacklist方式)
20170307 複数アプリ対応 whiltelist方式 Hammerspoonでホットキーのキーリマップ
20170306 高速化 HammerspoonのkeyStrokeの高速化 - rochefort’s blog
20170304 function名修正 remap -> keyStroke
20170301 evernoteのページ起動 Hammerspoon で Evernoteの特定ページを開く

さて

昨日、Dashのことを褒めたり(少しけなしたりしたのですが)
Listの移動に矢印キーを使わなければならないのが辛いと書きました。
Dash側にkey Bindingが用意されておらず、またメニューバーにも実装されていないため(メニューバーにあれば、Macのkeyboard設定でショートカットを適用できる)、どうやれるかを調べて見ました。

Macのkey binding tool

かつては Keyremap4macbook というツールでkeyboardのremapができたのですが、
現在は Karabiner という名前に変わっており、しかも macOS Sierra はサポート対象外となっています。
そして、Sierraで動作するKarabiner-Elementsが開発中とのことです。
ただこれは、本当に単純なキーの入れ替えを行うだけのものですので、Vim Binding を実現するには向きませんでした。

選択肢

macOS Sierra で Karabiner が使えない問題にどう対処したか - Qiita
 
naoyaさんが詳しく書かれていますが、Hammerspoon と keyhac が使えそうです。
しかし、keyhac は iTerm2 との相性が悪いらしく、まずは hammerspoon を試してみることにします。

Hammerspoon

alert、notification、ウィンドウ操作、マウス移動などなどいろんなことができ、それらのトリガーをkey bindingに設定することが可能です。
AppleScriptによく似ていますが、AppleScriptよりも書きやすそうです(ちなみにAppleScriptも実行できます)。
 
そして、ここで利用する言語はLuaになります。初めて触りましたが、割と書きやすそう。
Wkipediaを見てみると ソニックザヘッジホッグや FF14、Redisなどでも使われているようです。へー。

Getting Started

アプリを立ち上げて、メニューバーから Open Config すると $HOME/.hammerspoon/init.lua ファイルが開かれます。

とりあえずどうして良いかわからないので、公式を見て見ます。
Getting Started
 
ざっと眺めてみると、色々面白いです。
環境変数取得、ファイル変更監視、アプリケーションイベント監視、wifiイベント監視、iTunes操作など 様々なことができるようです。
 
debug用として以下のコードが紹介されていたので、とりあえずinit.luaに入れました。

-- 設定ファイル reload
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "R", function()
  hs.reload()
end)

-- auto reload
function reloadConfig(files)
    doReload = false
    for _,file in pairs(files) do
        if file:sub(-4) == ".lua" then
            doReload = true
        end
    end
    if doReload then
        hs.reload()
    end
end
myWatcher = hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start()
hs.alert.show("Config loaded")

参考

key bindingでは以下が非常に参考になりました。

 
その他、公式に記載されているリンク集です。色々参考になりそうです。
Sample Configurations · Hammerspoon/hammerspoon Wiki

さて本題

なんとなくわかってきたところで、Vim Binding をやってみます。

全てのアプリで j/k を有効にする

local function keyStroke(key, mods)
  mods = mods or {}
  return function()
    hs.eventtap.keyStroke(mods, key)
  end
end

hs.hotkey.new({'ctrl'}, 'J', keyStroke('down')),
hs.hotkey.new({'ctrl'}, 'K', keyStroke('up'))

これやって見たのですが、エディタによっては若干もっさりします。
あと、control keyを押しっぱなしで j/k 押しても動作しません。
そもそも私が使っているエディタ自身に key binding 機能があるので、特定アプリのみで動作(または除外)させる方法を模索してみます。

特定アプリでのみ実行

Dashだけで良いような気がしてきたので、特定アプリで動作するようにしてみます。 参考サイトを見てfilterを利用しましたが、hs.application.watcher というアプリのイベント監視を使ってもよかったかもしれません。filterは1つのアプリに対して操作しますが、watcherだと対象アプリの幅を広げることができそうです。

local function disableAll(keySet)
  for k, v in pairs(keySet) do v:disable() end
end

local function enableAll(keySet)
  for k, v in pairs(keySet) do v:enable() end
end

local vimBinding = {
  hs.hotkey.new({'ctrl'}, 'J', keyStroke('down')),
  hs.hotkey.new({'ctrl'}, 'K', keyStroke('up'))
}

hs.window.filter.new('Dash')
  :subscribe(hs.window.filter.windowFocused, function() enableAll(vimBinding) end)
  :subscribe(hs.window.filter.windowUnfocused, function() disableAll(vimBinding) end)

アプリの切り替えのたびにkey bindingの有効・無効化をしているのが少し解せません。
まぁでも、とりあえずこれで満足。

特定アプリを除外

おまけ。一応、特定アプリを除外する方法でも実装。

local function disableAll(keySet)
  for k, v in pairs(keySet) do v:disable() end
end

local function enableAll(keySet)
  for k, v in pairs(keySet) do v:enable() end
end

vimBinding = {
  hs.hotkey.new({'ctrl'}, 'J', keyStroke('down')),
  hs.hotkey.new({'ctrl'}, 'K', keyStroke('up'))
}
enableAll(vimBinding)
hs.window.filter.new('Atom')
  :subscribe(hs.window.filter.windowFocused, function() disableAll(vimBinding) end)
  :subscribe(hs.window.filter.windowUnfocused, function() enableAll(vimBinding) end)

やってみて

Hammerspoonすげー便利。automator, applescript, macのkeyboard shortcut を組み合わせて自動化している人はそこそこいると思いますが、Hammerspoon使うと短く書けて、しかもscript 1個ですみそうです。 これで、dashでの利用もはかどります。  
例えば、今現在Evernoteの特定ページをTODOリストにして、shortcutで表示するみたいなことやっているのですが、具体的には以下の段階を踏んで実現しています。

1. shellを作成(open evernote:///view/.... しているだけ)
2. shellをアプリ化
3. アプリをautomatorでサービス化
4. サービスをMacのショートカットキーに登録

Hammerspoon使うと、これが一発で書けそうです。wktkしてきました。