Hammerspoon で Typing が止まったら通知する

キーを打ち込み続けないと Hammerspoon に怒られる

はてブでこんな記事を見つけた。

この「ノンストップ・ライティング」という手法は、経験則から効果があることが分かる。継続的に思考を書き連ねることによって思考を活性化できることは分かっていたので、この機会に習慣化することにした。

先程挙げた記事の中では毎日タスクとしてこなしていたが、経験則から考えるに日々のタスクではなく自身に刷り込まれた習慣(または癖)として実践することに効果があると思っている。例えば、難しい実装の設計を考えている時に頭の中で思考の無限ループが起こることがあるが、その思考を一つ一つ書き連ねていれば無限ループは起こりにくくなり、結果として早く結論が出る。個人的な経験則を論拠にして申し訳ないけど、少なくとも自分にとっては書き連ねつつ思考するほうが効率的であることがわかっている。

ここから「ノンストップ・ライティング」という手法を Continuous Writing と呼ぶことにする。(少し意訳しているが Code を書く時に Non と付いた名詞をあまり使いたくないという個人的な主義がありまして… NonStopwriting: false とか言われるとなにやってるのか直感的にわからなくなるから。)

Continuous Writing を習慣化するにあたり大事なのは、Continuous Writing が実践できていないことに気づくことだ。これは、そのとき解決したい問題が難しければ難しいほど、または Interrupt してきた事象が楽しければ楽しいほど(Facebook とか Slack とか)、Continuous Writing が実践できていないことを忘れがちだからだ。思考の渦に飲み込まれても短いスパンで Continuous Writing の世界に戻ってこれるようにしたい。

Hammerspoon で Key の押下を検知し、一定時間押下されない場合に通知を流す

そこで Hammerspoon を使って Key が押下されない時間が続くと通知が流れるようにした。これで、画面に集中してしまって Continuous Writing が止まっていても一定時間で気づくことができる。現に気づくことができるようになった。

この記事ではその設定方法を書く。

Hammerspoon について

HammerSpoon は MacOS の挙動を自動化を支援するツールだ。lua で処理を書くことできる。

Sierra で Karabiner の全機能が使えないのでお世話になった人も多いのではないだろうか。(最近は Karabiner-Elements で多くの機能が使えるようになってきている)

そのため、インストール方法や lua についてと言った初級な話はしない。

目的を細かく分ける

目的は「Key の押下を検知して一定時間押下がなければ通知を飛ばす」なので、以下の 3 つのことができれば、これらを組み合わせて仕組みを作れる。

  1. 通知を飛ばす
  2. Key 押下の検知
  3. 一定時間経ったらチェック

通知してみる

Hammerspoon の公式ドキュメントはとても充実しているので公式ドキュメントを読みながら進める。

通知をするには hs.notify.show(title, subTitle, information[, tag]) -> notification で notification object を生成して hs.notify:send() -> notificationObject を call するのが良さそうだ。

なので、通知しようとすると以下のようになる。

hs.notify.show("Hammerspoon", "Notify", "Test"):send()

Key の押下を検知する

Key の押下を検知するには hs.eventtap.new(types, fn) -> eventtap という API を使うのが良さそうだ。

A function that will be called when the specified event types occur.

となっているので、Key を押下したら通知を飛ばすコードは以下のようになる。

local eventtap = hs.eventtap.new({ hs.eventtap.event.types.keyDown }, function(event)
hs.notify.show("Hammerspoon", "Key Stroke Event", "Test"):send()
return false
end)

一定時間何かされないことを検知する

一定時間で何か確認する API ももちろん用意されている。 hs.timer.doEvery(interval, fn) -> timer だ。

なので、 5 秒経ったら通知を飛ばすコードは以下のようになる。

local timer = hs.timer.doEvery(noStrokeCheckInterval, function()
hs.notify.show("Hammerspoon", "5 seconds", "Test"):send()
end)
timer:start()

Mac が Sleep していたり Screen Lock がかかっているときは動作を止めたい

ここまで紹介した 3 つの API だけで 一定期間 Key が押下されない場合に通知を流す という目的は達成できる。だが、もう一歩進めて作業していない時に通知が走らないようにしたい。

こういった目的を達成する API も用意されている。 hs.caffeinate.watcher.new(fn) -> watcher だ。

定義する function には hs.caffeinate.watcher の Constants が引数として渡されるので状態に応じた条件節を書く。

補足: local 変数は GC に回収されるらしい

以下のような記事を見つけた。

まだ自分は出会ったことがないのだが、local 変数は GC に回収されるらしい。自分は scope を気にして 基本的に local 変数を使うようにしていたので watcher に関しては global scope を使うことにした。他の local 変数に関しては watcher の callback 内で使用されているので watcher が GC で回収されない限りは回収されないだろうと考えたからだ。正直 lua の GC 分かってないのでこれで大丈夫なのかはわからない。

完成形

最終的に以下のようなコードになった。

--
-- ContinuousWriting Support
-- ref. http://blog.gururimichi.com/entry/2017/08/28/190030
--
do
local noStrokeSconds = 0
local noStrokeCheckInterval = 30
local isStroked = true
local timer = hs.timer.doEvery(noStrokeCheckInterval, function()
if isStroked then
noStrokeSconds = 0
isStroked = false
else
noStrokeSconds = noStrokeSconds + noStrokeCheckInterval
hs.notify.show("Hammerspoon", "No Key Stroke Event", "During " .. noStrokeSconds .." seconds"):send()
end
end)
local eventtap = hs.eventtap.new({ hs.eventtap.event.types.keyDown }, function(event)
isStroked = true
return false
end)
local function startContinuousWriting()
if not timer:running() then
timer:start()
end
if not eventtap:isEnabled() then
eventtap:start()
end
end
local function stopContinuousWriting()
if timer:running() then
timer:stop()
end
if eventtap:isEnabled() then
eventtap:stop()
end
end
caffeinateWatcher = hs.caffeinate.watcher.new(function(eventType)
if eventType == hs.caffeinate.watcher.screensaverDidStart then
print("-- stopContinuousWriting: hs.caffeinate.watcher.screensaverDidStart")
stopNonStopWriting()
elseif eventType == hs.caffeinate.watcher.screensaverDidStop then
print("-- startNonStopWriting: hs.caffeinate.watcher.screensaverDidStop")
startNonStopWriting()
elseif eventType == hs.caffeinate.watcher.screensaverWillStop then
print("-- stopContinuousWriting: hs.caffeinate.watcher.screensaverWillStop")
stopContinuousWriting()
elseif eventType == hs.caffeinate.watcher.screensDidLock then
print("-- stopContinuousWriting: hs.caffeinate.watcher.screensDidLock")
stopContinuousWriting()
elseif eventType == hs.caffeinate.watcher.screensDidSleep then
print("-- stopContinuousWriting: hs.caffeinate.watcher.screensDidSleep")
stopContinuousWriting()
elseif eventType == hs.caffeinate.watcher.screensDidUnlock then
print("-- startContinuousWriting: hs.caffeinate.watcher.screensDidUnlock")
startContinuousWriting()
elseif eventType == hs.caffeinate.watcher.screensDidWake then
print("-- startContinuousWriting: hs.caffeinate.watcher.screensDidWake")
startContinuousWriting()
elseif eventType == hs.caffeinate.watcher.sessionDidBecomeActive then
print("-- startContinuousWriting: hs.caffeinate.watcher.sessionDidBecomeActive")
startContinuousWriting()
elseif eventType == hs.caffeinate.watcher.sessionDidResignActive then
print("-- startContinuousWriting: hs.caffeinate.watcher.sessionDidResignActive")
startContinuousWriting()
else
print("-- Do nothing about ContinuousWriting: " .. eventType)
end
end)
startContinuousWriting()
caffeinateWatcher:start()
end

まとめ

Continuous Writing を習慣化するために Hammerspoon で Key の押下を検知して一定時間押下がなければ通知を飛ばす仕組みを作った。まだ使いはじめて数時間だが思いの外タイプしていない時間があるようで役立っている。

キーを押下しなくなってから何秒経ったかも教えてくれる
A single golf clap? Or a long standing ovation?

By clapping more or less, you can signal to us which stories really stand out.