A slashing odyssey

In the Mastodon announcement of my last post, I included some text that was struck through:

☃️ A shortcut I use to c̸h̸e̸a̸t̸ a̸t̸ play the NY Times Connections game. https://leancrew.com/all-this/2025/04/play-connections/

This was not done with <s> tags. Those use a horizontal line to strike through, not diagonals, and besides, Mastodon doesn’t support HTML tags.

Instead, each struck-through character was followed by the Long Solidus Overlay combining character. Combining characters overstrike the previous character, which is just what I wanted.

I used to have a Service that created struck-through text like this, but that was long ago and was written in Python 2, which I don’t use anymore. For the particular seven characters shown above, I just copied the combining solidus character from its FileFormat page and pasted it after each character I wanted struck through. That was the quickest way to get it done for a one-off.

But I do like making jokey strikethroughs like that, so a new Quick Action

1 for doing it seemed in order. Because the world has changed since that post in 2014, it seemed best to use Shortcuts to invoke a Python 3 script and save it as a Quick Action.

Here’s Shortcuts with the Quick Action:

Slash Text Shortcut

The Python code in the Run Shell Script step is this:

from sys import stdin
from string import whitespace

orig = stdin.read().rstrip('\n')
print(''.join(c if c in whitespace else c + '\u0338' for c in orig), end='')

I found that Shortcuts was adding a linefeed to the end of the selected text, so that’s why the rstrip('\n') method was added to read. The generator expression inside the join function uses Python’s somewhat tricky conditional expression, which puts an if-then-else on a single line but in then-if-else order. The generator expression adds the solidus combining character (\u0338) after each character that’s not whitespace.

Using this Quick Action is simple: select the text you want to strike through, control-click and choose Slash Text from the Services submenu. Like this:

Choosing Slash Text from the Services submenu

When I tested the Quick Action in some common text editing applications, like BBEdit, Mail, Pages, TextEdit, and even Stickies, it worked just fine. But you know I wouldn’t write a sentence like that if there weren’t another shoe waiting to drop. And you’re right. It didn’t work in Mona or Messages, the two apps in which I’d use it most frequently.

Error alert from Messages

Why did it fail in those apps? I have no idea, but whatever the reason, it was no good to me in its current state. What to do?

My first thought was to revert to using Automator to make the Quick Action. It’s supposed to be getting phased out in favor of Shortcuts, but it still works. Here’s my Slash Text Automator workflow:

Slash Text Automator workflow

The shell script is now run via bash:

source $HOME/.bashrc

slashtext

I couldn’t just use the Python code in Automator because it doesn’t allow me to use either /usr/bin/python3 or my Homebrew-installed Python as the shell. The shells it allows are

These are apparently unchangeable because they’re in a plist file on a read-only partition. Since I don’t have a Python executable in /usr/local/bin,

2 my workaround was to save my Python script as a file, slashtext, in my $PATH and call it via bash. The source code of slashfile is the same Python code as before but with a shebang line:

#!/usr/bin/env python3

from sys import stdin
from string import whitespace

orig = stdin.read().rstrip('\n')
print(''.join(c if c in whitespace else c + '\u0338' for c in orig), end='')

Going back to the shell script, my .bashrc file sets my $PATH when I work in the Terminal, so sourceing it gives the Quick Action my usual environment. That’s why I can call slashtext directly in the following line.

This Quick Action worked in every application I tried, an indication that Shortcuts still has a ways to go before it’s as reliable as Automator.

Speaking of reliability, my final thought was that I should give up on Quick Actions entirely and just make a Keyboard Maestro macro (download). Peter Lewis’s app is more trustworthy than most of what Apple’s put out recently. With the Python script already written, the only real work was adding some actions on either side of the script to deal with the clipboard. Here’s a screenshot of the macro:

KM Slash Text

Both the rstrip('\n') and the end=' ' are unnecessary here because no linefeed is added to the input and Keyboard Maestro strips trailing linefeeds from the output by default. But leaving those bits of code in doesn’t hurt anything.

Typing ⌥⇧⌘/ is certainly faster than control-clicking and navigating the Services submenu. Will I remember this keystroke combination? Probably not, but since I have KeyCue installed, I don’t need to. All I have to do is press the Control key, wait a couple of seconds for the KeyCue window to appear and choose Slash Text.

KeyCue window

That’s still easier than using the Services submenu.

By the way, Keyboard Maestro allows you to export macros as Text Services. For this one you’d first have to change the input of the shell script to the %TriggerValue% token and delete a couple of the clipboard actions, but otherwise the macro would be the same. I didn’t do it that way because I’d already convinced myself to avoid Quick Actions/Services.

If you really want to stick with Apple-supplied software while still having a fast way to slash text, you can build the Automator workflow, save it as a Quick Action, and then set a keyboard shortcut to it via the System Settings app. I’d use that solution if Peter Lewis or the Ergonis people decided to retire and no one took up their apps.

Update 6 Apr 2025 6:25 PM
John Gruber shared a Keyboard Maestro macro he uses to skip over the complicated mousework needed to access the Services menu. The macro, run via a keystroke combination, opens the submenu but doesn’t select any command. He then starts typing, which selects the Quick Action he wants, and hits Return to execute it. This takes longer to describe than it takes to do.

I like the hybrid nature of this approach. By tucking Quick Actions away in the Services menu, Apple has made them hard to get at. This macro brings them out into the light and requires the memorization of only one keystroke combination. It parallels how I use KeyCue to remind me of Keyboard Maestro macros whose keyboard shortcuts I’ve forgotten.


  1. Why did Apple decide to change their name from Services to Quick Actions even though they’re still invoked from the Services menu? Maybe it was to distinguish the action from the menu, but if that were the case, why do we now build shortcuts in the Shortcuts app? 


Play Connections

I play the NY Times Connections game every morning on my phone. The key to winning is to think through the possibilities and not be sucked in by the red herrings the game usually presents. For a long time, I tried to keep all the possibilites in my head before submitting any guesses. Then I thought of a way to help me keep track of the groups as I considered them. Later—and here’s where I entered a gray area that you might consider outright cheating—I thought of a way to automate the most boring part of that process.

Basically, I take a screenshot of the puzzle, open it in Photos, and use the highlighter tool to mark the tiles that form a group. Here’s what it looked like as I started on today’s puzzle (don’t worry, I’m not revealing any of the groups):

Highlighting Connections clues in Plotos

You’ll note that the puzzle image contains just the tiles. None of the buttons or other surrounding dross of the web page is included. That’s not really necessary, but it makes the image cleaner. I get that look by running this shortcut called Play Connections:

StepActionComment
1 Play Connections Step 01 Like it says, take a screenshot.
2 Play Connections Step 02 Crop the screenshot so just the game tiles appear with a little padding around them.
3 Play Connections Step 03 Save the image.
4 Play Connections Step 04 Open the Photos app to the Recents collection, which makes it easy to select the puzzle.

The y-offset of 490 pixels is set to make the padding roughly equal on all sides. The width of 1206 pixels is the width of my iPhone 16 Pro, and the height is the same because the game is basically a square. These values would have to change for people who play on a different phone.

When I have the game open in Safari on my phone, I press the side button, say “Play Connections” to Siri, and Photos opens a couple of seconds later, ready for me to bring up the cropped screenshot by tapping its thumbnail in the lower right corner of the Library. Then I select the highlighter tool and start playing. When I’m done highlighting, I return to Safari and submit my guesses.

We all have our boundaries, and with this I may have overstepped yours. But I watched Only Connect long before the Times stole Connections from it, and contestants typically go through many guesses in the early stages of the game to work out the red herrings. I see my little crutch as something akin to that.

Or maybe I’m just rationalizing my cheating.


Plotting a plunge

Earlier today, I opened the Stocks app to see how bad things were after yesterday afternoon’s tariff announcement. I thought its one-day plots weren’t very useful. Here’s APPL:

Stocks app plot of APPL for today

Strictly speaking, this is correct, as the share price started way down from yesterday’s close. And I guess Apple is trying to give us a sense of this by setting the top end of the plot’s range up where the price was yesterday. But anyone looking at today’s action would want to see yesterday’s closing price explicitly.

Google Finance does a better job:

Google Finance plot of APPL for today

The dashed horizontal line shows you where things were yesterday, and the closing price is also given as that line’s label. Much better.

Maybe if Tim Cook gave $1 million to the team making the Stocks app, they’d make better plots.

Update 3 Apr 2025 2:39 PM
Connor Graham points out that the dashed line at the top of the Stocks plot is yesterday’s close. I saw that line but thought it was associated with the 224 axis label off to the right. Nope. There’s actually a thin gray line above the dashed line, and that’s the 224 level. Here’s a zoomed-in view (something not available in the app itself):

Stocks app plot detail

What’s funny is that I could see the other horizontal grid lines, just not the one that’s close to the brighter dashed line. I bet people who study perception have a cool explanation for that.

So I should revise my criticism of the Stocks plot:

  • It’s grid lines are too faint to distinguish the top one from the dashed line.
  • Because it uses stupid choices for the grid locations (224? not 225?), the dashed line is always close to the top grid line.
  • Because it puts the grid labels too far below their associated grid lines, the top label looks like it applies to the dashed line.
  • It doesn’t label the dashed line. This is a minor criticism. If I’d been able to see that the dashed line was distinct from the top grid line, I would’ve known that the dashed line was yesterday’s close.

So Tim doesn’t have to spend the whole $1 million on Stocks.

Thanks, Connor!


Synonyms via JSON

While writing last week’s post on the built-in Apple thesaurus, I looked around for other ways to get a list of synonyms. I came across the Merriam-Webster API, which gives you free access (for light non-commercial use) to the M-W dictionary and thesaurus. I didn’t spend too much time with it—the Apple thesaurus is just so handy—but it was interesting, and M-W is certainly authoritative.

To use the M-W API, you have to register for a developer account. I don’t understand why they ask for a company name when it’s for non-commercial use (I entered “None”), but maybe the registration page covers commercial apps, too. Whatever. I got my API key almost immediately after submitting the form.

The thesaurus API works by returning JSON in response to a URL in this form:

https://www.dictionaryapi.com/api/v3/references/thesaurus/json/word?key=your-api-key

There’s extensive documentation for the API, but I just sort of looked through some output to see how easy it would be to extract what I might want. Here’s the JSON output for component. Get your scrolling finger limbered up.

[
  {
    "meta": {
      "id": "component",
      "uuid": "5989b740-1791-4d9f-9ce1-d4a8f6a49f11",
      "src": "coll_thes",
      "section": "alpha",
      "target": {
        "tuuid": "60760bc0-146a-4f7c-b424-debfb361e283",
        "tsrc": "collegiate"
      },
      "stems": [
        "component",
        "componential",
        "components"
      ],
      "syns": [
        [
          "building block",
          "constituent",
          "element",
          "factor",
          "ingredient",
          "member"
        ]
      ],
      "ants": [
        [
          "whole"
        ]
      ],
      "offensive": false
    },
    "hwi": {
      "hw": "component"
    },
    "fl": "noun",
    "def": [
      {
        "sseq": [
          [
            [
              "sense",
              {
                "dt": [
                  [
                    "text",
                    "one of the parts that make up a whole "
                  ],
                  [
                    "vis",
                    [
                      {
                        "t": "each set is composed of several distinct {it}components{/it}"
                      }
                    ]
                  ]
                ],
                "syn_list": [
                  [
                    {
                      "wd": "building block"
                    },
                    {
                      "wd": "constituent"
                    },
                    {
                      "wd": "element"
                    },
                    {
                      "wd": "factor"
                    },
                    {
                      "wd": "ingredient"
                    },
                    {
                      "wd": "member"
                    }
                  ]
                ],
                "rel_list": [
                  [
                    {
                      "wd": "basis"
                    },
                    {
                      "wd": "part and parcel"
                    }
                  ],
                  [
                    {
                      "wd": "detail"
                    },
                    {
                      "wd": "item"
                    },
                    {
                      "wd": "particular"
                    },
                    {
                      "wd": "point"
                    }
                  ],
                  [
                    {
                      "wd": "aspect"
                    },
                    {
                      "wd": "characteristic"
                    },
                    {
                      "wd": "facet"
                    },
                    {
                      "wd": "feature"
                    },
                    {
                      "wd": "trait"
                    }
                  ],
                  [
                    {
                      "wd": "division"
                    },
                    {
                      "wd": "fragment"
                    },
                    {
                      "wd": "particle"
                    },
                    {
                      "wd": "partition"
                    },
                    {
                      "wd": "piece"
                    },
                    {
                      "wd": "portion"
                    },
                    {
                      "wd": "section"
                    },
                    {
                      "wd": "sector"
                    },
                    {
                      "wd": "segment"
                    }
                  ],
                  [
                    {
                      "wd": "subcomponent"
                    }
                  ]
                ],
                "near_list": [
                  [
                    {
                      "wd": "aggregate"
                    },
                    {
                      "wd": "composite"
                    },
                    {
                      "wd": "compound"
                    },
                    {
                      "wd": "mass"
                    }
                  ],
                  [
                    {
                      "wd": "entirety"
                    },
                    {
                      "wd": "sum"
                    },
                    {
                      "wd": "summation"
                    },
                    {
                      "wd": "total"
                    },
                    {
                      "wd": "totality"
                    }
                  ],
                  [
                    {
                      "wd": "admixture"
                    },
                    {
                      "wd": "amalgam"
                    },
                    {
                      "wd": "amalgamation"
                    },
                    {
                      "wd": "blend"
                    },
                    {
                      "wd": "combination"
                    },
                    {
                      "wd": "intermixture"
                    },
                    {
                      "wd": "mix"
                    },
                    {
                      "wd": "mixture"
                    }
                  ]
                ],
                "ant_list": [
                  [
                    {
                      "wd": "whole"
                    }
                  ]
                ]
              }
            ]
          ]
        ]
      }
    ],
    "shortdef": [
      "one of the parts that make up a whole"
    ]
  },
  {
    "meta": {
      "id": "component",
      "uuid": "0bb8d43d-b0c8-4869-81e7-535094385289",
      "src": "CTcompile",
      "section": "alpha",
      "stems": [
        "component"
      ],
      "syns": [
        [
          "constituent",
          "individual",
          "particular",
          "cross-sectional",
          "divisional",
          "fragmentary",
          "partial",
          "local",
          "localized",
          "regional",
          "sectional"
        ]
      ],
      "ants": [
        [
          "across-the-board",
          "blanket",
          "broad-brush",
          "common",
          "general",
          "generic",
          "global",
          "overall",
          "universal",
          "all-embracing",
          "broad",
          "broad-gauge",
          "broadscale",
          "comprehensive",
          "extensive",
          "inclusionary",
          "overarching",
          "pervasive",
          "sweeping",
          "ubiquitous",
          "wholesale",
          "wide",
          "widespread",
          "aggregate",
          "collective",
          "complete",
          "full",
          "plenary"
        ]
      ],
      "offensive": false
    },
    "hwi": {
      "hw": "component"
    },
    "fl": "adjective",
    "def": [
      {
        "sseq": [
          [
            [
              "sense",
              {
                "dt": [
                  [
                    "text",
                    "as in {it}constituent{/it}"
                  ]
                ],
                "sim_list": [
                  [
                    {
                      "wd": "constituent"
                    }
                  ],
                  [
                    {
                      "wd": "individual"
                    },
                    {
                      "wd": "particular"
                    }
                  ],
                  [
                    {
                      "wd": "cross-sectional"
                    },
                    {
                      "wd": "divisional"
                    },
                    {
                      "wd": "fragmentary"
                    },
                    {
                      "wd": "partial"
                    }
                  ],
                  [
                    {
                      "wd": "local"
                    },
                    {
                      "wd": "localized"
                    },
                    {
                      "wd": "regional"
                    },
                    {
                      "wd": "sectional"
                    }
                  ]
                ],
                "opp_list": [
                  [
                    {
                      "wd": "across-the-board"
                    },
                    {
                      "wd": "blanket"
                    },
                    {
                      "wd": "broad-brush"
                    },
                    {
                      "wd": "common"
                    },
                    {
                      "wd": "general"
                    },
                    {
                      "wd": "generic"
                    },
                    {
                      "wd": "global"
                    },
                    {
                      "wd": "overall"
                    },
                    {
                      "wd": "universal"
                    }
                  ],
                  [
                    {
                      "wd": "all-embracing"
                    },
                    {
                      "wd": "broad"
                    },
                    {
                      "wd": "broad-gauge",
                      "wvrs": [
                        {
                          "wvl": "or",
                          "wva": "broad-gauged"
                        }
                      ]
                    },
                    {
                      "wd": "broadscale"
                    },
                    {
                      "wd": "comprehensive"
                    },
                    {
                      "wd": "extensive"
                    },
                    {
                      "wd": "inclusionary"
                    },
                    {
                      "wd": "overarching"
                    },
                    {
                      "wd": "pervasive"
                    },
                    {
                      "wd": "sweeping"
                    },
                    {
                      "wd": "ubiquitous"
                    },
                    {
                      "wd": "wholesale"
                    },
                    {
                      "wd": "wide"
                    },
                    {
                      "wd": "widespread"
                    }
                  ],
                  [
                    {
                      "wd": "aggregate"
                    },
                    {
                      "wd": "collective"
                    },
                    {
                      "wd": "complete"
                    },
                    {
                      "wd": "full"
                    },
                    {
                      "wd": "plenary"
                    }
                  ]
                ]
              }
            ]
          ]
        ]
      }
    ],
    "shortdef": [
      "as in constituent"
    ]
  }
]

That output came from running

curl https://dictionaryapi.com/api/v3/references/thesaurus/json/component?key=my-key | jq

Piping the output through jq put it into the nicely indented format. Without that, you just get a long inpenetrable string.

It was relatively easy to whip up a short Python script that turned the JSON into a more compact form:

 1  #!/usr/bin/env python3
 2  
 3  import json
 4  import requests
 5  from textwrap import fill
 6  import sys
 7  
 8  word = sys.argv[1]
 9  myKey = 'my-key'
10  mwURL = f'https://dictionaryapi.com/api/v3/references/thesaurus/json/{word}?key={myKey}'
11  
12  r = requests.get(mwURL)
13  j = json.loads(r.text)
14  
15  for m in j:
16    count = len(m['shortdef'])
17    for i, defn in enumerate(m['shortdef']):
18      print(fill(m['fl'] + ': ' + defn, width=70))
19      print(fill(', '.join(m['meta']['syns'][i]), width=70))
20      print()

There are no comments because this was just a quick proof of concept. Basically, it uses requests to get the API’s response, decodes the JSON into a Python data structure using the json module’s loads function, and then pulls out the parts of the response that I thought would be useful. Because the output for each part could be long, I used the fill function from the textwrap library, to make the lines no more than 70 characters long. The output for component is

noun: one of the parts that make up a whole
building block, constituent, element, factor, ingredient, member

adjective: as in constituent
constituent, individual, particular, cross-sectional, divisional,
fragmentary, partial, local, localized, regional, sectional

How it is that part can appear in the short definition but not in this list of synonyms is beyond me. Maybe it’s good that I didn’t pursue this further.

Update 2 Apr 2025 2:25 PM
Leon Cowle tells me that requests has a json method that does the work of loads. That sounds familiar, but it’s been so long since I used requests that I didn’t even bother to look for it. When I want to limit the time I spend on a script, I typically stick with what’s already in my brain and don’t go looking for improvements and efficiencies. With luck, though, this will remind me of json the next time I use requests. Thanks, Leon!