index rss mastodon twitter github linkedin email
Álvaro Ramírez
sponsor

Álvaro Ramírez

12 October 2022 Emacs: macOS sharing (DWIM style)

UPDATE: See an improved implementation here.

A few days ago, I wrote dwim-shell-commands-macos-reveal-in-finder. While I've written a bunch of other dwim-shell-commands, what set this case apart was the use of Swift to glue an Emacs workflow.

(defun dwim-shell-commands-macos-reveal-in-finder ()
  "Reveal selected files in macOS Finder."
  (interactive)
  (dwim-shell-command-on-marked-files
   "Reveal in Finder"
   "import AppKit
    NSWorkspace.shared.activateFileViewerSelecting([\"<<*>>\"].map{URL(fileURLWithPath:$0)})"
   :join-separator ", "
   :silent-success t
   :shell-pipe "swift -"))

There is hardly any Swift involved, yet it scratched a real itch I couldn't otherwise reach (reveal multiple dired files in macOS's Finder).

divinedominion's reddit comment got me thinking of other use-cases, so I figured why not push this Swift-elisp beeswax a little further… Let's add macOS's sharing ability via dwim-shell-command, so I could invoke it from the comfort of my beloved dired or any 'ol Emacs buffer visiting a file.

(defun dwim-shell-commands-macos-share ()
  "Share selected files from macOS."
  (interactive)
  (let* ((position (window-absolute-pixel-position))
         (x (car position))
         (y (- (x-display-pixel-height)
               (cdr position))))
    (dwim-shell-command-on-marked-files
     "Share"
     (format
      "import AppKit

       _ = NSApplication.shared

       NSApp.setActivationPolicy(.regular)

       let window = InvisibleWindow(
         contentRect: NSRect(x: %d, y: %s, width: 0, height: 0),
         styleMask: [],
         backing: .buffered,
         defer: false)

       NSApp.activate(ignoringOtherApps: true)

       DispatchQueue.main.async {
         let picker = NSSharingServicePicker(items: [\"<<*>>\"].map{URL(fileURLWithPath:$0)})
         picker.delegate = window
         picker.show(
           relativeTo: .zero, of: window.contentView!, preferredEdge: .minY)
       }

       NSApp.run()

       class InvisibleWindow: NSWindow, NSSharingServicePickerDelegate, NSSharingServiceDelegate {
         func sharingServicePicker(
           _ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?
         ) {
           if service == nil {
             print(\"Cancelled\")

             // Delay so \"More...\" menu can launch System Preferences
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
               NSApplication.shared.terminate(nil)
             }
           }
         }

         func sharingServicePicker(
           _ sharingServicePicker: NSSharingServicePicker,
           delegateFor sharingService: NSSharingService
         ) -> NSSharingServiceDelegate? {
           return self
         }

         func sharingService(
           _ sharingService: NSSharingService,
           didShareItems items: [Any]
         ) {
           NSApplication.shared.terminate(nil)
         }

         func sharingService(
           _ sharingService: NSSharingService, didFailToShareItems items: [Any], error: Error
         ) {
           let error = error as NSError
           if error.domain == NSCocoaErrorDomain && error.code == NSUserCancelledError {
             NSApplication.shared.terminate(nil)
           }
           exit(1)
         }
       }" x y)
     :silent-success t
     :shell-pipe "swift -"
     :join-separator ", "
     :no-progress t
     :utils "swift")))

Sure there is some trickery involved here (like creating an invisible macOS window to anchor the menu), but hey the results are surprisingly usable. Take a look…

share-done_x1.4.webp

I've pushed dwim-shell-commands-macos-share to dwim-shell-commands.el in case you'd like to give it a try. It's very much an experiment of sorts, so please treat it as such. For now, I'm looking forward to AirDropping more files and seeing if the flow sticks. Oh, and I just realised I can use this to send files to iOS Simulators. Win.

dwim-shell-command is available on melpa. What other uses can you find for it?