macOS向けのチャット自動送信ツールを作っていて、テキストの送信方法をどうするか悩んでいました。AppleScript、pyautogui、clipboard、CGEventと4つの選択肢があって、それぞれメリット・デメリットがあります。
1. 最初はAppleScriptから始めた
最初に試したのはAppleScriptでした。macOSの標準技術だし、安定してそうだなと思ったんです。
tell application "Claude" to activate
tell application "System Events"
key code 9 using command down # Cmd+V
delay 0.3
key code 36 using command down # Cmd+Enter
end tell
Code language: PHP (php)
実装自体は簡単でした。アプリを指定して、キーコードを送信するだけ。確かに動きます。
でも問題がありました。
1.1. アクセシビリティ権限が必要
System Eventsでキー操作を送信するには、アクセシビリティ権限が必要なんです。システム設定を開いて、手動で許可する必要があります。
システム設定 > プライバシーとセキュリティ > アクセシビリティ
初心者には結構ハードルが高いですよね。しかもエラーメッセージが「osascriptにはキー操作の送信は許可されません」とか出るんですが、これを見て何をすればいいか分かる人は少ないと思います。
それに、もっと気になることがありました。
1.2. 送信中にキーボードが使えない問題
AppleScriptで送信している間、自分のキーボードやマウスが使えなくなるんです。送信処理が終わるまで、他の作業ができません。
これは地味にストレスでした。
「もっと良い方法はないかな?」と思って、次に試したのがpyautoguiです。
2. pyautoguiはシンプルで良かった
pyautoguiはPythonのライブラリで、キーボード操作をシミュレートできます。
import pyautogui
pyautogui.hotkey('command', 'v')
time.sleep(0.3)
pyautogui.hotkey('command', 'return')
Code language: JavaScript (javascript)
実装がすごくシンプル。コードを見れば何をしているか一目瞭然です。
それに、アクセシビリティ権限が不要でした。これは大きなメリットですね。
2.1. でもやっぱり操作がブロックされる
pyautoguiも結局、送信中はキーボードが使えませんでした。これはAppleScriptと同じ問題です。
送信中に他の作業をしようとすると、変なところにテキストが入力されたりします。送信が完了するまで、じっと待つしかありません。
「ユーザーの操作に干渉しない方法はないのかな?」
そう思って調べていたら、CGEventという方法を見つけました。
3. CGEventとの出会い
CGEventは、macOSのシステムレベルでイベントを送信する仕組みです。pyobjcというライブラリを使います。
最初は「難しそうだな」と思ったんですが、実際に試してみると驚きました。
from Quartz import CGEventCreateKeyboardEvent, CGEventPost
# キーダウンイベント
event_down = CGEventCreateKeyboardEvent(None, keycode, True)
CGEventSetFlags(event_down, kCGEventFlagMaskCommand)
CGEventPost(kCGHIDEventTap, event_down)
# キーアップイベント
event_up = CGEventCreateKeyboardEvent(None, keycode, False)
CGEventPost(kCGHIDEventTap, event_up)
Code language: PHP (php)
3.1. 送信中も自由に操作できる
CGEventで送信している間、普通にキーボードやマウスが使えるんです。
これにはちょっと感動しました。送信処理が走っているのに、別のウィンドウで作業できる。メールを書いたり、ブラウザを見たり、全く問題ありません。
なぜこんなことができるのか調べてみました。
4. なぜCGEventは干渉しないのか
違いはイベントを送る「レベル」にあります。
AppleScriptとpyautoguiは、アプリケーション層やGUI層で動作します。つまり、実際のキーボード入力をシミュレートしているんです。だから、ユーザーが同時にキーボードを使うと競合してしまいます。
一方、CGEventはシステムカーネル層で動作します。システムに直接「このキーが押されたことにして」と指示を出す感じです。ユーザーの実際のキーボード入力とは別の経路なので、干渉しません。
例えるなら、AppleScriptとpyautoguiは「ロボットの手でキーボードを叩く」方法です。人間の手とぶつかります。
CGEventは「キーボードを経由せずに、直接コンピュータの脳に指令を送る」方法です。手がぶつかりません。
なるほど、と思いました。
5. 操作への干渉と権限の必要性
4つの方法を実際に試した結果を表にまとめてみます。
| 方式 | 送信中の操作 | アクセシビリティ権限 | 実装の難易度 | 依存ライブラリ |
|---|---|---|---|---|
| AppleScript | ブロックされる | 必要 | 中程度。スクリプトの文法に慣れが必要。 | |
| pyautogui | ブロックされる | 不要 | 一番簡単。 | pyautogui (軽量で、数MB) |
| clipboard | ブロックされる | 必要(AppleScript使用時) | 結局AppleScriptかpyautoguiを使うので、独自のメリットはあまりありません。 | |
| CGEvent | 自由に使える | 不要 | 中程度。キーコードの理解が必要 | pyobjc (約50MBでそこそこ大きい) |
pyautoguiの方が、実装が簡単で、すぐ動きます。権限設定も不要です。送信中に他の作業をしたいという要求がなければ、これで十分だと思います。
CGEventは権限が不要なのも嬉しいポイントです。pyobjcがある環境では干渉なしで動作します。
AppleScriptとclipboard方式は、もう使わないと思います。AppleScriptは古い技術で、権限も必要。clipboard方式は、結局AppleScriptかpyautoguiを使うので、独自のメリットがありません。
5.1. ハイブリッド方式という解決策
「pyobjcが入っていない環境ではどうするか?」という問題がありました。
そこで考えたのが、ハイブリッド方式です。
class HybridSender:
def __init__(self):
try:
from Quartz import CGEventCreateKeyboardEvent
self.method = "CGEvent"
print("CGEvent方式を使用(干渉なし)")
except ImportError:
self.method = "pyautogui"
print("pyautogui方式を使用(フォールバック)")
pyobjcがあればCGEventを使う。なければpyautoguiにフォールバックする。
これなら、どんな環境でも動作します。そして、pyobjcがある環境では最高のユーザー体験を提供できます。
6. CGEventは思ったより簡単
最初は「難しそう」と思っていたCGEventですが、実際に書いてみると、そこまで複雑ではありませんでした。
キーコードさえ分かれば、あとは定型的なコードです。
KEY_V = 0x09 # V
KEY_RETURN = 0x24 # Return
Code language: PHP (php)
macOSのキーコード一覧を見れば、必要なキーはすぐ分かります。
送信前のクリップボードとアクティブアプリを保存して、送信後に復元する処理も追加しました。
これがないと、ユーザーがコピーしていた内容が消えてしまいます。ちょっとしたことですが、ユーザー体験が大きく向上します。
6.1. タイミング調整は必要
どの方式でも、キー送信後にちょっと待つ必要があります。
pyautogui.hotkey('command', 'v')
time.sleep(0.3) # ここ大事
pyautogui.hotkey('command', 'return')
Code language: PHP (php)
0.3秒くらい待たないと、アプリがペーストを処理する前に送信してしまって、空のメッセージが送られたりします。
この待機時間は、アプリによって調整が必要かもしれません。
7. まとめ
macOSでテキストを自動送信する方法を4つ試してみました。
CGEvent方式が最強でした。送信中も自由に操作できて、権限も不要。ユーザー体験が圧倒的に良いです。
ただし、pyobjcが必要で、macOS専用です。
実用的には、CGEvent + pyautoguiのハイブリッドがベストだと思います。pyobjcがあれば最高の体験、なくても確実に動作します。
実際にツールを作っている方の参考になれば嬉しいです。