Double-clicking a source or config file in Finder opens it in Neovim inside a new iTerm2 window. Selecting multiple files opens them as tabs in a single window (nvim -p).
- iTerm2
- Neovim (
nvimon$PATH) - duti:
brew install duti
sudo rm -rf /Applications/NeovimLauncher.app # remove old build if rebuilding
bash build-neovimlauncher.sh
bash set-handlers.shOn first file open, macOS will prompt "NeovimLauncher wants access to control iTerm2" — click Allow. This is a one-time TCC permission grant.
| File | Purpose |
|---|---|
build-neovimlauncher.sh |
Builds /Applications/NeovimLauncher.app and registers it |
set-handlers.sh |
Runs duti to assign every extension to the app |
macOS Launch Services — the subsystem that handles double-clicks in Finder — only routes files to registered .app bundles. It sends an Apple Event (odoc) to the target application; it does not pass file paths as CLI arguments. A plain shell script can never receive this event. The bundle is the required wrapper.
Rather than writing a raw shell script as the bundle executable (which cannot receive Apple Events), osacompile compiles an AppleScript into a native applet and wraps it in a complete .app bundle in one step:
osacompile -o NeovimLauncher.app - <<'APPLESCRIPT'
...
APPLESCRIPTThe compiled applet binary (Contents/MacOS/droplet) is a real Mach-O binary that the OS can launch, and it natively handles the on open theFiles Apple Event handler.
on open theFiles
repeat with f in theFiles
set allPaths to allPaths & " " & quoted form of POSIX path of f
end repeat
...
end openon open is the AppleScript handler invoked by Launch Services when a file is opened with the app. Each element of theFiles is a file alias; POSIX path of f converts it to a Unix path string. quoted form of shell-escapes it, handling spaces and special characters safely.
All files are aggregated into a single nvim -p file1 file2 ... command so multiple files selected in Finder open as tabs in one window rather than spawning multiple iTerm2 windows.
osacompile generates a wildcard CFBundleDocumentTypes that accepts *.*. We replace it via PlistBuddy with a specific list of UTIs:
| UTI | Covers |
|---|---|
public.plain-text |
.txt, .md, and anything conforming to plain text |
public.source-code |
All programming language files |
public.data |
Extensionless files: Dockerfile, .env, .gitignore |
public.json |
.json, .jsonl |
public.xml |
.xml, .plist |
net.daringfireball.markdown |
.md, .markdown |
UTIs are hierarchical — registering public.source-code also catches every UTI that conforms to it (Python, Swift, JS, etc.) without listing each one explicitly. The explicit entries above are for types that sit outside that hierarchy (JSON, XML, Markdown).
PlistBuddy patches the plist in-place rather than overwriting it, preserving the keys osacompile emits that the applet runtime depends on (e.g. LSRequiresCarbon, OSAAppletShowStartupScreen).
macOS refuses to run unsigned or quarantined app bundles with a "damaged or incomplete" dialog. Two steps fix this:
codesign --force --deep --sign - # ad-hoc signature — no Apple Developer account needed
xattr -dr com.apple.quarantine # clear the quarantine flag set on newly created bundlesAn ad-hoc signature (-) satisfies the bundle integrity check without requiring a paid Apple Developer certificate.
lsregister -r -domain local -domain system -domain userForces macOS to rescan /Applications and update the UTI→app mapping database. Without this, the new bundle may not appear in "Open With" menus or be resolved by duti until the next login.
lsregister makes the app available as a handler. duti makes it the default:
duti -s com.custom.neovimlauncher .json allThe all role covers viewer, editor, and shell. set-handlers.sh runs this for every relevant extension. Verify any association with:
duti -x jsonWhen the app first tells iTerm2 to do something via AppleScript, macOS intercepts the Apple Event and shows a consent prompt. If you click Don't Allow, the permission is permanently denied and subsequent attempts fail silently. Reset it with:
tccutil reset AppleEvents com.custom.neovimlauncherbuild-neovimlauncher.sh offers to run this automatically at the end of the build in case you need to re-grant after a denied prompt.
If a specific file keeps opening in another app despite duti being set, it has a per-file override stored as an extended attribute:
xattr -d com.apple.LaunchServices.OpenWith /path/to/fileThis is set when you use "Open With" on a single file without clicking "Change All". Removing it causes the file to fall back to the global Launch Services rule.