I am a macOS user, and my attempts at creating programs to control my computer have necessitated working with AppleScript. Like every programming language, it has its idiosyncrasies, but one in particular sent me down a rabbit hole, which I hope this post can help you avoid should you find yourself in similar circumstances.

Many programming languages have a built-in key-value data structure, which are known by different names: hashes, maps, objects, dictionaries etc. The AppleScript structure equivalent is called a record, and they look outwardly similar to those of other languages:

{product:"pen", price:2.34}

However, a big difference is that while many other languages will allow you to use any kind of data type as a key (strings, integers etc), record keys can only be Properties, which are “effectively tokens created by AppleScript at compile time”, and essentially act like constants (which also means there’s no chance to, say, “constantize” a string received at run time). Therefore, this kind of record is not legal:

{"product":"pen", 5:2.34}

The result of this is that a script must always know in advance what keys it plans to use to look up values in a record: no lookup is possible using, say, some variable that references a string.

This is unfortunate, because I wanted to perform dynamic lookups on a record by fetching values from it based on some string I would receive from the result of a handler (function) call. Here is a code snippet indicating what I attempted to write in order to perform a “zoom in”, which would send different shortcut keystrokes depending on what application was currently in focus:

# Chrome Zoom In keyboard shortcut is ⌘+, while Postman is ⌘=
# NOTE: This record will raise a syntax error.
property zoomInKeys : {"Google Chrome":"+", "Postman":"="}
tell application "System Events"
    # returns a string like "Google Chrome" for the application currently in focus
    set activeApp to name of first application process whose frontmost is true
end tell
# Fetch the appropriate "zoom in" value from the record based on the `activeApp` key
set zoomInKey to activeApp of zoomInKeys

# Perform the keyboard shortcut
tell application "System Events" to tell process activeApp
    keystroke zoomInKey using {command down}
end tell

I initially thought that perhaps the reason for the error was because the record key properties follow the rules of Identifiers, which have a limited set of characters they are allowed to use (that do not include spaces). But…

“AppleScript provides a loophole […]: identifiers whose first and last characters are vertical bars (|) can contain any characters”.

So, I figured that changing the record definition to:

property zoomInKeys : {|Google Chrome|:"+", |Postman|:"="}

or

property zoomInKeys : {|"Google Chrome"|:"+", |"Postman"|:"="}

would work. Alas, they did not. The workaround for getting this code running correctly was to fall back to a traditional if statement:

tell application "System Events"
    set activeApp to name of first application process whose frontmost is true
end tell

if activeApp is "Google Chrome" then
    set zoomInKey to "+"
else if activeApp is "Postman" then
    set zoomInKey to "="
else
  display notification "Cannot zoom in" with title "Error"
  return
end if

# Perform the keyboard shortcut
tell application "System Events" to tell process activeApp
    keystroke zoomInKey using {command down}
end tell

At this point, the sane thing to do is to accept that you now have working code that is fit for purpose, and move on.

But, I could not shake the feeling that there must be a way for string keys to work, even though hours of internet searching turned up nothing. How could every other programming language I know of do this, but not AppleScript? It did not make sense to me.

So, I asked the bird site in a last ditch attempt, and it delivered in the form of Takaaki Naganoya, whose efforts in creating a solution using the Foundation framework led me to be able to change the original code to:

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

property zoomInKeys : {|Google Chrome|:"+", |Postman|:"="}

set zoomInKeysDict to ¬
    current application's NSDictionary's dictionaryWithDictionary:zoomInKeys

tell application "System Events"
    set activeApp to name of first application process whose frontmost is true
end tell

set zoomInKey to (zoomInKeysDict's valueForKey:activeApp) as anything

tell application "System Events" to tell process activeApp
    keystroke zoomInKey using {command down}
end tell

Now, this code works. But, the shotgun approach of bringing in a whole framework and other random handlers just to solve this small problem, coupled with the awkward readability of some of the APIs (looking at you, dictionaryWithDictionary), means that I think the code is now more difficult to understand, for very negligible benefit. So, if statements it is.

If I wanted to dive even further down the rabbit hole, I could have attempted adapting Takaaki’s other solution to the same problem, which was done in vanilla AppleScript, without using Foundation. But, at this point, I think I’m good.

If you are interested in seeing how I ended up using AppleScript for my own use case of mapping stenography chords to macOS keyboard shortcuts, check out my steno dictionaries GitHub repository.

Comments