Emacs Perspective Topics
- Introduction
An easy way to create new perspectives for your projects. Inspired by Xmonad’s TopicSpace.
Introduction
Persp-topics is a solution which enables a smooth workflow creating and using perspectives in Emacs. It’s based on my years of making and using a pile of little functions all not quite alike to create my various perspectives for different uses. This is so much better!
Scratching an Itch
Sometimes creativity is pushed into happening through annoyance. I found myself aonnoyed, and I decided this was the last time. That’s when it happens. I promise myself to make it the last time, never again will I set foot here.
I didn’t intend to write this code, or fix this problem that I saw. But I needed something, I needed to add a new perspective to my Emacs configuration. I looked at the now teenage template code I’d been putting up with. I didn’t want to do that hacky thing yet again. It had invaded my hydras, and it had grown to an ugly chunk of code to boot.
Inspiration
I’ve been an Xmonad user 15 years. I hardly touch my setup. It’s nice. I have often wished Emacs and Exwm had some of it’s features. One such feature is TopicSpace. Topics are a list of desktop definitions. I have one for my Emacs configuration development, I have one for Krita, I have one for the web. I used to have a lot more, but I’ve been using Perspectives in Emacs, so that takes some of the same responsibility, I have various development projects Mu4e/mail, Elfeed, Erc, and various other perspectives that I use. Xmonad, and TopicSpace don’t care about layouts. Xmonad layouts can be cycled through like buffers, layouts don’t care about their contents. Emacs is not quite the inverse, we need to create some layout as we open things.
I love perspectives, but they don’t have a mechanism like Xmonad’s Topics. I have many desktops in my Xmonad configuration. But none of them exist until I go to them by name. At that point the Topic sets my working directory and starts up all my tools. The Topic does nothing else until the desktop is empty and I visit it again. I use another package called Grid-select to choose my Topics/Desktops/windows, and other things. I sometimes yearn for an interface like Xmonad’s Grid-select. It is unlike any menu in Emacs.
You can find my grid-select repo here, and the gs-fun repo that uses it.
The promise
I made a promise with myself. If I’m going to do this perspective Topics thing, I must also do the Grid select thing. I don’t know if there is a table/grid I can use, or if I’ll have to create one. It’s just code, I’ll find out when I get there.
So two projects sprung out of an annoyance, One of them potentially difficult. Obviously I fulfilled my promise.
How it turned out
I wrote 3 packages in two weeks. I wrote 2 posts and the doc all at the same time. It went fast! The first commits were 4 and 6 days after I started. Then they kept getting improvements. The projects and the writing inspired each other, and so it went. Each one improving with the others.
The projects are available on my codeberg. grid-select is an Xmonad inspired selection and action menu. persp-topics is inspired by Xmonad’s TopicSpaces and gs-fun is inspired by these other two. Grid Select fun functions. Which in turn greatly inspired both grid-select and persp-topics. It also looks like I’ve got at least two more proper packages that should live out on their own. Getting these into Melpa is the first priority.
Here is a short video showing persp-topics-persp-switch, persp-topics-select-and-go, persp-topics-persp-kill, and gs-fun-goto-buffer.
This video creates, switches, and deletes perspectives from the perspective switch or directly from persp-topics-select-and-go. I use a lot of single key, auto-select actions. The Xmonad topic choice is particularly fast. I tried to move extra slowly.
Auto-select and orderless filtering are on.
The grids are different colors, persp-switch is violet, topics are steel blue, buffers are green and kill is burgundy.
Persp-Topics
Creating Persp-Topics was easy. I looked at all the hacky template functions I was using to create my perspectives with. They were all similar and different. I wrote a definition of what they would look like as data. Something that could handle all the choices being made. I had it working in a couple of hours. It continued evolving another 2 weeks.
I put a topic together, and wrote some code to use it. It was a bit over-engineered, it had some extra variables and decision making that were unecessary complexities.
I had problems setting the default-directory and using it. Then I cheated a little. I used the scratch buffer that every perspective is given at the start. That gave way to better ways which isn’t hacky.
| Here is a test topic just to make sure things are working reasonably. It spawns 3 things, a dired on the ~/play directory, an async-shell-command of an **ls -lathp | head** in the same directory, and also an eshell. All while splitting and moving around in the windows. |
(setq persp-topics
'(((:name . "test")
(:dir . "~/play")
(:spawn . (((:fn . dired)
(:args . "."))
((:shell . ls)
(:args . "-lathp | head")
(:split-fn . split-window-horizontally)
(:move-fn . windmove-right))
((:fn . eshell)
(:split-fn . split-window-vertically)))))))
It’s a super simple design, It could have been just a list, but the keywords give things meaning and context. It’s about the human interface, that things are readable. Because it wasn’t necessary.
I was happy until I started writing about it in the readme and here. I’ve actually been done a few times, each time I wake up in the morning with another thing to change.
Recursive Spawn Templates.
I thought I was done, this article was almost finished this morning. In my writing, I realized I needed spawn-templates. They took few minutes to create and they simplified a few things at the same time. They also inspired Quick templates.
Spawn templates are just a spawn entry list with a name. They are often similar or the same, so this makes it easy to share them between topics, or choose one for a one-off perspective for something. It enabled the Quick-topic functionality, I love it when nice design features just fall out of the code.
Well, it happened again, today, It’s a daily joke. What will I need to rewrite in this post today? I added recursive spawn-templates. A template may now be specified as part of a spawn entry. Which means a Topic or spawn template can have as many spawn-templates as you want.
Here are the test topics. One using a template. One just like the original.
(setq persp-topics
'(((:name . "test-with-template")
(:dir . "~/play")
(:spawn-template . test))
((:name . "test")
(:dir . "~/play")
(:spawn . (((:fn . dired)
(:args . "."))
((:shell . ls)
(:args . "-lathp | head")
(:split-fn . split-window-horizontally)
(:move-fn . windmove-right))
((:fn . eshell)
(:split-fn . split-window-vertically)))))))
Here is the new test template. Its slightly rearranged but that’s ok, it’s a test. The spawn template is part of shell command spawn entry, It could have just as easily been it’s own entry as well.
((:name . test)
( ((:shell . ls)
(:args . "-lathp | head")
(:spawn-template . dired-eshell-v))))
((:name . dired-eshell-v)
(:spawn (((:fn . dired)
(:args . ".")
(:split-fn . split-window-right)
(:move-fn . windmove-right))
((:fn . eshell)
(:split-fn . split-window-below)
(:move-fn . windmove-down))
((:move-fn . windmove-up)))
Readme .org .md
The two readme templates, org and md, have seen a lot of change and have been a small thorn. Needing to choose the template according to what kind of README you were encountering was not ideal.
This is now resolved. There are also set of interactive functions that handle various conversions of Markdown to org, into buffers, from buffers, from regions, from one file to another. They were all created on the path to making a function that would find whatever kind of README was present, and load it. The function persp-topics-find-file-readme will load an Org file or a Markdown file as an Org buffer or as a Markdown buffer. It acts just like find-file otherwise. So if it finds a .md file, be happy to get an Org buffer.
The readme templates are all good now. Two choices just in case. One that just loads the README no matter the type, and one that always loads an org buffer even if it is a markdown file.
;; Don't care, find an org or md and load it.
((:name . readme.org.md)
(:spawn . (((:fn . persp-topics-find-readme)))))
;; Find a README.md or README.org.
;; if markdown is found, convert it to org
;; on load. Does not create an org file.
((:name . readme)
(:quick . t)
(:spawn . (((:fn . persp-topics-find-readme)
(:args . t)))))
More complex templates.
Templates can do most anything you’d want. That they can use other templates makes them very composable, They tend to boil down to just a few different things that we want to see when we start up a perspective. Here are some of the templates that I’ve come up with. I used the elisp-dev template for a few days but have realized I like a split window setup and the dired was unecessary. So now I’m using ielm-right which has a split frame setup to load ielm, move back left and do magit-status. It’s like a project welcome screen. I like the brief look at magit-status that I get when the perspective pops up. I quit magit and I’m in the code and the README buffer is loaded too.
All my elisp project topics look like this today. It’ll probably them change soon. I need to put **ielm** and **eshell** into Popper with display-buffer-alist entries for both.
((:name . "persp-topics")
(:group . Dev)
(:dir . "~/play/Elisp/persp-topics")
(:spawn . (((:spawn-template . readme))
((:fn . find-file)
(:args . "persp-topics.el"))
((:spawn-template . ielm-right)))))
Here are a few templates. I’m quite happy with the one to start up Emms. It was the most complex of all my create perspective functions.
((:name . elisp-dev)
(:quick . t)
(:spawn . (((:spawn-template . readme.org))
((:spawn-template . dired-ielm)))))
;; splits the window right, opens ielm there
;; and then moves back. Opens magit-status.
((:name . ielm-right)
(:quick . t)
(:spawn . (((:fn . ielm)
(:split-fn . split-window-right)
(:move-fn . windmove-right))
((:move-fn . windmove-left))
((:fn . magit-status)))))
;; dired and eshell side-by-side.
((:name . dired-eshell)
(:quick . t)
(:spawn . (((:fn . dired)
(:args . "."))
((:fn . eshell)
(:split-fn . split-window-right)
(:move-fn . windmove-right)))))
;; This leaves the scratch buffer on the left
;; and splits dired and ielm on the right.
((:name . dired-ielm-v)
(:spawn . (((:fn . dired)
(:args . ".")
(:split-fn . split-window-right)
(:move-fn . windmove-right))
((:fn . ielm)
(:split-fn . split-window-below)
(:move-fn . windmove-down))
((:move-fn . windmove-up)))) )
;; split the window below with dired above
;; and eshell below.
((:name . dired-eshell-v)
(:spawn . (((:fn . dired)
(:args . ".")
(:split-fn . split-window-right)
(:move-fn . windmove-right))
((:fn . eshell)
(:split-fn . split-window-below)
(:move-fn . windmove-down))
((:move-fn . windmove-up)))))
((:name . Play-Emms)
(:spawn . (((:fn . emms-ext-start-music-daemon))
((:fn . emms-browser)
(:split-fn . split-window-right))
((:fn . emms-playlist-mode-go)
(:split-fn . split-window-right))
((:fn . emms-lock-queue)))))
Some Topic definitions
Here are some topic definitions, At this point they aren’t very interesting. They just have templates in them for the most part. Here are some variations.
I added a :group key, I’m not sure how I feel about them. I’ve turned them off in my topic selects. I avoided using Apps as a group because there is the All! action, I ended up with Logiciels which is fine for me. My computer is a melange of english and french anyway.
The most interesting thing about making topics is choosing the group names and making names that can be easily selected with one key. This became a much stronger goal when I turned auto-select on for all my grid-selects.
The :group keyword is not necessary unless you want to turn on persp-topics-select-by-group. If set, any topic selection will start with a topics groups selection. The group names are up to you, any topics without a group will disappear except for in the All! selection. This gives an easy way to keep topics that you may not be using much while trimming down your selection grids.
Auto-select wiith good unique letters in names make selections go very fast.
((:name . "Zenies-emacs")
(:group . Dev)
(:dir . "~/play/Emacsn/dev")
(:spawn . (((:spawn-template . readme))
((:fn . dired)
(:args . "."))
((:spawn-template . ielm-right)))))
((:name . "Email")
(:group . Logiciels)
(:dir . "~/")
(:spawn . (((:fn . mu4e)))))
((:name . "Mastodon")
(:group . Logiciels)
(:dir . "~/")
(:spawn . (((:fn . mastodon)))))
((:name . "Play music")
(:group . Logiciels)
(:dir . "/home/Music")
(:spawn-template . Play-Emms))
((:name . "persp-topics")
(:group . Dev)
(:dir . "~/play/Elisp/persp-topics")
(:spawn . (((:spawn-template . readme))
((:fn . find-file)
(:args . "persp-topics.el"))
((:spawn-template . ielm-right)))))
((:name . "grid-select")
(:group . Dev)
(:dir . "~/play/Elisp/grid-select")
(:spawn . (((:spawn-template . readme))
((:fn . find-file)
(:args . "grid-select.el"))
((:spawn-template . ielm-right)))))
((:name . "my melpa")
(:group . O-dev)
(:dir . "~/play/Emacs/My-Melpa")
(:spawn . (((:spawn-template . readme))
((:fn . dired)
(:args . ".")
(:split-fn . split-window-right)
(:move-fn . windmove-right))
((:move-fn . windmove-left)))))
((:name . "Flute Stuff")
(:group . Etudes)
(:dir . "~/play/Documents/Flute")
(:spawn-template . dired-eshell))
((:name . "Xmonad")
(:group . Sys)
(:dir . "~/play/System/xmonad-setup")
(:spawn . (((:fn . find-file)
(:args . ".xmonad/xmonad.hs")
(:spawn-template . elisp-dired-v)))))
(:plit-fn . split-window-right)))))))
No Topic? No Problem!
Quick Topics
In the topics selection menu there is always a choice Quick!, this choice causes persp-topics to use the persp-topics-quick-topic function. This function will prompt for a name, a directory, and a spawn template name to use. This function optionally takes all three values in that order, so it’s a good candidate for an apply-partially.
Voila! you have a reasonably useful perspective on that random thing you just cloned from Codeberg. Just now I created a dired-eshell perspective on my images directory. It was a fast and easy way to go look at the images I might want to use in this post.
Spawn where you are, without a new perspective.
There is also a function persp-topics-select-template-and-spawn That allows any spawn template to selected and run whenever you like.
It will use the default directory of the current buffer and spawn everything as if it were creating a perspective.
Quick Topic integrations
It’s super nice to create a perspective on a directory or Repo. It allows easy switching to that place and keeps whatever you might do there contained. Using Quick topics you can select the appropriate template and go.
Dired
The function persp-topics-dired-quick-topic will find a topic or create one with quick topics based on the current file or directory. If it can find a Topic definition for that directory it will go to it, or create it. Otherwise it will use persp-topics-quick-topics.
(general-define-key
:keymaps 'dired-mode-map
"C-c C-p" 'persp-topics-dired-quick-topic)
(setq persp-topics-dired-ask-before-switching nil)
Rrepo
If you have Rrepo, there is a function here that can be mapped to your Rrepo-mode-map. persp-topics-rrepo-persp-this will search topics by their directory to find a match for it’s current repo. If it finds one, it will create a perspective with that topic. If not found it uses Quick-topic along with it’s ID and install-path so that you only need to choose a spawn-template to have a new perspective on your repo.
Heres a general-define-key to add it to your rrepo keymap.
(general-define-key
:keymaps 'rrepo-installed-mode-map
"p" 'persp-topics-rrepo-persp-this)
Grid Select – The User Interface
This is part of my promise. Grid-select exists now, So I’m using it. Grid-select and persp-topics together is just a wonderful thing. Grid select has evolved a lot because of this project and vice versa.
Grid Select fonts and settings.
Persp-topics has settings for column-width and the three faces needed by grid-select. You can set them and forget them. Your persp-topics generated select-grids will always look the way you want them to.
Persp-topics has three sets of faces. One set is for grid selects having to do with topic or spawn-template selection. The other is for persp-switch. And the third is a warning face used by persp-kill.
The ts set is for Topics select. The ps set is for Persp-switch select.
;; Persp topics grid select settings.
(setq persp-topics-ts-column-width nil)
(setq persp-topics-ts-grid-face 'persp-topics-grid-face)
(setq persp-topics-ts-current-face 'persp-topics-current-face)
(setq persp-topics-ts-empty-face 'persp-topics-empty-face)
;; Persp Switch grid select settings.
(setq persp-topics-gs-column-width 18)
(setq persp-topics-ps-grid-face 'persp-topics-grid-face2)
(setq persp-topics-ps-current-face 'persp-topics-current-face2)
(setq persp-topics-ps-empty-face 'persp-topics-empty-face2)
;; warning faces for persp-kill The Larger size
(setq persp-topics-ps-grid-warning-face 'grid-select-grid-warning-faceL)
(setq persp-topics-ps-current-warning-face 'grid-select-current-warning-faceL)
(setq persp-topics-ps-empty-warning-face 'grid-select-empty-warning-faceL)
The persp-switch and persp-topics selects also have actions added. These also have a setting. These actions show on the edges of the grid along with all the choices.
(defvar persp-topics-persp-switch-action-list
'((Topics! . persp-topics-select-and-go)
(Quick! . persp-topics-quick-topic)
(Kill! . persp-topics-persp-kill))
"The action list to append to the persp-topics-persp-switch grid-select.
The Key values here will be shown along with the active perspective names
and will call the functions given if chosen.")
(defvar persp-topics-action-list '((Quick! . persp-topics-quick-topic))
"The action list to append to the persp-topics-persp-topics grid-select.
The Key values here will be shown along with the active perspective names
and will call the functions given if chosen.")
Using Persp-Topics
Once you have a few topics configured, using Persp-Topics is easy. The function persp-topics-select-and-go is almost the only function you need to know. I usually create quick topics from Dired or Rrepo.
There are two additional functions, persp-topics-persp-switch and persp-topics-persp-kill. Persp-topics-persp-switch is just like persp-switch But in addition to perspspectives to switch to it has actions for Quick!, Topics! and Kill! which allow you to create new perspective with persp-topics-quick-topic or persp-topics-select-and-go or kill them to clean up a little. You can always add or remove the actions as well.
There are only 4 functions. But you could just use Persp-topics-Persp-switch and have access to all the rest through the actions it has.
persp-topics-select-and-gopersp-topics-quick-topicpersp-topics-persp-switchpersp-topics-persp-kill
The odd one.
persp-topics-select-template-and-spawn
Persp-switch
Adding some actions to the persp-switch select has turned out to be a great idea. If you arrive there and don’t see what you want, you can go to Topics! and create it. If you just want a one-off perspective you can choose Quick! and if you decide you have too many open perspectives you can go Kill! a few.
Here’s an image of persp-switch. These are my active perspectives plus Topics!, Quick! and Kill!.
Summary
Thats about it for persp-topics, it is really simple and there isn’t much to see. It’s all about using it and aside from the actual code there are only pictures of grid-select. It does work really well. Between persp-topics, grid-select and gs-fun I navigate my perspectives and buffers with grid-select and it’s all super fast and flows really well. Switching between perspectives and creating new ones was never so easy.
If you have suggestions, ideas or cool things you’ve done with persp-topics please share, I’m always interested improving things and finding new ways to do something.
Enjoy. Zenie.