Count the times you hit your Mac app launcher's hotkey in a day. It is probably dozens, and nearly all of them open an app. The rest are a few personal commands: open the Downloads folder, open GitHub notifications, put the displays to sleep before walking away.
Those few are the part that takes setup. In Alfred a custom command is a Workflow, which needs the paid Powerpack. In Raycast it is a Quicklink or a Script Command, each configured in its own place. Either way, the list of what your launcher does lives in a preferences window, not somewhere you can see all of it at once.
This flow is the whole launcher on one canvas. A hotkey lists every installed app plus three rows of your own, a chooser floats up with the real app icons, and whatever you pick either opens or runs.
The flow
Open it with Option-Space
The trigger is Keyboard Shortcut set to option+space, with Consume Key on so the app in front never sees the press. It needs Accessibility permission the first time. If Alfred or Raycast still holds Option-Space, quit it or record a different combination, because two apps listening for the same keys is a coin toss.
List every installed app, plus your own rows
Installed Apps emits records, one {name, path, bundleID, kind} row per app. Folders is left at its four defaults (/Applications, /System/Applications, /System/Library/CoreServices/Applications, ~/Applications, subfolders included). The catalogue is walked once and kept in memory, and a run only walks again when one of those folders has changed, so pressing the hotkey does not scan your disk.
Extra Rows at the Top is where your own commands go. Each line becomes a row above the apps with kind set to custom and a name, nothing else. A row is a label, not an action: what it does is decided further down the flow.
Search apps with their real icons
Pick from List reads records (its default Rows), in the List layout, with Primary Column name, Thumbnail Column path and Search Columns name. A column of .app paths draws each app's real icon, fetched from macOS only for rows on screen. The Prompt is Open…, since there is no title bar and the search field is the question.
Picking leaves by Selected with {{selected}}, the whole row. Escape leaves by Dismissed, which nothing is wired to, so walking away does nothing.
Open the app or run your own command
The Condition named One of mine? checks selected.kind equals custom.
- No goes to Open File with
{{selected.path}}. Opening an.appbundle launches it. - Yes goes to a Template that writes
{{selected.name}}into a key calledcommand, then to Run Script.
The Template is there because Run Script never expands {{...}} in its body. Payload values reach the script as environment variables instead ($PAYLOAD_command), so a clipboard or webhook value can never become a shell command. The script is one case statement:
case "$PAYLOAD_command" in
"Downloads") open ~/Downloads ;;
"GitHub notifications") open "https://github.com/notifications" ;;
"Sleep Displays") pmset displaysleepnow ;;
*) echo "No command called $PAYLOAD_command" >&2; exit 1 ;;
esac
Adding a command is one line in Extra Rows at the Top and one line here. A folder, a URL and a shell command all go through open or the command itself.
Paste this into the Flow Builder to get the same flow:
On Option-Space, list my installed apps with three extra rows at the top: Downloads, GitHub notifications and Sleep Displays. Let me pick one from a searchable list with app icons. If I pick an app, open it. If I pick one of my own rows, run a bash script that opens ~/Downloads, opens https://github.com/notifications, or runs pmset displaysleepnow, depending on the row's name.
How it behaves
Search is a plain substring match. It ignores case and looks only at the columns you name, so code finds Visual Studio Code and vsc does not. Rows stay in list order: your rows first, then apps A to Z.
The app you were in stays in front. The chooser floats without activating Watchflows, so nothing jumps behind it while you type.
A misspelled row fails out loud. If a row's name and its case line drift apart, the *) arm exits 1 and the run shows up as failed in Execution History with the message, instead of silently doing nothing.
Pressing the shortcut twice replaces the chooser. The first one fails, because nobody answered it. Escape is how you close it. A chooser left open for fifteen minutes gives up and fails the same way.
Your rows get a generic glyph. A custom row has no path, so there is no icon to fetch.
What Raycast still does better
This flow does not learn which apps you open most, and there is no fuzzy matching. There is no extension store, no calculator in the search field, no file search and no window management. If those are why you run Raycast, keep Raycast and give this flow a different shortcut for your own commands. If you mostly open apps and a few personal commands, this is all of it, and you can read every rule it follows.
After importing: switch the flow on, grant Accessibility when macOS asks, and edit the three rows and case lines to your own.
Variations
- Launch a flow from the launcher. Add a row with a flow's name and send the Yes branch to Run Watchflow instead of the script.
- Run a Shortcut. A
caseline ofshortcuts run "Log Water"runs anything from the Shortcuts app. - Tell two copies of an app apart. Set Secondary Column to
bundleIDand Xcode and Xcode-beta stop looking identical. - Ask for a query. Add a row like Search GitHub, branch on it with a second Condition, and put a User Input and an Open URL behind it.