Proofreading with Writing Tools
January 1, 2026 at 6:51 PM by Dr. Drang
Yes, it’s been less than a week since I wrote about LLM proofreading and showed you the “final” version of the Keyboard Maestro macro I’d been using. And that wasn’t a lie because it is the final version of that macro. It’s just that I’m now using two new macros with a different approach. Today’s post will be about the first of these new macros, which combines the Proofreading command in Apple’s Writing Tools with BBEdit’s document comparison system.
The new macros came about because of some conversations I had after my previous proofreading posts. Marc Zeedar (via email), Giovanni Lanzani (via Mastodon), and Jason Snell (via Slack, but he also talked about it on Upgrade) all told me how they were using LLMs for proofreading, and all of their ideas were good in different ways. I’ve stolen different parts of their systems to come up with my new macros. Today’s macro is based mainly on Marc Zeedar’s work.
Marc is the publisher of xDev Magazine, which is focused on cross-platform development with the Xojo app system. As you might expect, his main proofreading system, which uses the OpenAI API, is built in Xojo, but he also has a couple of simple Keyboard Maestro macros that use Writing Tools. Marc’s main contribution to my new method of proofreading was to convince me that I shouldn’t fear having the LLM create a new version of the document I’m working on, one with all the changes it wants.
I needed convincing because I had the wrong model in my head for applying changes. I thought that if I had two versions of a document, my original and the one with the LLM’s changes, I’d have to either accept or reject each change in turn. While that’s fine for many changes, I had learned that LLMs often point out a mistake but don’t give me the fix I want. It’s a situation in which neither accept nor reject is the right answer. But if you use the comparison tools in BBEdit, you get a third option: you can edit text directly in the two-pane comparison window.
Here’s an example. (I know it’s small, but you probably don’t need to see the details to follow along. And you can zoom in if necessary.)

The document on the right is the original Markdown source for a post I wrote in February of last year. The document on the left has what the Writing Tools Proofread command thinks I should have written. The complaint is that I wrote “Jan 16 and Jan 2,” while Writing Tools thinks I should have written “Jan. 16 and Jan. 2,” with periods to make it clear that I was abbreviating “January.” Let’s say I agree that the text should be changed, but I don’t want what Writing Tools suggested; I want to use the full month name. I can just click in the right pane and add “uary” after each “Jan.” The regular text window with that file will be updated accordingly because the right pane isn’t just a view, it’s a live document. This makes the proofreading go very smoothly.
If you want to do this sort of thing by hand in BBEdit, choose Proofread from the Writing Tools submenu of the Edit menu.

It’s quite a ways down. A new window will pop up next to the frontmost document, and Writing Tools will spend some time working out the edits that should be made. When it’s done, the buttons at the bottom of the popup window will become active, and you can click Copy to put the edited text on the clipboard.

Now you create a new document in BBEdit, paste the clipboard contents into it, and choose Compare Two Front Windows from the Find Differences submenu in the Search menu.

That will bring up the two-pane comparison window we saw earlier, and you can step through the differences, applying the suggested changes, rejecting the suggested changes, or making your own changes as you see fit.
I, of course, refuse to do all that messing around with copying, pasting, and selecting menu items when I can write a Keyboard Maestro macro to do it for me. Here’s Proofread With Writing Tools (you may need to right-click the link to get it to download).

The only tricky thing about this macro is the Pause Until Conditions Are Met action. There’s no way to predict how long it will take for Writing Tools to generate the edited text. I initially thought I could pause until the Copy button became active, but that didn’t work. Then I realized that I could use my mouse click on the Copy button to get the macro going again.
You may be thinking, “Doesn’t Writing Tools suck? It is, after all, part of Apple Intelligence.” In my limited experience, it’s worked surprisingly well. I’ve had only two problems crop up:
- If the document is long and complex, Writing Tools may simply fail. It thinks for a long time and then puts up an error message saying it can’t do it. I don’t know what the underlying problem is, but it won’t finish unless you split the document into smaller chunks.
- It doesn’t understand Markdown footnote markers. Whenever I have something in the body of the document that looks like
[^note], it either replaces it with some other text (without the brackets and caret) or just deletes it. Obviously, I reject these changes, but it’s weird that it makes them. It’s never deleted the footnote text or the marker in front of it, just the marker in the body of the text.
My other new proofreading macro, which I hope to show you in the next few days, uses the Anthropic API instead of Writing Tools, but it still uses BBEdit’s comparison system. Thanks to Marc Zeedar for showing me its advantages.
Update 2 Jan 2026 7:59 AM
I added the last two actions to the macro this morning. The first of these (typing ⌘`) brings the text window with the edited text to the foreground, and the second closes it. Now when the macro finishes, the two-pane comparison window is in the foreground as before, but I no longer have to close the temporary text window.
Overlapping rectangle puzzle
December 31, 2025 at 12:13 PM by Dr. Drang
A couple of weeks ago, Scientific American published this puzzle.

There are nine overlapping rectangles, A through I. They overlap one another in a specific pattern using a notation I hadn’t seen before:
A\(D, F) F\(A, B, I)
B\(F, G) G\(B, C, I)
C\(G, H) H\(C, D, E)
D\(A, H) I\(E, F, G)
E\(H, I)
This means that rectangle A overlaps with two rectangles: D and F; rectangle F overlaps with three rectangles: A, B, and I; and so on. The goal is to correctly label the rectangles in the image.
My sense of the “right” way to handle this puzzle is to think of it as a network and use some powerful result from graph theory to solve it in an instant. But since the only thing I know about graph theory is that it exists, I went about it differently.
First, I labeled each rectangle with a code that indicates the number of rectangles it intersects and the number of rectangles its overlapping rectangles intersect.

For example, the rectangle at the top is labeled 3–222. That means it intersects three rectangles, and each of those intersecting rectangles intersects with two rectangles. I put the numbers after the dash in increasing order, just as the puzzle puts the rectangle letters in alphabetical order in its notation.
Then I added my notation after the puzzle’s notation:
A\(D, F) 2–23
B\(F, G) 2–33
C\(G, H) 2–33
D\(A, H) 2–23
E\(H, I) 2–33
F\(A, B, I) 3–223
G\(B, C, I) 3–223
H\(C, D, E) 3–222
I\(E, F, G) 3–233
The H and I rectangles are unique in my notation, so they could be identified immediately. I then went counterclockwise from H and saw that the leftmost rectangle had to be D (the only 2–23 intersecting H), the one at the bottom had to be A (the only 2–23 intersecting D), and then similarly up from the bottom through F, B, G, and C. E came from its connection to H and I.

When I looked at the SciAm solution, I was disappointed. It was faster than mine, but not because they used some clever math. They basically identified H and I using my method (albeit without my notation), figured out B and E from that, and then “the rest is simple.” It is simple, but it didn’t teach me anything new. Oh well, maybe next year.
Tweaking my screenshot macro
December 28, 2025 at 11:30 AM by Dr. Drang
In my recent post on the Keyboard Maestro episode of Mac Power Users, I needed a screenshot of a Typinator window and had trouble getting what I wanted. I tried using my SnapClip macro, but I found that when the macro’s user input window appeared,

the Typinator window went into the background (as expected) but didn’t come back to the foreground when the KM window was dismissed (not as expected). I think the reason was that the Typinator window isn’t a regular application window, but whatever the reason, I had to take the screenshot “by hand” using ⇧⌘4 instead of using SnapClip. My positioning of the crosshairs was off as I tried to get both the window and a desktop background border, so the image was a little wonky:

The borders are clearly of different widths. Also, and this may not be as obvious, the blue border color varies because my ⇧⌘4 screenshot captured some of the window’s shadow.
The answer was to rewrite SnapClip so the screenshot is taken before the Keyboard Maestro user input window appears. Now SnapClip can handle the Typinator window:

Much better.
The way SnapClip was written, it put up the user input window and used the results of that interaction to send options to a Python script named screenshot, the source code of which is included in the SnapClip post. screenshot called macOS’s built-in screencapture command with the appropriate arguments. To fix the “Typinator window problem,” I had to rewrite both the KM macro and the Python script.
Here’s the new version of SnapClip, which you can also download (you may need to right-click that link to download it instead of displaying its XML).

I’m not going to describe the whole thing. It’s basically the same as the old version but with these changes:
- The first action runs the
screencapturecommand and saves the resulting image in~/Pictures/Screenshots/zzzz.png. - All of the previous calls to execute the
screenshotscript have been changed to run a new Python script calledprocess-screenshot(which we’ll get to shortly) with~/Pictures/Screenshots/zzzz.pngas an argument.
As with the old version, I’ve colored the actions to help keep track of the nested Ifs. The outermost, which tests whether a title is given, is light blue-green, ; the middle one, which tests whether the image is supposed to have a background border, is orange, ; and the innermost, which tests whether the image is supposed to be uploaded to my server, is purplish-pink, .
The process-screenshot script is this:
python:
1: #!/usr/bin/env python3
2:
3: import tempfile
4: from PIL import Image
5: import os
6: import subprocess
7: import shutil
8: from datetime import datetime
9: import urllib.parse
10: import argparse
11:
12: # Handle the arguments
13: desc = 'Process a screenshot saved to a file.'
14: ep = '''If a title is given, it saves the image to a file on the
15: Desktop with a filename of the form yyyymmdd-title.png.
16: If no title is given, the image is put on the clipboard and the
17: upload option is ignored.'''
18: parser = argparse.ArgumentParser(description=desc, epilog=ep,
19: formatter_class=argparse.RawDescriptionHelpFormatter)
20: parser.add_argument('-b', '--background', help='add desktop background border', action='store_true')
21: parser.add_argument('-u', '--upload', help='upload to images directory and print URL', action='store_true')
22: parser.add_argument('-t', '--title', help='image title', type=str)
23: parser.add_argument('ssfile', help='path to screenshot file')
24: args = parser.parse_args()
25:
26: # Parameters
27: type = "png"
28: bgcolor = (85, 111, 137)
29: border = 32
30: optimizer = '/Applications/ImageOptim.app/Contents/MacOS/ImageOptim'
31:
32: # Add a desktop background border if asked for
33: if args.background:
34: snap = Image.open(args.ssfile)
35: # Make a solid-colored background bigger than the screenshot.
36: snapsize = tuple([ x + 2*border for x in snap.size ])
37: bg = Image.new('RGBA', snapsize, bgcolor)
38: bg.alpha_composite(snap, dest=(border, border))
39: bg.save(args.ssfile)
40:
41: # Optimize the file
42: subprocess.run([optimizer, args.ssfile], stderr=subprocess.DEVNULL)
43:
44: # Save it to a Desktop file if a title was given; otherwise,
45: # save it to the clipboard
46: if args.title:
47: sdate = datetime.now().strftime("%Y%m%d")
48: desktop = os.environ['HOME'] + "/Desktop/"
49: fname = f'{desktop}{sdate}-{args.title}.{type}'
50: shutil.copyfile(args.ssfile, fname)
51: bname = os.path.basename(fname)
52:
53: # Upload the file and print the URL if asked
54: if args.upload:
55: year = datetime.now().strftime("%Y")
56: server = f'user@server.com:path/to/images{year}/'
57: port = '123456789'
58: subprocess.run(['scp', '-P', port, fname, server])
59: bname = urllib.parse.quote(bname)
60: print(f'https://leancrew.com/all-this/images{year}/{bname}')
61: else:
62: subprocess.call(['impbcopy', args.ssfile])
63:
64: # Delete the temporary file
65: os.remove(args.ssfile)
The main differences between it and the screenshot script are:
- It doesn’t run
screencaptureand therefore doesn’t need to save an image file to~/Pictures/Screenshots/. - It takes the path of the already-saved screenshot as an argument and uses that variable,
args.ssfile, at various points in the script. It has a slightly different help message to reflect these changes:
usage: process-screenshot [-h] [-b] [-u] [-t TITLE] ssfile Process a screenshot saved to a file. positional arguments: ssfile path to screenshot file options: -h, --help show this help message and exit -b, --background add desktop background border -u, --upload upload to images directory and print URL -t, --title TITLE image title If a title is given, it saves the image to a file on the Desktop with a filename of the form yyyymmdd-title.png. If no title is given, the image is put on the clipboard and the upload option is ignored.
Otherwise, it follows the logic of screenshot.
Will this be my last version of SnapClip? Of course not. But it gets better every time I rewrite it, and I learn more about both my workflow and the ins and outs of macOS every time I change it.
Final (?) version of my LLM proofreading macro
December 27, 2025 at 10:57 AM by Dr. Drang
I’ve written two posts on using LLMs to proofread my posts before publishing. In the first post, I had a fairly rudimentary prompt and spent most of my time discussing the feedback ChatGPT gave me. By the second post, I had refined the prompt and was using a simple Keyboard Maestro macro to prepare the post text with line numbers to help me quickly get to the errors ChatGPT and Claude found.
The workflow described in the second post has served me pretty well for the past couple of months, but it did involve some unnecessary copying and jumping between apps, so I’ve improved the KM macro to take on what I used to do by hand.
Before I get into the specifics, I’ll start by saying that overall I’m happy with the results. Between the two of them, ChatGPT and Claude seem to ferret out almost all of my typos and poorly constructed sentences. Unfortunately, neither of them has emerged as the clear winner—one almost always finds mistakes that the other missed. So I’m still running my posts through both and don’t expect that to change, at least not for a while. And because this is the only use I have for either of them, I’m still using the free versions of each.
Being happy with the results doesn’t mean I trust them. Both flag “errors” that aren’t. My favorite recent error was this from Claude:
“watchOS 26” should be “watchOS 11” (or another actual watchOS version number - watchOS 26 doesn’t exist)
The free version of Claude is a little behind the times—or in denial.
Let’s move on to my workflow. Here’s my setup and how I get and use the LLM proofreading suggestions:
- I write all of my posts in Markdown in BBEdit.
- I access ChatGPT and Claude through site-specific browsers built using Unite. So I have apps named “ChatGPT” and “Claude” that are basically like visiting chatgpt.com and claude.ai in Safari.
- When I think I’m done writing a post, I open the ChatGPT SSB, start a new session, put the cursor in the text field and press ⌃⌥⌘C to run my Proofread Post macro (which we’ll get to soon). This pastes the prompt and a line-numbered version of my post’s Markdown source into the text field.
- ChatGPT processes what I’ve given it and returns a list of errors. Each error is preceded by the line number on which it was found.
- I activate BBEdit and go through the errors, correcting the ones that are real.
- I repeat the last three steps for Claude.
It doesn’t really make any difference whether I do ChatGPT or Claude first, but I’ve kind of settled into running ChatGPT before Claude.
In Keyboard Maestro, I have an LLM group whose macros run only when the ChatGPT or Claude SSB app is active.

This is the group in which my Proofread Post macro lives. Here it is:

If you’re interested, you can download it (you may need to right-click on that link) and adjust it to your needs.
The first step gets the text of the post by running this AppleScript:
applescript:
1: tell application "BBEdit"
2: get text of front text document
3: end tell
There’s some obvious fragility to this, but I’ve yet to run it when the frontmost BBEdit document wasn’t the Markdown source of the post I’m writing. The text is saved to the Keyboard Maestro variable MarkdownPost.
The second step passes the text through a shell filter,
nl -ba | sed -E 's/^ +//'
which adds line numbers to the beginning of each line. How this pipeline works is discussed in my earlier post. The result is saved back into MarkdownPost.
The third step combines the prompt and the line-numbered Markdown text and pastes it into the LLM’s text field for processing. The prompt is
Find typographical errors and grammatical mistakes in the following Markdown text. Do not make any changes, just tell me what you think should be changed. Ignore all embedded URLs, quoted text, and programming code. Only report actual errors, not style suggestions. I am using the linefeed character to end each line and have put line numbers at the beginning of each line; use those line numbers to report where the errors are. The text to analyze starts after a line with ten asterisks.
This is followed by a couple of blank lines, a line with ten asterisks, and the line-numbered Markdown text.
Because the third step creates an entry in Keyboard Maestro’s clipboard history that I don’t want, I added the final step to delete it.
I still read through my posts after the LLM proofreading, and I usually have my Mac read it aloud to me, too. But I’m not good at finding my own typos. ChatGPT and Claude find mistakes that my eyes and ears don’t.