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から入手できます。

Similar Posts