Hammerspoonという選択肢
karabiner-elementsがマウス制御できない時代があった。
その時はもっぱらAHK適用済みのWindowsばっかり使っていたが、
2017年後半くらいにいつの間にかマウス対応していた。
elementsが自分の中で実用に足る状態になるまでの繋ぎとして
使っていたのがHammerspoonだ。
ぶっちゃけ前身karabinerより出来ることは多い!
前身Karabinerを使いこなせていたかというと怪しいが、
Hammerspoonでやれることは多く、設定ファイルには
軽涼スクリプトのLuaを使えるということで、ゴリゴリ自分の好みな
設定・処理が追加出来てとても良かった。
そしてLua楽しい!
こんなこともやれる。
ウィンドウのサイズ変更とかも出来て、Windowsのエアロスナップみたいな処理も
させることが可能だし、IME切り替え時にWindows10のIMEみたいにスクリーン中央に
日本語入力・直接入力を数秒オーバーレイ表示することが出来たり、処理をゴリゴリ書けば
作業も自動化も出来たりする!
ただ欠点もあり
まず、Karabinerなどより高レイヤーで動作するので、重かったり全てのキー入力をハンドリングできなかったりする。(最近触ってないから記憶があやふや)。ただ、マウスのイベントについては、決め打ちで(0,0)から(100,100)にドラッグするような処理は大丈夫なんだけど、リアルタイムにドラッグを検出して、ユーザーの動作に
追従させるという言うようなことが少しトリッキーなことをしないと検知してくれなかったりと
面倒くさい部分があった。
あと、キーバインドを割り当てるときに修飾キーの組み合わせを良しなに解釈してくれず、
前組み合わせの動作を指定しないと思い通り動いてくれないという欠点は明らかにあった。
まぁそもそも色々できるうちのキーバインド機能であって、それ専門のソフトじゃないんだからね!
と言われればそれまでの話。
とりあえず設定を晒してみる
設定というか処理というか実装というか。こんな感じになるよ!
bambooRake/.hammerspoon
keymap = {
escape=53, f1=122, f2=120, f3=99, f4=118, f5=96, f6=97, f7=98, f8=100, f9=101, f10=109, f11=103, f12=111,
n1=18, n2=19, n3=20, n4=21, n5=23, n6=22, n7=26, n8=28, n9=25, n0=29, minus=27, hut=24, en=93, delete=51, fowardDel=117,
tab=48, q=12, w=13, e=14, r=15, t=17, y=16, u=32, i=34, o=31, p=35, atmark=33, kakko=30,
a=0, s=1, d=2, f=3, g=5, h=4, j=38, k=40, l=37, semicolon=41, colon=39, kakkotoji=42, enter=36,
z=6, x=7, c=8, v=9, b=11, n=45, m=46, commma=43, dot=47, slash=44, haifun=94,
space=49, left=123, up=126, down=125, right=124, home=115, pageUp=126, pageDown=121, kend=119, eisuu=102, kana=104
}
remapKey = {
{ 'h', {}, 'left'},
{ 'j', {}, 'down'},
{ 'k', {}, 'up'},
{ 'l', {}, 'right'},
{ 'a', {'cmd'}, 'left'}, -- beginning of line
{ 'e', {'cmd'}, 'right'}, -- end of line
{ 'd', {}, 'fowardDel'},
{ 'x', {}, 'delete'}
}
function btToMods(bef, afr, afrT)
-- bef getFlags() afr getFlags() afrT table
for key, val in pairs(bef) do
afr[key] = val
end
if afr['cmd'] then table.insert(afrT, 'cmd') end
if afr['alt'] then table.insert(afrT, 'alt') end
if afr['shift'] then table.insert(afrT, 'shift') end
if afr['ctrl'] then table.insert(afrT, 'ctrl') end
if afr['fn'] then table.insert(afrT, 'fn') end
end
function simpleRemap( fMods, fKeyCode, fTable )
for i, ival in ipairs(fTable) do
if fKeyCode == keymap[ival[1]] then
local ffMods = {}
for j, jval in ipairs(ival[2]) do
ffMods[jval] = true
end
local pMods = {}
btToMods(fMods, ffMods, pMods)
hs.eventtap.event.newKeyEvent(pMods, keymap[ival[3]], true):post()
hs.timer.usleep(12000)
hs.eventtap.event.newKeyEvent(pMods, keymap[ival[3]], false):post()
return true
end
end
return false
end
-- local keyCommon = {}
-- keyCommon:new = function(keyLabel)
-- local obj = {}
-- obj.keyLabel = keyLabel
-- obj.keyCode = keymap[keyLabel]
-- --0xf->keyDown, 0xf0->keyUp, 0xf00->keyRepeat
-- obj.keyState = 0x0
-- obj.rawModBit = 0x0
-- obj:analyzeMod = function( event )
-- local modTbl = event:getFlags()
-- if modTbl["cmd"] then self.rawModBit = bit32.bor(self.rawModBit, 0xf) end
-- if modTbl["alt"] then self.rawModBit = bit32.bor(self.rawModBit, 0xf0) end
-- if modTbl["shift"] then self.rawModBit = bit32.bor(self.rawModBit,0xf00) end
-- if modTbl["ctrl"] then self.rawModBit = bit32.bor(self.rawModBit,0xf000) end
-- if modTbl["fn"] then self.rawModBit = bit32.bor(self.rawModBit, 0xf0000) end
-- end
-- obj:update = function( event )
-- if hs.eventtap.event.types.keyDown == event:getKeyCode() then
-- elseif hs.eventtap.event.types.keyUp == event:getKeyCode() then
-- else
-- return false
-- end
-- end
-- return obj
-- end
pressLeft = false
pressRight = false
pressMiddle = false
clickCount = 0
clickPos = 0
pClickCount = 0
dragStart = false
qDown = 0
qUp = 0
function keyPressToMouse( fType, fMods, fKeyCode)
local pMods = {}
if keymap["f"] == fKeyCode then
if "keyup" == fType then
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseUp"], hs.mouse.getAbsolutePosition()):setProperty(hs.eventtap.event.properties.mouseEventClickState, pClickCount):post()
pressLeft = false
dragStart = false
qUp = hs.timer.secondsSinceEpoch()
elseif pressLeft then
clickCount = 0
-- hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseDragged"], hs.mouse.getAbsolutePosition()):post()
elseif "keydown" == fType then
qDown = hs.timer.secondsSinceEpoch()
if clickCount > 0 and (qDown - qUp) > 0.2 then
clickCount = 0
end
clickCount = clickCount + 1
clickPos = hs.mouse.getAbsolutePosition()
local clm = hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseDown"], clickPos, fMods)
clm:setProperty(hs.eventtap.event.properties.mouseEventClickState, clickCount)
clm:post()
pClickCount = clickCount
pressLeft = true
--hs.timer.doAfter(0.02, function() dragMouse(pos) end)
end
return true
elseif keymap["g"] == fKeyCode then
if "keyup" == fType then
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["middleMouseUp"], hs.mouse.getAbsolutePosition()):post()
pressMiddle = false
elseif pressMiddle then
-- hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["middleMouseDragged"], hs.mouse.getAbsolutePosition()):post()
elseif "keydown" == fType then
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["middleMouseDown"], hs.mouse.getAbsolutePosition()):post()
pressMiddle = true
end
return true
elseif keymap["v"] == fKeyCode then
if "keyup" == fType then
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["rightMouseUp"], hs.mouse.getAbsolutePosition()):post()
pressRight = false
elseif pressRight then
-- hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["rightMouseDragged"], hs.mouse.getAbsolutePosition()):post()
elseif "keydown" == fType then
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["rightMouseDown"], hs.mouse.getAbsolutePosition()):post()
pressRight = true
end
return true
elseif "keydown" == fType then
if keymap["r"] == fKeyCode then
hs.eventtap.event.newScrollEvent({0,-3}, {}, "line"):post()
hs.timer.usleep(12000)
return true
elseif keymap["n4"] == fKeyCode then
hs.eventtap.event.newScrollEvent({0,3}, {}, "line"):post()
hs.timer.usleep(12000)
return true
elseif keymap["t"] == fKeyCode then
hs.eventtap.event.newScrollEvent({-1,0}, {}, "line"):post()
hs.timer.usleep(12000)
return true
elseif keymap["n5"] == fKeyCode then
hs.eventtap.event.newScrollEvent({1,0}, {}, "line"):post()
hs.timer.usleep(12000)
return true
end
end
return false
end
local pressedIME = 0
local pXY = { x=-1, y=-1 }
local nXY = {}
local run = true
pressedHyper = false
eventtap = hs.eventtap.new({hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp, hs.eventtap.event.types.mouseMoved},
function(e)
--trueを返すと乗っ取る
--falseを返すと通過する
local keyCode = e:getKeyCode()
local keyDown = (e:getType() == hs.eventtap.event.types.keyDown)
local keyUp = (e:getType() == hs.eventtap.event.types.keyUp)
local mouseMv = (e:getType() == hs.eventtap.event.types.mouseMoved)
if pressedHyper then
if keyCode == 0x72 then
if keyUp then
--hs.alert.show("OFF")
pressedHyper = false
if pressLeft then
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseUp"], hs.mouse.getAbsolutePosition()):post()
pressLeft = false
dragStart = false
end
if pressRight then
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["rightMouseUp"], hs.mouse.getAbsolutePosition()):post()
pressRight = false
end
if pressMiddle then
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["middleMouseUp"], hs.mouse.getAbsolutePosition()):post()
pressMiddle = false
end
end
return true
end
local mods = e:getFlags()
if keyDown then
if keyPressToMouse("keydown", mods, keyCode) then return true end
if simpleRemap(mods, keyCode, remapKey) then return true end
end
if keyUp then
if keyPressToMouse("keyup", mods, keyCode) then return true end
end
if mouseMv then
if pressLeft then
local lpos = hs.mouse.getAbsolutePosition()
if math.abs(clickPos["x"]-lpos["x"])>5 or math.abs(clickPos["y"]-lpos["y"])>5 then
if not dragStart then
dragStart = true
local llpos = { x = clickPos["x"]-5, y = clickPos["y"]-5}
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseUp"], clickPos):setProperty(hs.eventtap.event.properties.mouseEventClickState, pClickCount):post()
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseDown"], clickPos, mods):setProperty(hs.eventtap.event.properties.mouseEventClickState, pClickCount):post()
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseDragged"], llpos, mods):setProperty(hs.eventtap.event.properties.mouseEventClickState, pClickCount):post()
end
end
if dragStart and run then
run = false
hs.timer.doAfter(0.02, function() run = true end)
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseDragged"], lpos, mods):setProperty(hs.eventtap.event.properties.mouseEventClickState, pClickCount):post()
end
end
end
else
if keyCode == 0x72 then
if keyDown then
--hs.alert.show("ON")
pressedHyper = true
return true
end
end
end
--IME設定ここから
if keyCode == 0x68 then
if keyDown then
pressedIME = pressedIME + 1
if pressedIME == 2 then
-- hs.eventtap.event.newKeyEvent({}, 104, true):post()
-- hs.timer.usleep(200000)
-- hs.eventtap.event.newKeyEvent({}, 104, false):post()
hs.alert.show("あ")
hs.timer.doAfter(0.1, function() hs.alert.closeAll(3) end)
return false --そのまま、かなキーのダウンを送る
end
end
if keyUp then
if pressedIME == 1 then
hs.eventtap.event.newKeyEvent({}, 102, true):post()
hs.timer.usleep(200000)
hs.eventtap.event.newKeyEvent({}, 102, false):post()
hs.alert.show("英")
hs.timer.doAfter(0.1, function() hs.alert.closeAll(3) end)
pressedIME = 0
elseif pressedIME > 1 then
pressedIME = 0
return false --そのまま、かなキーのアップを送る
end
end
return true
end
--IME設定ここまで
return false
end)
eventtap:start()
-- Command + Ctrl + ↑ : フルスクリーン
hs.hotkey.bind("alt", keymap["up"], function()
local win = hs.window.focusedWindow()
local f = win:frame()
local screen = win:screen()
local max = screen:frame()
f.x = max.x
f.y = max.y
f.w = max.w
f.h = max.h
win:setFrame(f)
end)
-- Command + Ctrl + ← : ウィンドウ左寄せ
hs.hotkey.bind("alt", keymap["left"], function()
local win = hs.window.focusedWindow()
local f = win:frame()
local screen = win:screen()
local max = screen:frame()
f.x = max.x
f.y = max.y
f.w = max.w / 2
f.h = max.h
win:setFrame(f)
end)
-- Command + Ctrl + ← : ウィンドウ右寄せ
hs.hotkey.bind("alt", keymap["right"], function()
local win = hs.window.focusedWindow()
local f = win:frame()
local screen = win:screen()
local max = screen:frame()
f.x = max.x + (max.w / 2)
f.y = max.y
f.w = max.w / 2
f.h = max.h
win:setFrame(f)
end)
うん。久々に見たけどなんかゴリゴリ書いてるな爆
これ書いてるとき、ぶっちゃけLuaが凄い楽しかったんだよね。
Luaの実行環境としてHammerspoonめっちゃよくて。
メニューバーの通知アイコンからコンソールとかリロードできたり
してさ。
ただただイベント駆動型
AHKやKarabinerが設定ファイルちっくな感じなのに対して、
hammerspoonの場合は、イベントループ内に入ってきたイベント内容で
分岐させて、それぞれのイベントに対してひたすら処理内容を
書くっていう感じで普通にプログラミング。
GUIアプリとかGame作ったことあって、プログラミングも苦じゃないって
方にはおススメですよ。
最後に
AHKやKarabinerと毛色は違うけど、めっちゃ使えるソフトです。
これだけ出来てフリーというのもありがたい。
karabiner-elementsとHammerspoon、どちらかがあるかぎりmacは安泰です。
WindowsはAHKがこけたらどうなるか分からない。
AHKも64対応やUnicode対応でなんか色々あったような印象をネット情報から
感じるので、AHKが存続し続けるか内心ビクビク&Windowsユーザー多数だから大丈夫っしょ
という両極端な会話を頭ん中でし続けています。