カテゴリー: shell script

各種GUIアプリ書類のオープン速度を向上するためにUIアニメーションの一時停止を

Posted on by

AppleScriptから各種GUIアプリを操作すると、各種GUIアプリ上の操作に要する時間がそのまま必要になります。端的なところでいえば、書類のオープン時のアニメーションも毎回行われるわけで、割と無意味なアニメーションに、そこそこの時間が消費されています。

そのUIアニメーションを無効化する処理について調査したところ、割と簡単に情報が見つかりました。

ここに掲載しているサンプルAppleScript(要・Metadata Lib)では、Finder上の最前面のウィンドウで選択中のフォルダ以下にあるPages書類をSpotlightの機能を用いてすべてピックアップし、それらを「UIアニメーションあり」「UIアニメーションなし」の条件でオープン/クローズだけ行うものです。

結論をいうと、M2 MacBook Air上で通常のUIアニメーションつきの処理では68秒かかっていたものが、UIアニメーションを無効化するだけで43秒で処理できました(125個のPages書類で実験)。

これはちょうど、M1 MacからM4 Macに買い替えるぐらいの処理速度の向上が、ただUIアニメーションを無効にするだけで実現できたということを意味します。

1書類あたり0.2秒の処理速度向上が見られたわけですが、これが400個の書類を処理するとなれば80秒も変わってくるわけで、割と洒落にならない速度向上が実現できます。もっと早く調べておけばよかったと思うことしきりです。

AppleScript名:UI アニメーションの無効化による速度向上実験.scptd

– Created by: Takaaki Naganoya
– Created on: 2025年09月22日

– Copyright © 2025 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
use mdLib : script "Metadata Lib"

tell application "Finder"
set aSel to (selection as alias list)
if aSel = {} then
set origPath to choose folder
else
set origPath to (first item of aSel)
end if
end tell

–SpotlightでPages書類を検出
set aResList to perform search in folders {origPath} predicate string "kMDItemContentType == %@ || kMDItemContentType == %@" search arguments {"com.apple.iwork.pages.sffpages", "com.apple.iwork.pages.pages"}
–return aResList

–UI Animationを許可
set a1Dat to current application’s NSDate’s timeIntervalSinceReferenceDate()
openClosePagesDocs(aResList, true) of me
set b1Dat to current application’s NSDate’s timeIntervalSinceReferenceDate()
set c1Dat to b1Dat – a1Dat

–UI Animationを禁止
set a2Dat to current application’s NSDate’s timeIntervalSinceReferenceDate()
openClosePagesDocs(aResList, false) of me
set b2Dat to current application’s NSDate’s timeIntervalSinceReferenceDate()
set c2Dat to b2Dat – a2Dat

return {c1Dat, c2Dat, length of aResList}

on openClosePagesDocs(aResList, animationF)
if animationF = true then
–UI Animationをオンにする
try
do shell script "defaults delete NSGlobalDomain NSAutomaticWindowAnimationsEnabled"
end try
else –UI Animationをオフにする
do shell script "defaults write NSGlobalDomain NSAutomaticWindowAnimationsEnabled -bool NO"
end if

–Pages書類を順次オープンしてタイトル(?)を取得
set pagesTitleList to {}
repeat with i in aResList
set aFile to POSIX file i

tell application "Pages"
open (aFile as alias)
end tell

tell application "Pages"
close front document saving no
end tell

end repeat

if animationF = false then
–UI Animationをオンにする
try
do shell script "defaults delete NSGlobalDomain NSAutomaticWindowAnimationsEnabled"
end try
end if
end openClosePagesDocs

ステージマネージャのON_OFF

macOS 13で搭載された「ステージマネージャ」機能のオン/オフを行うAppleScriptです。

ステージマネージャは、最前面のアプリのウィンドウだけを表示するようにする仕組みで、iPadOSに搭載されたものがそのままmacOSにも搭載されました。ドラッグ&ドロップをこのステージマネージャに対して行えるのと、表示ウィンドウを最前面のものだけに切り替えるものです。

つまり、マルチウィンドウのGUIに不慣れなユーザーのために用意された機能です。Windowsユーザー向けに用意した、ともいえるでしょう。

このステージマネージャのOn/Offを行います。動作内容はご覧のとおり、単にshellコマンドを呼び出しているだけです。現在オンになっているかどうかも、defaults readコマンドで同様に実行できることでしょう。


さんかく真っ先にオフにして、二度とオンにすることはなかったステージマネージャ(画面左端)


さんかく元に戻ると安心します

AppleScript名:ステージマネージャのON_OFF

– Created by: Takaaki Naganoya
– Created on: 2024年11月15日

– Copyright © 2024 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set aBool to true
stageManagerControl(aBool) of me

delay 5

set aBool to false
stageManagerControl(aBool) of me

on stageManagerControl(aBool as boolean)
set sText to "defaults write com.apple.WindowManager GloballyEnabled -bool " & (aBool as string)
do shell script sText
end stageManagerControl

マウントしたディスクイメージから元のdmgファイルのパスを取得

マウント中のディスクイメージ(.dmg)ファイルのパスを求めるAppleScriptです。

macOS 13.7.1上で作成し、15.1上でも動作確認していますが、それほど込み入った機能は使っていないので、OSバージョン依存はないことでしょう。

実際に、オープンソースのPDFビューワー「Skim」の各バージョンのdmgファイルをダウンロードして、sdef(AppleScript用語説明)ファイルを収集してバージョンごとの変化を追う作業を行なったときに、バージョン判定を行うために元のdmgファイルのパスを求める処理を行なったものです。


さんかくマウントしたディスクイメージ(Skim)


さんかくマウント元のディスクイメージファイル


さんかくマウントしたディスクイメージからパス情報を取得することで、一部ファイルを取り出してコピーする際にバージョン情報を反映させた

AppleScript名:マウントしたディスクイメージから元のdmgファイルのパスを取得.scpt

– Created by: Takaaki Naganoya
– Created on: 2024年10月16日

– Copyright © 2024 Piyomaru Software, All Rights Reserved

use AppleScript
use framework "Foundation"
use scripting additions

set dName to "Skim"

–Disk Imageのマウントを確認
tell application "Finder"
set dexRes to (get exists of disk dName)
end tell
if dexRes = false then error "指定のディスクイメージ「" & dName & "」はマウントされていません。" –マウントされていなかった

set aRes to getMountedDiskImageInfo("image-path : ") of me
–> "/Users/me/Downloads/Skim-1.4.6.dmg"

on getMountedDiskImageInfo(targMark)
try
set aRes to do shell script "hdiutil info"
on error
return false
end try

set aResList to paragraphs of aRes
repeat with i in aResList
set j to contents of i
if j begins with targMark then
set aOff to offset of targMark in j
set aLen to length of targMark
set aRes to text (aLen + 1) thru -1 of j
return aRes
end if
end repeat
return false
end getMountedDiskImageInfo

ディスプレイをスリープ状態にして処理続行

特定のディスプレイのみ消灯して処理続行、という処理を行なってみたかったのですが、いろいろ調べた結果、すべてのディスプレイを消灯する方法しか見つかりませんでした。

それでも、寝る前に電子書籍1冊分のデータをPDFにすべて書き出して連結する、といったAppleScriptを実行するのに、ディスプレイをあらかじめスリープ状態にしたあとで、処理終了後にコンピュータごとsleepにしてもよさそうです。

1000ページ分の電子書籍のPages書類をすべてPDFに書き出して、ファイル名順に連結してもM1 Mac miniで7分ぐらいで処理できるので、この程度の処理だとディスプレイを消して処理する意義はそれほどないのかもしれませんけれども。

AppleScript名:ディスプレイをスリープ状態にして処理続行.scpt
do shell script "pmset displaysleepnow"
beep 10

Keynoteの表の選択中のセルのデータをDeepLで翻訳して書き戻す

Keynoteの最前面の書類中の、現在表示中のスライドの表の選択中のセル中のテキストを取得してDeepLのREST APIを呼び出して指定言語に翻訳し、表のセルに翻訳後のテキストを書き戻すAppleScriptです。

DeepLのREST API呼び出しのためには、DeepL SE社のWebサイトで「DeepL API Free」(無料コース)か「DeepL API Pro」プランにサインアップして、API Keyを取得して、プログラムリスト中に記入したうえで実行してください。


さんかく実行前。Keynote書類上の表の翻訳対象のセルを選択して実行


さんかく実行後。Keynote書類上の表の翻訳対象のセルをに翻訳後の内容をストア

実際に使ってみると、けっこう翻訳に時間がかかるのと、一度翻訳した同じフレーズを再度翻訳させるのはコストがかかるため、ローカルに「翻訳キャッシュ」を作って、翻訳ずみの内容を再翻訳しないように工夫する必要がありそうです。

AppleScript名:Keynoteの表の選択中のセルのデータをDeepLで翻訳して書き戻す.scpt

– Created by: Takaaki Naganoya
– Created on: 2023年01月30日

– Copyright © 2023 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

property NSString : a reference to current application’s NSString
property NSCountedSet : a reference to current application’s NSCountedSet
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding

set myAPIKey to "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
set myTargLang to "EN" –翻訳ターゲット言語

set aTableDat to returnSelectedTableCellDataOnCurrentSlide() of me
–> {"プロパティ項目", "データ型", "読み/書き", "内容(サンプル)", "説明"}

set nList to {}
repeat with i in aTableDat
set j to contents of i
set tRes to translateWithDeepL(j, myAPIKey, myTargLang) of me
set the end of nList to tRes
end repeat

–表に翻訳した内容を書き戻す
storeSelectedTableCellDataOnCurrentSlide(nList) of me

on storeSelectedTableCellDataOnCurrentSlide(sList)
tell application "Keynote"
tell front document
tell current slide
try
set theTable to first table whose class of selection range is range
on error
return false –何も選択されてなかった場合
end try

tell theTable
set cList to every cell of selection range
if (length of cList) is not equal to (length of sList) then error

set aCount to 1
repeat with i in cList
set j to contents of i
tell j
set value of it to (contents of item aCount of sList)
end tell
set aCount to aCount + 1
end repeat
end tell
end tell
end tell
end tell
end storeSelectedTableCellDataOnCurrentSlide

on returnSelectedTableCellDataOnCurrentSlide()
tell application "Keynote"
tell front document
tell current slide
try
set theTable to first table whose class of selection range is range
on error
return false –何も選択されてなかった場合
end try

tell theTable
set vList to value of every cell of selection range
set cCount to count of column of selection range
set rCount to count of row of selection range

–複数行選択されていた場合にはエラーを返すなどの処理の布石
return vList
end tell
end tell
end tell
end tell
end returnSelectedTableCellDataOnCurrentSlide

–DeepLのAPIを呼び出して翻訳する
on translateWithDeepL(myText, myAPIKey, myTargLang)
set sText to "curl -X POST ’https://api-free.deepl.com/v2/translate’ -H ’Authorization: DeepL-Auth-Key " & myAPIKey & "’ -d ’text=" & myText & "’ -d ’target_lang=" & myTargLang & "’"
try
set sRes to do shell script sText
on error
error
end try

set jsonString to NSString’s stringWithString:sRes
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)

set tRes to aJsonDict’s valueForKeyPath:"translations.text"
if tRes = missing value then
set erMes to (aJsonDict’s valueForKey:"message") as string
error erMes
else
return contents of first item of (tRes as list)
end if
end translateWithDeepL

macOS 11でメニューバークロックのアナログ/デジタル切り替え

macOS 11は、サブマシンで検証用に入れておいたぐらいでメインマシンはずーっとmacOS 10.14.6のままでした。M1 Macを導入したのでメイン環境をmacOS 11に移行。

いままで(macOS 10.14.6で)メニューから手軽に切り替えられていたメニューバークロックのアナログ/デジタル切り替え。ふだんは見やすいデジタル表示で、画面キャプチャする際にはアナログ表示に切り替えていました。

これが、macOS 11ではメニューから直接切り替えられないのがストレスで、すぐに切り替え方を調べてmacOS標準搭載のスクリプトメニューから切り替えられるようにしてみました。現在の設定値を読み取ってトグル動作(アナログ→デジタル、デジタル→アナログ)を行います。

–> Watch Demo movie

AppleScript名:macOS 11_メニューバークロックのアナログ_デジタルトグル切り替え.scpt

– Created by: Takaaki Naganoya
– Created on: 2021年06月17日

– Copyright © 2021 Piyomaru Software, All Rights Reserved

use AppleScript version "2.7"
use framework "Foundation"
use scripting additions

set cRes to (do shell script "defaults read com.apple.menuextra.clock.plist IsAnalog") as integer as boolean
set newBool to (not cRes) as string
do shell script "defaults write com.apple.menuextra.clock.plist IsAnalog -bool " & newBool
do shell script "killall ControlCenter"

githubの機能でMarkdownをhtmlに

githubのREST APIを呼び出して、Markdownファイルの内容をHTMLにレンダリングするAppleScriptです。


さんかく他のプログラムに処理部分を組み込んで表示させたところ(本ScriptはただHTMLレンダリングするだけで表示しません)

MacDownなどのアプリケーションでMarkdownファイルをオープンする方法とコードの行数はどっこいどっこいですが、MacDownを必要としないので、どこの環境でも(インターネット接続していれば)実行できます。

志の低さが目を覆わんばかりの出来で、実に少ない行数でレンダリングできるものの、Markdownをサーバーに送ってレンダリングしてもらうので、処理速度は遅いです。あるいは、githubのREST API処理サーバーにつねに負荷がかかっているとか。

AppleScript名:githubの機能でMarkdownをhtmlに.scpt

– Created by: Takaaki Naganoya
– Created on: 2020年08月09日

– Copyright © 2020 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set mdFile to choose file of type {"net.daringfireball.markdown"} with prompt "Choose Markdown"
set mdStr to (read mdFile as «class utf8») as text

do shell script "curl -X POST https://api.github.com/markdown/raw -H ’Content-Type: text/plain’ -d " & quoted form of mdStr
set aString to result

return aString

CPUのバイトオーダーを取得

実行中のコンピュータ(多分Mac)のCPUのバイトオーダーを取得するAppleScriptです。

これまで、MacのCPUは68k(Big Endian)→PowerPC(Big Endian)→Intel(Little Endian)と来て、次はARM。

一応、IntelとArmは同じLittle Endianなので問題は少ないはずですが、ARM DTKをお持ちの方は試してコッソリ教えていただけるとありがたいです。

この情報がAppleScriptで取得できると何か「いいこと」があるかですが、別にメリットはありません。

ただ、CPUの名前が取得できると、処理速度を推測できてよいでしょう。Intel CPUに対してApple Silicon(ARM)がどの程度のパフォーマンスを発揮できるのか、実機が出てこないとわかりませんけれども。

PowerPCからIntelに変わったときには、先読みが深くなったためか(?)連番ファイルの処理で問題が出て、書き直しが必要になったことがありました。IntelからARMへの移行時には、iOS系のアプリケーションの操作を試みて問題が出たり、セキュリティ上の制限にひっかかったりすることでしょう。

AppleScript名:CPUのバイトオーダーを取得.scpt

– Created by: Takaaki Naganoya
– Created on: 2020年07月22日

– Copyright © 2020 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set bRes to getByteOrder() of me
–> true –Little Endian

on getByteOrder()
set aRes to do shell script "sysctl -n hw.byteorder"
if aRes = "4321" then
return false –Big Endian (PowerPC)
else
return true –Little Endian (Intel, Arm..maybe)
end if
end getByteOrder

Shane Stanleyからの投稿です。こっちの方がシンプルでいいですね。

AppleScript名:CPUのバイトオーダーを取得 v2.scptd

– Created by: Shane Stanley
– Created on: 2020年07月23日

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set bRes to current application’s NSHostByteOrder() — 1 = little endian, 2 = big endian, 0 = unknown

Finder上で「すべてのファイル名拡張子を表示」にチェックが入っているかを返す

Finderの環境設定で「すべてのファイル名拡張子を表示」にチェックが入っているかを確認するAppleScriptです。

iWorkアプリケーション(Keynote、Pages、Numbers)では、オープン中の書類名を返すときに、Finderの上記の環境設定の内容を反映して拡張子を含めたり、含めなかったりしつつ名称を返すという仕様。

「ファイル名単独で求めることなんてないでしょ?」と思いつつも、オープン中のドキュメントの存在確認を名称で行うため、けっこう左右されることになります。

AppleScript名:Finder上で「すべてのファイル名拡張子を表示」にチェックが入っているかを返す.scpt

– Created by: Takaaki Naganoya
– Created on: 2020年05月19日

– Copyright © 2020 Piyomaru Software, All Rights Reserved

set exRes to checkDisplayTheFileExtensionsInFinder() of me
–> true

on checkDisplayTheFileExtensionsInFinder()
try
set aRes to (do shell script "/usr/bin/defaults read NSGlobalDomain AppleShowAllExtensions") as integer
return aRes as boolean
on error
return false
end try
end checkDisplayTheFileExtensionsInFinder

アラートダイアログ上にWebViewでGoogle Chartsを表示(Calendar Chart)

アラートダイアログ上にWkWebViewを配置し、Google Chartsを用いてCalendar Chartを表示するAppleScriptです。

[フレーム]

自分の開発環境(MacBook Pro Retina 2012, Core i7 2.6GHz)で100日間のアクセス履歴を処理して7秒強ぐらいで描画が終了します。

調子に乗って300日分のアクセス履歴を処理したところ、表示まで1分ほどかかりました。あまり長い期間の描画を行わせるのは(このプログラムの書き方だと)向いていないと感じます。いまのところテストしただけで実用性は考えていませんが、この程度のグラフなら自前でNSImage上にボックスを描画して表示しても大した手間にはならないでしょう。


さんかくスクリプトエディタ上で実行したところ


さんかくScript Debugger上で実行したところ


さんかくスクリプトメニュー上で実行したところ

Safariのアクセス履歴は例によってsqliteのDatabaseにアクセスして取得していますが、AppleScriptのランタイム環境によっては、アクセス権限がないというメッセージが出てアクセスできないことがあります。ASObjC Explorer 4上では実行できませんでしたし、Switch Control上でも実行できません。

# 管理者権限つきで実行しても(with administrator privileges)実行できません → Switch Controlでも実行できるようになりました

最初に掲載したバージョンでは、グラフ化したときに表示月が1か月ズレるという問題がありました。

Google Chartsのドキュメントを確認したところ、

Note: JavaScript counts months starting at zero: January is 0, February is 1, and December is 11. If your calendar chart seems off by a month, this is why.

JavaScriptでMonthはJanuaryが0らしく、monthを-1する必要があるとdocumentに書かれていました。うわ、なにその仕様?(ーー;;;

AppleScript名:アラートダイアログ上にWebViewでGoogle Chartを表示(Calendar Charts)v1a.scptd

– Created by: Takaaki Naganoya
– Created on: 2020年05月07日

– Copyright © 2020 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use framework "WebKit"
use scripting additions

property |NSURL| : a reference to current application’s |NSURL|
property NSAlert : a reference to current application’s NSAlert
property NSString : a reference to current application’s NSString
property NSButton : a reference to current application’s NSButton
property WKWebView : a reference to current application’s WKWebView
property WKUserScript : a reference to current application’s WKUserScript
property NSURLRequest : a reference to current application’s NSURLRequest
property NSRunningApplication : a reference to current application’s NSRunningApplication
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property WKUserContentController : a reference to current application’s WKUserContentController
property WKWebViewConfiguration : a reference to current application’s WKWebViewConfiguration
property WKUserScriptInjectionTimeAtDocumentEnd : a reference to current application’s WKUserScriptInjectionTimeAtDocumentEnd

property returnCode : 0

script spd
property aRes : {}
property bRes : {}
end script

–Calculate Safari access frequency for (parameter days)
set (aRes of spd) to calcMain(100) of safariHistLib

set (bRes of spd) to ""
repeat with i in (aRes of spd)
set {item1, item2, item3} to parseByDelim(theName of (contents of i), "-") of me
set newLine to " [ new Date(" & (item1 as string) & ", " & ((item2 – 1) as string) & ", " & (item3 as string) & "), " & (numberOfTimes of i) & "]," & (string id 10)
set (bRes of spd) to (bRes of spd) & newLine
end repeat

set (bRes of spd) to text 1 thru -3 of (bRes of spd)

–Pie Chart Template HTML
set myStr to "<!DOCTYPE html>
<html lang=\"UTF-8\">
<head>
<div id=\"calendarchart\"></div>

<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>

<script type=\"text/javascript\">
// Load google charts
google.charts.load(’current’, {’packages’:[’calendar’]});
google.charts.setOnLoadCallback(drawChart);

// Draw the chart and set the chart values
function drawChart() {
var dataTable = new google.visualization.DataTable();
dataTable.addColumn({ type: ’date’, id: ’Date’ });
dataTable.addColumn({ type: ’number’, id: ’Web Access’ });
dataTable.addRows([
%@
]);

var chart = new google.visualization.Calendar(document.getElementById(’calendar_basic’));

var options = {
title: \"Web Activity\",
height: 350,
};

chart.draw(dataTable, options);
}
</script>
</head>
<body>
<div id=\"calendar_basic\" style=\"width: 1000px; height: 350px;\"></div>
</body>
</html>"

set aString to current application’s NSString’s stringWithFormat_(myStr, (bRes of spd)) as string

set paramObj to {myMessage:"Calendar Chart Test", mySubMessage:"This is a simple calendar chart using google charts", htmlStr:aString}
–my browseStrWebContents:paramObj–for debug
my performSelectorOnMainThread:"browseStrWebContents:" withObject:(paramObj) waitUntilDone:true

on browseStrWebContents:paramObj
set aMainMes to myMessage of paramObj
set aSubMes to mySubMessage of paramObj
set htmlString to (htmlStr of paramObj)

set aWidth to 1000
set aHeight to 300

–WebViewをつくる
set aConf to WKWebViewConfiguration’s alloc()’s init()

–指定HTML内のJavaScriptをFetch
set jsSource to pickUpFromToStr(htmlString, "<script type=\"text/javascript\">", "</script>") of me

set userScript to WKUserScript’s alloc()’s initWithSource:jsSource injectionTime:(WKUserScriptInjectionTimeAtDocumentEnd) forMainFrameOnly:true
set userContentController to WKUserContentController’s alloc()’s init()
userContentController’s addUserScript:(userScript)
aConf’s setUserContentController:userContentController

set aWebView to WKWebView’s alloc()’s initWithFrame:(current application’s NSMakeRect(0, 0, aWidth, aHeight – 100)) configuration:aConf
aWebView’s setNavigationDelegate:me
aWebView’s setUIDelegate:me
aWebView’s setTranslatesAutoresizingMaskIntoConstraints:true

set bURL to |NSURL|’s fileURLWithPath:(POSIX path of (path to me))
aWebView’s loadHTMLString:htmlString baseURL:(bURL)

— set up alert
set theAlert to NSAlert’s alloc()’s init()
tell theAlert
its setMessageText:aMainMes
its setInformativeText:aSubMes
its addButtonWithTitle:"OK"
–its addButtonWithTitle:"Cancel"
its setAccessoryView:aWebView

set myWindow to its |window|
end tell

— show alert in modal loop
NSRunningApplication’s currentApplication()’s activateWithOptions:0
my performSelectorOnMainThread:"doModal:" withObject:(theAlert) waitUntilDone:true

–Stop Web View Action
set bURL to |NSURL|’s URLWithString:"about:blank"
set bReq to NSURLRequest’s requestWithURL:bURL
aWebView’s loadRequest:bReq

if (my returnCode as number) = 1001 then error number -128
end browseStrWebContents:

on doModal:aParam
set (my returnCode) to (aParam’s runModal()) as number
end doModal:

on viewDidLoad:aNotification
return true
end viewDidLoad:

on fetchJSSourceString(aURL)
set jsURL to |NSURL|’s URLWithString:aURL
set jsSourceString to NSString’s stringWithContentsOfURL:jsURL encoding:(NSUTF8StringEncoding) |error|:(missing value)
return jsSourceString
end fetchJSSourceString

on pickUpFromToStr(aStr as string, s1Str as string, s2Str as string)
set a1Offset to offset of s1Str in aStr
if a1Offset = 0 then return false
set bStr to text (a1Offset + (length of s1Str)) thru -1 of aStr
set a2Offset to offset of s2Str in bStr
if a2Offset = 0 then return false
set cStr to text 1 thru (a2Offset – (length of s2Str)) of bStr
return cStr as string
end pickUpFromToStr

–リストを任意のデリミタ付きでテキストに
on retArrowText(aList, aDelim)
set aText to ""
set curDelim to AppleScript’s text item delimiters
set AppleScript’s text item delimiters to aDelim
set aText to aList as text
set AppleScript’s text item delimiters to curDelim
return aText
end retArrowText

on array2DToJSONArray(aList)
set anArray to current application’s NSMutableArray’s arrayWithArray:aList
set jsonData to current application’s NSJSONSerialization’s dataWithJSONObject:anArray options:(0 as integer) |error|:(missing value) –0 is
set resString to current application’s NSString’s alloc()’s initWithData:jsonData encoding:(current application’s NSUTF8StringEncoding)
return resString
end array2DToJSONArray

on parseByDelim(aData, aDelim)
set curDelim to AppleScript’s text item delimiters
set AppleScript’s text item delimiters to aDelim
set dList to text items of aData
set AppleScript’s text item delimiters to curDelim
return dList
end parseByDelim

script safariHistLib
property parent : AppleScript
use scripting additions
use framework "Foundation"

property |NSURL| : a reference to current application’s |NSURL|

script spd
property sList : {}
property nList : {}
property sRes : {}
property dRes1 : {}
property dRes2 : {}
end script


on calcMain(daysNum)
set (dRes1 of spd) to dumpSafariHistoryFromDaysBefore(daysNum) of me

set (dRes2 of spd) to {}
repeat with i in (dRes1 of spd)
copy (first item of i) as string to dStr
set convDstr to first item of (parseByDelim(dStr, {" "}) of me)
set the end of (dRes2 of spd) to convDstr
end repeat

–日付ごとに登場頻度集計
set cRes to countItemsByItsAppearance2((dRes2 of spd)) of me
return cRes as list
end calcMain


–NSArrayに入れたレコードを、指定の属性ラベルの値でソート
on sortRecListByLabel(aArray, aLabelStr as string, ascendF as boolean)
–ソート
set sortDesc to current application’s NSSortDescriptor’s alloc()’s initWithKey:aLabelStr ascending:ascendF
set sortDescArray to current application’s NSArray’s arrayWithObjects:sortDesc
set sortedArray to aArray’s sortedArrayUsingDescriptors:sortDescArray

–NSArrayからListに型変換して返す
set bList to (sortedArray) as list
return bList
end sortRecListByLabel


on dumpSafariHistoryFromDaysBefore(daysBefore)
–現在日時のn日前を求める
using terms from scripting additions
set origDate to (current date) – (daysBefore * days)
end using terms from

set dStr to convDateObjToStrWithFormat(origDate, "yyyy-MM-dd hh:mm:ss") of me

set aDBpath to "~/Library/Safari/History.db"
set pathString to current application’s NSString’s stringWithString:aDBpath
set newPath to pathString’s stringByExpandingTildeInPath()

set aText to "/usr/bin/sqlite3 " & newPath & " ’SELECT datetime(history_visits.visit_time+978307200, \"unixepoch\", \"localtime\"), history_visits.title || \" @ \" || substr(history_items.URL,1,max(length(history_items.URL)*(instr(history_items.URL,\" & \")=0),instr(history_items.URL,\" & \"))) as Info FROM history_visits INNER JOIN history_items ON history_items.id = history_visits.history_item where history_visits.visit_time>(julianday(\"" & dStr & "\")*86400-211845068000) ORDER BY visit_time ASC LIMIT 999999;’"

using terms from scripting additions
set (sRes of spd) to do shell script aText
end using terms from

set (sList of spd) to (paragraphs of (sRes of spd))

repeat with i in (sList of spd)
set j to contents of i

–Parse each field
set j2 to parseByDelim(j, {"|", "@ "}) of me

set the end of (nList of spd) to j2
end repeat

return (nList of spd)
end dumpSafariHistoryFromDaysBefore



on parseByDelim(aData, aDelim)
set curDelim to AppleScript’s text item delimiters
set AppleScript’s text item delimiters to aDelim
set dList to text items of aData
set AppleScript’s text item delimiters to curDelim
return dList
end parseByDelim


–出現回数で集計
on countItemsByItsAppearance2(aList)
set aSet to current application’s NSCountedSet’s alloc()’s initWithArray:aList
set bArray to current application’s NSMutableArray’s array()
set theEnumerator to aSet’s objectEnumerator()

repeat
set aValue to theEnumerator’s nextObject()
if aValue is missing value then exit repeat
bArray’s addObject:(current application’s NSDictionary’s dictionaryWithObjects:{aValue, (aSet’s countForObject:aValue)} forKeys:{"theName", "numberOfTimes"})
end repeat

–出現回数(numberOfTimes)で降順ソート
set theDesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:"theName" ascending:true
bArray’s sortUsingDescriptors:{theDesc}

return bArray
end countItemsByItsAppearance2


on convDateObjToStrWithFormat(aDateO as date, aFormatStr as string)
set aDF to current application’s NSDateFormatter’s alloc()’s init()

set aLoc to current application’s NSLocale’s currentLocale()
set aLocStr to (aLoc’s localeIdentifier()) as string

aDF’s setLocale:(current application’s NSLocale’s alloc()’s initWithLocaleIdentifier:aLocStr)
aDF’s setDateFormat:aFormatStr
set dRes to (aDF’s stringFromDate:aDateO) as string
return dRes
end convDateObjToStrWithFormat

end script

指定アプリケーションを指定言語環境で再起動

指定アプリケーションを、現在のユーザーアカウントで指定可能な言語環境を指定して再起動するAppleScriptです。

Xcodeにこのような機能があり、そのような処理を行える可能性について思い至っていました。shell commandについてはEdama2さんから教えていただきました。あー、やっぱりこういう感じなんですね。

次に、言語コードを求める方法ですが、調べても全言語の言語コードを求める方法が見当たらなかったので(これは、探し方が足りないだけ)、現在のユーザーアカウントで指定可能な言語コードを求める方法に落ち着きました。

本Scriptを実行すると、指定アプリケーションを起動する言語コードを選択。

処理するアプリケーションを選択。choose applicationコマンドが活躍するのはとても珍しいケースです。

英語(English)を指定した場合。

フランス語(French)を指定した場合。

日本語(Japanese)を指定した場合。

個人的には、英語環境でアプリケーションを動かしつつ、執筆用のアプリケーション(Keynote)だけ日本語環境で動かすことができて、些細なコマンドの呼称を確認する必要がなくていい感じです。あれ? 日本語環境でターゲットのアプリケーションだけ英語環境を指定して起動すればいいんじゃ????

AppleScript名:指定アプリケーションを指定言語環境で再起動

– Created by: Takaaki Naganoya
– Created on: 2020年04月30日
– Special Thanks to : Edama2
– Copyright © 2020 Piyomaru Software, All Rights Reserved

use AppleScript version "2.5"
use scripting additions
use framework "Foundation"
use framework "AppKit"

set aLocList to (current application’s NSLocale’s preferredLanguages()) as list

set targLanguage to choose from list aLocList
set anApp to choose application

set apFile to POSIX path of (path to anApp)
tell anApp to quit

set sText to "open -n -a " & quoted form of apFile & " –args -AppleLanguages ’(\"" & targLanguage & "\")’"
do shell script sText

Safariの履歴を読み込んでBest 10を求める。「その他」計算機能つき

Safariの閲覧履歴のdatabaseにアクセスして、domain単位でベスト10を求めるAppleScriptです。

自分の開発環境(MacBook Pro Retina 2012, Core i7 2.6GHz)で10日間のアクセス履歴を処理して0.8秒程度です。

以前のSafariではplistでヒストリを管理していましたが、気づいたらSqliteのDBで管理するように変わっていたので(Safari 10あたりで?)、よろしく抽出して加工してみました。おそらく、最低限の情報のみ抽出することで現状の半分ぐらいの処理時間で処理できるようになるとは思うのですが、汎用性を持たせるために現状のレベルにまとめています。

計算結果が当たり前すぎて予想よりはるかに面白くなかったので、そのことに逆に驚きました。

AppleScript名:Safariの履歴を読み込んでBest 10を求める。「その他」計算機能つき.scptd

– Created by: Takaaki Naganoya
– Created on: 2020年04月29日

– Copyright © 2020 Piyomaru Software, All Rights Reserved

use AppleScript version "2.7" — (10.13) or later
use scripting additions
use framework "Foundation"

property |NSURL| : a reference to current application’s |NSURL|

script spd
property sList : {}
property nList : {}
property sRes : {}
end script

set aRes to calcSafariHistoryBest10Domain(10) of me
–> {{theName:"piyocast.com", numberOfTimes:1100}, {theName:"www.youtube.com", numberOfTimes:710}, {theName:"twitter.com", numberOfTimes:354}, {theName:"www.google.com", numberOfTimes:331}, {theName:"syosetu.com", numberOfTimes:199}, {theName:"github.com", numberOfTimes:140}, {theName:"developer.apple.com", numberOfTimes:139}, {theName:"appstoreconnect.apple.com", numberOfTimes:121}, {theName:"xxxxxxxxxxxxx", numberOfTimes:106}, {theName:"Other", numberOfTimes:847}}

on calcSafariHistoryBest10Domain(daysBefore)
–現在日時のn日前を求める
set origDate to (current date) – (daysBefore * days)
set dStr to convDateObjToStrWithFormat(origDate, "yyyy-MM-dd hh:mm:ss") of me

set aDBpath to "~/Library/Safari/History.db"
set pathString to current application’s NSString’s stringWithString:aDBpath
set newPath to pathString’s stringByExpandingTildeInPath()

set aText to "sqlite3 " & newPath & " ’SELECT datetime(history_visits.visit_time+978307200, \"unixepoch\", \"localtime\"), history_visits.title || \" @ \" || substr(history_items.URL,1,max(length(history_items.URL)*(instr(history_items.URL,\" & \")=0),instr(history_items.URL,\" & \"))) as Info FROM history_visits INNER JOIN history_items ON history_items.id = history_visits.history_item where history_visits.visit_time>(julianday(\"" & dStr & "\")*86400-211845068000) ORDER BY visit_time ASC LIMIT 999999;’"
set (sRes of spd) to do shell script aText

set (sList of spd) to (paragraphs of (sRes of spd))

set (nList of spd) to {}

repeat with i in (sList of spd)
set j to contents of i

–Parse each field
set j2 to parseByDelim(j, {"|", "@ "}) of me

–Calculate URL’s domain only
set j3 to contents of last item of j2
set aURL to (|NSURL|’s URLWithString:j3)

if aURL = missing value then
set j4 to ""
else
set j4 to (aURL’s |host|()) as string
end if

set the end of (nList of spd) to j4
end repeat

–登場頻度でURLを集計
set aList to countItemsByItsAppearance((nList of spd)) of me


–Best 10の計算のために10位以下をまとめる
set best9 to items 1 thru 9 of aList
set best10 to items 10 thru -1 of aList

set otherTimes to 0
repeat with i in best10
set otherTimes to otherTimes + (numberOfTimes of i)
end repeat

set the end of best9 to {theName:"Other", numberOfTimes:otherTimes}
return best9
end calcSafariHistoryBest10Domain

on parseByDelim(aData, aDelim)
set curDelim to AppleScript’s text item delimiters
set AppleScript’s text item delimiters to aDelim
set dList to text items of aData
set AppleScript’s text item delimiters to curDelim
return dList
end parseByDelim

–出現回数で集計
on countItemsByItsAppearance(aList)
set aSet to current application’s NSCountedSet’s alloc()’s initWithArray:aList
set bArray to current application’s NSMutableArray’s array()
set theEnumerator to aSet’s objectEnumerator()

repeat
set aValue to theEnumerator’s nextObject()
if aValue is missing value then exit repeat
bArray’s addObject:(current application’s NSDictionary’s dictionaryWithObjects:{aValue, (aSet’s countForObject:aValue)} forKeys:{"theName", "numberOfTimes"})
end repeat

–出現回数(numberOfTimes)で降順ソート
set theDesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:"numberOfTimes" ascending:false
bArray’s sortUsingDescriptors:{theDesc}

return bArray as list
end countItemsByItsAppearance

on convDateObjToStrWithFormat(aDateO as date, aFormatStr as string)
set aDF to current application’s NSDateFormatter’s alloc()’s init()

set aLoc to current application’s NSLocale’s currentLocale()
set aLocStr to (aLoc’s localeIdentifier()) as string

aDF’s setLocale:(current application’s NSLocale’s alloc()’s initWithLocaleIdentifier:aLocStr)
aDF’s setDateFormat:aFormatStr
set dRes to (aDF’s stringFromDate:aDateO) as string
return dRes
end convDateObjToStrWithFormat

Xcode上で作成したアプリケーション上でDark Mode検出

Xcode上で作成したAppleScriptアプリケーションでDark Modeの検出を行いたいときに、NSAppearance’s currentAppearance()で取得したら、正しくModeの検出が行えませんでした。同じコードをスクリプトエディタ/Script Debugger上で動かした場合には正しくModeの判定が行えているのですが。

そこで、System Eventsの機能を用いてMode判定を行うように処理を書き換えたりしてみたのですが、Mac App Storeに出すアプリケーションでこの処理を記述していたら、これを理由にリジェクトされてしまいました。

仕方なく解決策を探してみたところ、macOS 10.13用に書いたshell scriptによる迂回処理を、そのまま他のOSバージョンでも動かせばよいのではないかと気づき、結局そこに落ち着きました。

AppleScript名:Dark Modeの検出(Xcode上でも正しく判定)

– Created by: Takaaki Naganoya
– Created on: 2020年04月22日

– Copyright © 2020 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set apRes to retLightOrDark() of me
–> true (Dark), false (Light)

on retLightOrDark()
try
set sRes to (do shell script "defaults read -g AppleInterfaceStyle")
return (sRes = "Dark") as boolean
on error
return false
end try
end retLightOrDark

mirroringの設定と解除

自分はディスプレイのミラーリング機能は日常的にあまり使っていません。実際にやってみたらどうだったのかをまとめてみました。

この手の処理をAppleScriptで書こうとしても、そのための命令が標準で内蔵されていないため、アプローチの方法はかぎられています。

(1)思いっきりハードウェア寄りのプログラム(Cとかで書いた)を呼び出す
(2)アクセシビリティ系の機能(GUI Scripting)を使ってシステム環境設定を操作する

の2つです。(2)は、画面上の要素の些細な変更によりプログラムを書き換える必要が出てくるうえに、信頼性が高くないので、あまりやりたくありません。もちろん、画面上の要素を検索しながら処理する方法もあるわけですが、それなりに(画面要素の検索に)時間がかかります。

そうなると、(1)を採用することになります。探すと.........すぐにみつかりました。「mirror-displays」というコマンドラインアプリケーションです。ソースコードを読んでみると、CoreGraphics系の各種フレームワークを呼び出している、Objective-Cで書かれた(ほとんどCのコード)プログラムです。

これをスクリプトバンドルの中に入れて呼び出してみました。1回実行するとメインモニタの内容が他のモニタにミラーリングされます。もう1回実行すると、ミラーリングが解除されます。

–> Download mirroring_toggle (Script Bundle with executable command in its bundle)

mirrorコマンドのオプションには、「どのモニタをミラーリングさせるか」といった指定ができるようですが、試していないのでとくに凝った指定も何もしていません。

mirrorコマンドはmacOS 10.14でビルドして10.10以降をターゲットにしてみましたが、そこまで古い環境は手元に残っていないのでテストしていません。また、macOS 10.15のマシンには複数台のモニタをつないでいないので、macOS 10.15上でのテストも行っていません。

AppleScript名:mirroringの設定と解除

– Created by: Takaaki Naganoya
– Created on: 2020年03月11日

– Copyright © 2020 Piyomaru Software, All Rights Reserved

–https://github.com/hydra/mirror-displays
–バンドルの中に入れたmirrorコマンドをただ呼んでるだけ
set myPath to POSIX path of (path to me)
set comPath to myPath & "/Contents/Resources/mirror"
do shell script quoted form of comPath

使用中のMacの製品呼称を取得する v4

使用中のMacの製品呼称を取得するAppleScriptです。

ながらく、この手のルーチンを使い続けてきましたが、macOS 10.15でエラーが出るようになりました。

理由を確認してみたところ、パス名の一部がmacOS 10.15で変更になっていることがわかりました。

目下、Xcode上でアプリケーションを作成すると、ローカライズしたリソースのフォルダについては、「English.lproj」ではなく「en.lproj」と、言語コードが用いられるようになってきました。この、「English」と「en」の変更がOS内部のコンポーネントについても行われた「だけ」というのが理由のようです。

ちなみに、パス名を無意味に途中で切ってつなげているのは、Blog(HTML)やMarkDownのドキュメントに入れたときに、折り返しされずにレンダリング品質を下げる原因になる(行がそこだけ伸びるとか、ページ全体の文字サイズが強制的に小さくなるとか)ためです。

AppleScript名:使用中のMacの製品呼称を取得する v4.scptd

– Created by: Takaaki Naganoya
– Created on: 2020年03月08日

– Copyright © 2020 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set myInfo to retModelInfo() of me
–> "Mac mini (Late 2014)"

on retModelInfo()
set v2 to system attribute "sys2"
— macOS 10.15.3 –> 15

if v2 < 15 then
–macOS 10.14まで
set pListPath to "/System/Library/PrivateFrameworks/ServerInformation.framework/" & "Versions/A/Resources/English.lproj/SIMachineAttributes.plist"
else
–macOS 10.15以降
set pListPath to "/System/Library/PrivateFrameworks/ServerInformation.framework/" & "Versions/A/Resources/en.lproj/SIMachineAttributes.plist"
end if

set aRec to retDictFromPlist(pListPath) of me
set hwName to (do shell script "sysctl -n hw.model")
–> "Macmini7,1"

set aMachineRec to retRecordByLabel(aRec, hwName) of me

set aMachineRec2 to contents of first item of aMachineRec
return (marketingModel of _LOCALIZABLE_ of aMachineRec2)
end retModelInfo

on retDictFromPlist(aPath)
set thePath to current application’s NSString’s stringWithString:aPath
set thePath to thePath’s stringByExpandingTildeInPath()
set theDict to current application’s NSDictionary’s dictionaryWithContentsOfFile:thePath
return theDict as record
end retDictFromPlist

on retRecordByLabel(aRec as record, aKey as string)
set aDic to current application’s NSDictionary’s dictionaryWithDictionary:aRec
set aVal to aDic’s valueForKey:aKey
return aVal as list
end retRecordByLabel

on retRecordByKeyPath(aRec as record, aKey as string)
set aDic to current application’s NSDictionary’s dictionaryWithDictionary:aRec
set aVal to aDic’s valueForKeyPath:aKey
return aVal
end retRecordByKeyPath

osascript系のAppleScriptランタイムを区別する

AppleScriptのランタイム環境が何であるかを区別できるようになりました。

それらのうち、/usr/bin/osascriptを呼び出しているランタイム環境について、さらに細分化して個別に識別するために、ランタイム環境そのものと親プロセスが何であるかをAppleScriptで調査してみました。

結論からいえば、親プロセスが「bash」ならTerminalからosascriptコマンドで起動されたと判断してよさそう。その他は、実行されたScriptの置かれているパスを求めて、macOS標準装備のScript Menuかアプリケーション内部のScript Menuかを見分けるぐらいでしょうか。

これを判定したプログラム自体は、別に技術的にすごいとか機密の塊とかいうことはなくて、「清書してなくて、きたない。無駄に長い。掲載されているプログラムを見ても何も感じない」ことから掲載していません。実験用のテストプログラムでも、ちょっと近年稀に見るぐらいひどい出来です。内容は、単にランタイムプログラムのプロセスIDを取得して、そのプロセスIDをもとにpsコマンドで親プロセスを求めて、コマンドからいろいろ情報を抜いてくるというだけのものです。

Script Menu

ランタイムのパラメータ:/usr/bin/osascript -P /Users/me/Library/Scripts/Finder/pTEST.scptd
親プロセス:/System/Library/Frameworks/Foundation.framework/Versions/C/XPCServices/com.apple.foundation.UserScriptService.xpc/Contents/MacOS/com.apple.foundation.UserScriptService

与えられているScriptが特定フォルダ(/Users/me/Library/Scripts/)以下にあるものかどうか、という識別は可能。ちょっと、根拠が弱そうです。

CotEditor内のScript Menu

ランタイムのパラメータ:/usr/bin/osascript -sd -T 14534 -P /Users/me/Library/Application Scripts/com.coteditor.CotEditor/010)M-pM^_M^MM^NM-pM^_M^SM^\AppleScriptM-cM^AM-(M-cM^AM^WM-cM^AM-&M-hM-'M-#M-iM^GM^H/0020)M-pM^_M^SM^\M-fM-'M^KM-fM^VM^GM-gM-"M-:M-hM-*M^MM-cM^AM^WM-cM^AM-&M-eM-.M^_M-hM-!M^LM-cM^AM^WM-cM^@M^AM-pM^_M^SM^DM-fM^VM-0M-hM-&M^OM-fM^[M-8M-iM-!M^^M-cM^AM-+M-gM-5M^PM-fM^^M^\M-cM^BM^RM-eM^GM-:M-eM^JM^[.@r.scpt
親プロセス:/System/Library/Frameworks/Foundation.framework/Versions/C/XPCServices/com.apple.foundation.UserScriptService.xpc/Contents/MacOS/com.apple.foundation.UserScriptService

親プロセスはScript Menuと同じですが、こちらも実行しているScriptのパスが/Users/me/Library/Application Scripts/com.coteditor.CotEditor/以下であること。これも、識別のための根拠が弱いです。

Terminal経由でosascriptコマンド実行

ランタイムのパラメータ:osascript /Users/me/Desktop/pTEST.scptd
親プロセス:-bash

これは、親プロセスが-bashであることから、明確に区別できます。Terminal.appではなくbashなんですね。

Terminal経由で/usr/bin/osascript(フルパス指定)でコマンド実行

ランタイムのパラメータ:/usr/bin/osascript /Users/me/Desktop/pTEST.scptd
親プロセス:-bash

フルパスで指定すると何か変わってくるかとも思いましたが、とくに変化はありませんでした。

Program AS Style runtime name Parent Process Name Script Path
Script Editor Script/Scriptd Script Editor
Script Editor Cocoa-AppleScript Applet CocoaApplet
Script Editor Applet applet
Script Debugger Script/Scriptd Script Debugger
Script Debugger Applet (Enhanced) FancyDroplet
ASOBjC Explorer 4 Script/Scriptd ASOBjC Explorer 4
Automator Workflow Automator
Automator Applet Application Stub
Script Menu Script/Scriptd osascript …com.apple.foundation.UserScriptService /Users/me/Library/Scripts/ ほか
CotEditor Script osascript …com.apple.foundation.UserScriptService /Users/me/Library/Application Scripts/com.coteditor.CotEditor/
Terminal.app (osascript) Script/Scriptd osascript bash

CotEditorの表示用フォント名を環境設定から取得する

CotEditorの環境設定値(plist)から表示用フォント名を取得するAppleScriptです。

本来であれば、CotEditorのwindowオブジェクト自体に属性値として表示用フォント名がついているのが(GUI側からWindowごとに表示フォントを指定できるので)理想的ですが、そういう機能は実装されていないので、無理やりplistから読み取ってみました。

CotEditorの表示用フォントは、環境設定で指定したものがデフォルトで用いられ、個別のウィンドウごとに任意のフォントを指定できるようになっています。本Scriptで取得できるのは、個別のウィンドウの設定値ではなく、環境設定値のほうです。

defaultsコマンドでplistを読むのはあまりおすすめできない方法ですが、できないよりはマシというところです。

まっさらな(macOSをインストールしたての)環境にCotEditorをインストールして一度も環境設定でフォントを指定していない環境だとエラーになる(項目が存在しない)ので、その場合に備えてエラートラップを仕掛けています。


さんかく環境設定から表示フォント名を取得して、各行をプロポーショナルフォント使用時の画面描画幅をもとにソートする処理を書いたときに利用

AppleScript名:CotEditorの表示用フォント名を環境設定から取得する

– Created by: Takaaki Naganoya
– Created on: 2019年10月15日

– Copyright © 2019 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

set fName to getCotEditorFontName() of me
–> "HiraginoSans-W3"

–CotEditorのplistから表示用のフォント設定(PostScript名)を取得する
on getCotEditorFontName()
try
set fnRes to do shell script "defaults read com.coteditor.CotEditor | grep fontName"
on error
return "" –インストール後に表示フォントの環境設定を一度も行っていないときにはエラーになる
end try
set fName to extractStrFromTo(fnRes, "= \"", "\";") of me
return fName
end getCotEditorFontName

–指定文字と終了文字に囲まれた内容を抽出
on extractStrFromTo(aParamStr, fromStr, toStr)
set theScanner to current application’s NSScanner’s scannerWithString:aParamStr
set anArray to current application’s NSMutableArray’s array()

repeat until (theScanner’s isAtEnd as boolean)
set {theResult, theKey} to theScanner’s scanUpToString:fromStr intoString:(reference)
theScanner’s scanString:fromStr intoString:(missing value)
set {theResult, theValue} to theScanner’s scanUpToString:toStr intoString:(reference)
if theValue is missing value then set theValue to "" –>追加
theScanner’s scanString:toStr intoString:(missing value)
anArray’s addObject:theValue
end repeat

if anArray’s |count|() is not equal to 1 then return ""

return first item of (anArray as list)
end extractStrFromTo

hiro さんのコメントから、defaultsコマンドのオプション追加でずいぶんと簡潔に書けるようで、書き直しておきました。

AppleScript名:CotEditorの表示用フォント名を環境設定から取得する v2
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

set fName to getCotEditorFontName() of me
–> "HiraginoSans-W3"

–CotEditorのplistから表示用のフォント設定(PostScript名)を取得する
on getCotEditorFontName()
try
set fnRes to do shell script "defaults read com.coteditor.CotEditor fontName"
on error
return "" –インストール後に表示フォントの環境設定を一度も行っていないときにはエラーになる
end try
return fnRes
end getCotEditorFontName

CotEditorで編集中のMarkdown書類をPDFプレビュー

CotEditorで編集中のMarkdown書類を、MacDownでPDF書き出しして、Skimでオープンして表示するAppleScriptです。

CotEditorにMarkdownのプレビュー機能がついたらいいと思っている人は多いようですが、MarkdownはMarkdownで、方言は多いし標準がないし、1枚もののMarkdown書類だけ編集できればいいのか、本などのプロジェクト単位で編集とか、目次が作成できないとダメとか、リンクした画像の扱いをどうするのかとか、対応しようとすると「ほぼ別のソフトを作るのと同じ」ぐらい手間がかかりそうです(メンテナー様ご本人談)。

そこで、AppleScript経由で他のソフトを連携させてPDFプレビューさせてみました。これなら、誰にも迷惑をかけずに、今日この時点からすぐにMarkdownのプレビューが行えます(当然、HTML書き出ししてSafariでプレビューするバージョンははるかかなた昔に作ってあります)。

[フレーム]

ただし、OS側の機能制限の問題で、CotEditor上のスクリプトメニューから実行はできません(GUI Scriptingの実行が許可されない)。OS側のスクリプトメニューに登録して実行する必要があります。

GUI Scriptingを利用してメニュー操作を行なっているため、システム環境設定で許可しておく必要があります。

本来であれば、PDFの書き出し先フォルダ(この場合は書き出しダイアログで、GUI Scirptingを用いてCommand-Dで指定して一律に場所指定が行えるデスクトップフォルダ)に同名のPDFが存在しないかどうかチェックし、存在すれば削除するといった処理が必要ですが、面倒だったのであらかじめMarkdown書類をUUIDにリネームしておくことで、書き出されたPDFも同じくUUIDのファイル名になるため、論理上はファイル名の衝突を回避できるため、削除処理を省略しています。

AppleScript名:🌏レンダリングしてPDFプレビュー
— Created 2019年06月15日 by Takaaki Naganoya
— 2019 Piyomaru Software
use AppleScript version "2.5"
use scripting additions
use framework "Foundation"
use framework "AppKit"

property NSUUID : a reference to current application’s NSUUID
property NSWorkspace : a reference to current application’s NSWorkspace

–オープン中のMarkdown書類を取得する
tell application "CotEditor"
tell front document
set cStyle to coloring style
if cStyle is not equal to "Markdown" then
display dialog "編集中のファイルはMarkdown書類ではないようです。" buttons {"OK"} default button 1
return
end if

set aPath to path
end tell
end tell

–一時フォルダにMarkdown書類をコピー
set sPath to (path to temporary items)
tell application "Finder"
set sRes to (duplicate ((POSIX file aPath) as alias) to folder sPath with replacing)
end tell

–コピーしたMarkdown書類をリネーム
set s1Res to sRes as alias
set aUUID to NSUUID’s UUID()’s UUIDString() as text –UUIDを作成する
tell application "Finder"
set name of s1Res to (aUUID & ".md")
end tell

–Markdown書類をデスクトップにPDF書き出し
set pdfRes to exportFromMacDown(POSIX path of s1Res) of me

–PDF Viewerでオープン
tell application "Skim" –Preview.appでもOK
activate
open pdfRes
end tell

–一時フォルダに書き出したMarkdown書類を削除
tell application "Finder"
delete s1Res
end tell

–指定のMacDownファイル(alias)をデスクトップ上にPDFで書き出し
on exportFromMacDown(anAlias)
set s1Text to paragraphs of (do shell script "ls ~/Desktop/*.pdf") –pdf書き出し前のファイル一覧

tell application "MacDown"
open {anAlias}
end tell

macDownForceSave() of me

tell application "MacDown"
close every document without saving
end tell

do shell script "sync" –ねんのため

set s2Text to paragraphs of (do shell script "ls ~/Desktop/*.pdf") –pdf書き出し後のファイル一覧

set dRes to getDiffBetweenLists(s1Text, s2Text) of me –デスクトップ上のPDFファイル名一覧の差分を取得
set d2Res to (addItems of dRes)

if length of d2Res ≥ 1 then
return contents of first item of d2Res
else
error "Error in exporting PDF to desktop folder…."
end if
end exportFromMacDown

on getDiffBetweenLists(aArray as list, bArray as list)
set allSet to current application’s NSMutableSet’s setWithArray:aArray
allSet’s addObjectsFromArray:bArray

–重複する要素のみ抜き出す
set duplicateSet to current application’s NSMutableSet’s setWithArray:aArray
duplicateSet’s intersectSet:(current application’s NSSet’s setWithArray:bArray)

–重複部分を削除する
allSet’s minusSet:duplicateSet
set resArray to (allSet’s allObjects()) as list

set aSet to current application’s NSMutableSet’s setWithArray:aArray
set bSet to current application’s NSMutableSet’s setWithArray:resArray
aSet’s intersectSet:bSet –積集合
set addRes to aSet’s allObjects() as list

set cSet to current application’s NSMutableSet’s setWithArray:bArray
cSet’s intersectSet:bSet –積集合
set minusRes to cSet’s allObjects() as list

return {addItems:minusRes, minusItems:addRes}
end getDiffBetweenLists

–注意!! ここでGUI Scriptingを使用。バージョンが変わったときにメニュー階層などの変更があったら書き換え
on macDownForceSave()
activate application "MacDown"
tell application "System Events"
tell process "MacDown"
— File > Export > PDF
click menu item 2 of menu 1 of menu item 14 of menu 1 of menu bar item 3 of menu bar 1

–Go to Desktop Folder
keystroke "d" using {command down}

–Save Button on Sheet
click button 1 of sheet 1 of window 1
end tell
end tell
end macDownForceSave

–Bundle IDからアプリケーションのPathを返す
on retAppAbusolutePathFromBundleID(aBundleID)
set appPath to NSWorkspace’s sharedWorkspace()’s absolutePathForAppBundleWithIdentifier:aBundleID
if appPath = missing value then return false
return appPath as string
end retAppAbusolutePathFromBundleID

CPU Family Nameを取得する

実行中のMacのMachine ID(MacBookPro10,1など)からIntel CPUのFamily Name(Ivy Bridgeなど)を取得するAppleScriptです。

Macの各モデルの搭載CPUについては、MacTrackerを参照しました。

MacTrackerのデータをコピー&ペーストでNumbers上に製品データを作成し(手動)、

ユニーク化(重複データの除去)処理を行なって(ASで処理)、プログラム上に展開しました(手動)。

実用性とか意味については、とくに考えていません。Machine IDとCPU Family Nameのデータについてはplistに記述してプログラム外部に追い出して独自でメンテナンスできるようにしたほうがよいと思われます。

AppleScript名:CPU Family Nameを取得する.scptd

– Created by: Takaaki Naganoya
– Created on: 2018年12月03日

– Copyright © 2018 Piyomaru Software, All Rights Reserved

use AppleScript version "2.4" — Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set macID to do shell script "sysctl -n hw.model" –get machine ID
–set macID to "MacBookPro99,1"–Error case

set myName to getIntelCoreProcessorFamilyName(macID) of me
—> "Ivy Bridge"

on getIntelCoreProcessorFamilyName(macID)
–Machine ID & CPU Core Model Names (uniquefied)
set macList to {{"iMac4,1", "Yonah"}, {"iMac4,2", "Yonah"}, {"iMac5,2", "Merom"}, {"iMac5,1", "Merom"}, {"iMac6,1", "Merom"}, {"iMac7,1", "Merom"}, {"iMac8,1", "Penryn"}, {"iMac9,1", "Penryn"}, {"iMac10,1", "Wolfdale"}, {"iMac11,1", "Wolfdale, Lynnfield"}, {"iMac11,2", "Clarkdale"}, {"iMac11,3", "Clarkdale, Lynnfield"}, {"iMac12,1", "Sandy Bridge"}, {"iMac12,2", "Sandy Bridge"}, {"iMac13,1", "Ivy Bridge"}, {"iMac13,2", "Ivy Bridge"}, {"iMac14,1", "Haswell"}, {"iMac14,3", "Haswell"}, {"iMac14,2", "Haswell"}, {"iMac14,4", "Haswell"}, {"iMac15,1", "Haswell"}, {"iMac16,1", "Broadwell"}, {"iMac16,2", "Broadwell"}, {"iMac17,1", "Skylake"}, {"iMac18,1", "Kaby Lake"}, {"iMac18,2", "Kaby Lake"}, {"iMac18,3", "Kaby Lake"}, {"iMacPro1,1", "Skylake"}, {"Macmini1,1", "Yonah"}, {"Macmini2,1", "Merom"}, {"Macmini3,1", "Penryn"}, {"Macmini4,1", "Penryn"}, {"Macmini5,1", "Sandy Bridge"}, {"Macmini5,2", "Sandy Bridge"}, {"Macmini5,3", "Sandy Bridge"}, {"Macmini6,1", "Ivy Bridge"}, {"Macmini6,2", "Ivy Bridge"}, {"Macmini7,1", "Haswell"}, {"Macmini8,1", "Coffe Lake"}, {"MacPro1,1", "Woodcrest"}, {"MacPro2,1", "Clovertown"}, {"MacPro3,1", "Harpertown"}, {"MacPro4,1", "Bloomfield, Gainestown"}, {"MacPro5,1", "Bloomfield, Gulftown, Westmere"}, {"MacPro5,1", "Bloomfield, Westmere, Gulftown"}, {"MacPro6,1", "Ivy Bridge"}, {"MacBook1,1", "Yonah"}, {"MacBook2,1", "Merom"}, {"MacBook3,1", "Merom"}, {"MacBook4,1", "Penryn"}, {"MacBook5,1", "Penryn"}, {"MacBook5,2", "Penryn"}, {"MacBook6,1", "Penryn"}, {"MacBook7,1", "Penryn"}, {"MacBook8,1", "Broadwell"}, {"MacBook9,1", "Skylake"}, {"MacBook10,1", "Kaby Lake"}, {"MacBookAir1,1", "Merom"}, {"MacBookAir2,1", "Penryn"}, {"MacBookAir3,1", "Penryn"}, {"MacBookAir3,2", "Penryn"}, {"MacBookAir4,1", "Sandy Bridge"}, {"MacBookAir4,2", "Sandy Bridge"}, {"MacBookAir5,1", "Ivy Bridge"}, {"MacBookAir5,2", "Ivy Bridge"}, {"MacBookAir6,1", "Haswell"}, {"MacBookAir6,2", "Haswell"}, {"MacBookAir7,1", "Broadwell"}, {"MacBookAir7,2", "Broadwell"}, {"MacBookAir8,1", "Amber Lake Y"}, {"MacBookPro1,1", "Yonah"}, {"MacBookPro1,2", "Yonah"}, {"MacBookPro2,2", "Merom"}, {"MacBookPro2,1", "Merom"}, {"MacBookPro3,1", "Merom"}, {"MacBookPro4,1", "Penryn"}, {"MacBookPro5,1", "Penryn"}, {"MacBookPro5,2", "Penryn"}, {"MacBookPro5,5", "Penryn"}, {"MacBookPro5,4", "Penryn"}, {"MacBookPro5,3", "Penryn"}, {"MacBookPro7,1", "Penryn"}, {"MacBookPro6,2", "Arrandale"}, {"MacBookPro6,1", "Arrandale"}, {"MacBookPro8,1", "Sandy Bridge"}, {"MacBookPro8,2", "Sandy Bridge"}, {"MacBookPro8,3", "Sandy Bridge"}, {"MacBookPro9,2", "Ivy Bridge"}, {"MacBookPro9,1", "Ivy Bridge"}, {"MacBookPro10,1", "Ivy Bridge"}, {"MacBookPro10,2", "Ivy Bridge"}, {"MacBookPro11,1", "Haswell"}, {"MacBookPro11,2", "Haswell"}, {"MacBookPro11,3", "Haswell"}, {"MacBookPro12,1", "Broadwell"}, {"MacBookPro11,4", "Haswell"}, {"MacBookPro11,5", "Haswell"}, {"MacBookPro13,1", "Skylake"}, {"MacBookPro13,2", "Skylake"}, {"MacBookPro13,3", "Skylake"}, {"MacBookPro14,1", "Kaby Lake"}, {"MacBookPro14,2", "Kaby Lake"}, {"MacBookPro14,3", "Kaby Lake"}, {"MacBookPro15,2", "Coffee Lake"}, {"MacBookPro15,1", "Coffee Lake"}}

–2D List内の検索
set gList to searchInListByIndexItem(macList, 1, macID) of me
if gList = missing value or gList = {} then
error "Error:" & macID & "is newer Machine than I expected in Dec 2018 or Older PowerPC Mac , may be"
end if

set g2List to FlattenList(gList) of me –複数の結果が得られた場合に備える
return contents of second item of g2List
end getIntelCoreProcessorFamilyName

–2Dリストから、指定インデックスアイテムで、指定データが該当する最初のものを返す
on searchInListByIndexItem(aList as list, itemNum as integer, hitData as string)
set setKey to current application’s NSMutableSet’s setWithArray:aList

if itemNum < 1 then return {}
set aPredicateStr to ("SELF[" & (itemNum – 1) as string) & "] == ’" & hitData & "’"

set aPredicate to current application’s NSPredicate’s predicateWithFormat:aPredicateStr
set aRes to (setKey’s filteredSetUsingPredicate:aPredicate)
set bRes to aRes’s allObjects()

set cRes to bRes as list of string or string –as anything
return cRes
end searchInListByIndexItem

–By Paul Berkowitz
–2009年1月27日 2:24:08:JST
–Re: Flattening Nested Lists
on FlattenList(aList)
set oldDelims to AppleScript’s text item delimiters
set AppleScript’s text item delimiters to {"????"}
set aString to aList as text
set aList to text items of aString
set AppleScript’s text item delimiters to oldDelims
return aList
end FlattenList

teratailの指定IDのユーザー情報を取得する_curl

プログラミング系質問サイトteratailのREST APIを呼び出して、指定ユーザー名の情報を取得するAppleScriptです。

TeratailのREST APIは、タグ、ユーザー、質問の3ジャンルの情報取得を行えるように整備されており、特定カテゴリ(タグで分類)の新規質問が投稿されたかどうかを定期的に確認するようなAppleScriptを作って運用することもできます(そこまでやっていないですけれども)。

REST API呼び出しにはNSURLConnectionからNSURLSessionに移行していますが、どうもNSURLSessionだと呼び出せない(AppleScriptからの呼び出し処理が完了しない)サービスがあったりするので、結局shellのcurlコマンドを呼び出すのが手短にすむケースが多いようです。

Teratailの場合も、NSURLSessionで呼び出せるAPIもあれば、結果が返ってこないAPIもあり、NSURLConnectionよりも使い勝手がよくないと感じています(個人の感想です)。

このあたり、将来的なmacOSのアップデートでNSURLConnectionが使えなくなる日が来るのかもしれませんが、curlコマンドを使うように集約するべきなのか、NSURLSessionで書き換えるべきなのか悩ましいところです。

AppleScript名:teratailの指定IDのユーザー情報を取得する_curl
— Created 2018年11月26日 by Takaaki Naganoya
— 2018 Piyomaru Software
use AppleScript version "2.5"
use scripting additions
use framework "Foundation"

property |NSURL| : a reference to current application’s |NSURL|
property NSString : a reference to current application’s NSString
property NSURLSession : a reference to current application’s NSURLSession
property NSURLQueryItem : a reference to current application’s NSURLQueryItem
property NSJSONSerialization : a reference to current application’s NSJSONSerialization
property NSURLComponents : a reference to current application’s NSURLComponents
property NSMutableDictionary : a reference to current application’s NSMutableDictionary
property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest
property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding
property NSURLSessionConfiguration : a reference to current application’s NSURLSessionConfiguration

set aUserRes to searchOneUserByDisplayName("Piyomaru") of me
–> {meta:{limit:20, message:"success", hit_num:1, total_page:1, page:1}, users:{{score:43, photo:"https://teratail.storage.googleapis.com/uploads/avatars/u6/66639/MSIS21by_thumbnail.jpg", display_name:"Piyomaru"}}}

on searchOneUserByDisplayName(aName)
set aRec to {q:aName}
set reqURLStr to "https://teratail.com/api/v1/users/search"
set bURL to retURLwithParams(reqURLStr, aRec) of me

set tmpData to (do shell script "curl -X GET \"" & bURL & "\"")
set jsonString to NSString’s stringWithString:tmpData
set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding)
set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value)
if aJsonDict = missing value then return false
return (aJsonDict as record)
end searchOneUserByDisplayName

on retURLwithParams(aBaseURL, aRec)
set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec
set aKeyList to (aDic’s allKeys()) as list
set aValList to (aDic’s allValues()) as list
set aLen to length of aKeyList

set qList to {}
repeat with i from 1 to aLen
set aName to contents of item i of aKeyList
set aVal to contents of item i of aValList
set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal)
end repeat

set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL
aComp’s setQueryItems:qList
set aURL to (aComp’s |URL|()’s absoluteString()) as text

return aURL
end retURLwithParams