-
Using a Game Controller to Speed Up Tedious Mac Tasks
FULL DISCLOSURE: I personally prefer using Keyboard Maestro over Hammerspoon for handling the “actions” in this tutorial. That said, the Hammerspoon approach seemed worth mentioning, even if I don’t have much confidence in my Lua code — so here it is, just in case it’s useful!
If you spend a lot of time performing repetitive tasks on your Mac, using a game controller as an input device can make things much more efficient—and more comfortable. Whether you’re organizing files, editing video, or culling photos, a controller lets you execute common actions with minimal effort.
In this post, I’ll walk through how I use a game controller to speed up photo culling—the process of selecting the best images from a shoot. While this tutorial focuses on photo review, the same approach can be applied to many other workflows.
Culling is an essential but tedious part of a photographer’s workflow. I’ve spent countless hours sorting through tens of thousands of images in ExcireFoto 2025 and Adobe Lightroom, and I quickly realized that using just a keyboard and trackpad was slowing me down. That’s when I started experimenting with using a USB/Bluetooth game controller for culling. Instead of hunching over a keyboard, I can sit back and control everything with a lightweight, ergonomic device in my hands.
Here’s why it works so well:
- Ergonomic – Game controllers are made to fit your hands. It’s natural to hold them for hours at a time.
-
Quicker actions with muscle memory – Pressing controller buttons becomes second nature, making repetitive tasks much faster.
-
Keeps your eyes on the screen – No need to glance at your keyboard; your fingers always know where to go.
-
Better posture and comfort – You can position your monitor optimally without worrying about your keyboard.
-
Simple to program – Setting up button mappings takes only minutes.
-
Free software – You don’t need expensive tools to get this working.
-
Gamifies tedious work – Making a dull task feel more interactive can increase efficiency.
-
More control over navigation – Assign multiple actions to different buttons or combinations.
-
Works for more than just culling – This method applies to video editing, file organization, and more.
I’ve done various iterations of this over time. Today I realized I’ve never blogged about it. So, here goes!
-
Double-tap Modifier Hotkeys in Any Application
PHPStorm has this great feature where you can double-tap the shift key to open the “Search Everywhere” utility (or at least that’s how I’ve configured my PHPStorm). It’s wonderful and I use it all day. I wanted the same behavior in some of my other applications but quickly realized it’s not achievable out-of-the-box, even with Keyboard Maestro. I tried to use the double-tap usb device key as a trigger but it didn’t work. I ended up solving the problem using Karabiner Elements.
The idea is simple: Use Karabiner Elements to turn a double-tapped left shift into a hotkey I’d not realistically have set up in any application, then use that hotkey as the hotkey in Keyboard Maestro. I chose <cmd-shift-opt-ctrl-f>.
First, here is the code, which you can create as a Complex Modification in Karabiner Elements:
-
Using OBS and Blackhole as an Audio Router
I’ve been having a difficult timing capturing aggregate audio from my Shure MV7 microphone and my system audio. After trying the BlackHole + Audio Midi Setup unsuccessfully I found some information about how to use OBS Studio + BlackHole to configure, monitor, and send audio from many sources into a single output.
Using this configuration I’m able to record, transcribe, etc. all of this audio as if it were a single input source. An added bonus is that I can monitor the input, adjust levels, etc. from the OBS Studio user interface.
For what it’s worth, this entire process seems to work exactly the same with the VB-CABLE Virtual Audio Device. Just use VB-CABLE instead of BlackHole 16ch in steps 3 and 5 below.
Step 1: Install Blackhole ( brew install blackhole-16ch )
Step 2: Install OBS Studio
Step 3: Open OBS Studio’s Settings. Set Audio ➙ Advanced ➙ Monitoring Device to BlackHole 16ch
Step 4: Add macOS Audio Capture sources to OBS Studio and configure each to use Monitor and Output (monitor is the important piece). In the example below I’m getting audio from my mic and two specific applications (Chrome and Zoom).
Step 5: Use BlackHole 16ch as the “microphone” or “input” in the app of your choosing (audio recorder, transcription tool, etc.)
Bonus Tip
What if you want to control your microphone output to Zoom (or whatever) from within OBS Studio? You can use a second virtual audio device! Using the OBS Audio Monitor plugin you can route the actual audio “Output” from OBS Studio to whatever device you choose. The “Audio Mixer” dock controls the output to BlackHole (via the “Monitor Only” setting and corresponding Monitor -> BlackHole 16ch application setting), and the Audio Monitor dock controls output to VB-CABLE (via the “Monitor and Output” setting).
- Install the VB-CABLE virtual audio device
- Install the OBS Audio Monitor plugin
- Enable the Audio Monitor dock
- Configure the output to go to VB-CABLE
- In whichever apps you’d like, use VB-CABLE as the “Microphone” (input) instead of your actual microphone.
- Make sure only your microphone is set to “Monitor and Output”
- If desired, you can set a hotkey to mute/unmute your microphone via Settings ➙ Hotkeys ➙ Shure MV7 in (for example)
- You can set the same hotkey to both mute and unmute, and it’s a universal hotkey that is available even if OBS isn’t the front window.
Bonus Bonus Tip
One more for ya! Enabling the Waveform plugin in the preview area is a great way to have a visual reminder when your microphone is “hot.”
After disabling all of the unneeded docks here’s what I’m left with:
You can even add additional waveforms for your other input sources:
-
You Can’t Fit a Square Title into a Round Url
For whatever reason I always mix up the format for markdown links.
Here are the possibilities (only one is valid):
- [https://agileadam.com](View my site)
- [View my site](https://agileadam.com) ← the correct one!
- (https://agileadam.com)[View my site]
- (View my site)[https://agileadam.com]
I’ve come up with a mnemonic I’m going to try employing:
You can’t fit a square title into a round url.
It’s a variation of the idiom, “You can’t fit a square peg into a round hole.”
Square brackets. Round parentheses. You get it.
Update July 12, 2024
This still isn’t landing for me. “Can’t fit a square peg into a round hole” is easy to remember… but which is the peg and which is the hole? Square URL? Square Title? Round Title? Hmmm.
Maybe an acronym would serve me better? Here are my initial thoughts:
- “SLAP” (Square Label, Address in Parentheses)
- “SNAP” (Square Name, Address in Parentheses)
- “SLAP” (Square Label, Address in Parentheses)
If you take a closer look, it’d be easy to accidentally assume “L” means “link” (aka URL), so maybe I should rule those out. That leaves me with “SNAP”, which seems like it’d be worth trying for awhile.
I wish I knew what drives me to go down these rabbit holes.
-
Showing Salesforce RecordId in the Contact List
I have a table of contacts in Salesforce. To my surprise you cannot choose to show the RecordId (e.g., 0036g00002d4BOHAAU) in the field configuration. I did some digging and it seems it’s not possible without using some workaround that requires a calculated field or similar. I just needed the IDs for a one-time use. I realized the ID is included in the linked fields but didn’t want to have to hover each of the “Name” and “Account Name” links to see the IDs. So, I whipped up some javascript to simply append the RecordId to the end of these links. I can run the javascript via console, or even add it to a user script or place it automatically with a code injector extension.
BEFORE AFTER -
Using lnav to View Live DDEV Logs
https://lnav.org/ is a fantastic tool to work with log files. The SQL query functionality is a lot like https://github.com/multiprocessio/dsq, which also impresses.
My quick tip is that lnav can read from stdin. This means you can pipe content into it. Here’s how I monitor my ddev environment in realtime. Hooray for colors!
1ddev logs -f | lnav -qIf you haven’t explored lnav you’ll want to dive into the documentation a bit. It does a lot more than just give pretty colors to your log files / output. https://docs.lnav.org/en/v0.11.2/hotkeys.html is certainly worth bookmarking.
-
Quick Search Results using Raycast and DDGR
This is more or less a proof of concept. I wanted a fast way to look up facts without navigating away from what I’m working on.
Using Raycast is a natural place to start given you can pull it up and make it go away with ease. I looked for extensions in the Raycast Store (they’re free, despite the name) that might help but nothing seemed to do what I wanted. The Wikipedia extension does a great job of pulling information but it’s not as useful if I want a quick fact like “zip code for Portland Maine”.
Next I began looking for command line tools that return web search results. Googler came up as a top choice but the project has been archived. ddgr was the next one I came across; it works beautifully to return DuckDuckGo search results as JSON. I installed it with homebrew ( brew install ddgr ).
Finally, I whipped up a quick Raycast script command that would accept an argument, use ddgr to get the top search results, then spit them out in the Raycast output window. Raycast scrolls to the bottom of the output automatically, so I reversed the order of the top 10 results so that the most relevant appears at the bottom. That’s it!
There isn’t any graceful error handling, automated test coverage, etc. Use it at your own risk. 😉
12345678910111213141516171819202122232425262728#!/usr/bin/env python3# Required parameters:# @raycast.schemaVersion 1# @raycast.title DDGR Search# @raycast.mode fullOutput# Optional parameters:# @raycast.icon 🔍# @raycast.argument1 { "type": "text", "placeholder": "search query", "percentEncoded": false }# Documentation:# @raycast.description Search using the ddgr command line tool# @raycast.author Adam Courtemancheimport jsonimport subprocessimport sysquery = sys.argv[1]output = subprocess.check_output(["ddgr", "--json", query])data = json.loads(output)# Reverse the results so the most relevant are at the bottom# because Raycast will scroll you to the bottom of the outputdata.reverse()for result in data:print(f"{result['abstract']} {result['url']}")print("") -
Handling Daylight Saving Time in Cron Jobs
- Our business is located in Maine (same timezone as New York; we set clocks back one hour in the Fall and ahead one hour in the Spring)
- Server A uses ET (automatically adjusts to UTC-4 in the summer, and UTC-5 in the winter)
- Server B uses UTC (not affected by Daylight Saving Time) – we cannot control the timezone of this server
We want to ensure both servers are always referencing, figuratively, the same clock on the wall in Maine.
Here’s what we can do to make sure the timing of cron jobs on Server B (UTC) match the changing times of Server A (ET).
12345# Every day at 11:55pm EST (due to conditional, commands only run during Standard Time (fall/winter)55 4 * * * [ `TZ=America/New_York date +\%Z` = EST ] && php artisan scrub-db >/dev/null 2>&1# Every day at 11:55pm EDT (due to conditional, commands only run during Daylight Saving Time (spring/summer)55 3 * * * [ `TZ=America/New_York date +\%Z` = EDT ] && php artisan scrub-db >/dev/null 2>&1Only one of the php artisan scrub-db commands will execute, depending on the time of year.
-
You may want to disable “Preferred Activities” on your Google Nest Wifi
Today I had the pleasure of experiencing fiber internet for the first time. The technicians were excellent and had it up and running in no time. Their hardwired tests showed great upload and download speeds. On my computer (hardwired also), however, my upload seemed to be stuck around 13Mbps. They assured me it was something about my configuration (cable, router, computer, etc.). I did some googling and mentioned it to co-workers. One co-worker mentioned something that lead me to the “Preferred Activities” settings for my Google Nest Wifi. Unchecking a single checkbox instantly (no reboot required) bumped my upload speed up from 13Mbps to 800Mbps.
Before (preferred activities enabled for Video Conferencing in Google Home app; I think this is default):
After (no Preferred Activities boxes checked)
Also, I want to give a shout out to GoNetSpeed. They were a well-oiled machine from the first phone call to the final setup. And they delivered on their promise a day early.
-
Wooshy + Keyboard Maestro = Incredible Power
UPDATE: I have a more reliable way of doing this with Applescript + KM. I will post someday when I have time.
I don’t have time to write a full post, but this is too good to not share quickly.
Wooshy lets you find+click elements on the screen by their hover text (in addition to many other features). This means you can target buttons that would be hard to target, like a color picker box (see below).
Keyboard Maestro lets you programmatically invoke/use Wooshy.
Using both, you can target virtually anything on the screen with a high degree of accuracy.
Here’s a visual example. Note that the steps in purple are easily be automated with Keyboard Maestro. Also it’s worth noting you can target all of the text on the hover text for even more accuracy (so I’d have KM search for “Set the text color, Option-click for Color wheel” in the example below. Each keystroke takes more time, so it may not be worth it.
The following KM macro doesn’t have any safety checks (and I really hate using Pause), but it does illustrate the potential: