‹ . local author .

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

« Newer posts
Page 2
Older posts »

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

In Emacs, my text editor of choice, tab-bar-mode lets you replicate something like the experience of using tabs in a web browser. For me, this means opening a lot of tabs with notes, lists, drafts, etc., such that my tab bar becomes a visual buffet of whatever documents are pertinent to the task at hand, be it writing, teaching, coding, whatever.

This works great for me, on a project by project basis. But what if I want to switch to another task or project? What is to become of the perfectly curated set of tabs I have open?

The problem of tab proliferation is a distinctly modern one. Raise your hand if the top of your web browser looks like a graveyard of articles you’ve been meaning to read since the last time your computer crashed. Many browsers have solved this issue by allowing users to save and restore groups of tabs under meaningful names, like “Research Project” or “Articles I Still Won’t Read.” But who does that? (Ok, I do…)

As far as I know,™ Emacs does not offer any built-in way to save and restore a beautifully curated row of tabs. There are many options for saving and restoring activities, workspaces, and frame/buffer configurations. But none that do exactly what I want, namely, allow me to save and later re-open a set of tabs, one file per tab, in a specific order.

So, I wrote tab-sets.el.

I find tab-sets most useful for teaching and writing.

For example: I sometimes teach the same class several times in a week. I therefore often found myself repeating the process of finding and re-opening all the files that I wanted to reference for that class, mostly lecture notes and slides (exported with org-reveal). Because these files can be spread out across my file system, finding and re-opening them is rarely as simple as navigating to a dedicated directory.

Now, with tab-sets.el, I can use a single command, tab-sets-save, to save the current frame’s tabs under a meaningful name, say, “Week 1 Class”, and safely close the frame. When it’s time to teach the same class again, I can call tab-sets-open, select “Week 1 Class,” and find a brand new frame displaying all the tabs exactly as they were when I saved them.

The same process works for quickly returning to a writing project.

A few niceties include:

  • minibuffer annotations, offering a preview of each tab set
  • optional integration with bookmarks.el, meaning tab sets are accessible as standard bookmarks, through list-bookmarks
  • optional integration with embark, meaning you can open, rename, or delete a tab set right from the minibuffer

This package isn’t published anywhere, so install directly, however you want or know how. For example:

(use-package tab-sets
  :ensure nil
  :vc (:url "https://github.com/localauthor/tab-sets")
  :custom
  (tab-sets-data-file "~/.emacs.d/var/tab-sets.eld")
  :config
  (tab-sets-setup-embark))

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

“But nature is a stranger yet”

   in the above film –
           Ežerų dugne (In the Depths of Lakes) (2014), dir. Akvilė Anglickaitė
— —
divers explore their love for four Lithuanian lakes
and the alien terrain at their depths:
— —

— —

             on view at MO until March 30, 2025,
                 as part of an exhibition on solastalgia

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!


« Newer posts
Page 2
Older posts »