Create a gist now

Instantly share code, notes, and snippets.

@shiro-t /init.lua
Last active Aug 3, 2017

What would you like to do?
My init.lua
-- key mappings
defaultKeyMapper = {
{{"ctrl"}, 'a', {'cmd'}, 'left' },
{{"ctrl"}, 'b', {}, 'left' },
{{"ctrl"}, 'e', {'cmd'}, 'right' },
{{"ctrl"}, 'f', {}, 'right' },
{{"ctrl"}, 'h', {}, 'delete' },
{{"ctrl"}, 'n', {}, 'down' },
{{"ctrl"}, 'p', {}, 'up' },
}
appKeyMappers = {
["Microsoft PowerPoint"] = { },
["Microsoft Excel"] = { },
["Microsoft Word"] = { },
["Microsoft OneNote"] = { },
["Microsoft Outlook"] = {
{{"cmd"}, 'return', {}, 'return' },
{{"ctrl"}, 'return', {}, 'return' },
{{"ctrl"}, 'i', {'ctrl'}, 'k' },
{{"ctrl"}, 'o', {'ctrl'}, 'l' },
},
}
-- modal maker
delay = hs.eventtap.keyRepeatDelay()
interval = hs.eventtap.keyRepeatInterval()
function bindKeyMappers(keyMappers,modal)
for i,mapper in ipairs(keyMappers) do
modal:bind(mapper[1], mapper[2], function()
modal.triggered = true
hs.eventtap.keyStroke(mapper[3],mapper[4], delay )
end, nil, function()
hs.eventtap.keyStroke(mapper[3],mapper[4], interval )
end)
end
return modal
end
-- globals
mzero = hs.hotkey.modal.new({}, nil )
modal = mzero
appKeyModals = {
[0] = bindKeyMappers( defaultKeyMapper, hs.hotkey.modal.new({}, nil ) )
}
-- 0 is default only keymap
-- main function
function applicationWatcher(appName, eventType, appObject)
if (eventType == hs.application.watcher.activated) then
--print(appName)
if( modal ~= mzero ) then
modal:exit()
end
keyMappers = appKeyMappers[appName]
if( keyMappers == nil ) then
-- print("passthrough")
modal = mzero
else
modal = appKeyModals[appName]
if( modal == nil ) then
if( #keyMappers == 0 ) then
-- print("default modal")
modal = appKeyModals[0]
else
-- print("create new modal")
modal = hs.hotkey.modal.new({}, nil )
bindKeyMappers(defaultKeyMapper, modal )
bindKeyMappers(keyMappers, modal )
appKeyModals[appName] = modal
end
end
end
if( modal ~= mzero ) then
-- print("set keymap")
modal:enter()
end
end
end
appWatcher = hs.application.watcher.new(applicationWatcher)
appWatcher:start()
Owner

shiro-t commented Apr 5, 2017 edited

defaultKeyMapper はどのアプリケーションでも共通のキーバインドを定義。ここでは Ctrl-a,b,e,f,p といった Emacs Keybinding での基本的なカーソル移動操作と、ctrl-h による削除を定義している。

appKeyMappers はキーマップを変更するアプリケーションの定義と、アプリケーション独自のキーマップを追加するためのテーブル。
私は Microsoft Office で Emacs Keybinding が使いたいため、各アプリケーションをここで定義。
なお、スペースを含む名前をテーブルに入れる場合、[] でくくった上でダブルクオートでくくる事になるので注意。

Outlook については、アプリケーション独自の追加キーマップとして Cmd-Return と Ctrl-Return をただの Return にリマップと、Ctrl-io を Ctrl-kl にリマップしている。前者は Ctrl-Return でメールが送信処理にマップされているのを無効化するためにある。よく誤操作で送信してしまうためだ。
後者は、ATOK のキーバインドで Ctrl-io を文節の伸ばし縮みに使用しているが、Outlook 側で Ctrl-o あたりが使用されているための改変だ。これにあわせて ATOK 側でも Ctrl-kl を Ctrl-io と同じ用途に追加している。

実際の処理は最後の 90,91行目から始まる。ここで appwatcher という監視オブジェクトを作成、実行している。
監視オブジェクトはアプリケーションの起動終了などのイベントが発生するとコールバック関数 applicationWatcher() を呼ぶ。

関数applicationWatcher() では、まず、アプリケーションのアクティベート(最前面に来てアクティブなアプリケーションとなる)以外のイベントは無視している。

アプリケーションがアクティベートされた場合、まず現在の modal を終了させる。
次にアプリ名で appKeyMappers を検索、リマップ対象かを確認している。

appKeyMapper からの戻りが nil でなければ、リマップ対象と言うことで、今度は appKeyModals を検索、こちらに modal があればそれを利用する。appKeyModals に modal がなければ、keyMappers から modal を作成、appKeyModals に登録の上、これを利用する。

modal とは要するにキーマップのモードである。キーのリマップが一段、たとえば Ctrl-A を Command+← に置き換えるだけなら簡単なのだが、Emacs のファイルの保存のように Ctrl-x の次に Ctrl-w を押すような、多段階のキーマップも考えられる。どうやらこういうときのために、Hammperspoon ではあらかじめ複数のキーマップを定義しておき、切り替えて使うことが可能になっている。例えば通常キーマップで Ctrl-x を押されたら、Ctrl-x 後にだけ有効なキーマップに遷移する、という流れができる。この個々のキーマップを modal というオブジェクトで格納している。
( ウィンドウシステムでプログラムを書いたことのある人なら、Modal Event Loop とか Modal Panel などの特定の状態遷移を指すモードという言葉はなじみがあるかと。 )

ここではその「複数のキーマップを定義して、切り替えることができる」を利用して、アプリケーションごとに modal を生成、切り替えて使うようにしている。起動時に appKeyMappers に含まれる全てのアプリケーションの modal を作るのも無駄に感じられたので、一度作った modal は変数 appKeyModals に格納しておき、二度目以降は再利用している。

また、appKeysModals には最初からデフォルトキーマップだけの modal が登録されている。
デフォルトキーマップ以外に追加していない、言い換えるとアプリケーション名に対応する定義を格納した変数keyMappers の要素数が0の場合、このデフォルトだけのキーマップを利用する。これは50行目の初期化の時点で作られている。デフォルトのキーバインドの変更しか行わない modal が複数生成されないようにして、メモリと処理数を減らしている。(けちくさいと思う、我ながら。)

modal が有効にされるのはリマップ対象のアプリだけである。それ以外の場合は modal は有効にされない。mzero は「modal's zero」の略で、変数 modal の値が mzero と等しい場合は有効も無効もしないようになっている。アプリ切り替え時の判定は若干増えるが、それ以降、キーを叩く度に何の設定もされていない modal を通るか、そもそも modal がないのでスルーされるかだと、後者の方がよいと感じられたため。
nil を使っていないのは、不用意に変数に nil を投入することで GC に目を付けられるのを回避するため。mzero 自体はどの値でも良いのだが、一応型を合わせるために(若干のメモリの無駄だが) modal を一個作って代入している。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment