A slashing odyssey
April 6, 2025 at 10:47 AM by Dr. Drang
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
Here’s Shortcuts with the Quick Action:
The Python code in the
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
from the submenu. Like this: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.
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:
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
/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
/usr/bin/perl
/usr/bin/ruby
/usr/local/bin/python
/usr/local/bin/python3
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
,
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 source
ing 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:
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 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.
submenu. Will I remember this keystroke combination? Probably not, but since I haveThat’s still easier than using the
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 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
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.-
Why did Apple decide to change their name from Services to Quick Actions even though they’re still invoked from the ↩
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? -
Yes, I could make a symbolic link, but I didn’t want to. ↩
Play Connections
April 4, 2025 at 7:53 AM by Dr. Drang
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):
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
:Step | Action | Comment |
---|---|---|
1 | ![]() |
Like it says, take a screenshot. |
2 | ![]() |
Crop the screenshot so just the game tiles appear with a little padding around them. |
3 | ![]() |
Save the image. |
4 | ![]() |
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
April 3, 2025 at 1:38 PM by Dr. Drang
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:
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:
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):
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
April 2, 2025 at 12:18 PM by Dr. Drang
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!