Maccy × Hammerspoon:macOSのクリップボード操作をスクリプトで自動化する
Maccy × Hammerspoon:macOSのクリップボード操作をスクリプトで自動化する
Maccy自体にはスクリプト用のAPIがないので、Hammerspoonと組み合わせて使います。Maccy Appはクリップボード履歴の保存と検索を担当し、macOS向けの無料Lua自動化ツールであるHammerspoonが、クリップボード内容の書き換え、クリップボードを経由しないテキスト入力、独自ホットキーからのMaccy Mac呼び出しを担当します。この2つを組み合わせれば、より大がかりなツールを入れなくても、クリップボード周りの自動化のほとんどをカバーできます。
そもそもなぜ組み合わせるのか?
Maccy Clipboardは意図的にシンプルに作られています。メニューバーのポップアップ、高速な検索、ピン留め、無視リストが主な機能です。私の知る限り、AppleScript辞書、CLI、URLスキームのいずれのAPIも公開されていません 。ローカルファーストでプライバシーを重視するツールとしては妥当な設計ですが、「トラッキングパラメータを除去してからペースト」のような条件付きの処理は、別の場所で行う必要があるということでもあります。
Hammerspoonが向いているのは、オープンソースでローカルで動作し、macOSのペーストボードやキーボードのAPIを直接扱えるからです。どちらもHomebrewでインストールできます。
brew install --cask maccy hammerspoon
続いて、システム設定 → プライバシーとセキュリティ → アクセシビリティでHammerspoonを許可します。これがないと、キー入力系のレシピはすべて動きません。設定ファイルは ~/.hammerspoon/init.lua にあり、メニューバーのHammerspoonアイコンから、または hs.reload() でリロードできます。
それぞれの得意分野は?
| やりたいこと | Maccyだけで可能か | Hammerspoonが必要か |
|---|---|---|
| 履歴の検索、ピン留め、ペースト | 可能 | 不要 |
| Maccyのポップアップ用ホットキーの変更 | 可能(設定 → 一般) | 不要 |
| 特定のアプリやコンテンツタイプを記録しない | 可能(設定 → 無視) | 不要 |
| ペースト前にクリップボードのテキストを書き換える | 不可 | 必要 |
| クリップボードを使わずにスニペットを入力する | 不可 | 必要 |
| 一定時間後にクリップボードを自動で空にする | 不可 | 必要 |
| スクリプトから履歴の特定の1件を削除する | 不可 | 確実にはできない |
目安として、Maccyに設定項目があるならそれを使い、Maccyでは表現できないロジックにだけLuaを使うのがおすすめです。
レシピ1:カスタムホットキーでMaccyを開くには?
単純にショートカットを変えたいだけなら、Hammerspoonは不要です。Maccy → 設定 → 一般でポップアップのショートカットを変更してください。Hammerspoonが活きるのは、アプリごとに別のショートカットにするなど、条件付きでトリガーしたい場合です。Maccyのデフォルトは ⇧⌘C です 。これをHammerspoonから発火させるには、次のように書きます。
local hyper = {"ctrl", "alt", "cmd"}
hs.hotkey.bind(hyper, "v", function()
hs.eventtap.keyStroke({"shift", "cmd"}, "c")
end)
必ず手元の環境で試してください。合成されたキーイベントでは、すべてのアプリのグローバルホットキーが反応するとは限りません 。また、同じキーの組み合わせを両方のツールに割り当てるのは避けましょう。先に登録したほうが優先され、もう一方はエラーも出さずに何も起きなくなります。
レシピ2:URLをペーストする前にトラッキングパラメータを除去するには?
これが定番のユースケースです。utm_* だらけのリンクをコピーし、ホットキーを押して、きれいなURLをペーストします。
local function stripTracking(url)
for _ = 1, 3 do -- 繰り返す:隣接するパラメータは1回の置換では取りこぼすため
url = url:gsub("([?&])utm_[%w_]+=[^&#]*&?", "%1")
url = url:gsub("([?&])fbclid=[^&#]*&?", "%1")
end
url = url:gsub("[?&]$", "")
return url
end
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "u", function()
local clip = hs.pasteboard.getContents()
if not clip or not clip:match("^https?://%S+$") then
hs.alert.show("クリップボードが単一のURLではありません")
return
end
hs.pasteboard.setContents(stripTracking(clip))
hs.timer.doAfter(0.05, function()
hs.eventtap.keyStroke({"cmd"}, "v")
end)
end)
50ミリ秒のディレイは重要です。ペーストボードへの書き込みと同時にペーストすると、古い内容が貼り付けられることがあります。なお、整形後のURLは、元のURLと並んでMaccyに新しい項目として記録されます。気になる場合はレシピ5を参照してください。
レシピ3:クリップボード履歴を汚さずにスニペットを貼り付けるには?
そもそもクリップボードを使わない、というのが答えです。hs.eventtap.keyStrokes は、キーを押したかのように文字を1つずつ入力するため、Maccyには一切記録されません。
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "e", function()
hs.eventtap.keyStrokes("you@example.com")
end)
実際に使ってみて気づく注意点は次のとおりです。長い文字列では、ペーストより入力のほうが遅くなります。ASCII以外の文字や一部の入力メソッドでは、挙動がおかしくなることがあります 。また、パスワード欄などのセキュア入力フィールドでは、macOSが合成キーストロークをブロックするため何も入力されません。
レシピ4:クリップボード上のJSONを整形するには?
hs.hotkey.bind({"ctrl", "alt", "cmd"}, "j", function()
local ok, data = pcall(hs.json.decode, hs.pasteboard.getContents() or "")
if ok and data then
hs.pasteboard.setContents(hs.json.encode(data, true))
hs.alert.show("JSONを整形しました")
else
hs.alert.show("クリップボードが有効なJSONではありません")
end
end)
正直に言うと制約があります。Luaのテーブルは順序を保持しないため、キーの並びがコピー元と変わってしまいます。順序が重要な用途では、hs.task で jq を呼び出すほうがよいでしょう。
レシピ5:Hammerspoonの書き込みでMaccyが散らからないようにするには?
多くのクリップボードツールは、「transient(一時的)」というペーストボードタイプの慣例を尊重しています。これは、管理ツールに対してその項目をスキップするよう伝えるための仕組みです。Maccyにはデフォルトで無視するタイプのリストがあり、設定 → 無視で追加できます 。そこに org.nspasteboard.TransientType を追加したうえで、自分の書き込みにタグを付けます。
hs.pasteboard.setContents(cleaned)
hs.pasteboard.writeDataForUTI(nil, "org.nspasteboard.TransientType", "", true) --
これは実験的な方法として扱ってください。頼りにする前に、Maccy側で本当にその項目がスキップされているか確認しましょう。
レシピ6:クリップボード上の機密情報を自動で消せる?
部分的には可能です。ペーストボードのウォッチャーでAPIキーらしき文字列を検出し、30秒後にクリップボードをクリアできます。
local function looksSecret(s)
if #s < 20 or s:find("%s") then return false end
return s:match("^sk%-[%w_%-]+$") or s:match("^AKIA[%u%d]+$") or s:match("^ghp_%w+$")
end
local clearTimer
clipWatcher = hs.pasteboard.watcher.new(function(content) -- グローバル変数のままにする。さもないとガベージコレクションされる
if type(content) == "string" and looksSecret(content) then
if clearTimer then clearTimer:stop() end
local count = hs.pasteboard.changeCount()
clearTimer = hs.timer.doAfter(30, function()
if hs.pasteboard.changeCount() == count then
hs.pasteboard.clearContents()
end
end)
end
end)
clipWatcher:start()
重要なのはその限界です。ウォッチャーが反応した時点で、Maccyはすでにその機密情報を保存しています。システムのクリップボードをクリアしても、Maccyの履歴には影響しません。機密情報については、Maccyの無視設定で、コピー元のアプリ(パスワードマネージャーなど)またはパターンによるルールを追加し、そもそも保存されないようにしてください。Hammerspoonはあくまで第二の防御層として使います。ウォッチャーのコールバック引数は、お使いのHammerspoonのバージョンで確認してください 。
よくあるトラブルと対処法
| 症状 | 考えられる原因 | 対処法 |
|---|---|---|
| ホットキーが反応しない | アクセシビリティの許可がない | 「プライバシーとセキュリティ」でHammerspoonを再度有効にし、再起動する |
| 古いクリップボード内容が貼り付けられる | 書き込みと⌘Vの間にディレイがない | ペースト処理を hs.timer.doAfter(0.05, …) で包む |
| しばらくするとウォッチャーが動かなくなる | ウォッチャーのオブジェクトがガベージコレクションされた | グローバル変数に保持する |
| 特定のアプリでしかショートカットが効かない | そのアプリ自身のキー割り当てと競合している | Hyperキーの組み合わせ(⌃⌥⌘+英字)を使う |
| 入力欄に何も入力されない | セキュア入力が有効になっている | 仕様どおりの動作。macOSが意図的にブロックしている |
ペーストボード監視とプライバシーについて
最近のmacOSでは、アプリがペーストボードを読み取ったときにユーザーへ通知する方向に進んでいます 。ウォッチャースクリプトはコピーしたものをすべて読み取ります。Luaコードはローカルに保ち、クリップボードの内容をディスクにログとして書き出さないでください。ネットで見つけた設定は、実行前に必ず中身を確認しましょう。
ここからどう進めるか
まずはレシピ2から始めるのがおすすめです。短く、すぐに役立ち、ほかのレシピの土台になる「書き込んでからペースト」というパターンが身につきます。そのうえで、どの処理にホットキーを割り当てる価値があるかを考えてみてください。日々のクリップボード作業の中で、1つだけ繰り返しの手間をなくせるとしたら、何を選びますか?
Maccyは無料のオープンソースソフトウェアです。maccy.appから入手できます。