‹ . local author .

: notes on place, literature, history, and method :

Emacs Popup Frames Anywere — MacOS Edition

This post shows how to set up Emacs popup frames in macOS, allowing quick access to Emacs functions from anywhere on your system.

——————————————————————————————

As much as I would like to live in Emacs full time, I’m not quite there. But a recent post by Protesilaos Stavrou (aka Prot) is helping me bridge the gap.

Prot has written a short but powerful bit of code that allows you to access any piece of Emacs goodness — like org-capture — from outside Emacs with just one keystroke. The only requirement is that you are running in server-mode or using the Emacs daemon.

Whether you’re in a web browser, a PDF reader, or just staring at an empty desktop, your favorite Emacs command can be right at your fingertips. See Prot’s video for a thorough demonstration.

Now, I can quickly open a PDF using citar or copy a password from my pass-store. See below for some other useful popups — org-agenda view, mu4e inbox, and a quick translation interface.

Setting Up Emacs Popup Frames in MacOS

Since Prot wrote the code for use in Linux, it didn’t immediately work in MacOS. It turns out, the only necessary change was to add a window-system frame parameter to the popup frame. (See code below).

But while I was at it, I made a few additions: 1) a title frame parameter, which allows me to configure the popup frame size and placement through my tiling window manager, yabai; and 2) an optional argument that runs delete-frame after the main command runs. This makes it possible to call commands that are minibuffer-centric — in my case, that’s citar-open-file and password-store-copy.

Here’s the main code:

(defun popup-frame-delete (&rest _)
  "Kill selected frame if it has parameter `popup-frame'."
  (when (frame-parameter nil 'popup-frame))
  (delete-frame))

(defmacro popup-frame-define (command title &optional delete-frame)
  "Define interactive function to call COMMAND in frame with TITLE."
  `(defun ,(intern (format "popup-frame-%s" command)) ()
     (interactive)
     (let* ((display-buffer-alist '(("")
                                    (display-buffer-full-frame))))
       (frame (make-frame
               '((title . ,title)
                 (window-system . ns)
                 (popup-frame . t)))))
     (select-frame frame)
     (switch-to-buffer " popup-frame-hidden-buffer")
     (condition-case nil
         (progn
           (call-interactively ',command)
           (delete-other-windows))
       (error (delete-frame frame)))
     (when ,delete-frame
       (sit-for 0.2)
       (delete-frame frame)))))

(use-package server
  :defer 1
  :config
  (unless (server-running-p)
    (server-start)))

And here are the macro calls that define commands:

(popup-frame-define org-capture "capture-popup")
(add-hook 'org-capture-after-finalize-hook #'popup-frame-delete)

(popup-frame-define password-store-copy "minimal-popup" 'delete-frame)

(popup-frame-define citar-open-files "minimal-popup" 'delete-frame)

Triggering Popups with Keyboard Shortcuts

To quickly bring up these popup frames with keyboard shortcuts, I use skhd, a hotkey daemon for macOS. The following configuration goes in the skhd config file:

cmd + ctrl - c : emacsclient -e '(popup-frame-org-capture)' || yabai -m window --focus recent
cmd + ctrl - o : emacsclient -e '(popup-frame-citar-open-files)' || yabai -m window --focus recent
cmd + ctrl - p : emacsclient -e '(popup-frame-password-store-copy)' || yabai -m window --focus recent

Managing Windows with yabai

Finally, I have configured my tiling window manager, yabai, to give the popup frames the desired size and placement.

First, the following goes in the yabai config file:

yabai -m signal --add event=window_created app="Emacs" action="~/.yabai-emacs-window-handler.sh"

This will run a bash script (located at ~/.yabai-emacs-window-handler.sh) every time a new Emacs frame is created. The script tells yabai how to move and resize the new frame, depending on the frame’s title. Here is that script:

#!/bin/bash

data=$(yabai -m query --windows --window $YABAI_WINDOW_ID)

title=$(echo $data | jq .title)
display=$(echo $data | jq .display)

if [[ $title =~ "capture-popup" && $display == 1 ]]; then
yabai -m window $YABAI_WINDOW_ID --toggle float --move abs:430:230
yabai -m window $YABAI_WINDOW_ID --resize abs:655:300
yabai -m window $YABAI_WINDOW_ID --focus
elif
[[ $title =~ "minimal-popup" && $display == 1 ]]; then
yabai -m window $YABAI_WINDOW_ID --toggle float --move abs:430:230
yabai -m window $YABAI_WINDOW_ID --resize abs:655:200
yabai -m window $YABAI_WINDOW_ID --focus
else
false
fi

Additional Custom Commands

In some cases, an Emacs command will work nicely in a popup frame right out of the box. A good case in point is org-capture. In other cases, I have needed to write my own custom commands. Here a few:

Google Translate Interface

(defun my/translate ()
  (interactive)
  (let ((choice
         (completing-read "Select: " '("EN->LT" "LT->EN"))))
    (cond ((string= choice "EN->LT")
           (google-translate-query-translate-reverse))
          ((string= choice "LT->EN")
           (google-translate-query-translate)))))

(popup-frame-define my/translate "minimal-popup")

Unread Emails in mu4e

(defun my/mu4e-unread ()
  (interactive)
  (mu4e 'background)
  (mu4e-search-bookmark
   (mu4e-get-bookmark-query ?u)))

(popup-frame-define my/org-agenda "large-popup")

For this, I add another “elif” to the bash script, to ensure I can see the whole inbox in the popup:

elif
[[ $title =~ "large-popup" && $display == 1 ]]; then
yabai -m window $YABAI_WINDOW_ID --toggle float --move abs:350:130
yabai -m window $YABAI_WINDOW_ID --resize abs:730:600
yabai -m window $YABAI_WINDOW_ID --focus

Custom Org-Agenda View

(defun my/org-agenda (&optional p)
  (interactive "P")
  (org-agenda p "d")
  (org-agenda-redo-all))

(popup-frame-define my/org-agenda "large-popup")

Just don’t forget to add a system-wide keyboard shortcut for each new command!