GUIスクリプティングなAppleScript環境を快適にする
GUIスクリプティングはとっても便利だ。これはOSを操作するAppleScriptと言えるので、基本的にマウスやキーボードの操作なら何でも操作できる可能性を秘めている。
しかし、メニューやショートカットキーを操作するのは意外と大変。操作するまでの事前の手順や書き方が煩雑だったり、ちょっとしたことで操作できない状態によく陥る...。もっとシンプルに、確実に操作できる環境を手に入れたいのだ!
基本
- 例:選択項目をSpotlight検索する
- キーワードをコピー&ペーストするよりは、選択中の文字列がそのままキーワードになればもっと幸せになるかと思って。
(*ファイル名:spotlight_with_selection.scpt*) delay 0.5 --Quicksilverから利用する時は、このひと呼吸が必要 tell application "System Events" --ショートカット操作をする限り、GUIスクリプティングは無効でも動作する --tell application "APP_NAME" to activate--必要に応じて、アプリケーション"APP_NAME"をアクティブにする keystroke "c" using {command down} --command-C (コピー) keystroke space using {control down} --command-space (Spotlightの起動) delay 0.1 --ショートカットを連携させる時に、ひと呼吸が必要な場合もある keystroke "v" using {command down} --command-V (ペースト) end tell
- 例:メールを送信した時にエンコードを自動に設定する
- メールを返信した時の送信済みメールの文字化けを自動的に解消する。自分で書いたメールが文字化けするなんて堪えられない...。
(*ファイル名:mail_encoding_auto.scpt*) on adding folder items to this_folder after receiving added_items
tell application "Finder" to open added_items tell application "System Events" tell process "Mail" set frontmost to true keystroke "1" using {command down, option down} --自動 keystroke "w" using {command down} click menu item "メッセージビューア" of menu "ウインドウ" of menu bar item "ウインドウ" of menu bar 1 keystroke "w" using {command down} keystroke "n" using {command down, option down} end tell end tell end adding folder items to
親切バージョン
- もし、システム環境設定 >> ユニバーサルアクセス >> 補助装置にアクセスできるようにする にチェックがないとGUIスクリプティングが動かず悩むことになる。
- だから、以下のようなチェックをしておくと、人に優しいスクリプトになる。
delay 0.5 --Quicksilverを利用する時は、このひと呼吸が必要 tell application "System Events" if UI elements enabled then --ショートカット操作をする限り、GUIスクリプティングは無効でも動作する --操作対象のアプリを最前面に設定する(スクリプトメニューから実行する場合必要) tell process "Mail" set frontmost to true keystroke "1" using {command down, option down} --自動 keystroke "w" using {command down} click (menu item "メッセージビューア" of menu "ウインドウ" of menu bar item "ウインドウ" of menu bar 1) delay 0.1 --ショートカットを連携させる時に、ひと呼吸が必要な場合もある keystroke "w" using {command down} keystroke "n" using {command down, option down} end tell else tell application "System Preferences" activate set current pane to pane "com.apple.preference.universalaccess" set msg to "GUIスクリプティングが利用可能になっていません。\n\"補助装置にアクセスできるようにする\" にチェックを入れて続けますか?" display dialog msg buttons {"キャンセル", "チェックを入れて続ける"} with icon note end tell set UI elements enabled to true delay 1 tell application "System Preferences" to quit delay 1 end if end tell
- 上記を雛形として、あとは tell process "xxxx" 〜 end tell ブロック内を必要なコードに置き換えれば良いのだが、
- 毎回、コピー&ペーストをして書き換えていたのでは、長期的にはプログラマに優しくない。
- コードの重複を生むみ、修正するときの修正箇所の多さに四苦八苦する。
- コードの見渡しが悪く、全体を読み難くなる。(長ったらしいコードがダラダラと続くので)
そこで、機能ごとに再利用できる一般的な部品として切り出しておくことが重要になる。
GUIスクリプティングのチェックを一般化する
--GUIスクリプティングが無効なら、有効にすることを勧めるメッセージを出力する on check() tell application "System Events" if UI elements enabled is false then tell application "System Preferences" activate set current pane to pane "com.apple.preference.universalaccess" set msg to "GUIスクリプティングが利用可能になっていません。\n\"補助装置にアクセスできるようにする\" にチェックを入れて続けますか?" display dialog msg buttons {"キャンセル", "チェックを入れて続ける"} with icon note end tell set UI elements enabled to true delay 1 tell application "System Preferences" to quit delay 1 end if end tell end check
メニュー操作を便利にする
- メニュー操作のポイントは、実際にメニュー操作を行う以下のコード部分。
click menu item "メッセージビューア" of menu "ウインドウ" of menu bar item "ウインドウ" of menu bar 1
- of を利用した表現は見難いので、以下のように 's を利用した逆順の表現に変形してみた。
click menu bar 1's menu bar item "ウインドウ"'s menu "ウインドウ"'s menu item "メッセージビューア"
- そして、操作するメニューの階層によって、コード表現は以下のように変化した。(3階層まである編集メニューの例に変更)
click menu bar 1's menu bar item "編集" --編集 click menu bar 1's menu bar item "編集"'s menu "編集"'s menu item "検索" --編集/検索 click menu bar 1's menu bar item "編集"'s menu "編集"'s menu item "検索"'s menu "検索"'s menu item "次を検索" --編集/検索/次を検索
- つまり、1階層目のメニューバーだけは特殊だけど、2階層目以降は同じパターンの繰り返しなのだ。
- 操作したい最終階層の'sを削除すれば、それがメニューを操作するコードになる。
- 上記のような仕組みは理解できたのだが、人間が見て一目で分かる書き方とは言えない...。
- 理想としては、"編集/検索/次を検索" のようなパス文字列でメニュー操作を実現したい。
- 最初はメニューパスが何回層あるかを確認して if で階層ごとに分けて対応してみた。
- しかし、これでは操作する階層分だけ対応するコードを書く必要がある。
- せいぜい5階層ぐらいまで対応すれば、ほとんどの操作は問題ないのかもしれないが...。
- そこで、ちょっと工夫して、最終的には以下のようにしてみた。(ついでに、アイテム番号によるパス指定にも対応した。)
on click_menu(app_name, menu_path) if menu_path is "" then error "menu_path が入力されていません。" end if if app_name is "" then set app_name to frontmost_app() end if set mp to split(menu_path, "/") tell application "System Events" tell process app_name if "AppleScript Runner" is in my every_process() or frontmost is false then set frontmost to true end if if mp's length = 1 then menu bar 1's (menu bar item (my to_number(mp's item 1))) else --menu bar 1's menu bar item (mp's item 1)'s menu (mp's item 1) menu bar 1's (menu bar item (my to_number(mp's item 1)))'s menu 1 repeat with i from 2 to mp's length if i < mp's length then --result's menu item (mp's item i)'s menu (mp's item i) result's (menu item (my to_number(mp's item i)))'s menu 1 else result's (menu item (my to_number(mp's item i))) end if end repeat end if click result --click:クリックする/pick:選択する--ほぼ同等だが、アイコンメニューにはclickが必須 delay 0.1 --連続してメニューを操作する時、ひと呼吸必要 end tell end tell end click_menu --起動中のアプリケーション名をリストで取得する on every_process() tell application "System Events" processes's name end tell end every_process --最前面のアプリケーション名を取得する on frontmost_app() tell application "System Events" set name_list to processes's name whose frontmost is true name_list's first item end tell end frontmost_app on split(sourceText, separator) if sourceText = "" then return {} set oldDelimiters to AppleScript's text item delimiters set AppleScript's text item delimiters to {separator} set theList to text items of sourceText set AppleScript's text item delimiters to oldDelimiters return theList end split on to_number(str) try str as number on error str end try end to_number
- これで、以下のようにものすごくシンプルに書けるようになった!
click_menu("Mail", "ウインドウ/メッセージビューア") --以下のコードと同等 (* tell application "System Events" tell process "Mail" set frontmost to true click menu item "メッセージビューア" of menu "ウインドウ" of menu bar item "ウインドウ" of menu bar 1 end tell end tell *)
キーボードショートカットを便利にする
- キー操作でも、上記メニュー操作と同じように、シンプルな書き方と便利さを追求してみた。
on shortcut(app_name, key_text) if key_text is "" or key_text is {} then error "key_text が入力されていません。" end if if (count of key_text) = 1 then set key_list to split(key_text's first item, "-") else set key_list to split(key_text, "-") end if set last_key to downcase(key_list's last item) set modifier_key to {} if "command" is in key_list then set modifier_key to modifier_key & command down if "option" is in key_list then set modifier_key to modifier_key & option down if "control" is in key_list then set modifier_key to modifier_key & control down if "shift" is in key_list then set modifier_key to modifier_key & shift down if last_key is "delete" then set last_key to 51 --delete else if last_key is "esc" then set last_key to 53 --esc else if last_key is "←" then set last_key to 123 --← else if last_key is "→" then set last_key to 124 --→ else if last_key is "↓" then set last_key to 125 --↓ else if last_key is "↑" then set last_key to 126 --↑ else if last_key is "space" then set last_key to space else if last_key is "tab" then set last_key to tab else if last_key is "return" then set last_key to return else if last_key's length is 3 then try set last_key to last_key as number end try end if press_key(app_name, last_key, modifier_key) end shortcut --キー操作を実行する --利用例: -- press_key("1", command down) -- press_key(126, command down) on press_key(app_name, normal_key, modifier_key) if app_name is "" then set app_name to frontmost_app() end if tell application "System Events" tell process app_name if "AppleScript Runner" is in my every_process() or frontmost is false then set frontmost to true end if if my is_number(normal_key) then key code normal_key using modifier_key else keystroke normal_key using modifier_key end if end tell end tell end press_key --起動中のアプリケーション名をリストで取得する on every_process() tell application "System Events" processes's name end tell end every_process --最前面のアプリケーション名を取得する on frontmost_app() tell application "System Events" set name_list to processes's name whose frontmost is true name_list's first item end tell end frontmost_app on do_ruby_script(ruby_code) set shell_code to "ruby -e \"puts(" & ruby_code & ")\"" do shell script shell_code end do_ruby_script on downcase(str) do_ruby_script("'" & str & "'.downcase") end downcase on split(sourceText, separator) if sourceText = "" then return {} set oldDelimiters to AppleScript's text item delimiters set AppleScript's text item delimiters to {separator} set theList to text items of sourceText set AppleScript's text item delimiters to oldDelimiters return theList end split on is_number(num) if num = {} or num = "" then false else (count of num) is 0 end if end is_number
- これで、以下のようにシンプルに書けるようになった!
shortcut("Mail", "command-option-N") --以下のコードと同等 (* tell application "System Events" tell process "Mail" set frontmost to true keystroke "n" using {command down, option down} end tell end tell *)
最初のひと呼吸
- 出来上がった便利なAppleScriptは、自分の場合、Quicksilverでショートカットを割り当てて実行することが多い。
- この時ちょっとしたコツが必要で、スクリプトを実行する最初のところで、ひと呼吸おく必要があるのだ。(環境にもよるが0.2〜0.5秒くらい)
- このひと呼吸をおかないと、せっかく作った便利なスクリプトが正常に動作しないことがある。(理由は分からない...。)
- このひと呼吸を click_menu や shortcut に織り交ぜてしまうと、操作する度にひと呼吸おいてしまって、処理が遅くなってしまう。
- 処理の最初に delay 0.5 と書いても良いのだが、Quicksilverのショートカットから実行するときだけ必要なので、以下のようにしてみた。
--初期化処理(Quicksilverからの起動なら、ひと呼吸置いて実行する) on init() if is_from_quicksilver() then delay 0.2 end if end init --Quicksilverから起動しているかどうか on is_from_quicksilver() try my name as text false on error true end try end is_from_quicksilver
ライブラリとして保存して利用
- 以上、出来上がったスクリプトを ~/Library/Scripts/_gui.scpt として保存した。
- このスクリプト(_gui.scpt)は、以下のようにして再利用することができる。(基本の2例を書き直してみた)
(*ファイル名:spotlight_with_selection.scpt*) property GUI : load script file ((path to scripts folder as text) & "_gui.scpt") --set GUI to load script file ((path to scripts folder as text) & "_keyboard.scpt") GUI's init() --GUI's check() --キー操作のみの場合は不要 GUI's shortcut("", "command-c") GUI's shortcut("", "control-space") GUI's shortcut("", "command-V")
(*ファイル名:mail_encoding_auto.scpt*) property GUI : load script file ((path to scripts folder as text) & "_gui.scpt") on adding folder items to this_folder after receiving added_items tell application "Finder" to open added_items GUI's init() GUI's check() GUI's shortcut("Mail", "command-option-1") GUI's shortcut("", "command-W") GUI's click_menu("", "ウインドウ/メッセージビューア") GUI's shortcut("", "command-W") GUI's shortcut("", "command-option-N") end adding folder items to
かなりシンプルに書けるようになった!便利なスクリプトをいっぱい作って楽しよう!