Keyboard Maestro, Sidecar and Big Sur

Published on by Geoff Taylor

It was recently brought to my attention that my Keyboard Maestro macro for Sidecar doesn’t work under Big Sur. I have Big Sur installed, but my main OS is still Catalina, so I hadn’t yet thought to try the macro under Big Sur.

After a couple of hours of debugging with Script Debugger and Accessibility Inspector – not to mention a good deal of trial and error – I discovered that Big Sur significantly changed the way the menu bar is exposed to AppleScript. (I’ll try to cover that in a separate post; it’s a bit too much to go into in this post.) I finally figured out how to access the right menu bar items with AppleScript and updated the macro’s script to work under Big Sur.

You can download the updated macro here or just create a macro with an Execute AppleScript action. Copy and paste this code into the action:

set deviceName to "The name of your iPad" -- Change this to the name of your iPad

set displayMenu to "" -- Do not change this

tell application "System Events"
	tell its application process "ControlCenter"
		-- Get all menu bar items.
		set menuBarItems to menu bar items of menu bar 1
		
		-- Determine if the Display menu is in the menu bar.
		repeat with mbi in menuBarItems
			if name of mbi contains "Display" then
				set displayMenu to mbi
			end if
		end repeat
		
		-- If the Display menu is in the menu bar, get the Sidecar device.
		-- In Big Sur, it's a toggle button (checkbox) instead of a menu item.
		if displayMenu is not equal to "" then
			click displayMenu
			set deviceToggle to checkbox 1 of scroll area 1 of group 1 of window "Control Center" whose title contains deviceName
			
			-- Click the device.
			click deviceToggle
			
			-- The menu name changes when Sidecar is toggled, so we need to get the Display menu again, then click to close it.
			set displayMenu to (first menu bar item whose name contains "Display") of menu bar 1
			click displayMenu
		else
			-- If the Display menu isn't in the menu bar, display an error message.
			set errorMessage to "Display menu not found in menu bar. Open System Preferences > Dock & Menu Bar. Set Display to \"Show in Menu Bar > Always.\""
			display dialog errorMessage with icon caution
		end if
	end tell
end tell

You need to change two things before you can use this code. First, change “The name of your iPad” (in the first line of the script) to the real name of your iPad (leave the quotation marks around the name). Second, open System Preferences > Dock & Menu Bar, and set the Display menu to always show in the menu bar.

Dock & Menu Bar Preferences
Dock & Menu Bar Preferences

PopClip extension: Backticks

Published on by Geoff Taylor

I made a new PopClip extension. It’s similar to the official Quotes or Brackets extension, except for programmers. It encloses the selected text within ` ` , ' ', " ", ( ), [ ], { } or < >. Download it here. View the source on GitHub.

PopClip extension for Choosy

Published on by Geoff Taylor

I made a PopClip extension for Choosy. Download it here. View the source on GitHub.

Job application websites

Published on by Geoff Taylor

I was looking around in my home directory and found a file from 2013. That was the year that I finished my MBA, and I was applying for new jobs — and judging by the content of the file, I was thoroughly frustrated with many companies’ job application websites. Here’s the list of annoyances I catalogued in that file.

Unhelpful error messages:

  • I filled out an application, attached my resume and cover letter, and clicked Submit. Result: “Server error. Contact the service desk.”
  • I filled out and submitted an application, and a popup dialog informed me that “Only a-z, A-Z and 0-9 are allowed.” It would’ve been helpful to tell me which of the many fields it was referring to.

Asking you to provide superfluous information:

  • “Please choose a username.” Why not just use the email address that I already provided?
  • “Two phone numbers are required.” How many people actually have two phone numbers?
  • “Create a unique identifier, for example the last four digits of your phone number followed by your zip code.” I imagine I could go to any major metropolitan area and find at least two people who have the same “unique” combination. Why can’t the email address I already provided serve as a unique identifier?

Not understanding that answering a question a certain way precludes you from answering a follow-up question:

  • “May we contact this employer? (Yes/No)” Since it was asking about my current employer, I selected “No” and left the contact information blank. Result: “Contact Name and Contact Phone are required.”
  • “Is this your current employer? (required)” I selected “Yes.” Result: “Reason for leaving” was required.

Asking you to provide the same information twice:

  • “Type or paste your resume in the space below (required).” Then, five screens later: “Attach your resume (required).”

And finally, this oddly worded question:

  • “To get an understanding of your salary expectations, I need to know what salary you are targeting for a new position. Some examples: My current rate is… / My last salary was… / I won’t work for less than… I need to understand if our expectations with respect to compensation are aligned before we move forward.”

I’m not sure why they wrote such a long, casually worded sentence when they could’ve just written “salary requirements.” It sounds like someone was dictating the question, and it was copied into the application verbatim.

Backing up Music playlists in macOS Catalina

Published on by Geoff Taylor

After one of my Music playlists disappeared from my Mac and my iPhone, I wanted a way to make automatic, scheduled backups of my playlists. On macOS Catalina, this turned out to be harder than I expected.

Catalina’s Music app allows you export a playlist as an XML file. If the playlist is ever deleted, you can import that XML file to recreate the playlist. But this is a manual, cumbersome process. You have to manually select each playlist, click File > Library > Export Playlist and — as long as you’re OK with the default file name and folder — click Save.

I thought I could automate this process with AppleScript, but the Music app’s AppleScript interface doesn’t provide an export capability. The logical fallback was UI (user interface) scripting via System Events, which allows you to script UI actions like clicking a menu. This came with its own challenges.

First, you have to know the correct names of the UI elements. Almost nothing has an intuitive name that you could grasp by visually inspecting the application window. First, I used this Keyboard Maestro macro to get a list of all of the UI elements. It outputs a plain text file, so I just searched that text file for the name of a playlist.

A portion of the output of the Keyboard Maestro macro:

application Process "Music"
    window Music
        splitter group 1
            scroll area 1
                outline 1
                ...
                    row 23
                        UI element 1
                            static text "80s"

Once I knew where the playlists were in the UI hierarchy, I used Script Debugger’s Explorer, which allows you to drill down from System Events to the relevant process (application) and from there to the application’s UI elements. Once you find the element you need, you can right-click it and select Copy Reference to copy an AppleScript code block that will provide access to that element. (At $99.99, Script Debugger is expensive, but there’s a free trial, and it offers many powerful features beyond the Explorer.)

(There are other ways to get this information, including Xcode’s Accessibility Inspector, which is free but not very intuitive.)

 
In Script Debugger, it’s easy to drill down to a specific playlist because the UI elements’ properties are displayed in the tree view.

There’s still some trial and error involved to distinguish the UI elements that can perform or accept actions from the elements that are just containers to navigate through. For example, to get the name of a playlist with System Events, you have to drill through System Events > process “Music” > window “Music” > splitter group 1 > scroll area 1 > outline 1 > row > UI element 1 > static text. The static text element contains a name property that contains the name of the playlist. (It also contains a value property that contains the same value. I don’t know if one or the other is the “correct” property in this case, but name worked for me.)

If you want to interact with the playlist, however, you refer to the row. But do you click it or select it? According to its definition, it responds to both. Finding the right one, select, was just trial and error.

 
A row responds to both click and select. Finding the right command is often trial and error.

After figuring out all of that, and doing a little more work, I had a working script that could export all of my playlists without any action on my part (other than running the script). Here’s the completed script (and GitHub repo if you’d rather see it there):

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

on pathExists(pathName)
  (* Check if pathName is an existing file or folder.
    First, try to get a reference pathName by using alias. If it succeeds, return true.
    If it fails, try to get a reference to pathName as a POSIX path. If it succeeds, return true.
    Else return false.
  *)
  try
    pathName as alias
    return true
  on error
    try
      POSIX file pathName as alias
      return true
    on error
      return false
    end try
  end try
end pathExists

set alertResult to display alert ¬
  "Back up Music playlists now?" message "Quitting in 10 seconds..." buttons {"Yes", "No"} ¬
  default button "Yes" giving up after 10

set runBackup to button returned of alertResult
set gaveUp to gave up of alertResult

if runBackup = "No" or gaveUp then
  error number -128
end if

set backupFolder to "Playlist Backup" -- The name of the folder that will contain the backups, grouped in subfolders by date
set baseFolder to POSIX path of (path to documents folder) & backupFolder -- backupFolder is a subfolder of ~/Documents
set baseFolderPath to POSIX file baseFolder -- Get baseFolder's POSIX path so that we can concatenate it with the current date
set currentDate to do shell script "date +'%Y%m%d'" -- Get the current date in YYYYMMDD format
set saveFolder to POSIX path of baseFolderPath & "/" & currentDate -- The path to today's folder
set baseFolderExists to pathExists(baseFolderPath) -- Check if baseFolder exists
set saveFolderExists to pathExists(saveFolder) -- Check if saveFolder exists

set the clipboard to saveFolder -- Put saveFolder's path on the clipboard so we can use it later

tell application "Finder"
  if baseFolderExists then
    -- If baseFolder exists, check if saveFolder exists. If not, create saveFolder.
    if saveFolderExists is false then set newSaveFolder to make new folder at baseFolderPath with properties {name:currentDate}
  else
    -- Else create baseFolder and saveFolder
    set newBaseFolder to make new folder at (path to documents folder) with properties {name:backupFolder}
    set newSaveFolder to make new folder at baseFolderPath with properties {name:currentDate}
  end if
end tell

tell application "Music"
  activate
  set userPlaylists to (name of every user playlist whose name does not start with "Purchased" and smart is false) -- Get all user playlists except "Purchased on iPhone," etc. and smart playlists
end tell

(* The Music app's AppleScipt interface doesn't offer a way to export playlists, so we have to script the UI using System Events.
    This block iterates over the playlists in the Music app. When it finds a playlist with a name that matches userPlaylists,
    it selects the playlist, clicks File > Library > Export Playlist, and saves the playlist as an XML file in saveFolder.
    This uses the default file name, which is <playlist name>.xml.
*)
tell application "System Events"
  tell its process "Music"
    tell its window "Music"
      tell its splitter group 1
        tell its scroll area 1
          tell its outline 1
            repeat with thisRow in (every row)
              tell thisRow
                tell its UI element 1
                  repeat with staticText in (every static text)
                    if userPlaylists contains (name of staticText) then
                      set selectedRow to select thisRow
                      tell application "System Events"
                        tell its process "Music"
                          set focused of window "Music" to true
                          delay 2
                          click menu item "Export Playlist…" of menu "Library" of menu item "Library" of menu "File" of menu bar 1
                          delay 2
                          tell its window "Save"
                            keystroke "G" using {command down, shift down} -- Shift-Cmd-G to change directories using the "Go to Folder" dialog
                            delay 2
                            keystroke (the clipboard) -- Paste saveFolder's location into the "Go to Folder" dialog
                            delay 2
                            keystroke return
                            delay 2
                            click button "Save"
                            delay 1
                          end tell
                        end tell
                      end tell
                    end if
                  end repeat
                end tell
              end tell
            end repeat
          end tell
        end tell
      end tell
    end tell
  end tell
end tell

tell application "Music"
  quit
end tell

My next challenge was getting the script to run on a schedule. Since Keyboard Maestro can run AppleScripts, I first tried creating a macro with an “At time” trigger. That worked fine up until the tell application "System Events" block, which didn’t run. After doing some research, I concluded that a launch agent was probably the right approach. After more research, I concluded that manually creating the file wouldn’t be fun, so I bought LaunchControl. It made the task of creating the launch agent easy. I tested the launch agent, and everything worked. But I wanted it to run at night when I’m asleep, not while I’m using my computer. I ran another test when the display, but not the computer, was sleeping. This worked until it hit the tell application "System Events" block, then it stopped. Eventually I figured out that, while the script will run when the display is asleep, the Music app doesn’t get focus, and System Events can’t click the menus if Music isn’t the frontmost app. I found and tried several possible solutions, but nothing worked. I concluded that the script will only work if the display is awake.

The solution I settled on isn’t ideal, but at least now I can back up my playlists with almost no action on my part. I added a display alert line at the beginning of the script which asks if I want to run the backup job. If I click “No” or do nothing for 10 seconds, the script just quits. If I click “Yes,” the script runs, and I have to stop using the computer for several minutes so that the Music app stays in the foreground.

 
An prompt asks if you want to run the backup.

I changed the launch agent schedule to run at a time in the late afternoon when I’m likely to be using the computer. I no longer have to think about backing up my playlists; I just have to be in front of the computer at the right time so that I can click “Yes.”

It would be nice if Apple would update the scripting interface so that this could be done without UI scripting and thus happen completely in the background (or revert to storing playlists as XML files in the file system so that I could just include them in my regular backups). At any rate, I have a solution that requires almost no effort on my part, and that’s good enough for now.