Emacs Grid Select
An alternative User Interface to select lists. Inspired by Xmonad’s Grid-Select.
Introduction
As with many things my creativity was sparked by an annoyance, and this package was part of the solution. I had 3 packages to write in order to do everything that was going to solve my annoyances.
Grid select was just a user interface for the first project. But if I did that, I must go ahead and use it elsewhere. That hasn’t been a problem. I’ve got a growing list of things to use it for.
grid-select was the second and most difficult project, which I picked up halfway through the first. It is more than double the size of persp-topics. All three seem to be wrapping up at the same time. Or at least slowing down to place I can rest.
All 3 siblings 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.
What is it?
Grid select is essentially a selection list that is arranged in a center focused grid starting from the center. I’ve come to think of it as spiraling outward from the center. Each time around places 4 values and and follows the same formula of (x y) -> (y -x). Each consecutive concentric ring takes more times around than the last. The result is often a diamond shape, with a preference for the X and Y axis. Grid select turns into a square at 9, 25, 49, 81, …
Each item down the list gets further from the center with each arm of the spiral. The top of the list is always within a keystroke or two from the center.
The grid can also be filtered just like a select list by typing a few letters. This often brings the choice to the center of the grid.
I’ve been using grid select in Xmonad for years, I use it for desktops, scratchpads, windows, search-engines, etc. It is very nice to use, and I have always wished for the same functionality within Emacs.
Here is a very simple example of how it works. This just a select list. Filtering is set to do one character on the current list.
How did it go?
I was familiar with tabulated-list but that didn’t seem like the right tool. It was, but that’s not where I started. I looked at Grid Table which is cool but far more than I needed. I chose to use vtable. I’d never used vtable. It seemed to handle most of the basic needs I had and it seemed lighter weight than a mode. I’m not sure that is true. Eieio is not pretty to me.
It went fast. As soon as I got a grid populated correctly I called vtable and there it was. It was a bit ugly, but it sort of worked. Arrow navigation was broken, tables in emacs are mostly row, not cell centric. I used my initial keymap to iron out movement and reliably find the center of the grid. I then needed to add filtering.
Read-key recursion
In order to filter I needed to catch any keycodes that were not arrows, escape, enter or C-g. I needed to catch backspace to go back. The way to do that is read-key. I recreated my keymap with a cond and started catching keys. The filtering was easy, but re-rendering, recentering was problematic, and broke all the navigation again.
By the end of the day it was all working. Ugly, but working. I could even put it in a posframe. I still had a list of things I wanted that vtable doesn’t do. I tried setting some font faces with no luck.
Tabulated list.
The next day I replaced vtable with tabulated-list Initially there were similar problems, but over-all it went smoothly and after just a morning it was working again with a propertized set of entries. Font faces made all the difference in how it looked. Re-rendering was easier. Tabulated list does at least remember it’s position if you ask it. Re-rendering is fundamental in how tabulated-list works.
Locating values was much simpler. I just used the row ID and the column name.
Propertizing the cells.
I really wanted a highlighted current cell, and at least some sort of visual indication of the grid. I also wanted centered values. Empty cells needed to be invisible. My format-entries and propertize functions took care of these things easily with few problems. It’s business as usual with tabulated-list.
Grid select has three font faces, grid-face, current-face and empty-face. One face for all the values, one for the currently selected value and one for all the empty cells on the edges. To get the current cell face working I had to add re-rendering with each movement. That was finicky, rerendering kept moving my position. Propertizing of the values just worked.
Posframe
Posframe is great, but it doesn’t work in a TUI. Grid-select can go either way. Posframe can be turned off, and better, it can be left on, and grid select will do the right thing if it detects it is not in a GUI. Both the buffer and posframe modes look great. Much nicer than I thought I could obtain.
Filtering
Filtering was working really well, but I refactored it and in doing so I started using a stack to hold the previous item list which allowed me to create a sort of orderless filter by resetting the filter string every time instead of accumulating an increasingly restrictive filter string. But that didn’t prevent the accumulation method from working, so I made it a setting. Consequently, there are two ways of filtering and you can choose between them with grid-select-accumulate-filter-string, true or false. Backpace simply shortens the filter if there is one, and pops the last item list.
The invasion
The first thing I did was use select-grid in my other project, Persp-topics. More about that in another post. The next thing I did was start the third package. It was difficult to name, it had a lot of different facets. It eventually became GS-fun, because it was a bunch of Grid-select functions and it was a lot of fun. I started using grid-select to switch perspectives, buffers, and popper buffers, perspective buffers minus the popper buffers and anything else I could think of. Just in writing this post I’ve used grid select in one aspect or another many times.
Grid select has invaded my Emacs use. I use it all the time and I keep thinking of more ways to use it. It frequently gives me a smile.
The persp-switch grid-select with Topics!, Quick!, and Kill! actions are 2 ways to create perspectives.
The Perspective Buffers Minus the Popper Buffers.
Both switch-to-buffer and persp-switch select grids have additional actions to let you create or kill things. The actions are just a setting that you can change as you wish.
Using grid-select
Using grid select is easy.
grid-select takes multiple keywords. At a minimum it needs :items and/or :action-list. The rest is related to behavior or appearance.
:itemsA simple list of strings or symbols:actionA function to call with the choice.:action-listAn alist of strings or symbols paired with functions.:history-keyA keyword to use to categorize history entries. Turns on Recent order.:current-itemThe current active item that will be in the list, ie, buffer-name.:column-widthThe column width of the table.:grid-faceThe face for grid values.:current-faceThe face for the current selection.:empty-faceThe face to use on empty-cells.:posframeUse posframe if > 0, turn it off if < 1.:auto-selectauto-select if > 0, turn it off if < 1.:continueRecursively continue after an action.
Any can be set or nil. The defaults will be used accordingly. :Posframe greater than 0 will result in a posframe grid while 0 or less will give a buffer grid. The same rules apply to :auto-select
When :auto-select is on grid-select will automatically select the last item if filtering has reduced it to one non-nil item.
Here are two simple examples, where it all started.
(grid-select :items '(one two three four five))
;; with column-width, fonts and force posframe.
(grid-select :items '(one two three four five)
:column-width 20
:grid-face 'my-grid-face
:current-face my-current-face
:empty-face my-empty-face
:posframe 1)
Continue
Continue is set by :continue t, It causes grid-select to recurse preseving the current filtered item history and the current shape of the grid. It will continue as long as it finds an action for a choice or it is cancelled. If there is not an default action or an action-list entry for a choice, then the choice is returned.
This is great for action based grids like gs-fun-windows in the gs-fun project. But it doesn’t work very well for the Kill! functions. This is because continue maintains the original list, so items that have been killed will remain as choices.
It is easy enough to add recursion around a grid-select. This is how the Kill functions work.
(defun foo ()
"A recursive select-grid-function"
(when
(select-grid ...)
(foo))
List Order
List order is important, Select grid will start at the center and work outward so the most frequently desired items should be at the top of your list. The first item will always be at the center under your cursor.
Recent history order makes all the difference. Everything you want is under your fingertips.
Recent History Order
If a :history-key is given, grid select will add entries with that key to it’s history file. The lists presented will be reordered in accordance with the order of the history file. Additionally, if the :current-item is provided grid-select will ensure that it is not at the top of the list. The current-item might be the return values from buffer-name or persp-curr-name for instance.
(setq grid-select-history-size 1000)
(setq grid-select-history-file-name "grid-select-history.el")
Auto Select
Auto-select is somewhat related to filtering. If it is turned on, if the select list has been filtered down to one item and is not nil, that item is automatically selected. This saves typing [Return] to select. it. But could end with bad selections.
Some simple examples
Here are some of the basic uses with just an item list and a few settings. Grid-select has reasonable defaults so for basic use there isn’t a need to do much else.
The last example adds most recent history and order. The current-item is not practical, it should be a function like (buffer-name) or (persp-current-name) This example will just prevent ‘one from ever being in the center.
The :history-key turns on history. The :current-item wil not be allowed to be the first item on the list.
(setq my-list
'(one two three four five six seven eight nine ten eleven twelve thirteen))
(grid-select :items my-list)
;; change the column width.
(grid-select :items my-list
:column-width 10)
;; Turn off posframe.
(grid-select :items my-list
:posframe -1)
;;; Create a buffer selection with recent history order.
;;; 'One' will never be in the center with this.
(grid-select :items my-list
:column-width 10
:history-key :my-numbers
:current-item 'one
Actions, GS-Fun
The GS-Fun project is about doing the little things I wanted, like persp and buffer switching. GS-Fun is also about exploration and growing new functions that might belong somewhere else as with what is now persp-topics-persp-switch. One of the fun things I did was this function. It didn’t look like this in the beginnng. As silly as it seemed, it inspired grid-select :action and :actions-list. Which led to having some really nice features elsewhere.
One morning I woke up with a new use I had to try. We all know C-x 0, 1, 2 and 3. But what if I had a select grid for that? Five minutes, a grid select and a cond. The next day I created :action and :action-list.
I don’t need to show you the old code, you can imagine a list, a select-grid and a cond. This is how it is now. I think it is settling. I added auto-select and gave all the Actions names with unique capitolized letters. It’s super nice with auto-select on.
This function now also uses :continue to keep taking input until cancelled. It also now has movement commands which fall into a WASD configuration when filtered by !.
(defun gs-fun-windows ()
"A `grid-select' to split, balance and delete windows.
Actions are named for single letter selection. Auto-select
and continue are turned on."
(interactive)
(let ((action-list '((|Balance| . balance-windows)
(|split-Right| . split-window-right)
(|split-beLow| . split-window-below)
(|delete-Others| . delete-other-windows)
(|Delete| . delete-window)
(Up! . windmove-up)
(Right! . windmove-right)
(Down! . windmove-down)
(Left! . windmove-left))))
(grid-select :action-list action-list
:auto-select 1
:continue 1
:grid-face 'grid-select-grid-faceL
:current-face 'grid-select-current-faceL
:empty-face'grid-select-empty-faceL)))
Combining it all
Items, action, and action-list worked together from the beginning. The items are ordered by history if there is a :history-key, and the keys from the action list are appended. When a choice is made it might do an action, and in any case it returns the choice.
- It calls the corresponding function in the action-list, or
- It calls the action function with the choice
This allows for a grid select that is both a selection chooser, and an action chooser at the same time. A great example of this in use is in my project persp-topics, the function persp-topics-persp-switch uses all of these features. It allows choosing a perspective to switch to or choosing an action to do. Persp-topics has it’s own font settings to use for select grid so it uses those with every call to select-grid. It also has separate settings for =auto-select = so you can tweak the behaviors how you like.
The persp-kill function is recursive so you can kill one thing after another and cancel when you are done.
(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.")
(defun persp-topics-persp-switch ()
"The same as `persp-switch' but with defineable actions.
By default the actions are Topics, Quick and Kill. This makes it easy to
select a topic and go, create a new quick topic or kill a perspective
you are done with."
(interactive)
(message "Switch Perspectives.")
(grid-select :items (persp-names)
:action 'persp-switch
:action-list persp-topics-persp-switch-action-list
:column-width persp-topics-ps-column-width
:history-key :persp
:current-item (persp-current-name)
:auto-select persp-topics-persp-switch-auto-select
:grid-face persp-topics-ps-grid-face
:current-face persp-topics-ps-current-face
:empty-face persp-topics-ps-empty-face))
(defun persp-topics-persp-kill ()
"A recursive function to select and kill perspectives."
(interactive)
(message "Kill perspectives.")
(when (grid-select :items (persp-names)
:action 'persp-kill
:column-width persp-topics-ps-column-width
:grid-face persp-topics-ps-grid-warning-face
:current-face persp-topics-ps-current-warning-face
:empty-face persp-topics-ps-empty-warning-face)
(persp-topics-persp-kill)))
Functions to use.
There is only one function. It does a lot of different things. I hope it sparks your imagination. I think it’s fun. I’m ready to think about new ways to use it.
grid-select
Summary
Grid-select has been a great project. It has brought me much joy in its creation and it continues to make me smile as I use it. It has sparked my creativity as I think of new things I could do with it. I’d love to hear any ideas to improve it or use it. I think there is a lot of potential that I just can’t even imagine.
If someone out there likes playing with font faces, a set of accessible and pretty font faces that people could use with grid-select would be fantastic.
You can find my grid-select related projects grid-select, persp-topics, and gs-fun at my codeberg. The readme’s will tell you everything you will want to know.
Enjoy Zenie