emacs-plus: Emacs Client.app
After fixing PATH injection on macOS 15, I'm dusting off a year-old feature: proper Finder integration for emacsclient. Using AppleScript instead of shell scripts, Emacs Client.app now handles file opening from Finder, drag-and-drop, and auto-starts the daemon. With one cosmetic icon issue that needs your help to solve.
Despite my rather sporadic involvement lately, emacs-plus is alive and well. We recently fixed a nasty PATH injection bug on macOS 15 that was causing native compilation to fail for many users. And now I'm finally dusting off a feature that's been sitting on my shelf for almost a year: proper Finder integration for emacsclient.
If you run Emacs as a daemon, you've probably noticed that emacs-plus only shipped Emacs.app - a regular macOS application with no awareness of the server/client setup. There was simply no way to use emacsclient from Finder.
After several iterations and a bit of AppleScript wrestling, I'm happy to announce that emacs-plus now ships with Emacs Client.app - a proper macOS application that makes emacsclient a first-class Finder citizen. And it works exactly how you'd expect.
#1The Problem
Running Emacs as a daemon is great for performance - instant frame creation, persistent state, no startup time. But macOS integration has always been rough:
- Right-click → "Open With" doesn't work with command-line tools
- Drag-and-drop onto a shell script icon? Not happening
- Setting
emacsclientas your default text editor? Forget about it
The core issue is that macOS communicates file-opening requests through AppleEvents, not command-line arguments. When you double-click a file or use "Open With", macOS sends an application:openFiles: event to the target application. Shell scripts can't receive these events - they only see arguments passed via the command line.
#1The Journey: Shell Script → AppleScript
My first attempt used a simple shell script wrapper bundled as an app. It looked promising in testing, but the moment you tried "Open With" from Finder… nothing. The script would launch, but it had no idea which file to open.
After digging through Apple's documentation and finding some prior art (shoutout to Nicholas Kirchner's implementation), the solution became clear: AppleScript.
#2Why AppleScript?
I evaluated four approaches:
| Approach | Handles Finder Events | Complexity | Build Requirements |
|---|---|---|---|
| Shell Script | ❌ No | Very Low | None |
| Swift Binary | ✅ Yes | Very High | Xcode, Swift compiler |
| Automator | ✅ Yes | High | AppleScript + Automator |
| AppleScript | ✅ Yes | Low | Built-in osacompile |
AppleScript won because it:
- Natively handles the
on openevent for files from Finder - Compiles during installation using the built-in
osacompilecommand - Requires zero external dependencies
- Keeps the formula simple and maintainable
#1How It Works
Emacs Client.app is built using two AppleScript handlers that cover all the ways you might launch it.
#2Handler 1: Opening Files (on open)
Triggered when you:
- Right-click a file → "Open With → Emacs Client"
- Drag files onto the Emacs Client.app icon
- Set Emacs Client as default and double-click a file
on open theDropped repeat with oneDrop in theDropped set dropPath to quoted form of POSIX path of oneDrop set pathEnv to "PATH='/opt/homebrew/bin:...' " do shell script pathEnv & "/opt/homebrew/bin/emacsclient -c -a '' -n " & dropPath end repeat tell application "Emacs" to activate end open
Note: This example shows paths for Apple Silicon Macs (/opt/homebrew). Intel Macs use /usr/local instead. The actual implementation uses the correct prefix for your system.
Key implementation details:
POSIX path of oneDrop- Converts macOS file aliases to Unix pathsquoted form of- Handles spaces and special characters in filenamesemacsclient -c- Creates a new frame-a ''- Auto-starts the daemon if not running (more on this below!)-n- Returns immediately without blockingtell application "Emacs" to activate- Brings the frame to the foreground
#2Handler 2: Launching Empty Frame (on run)
Triggered when you:
- Launch Emacs Client from Spotlight
- Click it in the Dock
- Double-click it in Finder (without files)
on run set pathEnv to "PATH='/opt/homebrew/bin:...' " do shell script pathEnv & "/opt/homebrew/bin/emacsclient -c -a '' -n" tell application "Emacs" to activate end run
Same idea, just without file arguments - creates a fresh frame.
#2PATH Injection
Just like the PATH injection I added to Emacs.app, Emacs Client.app respects the EMACS_PLUS_NO_PATH_INJECTION environment variable (or the new build.yml setting once that ships).
During the build, your PATH is captured and injected into the AppleScript source:
path = ENV['PATH'].split("#{HOMEBREW_PREFIX}/Library/Homebrew/shims/shared:").last escaped_path = path.gsub("'", "\\\\'")
This ensures Homebrew-installed binaries are available when launching from Finder or Spotlight, where the environment is minimal.
#2The Magic of -a '' (Alternate Editor)
Here's the best part: you don't need to manually manage the daemon anymore.
The -a '' flag tells emacsclient:
- Try to connect to an existing daemon
- If no daemon is running, start one automatically
- Then open the file/frame
This is way more reliable than checking daemon status manually. It handles edge cases like:
- Daemon crashed or was killed
- Socket file exists but daemon isn't running
- Multiple Emacs versions installed
- First launch after a reboot
No more ps aux | grep daemon, no more "is the server running?" confusion. It just works.
#1Installation and Usage
Starting with emacs-plus@30 and emacs-plus@31, the app is built automatically during installation. After installing, create an alias in /Applications:
osascript -e 'tell application "Finder" to make alias file to posix file "$(brew --prefix)/Emacs Client.app" at posix file "/Applications" with properties {name:"Emacs Client.app"}'
Then you can:
- Set as default application: Right-click any text file → Get Info → Open with → Emacs Client → "Change All…"
- Use "Open With": Right-click any file → Open With → Emacs Client
- Drag and drop: Drag files onto the Emacs Client.app icon in your Dock
- Launch empty frame: Open from Spotlight (
Cmd+Space, type "Emacs Client")
#1Known Issue: Generic App Icon
There's one cosmetic issue I haven't cracked yet: the icon shows a generic app icon instead of the Emacs icon.
The build process tries to replace the default AppleScript droplet icon by:
- Copying
Emacs.icnstoapplet.icnsin the app's Resources folder - Updating
CFBundleIconFileinInfo.plistto referenceapplet - Removing the default
droplet.icnsanddroplet.rsrcfiles
This works on some macOS versions (including macOS 15 Sequoia), but fails on macOS 26 Tahoe for reasons I haven't fully investigated yet. I've tried the usual suspects:
-
Touching the app bundle to update modification time
-
Running the full Launch Services reset:
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ -kill -r -domain local -domain system -domain user -
Various
PlistBuddyincantations
None of these helped, so the root cause remains unclear.
If you have experience with macOS icon caching or Launch Services, I'd love your help! The relevant code is in Library/EmacsBase.rb in the create_emacs_client_app method. Check out the implementation on GitHub.
Update [2025-11-25]: This issue has been resolved in commit caae572. The fix involved using sips and DeRez/Rez to properly embed the icon resource into the compiled AppleScript application.
#1What's Next: Community Patches and Icons
This big feature is done (minus the icon glitch), and I'm already planning the next major improvement: a community patches and icons system (see issue #851).
The idea is simple: instead of committing to maintain dozens of patches and icons indefinitely, I'll provide infrastructure for community contributions through a build.yml configuration file:
patches: - smooth-cursor # From community registry - my-custom-patch: # External URL url: https://example.com/my.patch sha256: abc123... icon: modern-flat settings: no_path_injection: true
This will enable:
- Three-tier system: Built-in patches (maintained by me), community patches (maintained by contributors), and wild-west URLs (maintained by users)
- No long-term commitment: Community features can be added/removed without my involvement
- Easy discovery: A registry of available patches and icons
- Helper scripts: Tools to create and test community patches automatically
The goal is to make emacs-plus more sustainable whilst still providing the customisation users want. I'll write a detailed post once this lands.
#1Help Wanted: Testing and Feedback
This feature just shipped in emacs-plus@30 and emacs-plus@31. If you're using Emacs daemon, please give it a try!
I'm especially interested in:
- Icon rendering: Does the Emacs Client.app show the correct icon on your system? (Check
/Applications/Emacs Client.appafter creating the alias) - File type associations: Can you set Emacs Client as the default app for your preferred file types?
- Daemon auto-start: Does the
-a ''magic work reliably for you? - Edge cases: Unusual workflows, multiple Emacs versions, custom socket locations, etc.
Report issues or share feedback on GitHub or in the PR discussion.
And if you know the secret to making macOS respect custom icons in AppleScript-compiled apps, please, for the love of Emacs, share your wisdom.
Thanks to Nicholas Kirchner for the prior art on using AppleScript for emacsclient integration, and to @elken for early testing and feedback.