sc
Scott avatar
ttwillsey

One (More?) Thing

Mac

Part of the Mac series

Just about one year ago, Joe Rosensteel wrote on SixColors about putting local weather data in his menu bar. Aside from the entire system that gets the weather and gets it to his computer, the way he displays it in the menu bar is with a little utility app called One Thing. I thought it was a cool app, and I downloaded it, but never really made use of it. Until today.

Today I was digging through web server logs to solve a non-pressing but quite interesting problem, and I realized I was tired of using What’s My IP type web services to see who I am so I would know which log entries on my server are me. I put the IP in Tot so I could easily refer to it, and then I thought, “that’s great, but what about when it eventually changes?” Even though it almost never happens, it does happen very occasionally. Also, when I’m working with my terminal app in fullscreen mode, I don’t want to have to dig around for a note.

Enter One Thing.

I figured the easiest way to get a value into One Thing for this purpose would be from the command line, so I installed the One Thing command line tool. Then I googled for a nice command to get my external ip address from the command line and found

dig -4 TXT +short o-o.myaddr.l.google.com @ns1.google.com

I decided to make Peter happy by making ChatGPT do my scripting for me and asked MacGPT to write a bash script that runs the dig command, removes the quotes the dig command places around the IP address, and then passes the output to the one-thing command line tool. That script looks like this:

#!/bin/bash
# Execute the dig command and store the output in a variable
IP_ADDRESS=$(dig -4 TXT +short o-o.myaddr.l.google.com @ns1.google.com)
IP_ADDRESS=${IP_ADDRESS//\"/}
# Pass the IP address to the one-thing command
one-thing $IP_ADDRESS

The comments in the script are MacGPT’s, by the way.

Although I’ve done tons of cron jobs on linux servers, I don’t think I’ve ever actually scheduled any tasks on a Mac in recent history. I knew I was going to want to use launchd but I was fuzzy on the details. I asked MacGPT what to do, and it told me to make a plist file that references my bash script, shove it in /Library/LaunchDaemons, and set some file permissions and file ownership settings, and register it with launchd using the launchctl command.

I replied, “that’s cool, but maybe you should write the plist file for me”, and it did.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.scottwillsey.my-ip-address</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/scott/Scripts/bash-scripts/my-ip-address.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>12</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>

I moved this file to /Library/LaunchDaemons, set owner and permissions with sudo chown root:wheel com.scottwillsey.my-ip-address.plist and sudo chmod 644 com.scottwillsey.my-ip-address.plist.

Now /Library/LaunchDaemons looks like this:

scott@Songoku:bash-scripts ll /Library/LaunchDaemons
total 72
drwxr-xr-x 11 root wheel 352 Apr 8 17:12 .
drwxr-xr-x 70 root wheel 2240 Apr 7 13:30 ..
-rw-r--r--@ 1 root wheel 569 Mar 16 14:12 com.backblaze.bzserv.plist
-rw-r--r-- 1 root wheel 428 Mar 14 19:02 com.microsoft.autoupdate.helper.plist
-rw-r--r--@ 1 root wheel 594 Nov 16 23:23 com.objective-see.blockblock.plist
-rw-r--r--@ 1 root wheel 399 Jun 11 2018 com.objective-see.ransomwhere.plist
-rw-r--r--@ 1 root wheel 781 Nov 11 09:20 com.rogueamoeba.aceagent.plist
-rw-r--r--@ 1 root wheel 777 Nov 11 09:20 com.rogueamoeba.acetool.plist
-rw-r--r-- 1 root wheel 598 Apr 8 17:10 com.scottwillsey.my-ip-address.plist
-rw-r--r-- 1 root wheel 580 Nov 26 09:26 net.telestream.LicensingHelper.plist
-rw-r--r-- 1 root wheel 532 Jan 13 12:26 us.zoom.ZoomDaemon.plist
scott@Songoku:bash-scripts

As usual, the script itself needs execute permissions.

chmod +x my-ip-address.sh

And now I have my IP address in my menu bar!

The IP address in my menu bar

By the way, the IP address shown isn’t really mine. It’s Microsoft’s. Don’t try to DDOS them, but if you do I can only assume they have better DDOS protection than I do.

AppleScript GPT

Mac

Part of the Mac series

I’ve been playing with using ChatGPT (via MacGPT) with the gpt-3.5-turbo model to help script things. Today I wanted an AppleScript to rename some files by sticking a UUID in between the base file name and the file extension (e.g., rename MyImage.png to MyImage-FFE9D8C8-8AEE-42E2-8A48-F0C328A9E5F2.png). I asked MacGPT to write the script and this is what it came up with:

-- Prompt user to select input directory
set inputFolder to (choose folder with prompt "Please select the input directory")
-- Prompt user to select output directory
set outputFolderAlias to (choose folder with prompt "Please select the output directory")
set outputFolder to (outputFolderAlias as string)
-- Generate a UUID
set uuid to (do shell script "uuidgen")
tell application "Finder"
-- Get a list of all files in the input directory
set fileList to every file of folder inputFolder
-- Loop through each file and copy it to the output directory with a new name
repeat with aFile in fileList
set fileName to name of aFile
set fileExtension to name extension of aFile
set baseName to text 1 thru ((length of fileName) - (length of fileExtension) - 1) of fileName
set newName to baseName & "-" & uuid & "." & fileExtension
set newFile to outputFolder & newName
duplicate aFile to folder outputFolder with replacing
delay 3
set name of (file newFile) to newName
end repeat
end tell

It actually had delay 1 but I set it to 3.

But this script is wrong. Running this script generates an error about not being able to rename the apparently already renamed file, complete with UUID, to the desired (same) name, complete with UUID.

Finder got an error: Can’t set file "Macintosh HD:Users:scott:Documents:Podcasts:FwB:BrewsImages:OUT:Cup_Shots_Kagoshima_800x-559DCE44-3B60-4CE9-8AEE-1A862C6498FA-CBC0F756-F7E3-4368-ACFB-98F4DC50E1BB.png" to "Cup_Shots_Kagoshima_800x-559DCE44-3B60-4CE9-8AEE-1A862C6498FA-CBC0F756-F7E3-4368-ACFB-98F4DC50E1BB.png".

The reason is simple: ChatGPT set the newFile variable to outputFolder & newName and then tries to set the name of file newFile to newName later.

The correct code for the repeat loop section is:

repeat with aFile in fileList
set fileName to name of aFile
set fileExtension to name extension of aFile
set baseName to text 1 thru ((length of fileName) - (length of fileExtension) - 1) of fileName
set newName to baseName & "-" & uuid & "." & fileExtension
set newFile to outputFolder & fileName
duplicate aFile to folder outputFolder with replacing
delay 3
set name of (file newFile) to newName
end repeat

The only difference here is that now when the newFile variable is created, it’s set to outputFolder & fileName (the original filename) so that when it looks for the file to rename, it correctly looks for the original file name.

I told MacGPT about its mistake and it claims to be glad that I did, but I wonder if it learns that way. I don’t think its model changes based on user feedback, but I don’t really know what all goes into improving its learning.

By the way, in case you’re wondering why I’m using AppleScript in 2023 for a new task, it’s because I can use AppleScript very simply to tell Retrobatch to run a saved workflow that crops images to a square ratio and save them as png. Since I’m already running the AppleScript, I’m also using it to stick the UUID into the file names, something I was using a shell script for. I would have just had my Retrobatch workflow run the script, but I couldn’t get it to work correctly and I’m not sure why. The documentation on how the shell script action in Retrobatch works isn’t super comprehensive, and I didn’t find anything in the Flying Meat forum that helped either.

And to answer what might now be another question of yours, in general I stick a UUID in image file names because I don’t ever want to have to worry about image file name collisions when I shove them in the site images folder. I just want to upload them and be done with it. If I have the UUID in the file name when I start writing my post, all I have to do is make sure none of the images in that particular post have the same name (I use one UUID for all images in a post).

Bunch of Amphetamine

Mac

Part of the Mac series

Last month I wrote about Bunch, a wonderful utility for scripting work sessions, complete with sets of apps and the ability to customize various Mac settings. Last night as I was creating transcripts for Friends with Brews,, I realized that part of my transcription workflow could be handy for podcasting as well – namely, starting an Amphetamine session.

Sounds a little extreme, you might be thinking, but it’s actually less controversial than that. Amphetamine is a Mac utility that can keep your Mac from sleeping for a predetermined period of time, with granularity. Want to let the monitor sleep, but nothing else? Can do. Want to set a trigger so that whenever a specific app is running, Amphetamine is active? Can do!

In my case, since I already set up for podcasting by running my podcast bunch, I decided to see if I could trigger an Amphetamine session on and off with it. The answer is simple – can do! The reason is William Gustafson, creator of Amphetamine, made it possible to trigger sessions with AppleScript, and Brett Simmons, author of Bunch, supports running AppleScript commands in a bunch. Problem solved.

Starting an Amphetamine session with AppleScript in a bunch script:

* tell application "Amphetamine" to start new session

The asterisk tells Bunch that the following is an AppleScript command. It’s not part of the AppleScript syntax.

Stopping an Amphetamine session with AppleScript in a bunch:

* tell application "Amphetamine" to end session

Just by adding these lines to my podcast bunch, I now have the ability to stand in front of my Mac for long podcast sessions without touching mouse or keyboard and not having to worry about the screensaver kicking on. Perfect.

Astro Markdown Image Story

Part of the Astro series

Until now, Astro hasn’t had a built-in way to dump image links in straight Markdown content files and have Astro generate optimized images and responsive HTML for them. This caused me a problem, which I partially solved by using MDX instead of Markdown for blog posts, and importing and calling Astro Image inside the MDX post files. This SOUNDS great, because this is the whole purpose of MDX, in MDX’s own words:

MDX allows you to use JSX in your markdown content. You can import components, such as interactive charts or alerts, and embed them within your content. This makes writing long-form content with components a blast. 🚀

There are, however, a couple problems with this. One of them I’ve spoken about on this site, which is MDX makes it very hard to generate full-content RSS feeds with Astro (part 1 and part 2 of that saga here and here).

Also, using the Astro Image component directly in my content means mixing writing and implementation details, something I strongly dislike. When I’m writing a blog post, I don’t WANT to have to remember Astro Image syntax, and I don’t WANT to have to remember exactly what widths I like to specify and what media-query-ish styling I put in the sizes attribute. I just want to write and to let my system handle all that by itself. That’s what computers are for.

Here’s what it looks like when I want to put an image in one of my posts using MDX as my content file format and the Astro Image component directly inside my blog post:

---
import { Picture } from "@astrojs/image/components";
import somethingSomething from "/images/posts/somethingsomething.png";
---
<Picture
src={somethingSomething}
widths={[600, 900, 1200, 1500]}
sizes="(max-width: 800px) 90vw, 800px"
formats={["avif", "webp", "png"]}
alt={"This is a lot of work just to drop an image in a blog post"}
/>

I don’t want to remember that. I never want to think about that at all. I want to put an image link in using standard markdown and have Astro do all that for me.

[![This is a lot of work just to drop an image in a blog post](../../assets/images/posts/somethingsomething.png)](/images/posts/somethingsomething.png)

I have two pieces of good news for you if you’re in the same boat as me:

  1. The wonderful people at Astro are building an Astro Assets integration that can create optimized versions of and responsive img tags for images linked to in Markdown.

  2. In the meantime, you can use the really nice and fully functional Astro Markdown Eleventy Image Astro component by CJ Ohanaja. As you may have guessed, it uses Eleventy Image to do the work of intercepting Markdown image links and replacing them with responsive ones (and generating the responsive images themselves, of course).

The Astro Assets integration loudly proclaims itself as experimental, and that’s not self-deprecation: it won’t build. It runs great in the dev server, but it gives all kinds of wacky errors when trying to build. But just using it in dev mode is enough to see the future, and it’s great.

As for Astro Markdown Eleventy Image, it works great in build, but it doesn’t bother to optimize anything in dev mode. That means if you use the browser inspector tools to look at your images while testing in dev mode, you’ll see gigantic original file sizes. You’ll have to build and run preview to serve up the built pages locally to see its handiwork.

But the good news is, you can quit or never start using MDX right this minute, and you can still have optimized images from Markdown image links with Astro.

By the way, in case you’ve forgotten my RSS story at the start of this, now that I’m using straight Markdown files for my posts again, I can just straight up go back to using Astro RSS and generate an RSS feed with full post content, and not have to do my hacky custom nonsense anymore.

That’s such good news for me, because that hack only generated the RSS file in dev mode, so every time I did a build I had to copy the RSS.xml to the dist folder, AND remember to change all the link prefixes from http://localhost:3000 to https://scottwillsey.com.

Another annoying implementation detail I never want to think about again, vanquished!

Friends With Transcripts

I talked before about Whisper.cpp and my goal of getting episode transcripts up on the Friends with Brews website. That day is here! We now have transcript functionality on the site.

You’ll notice I said transcript functionality. I’m weasel-wording it a bit there because now I need to generate transcripts for all the episodes. So far I have them for episodes 1, 22, and 26 (the current podcast episode as of this writing).

On the Friends with Brews homepage, click the Transcripts link under the podcast description paragraph, and you’ll see a list of available transcripts.

Friends with Brews episode transcripts

In addition, any episodes with available transcripts will show a transcript link under its episode title.

Episode transcript link

The transcript pages themselves have links to the episode page, to the transcripts index, and to the episodes index, in addition to an episode description followed by the transcript.

Episode 26 transcript

I still have more work to do on this feature. I plan to make the raw transcripts downloadable, and also to integrate them into the RSS feed with srt formatting, at the suggestion of John Chidgey.

One thing you’ll notice right away is the transcripts are not perfect. I haven’t done any A B testing yet, but I think the transcript better separates transitions in speakers if I only output the transcript as a raw text file and don’t simultaneously output the srt file and the raw text file. At any rate, Whisper.cpp doesn’t know about individual speakers, and so there are no names showing who is saying what.

Also, Whisper gets some things wrong, and there will occasionally be some confusing text that doesn’t exactly match what we were saying at that point. Overall though, I think they’re pretty good and at least you can search the site and find what episode contained some specific mention of something. Again, it’s not perfect - if you search for Shaquille O’Neal (mentioned in episode 1), you won’t find him, because the transcript butchered the spelling of his name and I didn’t fix every typo that Whisper.cpp made.

Still, I think having transcripts, even imperfect ones, is a net gain for the site and the podcast. It adds more work for me as I have to generate them and then clean them up, but now that I have the functionality built into my Astro code, getting newly generated transcripts published is a snap.

Dimensions Are a Nightmare

Part of the Astro series

I’ve written so much about images and image optimization and yet the reality is I still have no clue exactly how it works.

Case in point: I installed Christian Ohanaja’s Astro Remark Eleventy Image plugin to parse my Friends with Brews show notes markdown files and replace any markdown images with responsive images (it both generates the image sources and creates the responsive HTML, as with any real image optimizer).

In the version I installed at the time, I immediately found that because the large source image’s width and height were included in the img element’s width and height properties, the browser ignored the size directives in the sources, and displayed the image at the x and y dimensions specified in the img tag.

Kind of defeats the point of size directives.

This is NOT an issue with Astro Remark Eleventy Image. In fact Christian now allows custom HTML markup to override this. This happens with any Picture element that includes an img tag with width and height properties. It doesn’t matter if it’s handwritten, generated by this plugin, generated directly using eleventy-img, or generated using some other image optimization plugin or scheme.

The biggest issue with NOT including them so that the browser respects the size directives instead is that now you’re subject to Cumulative Layout Shift (CLS)1 because the browser doesn’t understand how large the image will be in advance.

If anyone knows of a way to use Picture element sizes without overriding them unintentionally with img height and width but still managing to avoid CLS, I’d love to hear more about it. Tell me!

Footnotes

  1. See How To Fix Cumulative Layout Shift (CLS) Issues — Smashing Magazine

Bunch

Mac

Part of the Mac series

I’ve written a bunch of words on this site about programming stuff in Astro, but there are bunches of other things that can be scripted too. Literal Bunches in fact – enter Bunch, a Mac automation app for launching apps and running commands with just a click. It’s written by Brett Terpstra, which is a name any Mac automation geek will know.

Bunch works as a menubar app that lists your Bunches. Click on a Bunch in the list, and it executes whatever is inside that Bunch, be it names of apps to launch or to close, or commands that can include system tasks, AppleScripts, Automator workflows, or even Bash scripts.

By default, Bunches are toggles – the first time you click on a Bunch name in the menubar list, the Bunch opens. Any apps or commands that are set to open or run do so. The next time you click the Bunch in the menubar list, it does the reverse. It closes any apps that are not explicitly set to remain open when the Bunch is toggled off (or “closed”, in Bunch parlance). It also runs any commands you have set up specifically to run when the Bunch is closed.

Bunch menubar menu

Talking about in the abstract isn’t super helpful. So here’s a podcast Bunch of mine! Please note that I’m still not super fluent in Bunch and this is void where prohibited and etc, etc, etc.

---
title: Podcast
---
call_app = ?[FaceTime, Discord, Skype, Zoom] "Which calling app?"
Farrago
${call_app}
%Safari
https://docs.google.com/
%Finder
- ~/Documents/Podcasts/FwB
- ~/Library/Mobile Documents/com~apple~CloudDocs/Documents/Podcasts/FwB
- ~/Music/Audio Hijack
Audio Hijack^
(audio output AirThingies)
(audio input Shure Beta 87a & Farrago)
!<<#On Close
___
#[On Close]
studio = $ studio_display_check.sh
if studio is "true"
(audio output studio display speakers)
else
(audio output MacBook Pro Speakers)
end

That’s a lot. Here’s how it works:

---
title: Podcast
---

This top section is frontmatter and just determines what this Bunch is called in the menubar.

call_app = ?[FaceTime, Discord, Skype, Zoom] "Which calling app?" pops up a dialog box with a menu too choose which app I’m talking to cohosts on. Truthfully, it’s going to almost always be FaceTime for Friends with Brews, but other people on other podcasts use different ones. Slight aside, I’m a firm believer of podcasters always recording their end locally and the editor using all the original (better sounding) tracks, but not everyone does this.

Farrago
${call_app}

The next couple lines open my soundboard app Farrago and then whichever communication app I selected from the menu mentioned above.

%Safari
https://docs.google.com/

These two lines open Safari and then load Google Docs, which we use for show notes. The %Safari notation with the percent sign means that when I close the bunch, Safari is not closed along with the other apps in this bunch, but stays open.

%Finder
- ~/Documents/Podcasts/FwB
- ~/Library/Mobile Documents/com~apple~CloudDocs/Documents/Podcasts/FwB
- ~/Music/Audio Hijack

This section opens a finder window and opens tabs for me with some podcast related file locations.

Audio Hijack^ just opens Audio Hijack and makes it the active (focused) program.

(audio output AirThingies)
(audio input Shure Beta 87a & Farrago)

These illustrate one of Bunch’s coolest features, the ability to call system level commands. These lines do just what they look like: They set my Mac to output audio through my AirPods Pro and to use my virtual Loopback device that combines my mic and my soundboard as my audio in. This means I can set FaceTime or Zoom (or whatever app we’re talking on) to use this as its audio input, and my cohosts can hear whatever I play on the soundboard.

I’m going to cover the rest of this Bunch all at once.

!<<#On Close
___
#[On Close]
studio = $ studio_display_check.sh
if studio is "true"
(audio output studio display speakers)
else
(audio output MacBook Pro Speakers)
end

Basically the first line of this says “hey, when this Bunch is closed (toggled off), run the #On Close snippet”. The On Close snippet is in a special section at the bottom that is reserved for any snippets or snippet fragments you want to include.

My On Close snippet just runs a shell script located in the same folder as the Bunch to see if I’m connected to my Studio Display or not, and if I am, sets the output back to the Studio Display speakers. Otherwise, it sets the output to the Mac’s internal speakers.

Because this only runs when the Bunch is closed, meaning I’m done podcasting, this is exactly what I want.

This looks confusing, and I’m not going to lie – it took me awhile to get this working the way I wanted. Part of my issue was that I didn’t understand how Bunches work by default, and I thought I had to make a “Start Podcasting” Bunch and a “Stop Podcasting Bunch”, not realizing that it was set to toggle and just by choosing “Podcasting” again it would close any apps I didn’t explicitly say not to close. The rest of it was just learning the syntax. Fortunately, Brett has written excellent documentation for Bunch.

The fact that you can use conditional logic and use the output from shell scripts and set system settings and so many other things makes this a super flexible, powerful automation tool for the Mac. I used to open all these programs and set my audio settings manually, and now it requires just that many fewer clicks every time I want to podcast.

By the way, Brett has many more amazing utilities. Check out Gather CLI, for example, which lets you fetch the contents of a web page and have them converted to markdown syntax. It’s amazing and it’s perfect for doing things like saving information to Obsidian.