8 min read

Sylvesters, Stooges, and Success (adventures in AppleScript)

Coding in general, and maybe AppleScript specifically, is the only zone where I think I understand the appeal of sadomasochism. It's a journey in which the pleasure arrives after pain.

I added a new feature to an important AppleScript, and I finally fixed an old, hugely-frustrating problem. And in both cases, success came because I decided to try The Really Stupid Way after The Logical And Appropriate Way failed.

The Task

I've got a bunch of regular tasks that involve going through a bunch of webpages and adding links and descriptions to an OmniOutliner document. When I was only performing these tasks infrequently, I did it like this:

  1. Copy some text from the webpage into the Clipboard.
  2. Tab over to OmniOutliner.
  3. Create a new row.
  4. Paste the text into the "Topic" column of the new row,
  5. Tab back into Chrome.
  6. Copy the URL to the clipboard.
  7. Tab back into OmniOutliner.
  8. Paste the URL into the "Link" column.

After performing these steps a certain number of times (let's call it a number between 8 and 100) it was clear that I should automate this. It looked like it'd be a snap.

The First Success

And it was. With this script installed in my OmniOutliner Scripts menu, the process of adding a story from Chrome to an outline document was just:

  1. In Chrome, select the text that I wanted to include in the outline.
  2. Go over to Omnioutliner. Create a new, empty row in the outline, where I wanted the new item to appear.
  3. Run the script.

Bob's your uncle. I've been using this script for so long that I no longer pat myself on the back every single time.

Room For Improvement, Opportunity For Failure

Today, it occurred to me that a "speedrun" version might be useful.

Instead of switching between Chrome and OmniOutliner throughout this entire process, why not stay in Chrome and complete the "Choose links for the outline" process, and then move on to OmniOutliner and then organize all of those links into categories It'd sure save me a lot of command-tabbing between Chrome and OmniOutliner.

Welp, I spent an hour on it, and the script never worked properly. I was scripting OmniOutliner "the right way":

  1. Get the ID of the row of the outline that currently has focus
  2. Create a new row after that row
  3. Fill the row with data collected from Chrome, as usual.

Worse, the script wasn't even failing in a courteous fashion. Sometimes, the new item would appear as the first child of a topic, instead of after the row that I thought had focus. Sometimes it'd even insert blank row.

And sometimes! Sometimes…dear reader.

Sometimes it would appear that the script had screwed up. But then I'd click on a row and everything would redraw and the blank line was gone and the new line had appeared correctly, in the right place. Huh?

The Sylvester Cycle

This committed me to a lonnng spin in the Sylvester Cycle of coding.

The Sylvester Cycle is what happens when a piece of code fails, but you immediately spot the obvious problem, which you should have seen to begin with. So you fix the code, and it fails again. And, whoops! No, no, now I can see the failure point and I'm 100% sure I know how to fix it…

I have christened this syndrome thusly because every time I fall into this trap, I'm reminded of this scene from a Warner Brothers cartoon:

This is exactly what it’s like when a piece of code/script that you thought was a slam-dunk doesn't work. Except the little mouse that's been secretly yanking the trigger is the Coding Gods, aka the ineffable quality of the Universe that is amused by human suffering.

Then I gave up, and resigned myself to making it work

See, here's the problem: I was trying to be a Good Boy.

A Good Boy (and I urge you to adapt the pronoun to whatever is personally-appropriate) relies on the scripting interface that the fine people at The Omni Group so generously provided. They worked really hard on it. One mustn’t appear ungrateful.

A Good Boy also interacts with outlines via its clear, well-architected document structure/model. They navigate the data structure, politely. They are refined and genteel. A Good Boy behaves like any of the Three Stooges, in one of the many shorts in which they've spent months being intensively and successfully trained in how to be Gentlemen of Society.

After you've reached the limits of success that being a Good Boy can deliver, however, one inevitably must revert – like a Stooge – to one's baser instincts.

Some examples of uncouth habits that no amount of education or positive reinforcement can fully erase:

  • Eating peas by shoveling them into your mouth from the blade of a knife
  • Addressing the city opera company's most generous patron, a woman with connections to European royalty, as "Toots"
  • Automating apps via the use of System Events

To be specific, a Good Boy does the right thing at first. They start off writing something like

set newRow to make new row after selected row of the front document

(It should go without saying that this script definitely doesn't work.)

But an hour or more later, and against their aspirational desire to always do things the Sophisticated and Elegant way, they just want the damn thing to work.

How do I create a new, empty row after the current one, without any script? I just manually hit the "Return" key. So, to hell with this: let's just do some AppleScript that makes OmniOutliner the frontmost app, and then sends it a "Return" keystroke, via a System Event.

Viz:

tell application "OmniOutliner"
    activate
end tell

tell application "System Events"
    repeat until frontmost of process "OmniOutliner" is true
        delay 0.1
    end repeat
    keystroke return
end tell

And this worked. Worked fine. Worked fine the very first time I ran the script, in fact.

(The repeat loop covers the script's butt: it holds off on sending the Return until it knows for absolute sure that OmniOutliner is frontmost, and will therefore receive it. So I was at least sending a message to the Gods that even here, when I lay in the gutter, I am still looking up at the stars.)

Stooge Instincts also fixed an longstanding issue

At the top of this post, I described the first iteration of this script as a "Bob's your uncle"-level success. This was a lie. I apologize. I felt it was necessary, for the logical flow of this narrative.

The bit of AppleScript that copies the selected text on the webpage and uses it in the new item of the outline never worked properly. I couldn't figure it out.

At the time, I dutifully Sylvestered my way through the problem. Chrome's AppleScript library is rather threadbare. In this situation, System Events is exactly the right way to get the selected text. You can state this openly amongst polite society: the fainting couch will remain uncontaminated by the limp form of a shocked dowager countess.

So I wrote a function that copied the selected text to the clipboard and then stashed it into a variable. It seemed straightforward, but the text never found its way into OmniOutliner. I eventually shook my head and reached for my "Good Enough" rubber stamp and inking pad. The rest of the script worked fine, and as-is, this was a huge time saver. The requirement that I select the text and copy it to the clipboard manually before running the script wasn't a big deal.

Today, after I finally got that whole "insert a new row after the current one" script working, I took a fresh look at the clipboard problem. This time, my head was already in Stooge Mode. I fixed it in something under two minutes.

I asked myself "why am I copying the Clipboard into a variable?" Because…it just seemed like a good idea. I imagined that some future version of the script would like to use that data.

Those were some fine instincts, son! But the script doesn't work.

What if we just have OmniOutliner read the contents of the system-wide Clipboard directly? The system-wide Clipboard, in case you forgot, Andy, is system-wide.

I made the change, and it worked…first time and forever.

I Am Ungrateful For Success

To recap today's achievements:

  • I've added a useful new feature to an automation that was already one of my all-star scripts.
  • I eliminated an inconvenient and unnecessary step in that automation.
  • I even got a new blog post out of it.

I'm happy!

I'm definitely at least 95% happy.

It's just that…I tried to be a Good Boy. I tried to do everything right. But I only got what I wanted after I deliberately turned my back on every expectation that Polite Society had placed upon my shoulders.

("the communities and traditions of software developers" is perhaps orthogonal to "Polite Society." At least that's something that Polite Society has always made very, very clear to me, specifically. And they've hardly been polite about it. Nonetheless.)

When success comes through the application of brute force and ignorance…it's unsatisfying. One begins to question the whole point of the previous 145,000 years of human evolution. I suppose it wasn't a complete waste of time. We did get "dogs as domesticated companions" and the two-part Season 8 Christmas episode of "Bob's Burgers" out of it.

Among the other benefits of evolution: the understanding that when "beat it with a rock" is your opening gambit and it doesn't work, you're left with something that's more broken than what you started with. Fine.

The Two Flow States Of Coding/Scripting

I'm still going to sulk for a little bit.

There's some solid wisdom to be gained from this experience. There are two familiar flow states to almost every kind of problem-solving process. Knowing when to switch from the first state to the second is key.

At the beginning of a coding project, you want to do things right:

  • You're coding everything "the right way," without using any hacks or shortcuts. Nor are you limited to doing things a certain way because you decided to stop learning new things way back in…well, you don’t know the exact year, but you were running Codewarrior off of floppy discs and all of your code came out great!
  • Each time you encounter a bug, you analyze the problem and fix your code, possibly acquiring a more nuanced understanding of these APIs or whatever in the process. It introduces delays, but at the end, you feel as though the heat and pressure has created diamonds, so to speak.
  • And isn't this what attracted you to coding in the first place? It's a merry puzzle.
  • You're also striving to make life easier for Future You. Your code is organized in a thoughtful and logical manner. Any scraps of code that might come in handy elsewhere inside this project, or even to a different project entirely, is made into an external function. Etc.

This initial mindset is valuable and necessary.

(nods, sagely)

But it won't survive through the end of the project. Your mindset at the end of the project is:

  • "The side of the fence that nobody ever sees" can go to hell.
  • Who decided that this was even a thing? Whoever they are, they're a dope.
  • There's good reasons why we don't allow people to see that side of fence.
  • Code is only valuable if it works. This code is not intended to be read aloud, from a lectern, for the appreciation of laudanum-addled aesthetes attending a Victorian-era literary salon.

A simpler way of putting it is that hitting it with a rock isn't elegant or satisfying, but if it wasn’t a perfectly credible way to solve certain (but by no means all) problems in certain (but by no means all) situations…then why did God leave so many of these things within our reach?

As you contemplate this wisdom, please enjoy 1935's Hoi Polloi, featuring Les Trois Corniauds. ⓘ