どうも macOS Sierra に非対応のとのこと。Karabiner はカーネルにモジュール(KEXT)を読み込ませ、カーネル内部のキーイベントを操作することでキーバインドの変更を実現している模様だが、そのためカーネルの変更に弱い面がある。
一方の Apple はセキュリティ面やOSの安定稼働の面から、KEXT による拡張を抑制する方向にある。Microsoft も 64bit 移行やOSのアップグレードの機会を通じて電子署名されていないデバイスドライバはロードできないようにしているし、iOS のようなモバイルデバイスのOSでは、もはやカーネルにモジュールを追加することすら不可能になっている。
幸い、Karabiner では Karabiner Elements という新しいキーリマップソフトウェアを開発、そこから従来の Karabiner の機能を追加していく形で刷新するようで、これは期待が持たれる。とはいえ、一からの作り直しなのでしばらくは Karabiner なしの生活に耐えなければならないようだ。
私が Karabiner を使ってる理由は、実のところ「Microsoft Office で Emacs キーバインド(のサブセット)が使いたい」 につきる。Karabiner Elements では現在のところアプリケーションごとのキーマップというのは実装されてないので、これは対応できないことになる。
いい加減 面倒になってきたので色々探して試した結果、Hammerspoon を利用することに落ち着いた。Hammerspoon はキー入力などのイベントを Lua という言語を使ってカスタマイズするもので、単なるキーリマップ以上の操作もできるようだ。
また、Accessibility API を通じてキーイベントを取得、操作を行う。公式のAPI を通しているため、OSのアップデートによる影響も(KEXTを使うのに比べると)低い訳だ。
Hammerspoon は ~/.hammerspoon/init.lua というスクリプトを読み、これに応じて処理をおこなう。私のスクリプトは以下の通りだ。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- disable all settings | |
local passthrough = {} | |
-- default key mappings | |
local defaultKeyMapper = { | |
{{"ctrl"}, 'a', {'ctrl'}, 'left' }, | |
{{"ctrl"}, 'b', {}, 'left' }, | |
{{"ctrl"}, 'e', {'ctrl'}, 'right' }, | |
{{"ctrl"}, 'f', {}, 'right' }, | |
{{"ctrl"}, 'h', {}, 'delete' }, | |
{{"ctrl"}, 'n', {}, 'down' }, | |
{{"ctrl"}, 'p', {}, 'up' }, | |
} | |
-- custom keymapping : Human readable configuration. modal object will be created dynamically from this. | |
-- Four type of settings. | |
-- "appname" = {} : default keymappings is applied and addtional keymappings is also applied. | |
-- "appname*" = {} : only the keymappings is applied. defaults is ignored. | |
-- "appname" = passthrough : no keymappings is applied. "pass through" | |
-- "appname" = defaultKeyMapper : only the default keymappings is applied. | |
-- | |
-- modifier : fn, ctrl, option,alt, command,cmd, shift | |
appKeyMappers = { | |
["Finder"] = passthrough, | |
["Terminal"] = passthrough, | |
["ターミナル"] = passthrough, | |
["CotEditor"] = passthrough, | |
["Code"] = passthrough, | |
["zoom.us"] = passthrough, | |
["Slack"] = passthrough, | |
["Google Chrome"] = passthrough, | |
["Windows App"] = passthrough, | |
["Screen Sharing"] = passthrough, | |
["画面共有"] = passthrough, | |
["Safari*"] = { | |
{{"ctrl"}, 'b', {}, 'left' }, | |
{{"ctrl"}, 'f', {}, 'right' }, | |
{{"ctrl"}, 'h', {}, 'delete' }, | |
{{"ctrl"}, 'n', {}, 'down' }, | |
{{"ctrl"}, 'p', {}, 'up' }, | |
{{"cmd"}, 'b', {"cmd","ctrl"},'1' }, | |
}, | |
["VMware Fusion*"] = { | |
{{"cmd"}, 'w', {}, 'w' }, | |
}, | |
-- ["ATOK お気に入り文書編集ツール"] = defaultKeyMapper, | |
-- ["Microsoft Word"] = defaultKeyMapper, | |
["Microsoft OneNote"] = { | |
{{"ctrl"}, 'k', {"ctrl","shift"}, 'right', {"cmd"}, 'x' }, | |
{{"ctrl"}, 'y', {"cmd"}, 'v' }, | |
}, | |
["Microsoft Excel"] = { | |
{{"ctrl"}, 'return', {}, 'f2' }, | |
}, | |
-- custom keymapping | |
["Microsoft PowerPoint*"] = { | |
{{"ctrl"}, 'a', {'cmd'}, 'left' }, | |
{{"ctrl"}, 'b', {}, 'left' }, | |
{{"ctrl"}, 'e', {'cmd'}, 'right' }, | |
{{"ctrl"}, 'f', {}, 'right' }, | |
{{"ctrl"}, 'h', {}, 'delete' }, | |
{{"ctrl"}, 'n', {}, 'down' }, | |
{{"ctrl"}, 'p', {}, 'up' }, | |
}, | |
["Microsoft Outlook*"] = { | |
{{"ctrl"}, 'a', {'ctrl'}, 'up' }, | |
{{"ctrl"}, 'b', {}, 'left' }, | |
{{"ctrl"}, 'e', {'ctrl'}, 'down' }, | |
{{"ctrl"}, 'f', {}, 'right' }, | |
{{"ctrl"}, 'h', {}, 'delete' }, | |
{{"ctrl"}, 'n', {}, 'down' }, | |
{{"ctrl"}, 'p', {}, 'up' }, | |
{{"cmd"}, 'return', {}, 'return' }, | |
{{"ctrl"}, 'return', {}, 'return' }, | |
{{"ctrl"}, 'i', {'ctrl'}, 'k' }, | |
{{"ctrl"}, 'o', {'ctrl'}, 'l' }, | |
}, | |
} | |
delay = hs.eventtap.keyRepeatDelay() | |
interval = hs.eventtap.keyRepeatInterval() | |
function bindKeyMappers(keyMappers,modal) | |
local i,mapper | |
for i,mapper in ipairs(keyMappers) do | |
if( #mapper == 4) then | |
modal:bind(mapper[1], mapper[2], | |
function() | |
hs.eventtap.keyStroke(mapper[3],mapper[4], delay ) | |
end, | |
nil, | |
function() | |
hs.eventtap.keyStroke(mapper[3],mapper[4], interval ) | |
end) | |
elseif(#mapper == 6 ) then | |
modal:bind(mapper[1], mapper[2], | |
function() | |
hs.eventtap.keyStroke(mapper[3],mapper[4]) | |
hs.eventtap.keyStroke(mapper[5],mapper[6]) | |
end, | |
nil, nil) | |
end | |
end | |
return modal | |
end | |
-- globals : | |
-- appKeyModals contains modal objects for apps. modal objects are used for modify key inputs. | |
-- modal object will be create one per apps. modal for passthrough and default key mapping will be shared by apps. | |
-- | |
-- 0 is modal for passthrough | |
-- 1 is modal default key mapping | |
-- "appname" is modal for the application | |
appKeyModals = { | |
[0] = hs.hotkey.modal.new( passthrough, nil ), | |
[1] = bindKeyMappers( defaultKeyMapper, hs.hotkey.modal.new({}, nil ) ) | |
} | |
modal = appKeyModals[1] | |
modal:enter() | |
-- Disable if InputMethod is ATOK | |
-- function imWatcher() | |
-- ATOKsrc = "com.justsystems.inputmethod.atok31.Japanese" | |
-- ABCsrc = "com.apple.keylayout.ABC" | |
-- | |
-- local source_id = hs.keycodes.currentSourceID() | |
-- print("source_id: " .. source_id .. "\n") | |
-- if( source_id == ATOKsrc ) then | |
-- | |
-- | |
-- | |
-- | |
-- | |
-- end | |
-- hs.keycodes.inputSourceChanged( imWatcher ) | |
-- main function | |
function applicationWatcher(appName, eventType, appObject) | |
if (eventType == hs.application.watcher.activated) then | |
-- print(appName) | |
modal:exit() | |
local useDefault = true | |
local keyMappers = appKeyMappers[appName] | |
if( keyMappers == nil ) then | |
keyMappers = appKeyMappers[appName.."*"] | |
if( keyMappers ~= nil ) then | |
useDefault = false | |
-- print( "useDefault = false" ) | |
end | |
end | |
if( keyMappers == nil ) then | |
-- mapper isn't exist, use default modal. | |
-- print("no definition:default") | |
modal = appKeyModals[1] | |
else | |
modal = appKeyModals[appName] | |
if( modal == nil ) then | |
-- mapper is exist, but modal hasn't been created. | |
if( keyMappers == passthrough ) then | |
-- print("passthrough") | |
modal = appKeyModals[0] | |
elseif( keyMappers == defaultKeyMapper ) then | |
-- print("default modal") | |
modal = appKeyModals[1] | |
else | |
-- print("create new modal") | |
modal = hs.hotkey.modal.new({}, nil ) | |
if( useDefault ) then | |
-- "appname*" | |
bindKeyMappers(defaultKeyMapper, modal ) | |
end | |
bindKeyMappers(keyMappers, modal ) | |
end | |
appKeyModals[appName] = modal | |
else | |
-- modal is already exist, use existing one. | |
-- print("use existing modal") | |
-- print( appName ) | |
end | |
end | |
-- print("set keymap") | |
modal:enter() | |
end | |
end | |
appWatcher = hs.application.watcher.new(applicationWatcher) | |
appWatcher:start() |