⚙️ RectangleやAmphetamineを吸い込んだHammerspoonについて紹介したい
iTermのホットキー起動を残したくて触り始めたら、他のアプリまで自作luaに吸い込んでいた話。
PLT-85 · FEATURED ⚙️ RectangleやAmphetamineを吸い込んだHammerspoonについて紹介したい
iTermを使っていた頃、ホットキー一発でターミナルをスッと呼び出せる機能が好きでした。とても手に馴染んでいた。 その後ターミナルをGhostty、さらにcmuxと乗り換えて今に至るのですが、乗り換えるたびにホットキー起動が無いことに困っていました。あと、めちゃ寂しい。
これをなんとか自分で再現できないかと探していて、Hammerspoonに出会いました。
Hammerspoonって?
macOSをLuaで好き勝手にいじれる自動化ツールです。
ホットキー起動とかはそれはそれはもうあっさり書けます。 luaで書くので生成AIとも相性がよく、ちょっとお願いするだけでいい感じになります。 こういうショートカット作って!が対話でできるのは非常に便利。
ということは、これが書けるなら簡単な使い方しかしていない他のアプリも、全部置き換えられるのでは…?と思いました。 ちょうどメニューバーのアイコンが増えすぎて困っていたのもあって(以前、Iceで隠す記事も書きました)、「ちょっとしか使っていないアプリを、巻き取ってみようかな」と。
今回はその「Hammerspoonに吸い込まれていった者たち」を、実際のluaと並べてみます。どれも完全な設定ではないので、「こんな雰囲気で」とAIに渡せば、たぶんすぐ動くと思います。
アプリの呼び出し
最初に書いたのがこのホットキー起動です。
書いてみると、ターミナルに限った話じゃなくて。どのアプリでも同じことができる。 そして、そのアプリ自体に設定がなくても、設定できるのがいいところ。 今は「最前面にいれば引っ込めて、いなければ前に出して最大化する」というトグルにして、cmuxだけでなく頻繁に使うConductorとArcに割り当てています。
local function maximizeAppWindow(app) local win = app and app:mainWindow() if win then win:maximize() end -- mainWindow が nil でも落ちないようにend
local function launchOrFocusApp(appName, bundleID) if bundleID then hs.application.launchOrFocusByBundleID(bundleID) else hs.application.launchOrFocus(appName) endend
local function toggleAppMaximized(appName, bundleID) local app = hs.application.find(appName) if app then if app:isFrontmost() then app:hide() -- 最前面にいるなら引っ込める else app:activate() hs.timer.doAfter(0.1, function() maximizeAppWindow(app) end) -- 前に出して最大化 end else launchOrFocusApp(appName, bundleID) -- 起動してなければ起こす endend
-- bundleID は自分の環境のもの。hs.application.find(app):bundleID() などで確認できますhs.hotkey.bind({"ctrl", "alt"}, "m", function() toggleAppMaximized("cmux") end)hs.hotkey.bind({"ctrl", "alt"}, "c", function() toggleAppMaximized("Conductor", "com.conductor.app") end)hs.hotkey.bind({"ctrl", "alt"}, "a", function() toggleAppMaximized("Arc", "company.thebrowser.Browser") end)cmuxとConductorを行き来しながらAIに作業させる、みたいな使い方をしているので、Ctrl+Option+MとCtrl+Option+Cの2つを叩いているだけでいい感じです。Spotlightでもいいだろ〜っていう意見もあると思いますが、耳を塞ぎます。
ウィンドウ管理:from Rectangle
次に手をつけたのがウィンドウ管理です。 今まではRectangleを使っていました。不満があったわけじゃないし、無料版の範囲でのみ使ってました。非常に便利なアプリです。 ただ、僕が使っていたのは左右半分と四隅の4分割くらい。高機能なアプリなのに、その程度しか触っていなかったです。
なので今回に関しては、これくらいなら置き換えていいな。となり、以下のように変えました
local function moveWindow(position) local win = hs.window.focusedWindow() if not win then return end
local positions = { left = {0, 0, 0.5, 1}, -- 左半分 right = {0.5, 0, 0.5, 1}, -- 右半分 full = {0, 0, 1, 1}, -- 最大化 } win:moveToUnit(positions[position])end
hs.hotkey.bind({"ctrl", "alt"}, "left", function() moveWindow("left") end)hs.hotkey.bind({"ctrl", "alt"}, "right", function() moveWindow("right") end)hs.hotkey.bind({"ctrl", "alt"}, "return", function() moveWindow("full") end)moveToUnitは画面を縦横0〜1の比率で見て、そこにウィンドウをはめてくれる関数です。{0, 0, 0.5, 1}なら「左上から、幅50%・高さ100%」。これにCtrl+Option+U/I/J/Kで四隅の4分割も足して、僕が実際に使う操作はだいたい網羅できました。
そして、アプリをひとつ巻き取るたび、メニューバーのアイコンがひとつ消える。これがいいですね。 便利だから〜というよりも、これが楽しいのではないかとさえ思えてきたりw掃除気分。
離席中もMacは働かせる:from Amphetamine
AIにコードを書かせて放置している間、席を立っても、ノートのフタを閉じても、処理を止めたくない。それだけ。たったそれだけのために、Amphetamineを入れていました。
Hammerspoonのhs.caffeinateでスリープは止められます。ただ、これで止まるのは「放っておくと寝る」まで。フタを閉じるとあっさり寝ます。離席中こそ閉じたいのに、ここが地味な穴でした。
調べると、フタ閉じスリープを切るにはpmset -a disablesleep 1を叩くしかない。でもこれはsudoが要る。離席のたびにパスワードを打つのも変なので、sudoersでpmsetだけパスワードなし(NOPASSWD)で通しておく前提にしました。許可するのはこの1コマンドだけ、に絞ったうえで(そもそもAmphetamineみたいな常駐アプリも、フタ閉じ制御は裏で特権を使ってやってるはずなので、それを自分の手でやるだけ、とも言えます)。
そのうえで「離席モード」をCtrl+Option+Lに。押すと画面をロックして、フタを閉じても15分は本体が動き続ける。sudoが通らないPC(NOPASSWD未設定)では、閉じて寝てしまう事故を防ぐために、そもそも発動しないようにしてあります。
local function setLidSleepDisabled(disabled) -- pmset でフタ閉じスリープを切る。sudo -n(非対話)なので sudoers の NOPASSWD 前提 local _, ok = hs.execute("/usr/bin/sudo -n /usr/bin/pmset -a disablesleep " .. (disabled and "1" or "0"), true) return ok == trueend
local function startAwayMode(seconds) if not setLidSleepDisabled(true) then hs.alert.show("離席モード使用不可: pmset の sudoers 未設定") return -- 切れないなら発動しない(フタ閉じ→スリープ事故の防止) end hs.timer.doAfter(seconds, function() setLidSleepDisabled(false) end) -- 15分で自動復帰 hs.timer.doAfter(0.6, hs.caffeinate.lockScreen) -- 一瞬アラートを見せてからロックend
hs.hotkey.bind({"ctrl", "alt"}, "l", function() startAwayMode(15 * 60) end)caffeinateで粘って、最後の一番固いところ(フタ閉じ)だけpmsetに頼る。きれいではないけど、これで「ロックして立ち去る、でもMacは働き続ける」が手に入りました。
おまけ:チートシート
機能を自分で足していけるのは楽しいんですが、ひとつ問題があって。僕は記憶力が無いです。妻にも3歩歩いたら忘れると毎日のように言われる。設定した時点で満足してしまって、肝心の「何のキーに割り当てたか」をすぐに忘れてしまいます。
なので、Ctrl+Option+/で今あるショートカット一覧を画面に出すチートシートを作りました。hs.canvasで半透明のパネルを描いて、登録済みのキーバインドを並べているだけです。

ちょっとだけ凝ったのは、Ctrl+Optionを押しっぱなしにすると0.35秒後に勝手に一覧が出てくるところ。「あれ、何だっけ」と迷ったときに、キーを押したまま固まっていると答えが出る。忘れる前提で作ってあるわけです。
-- Ctrl+Option を押しっぱなしにすると、少し遅れてチートシートが出るhs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(event) local flags = event:getFlags() if flags.ctrl and flags.alt and not flags.cmd and not flags.shift then showCtrlAltCheatsheetAfterDelay() -- 0.35秒後に表示 else cancelCtrlAltCheatsheet() end return falseend):start()flagsChangedは修飾キーの状態が変わるたびに飛んでくるイベントです。これを見て「今ちょうどCtrl+Optionだけ押されている」を検出しているようです。
Kindle の窓を毎回同じ場所に
最後はおまけのおまけ。
これはアプリの置き換えというより、僕専用の自動化です。
僕はKindleのスクショを撮るとき、ウィンドウ単位ではなく「画面のこの座標範囲」を固定で撮りたい。ヘッダーやUIを切り落として、本文だけをきれいに残したいからです(Shotomaticというツールに渡している)。そうすると、Kindleの窓が毎回同じ位置・同じ大きさにいてくれないと困る。
そこで、Ctrl+Option+Sで今の窓の位置を保存して、Ctrl+Option+Dでそこに戻す、というのを書きました。
-- Ctrl+Opt+S: 今の Kindle ウィンドウの位置・サイズを記録local frame = win:frame()local layout = { x = frame.x, y = frame.y, w = frame.w, h = frame.h }-- ホスト名ごとに json で保存(Macが変わっても困らないように)
-- Ctrl+Opt+D: 記録した位置に戻すwin:setFrame(hs.geometry.rect(layout.x, layout.y, layout.w, layout.h))座標をホスト名ごとのjsonに保存しているので、デスクトップとノートで画面サイズが違っても、それぞれの定位置を覚えていてくれます。ここまでくると、もう既製アプリで代用するものでもない。完全に、僕の作業専用です。
次にやってみたいこと:環境の変化で動かす
ここまで紹介したものは、全部「自分でホットキーを押して発動する」自動化です。僕が能動的にキーを叩いて、Macが応える。
でもHammerspoonは「自分が何もしていなくても、環境が変わったら動く」ことができるようなのです。例えば、WiFiが切り替わったとき、とか。
これが動けば、「会社のWiFiに繋いだら自動でスリープ防止をオンにする」みたいなことができる。さっき手で押していたショートカットを、環境のほうから勝手にやってくれるわけです。ここはまだ全然開拓できていなくて、研究の余地がありそうだなと思っています。
ちなみにクリップボード履歴も置き換えられそうな筆頭なんですが、これは今のところAlfredで満足しているので、無理に巻き取らなくていいかな、と。なんでもかんでも自分のコードにしようとすると、それはそれで疲れますしね。
まだまだやれることがありそうで、ちょっとしたお掃除好き(兼カスタマイズ好き)におすすめしたい、Hammerspoonでした。