My post detailing a Keyboard Maestro macro to open Jupyter notebooks had a dumb bug in the second shell pipeline, which fetches the URL of the desired notebook.

You’d hit it if:

  • You have more than one notebook server running.
  • The working directory of one is beneath another.
  • The subdirectory server was started more recently.
  • You tried to open the parent server with the macro.

The shorter path of the parent would match part of the child’s path.

The original grep pattern was:

grep "$KMVAR_dir"

And is now:

grep ":: $KMVAR_dir$"

So that it only matches the exact directory chosen in the list prompt, and not one of its children.

I’ve updated the Keyboard Maestro macro file too.

When I use images here, I tend to give ones without any transparency a border, which is done using CSS and applied to img tags unless they have a no-border class.

Like a good web citizen, I also specify image dimensions in HTML:

“The image’s rendered size is given in the width and height attributes, which allows the user agent to allocate space for the image before it is downloaded.”

In fact my BBEdit image snippet makes it a doddle:

<p <#* class="full-width"#>>
        alt="<#alt text#>"
        <#* class="no-border"#>

But this causes a problem, which I’ve spotted in a couple of my recent posts.

If you specify the image dimensions, and use a CSS border, and have your CSS box-sizing set to border-box, then the CSS border shrinks the amount of space available to the image to its specified dimensions − 2 × the border width.

So if you specify your img dimensions to match the dimensions of the file, then the image itself will be shrunk within the element.

This animation shows this situation, and what happens when you toggle the CSS border. Watch what happens to the image itself.

An animation showing an image being squeezed within the space it has been allocated, causing distortion.

(It’s got a slight offset from the text because it’s a screenshot of this blog and includes some of the background on each side.)

In contrast, this animation shows what happens when the dimensions are not specified, and so the image is free to grow when the border is applied:

An animation showing an image growing when a CSS border is applied, with no distortion to the image itself.

Really the culprit here is box-sizing: border-box, forcing the border to remain within the size of the img element itself. This is a behaviour you actually want, as it solves the old CSS problem of juggling widths, borders and padding within a parent element. Check out MDN’s box-sizing page to see what I mean.

What are my options, then?

  • Change box-sizing.

    I’m not touching this because the potential sizing headaches are not worth it, even just for img elements.

  • Apply a border to the image files themselves.

    No, because if I change my mind about the CSS, previously posted images are stuck with the old style forever. CSS borders should also work correctly across high-density displays, whereas a 1px border in the file may not.

  • Don’t specify dimensions in the HTML.

    I don’t like the idea of making pages of this site slower to render, but I think this is the least bad option, particularly given that this site is already pretty fast.

It’s not ideal, but that BBEdit snippet is now just:

<p <#* class="full-width"#>>
        alt="<#alt text#>"
        <#* class="no-border"#>

Hey, at least it makes images quicker to include in posts!

I have a startup item that launches a Jupyter notebook so that the server is always running in the background. It’s an attempt to reduce the friction of using the notebooks.

By default, Jupyter starts the server on port 8888 on localhost, but expects a token (a long hexadecimal string) before it’ll let you in. If you list the currently running servers in the terminal you can see the token and also the server’s working directory.

% jupyter-notebook list
Currently running servers:
http://localhost:8889/?token=…hex… :: /Users/robjwells
http://localhost:8888/?token=…hex… :: /Users/robjwells/jupyter-notebooks

We can use this to make finding and opening the particular notebook server you want a bit easier, using Keyboard Maestro.

A screenshot showing the (minimised) Keyboard Maestro steps

The macro uses the jupyter-notebook command, so that’ll need to be in your $PATH as Keyboard Maestro sees it.

The first and third steps both execute jupyter-notebook list and use Unix tools to extract parts from it.

In between, if there’s more than one notebook server running, the macro prompts the user to choose one from a list of their working directories.

A Keyboard Maestro list selection dialogue

Here’s the first step, where we fetch the list of working directories.

jupyter-notebook list | tail -n +2 | awk '{print $3}'

Our +2 argument to tail gets the output from the second line, chopping off the “Currently running servers:” bit. Then awk prints the third field, which contains the directory. (The first is the URL, the second the double-colon separator.)

The third step fetches the corresponding URL for a directory:

jupyter-notebook list | grep ":: $KMVAR_dir$" | awk '{ print $1 }'

Since the user has specified a directory already, we use grep with the Keyboard Maestro variable to find just that one line, and use awk again to extract the URL field.


There was a bug in the original version of this snippet of shell script, where a parent path could match a child path (as it was only looking for the path itself without an anchor on either side). It was only luck that had me miss this with my example, with the more recently started home directory notebook server being listed ahead of one in a subdirectory, which grep would have also matched. The code above and the macro file have been fixed.

Obviously, this won’t work if you have more than one notebook server running from the same directory. (But you wouldn’t do that, right?)

Here’s the macro file if you’d like to try it out.

After I published my post about manipulating tables (of data) in R, I noticed that there was something amiss with the HTML table in that post showing an example section of our newsroom rota.

A screenshot showing a table laid out with table-layout: fixed in CSS, with many cells wrapping with scrollbars in an unreadable fashion.

When I first wrote the CSS for this site, roughly five years ago, I had HTML tables set so that the whole table would scroll were it to be too wide for its containing column. At least, I’m pretty sure it worked like that.

Anyway, as you can see above, it doesn’t work like that now. The table there is laid out with the following CSS:

table {
  table-layout: fixed;
  width: 100%;

Which has the effect of restricting the table size to 100%, and doing odd things to the cells if there’s too much to fit in whatever width 100% happens to be.

As an attempted quick fix, I removed the table-layout property so that it would inherit the default, auto. The width is still 100% to provide some consistency, rather than having an odd assortment of table widths.

So the CSS is now this:

table {
  width: 100%;

This has the effect of having the table overflow the container horizontally if the content is too wide, like so:

A screenshot showing a table laid out with table-layout: auto in CSS, with the table overflowing its container horizontally.

Which is perhaps more readable if pretty ugly. And not what I wanted: to scroll the entire table within its container.

I said attempted earlier because I, er, never deployed the change on the site (it’s been a busy couple of weeks, contrary to the post tempo).

In the meantime, I stumbled across a fix by opening Safari’s reader mode, in which tables scroll horizontally within their container! The secret? The table is wrapped in an enclosing div, which has its overflow-x property set to auto, and then the table scrolls within the div.

Here’s what that looks like when rendered:

A screenshot showing a table laid out and scrolling within a containing div with its overflow-x property set to auto.

Here’s the HTML:

<div class="table-container">

And here’s the CSS:

table {
  width: 100%;

.table-container {
  overflow-x: auto;

You want auto instead of scroll as the latter shows the scrollbar all the time.

One of my responsibilities at work is to provide a list of people who our printers should call if there’s ever a problem with the edition. Usually that’s the chief sub, or whoever is covering her.

I also prepare the rota for the journalistic staff, which I use as the source of information for the responsibility list.

This job has largely escaped automation. I do have a Python script that prints a nice template report for the week ahead, complete with BBEdit placeholders, but working out whose name should be attached to each edition is just done by reading the rota across and deleting names from the template list until you’re down to one.

However, I’ve found things of this nature, if not automated, are put off, forgotten, or done wrong. This, because it’s not actually vital to anything, is no exception, particularly when I’m pulled into jobs that actually are vital.

The report looks a little like this, so you get the idea:

Tue May 08    16pp    Alice Jones
Wed May 09    16pp    Bob Smith
Thu May 10    16pp    Rob Wells

And so on, with the pagination in the middle column.

The pagination is consistent (16 in the week, 24 on the weekend) with occasional larger editions. It can either be predicted with total certainty or none at all, as the large editions vary considerably with advertising and feature articles.

The responsibility can’t be predicted because we don’t work fixed patterns (we don’t have enough staff to do so). However, it can be done in advance once the newsroom rota is completed.

So let’s forget the pagination and just focus on pulling together a list of every production day in the completed period and who is the chief sub.

Our newsroom rota is just a spreadsheet, which is actually the best tool I’ve found so far for handling a couple dozen people with intricate job-cover links between them. (The rota used to be laid out in InDesign, which, no matter what you think about spreadsheets or InDesign, was much more difficult.)

It looks a bit like this (the real spreadsheet has proper formatting and so on):

Sun 6/5 Mon 7/5 Tue 8/5 Wed 9/5 Thu 10/5 Fri 11/5 Sat 12/5 Lieu add Lieu tot
Rob Wells Off Sport Ch Sub Sport 10
Alice Jones Off 0.25 4.5

There’s a fair amount of information: names, dates, days off, cover responsibilities, new and accrued TOIL. It’s entirely designed for humans, not computers (and it takes the humans a little while until they’re able to read it).

A lot is implicit. If we assume in this example that Alice is the chief sub, she is performing that role on her usual working days (the empty cells). It is only marked for people who have to cover someone else’s job.

This table is not something that you can just chuck into a computer program; it needs cleaning up first.

Thankfully, R (and the Tidyverse particularly) is a great environment in which to wrangle your data, and to do so fairly quickly. All the code below was pulled together in about 30 minutes total (with a good 10 minutes of reading documentation and fixing errors in the original source data). Writing this post has taken much longer.

In our example below we’re going to have four workers who each cover the chief sub at different times. Here we’re going make “Dan Taylor” the chief sub. Congratulations, Dan!

First we’ll pull in our libraries.


Then we’ll read in the data, which is saved in a TSV file after copying and pasting from the spreadsheet into a text document. We’ll select only the production days and the unnamed first column (named X1 on import), excluding Saturdays and the TOIL columns.

wide <- read_tsv('chsub.tsv') %>%
    select(matches('^(Mon|Tue|Wed|Thu|Fri|Sun) |X1')) %>%
    rename(name = X1)

Then we’ll use a tidyr function, gather(), to transform our wide format into a tall one by selecting the date columns. It’s easier to get a feel for gather() by looking at the output.

tidy <- wide %>%
    gather(matches('^(Mon|Tue|Wed|Thu|Fri|Sun) '),
           key = date,
           value = status)
## # A tibble: 6 x 3
##   name           date     status
##   <chr>          <chr>    <chr>
## 1 Alice Jones    Sun 29/4 Off
## 2 Bob Smith      Sun 29/4 Sick
## 3 Carol Williams Sun 29/4 Booked
## 4 Dan Taylor     Sun 29/4 <NA>
## 5 Alice Jones    Mon 30/4 <NA>
## 6 Bob Smith      Mon 30/4 Off

We now have a row for each person for each day, along with their “status” for the day.

But Dan doesn’t have his chief sub days marked, as it would be nearly every day. Let’s split out Dan’s rows and replace the empty cells with Ch Sub, the same status string used by everyone else. Then we’ll combine the filled-out Dan rows with all the non-Dan rows from the original data frame.

dan_replaced <- tidy %>%
    filter(name == 'Dan Taylor') %>%
    replace_na(list(status = 'Ch Sub'))

all <- tidy %>%
    filter(name != 'Dan Taylor') %>%

## # A tibble: 6 x 3
##   name       date      status
##   <chr>      <chr>     <chr>
## 1 Dan Taylor Sun 30/12 Ch Sub
## 2 Dan Taylor Mon 31/12 Ch Sub
## 3 Dan Taylor Tue 1/1   Ch Sub
## 4 Dan Taylor Wed 2/1   Ch Sub
## 5 Dan Taylor Thu 3/1   Ch Sub
## 6 Dan Taylor Fri 4/1   Ch Sub

Great. But poor Dan, he’s working every day over New Year 2018-2019. In reality, I haven’t done that far on the rota, just up to October. We’ll convert all those dates now, and filter out all the newly missing entries where the month was outside our range.

dated <- all %>%
        date = dmy(str_c(str_extract(date, '\\d+/[4-9]'), '/2018'))
    ) %>%

Let’s get only the chief sub-related rows and sort them by date.

chsub <- dated %>%
    filter(str_detect(status, 'Ch Sub')) %>%
    arrange(date) %>%
    select(date, chief_sub = name)
## # A tibble: 6 x 2
##   date       chief_sub
##   <date>     <chr>
## 1 2018-04-29 Dan Taylor
## 2 2018-04-30 Dan Taylor
## 3 2018-05-01 Dan Taylor
## 4 2018-05-02 Bob Smith
## 5 2018-05-03 Dan Taylor
## 6 2018-05-04 Carol Williams

Exactly what we want. Now time for a bit of formatting to make this giant list somewhat acceptable for other people. This is also where my knowledge of R runs out.

formatted <- str_c(
    format(chsub$date, '%a %Y-%m-%d'),
           sep = '  ')

fd <- file('output.txt')
writeLines(formatted, fd)

So we’ll switch to Python, printing a blank line between each production week (of six days).

with open('output.txt') as f:
    for idx, line in enumerate(f.readlines()):
        if idx % 6 == 0:
        print(line, end='')
Sun 2018-08-19  Carol Williams
Mon 2018-08-20  Carol Williams
Tue 2018-08-21  Alice Jones
Wed 2018-08-22  Carol Williams
Thu 2018-08-23  Carol Williams
Fri 2018-08-24  Carol Williams

Sun 2018-08-26  Carol Williams
Mon 2018-08-27  Carol Williams
Tue 2018-08-28  Carol Williams
Wed 2018-08-29  Dan Taylor
Thu 2018-08-30  Dan Taylor
Fri 2018-08-31  Dan Taylor

Perfect. And ready for whenever I get time to update the rota again.