AppleScript Dictionary Menu

EDIT (June 14, 2021). This thread contains numerous versions of my script but, IMO, the script in post 16 is by far the best. So, if anyone is interested in testing this script, I would suggest they skip to post 16, copy and paste that script into a script editor, and run the script. Its operation requires no real explanation.

This script displays a dialog in which the user can select an AppleScript dictionary to open. The available dictionaries consist of three default dictionaries–Finder, System Events, and Standard Additions–plus dictionaries for currently-running visible apps. Additional default dictionaries such as Safari are easily added, although a very few such as Python will not work.

I run this script by way of FastScripts, but it can also be saved and run as an application. This script is designed for use with Script Editor and assumes that Script Editor is running.

I received help writing this script in the following MacScripter thread:

https://macscripter.net/viewtopic.php?id=47325

This script is a rewrite of a script originally written by Nigel Garvey:

https://www.macscripter.net/viewtopic.php?pid=191643


set defaultApps to {"Script Editor", "StandardAdditions", "System Events"}

tell application "System Events"
	set activeApps to name of every process whose visible is true and has scripting terminology is true
end tell

set appList to sortList(activeApps & defaultApps)

choose from list appList with title "AppleScript" with prompt "Select a dictionary to open:" default items {item 1 of appList} OK button name "Open"

if result = false then
	error number -128
else
	set selectedApp to item 1 of result
end if

if selectedApp = "StandardAdditions" then
	set appFile to ((path to scripting additions folder as text) & "StandardAdditions.osax:") as alias
else
	set appFile to path to application selectedApp
end if

tell application "Script Editor"
	set allDictionaries to name of every document whose path ends with ".sdef"
	if selectedApp is in allDictionaries then
		focusWindow(selectedApp) of me
	else if (selectedApp & ".sdef") is in allDictionaries then
		focusWindow((selectedApp & ".sdef")) of me
	else
		activate
		open appFile
	end if
end tell

on focusWindow(selectedApp)
	
	tell application "Script Editor" to activate
	tell application "System Events" to tell process "Script Editor" to perform action "AXRaise" of window selectedApp
	
end focusWindow

on sortList(unsortedList) # obtained from developer.apple.com then modified
	
	set sortedList to {}
	set indexList to {}
	set listCount to (count unsortedList)
	
	repeat listCount times
		set lowTextItem to missing value
		repeat with i from 1 to listCount
			if i is not in indexList then
				set anItem to (item i of unsortedList)
				if lowTextItem is missing value then
					set lowTextItem to anItem
					set lowIndexItem to i
				else if anItem < lowTextItem then
					set lowTextItem to anItem
					set lowIndexItem to i
				end if
			end if
		end repeat
		if lowTextItem is not in sortedList then set end of sortedList to lowTextItem
		set end of indexList to lowIndexItem
	end repeat
	
	return sortedList
	
end sortList

Not bad! :slight_smile:

It’s very similar in concept to a script I’ve been using myself for the past twenty years or so! :slight_smile: I’ve had to rewrite mine a few times as various things have either stopped working or been reinstated over the systems, but the basic idea hasn’t changed. For historical reasons, mine presents the Finder selected at the top of the choose dialog, with the other open scriptable apps sorted alphabetically below it. I have similar scripts for scripting additions and scriptable-but-not-necessarily-open applications in the CoreServices folder; but of course there’s now only one OSAX dictionary worth opening and the CoreServices script keeps opening the wrong dictionaries for reasons I’ve never been able to determine!

When you get round to adapting yours for multiple dictionaries, you should find the windows will cascade naturally.

Nigel. Thanks for looking at my script and for the compliment.

When I began work on this project, my script operated in a manner similar to one written by Daniel, the developer of FastScripts. I then happened upon your script and was inspired to take a similar approach, which seemed more useful overall. Anyways, I make note of this and provide a link to the thread containing your script in post 25 of the MacScripter thread linked in post 1 above.

Post deleted by peavine.

Hi peavine.

I get an error on the ‘set bounds’ line at the bottom. The names of Script Editor’s dictionary windows end with “.sdef”, which isn’t allowed for in your handlers.

I think you’ll find that’s only if you have Show all filename extensions on in the Finder. (Actually, it probably depends on the state when SE was launched.)

You’re right. It is. It shows you have to be careful about what assumptions you make when writing scripts. :slight_smile:

it also supports my working hypothesis that the Finder is the root of all scripting evil ;).

More seriously, this is yet another good reason why it’s best not to refer to windows by name, unless the app in question uses some kind of persistent ID as a name. It’s not an issue confined to Script Editor or dictionaries.

Thanks Nigel and Shane.

I did the following:

  • Deleted my post 4, which contained the erroneous script.

  • Revised my post 1 to include an updated script, which works with dictionary names both with and without an .sdef extension.

The operation of this script is similar to that in post 1 above except that the user specifies dictionary names/paths in a text file. A few comments:

  • The first time the script is run, it creates two small text files in the user’s preferences folder.

  • The user should then enter in the data file the desired dictionary names/paths (see below).

  • In versions of macOS prior to Catalina, the path set in the script by editorApp will need to be changed. The script will not work properly otherwise.

-- Revised 2020.09.29

main()

on main()
	set preferencePath to POSIX path of (path to preferences)
	set dataFile to preferencePath & name of me & " Data.txt"
	set settingFile to preferencePath & name of me & " Setting.txt"
	set editorApp to "/System/Applications/Utilities/Script Editor.app"
	
	set {defaultSelection, dictionaryNames, dictionaryPaths} to readFile(dataFile, settingFile)
	
	set dialogList to {"Active App", "--"} & dictionaryNames & {"--", "Data File"}
	choose from list dialogList default items defaultSelection OK button name "Open" with title "AppleScript Dictionaries"
	set selectedApp to result as text
	
	if selectedApp = "false" or selectedApp = "--" then
		error number -128
	else if selectedApp = "Active App" then
		openActiveApp(editorApp)
	else if selectedApp = "Data File" then
		do shell script "open " & quoted form of dataFile
	else
		openSelectedApp(dictionaryNames, dictionaryPaths, editorApp, selectedApp)
	end if
	
	writeFile(settingFile, selectedApp)
end main

on readFile(dataFile, settingFile)
	try
		set defaultSelection to paragraph 1 of (read POSIX file settingFile)
	on error
		set defaultSelection to "Data File"
	end try
	
	set {dictionaryNames, dictionaryPaths} to {{}, {}}
	try
		set openedFile to paragraphs of (read POSIX file dataFile)
	on error
		writeFile(dataFile, linefeed & linefeed)
		errorDialog("The dictionary data file could not be found and has been created.", "Please rerun this script and add dictionary names and paths to the data file.")
		error number -128
	end try
	
	repeat with i from 1 to (count openedFile) by 2
		if item i of openedFile > "" then
			set end of dictionaryNames to item i of openedFile
			set end of dictionaryPaths to item (i + 1) of openedFile
		else
			exit repeat
		end if
	end repeat
	
	return {defaultSelection, dictionaryNames, dictionaryPaths}
end readFile

on openActiveApp(editorApp)
	try
		tell application "System Events" to set activeApp to POSIX path of (application file of (first process whose frontmost is true and has scripting terminology is true))
		do shell script "open -a " & quoted form of editorApp & space & quoted form of activeApp
	on error
		tell application "System Events" to set activeApp to name of first process whose frontmost is true
		errorDialog("Dictionary not found for " & quote & activeApp & quote, "This app may not be scriptable")
	end try
end openActiveApp

on openSelectedApp(dictionaryNames, dictionaryPaths, editorApp, selectedApp)
	repeat with i from 1 to (count dictionaryNames)
		if item i of dictionaryNames = selectedApp then exit repeat
	end repeat
	
	try
		do shell script "open -a " & quoted form of editorApp & space & quoted form of (item i of dictionaryPaths)
	on error
		errorDialog("Dictionary not found for " & quote & selectedApp & quote, "Check the path in the data file")
	end try
end openSelectedApp

on writeFile(theFile, theText)
	try
		set openedFile to open for access (POSIX file theFile) with write permission
		set eof of openedFile to 0
		write theText to openedFile
		close access openedFile
	on error
		try
			close access openedFile
		end try
	end try
	delay 0.5
end writeFile

on errorDialog(textOne, textTwo)
	display alert textOne message textTwo buttons {"OK"} default button 1 as critical
end errorDialog

Just by way of example, the following are the contents of my data file. Many of the paths may not work on versions of macOS prior to Catalina and will need to be edited.

This script is a revision of my script in post 10. This revision was done only to incorporate ASObjC code.

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set preferencePath to POSIX path of (path to preferences)
	set dataFile to preferencePath & "AppleScript Dictionary Data.txt"
	set settingFile to preferencePath & "AppleScript Dictionary Setting.txt"
	set activeApp to current application's NSWorkspace's sharedWorkspace()'s frontmostApplication()
	set defaultSelection to readSettingFile(settingFile)
	set {dictionaryNames, dictionaryPaths} to readDataFile(dataFile)
	
	set dialogList to {"Active App", "--"} & dictionaryNames & {"--", "Data File"}
	choose from list dialogList default items defaultSelection OK button name "Open" with title "AppleScript Dictionaries"
	set selectedApp to result as text
	
	if selectedApp = "false" or selectedApp = "--" then
		error number -128
	else if selectedApp = "Active App" then
		openActiveApp(activeApp)
	else if selectedApp = "Data File" then
		set theWorkSpace to current application's NSWorkspace's sharedWorkspace()
		set theFile to current application's |NSURL|'s fileURLWithPath:dataFile
		theWorkSpace's openURL:theFile
	else
		openSelectedApp(dictionaryNames, dictionaryPaths, selectedApp)
	end if
	
	writeFile(settingFile, selectedApp)
	delay 1
end main

on readSettingFile(theFile)
	set theText to current application's NSString's stringWithContentsOfFile:(theFile) encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
	if theText = missing value then
		return "Data File"
	else
		return theText as text
	end if
end readSettingFile

on readDataFile(theFile)
	set theText to current application's NSString's stringWithContentsOfFile:(theFile) encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
	if theText = missing value then
		writeFile(theFile, linefeed & linefeed)
		errorDialog("The dictionary data file could not be found and has been created.", "Please rerun this script and add dictionary names and paths to the data file.")
	end if
	
	set theArray to theText's componentsSeparatedByCharactersInSet:(current application's NSCharacterSet's characterSetWithCharactersInString:(linefeed))
	set theList to theArray as list
	
	set {dictionaryNames, dictionaryPaths} to {{}, {}}
	repeat with i from 1 to (count theList) by 2
		if item i of theList > "" then
			set end of dictionaryNames to item i of theList
			set end of dictionaryPaths to item (i + 1) of theList
		else
			exit repeat
		end if
	end repeat
	return {dictionaryNames, dictionaryPaths}
end readDataFile

on openActiveApp(activeApp)
	set theBundle to current application's NSBundle's bundleWithURL:(activeApp's bundleURL())
	set dictionaryExists to (theBundle's objectForInfoDictionaryKey:"NSAppleScriptEnabled") -- thanks Shane
	if dictionaryExists = missing value then
		errorDialog("Dictionary not found", quote & (activeApp's localizedName as text) & quote & " may not be scriptable")
	end if
	
	set activeAppPath to (activeApp's valueForKeyPath:"bundleURL.path") as text
	set theWorkSpace to current application's NSWorkspace's sharedWorkspace()
	theWorkSpace's openFile:activeAppPath withApplication:"Script Editor"
end openActiveApp

on openSelectedApp(dictionaryNames, dictionaryPaths, selectedApp)
	repeat with i from 1 to (count dictionaryNames)
		if item i of dictionaryNames = selectedApp then exit repeat
	end repeat
	
	try
		set theWorkSpace to current application's NSWorkspace's sharedWorkspace()
		theWorkSpace's openFile:(item i of dictionaryPaths) withApplication:"Script Editor"
	on error
		errorDialog("Dictionary not found for " & quote & selectedApp & quote, "Check the path in the data file")
	end try
end openSelectedApp

on writeFile(theFile, theString)
	set theString to current application's NSString's stringWithString:theString
	theString's writeToFile:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end writeFile

on errorDialog(textOne, textTwo)
	display alert textOne message textTwo buttons {"OK"} default button 1 as critical
	error number -128
end errorDialog

main()

When first using this script, the user has to place desired dictionary names and app paths in a data file. This is easily done, but the location of a few apps requires some research. So, I have included the following, which can be copied directly into the data file. These paths work under Catalina but may need to be changed with other versions of macOS.

How come this has to be done ? Not that it’s a bad idea at all, but your comment about “research” infers detective work over path to application … or to the Finder, which provides the application file class to access properties for applications registered with the system, including its path, and whether or not it’s scriptable.

CK. Thanks for looking at my script and for the suggestion.

The following is a simple script that logs whether an app has an AppleScript dictionary and copies the app’s path to the clipboard. The clipboard’s contents can then be pasted directly into the data file.

set theApp to text returned of (display dialog "Enter the name of an application:" default answer "")
if theApp = "" then error number -128

set thePath to POSIX path of (path to application theApp)
delay 0.5 -- try different values
tell application "System Events" to tell process theApp to has scripting terminology
set hasScriptingTerminology to result

set the clipboard to thePath
log theApp
log thePath
log hasScriptingTerminology

I revised my script to simplify it’s operation. I’ve tested it with many macOS apps without issue, but there will be a few where the script does not work as expected. Python is an example. Standard Additions is included in the script as a default dictionary, and, if deleted, it can only be added back by removing the script’s plist files.

-- Revised 2021.06.11

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set preferencesFolder to POSIX path of (path to preferences as text)
	set settingPlist to preferencesFolder & "AppleScriptDictionarySetting.plist"
	set dataPlist to preferencesFolder & "AppleScriptDictionaryData.plist"
	set theWorkspace to current application's NSWorkspace's sharedWorkspace()
	
	set dialogDefault to readPlist(settingPlist) as text
	if dialogDefault = "missing value" then set dialogDefault to "Add a Dictionary"
	
	set dictionaryNames to readPlist(dataPlist)
	if dictionaryNames = missing value then
		set dictionaryNames to {"Standard Additions"}
	else
		set dictionaryNames to (dictionaryNames's sortedArrayUsingSelector:"caseInsensitiveCompare:") as list
	end if
	
	set dialogList to dictionaryNames & {"--", "Add a Dictionary", "Delete a Dictionary"}
	choose from list dialogList default items dialogDefault with title "AppleScript Dictionary"
	set selectedItem to result as text
	
	if selectedItem = "false" or selectedItem = "--" then
		error number -128
	else if selectedItem = "Add a Dictionary" then
		set dictionaryNames to addDictionary(dictionaryNames, theWorkspace)
		writePlist(dataPlist, dictionaryNames)
	else if selectedItem = "Delete a Dictionary" then
		set dictionaryNames to deleteDictionary(dictionaryNames)
		writePlist(dataPlist, dictionaryNames)
	else
		openDictionary(selectedItem, theWorkspace)
	end if
	
	writePlist(settingPlist, {selectedItem})
end main

on openDictionary(theApp, theWorkspace)
	set thePath to (theWorkspace's fullPathForApplication:theApp) as text
	if thePath = "missing value" then set thePath to getThePath(theApp)
	theWorkspace's openFile:thePath withApplication:"Script Editor"
	delay 0.5
end openDictionary

on addDictionary(dictionaryNames, theWorkspace)
	display dialog "Enter the name of the app:" default answer "" buttons {"Cancel", "OK"} cancel button 1 default button 2 with title "AppleScript Dictionary" with icon note
	set theApp to text returned of result
	if theApp = "" or theApp is in dictionaryNames then error number -128
	
	set thePath to (theWorkspace's fullPathForApplication:theApp) as text
	
	set theBundle to current application's NSBundle's bundleWithPath:(thePath)
	if theBundle = missing value then errorDialog("An app named " & quote & theApp & quote & " was not found")
	set dictionaryExists to (theBundle's objectForInfoDictionaryKey:"NSAppleScriptEnabled") -- thanks Shane
	if dictionaryExists = missing value then errorDialog("A dictionary was not found for " & quote & theApp & quote)
	
	set end of dictionaryNames to theApp
	return dictionaryNames
end addDictionary

on deleteDictionary(dictionaryNames)
	if dictionaryNames = {} then errorDialog("There are no dictionaries to delete")
	
	set deleteNames to (choose from list dictionaryNames with title "Finder Bookmark" with prompt "Select dictionaries to delete:" default items {item 1 of dictionaryNames} with multiple selections allowed)
	if deleteNames = false then error number -128
	
	set dictionaryNames to current application's NSMutableArray's arrayWithArray:dictionaryNames
	set deleteNames to current application's NSArray's arrayWithArray:deleteNames
	dictionaryNames's removeObjectsInArray:deleteNames
	return (dictionaryNames as list)
end deleteDictionary

on getThePath(theApp)
	if theApp = "Standard Additions" then
		return "/System/Library/ScriptingAdditions/StandardAdditions.osax"
	else
		errorDialog("A path was not found for the app " & quote & theApp & quote)
	end if
end getThePath

on readPlist(thePath)
	set theArray to current application's NSMutableArray's arrayWithContentsOfFile:thePath
	return theArray
end readPlist

on writePlist(thePath, theList)
	set theArray to current application's NSMutableArray's arrayWithArray:theList
	theArray's writeToFile:thePath atomically:true
end writePlist

on errorDialog(dialogText)
	display dialog dialogText buttons {"OK"} cancel button 1 default button 1 with title "AppleScript Dictionary" with icon stop
end errorDialog

main()