kumamotone’s blog

iOS/Android アプリエンジニアです https://twitter.com/kumamo_tone

HammerspoonでどのウィンドウでもEmacs風キーバインドを使う

経緯

macOS Sierra になってからKarabinerが使えなくなって以来(自分は2017年4月頃〜)、Hammerspoon を使ってています。

現在ではSierra以上でも動くKarabiner-Elementsの安定版が出ているので使ってみたのですが、以下の2点が自力で克服できておらず、まだちょっとしんどいのでまたHammerspoonに戻ってきました(誰かがなんとかしてそうだけど…)。

  • Office(Outlook) で何故かバインド(Ctrl+A, Ctrl+E)が効かなくなる
  • Ctrl+A/E の移動は、単なる行頭行末移動ではなくMove to beginning of text (インデントを考慮して行頭) になってほしい

というわけでほぼほぼ1年使ってある程度手に馴染んできたのと、Karabiner-Elementsの安定版が出た現在でも Hammerspoon で設定を書きたい、書きたいがうまくいっていないという人もいるんではないかと思い、一旦軽くまとめてみることにしました。

HammerSpoon

www.hammerspoon.org

Hammerspoon はキーリマップツールというよりかは自動化ツールと謳っていて、リマップ以外にウィンドウサイズの変更や移動、WiFiやUSBデバイスの監視なんかもできるみたいみたいです。

設定ファイルはLuaという言語で書きます。ある程度馴染みのあるPythonで設定が書けるkeyhacというソフトもあるので、最初はLua…うぐぐ…となったのですが簡単な範囲なら思ったより全然困ったことが起こらず直感的に書くことができました。

設定ファイル

とりあえず設定ファイルを晒します。

--
-- Hammerspoon用 KeyRemap 設定
--

local function keyCode(key, modifiers)
   modifiers = modifiers or {}
   return function()
      hs.eventtap.event.newKeyEvent(modifiers, string.lower(key), true):post()
      hs.timer.usleep(1000)
      hs.eventtap.event.newKeyEvent(modifiers, string.lower(key), false):post()
   end
end

local function remapKey(modifiers, key, keyCode)
   hs.hotkey.bind(modifiers, key, keyCode, nil, keyCode)
end

local function disableAllHotkeys()
   for k, v in pairs(hs.hotkey.getHotkeys()) do
      v['_hk']:disable()
   end
end

local function enableAllHotkeys()
   for k, v in pairs(hs.hotkey.getHotkeys()) do
      v['_hk']:enable()
   end
end

local function handleGlobalAppEvent(name, event, app)
   if event == hs.application.watcher.activated then
      -- hs.alert.show(name)
      if name ~= "iTerm2" then
         enableAllHotkeys()
      else
         disableAllHotkeys()
      end
   end
end

appsWatcher = hs.application.watcher.new(handleGlobalAppEvent)
appsWatcher:start()

--
-- ここから KeyRemap 設定
--

-- カーソル移動
-- 現状 hs.hotkey.bind の挙動が怪しいので getFlags+getKeyCode を使うといい
hs.eventtap.new({hs.eventtap.event.types.keyDown}, function(e)
  -- Ctrl + Shift + FBNP(ctrl単体のものよりより先に書く必要がある)
  if e:getFlags().ctrl and e:getFlags().shift then
    if e:getKeyCode() == 35 then
      hs.eventtap.event.newKeyEvent({"shift"}, "up", true):post(); return true;
    elseif e:getKeyCode() == 11 then
      hs.eventtap.event.newKeyEvent({"shift"}, "left", true):post(); return true;
    elseif e:getKeyCode() == 45 then
      hs.eventtap.event.newKeyEvent({"shift"}, "down", true):post(); return true;
    elseif e:getKeyCode() == 3 then
      hs.eventtap.event.newKeyEvent({"shift"}, "right", true):post(); return true;
    elseif e:getKeyCode() == 6 then
      hs.eventtap.event.newKeyEvent({'shift','cmd'}, 'z', true):post(); return true;
    end
  end

  -- Ctrl + FBNP
  if e:getFlags().ctrl then
    -- log の吐き方
    -- local log = hs.logger.new('mymodule','debug')
    -- log.i(e:getKeyCode())
    if e:getKeyCode() == 35 then
      hs.eventtap.event.newKeyEvent({}, 'up', true):post(); return true;
    elseif e:getKeyCode() == 11 then
      hs.eventtap.event.newKeyEvent({}, 'left', true):post(); return true;
    elseif e:getKeyCode() == 45 then
      hs.eventtap.event.newKeyEvent({}, 'down', true):post(); return true;
    elseif e:getKeyCode() == 3 then
      hs.eventtap.event.newKeyEvent({}, 'right', true):post(); return true;
    -- PCライクなバインディング、たとえば
    -- ctrl + W を cmd + W にするのも hs.hotkey.bind だと何故か出来ないので
    -- こっちの方法を使っている
    -- elseif e:getKeyCode() == 6 then
    --   hs.eventtap.event.newKeyEvent({'cmd'}, 'z', true):post(); return true;
    -- elseif e:getKeyCode() == 7 then
    --   hs.eventtap.event.newKeyEvent({'cmd'}, 'x', true):post(); return true;
    -- elseif e:getKeyCode() == 8 then
    --   hs.eventtap.event.newKeyEvent({'cmd'}, 'c', true):post(); return true;
    -- elseif e:getKeyCode() == 9 then
    --   hs.eventtap.event.newKeyEvent({'cmd'}, 'v', true):post(); return true;
    -- elseif e:getKeyCode() == 13 then
    --   hs.eventtap.event.newKeyEvent({'cmd'}, 'w', true):post(); return true;
    -- elseif e:getKeyCode() == 46 then
      -- hs.eventtap.event.newKeyEvent({}, 'return', true):post(); return true;
      -- hs.eventtap.keyStroke({}, 'return');
    end
  end

  return false
end):start()

-- 行頭行末移動
-- home/end は 行頭ではなく、Move to beginning of text (インデントを考慮して行頭) になってほしい
remapKey({"ctrl"}, "a", keyCode("left", {"cmd"}))
remapKey({"ctrl"}, "e", keyCode("right", {"cmd"}))

remapKey({"alt"}, "b", keyCode("left", {"alt"}))
remapKey({"alt"}, "f", keyCode("right", {"alt"}))
remapKey({"alt"}, "n", keyCode("down", {"alt"}))
remapKey({"alt"}, "p", keyCode("up", {"alt"}))

remapKey({"alt"}, "h", keyCode('delete', {"alt"}))
local function deleteWordForward()
  keyCode('right', {'shift', 'alt'})()
  keyCode('delete')()
end
remapKey({'alt'}, 'd', deleteWordForward)

-- Return
remapKey({'ctrl'}, 'm', keyCode('return'))

-- Delete
-- Ctrl+H を文字編集以外(ブラウザバック)でも使いたいため
remapKey({'ctrl'}, 'h', keyCode('delete'))

-- Ctrl+K は OS 標準のものを使用
-- Office だと効かない 悔しい

-- ページスクロール
remapKey({'ctrl'}, 'v', keyCode('pagedown'))
remapKey({'alt'}, 'v', keyCode('pageup'))

-- Esc
remapKey({'ctrl'}, 'g', keyCode('escape'))

--
-- 参考
--

-- 【テンプレ】
-- Karabiner 使えない対策: Hammerspoon で macOS の修飾キーつきホットキーのキーリマップを実現する - Qiita
-- http://qiita.com/naoya@github/items/81027083aeb70b309c14

-- 【行頭、行末移動】
-- Sierra+Hammerspoonでキーバインドを設定する - たまめも(tech)
-- http://tamamemo.hatenablog.com/entry/2017/01/30/183650

-- 【hotkey.bind の代替】
-- 5ch
-- https://potato.5ch.net/test/read.cgi/mac/1485327943/#217

-- 【困った時】
-- Hammerspoon docs
-- http://www.hammerspoon.org/docs/index.html

-- 【Ctrl+K (使わなかった)】
-- http://qiita.com/swdyh/items/04f7da8c1209a067add5
-- local function killLine()
--   keyCode('e', {'shift', 'ctrl'})()
--   keyCode('x', {'cmd'})()
-- end
-- remapKey({'ctrl'}, 'k', killLine)

やりたいこと

Ctrl + X 始動のキーバインド等は使っていないので基本的な部分をEmacsっぽくしています。以前Windows用にKeyhacの設定ファイルを書いてた時にはもっとゴチャゴチャ色々書いていたのですが、割りと削ぎ落とされてこのくらいになりました。

Ctrl + FBNP: →←↓↑

Emacs風移動。あんまり合理的とも思わないキーマッピングですが、身体に馴染んでしまっているので墓場まで持っていくしかない。テキストエリアだけでなくFinderやChromeなどでも使いたいので完全にリマップしてしまいたいキーです。

Shiftキーを押すと範囲を選択したいし、Altキーを押すと単語ジャンプにしたいです。単語ジャンプはプログラミングのときなど、ASCII文字ばっかり出てくる時は大抵活躍しますし、JetBrainsのエディタならAlt+↑↓はかなり素早く範囲指定できるので協力です。

2017年4月時点では hs.hotkey.bind の挙動が怪しいらしく、カーソル長押し移動が効かなくなるので下記を参考に getFlags+getKeyCode で設定するようにしました。がもしかするとAltキーのほうが今元気に動いているのでこんなに回りくどい方法を取る必要ないかも‥

【Karabiner】キーリマップ・カスタマイズ総合 for Mac【英かな】 [無断転載禁止]©2ch.net

Ctrl + A/E: cmd + ←→

Ctrl + A/E は 行頭行末ではなく、Move to beginning of text (インデントを考慮して行頭) になってほしい(未設定の状態だとXcodeでカーソルが完全に左に行ってしまいつらい気持ちになる)のでcmd + ←→にリマップしました。

JetBrainsのエディタとかだとならないのでXcode使わない人はわざわざマッピングする必要無いかもです。

Ctrl + H: BackSpace, Ctrl + D: Delete

他のCtrl + Hは他のショートカットキーに負けて発動しなくなることがあるし、ブラウザの戻るキーとしても使いたい(ChromeのGo Back With Backspaceというエクステンションを使っています)ので delete にリマップしたいです。

Ctrl+D に関してはOS標準のでほしい挙動になるのでリマップしていないです。他のキーはたいてい大丈夫なのですが、JetBrainsのエディタだとこのマッピングが大体コンフリクトするのでエディタ側のリマップ設定を削除してます。

Ctrl + M: Enter

Enterは遠い(JIS配列だとことさら遠い)のでリマップしておきたいです。

Ctrl +G: Esc

Esc は遠いのでリマップしておきたいです。

Ctrl + V / Alt + V: PageUp / PageDown

このリマップは設定してはいるもののちょっと使いづらくって、表示領域の移動はしてもカーソル移動をしてくれないときがあり、そういうときはトラックパッドに手を伸ばしたほうが速いという感じになります。

Ctrl + Z/X/C: cmd+ Z/X/C

これは最近使ってないです。LinuxやPCのときの癖でCtrl+Z/X/C/Vを押してしまいがちだったので設定していましたが、すっかりMacに慣れて最近は素直にcmdキーを押すようになりました。

困ってるとこ

起動してしばらく経つと Finder などで Ctrl + FBNP が効かなくなるときがある

Reloadするとなおります。たまに起こる。

Chrome の検索バーで Ctrl+N すると検索してほしいが、Chromeに Ctrl+N のショートカットが設定されていて変なURLに飛ぶ

Firefoxだと起きないです。説明が難しいのですが、Chrome はCtrl+Nがホスト名を補完してくようとするキーバインドみたいで、これに負けてしまいます(Karabinerだと起きない)。たとえば hogehoge → Ctrl + N するとhogehoge.com に遷移してしまい、しょんぼりします。

Ctrl+K が Outlookで効かない

local function killLine()
    keyCode('e', {'shift', 'ctrl'})()
    keyCode('x', {'cmd'})()
  end
remapKey({'ctrl'}, 'k', killLine)

Ctrl+Kが効かなくなる場面があって、たとえばお仕事でよく使うOutlookだと効かなくなります。

killLine(Ctrl-K一発でその行を消して内容をクリップボードに入れる)のは上記のような方法で実現できるのですが、自分の欲しい挙動であるmac OSの標準の挙動とちょっと違います。

以下の挙動になるキーマップが思いつけば設定できそうです。

  • カーソル位置から右側を削除
  • カーソル位置が右端ならその位置の改行を削除
  • クリップボードは汚してほしくない

ただ行まるまる1行コピー、削除、貼り付けのショートカットは

まとめ

Hammerspoonの設定ファイルの紹介と、なんでこういう設定にしているのかという理由などをまとめてみました。

個人的には国産 Karabiner-Elements にとても期待してます(少額ですが寄付させていだきました)。Office で Ctrl+A/E が効かなくなる問題はぜひご存知な方いらっしゃったら対策を教えてください><

若い頃(学生の頃)は「設定ファイルをゴチャゴチャいじるのは時間の無駄、デフォルトが一番じゃよ」という達観おじさん達を少し軽蔑していたのですが、自分も段々気持ちがわかってくるようになり、こういう設定ファイルをいつまで書いているかはわかりませんが、まだ多少、老いに抗っていきたいと感じてます。