BBEdit, Tail Mode, and bbtail

A new version of BBEdit, 14.5, was released a couple of days ago, and the feature that caught my eye was Tail Mode. It’s a way of viewing a log file in a way that persistently updates as new text is added to the file. Lots of processes send information (errors, connections, etc.) to log files, and when you’re debugging code, it’s often very useful to be able to see the log file update in real time. That’s what Tail Mode is for.

macOS has the Console app for doing this, but I’ve never liked Console. There’s also the old-school way of using the “follow” option to the tail command in a Terminal window:

tail -f /var/log/system.log

It is this use of tail that gives Tail Mode its name.

What I like about Tail Mode is that it works like tail -f but by doing so in a BBEdit window, all my ingrained habits for searching and copying text will work, which they don’t in Terminal or iTerm.

To turn on Tail Mode, you can choose the View‣Text Display‣Tail Mode menu item or bring up the Text Options window and check the Tail Mode box.

Text Options

You can also set a file’s language to Log File, which will turn Tail Mode on without any other action from you. By default, files with a .log extension are opened with their language set to Log File and Tail Mode on.

Yesterday, Mark Gardner had a great idea:

Already emailed asking for a bbedit -f or bbtail command

Although I have a feeling Rich Siegel will soon have a bbtail command to go along with bbdiff and bbfind, I couldn’t wait. Here’s my combination shell script/AppleScript:

 1:  #!/bin/zsh
 3:  # Get the absolute path to the file argument.
 4:  f="$1:a"
 6:  # Open the file in BBEdit and go to the end. By setting the source
 7:  # language to Log File, the Text Display will be set to Tail Mode.
 8:  # This works for BBEdit 14.5 and later.
 9:  osascript << OSAEND
10:  tell application "BBEdit"
11:    activate
12:    open "$f"
13:    tell front text window
14:      set source language to "Log File"
15:      set lastChar to length of contents
16:      select character (lastChar + 1)
17:    end tell
18:  end tell

I think the comments explain everything reasonably well. I’ll just link to the parts of the zsh user manual that cover the :a modifier used in Line 4 to expand the filename argument to its full absolute path and the << redirection that creates a here-document out of the AppleScript code between the OSAEND strings and feeds it to the osascript command.

As far as I can tell, BBEdit doesn’t have an AppleScript for setting Tail Mode directly, but by setting the language to Log File in Line 14, I achieve the same result.

I suppose I could have done this entirely in AppleScript with an on run routine, but I don’t know how to get an absolute path from a partial path in AppleScript.

Watches and stages

Two announcements at WWDC are strongly connected in my mind: watchOS 9 not being available for the Series 3 Apple Watch and Stage Manager not being available for non-M1 iPads. The former got a raised eyebrow from most Apple pundits—after all, the Series 3 is currently for sale—but the consensus was “At least this will force Apple to pull the damned thing.” The latter has caused quite a ruckus, forcing Craig Federighi to go out and give interviews explaining Apple’s reasoning to angry customers with relatively new iPads.

From the start, the iPad has always maintained this extremely high standard for responsiveness and interactivity. That directness of interaction in that every app can respond to every touch instantaneously, as if you are touching the real thing underneath the screen.

This explanation—pre-M1 iPads just wouldn’t provide the wonderful touch experience Apple insists on and its customers expect—has not been accepted calmly. The best summary of opinions on the topic has been collected by Michael Tsai.1

At first glance, Apple’s exclusion of of Stage Manager from pre-M1 iPads seems consistent with its exclusion of watchOS 9 from Series 3 watches. I don’t think anyone asked Federighi about the Series 3 because it’s obvious to everyone that it’s just too old and too underpowered to keep up with watchOS 9.

But if I had been interviewing Federighi and he started going on about extremely high standards for responsiveness and interactivity, I would have held out my left arm and asked him about the responsiveness and interactivity of the Series 3 on my wrist. “Does this,” I would ask, “respond to every touch instantaneously, as if you are touching the real thing underneath the screen?”

You see, things have changed since my defense of the Series 3 back in September. I was still running watchOS 7 then and the experience—apart from OS updates—was perfectly fine. But a couple of months ago, I bit the bullet and went through the multi-hour experience to update to watchOS 8. I had read that watchOS 8 would make the pain of updates a thing of the past. What I had not read was that the occasional pain of long updates would be replaced by the daily pain of a watch that commonly takes a second or two to respond to taps and swipes—if it responds at all.2

So watchOS 8 basically did to my Apple Watch what Federighi says Stage Manager will do to older iPads. I guess that means I’m defending Apple’s Stage Manager decision. But it also means that stuff about extremely high standards for responsiveness is bullshit.

  1. I could add that sentence to any post about a current Apple topic. But in this case Michael has exceeded his usual high standard. 

  2. There are some nice things about watchOS 8. Entering my passcode is actually faster than it used to be because that screen is as responsive as ever, and Apple finally realized there’s no need for an OK button; it can just log me in when I’ve tapped the last key of my passcode. It’s things like that that have kept me from scouring the internet for a way to revert to watchOS 7. But it’s a close balance. 

Remembrance of moofs past

All the old Mac users—and some not so old—had wistful smiles on their faces today. Clarus the dogcow is back.


If you need an introduction to Clarus, this history by the not-so-old Stephen Hackett is a good place to start and has lots of links to follow. My memories of Clarus—who didn’t have a name when I encountered her—are very closely tied to a piece of Mac software that has nearly faded away altogether: the Page Setup dialog box.

LaserWriter Page Setup

Nowadays, you can go years without using Page Setup. Many apps, including the editor I’m typing this in and the web browser behind it that has all my links open, don’t even include a Page Setup item in their File menu. As you can see, much of what it did has been rolled into Print.

And here’s the thing about Clarus and Page Setup: although Stephen’s history—and the screenshot above, which I nicked from him—puts her in the LaserWriter’s Page Setup, my memory is that she started in the ImageWriter’s Page Setup. And she did a lot more than just stand around inside a page-shaped box.

In addition to the rather dull options for portrait or landscape printing, the ImageWriter’s Page Setup gave you checkboxes for more exciting options.1 And Clarus would do tricks when you selected these other options.

There was even a lifehack (a word that didn’t exist back then) that got passed around for using Page Setup to get a kind of poor man’s heads-up display for your car. You could type out driving directions and print the page with a combination of left/right mirroring and inverted colors. Put the resulting piece of paper on your dashboard and you could read the directions without taking your eyes off the road. As the Clarus nostalgia washed over me today, I made a simple one to demonstrate.

Heads-up display

The black background made most of the reflected image see-through so it didn’t obscure your view. I’m not sure where this idea came from, but I’m guessing it was someone who sold ink cartridges.

Should I be pitting my memories from 35 (or more) years ago against Stephen’s research? Seems foolish. And although it’s not the way I remember it, I’m perfectly willing to believe that Clarus did debut in the LaserWriter’s Page Setup and then got backported to the ImageWriter’s. Maybe it’s because I had an ImageWriter for a couple of years before I got access to a LaserWriter that has me associating her with the ImageWriter first.

But I am absolutely certain Clarus performed those tricks in the ImageWriter’s Page Setup dialog box. That was what made her so charming, even before we knew her name.

  1. For moderate varieties of exciting. 

Updated AppleScript module for Python

JavaScript for Automation (JXA) isn’t the only way to do AppleScripty things in programs written in a language other than AppleScript. The osascript command allows you to run AppleScript within any script, as long as the scripting language has a way to call out to system commands. Shell commands can use backticks or the $() construct. Perl has backticks (which it nicked from the shell), the qx() construct, and the system command. Ruby is very much like Perl, but has %x() instead of qx(). Python has a variety of tools in the subprocess module.

Several years ago, I wrote a short Python module called applescript that used subprocess to run AppleScript code within my Python scripts. This was back when I was using Python 2, which has a slightly different subprocess than today’s Python 3. Even when I switched to using Python 3 (via Anaconda) for most of my programming, I stuck with Python 2 for the scripts that used the applescript module. After all, the scripts worked and Apple wasn’t going to just remove the Python 2 at /usr/bin/python, was it?

Well, Apple did just that in one of Monterey’s point releases, and the various Python 2 scripts I’d been relying on for nearly a decade all broke. So I rewrote the applescript module to work in Python 3, applied the 2to3 command to the old scripts that needed updating, and all’s well with the world again.

Here’s the new version of

 1:  import subprocess
 3:  def asrun(ascript):
 4:    "Run the given AppleScript and return the standard output."
 6:    osa =['/usr/bin/osascript', '-'], input=ascript, text=True, capture_output=True)
 7:    if osa.returncode == 0:
 8:      return osa.stdout.rstrip()
 9:    else:
10:      raise ChildProcessError(f'AppleScript: {osa.stderr.rstrip()}')
13:  def asquote(astr):
14:    "Return the AppleScript equivalent of the given string."
16:    astr = astr.replace('"', '" & quote & "')
17:    return '"{}"'.format(astr)

The asquote function, which I seldom use, is about the same as it was. The asrun function takes advantage of the run function in Python 3’s subprocess module to execute the string of AppleScript passed to it.

Here’s an example script, called lastnames, that uses asrun:

 1:  #!/usr/bin/env python
 3:  import sys
 4:  from applescript import asrun
 6:  cmdTemplate = '''
 7:  tell application "Contacts"
 8:    set myPeople to name of every person whose last name is "{0}"
 9:  end tell
10:  set AppleScript's text item delimiters to linefeed
11:  get myPeople as text
12:  '''
14:  cmd = cmdTemplate.format(sys.argv[1])
16:  people = asrun(cmd)
17:  print(people)

The cmdTemplate string is straight AppleScript except for the {0} in Line 8. This is a placeholder that the format function (Line 14) uses to insert the script’s first argument. If the script is run like this from the command line,

lastnames Smith

then “Smith” will be format’s argument, and the output will be a string of the full names of all the Smiths in my Contacts, one line per Smith.

This is, of course, a toy script. The applescript module becomes truly useful in scripts that use Python to assemble disparate pieces of information to build the AppleScript command, which then pulls other information out of an app and presents it back to Python for further processing. I tend to use applescript with Contacts, Calendar, Reminders, and Mail—apps that contain the information I use to run my business but whose AppleScript dictionaries can’t process that information the way I want.