What is a JSON feed? Learn more

JSON Feed Viewer

Browse through the showcased feeds, or enter a feed URL below.

Now supporting RSS and Atom feeds thanks to Andrew Chilton's feed2json.org service

CURRENT FEED

And now it’s all this

I just said what I said and it was wrong. Or was taken wrong.

JSON


Weighing in

Permalink - Posted on 2020-09-26 13:06

In yesterday’s post, I mentioned that it took me a year to finally get around to writing a script I’d been thinking about. Today’s post is one whose seed was planted almost two years ago.

In October 2018, Myke Hurley and Stephen Hackett were in Chicago for a Relay FM event that I attended. During the event, they recorded this episode of Ungeniused, their podcast about weird articles on Wikipedia. In honor of the city they were in, the article they chose was “Raising of Chicago,” which describes how, in the 1850s and 1860s, the roadways and buildings of the city were elevated as much as six feet to get them up out of the muck and allow decent drainage of both stormwater and wastewater.

The roadways were easy because you don’t really lift a street; you just add a bunch of fill and build a new road on top of that. But substantial buildings had to be jacked up to keep their ground floors from becoming basements. Here’s an image from the article of a large team of men doing just that.

Briggs House image from Wikipedia

One of the advantages of blogging over podcasting is you get to include cool pictures like this.

As they were describing the process, Myke and Stephen mentioned the weights of some of the buildings that were lifted, and Stephen asked, “How do you estimate the weight of a building?” After the recording, I told him that the weights of significant buildings are always known by the people who build them. I further said that I would write a post about it. And to prove that I’m a man of my word, here I am… two years later.

(Another advantage of blogging—at least if you don’t have advertisers to satisfy—is that you’re not on a schedule.)

There are two good reasons architects and engineers know the weights of the buildings they design: economic and structural.

The economic reason is simple. Buildings are made primarily from commodity products—things that are sold by weight or volume. When you buy the steel, concrete, stone, masonry, wood, plaster, tile, glass, etc. that your building is made from, you are, in effect, buying your building by the pound. That was as true in 1850 as it is today.

The structural reason is nearly as simple as the economic one. The building has to stand up, which means that each part has to carry both its own weight and the weight of all the parts it supports. The top floor has to carry its own weight and that of the roof. The next floor down has to carry its own weight and that of the top floor and roof. And so on down the building. Ultimately, the entire weight of the building and its contents passes through the foundation and into the soil. The designer has to know what that weight is in total and how it’s distributed among the columns and walls of the building; otherwise, the foundation will crack or sink into the soil.

In fact, what you need to know about the weights and their distribution in order to design the building is exactly what you need to know if you want to jack that building up. The parts of the foundation that had to be extra large or extra strong because they carry more weight will also need more or larger jacks to lift it.

It can be difficult in older buildings to know what all these weights are because the plans have been discarded or misplaced. But that wasn’t a problem in the Chicago of 1850s. There simply were no old buildings back then—not any of significant size, anyway. Chicago was a very young city.

In summary, knowing the weight of a building goes hand in hand with designing it, because it’s a critical part of the building’s cost and its ability to stand up.

So there you go. Better late than never. Speaking of which…

If you’re like me and put things off, you haven’t made your donation to St. Jude’s Children’s Research Hospital yet. You’ve been hearing about it, of course, on the podcasts you listen to, and you’ve told yourself you’re going to do it. But while September is getting long in the tooth, it’s not over yet, and your donation will still help the families that rely on St. Jude’s and still count toward Relay’s fundraising goals for the month.

Stephen will appreciate your donation even more than knowing how much a building weighs.


[If the formatting of equations looks odd in your feed reader, visit the original article]


Running numbers

Permalink - Posted on 2020-09-25 15:40

About a year ago, I wrote a post comparing jot and seq, two utilities for generating sequences of numbers. They differ in the ordering of their arguments and in some of the options available for formatting the numbers. The upshot was that I liked some parts of jot, some parts of seq, and some parts of neither. The unwritten conclusion of the post was that I should write my own sequence-generating command with arguments and options tuned to the way I think and work. I’ve finally gotten around to doing that.

Here’s the code for run, a simple Python script that does the same simple task that jot and seq do, but with arguments ordered in a way that makes sense to me.

python:
 1:  #!/usr/bin/env python
 2:  
 3:  import docopt
 4:  import codecs
 5:  
 6:  usage = '''Usage:
 7:  run [options] <stop>
 8:  run [options] <start> <stop>
 9:  run [options] <start> <stop> <step>
10:  
11:  Generate a run of integers or characters. Similar to jot and seq.
12:  
13:  Options:
14:    -f FFF   formatting string for number
15:    -s SSS   separator string
16:    -c       characters instead of integers
17:    -r       reverse the run
18:    -h       show this help message
19:  
20:  The run of numbers can be integers or reals, depending on the values of start, stop, and step. The defaults for both start and step are 1. If -c is used, then start and stop must both be given as characters and step (if given) is an integer.'''
21:  
22:  # The arguments for -f and -s come in as raw strings, but we
23:  # need to be able to interpret things like \t and \n as escape
24:  # sequences, not literals.
25:  def interpret(s):
26:    if s:
27:      return codecs.escape_decode(bytes(s, 'utf8'))[0].decode('utf8')
28:    else:
29:      return None
30:  
31:  # Handle the command line options and arguments.
32:  args = docopt.docopt(usage)
33:  fstring = interpret(args['-f']) or '{}'
34:  sep = interpret(args['-s']) or '\n'
35:  rev = args['-r']
36:  char = args['-c']
37:  step = int(args['<step>'] or 1)
38:  
39:  # The interpretation of start and stop depend on -c
40:  if char:
41:    start = ord(args['<start>'])
42:    stop = ord(args['<stop>'])
43:  else:
44:    start = int(args['<start>'] or 1)
45:    stop = int(args['<stop>'])
46:  
47:  # Generate the run as a list of integers.
48:  # Include stop if it fits the sequence.
49:  run = list(range(start, stop, step))
50:  if run[-1] + step == stop:
51:    run += [stop]
52:  
53:  # Convert to text
54:  if char:
55:    runText = [ fstring.format(chr(n)) for n in run ]
56:  else:
57:    runText = [ fstring.format(n) for n in run ]
58:  
59:  # Reverse the list if asked.
60:  if rev:
61:    runText.reverse()
62:    
63:  print(sep.join(runText))

As you can see, run takes one, two, or three arguments. If given one argument, it prints the integers from 1 to that number, e.g.,

run 5

produces

1
2
3
4
5

This is just like jot and seq with a single argument.

With two arguments, run prints the integers from the first through the second, e.g.,

run 4 9

produces

4
5
6
7
8
9

This is just like seq, but very different from jot, which would need

jot 6 4

to produce the same list of integers.

With three arguments, run prints the integers from the first through the second, stepping by the third, e.g.,

run 2 12 2

produces

2
4
6
8
10
12

The argument ordering of start stop step is unlike both jot and seq, which would need

jot - 2 12 2

and

seq 2 2 12

both of which I find difficult to remember.

Undoubtedly, I find start stop step easy to remember because that’s the ordering of arguments in Python’s range() function, which I use all the time. The difference between run and range is that run’s default starting point is 1 instead of 0, and it continues through to the second argument instead of stopping just short of it. In other words, it’s written for humans, not programmers.

As you can see, though, if start, stop, and step don’t align, run won’t generate the stop value. For example,

run 1 10 2

will only print

1
3
5
7
9

because 10 doesn’t fit in the sequence. This was, to me, the best way to interpret arguments that aren’t in sync with one another.

The -f option expects a formatting string in the style of Python’s format method. Again, this syntax is easy for me to remember because I use it frequently. I can, for example, generate a sequential list of parts like this:

run -f 'Part 52Q39-{:02d}' 8 13

gives

Part 52Q39-08
Part 52Q39-09
Part 52Q39-10
Part 52Q39-11
Part 52Q39-12
Part 52Q39-13

The -s option lets me specify the separator between items in the list:

run -s ', ' 5

produces

1, 2, 3, 4, 5

This is like jot’s -s option but unlike seq’s, which treats the argument of -s as a suffix, not a separator:

seq -s ', ' 5

gives

1, 2, 3, 4, 5, 

with the comma and space trailing after the last item, too. I don’t know why anyone would want this.

Like jot, but not seq, run can produce a sequence of characters:

run -c -f 'Apt. {}' A E

gives

Apt. A
Apt. B
Apt. C
Apt. D
Apt. E

The -r option for reversing the list isn’t necessary—I could always pipe the result through sort -r—but I need reversed lists often enough that it’s worth having built-in.

You’ll note that I still use docopt to deal with options and arguments. It’s a great library, so much nicer to use than the supposedly easy argparse. One oddity that came up in this script was that I couldn’t include the default values for -d and -s in the usage string. Normally, I’d do something like

  -s SSS   separator string [default: \n]

and docopt would automatically assign the default string to args['-s'] if run was called without an -s option. But writing it that way caused a linebreak to appear at that point in the help message, which I didn’t want. Even worse, escaping the backslash by writing it as

  -s SSS   separator string [default: \\n]

meant that the literal character pair \n would be the default separator, which was flatly wrong. So I left the defaults out of the usage string and handled them in Lines 23 and 24.

And speaking of escapes, the interpret function in Lines 25–29 is needed to handle escape codes like \n and \t in the format and separator strings. When docopt processes the options, it treats the arguments to -s and -f literally, which is not what I want. When I say

run -s '\t' 5

I want

1   2   3   4   5

with actual tab characters between the numbers, not

1\t2\t3\t4\t5

I learned how to use the codecs module to process escapes from this Stack Overflow page.

I’m not done with run. It doesn’t handle floating point numbers, and its error handling consists of passing Python’s errors along to the user. Since I’m the only user, this latter deficiency isn’t all that bothersome, but I suspect I’ll be wishing I could use it for fractional steps before too long. One of the nice things about homemade programs, though, is that they can grow with you. You get to tweak them as you see how they work—and don’t work—under real-world conditions.


[If the formatting of equations looks odd in your feed reader, visit the original article]


Betas

Permalink - Posted on 2020-09-19 13:26

Apple has been doing public betas for its operating systems for five years now, and I can’t say I’ve seen any improvement in the quality of the software that’s come out since then. If the public beta program were an experiment, I’d be tempted to call it a failure, but only if software quality were the point. I doubt that it is.

It’s probably too early to assess the quality of the initial releases of iOS/iPadOS 14 and watchOS 7, but there are some troubling stories about data failing to sync across devices and crummy battery life (especially on watches). Even if the OSes themselves turn out to be of decent quality, the release was handled poorly. I’m not sympathetic to third-party developers who didn’t get their products prepared over the past few months, but by announcing on Tuesday that the release would be Wednesday, Apple forced all the third-party developers into a one-day fire drill that will lower the overall quality of software on iDevices for a couple of weeks, at least.

Of course, the reigning king of poor initial OS quality is iOS 13, whose many, many, many early releases would have been reminiscent of a silent movie comedy were it not for the loud groans coming from its users. But iOS 13 also had the great counter-example to public betas: the cursor/pointer support in iOS 13.4. Here was a major update kept hidden in Cupertino until it was sprung on the world in late March, and it pretty much just worked right from the start. Yes, there were apps that couldn’t use cursor support, but they didn’t lose functionality when it came out. I don’t recall stories of anyone suffering under iOS 13.4. Somehow, Apple managed to test it very well internally.

Which raises the question: what is the point of the public beta program? Is it really intended to improve the quality of the released version? If so, why do we keep hearing of bugs that are reported but persist throughout the beta cycle? Whatever its original purpose, the public beta program is now a marketing tool—a way to get Apple enthusiasts hyped about the new releases and hyped to buy the new products that come out alongside the new software.


[If the formatting of equations looks odd in your feed reader, visit the original article]


A bug that isn't and an error that is

Permalink - Posted on 2020-09-12 14:08

I don’t want to keep talking about dates in Shortcuts, but I keep learning new things.

Yesterday, I said

As for the Shortcuts bug, it’s something I should have found in testing right away. Although the Ask for Input step at the top of the shortcut allows me to choose a date other than the default of the current date, the event that gets added to my calendar is always three weeks after the current date. No matter what I change the date to in the picker. Kind of makes the picker pointless.

This was wrong. There is no bug in the date picker. The source of the problem was a date parsing error later in the shortcut.

You may recall that I used a trick suggested by reader Tony (@stanthwaite), the gist of which can be seen in this screenshot from his tweet:

Changing the time by reformatting

This morning, I was thinking about this “bug” and decided it didn’t make any sense. If the date picker didn’t work, surely there’d have been an outcry long ago; I wouldn’t be the first to run across it. So I started testing.

Here’s the simple testing shortcut:

Test Date Adjustment

I started out setting the custom date format of Provided Input in Step 2 to the format Tony suggested: yyyy MM dd 21:00:00.

Date format without hyphens

When the date picker came up, I set it to September 10.

Date picker set to September 10

And the alert popped up with the time I wanted, 9:00 PM, but the date set to September 17—five days from today, not five days from the date I chose in the picker.

Incorrect date with correct time

Adding hyphens to the custom date format,

yyyy-MM-dd 21:00:00

fixed the problem. That weird format Europeans like,

dd MMM yyyy 21:00:00

also worked. Both of them gave a result of “Sep 15, 2020 at 9:00 PM” when I chose September 10 in the picker.

While I understand that there are limits to what Shortcuts can parse, I don’t understand the reversion to the current date when the parsing fails. By the time we get to the second step, Shortcuts should already know that I’ve chosen September 10 in the picker. But whatever the reason, the upshot is this: If you’re going to use a custom format to adjust a date or time, use the simplest format you can for the part that isn’t getting adjusted.


[If the formatting of equations looks odd in your feed reader, visit the original article]


Contact lenses and date parsing again

Permalink - Posted on 2020-09-11 14:04

After last week’s post on my contact lens calendar shortcut, a couple of readers tweeted1 me other ways to deal with dates in Shortcuts. I want to talk about one of them and how I used it in my rewrite of the shortcut. The rewrite was inspired less by the change in how the dates get parsed and more by a bug in another aspect of Shortcuts’s date handling. I’ll talk about that, too.

The date suggestion came from Tony (@stanthwaite), who showed how I could eliminate the parsing from my shortcut by setting the time directly in the date format.

Changing the time by reformatting

I am not a purist when it comes to variable typing. For many years, I programmed in Perl, where a string is a string, unless it looks like a number, in which case it’s a number, unless you treat it like a string, in which case it’s a string. You don’t hear Perl programmers talk much about language orthogonality. But I do find it distasteful to use a formatting command—which is there to set how the date and time are displayed as strings—to change the date and time themselves.

What bothers me most about this feature, though, is that it’s basically undiscoverable.2 Which gets to my main complaint about Shortcuts: its documentation is “try it and see.” That’s fine for a video game but not for a programming language.

Ultimately, I know I’ll get over my distaste, as using formatting to change a date or time can be really powerful. Thanks to Tony for teaching me this one weird trick.

As for the Shortcuts bug, it’s something I should have found in testing right away. Although the Ask for Input step at the top of the shortcut allows me to choose a date other than the default of the current date,

Old Contact Lens Calendar Step 01

the event that gets added to my calendar is always three weeks after the current date. No matter what I change the date to in the picker. Kind of makes the picker pointless.

Since I can’t fix the bug, I need to work around it. This got me thinking about how I expect to use the shortcut and whether I need the date picker at all. As I said in the original post, I expect to run this shortcut in the evening when I throw a set of contact lenses away. If I forget to do it then, I’ll run it the next morning when I start a new set. So I don’t really need to be able to choose any date. When asked for when I threw my contacts away, I just need to be able to answer Today or Yesterday. That calls for a menu, not a date picker.

Date choosing menu

With that in mind, here’s the new Contact Lens Calendar shortcut:

StepActionComment
1 Contact Lens Calendar Step 01 Get today’s date.
2 Contact Lens Calendar Step 02 Choose the day I threw away the last set of contacts.
3 Contact Lens Calendar Step 03 If it was today…
4 Contact Lens Calendar Step 04 Set the Throw Date variable to it. Hidden in this step is Tony’s trick to use formatting to set the time of Date (and therefore throwDate) to 9:00 PM.
5 Contact Lens Calendar Step 05 If it was yesterday…
6 Contact Lens Calendar Step 06 Get yesterday’s date…
7 Contact Lens Calendar Step 07 and set Throw Date to that. Use Tony’s trick again.
8 Contact Lens Calendar Step 08
9 Contact Lens Calendar Step 09 Now get the event date three weeks from Throw Date. This magic variable will be called End Date.
10 Contact Lens Calendar Step 10 And get the day after Throw Date. This magic variable will be called Start Date.
11 Contact Lens Calendar Step 11 Create an event for 9:00 PM with an alert. The event’s Notes tell me when I started wearing the current set and remind me to run this shortcut again to set the next event.

This is longer than the original shortcut, but it’s easier to use. And it actually works, which is helpful.


  1. I’m basically off Twitter. I do follow links that take me to Twitter’s website, and when I’m there I do look at my notifications. But email is really the best way to get in touch with me now. 

  2. It’s tempting to say this is part of Apple’s continuing trend toward hidden features—of a piece with long presses vs. long-but-not-that-long presses and swipes up from the bottom vs. swipes up-but-not-that-far-up from the bottom. But Shortcuts’ opacity can’t be blamed on Apple’s current design culture; it was a ball of undocumented features back when it was Workflow. 


[If the formatting of equations looks odd in your feed reader, visit the original article]


Woodward

Permalink - Posted on 2020-09-09 22:33

Rage cover

The only question I want asked of the legendary journalist while he goes on another of his legendary promotional tours, is whether he ever ran across, in one of his legendary deep background interviews, anyone who estimated how many Americans died because he sat on Trump’s lies for six months to avoid spoiling his legendary book and depressing its legendary sales.


[If the formatting of equations looks odd in your feed reader, visit the original article]


Parsing date strings in Shortcuts

Permalink - Posted on 2020-09-05 04:39

I didn’t realize until today that Shortcuts has a date parser. It doesn’t call itself a date parser, and the documentation doesn’t explain what kinds of text strings it can parse, but it’s Shortcuts, so what would you expect?

I found it more or less by accident as I was writing a shortcut similar to this one from a few months ago. Last year, I switched from monthly contact lenses to semi-monthly. They’re thinner, more comfortable, and settle in place almost immediately.1 But without a big, obvious change in the calendar, I’ve found it harder to remember to change them in the middle of the month. On the plus side, I’ve inadvertently learned that I can wear them much longer than 15–16 days with no ill effects.

Based on this experience, I’ve decided I should switch them out every three weeks. Again, this is not a schedule I’m going to remember on my own, so I built a shortcut to run on the night I throw a set of lenses away. It adds an event to my personal calendar to remind me to throw the next set away in three weeks.

Contact lens calendar event

I expect to run the shortcut at any time of day, possibly even the next morning when I open up a new set of contacts. But I want the event—and the alert associated with it—to be at a particular time in the evening. This means I can’t get the date and time of the event by just adding three weeks to the date and time at which I run the shortcut. And I certainly wasn’t going to force myself to use those idiotic spinners to set the time—the time is fixed and the shortcut should handle it automatically.

Here’s my solution, which you can download:

StepActionComment
1 Contact Lens Calendar Step 01 Get the date I threw away the last set of contacts. This should be the current date, but I allow an adjustment in case I run this the next morning.
2 Contact Lens Calendar Step 02 Create a text string with the date from Step 1 and a time of 9:00 pm. The format of Provided Input is shown later in the post.
3 Contact Lens Calendar Step 03 Here’s the parsing step. It turns the date string from Step 1 into a date/time combo.
4 Contact Lens Calendar Step 04 This is the date/time for the calendar event. The magic variable for this step is called End Date.
5 Contact Lens Calendar Step 05 If I throw the contacts away on the date set in Step 1, I start wearing a new set a day later. This magic variable is called Start Date.
6 Contact Lens Calendar Step 06 Here we create a new calendar event for throwing away the next set of contacts. There’s an alarm and some information in the Notes field.

Here’s how the Provided Input date is formatted:

Formatting the date for parsing

As I said at the top, I don’t know all the formats the Get Dates From Input step can parse, but I figured yyyy-mm-dd HH:MM:SS would work, and it does.

Most scripting languages I’ve used have a way to change some date/time fields while leaving the others untouched, but Shortcuts apparently doesn’t. I was searching and scrolling, trying to find such a command, when I ran across Get Dates From Input. It does the job.

Why don’t I just set up an event that repeats every three weeks? Mainly because of my poor experience with doing that for switching out my CPAP supplies: the real world often intervenes, forcing you to change your carefully planned schedule. I find it easier to run a shortcut than to reset a recurring event. If I find that my contact lens schedule doesn’t change after several months of using this, I’ll switch to a long-running recurring event.

Update Sep 12, 2020 7:27 AM  An updated shortcut, with a different way of setting the time and a workaround for a Shortcuts bug, is here.


  1. I have astigmatism, so my contacts aren’t axisymmetric; they have to be oriented a particular way to work. They’re weighted to help them spin around to the right angle. When I was wearing monthlies, the right contact would usually take at least five minutes to rotate to the correct position and sometimes wouldn’t settle in for half an hour or so. 


[If the formatting of equations looks odd in your feed reader, visit the original article]


A battery BitBar bonanza

Permalink - Posted on 2020-08-30 23:36

I started reading this thread on the Keyboard Maestro forum because I’ve been interested in the Stream Deck (yes, that’s an affiliate link) for a while and will probably be getting one soon. I kept reading because TJ Luoma’s answer made me realize I didn’t need the Stream Deck to use the ideas in his solution; it would work just as well with BitBar.

The idea is to use the output of ioreg to get the battery levels of the keyboard, trackpad, and mouse, and to then use that information to tell the user it’s time to recharge. You may be thinking, “isn’t that what the low battery notifications are for?” Yes, but the problem is those notifications always seem to appear when you’re in the middle of work and don’t want to be interrupted. The idea behind a notice on your Stream Deck or in your menu bar is that you don’t need to dismiss it to get back to work, and it’s still there to remind you when you have time to plug in or change batteries.

As a practical matter, I couldn’t just tweak TJ’s solution to make it work with BitBar. TJ is a shell scripting wizard, and his script is strong evidence of that. Although I can follow the logic of what he’s doing, I would never feel comfortable trying to adjust it to my needs. So I took his ideas, rewrote them in Python, and added the parts necessary to drive BitBar.

The script, or plugin, as BitBar likes to call it, is batteries.1h.py, and it gives me a menu bar item that looks like this:1

BitBar batteries

When one of the input devices gets low on battery, the menu bar icon changes from a battery,🔋, to a plug, 🔌, to tell me its time to plug in. And if I ever connect an I/O device that isn’t a keyboard, trackpad, or mouse, the icon will change to a joystick, 🕹.

Here’s the source code of batteries.1h.py

python:
 1:  #!/usr/bin/python3
 2:  
 3:  import subprocess
 4:  import re
 5:  
 6:  # Initialize
 7:  limits = {'keyboard': 20, 'trackpad': 15, 'mouse': 15}
 8:  deviceTypes = limits.keys()
 9:  anyLow = False
10:  anyOdd = False
11:  menuString = ['---']
12:  
13:  # Regexes for capturing product names and battery percentages
14:  productRE = re.compile(r'Product" = "(.+?)"')
15:  batteryRE = re.compile(r'"BatteryPercent" = (\d+)')
16:  
17:  # Capture the output of ioreg. Remarkably, the output is
18:  # encoded in MacRoman, which will rear its ugly head if a
19:  # device has a name like Drang’s Mouse (w/ curly apostrophe).
20:  cmd = 'ioreg -r -k BatteryPercent'.split()
21:  ioreg = subprocess.check_output(cmd).decode('macroman')
22:  
23:  # The ioreg output is a series of paragraphs, one for each
24:  # product. Go through each, looking for low batteries and
25:  # adding the appropriate item to menuString. Low batteries are
26:  # printed in red; weird results, like unknown devices and
27:  # missing battery percentages, are printed in purple.
28:  for p in ioreg.split('\n\n'):
29:    productMatch = productRE.search(p)
30:    batteryMatch = batteryRE.search(p)
31:    if productMatch and batteryMatch:
32:      name = productMatch.group(1)
33:      battery = int(batteryMatch.group(1))
34:      device = ''
35:      for d in deviceTypes:
36:        if d in name.lower():
37:          device = d
38:          break
39:      if device:
40:        if battery < limits[device]:
41:          menuString.append(f'{device.capitalize()} {battery}%|color=#AA0000')
42:          anyLow = True
43:        else:
44:          menuString.append(f'{device.capitalize()} {battery}%')
45:      else:
46:        menuString.append(f'{name}|color=purple')
47:        anyOdd = True
48:  
49:  # BitBar output
50:  if anyLow:
51:    print('🔌')
52:  elif anyOdd:
53:    print('🕹')
54:  else:
55:    print('🔋')
56:  print('\n'.join(menuString))

The key to this was the recognition that the output of

ioreg -r -k BatteryPercent

can be thought of as a series of paragraphs, one for each I/O device with an entry named BatteryPercent. The script captures the output of this command in Line 21, splits it into paragraphs on Line 28, and extracts the product name and battery level for each device. This information is used to construct the multiline text output in Lines 50–56 that BitBar parses to assemble the menu.

One weird thing I found while writing this script is that ioreg uses the old MacRoman text encoding for its output. Both of my devices used possessives in their product names, e.g., “Dr. Drang’s Trackpad,” and the curly apostrophe has a byte value (in hex) of 0xD5. When I first started looking through ioreg’s output, I saw that this was rendered in my terminal as “Dr. DrangÕs Trackpad,” because 0xD5 is Õ in UTF-8 (and Latin-1). Thus the call to decode('macroman') in Line 21. I realize updatingioreg` is not one of Apple’s most pressing concerns, but it’s been two decades, Craig. None of Apple’s command line utilities should be spitting out MacRoman anymore.

I must note that TJ’s script is more tolerant of unusual output from ioreg than mine is and handles it in a more granular way. I flatly ignore certain useless outputs, because I just don’t think they’ll ever show up. It’s part of my devil-may-care personality.

Anyway, this was a fun exercise, and I now have another potentially useful item in my menu bar. It’ll be months before I know whether I take heed of this warning, but one thing I do know is that I’ve been dismissing low battery notifications for years; a warning in the menu bar has to be better than that.


  1. Yes, I’m also using the air quality BitBar plugin that Jason Snell wrote a few days ago. 


[If the formatting of equations looks odd in your feed reader, visit the original article]


RSS and the pleasure of not thinking

Permalink - Posted on 2020-08-28 15:19

I listened to the recent Mac Power Users episode on RSS while on a long walk the other day, and I really enjoyed it. Partly, of course, because I just like listening to Stephen and David, but mainly because I didn’t feel I had any stake in it.

As I’ve mentioned here several times, I have a homemade system for reading RSS feeds. And although I am willing to switch to a different setup, that different setup would have to be a tremendous improvement. Here are the advantages of what I have:

  • On the reader side, it looks the way I want it to look because it’s just an HTML page that I wrote. There are no buttons or toolbars on-screen except the ones I want.
  • It always gives me up-to-date articles. In my experience,1 some RSS aggregator services don’t poll sites often enough (or use WebSub) to keep up with edited articles. So what you get from the aggregator may not be what the article currently says. My system updates every 20 minutes, so I don’t miss edits unless they’re very recent.
  • I don’t have to think about moving to a new aggregator because the one I’m currently using just went out of business or decided to increase its subscription fee. Or because an aggregator I’m not using just reduced its fee.
  • I don’t have to think about switching readers for all those same reasons.

Honestly, it’s the “not thinking” part that’s the best. Over the 35 years I’ve been a computer user, way too much of my time has been spent thinking about the “right” software to buy. Some of this has been forced on me—when an app or service stops working, there’s no way to avoid thinking about the alternatives—but a lot has been self-inflicted. It’s nice to have one part of my computing life that’s stable and should continue to be stable for years to come.2

In some ways, I suppose, listening to Stephen and David talk about RSS was akin to schadenfreude. I could walk along, smug in the knowledge that I wouldn’t be balancing the upsides and downsides. I could just turn off my mind, relax, and float downstream.


  1. My experience is, admittedly, now well out of date, as I haven’t looked into aggregators in several years. I suspect stale articles aren’t much of a problem if you use one of the big, popular aggregators. 

  2. Was the time I spent writing my RSS scripts more than the time I would now spend thinking about the “best” RSS aggregator and reader? Doesn’t matter. I enjoyed writing the scripts. I learned new things and got satisfaction out of seeing them run correctly. I get nothing like that out of comparing apps and services. 


[If the formatting of equations looks odd in your feed reader, visit the original article]


A new old Python

Permalink - Posted on 2020-08-24 12:59

You may have noticed something new in yesterday’s scripts: the shebang lines were

#!/usr/bin/python3

I’ve been using Python 3 for quite a while, but it’s been a version installed through Anaconda, not one that came from Apple. The reasons are

  1. Apple didn’t provide a Python 3 until Catalina; and
  2. I didn’t install Catalina on either of my Macs until this past month.

I intend to keep using the Anaconda-installed version as my regular Python because its environment has all the tools I regularly use in my work: NumPy, SciPy, Pandas, and Matplotlib. But the BitBar scripts were a good way to try out Apple’s Python 3; they needed Python 3’s UTF-8 support1 and didn’t need any of those math/science libraries.

While I said above that Apple provides Python 3 in Catalina, that may be stretching the definition of “provide.” If you look in /usr/bin, you’ll find something called python3, but that something may be just a placeholder. If you haven’t installed the Command Line Developer Tools, trying to execute a script via /usr/bin/python3 will get you an error message about an “invalid active developer path.” This happened to me on one of my Macs; presumably, the CLDTs had already been installed on the other Mac and were updated when I switched to Catalina.

If you need to install the CLDTs, this explanation by Flavio Copes of how to do so via the xcode-select command is clear and concise. Once you’ve done so, you can test your new Python 3 by running

/usr/bin/python3 --version

at the command line. You should get Python 3.7.3 as the response. This was the version released over a year ago, which means its remarkably fresh for an Apple-supplied command-line tool.

If you need to install third-party libraries, as I did with the Mechanize and BeautifulSoup libraries used in my library BitBar script, you’ll have to run the Python 3 version of pip like this:

/usr/bin/pip3 install mechanize

You’ll probably get a warning that your version of pip isn’t up to date. As with Python 3 itself, the pip that comes from Apple is over a year old. It’ll still work.

As I said earlier, I don’t expect to be using Apple’s Python 3 in the future, but it’s nice to see that Mac users can use a modern Python without resort to third-party systems like Homebrew or Anaconda.


  1. OK, they didn’t need UTF-8 support, but I did. I’m too old to keep doing the encode/decode dance


[If the formatting of equations looks odd in your feed reader, visit the original article]


BitBar, SuperDuper, and library books

Permalink - Posted on 2020-08-23 20:57

Jason Snell’s recent post on BitBar inspired me to build a couple of menu bar notices of my own.

The first was a rewrite of the SuperDuper notice I used to use with a similar menu bar utility called AnyBar. It puts a thumbs up in the menu bar if the most recent scheduled SuperDuper backup was successful, and a thumbs down if it wasn’t. Clicking on the item brings up a summary of SuperDuper’s log file:

BitBar SuperDuper notice

The log summary is in gray text because it doesn’t do anything—it’s like a series of disabled menu items.

The second BitBar notice is a little more complicated. It gets the status of items my family has checked out or on hold at our local library and presents them in the menu. If a checked-out item is due soon (or overdue) or if an item on hold is ready to be picked up, the book item in the menu bar is red; otherwise, it’s blue. Either way, the items are listed in the submenus.1

BitBar Library

In addition to the submenus, this one also allows some items in the menu to be chosen. If an item is due soon, it will be enabled in the menu, and choosing it will open Safari to the library’s login page. This speeds up renewing a book if I want to keep it longer.

BitBar uses the daunting words “plugin” and “API” to describe the code that configures these menu bar items, but they’re just programs that write lines of text to standard output. If you’ve ever written any kind of program in any language, you can write a BitBar plugin. There are several rules for the output, but the main ones are:

  • Multiple lines will be cycled through over and over.
  • If your output contains a line consisting only of ---, the lines below it will appear in the dropdown for that plugin, but won’t appear in the menu bar itself.
  • Lines beginning with -- will appear in submenus.

You save the programs in a folder that you set up when you run BitBar the first time (before you have any plugins written). I use ~/.bitbar.

My SuperDuper plugin, named superduper.6h.py in accordance with the BitBar plugin naming format, is this Python script:

python:
 1:  #!/usr/bin/python3
 2:  
 3:  import os
 4:  
 5:  # Where the SuperDuper! log files are.
 6:  logdir = (os.environ["HOME"] +
 7:            "/Library/Application Support/" +
 8:            "SuperDuper!/Scheduled Copies/" +
 9:            "Smart Update Backup from Macintosh HD.sdsp/Logs/")
10:  
11:  def sdinfo(s):
12:    "Return just the timestamp and process information from a SuperDuper line."
13:    parts = s.split('|')
14:    ratespot = parts[3].find("at an effective transfer rate")
15:    if ratespot > -1:
16:      parts[3] = parts[3][:ratespot]
17:    detailspot = parts[3].find("(")
18:    if detailspot > -1:
19:      parts[3] = parts[3][:detailspot]
20:    return "%s: %s" % (parts[1].strip(), parts[3].strip(' \\\n'))
21:  
22:  # Get the last log file.
23:  logfiles = [x for x in os.listdir(logdir) if x[-5:] == 'sdlog']
24:  logfiles.sort()
25:  lastlog = logdir + logfiles[-1]
26:  
27:  # Collect data for BitBar
28:  notices = []
29:  with open(lastlog) as f:
30:    for line in f:
31:      for signal in ["Started on", "PHASE:", "Copied", "Cloned", "Copy complete"]:
32:          if signal in line:
33:            notices.append(sdinfo(line))
34:  
35:  # Format output for BitBar
36:  if "Copy complete." in notices[-1]:
37:    print("👍🏻")
38:  else:
39:    print("👎🏻")
40:  print("---")
41:  print("\n".join(notices))

The bulk of this script was taken from the predecessor to my AnyBar script, which I wrote for GeekTool.2 It reads through the long SuperDuper log file and plucks out just the lines of interest, putting them in the list notices.

The last section, Lines 36–41, looks at the last item in notices to see if the backup finished. If it did, it prints a 👍🏻; if not, it prints a 👎🏻. Then comes the --- separator in Line 40 to divide the menu title from the stuff in the dropdown. Finally, all the items in notices are printed line by line to populate the menu itself.

The library plugin, library.3h.py, consists of the following Python code, most of which was copied from an old script that sends me a daily email with the status of the family library accounts.

python:
  1:  #!/usr/bin/python3
  2:  
  3:  import mechanize
  4:  from bs4 import BeautifulSoup
  5:  from datetime import timedelta, datetime
  6:  import re
  7:  import textwrap
  8:  
  9:  # Family library cards
 10:  cardList = [
 11:  {'patron' : 'Dad',  'code' : '12345678901234', 'pin' : '1234'},
 12:  {'patron' : 'Mom', 'code' : '98765432109876', 'pin' : '9876'},
 13:  {'patron' : 'Son1',   'code' : '91827364555555', 'pin' : '4321'},
 14:  {'patron' : 'Son2',  'code' : '11223344556677', 'pin' : '5678'}]
 15:  
 16:  
 17:  # The login URL for the library's account information.
 18:  lURL = 'https://library.naperville-lib.org/iii/cas/login?service=https%3A%2F%2Flibrary.naperville-lib.org%3A443%2Fpatroninfo~S1%2FIIITICKET&scope=1'
 19:  
 20:  # Initialize the lists of checked-out and on-hold items.
 21:  checkedOut = []
 22:  onHold = []
 23:  
 24:  # Dates to compare with due dates. "Soon" is 2 days from today.
 25:  today = datetime.now()
 26:  soon = datetime.now() + timedelta(2)
 27:  
 28:  # Function that returns a truncated string
 29:  def shortened(s, length=30):
 30:    try:
 31:      out = s[:s.index(' / ')]
 32:    except ValueError:
 33:      out = s
 34:    out = out.strip()
 35:    lines = textwrap.wrap(out, width=length)
 36:    if len(lines) > 1:
 37:      return lines[0] + '…'
 38:    else:
 39:      return out
 40:  
 41:  # Go through each card, collecting the lists of items.
 42:  for card in cardList:
 43:    # Open a browser and login
 44:    br = mechanize.Browser()
 45:    br.set_handle_robots(False)
 46:    br.open(lURL)
 47:    br.select_form(nr=0)
 48:    br.form['code'] = card['code']
 49:    br.form['pin'] = card['pin']
 50:    br.submit()
 51:  
 52:    # We're now on either the page for checked-out items or for holds.
 53:    # Get the URL and figure out which page we're on.
 54:    pURL = br.response().geturl()
 55:    if pURL[-5:] == 'items':                            # checked-out items
 56:      cHtml = br.response().read()                        # get the HTML
 57:      br.follow_link(text_regex='requests? \(holds?\)')   # go to holds
 58:      hHtml = br.response().read()                        # get the HTML
 59:    elif pURL[-5:] == 'holds':                          # holds
 60:      hHtml = hHtml = br.response().read()                # get the HTML
 61:      br.follow_link(text_regex='currently checked out')  # go to checked-out
 62:      cHtml = br.response().read()                        # get the HTML
 63:    else:
 64:      continue
 65:  
 66:    # Parse the HTML.
 67:    cSoup = BeautifulSoup(cHtml, features="html5lib")
 68:    hSoup = BeautifulSoup(hHtml, features="html5lib")
 69:  
 70:    # Collect the table rows that contain the items.
 71:    loans = cSoup.findAll('tr', {'class' : 'patFuncEntry'})
 72:    holds = hSoup.findAll('tr', {'class' : 'patFuncEntry'})
 73:  
 74:    # Due dates and pickup dates are of the form mm-dd-yy.
 75:    itemDate = re.compile(r'\d\d-\d\d-\d\d')
 76:  
 77:    # Go through each row of checked out items, keeping only the title and due date.
 78:    for item in loans:
 79:      # The title is everything before the spaced slash in the patFuncTitle
 80:      # string. Some titles have a patFuncVol span after the title string;
 81:      # that gets filtered out by contents[0]. Interlibrary loans
 82:      # don't appear as links, so there's no <a></a> inside the patFuncTitle
 83:      # item.
 84:      title = item.find('td', {'class' : 'patFuncTitle'}).text
 85:  
 86:      # The due date is somewhere in the patFuncStatus cell.
 87:      dueString = itemDate.findall(item.find('td', {'class' : 'patFuncStatus'}).contents[0])[0]
 88:      due = datetime.strptime(dueString, '%m-%d-%y')
 89:      # Add the item to the checked out list. Arrange tuple so items
 90:      # get sorted by due date.
 91:      checkedOut.append((due, card['patron'], title))
 92:  
 93:    # Go through each row of holds, keeping only the title and place in line.
 94:    for item in holds:
 95:      # Again, the title is everything before the spaced slash. Interlibrary loans
 96:      # are holds that don't appear as links, so there's no <a></a> inside the
 97:      # patFuncTitle item.
 98:      title = item.find('td', {'class' : 'patFuncTitle'}).text
 99:  
100:      # The book's status in the hold queue will be either:
101:      # 1. 'n of m holds'
102:      # 2. 'Ready. Must be picked up by mm-dd-yy' (obsolete?)
103:      # 3. 'DUE mm-dd-yy'
104:      # 4. 'IN TRANSIT'
105:      status = item.find('td', {'class' : 'patFuncStatus'}).contents[0].strip()
106:      n = status.split()[0]
107:      if n.isdigit():                         # possibility 1
108:        n = int(n)
109:        status = status.replace(' holds', '')
110:      elif n[:5].lower() == 'ready' or n[:3].lower() == 'due':  # possibilities 2 & 3
111:        n = -1
112:        readyString = itemDate.findall(status)[0]
113:        ready = datetime.strptime(readyString, '%m-%d-%y')
114:        status = 'Ready<br/> ' + ready.strftime('%b %d')
115:      else:                                   # possibility 4
116:        n = 0
117:  
118:      # Add the item to the on hold list. Arrange tuple so items
119:      # get sorted by position in queue. The position is faked for
120:      # items ready for pickup and in transit within the library.
121:      onHold.append((n, card['patron'], title, status))
122:  
123:  
124:  # Sort the lists.
125:  checkedOut.sort()
126:  onHold.sort()
127:  
128:  # Assemble the information for the menu
129:  checkedAlert = False
130:  holdAlert = False
131:  checkedLines = []
132:  holdLines = []
133:  for item in checkedOut:
134:    suffix = ''
135:    if item[0] <= soon:
136:      suffix = f'|href="{lURL}"'
137:      checkedAlert = True
138:    checkedLines.append(item[0].strftime('--%b %-d  ') + shortened(item[2]) + suffix)
139:  for item in onHold:
140:    suffix = ''
141:    if "Ready" in item[3]:
142:      suffix = '|color=#700000'
143:      holdAlert = True
144:    holdLines.append('--' + shortened(item[2]) + suffix)
145:  
146:  # Print the information in BitBar format
147:  if checkedAlert or holdAlert:
148:    print('📕')
149:  else:
150:    print('📘')
151:  print('---')
152:  if checkedAlert:
153:    print('Checked out|color=#CC0000')
154:  else:
155:    print('Checked out')
156:  print('\n'.join(checkedLines))
157:  if holdAlert:
158:    print('Holds|color=#700000')
159:  else:
160:    print('Holds')
161:  print('\n'.join(holdLines))

The BitBar-specific stuff starts on Line 128. It’s more complicated than the SuperDuper script, in that:

  • The character used for the menu title is based on two criteria instead of just one.
  • It has submenus, the items of which are distinguished by a -- prefix.
  • It uses the |color=#700000 suffix to make overdue or ready items red. I’ve found BitBar’s coloring of items somewhat sketchy. Often the items start off black and don’t turn red until I’ve moved the mouse over them, away from them, and then back over them again.
  • It uses the |href= suffix to enable the menu item and cause Safari to open the given URL when chosen.

Still, there’s not much to the BitBar part of either of these scripts. Now I’m wondering what other informational scripts I have that can be converted into BitBar form.


  1. No, the Nero Wolfe video wasn’t actually overdue. Because of the pandemic, our library quarantines returned items for a few days before updating their status in the system and reshelving. So until the Current Situation is over, this notice will be red more than it should be. 

  2. Seems longer than six years since I used GeekTool; I stopped because I usually have too many windows open to see information written on the desktop. 


[If the formatting of equations looks odd in your feed reader, visit the original article]


Epic relief

Permalink - Posted on 2020-08-18 12:42

One of the great things about not being—or having aspirations of being—a professional Apple blogger is that there’s no compulsion to write about every Apple story that pops up. I was reminded of this a few days ago when the Apple/Epic war broke out. As my RSS reader filled with links and summaries and hot takes, a wave of relief washed over me. It was like seeing that my grades on the midterms and homework were good enough that I didn’t have to take the final exam.

Mind you, it’s not that I have no opinions. My chief opinion is that I should update my homemade feedreading system to give me a faster way to delete tedious entries.


[If the formatting of equations looks odd in your feed reader, visit the original article]