One of my favourite—and most used—modules is XMonad.Actions.TopicSpace. However, it seems relatively unknown among the general XMonad community. I fear this is due to the fact that the module is quite old and formerly had a rather high barrier to entry. Despite having been given shiny new documentation, lots of people probably did not bother revisiting it and thus still don’t really understand why they might be interested in using topics instead of workspaces. Time to change that!
First of all, this post is not to be seen as a tutorial on X.A.TopicSpace, but much rather as a showcase of how its functionality could be used day to day. If you like what you see, perhaps check out the docs and give it a spin yourself! I have already written an introduction to the module in the post about my research workflow:
XMonad has a module called TopicSpace, which upgrades the X11 workspace—virtual desktop—concept to so-called topics. These are workspaces with a “theme” associated to them; for example, I have a topic for every project that I’m currently working on. This results in a very clean separation of concerns. Plus, I always know where my windows are!
Every topic has a directory and a “startup hook”, firing when the topic is switched to and empty, associated to it. While most convenient for programming related tasks—e.g., spawn
ghcidin the relevant directory or automatically build and open this website—it’s also quite convenient for mathematical projects.
I have set up special keybindings to bring up an Emacs session in the topic directory, or spawn a terminal there. Switching to topics is done fuzzily via the XMonad prompt, which means I only have to type a few characters to get to my destination. This makes it feasible to have 30 topics, instead of the usual 9 or so, in the first place. As a result, it’s rather fast to go from thinking about a certain problem to working on it.
At a glance, this probably does not sound very impressive—so one can have a directory and some function associated to a workspace (hereafter also called “topic”), big deal. However, we will see that with a bit of creativity this can be used to great effect.
The most obvious application of all of this is to have workspaces that
do one and only one thing. For example, I have a topic dedicated to
connecting to a VPN, should the need arise. Naturally, I automatically
want to execute
openvpn and pick a random server whenever I happen to
enter that workspace and it’s empty (i.e.,
openvpn is not already
More such use cases include having a topic dedicated to my RSS feed reader, instant messaging, or IRC. Since I only show workspaces with windows on them in xmobar, I can just glance at my status bar to find out whether I currently have, for example, IRC open. No additional program for checking the status of things necessary! Obviously, this modus operandi takes a bit of discipline to uphold over the course of the day, but I find that such a separation of concerns greatly reduces mental load regarding what’s currently happening on my computer. Definitely worth it.
In terms of code, this—as well as the following examples—heavily use the new interface to XMonad.Util.Run, which allows one to spawn processes in a declarative and compositional way; I’ve written about this in another post. For example, my RSS topic is specified thusly:
-- import XMonad.Actions.TopicSpace (inHome) -- import XMonad.Util.Run "7:RSS" $ proc $ inEditor , inHome >-> setFrameName "elfeed" >-> execute (elispFun "elfeed")
inHome is a little helper function that takes a topic name and
an action, and creates a new topic with
$HOME as its associated
You can find all of my topics (and there are a lot of them) here.
Spawning everything in the topic directory
More generally, programming projects in the same language almost always require me to open the same set of standard tools, so it’s extremely convenient to directly spawn them upon first visit. This allows for very little friction before starting to work on whatever I wanted to work on.
For example, I want to open Emacs and ghcid in every Haskell project of mine—so why not automate this? Using what X.U.Run gives us, we can quickly throw together a function that executes the given instruction inside of a terminal:
-- import XMonad.Actions.TopicSpace (currentTopicDir) -- 'topicConfig' is my personal topic configuration. -- | Execute a program in the topic directory (inside a terminal). executeInTopic :: String -> X () = proc $ (termInDir >-$ currentTopicDir topicConfig) executeInTopic p >-> execute p
Similar functions can be created for spawning the terminal and editor:
-- Whatever you're looking for, it's probably in X.A.TopicSpace -- or X.U.Run. -- | Spawn terminal in topic directory. spawnTermInTopic :: X () = proc $ termInDir >-$ currentTopicDir topicConfig spawnTermInTopic -- | Spawn editor in the current topic directory. spawnEditorInTopic :: X () = proc $ inEditor >-$ currentTopicDir topicConfigspawnEditorInTopic
Check the documentation of XMonad.Util.Run to see how
termInDir are defined and may be customised.
In my mathematical and other work-adjacent projects I keep it pretty simple; an editor there is mostly sufficient.
But we can also get a little bit more fancy. Since the topic action is
just a general
X action, we can really do anything we want in there.
In addition to spawning programs, all of my Haskell projects should
default to the
spawnHaskell :: X () = sendMessage (JumpToLayout "Hacking") spawnHaskell *> spawnEditorInTopic *> executeInTopic "ghcid"
And Voilà, we can now attach this action to all the topics that we want!
Note that the
*> operator is—in this case—just the sequencing of
actions. If you’re more comfortable with
do notation, you can also
write the above as
spawnHaskell :: X () = do spawnHaskell JumpToLayout "Hacking") sendMessage ( spawnEditorInTopic"ghcid" executeInTopic
Furthermore, since the associated directory for a topic can easily be
$HOME by default (as we’ve seen, TopicSpace even exports the
inHome function), spawning programs in certain topics can easily be
made to replace the default keybindings!
For the sake of completeness, I will showcase one slightly more
complicated example. My main shell environment is
eshell and getting
sane behaviour there presents one with a few more obstacles than
spawnTermInTopic did. It also uses
inProgram instead of
allowing access to a different instance of the Emacs server.
-- | Spawn an eshell frame in the current topic directory. spawnEshellInTopic :: X () = currentTopicDir topicConfig >>= \dir -> spawnEshellInTopic $ inProgram "emacsclient -a '' -c -s eshell" proc >-> execute (progn [ "eshell" <> quote "new-shell" "eshell/cd" <> asString dir , "eshell/clear-scrollback" , "eshell-send-input" , ])
All in all, we have something that looks a little bit like this:
Testing this website
Much in the same vein as my Haskell topics, I find the
to be extremely handy—you can probably guess what it’s used for. Its
spawnWebsite switches to the “Tall” layout, spawns
an Emacs frame in the topic directory, builds the website, and opens a
browser window pointing to the local copy:
spawnWebsite :: X () = switchToLayout "Tall" spawnWebsite *> spawnEditorInTopic *> executeInTopic "hakyll-build.sh --hold" *> spawn "browser-new-window.sh localhost:8000"
The whole thing looks like this:
Hopefully these examples have convinced you to give TopicSpace a spin; perhaps you’ve even gotten some ideas of your own you’d like to try out. Although conceptually very simple, the module can be used in a variety of ways to automate boring tasks just that tiny bit more—definitely a win in my book!