Clean Downloads Folder on Mac with Hazel
I have a confession to make. I eased into this by sharing the same info on Twitter/Mastodon already.
My ~/Downloads folder is a mess.
It’s much less messy than your Downloads folder, most likely, but still.
I achieved relative de-messification by automatically filing old downloads into sub-folders, one per month, like ~/Downloads/2022-05 Downloaded. That was amazing because the actual Downloads folder was clean, and old stuff was somewhat highlighted.
But I have 2019-06 Downloaded, and 2020-06 Downloaded, and then some. I often do not delete these. And since there are so many super old archives already, I stopped manually deleting newer ones, too.

I tagged this post #minimalism and #productivity, so here’s what I’m going to do, now that I’ve confessed:
Even though I know deep down in my heart that there are useful article PDFs and other downloads in some of these folders – I will delete everything that’s older than 3 months!1
And I’ll adjust my Hazel rules accordingly!
Here’s the old and trusty archival rule:

And here’s the new rule that matches the YYYY-MM Downloaded pattern and trashes everything older than a couple of months:

And the result is magical!

I am still afraid that I’ve overlooked something, of course. That’s why I didn’t trash the old folders in the first place. But I can assume that it wasn’t important – or else I’d have moved it someplace else.
With the clean(er) slate, it’ll be easier to keep a tap on things, I hope. For starters, I don’t need to scroll down two windowfuls of folder names to get to actual downloads, so I don’t overlook unfiled invoices in the future.
Ah well, the benefits of blogging and publications in general. Peer pressure, even if only imagines, is really a thing. So y’all have bullied me into this as I was writing. Thanks, I guess.
-
Ok, I lied a bit – I did quickly review all the folders to check if I forgot to move any downloaded invoice into my actual invoice archive for bookkeeping and the like. Then I deleted everything. ↩
Weak Self -- Closure Rules of Thumb
In Swift, you can weak-ify references to self in escaping closures, and then you need to deal with the case that the reference is gone when the block is called.
Last month, Benoit Pasquier and Chris Downie presented different takes on the problem. That discussion was excellent. It prompted me to take some more time to revisit this problem systematically, and I took away a couple of notes for future-me.
The three popular options as Chris listed them are these:
- Do not capture strong reference
- Override
selfwith strong reference - Bind
strongSelf
As always, either way has its pitfalls.
I’ve used all approaches in my own and client apps.
But I do prefer (2), to use [weak self] in for outer and inner closures, with guard let self = self else { return } early on because this can symmetrically and consistently be used everywhere: It doesn’t matter if it’s the inner or outer closure. You can do this habitually, with code completion or templates, and write static code analyzers to catch that in PRs. These are all benefits in my book.
The dance with strongSelf creates a parallel set of problems and can make the inner closure’s setup depend on the outer closure I would like to avoid because I know I’m too stupid to get this correct 100% of the time.
See the rundown below for details.
1. Do not capture strong reference
Use self?.foo everywhere – but self can become nil in the middle of the block.(via Chris Downie)
Might not be what you want to use outside of one-liners.
2. Overriding self
guard let self = self else { return } override the local weak, optional self reference with the strong, non-optional one.
-
Benoit’s point: You can forget
weakifying inner closures and accidentally create a retain cycle again! - Can arguable make it a bit harder to mess this up when you do it consistently, like a machine. (See 3. below.)
Problematic example adapted from Benoit:
self.doSomething = { [weak self] in
guard let self = self else { return }
self.doSomethingElse = { // ⚠️ forgot [weak self], now there's a cycle!
self.foo()
}
}
self.doSomething()
Could potentially be detected by static analyzers and SwiftLint.
3. Capture in e.g. strongSelf
guard let strongSelf = self else { return } – Bind strong self reference inside the block’s scope with a new name.
Swift compiler doesn’t warn you if you accidentally bind strongSelf from outside. Example by Chris
firstChild.playLater { [weak self] in
guard let strongSelf = self else { return }
strongSelf.gamesPlayed += 1
strongSelf.secondChild.playLater {
if let strongSelf = self {
// 👍 Locally bound the weak self reference.
// (But didn't use the bound variable.)
print("Played \(self?.gamesPlayed ?? -1) with first child.")
}
// ⚠️ Strongly captures `strongSelf` from outside by accident
// and creates cycle.
strongSelf.gamesPlayed += 1
completion(strongSelf.gamesPlayed)
}
}
To mitigate, make sure to always
guard let strongSelf = self else { return }
at the beginning of a block. Could be detected by static code analyzers.
But this is too clever for my taste: in the example above, you don’t need to pass in any weak reference. The outer block weak-ifies the reference to self already, and that’s enough. Then the strongSelf reference lives next to it and creates a parallel set of problems. – Instead, I favor making the same mistake in all places and consistently apply [weak self] in.
You can of course rewrite this to require [weak self] in the inner closure, too. Doesn’ hurt (I just tried), but is also not necessary to get a weak reference in the inner closure.
Chris’s rules summarize this nicely:
- Only use a strong
selffor non-@escapingclosures (ideally, omit it & trust the compiler)- Use
weak self if you’re not sure- Upgrade
selfto a strongly-retainedselfat the top of your closure.
TextKit 2 Example App from the Apple Docs
The Apple Developer Docs have an example app, “Using TextKit 2 to Interact with Text”. That’s related to WWDC 2021’s introduction to TextKit 2.
Availability:
- iOS 15.0+
- iPadOS 15.0+
- macOS 12.0+
- Xcode 13.0+
So it’s usable on macOS Monterey and the latest iOS.
I ran the demo app and resized the window a bit. It’s buttery smooth, and that’s good.
But the butter, in part, comes from each block seeming to move up/down independently. Spring-loaded, so to speak. You can see this in the following demo when I move the mouse rather quickly:
When multi-line paragraphs occupy many more or far fewer lines than they did before, the headings between these paragraphs move at different speeds, then ease-out as they catch up.
I believe the intention here is to show what a block or text fragment is, and that you can animate them individually. TextKit 2 is block-based; so you have a paragraph block, and headings blocks. These are laid-out, eh, en bloc. When you turn on the turtle mode in the toolbar, the ease-out animation is even more pronounced.
Most of this is visible in TextDocumentView.swift:
func textViewportLayoutController(
_ controller: NSTextViewportLayoutController,
configureRenderingSurfaceFor textLayoutFragment: NSTextLayoutFragment) {
// (1) Tuple-returning API
let (layer, layerIsNew) = findOrCreateLayer(textLayoutFragment)
if !layerIsNew {
let oldPosition = layer.position
let oldBounds = layer.bounds
layer.updateGeometry()
if oldBounds != layer.bounds {
layer.setNeedsDisplay()
}
if oldPosition != layer.position {
// (2) Layer animation
animate(layer, from: oldPosition, to: layer.position)
}
}
if layer.showLayerFrames != showLayerFrames {
layer.showLayerFrames = showLayerFrames
layer.setNeedsDisplay()
}
contentLayer.addSublayer(layer)
}
-
The
findOrCreateLayermethod is interesting because the Apple developer(s) who wrote this sample return a tuple. That’s refreshingly new. But that’s not TextKit 2 API, just part of theTextDocumentView. -
The
layeris of typeTextFragmentLayer, which is aCALayersubclass. So you can animate eachNSTextLayoutFragmentindependently.
Here’s another video showing slow-motion with layer borders turned on. You can see how the layers move until they touch:
It’s interesting, to say the least, that layout fragments or blocks will be laid-out with independent CALayers or NSView. I’m not certain if that’s actually true, to be honest, but this seems to be the implication when you mess with NSTextViewportLayoutController, because the docs for the delegate method implementation from above is:
The delegate presents the text layout fragment in the UI, for example, in a sublayer or a subview. Layout information such as
viewportBoundsontextViewportLayoutControllerisn’t up to date at the point of this call.
I need to actually experiment with TextKit 2 to get any understanding of the components at all. Wanted to share my surprise about CALayer usage here, though.
When you remove the delegate method, by the way, there won’t be any laid-out content. So either the TextKit 2 demo is demonstrating very low level interaction or making text editors of the future will just be different.
Will keep you posted when I find out more.
How to Upgrade Sendy from Version 5 to 6 from the Shell via SSH
I’m using Sendy for our newsletters. Recently, verison 6 was released. Here’s how I updated like a pro on my VPS via SSH.
The beginner-friendly but ultimately drag-and-drop/FTP-based online instructions assume that you will be moving files over from “new location” to “existing location”. That’s quite cumbersome when all you have is SSH.
Here’s how I translated the steps to the shell to merge the v6 files into your v5 installation.
Note: I assume Sendy is installed in the
~/sendy.mydomain.com/directoy, on themydomain.comserver. Replace this with your actual settings, of course!
Prepare the files on the host
You can prepare the host whichever way you like, as long as the result is an unzipped v6 directory and your old v5 installation on the host machine.
Here’re instructions assuming ~/sendy.mydomain.com/ on the host is the existing v5 installation, and ~/sendy/ will be the unzipped v6.
- Download the latest Sendy version locally using your license key: https://sendy.co/get-updated – I didn’t find a way to bypass browser-based download. So no
wgetdownload to the server this time. -
Copy the
.zipto your host viascp, e.g. using:me@local:~/Downloads $ scp sendy-6.0.1.1.zip user@mydomain.com:~/ -
ssh onto the host machine:
me@local:~/Downloads $ ssh user@mydomain.com -
Backup your current install into an archive (replace paths as needed):
admin@server:~/ $ tar -czf sendy-bak.tar.bz2 sendy.mydomain.com/ -
Finally unzip Sendy into a new directory:
admin@server:~/ $ unzip sendy-6.0.1.1.zip ... # This is what we'll be working with: admin@server:~/ $ ls sendy sendy-6.0.1.1.zip sendy.mydomain.comThe
sendy-6.x.x.zipfile will unzip intosendy/by default: so make sure you don’t run into directory name conflicts or change the destination.
Perform the upgrade from the terminal
Now perform all the update instruction steps, but from the shell. One by one:
# Copy existing .htaccess file
admin@server:~/ $ cp sendy.mydomain.com/.htaccess sendy/
# Copy existing config file
admin@server:~/ $ cp sendy.mydomain.com/includes/config.php sendy/includes/config.php
# Merge v6 into v5 (AFTER THE BACKUP, SEE STEP 4 ABOVE)
admin@server:~/ $ rsync -ar --progress sendy/ sendy.mydomain.com/
Note we skipped two steps:
- Transfering language files, because we’ll be merging the new installation’s
locale/directory with the existing one; - Removal of the
uploads/subdirectory, becausersync’s merging does not remove files in directories – with these options at least.
rsync does a lot of things the DWIM (Do What I Mean) way by default. Drag-and-drop copying of new files over existing ones can – depending on your OS! – replace contentful directories with empty ones, or merge them, or skip empty source directories.
We did have to preserve two important files, though: .htaccess and includes/config.php. That’s why we need to either remove them from the source (the v6 folder) or overwrite them. That’s a two-way merge, so to speak, where we merge setting changes into the freshly unzipped folder, then copy everything over into the old location
If want to do a two-way merge on existing the language files and uploads, too, excluding English language files as the docs instruct, use:
# Transfer existing uploads
admin@server:~/ $ rsync -ar --progress ~/sendy.mydomain.com/uploads/ sendy/uploads/
# Keep custom language files, except en_US, which Sendy ships with
admin@server:~/ $ rsync -ar --exclude "en_US/" --progress ~/sendy.mydomain.com/locale/ sendy/locale/
But that’s not necessary. And depending on your amount of uploads creates waste by duplicting the files.
As always, if you want to verify what would happen (to check if skipping English really worked), add the --dry-run flag, e.g. before --progress.
Troubleshooting
If you botched up the copy process, remove the broken install and unarchive your backup (YOU DID MAKE THE BACKUP, RIGHT?):
# Remove broken install (not rm'ing, just moving away)
admin@server:~/ $ mv sendy.mydomain.com{,_broken}
# Unarchive into ~/sendy.mydomain.com
admin@server:~/ $ ls
sendy # The extracted v6
sendy-bak.tar.bz2 # The backup archive
sendy.mydomain.com_broken
admin@server:~/ $ tar -xzf sendy-bak.tar.bz2
admin@server:~/ $ ls
sendy-v6.0
sendy # The extracted v6
sendy-bak.tar.bz2 # The backup archive
sendy.mydomain.com # NEW: The restored backup
sendy.mydomain.com_broken
Cleanup
If everything works, download the backup to your machine …
# On your computer:
me@local:~/Downloads $ scp user@mydomain.com:~/sendy-bak.tar.bz2 \
2022-04-26_sendy-backup.tar.bz2
…, remove it from the host, and remove the unzipped v6 files and the download:
admin@server:~/ $ rm -rf sendy-6.0.1.1.zip sendy/ sendy-bak.tar-bz2
Finished.
Center Window on the Current Monitor in Emacs, Simplified
Louis Brauer over on Xah Lee’s Discord helped simplify my Emacs window centering code (cf. original single monitor solution. It’s great, and only a handful of lines!
It’s compatible with Emacs 26.0.90 (reference commit or newer:
(defun my/frame-recenter (&optional frame)
"Center FRAME on the screen.
FRAME can be a frame name, a terminal name, or a frame.
If FRAME is omitted or nil, use currently selected frame."
(interactive)
(unless (eq 'maximised (frame-parameter nil 'fullscreen))
(modify-frame-parameters
frame '((user-position . t) (top . 0.5) (left . 0.5)))))
That is, uh, very simple in comparison to my old approach where I computed the center of the frame inside the monitor’s manually!
All credit goes to Luis for this.
Transient Menus Galore
I just wanted to take a short minute to shout out to two transient.el based packages I discovered this week.
org-menu

One is org-menu, and bound to C-c m, it shows a transient menu of, well, transient keybindings to navigate around, move outline items, and do a lot of other things.
This was the first time I’ve seen a transient menu stay on-screen until it’s manually dismissed. Didn’t know it could do that, and it makes a lot of sense to group key bindings that you usually repeat or use in succession to make access to them easier without having to repeat the prefixes.
For example, C-n m n n n n to open org-menu and then interactively go down 4 headings, one by one, can considered to be be simpler than hitting C-c C-n four times.
For text editing tasks this can be useful, too, like doing sentence-level manipulations. Invoke the sentence manipulation transient menu, then move a sentence to the right a couple of times; things like that, maybe.
I’m underselling what org-menu can do; there’s a special menu for table editing that is much appreciated because I just can’t get the default key bindings for “add row” and “delete row” right.
Tray

The other transient package I found is tray by Jonas Bernoulli, creator of transient, himself.
It describes itself as “Transient menus for a wide variety of things.”
So I looked at the source and found tray-mml that defines a quick menu for common email tasks. Like attaching a file into an email buffer from the email buffer, for which I always forget the key bindings (C-c RET f, by the way).
Usually, I browse a directory and then figure I want to send the file from there to someone. I use an Embark action for that, which is like a transient contextual menu: go to a file in a dired listing, hit C-, a to open Embark and attach the file-at-point. It then opens a new email draft, or uses an open email buffer, if any exists.
Attaching files is pretty boring, but making signing email easier or quoting the selected text? Yes please! Requires fewer key bindings for these commands.
Fixing Our Recent Xcode 13.3.x Dependency Cycle Errors
So for quite some time, I had the Swift Build System Integration flag enabled to speed up my builds. It was good. I set it, it worked, I forgot about it.
With Xcode 13.3 and 13.3.1, now, we got a lot of dependency cycle warnings out of nowhere. Clean build works, but any build afterwards would occasionally fail.
With 13.3.1 that I downloaded today, this happened for every build after a clean build. Each and every one after the first build after a clean! Oof.
Here’s just one example, so you know what I’m talking about; it has nothing to to with Sparkle. Sparkle is fine. It’s Xcode that is drunk.
Cycle in dependencies between targets ‘Sparkle’ and ‘SparkleDownloader’; building could produce unreliable results. This usually can be resolved by moving the target’s Headers build phase before Compile Sources.
Cycle path: Sparkle → SparkleDownloader → Sparkle
Cycle details:
→ Target ‘Sparkle’ has target dependency on Target ‘SparkleDownloader’
→ Target ‘SparkleDownloader’ has link command with output ‘…’
→ Target ‘Sparkle’ has link command with output ‘…’
So I looked this up real quick, and no – I cannot order the “Headers” build phase above the “Compile Sources” build phase, because in a pure Swift project, the “Headers” build phase doesn’t even exist on the offending targets. (I tried to use this on all other modules, though, but most were already in that order; and those that weren’t had no effect.)
The Apple Dev Forums produces an interesting hint about the aforementioned EnableSwiftBuildSystemIntegration flag – which, contrary to the actual tip, I turned off.
defaults write com.apple.dt.XCBuild EnableSwiftBuildSystemIntegration 0
And this worked!
I don’t know why one person needs to enable this while another has to disable it, but well, there you go – if in doubt, turn it to not-whatever-your-value-is.
$ defaults read com.apple.dt.XCBuild EnableSwiftBuildSystemIntegration
0
How to Assemble Menu Bar App Screenshots for Mac App Store
To make menu bar app screenshots for the Mac App Store, you need to fill 2800 by 1880 pixels with something that highlights a widget in the corner of the screen.
Geoff Hackworth did this:

- Take retina screenshot of the app in action (he used Sidecar on his iPad to get his non-retina Mac to produce a large picture).
- Prepare a 2880x1800px canvas with a MacBook bezel template, put the screenshot inside.
- Center the picture so the icon and the menu bar popup window takes center stage.
- Put marketing text around that.
Since today, you can look at the result of this on the App Store: check out Geoff’s app “SF Menu Bar”.
Aergrind by Knock Coffee Grinder, and Their Amazing Customs Duty Service
I bought a hand grinder that would replace our 40+ years old, dull antique. Of course I looked for advice on James Hoffmann’s YT channel and figured the Aergrind by Knock would be a great fit. As the man coffee scientist himself says, the grinder is so good it would’ve blown away any cheaper competition from his other video, so he included it in the premium segment comparison. That was one reason I considered this.

- 12 month back-to-base UK warranty
- Dimensions - H 145mm x W 49mm
- Weight - 390g
- Capacity - 22-27g (depending on bean varietal & roast profile)
It’s a bit small for a 30+ grams of coffe for a 3/4th serving in a French press (at James Hoffmann’s recommended 70g/liter), but it’s portable, thus great for camping trips, and would fit inside an Aeropress (that we don’t yet have). Knock produce bigger grinders, too, I just didn’t pick these for the neat “fits into Aeropress” reason, and we usually don’t have guests that gulp liters upon liters of coffee.
It’s a good grinder. It produces the best coffee grounds I ever had. The metal is smooth to the touch, everything including the lid has a very appealing and premium-feeling weight, and it has a somewhat industrial look I dig. For bonus points, it even comes with some replacement O-rings, which is a nice touch.
I may post something resembling a review video in the future after more testing and dialing-in of grind settings.
Meanwhile, this shout-out so you and all your friends buy their products to support this company is motivated by something that happens behind the scenes. On top of making great products in the UK, they also go out of their way to make all purchases simpler for customers at their cost. That deserves some attention, I believe.
Lemme explain how I discovered that.
Net loss due to flat fees instead of tricky customs handling
After purchase, I asked them to send me a VAT invoice because the order page didn’t offer a download for this and I usually file that stuff away. A routine email. That turned out to be rather tricky, though, after the purchase has already been made, for reasons I’ll try to explain real quick. I dropped the issue because the reason it was tricky is this: t
So the company is based in the UK. There’s this Brexit thing going on where the UK is leaving the European Union. I’m in Germany, a EU country; so all of the comfortable within-EU-VAT handling is not available to them anymore and they have to do this on their own.
The standard procedure for non-digital goods that are shipped from the UK would now be to send the product, and then the customer has to pay customs duty. I had to do this at a customs office, once, picking up the package there. Was quite the hassle. Via UPS, I found you sometimes can pay in advance when customs are cleared, and sometimes you have to pay upon receipt. So it varies from an annoying procedure to a payment formality.
Instead of any of this, the folks at Knock do this up-front. They charge a flat fee for shipment to EU countries and pay VAT and customs up-front with DHL. That usually nets them a loss. But it’s a nicer customer experience. If I had emailed them first and insisted on handling customs duty, I’d have paid more and the process would’ve been worse.
From my point of view, Knock deserves all the support and shout-outs, just like fellow indie app developers when they go out of their way to make the lives of customers simpler. It’s the indie spirit all right.
If you want any in-depth look at the Aergrind, I’ll whip something up. Just leave requests in the comments. I’m taking pictures of coffee grounds at varying grinder settings anyway at the moment to record differences. Meanwhile, I like the ground coffee I tasted so far, so do check out their products!
Add Any Directory to project.el's List of Projects
I am using the built-in project.el in Emacs to find files in various git-backed projects. But I also have a shared folder of blog post drafts with Sascha at zettelkasten.de that does not have any version control backing. I couldn’t get that folder to show up in project.el’s list because of that.
Until now.
My own attempts to make something like this work came to a halt when I encountered the generic project-root function. That stuff goes over my head. Finding a project marker file worked okay, but that isn’t enough, apparently
;; This does not suffice!
(defun ct/dir-contains-project-marker (dir)
"Checks if `.project' file is present in directory at DIR path."
(let ((project-marker-path (concat dir "/" ".project")))
(when (file-exists-p project-marker-path)
dir)))
(customize-set-variable 'project-find-functions
(list #'project-try-vc
#'ct/dir-contains-project-marker))
With only that, the function correctly reports a directory path as being a project, but something fails in project-root:
cl-no-applicable-method: No applicable method: project-root, "~/path/to/drafts/"
Didn’t know how to fix that.
From Karthik Chikmagalur’s (better known as @karthink) GPL’ed project-x package, I took the functions and variables that were used to mark a directory path at project-indexable by adding a .project file to the root. The rest, like window restoration, I left out. It’s quite simple, actually, and I am printing the source below:
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; For a full copy of the GNU General Public License
;; see <http://www.gnu.org/licenses/>.
(defgroup project-local nil
"Local, non-VC-backed project.el root directories."
:group 'project)
(defcustom project-local-identifier ".project"
"Filename(s) that identifies a directory as a project.
You can specify a single filename or a list of names."
:type '(choice (string :tag "Single file")
(repeat (string :tag "Filename")))
:group 'project-local)
(cl-defmethod project-root ((project (head local)))
"Return root directory of current PROJECT."
(cdr project))
(defun project-local-try-local (dir)
"Determine if DIR is a non-VC project.
DIR must include a file with the name determined by the
variable `project-local-identifier' to be considered a project."
(if-let ((root (if (listp project-local-identifier)
(seq-some (lambda (n)
(locate-dominating-file dir n))
project-local-identifier)
(locate-dominating-file dir project-local-identifier))))
(cons 'local root)))
(customize-set-variable 'project-find-functions
(list #'project-try-vc
#'project-local-try-local))
So my own approach was missing a definition like this:
(cl-defmethod project-root ((project (head local)))
"Return root directory of current PROJECT."
(cdr project))
Can’t explain how the cl-defgeneric and cl-defmethod stuff works at all; I can only highlight that this is all that was necessary. How this differs from the couple of built-in project-root method definitions, and how Elisp figures out which one to use? I have no clue.
Now I merely have to add a .project file to a directory and project.el will remember this directory in my list of project paths, so I can quickly jump to a draft from anywhere quickly.