Displaying Org Mode Pomodoro Clock in the Menu Bar

As I’ve written in previous posts(1, 2), I’m a huge fan of Emacs Org mode. One thing that I’ve been doing on-and-off is to use Org mode to also track my time and implement the pomodoro method. It’s not entirely perfect, though one thing that has helped me a lot is to display the current pomodoro status in the menu bar, which makes it much easier to keep an eye on things even when I’m not using Emacs. No matter how great an OS Emacs is, surely you can’t live your whole life within it? ;)

The following screenshots show this in action under four different pomodoro clock types, respectively:

  • Normal pomodoro: pomodoro-normal-screenshot
  • Pomodoro in overtime (enabled with org-pomodoro-manual-break set to t): pomodoro-overtime-screenshot
  • Short break pomodoro-short-break-screenshot
  • Long break pomodoro-long-break-screenshot

I’ll describe my setup in MacOS. The method should be replicable in Linux and Windows with minimum adaptations. I also came across a post for Linux and an app for Windows achieving something similar using alternative approaches, which you might be interested in checking out.

There are two key pieces to this approach:

  • the function org-pomodoro-format-seconds, which outputs the pomodoro clock according to org-pomodoro-time-format (not necessarily in seconds, thus it’s somewhat of a misnomer).
  • the open-source app BitBar which evaluates an arbitrary command and displays the output in the menu bar.

The following is a step-by-step walkthrough:

  1. Specify the format you want the time to be displayed via org-pomodoro-time-format. In my case I have a simple format of mm:ss:
(setq-default org-pomodoro-time-format "%.2m:%.2s")
  1. Define a function that will produce the final output to be displayed on the menu bar for the script. In my case I made the following additions to the string output:
    • Prepend prefix characters “P”, “O”, “B”, “LB” to indicate the exact state of the pomodoro.
    • Display “N/A” when there’s no ongoing pomodoro.

    The entire function definition is as follows:

(defun jx/produce-pomodoro-string-for-menu-bar ()
  "Produce the string for the current pomodoro counter to display on the menu bar"
  (let ((prefix (cl-case org-pomodoro-state
            (:pomodoro "P")
            (:overtime "O")
            (:short-break "B")
            (:long-break "LB"))))
          (if (and (org-pomodoro-active-p) (> (length prefix) 0))
            (list prefix (org-pomodoro-format-seconds)) "N/A")))
  1. Install BitBar and ensure it runs normally. Open BitBar’s plugin folder. Create a shell script org-clock.1s.sh that calls emacsclient to evaluate the above-defined function:
    • The 1s part indicates that BitBar should evaluate this script every one second.
    • You may need to run chmod +x org-clock.1s.sh after creating it to make it executable.
    • If the emacsclient command fails, you may need to ensure that (server-start) is run automatically every time Emacs starts, so that the Emacs server/client is available.

    The script is as follows:


export PATH='usr/local/bin:/usr/bin:$PATH'

# The default output of (org-pomodoro-format-seconds) contains parentheses, which I don't need. Sub them out with sed.
emacsclient --eval "(jx/produce-pomodoro-string-for-menu-bar)" | sed 's/["()]//g'

That’s it! Now, whenever you start a pomodoro in org-mode, you should be able to see the clock reflected in your menu bar :)

Differences between Vim Regex, Emacs Regex and PCRE

When I first switched over from Vim to Spacemacs, one thing that really caught me off guard was the behavior of / search under evil-mode. evil-mode reproduces the Vim experience so well, it’s easy to forget the underlying Emacs base of it all.

Turns out, both Vim and Emacs have their own regex syntaxes that are slightly different from the one used by PCRE (Perl-Compatible Regular Expressions). vi (and its predecessor ed) and emacs are both older than Perl, so this is to be expected.

I would like to list some of the differences that are likely to be encountered in your day-to-day work. I will also list some resources that you can refer to for more details.

Differences between Emacs Lisp regex and PCRE regex

The following are some common gotchas of Elisp regex:

  • You need to escape some additional symbols such as:
    • backslash \: \\
    • alternation |: \|
    • grouping ( and ): \( and \)
    • counting { and }: \{ and \}
  • \s begins a syntax class. Whitespaces are denoted as \s- instead of \s.
  • Use [0-9] or [:digit:] instead of \d to denote digits.
  • Use \1, \2 etc. instead of $1, $2 to refer to the results of capturing parentheses.

For more details, refer to The EmacsWiki and this SO question.

In addition, there is a tool which converts PCRE regex to Emacs regex.

Differences between Vim regex and PCRE regex

This SO answer by J-P summarizes some the common gotchas of Vim regex:

Perl    Vim     Meaning
x?      x\=     Match 0 or 1 of x
x+      x\+     Match 1 or more of x
(xyz)   \(xyz\) Use brackets to group matches
x{n,m}  x\{n,m} Match n to m of x
x*?     x\{-}   Match 0 or 1 of x, non-greedy
x+?     x\{-1,} Match 1 or more of x, non-greedy
\b      \< \>   Word boundaries
$n      \n      Backreferences for previously grouped matches

You can also get some additional information via :help perl-patterns.

There are some ways to change the behavior of Vim regex:

  • Use the “very magic regex mode” by prepending the pattern with \v. However, note that \b will still mean “backspace” instead of “word boundaries”.
  • Manually run perl: :perldo s/searchme/replaceme/g.
  • Use the plugin eregex.vim which performs the conversion automatically and allows you to search using PCRE regex by default.

Elixir Process Orchestration In Kubernetes With Libcluster And Swarm

Update: horde is a library that is “built to address some perceived shortcomings of Swarm’s design.” (from the introductory blog post), and is currently (as of December 2020) more actively maintained than Swarm. It also works together with libcluster. We used horde for some new use cases and we recommend you to also check it out if Swarm doesn’t fit your need.

Collaborated with Rocky Neurock

We run Elixir on Kubernetes. While there were some rough edges for such a setup a few years ago, it has become much easier thanks to libraries such as libcluster and Swarm. Recently, we had a use case which those two libraries fit perfectly. However, since the libraries are relatively new, you might find the documentation and examples a bit lacking and it took us a while to get everything working together. We wrote this comprehensive walk-through to document our experience.

Note that you’d need to run your Elixir instances as releases, either with distillery or built-in releases (Elixir 1.9+), in order for this to work. This walk-through is based on our setup using Distillery.

libcluster and Swarm

First, a bit of background information:

The ability for different nodes to form a cluster and maintain location transparency has always been a huge selling point for the BEAM VM. libcluster is a library that helps with automatic node discovery and cluster formation, especially when the nodes are run in Kubernetes pods.

The main reason you might want a cluster is so you can organize and orchestrate processes across different nodes. This is where Swarm comes into the picture. Swarm maintains a global process registry and automates tasks such as process migration/restart after the cluster topology changes.

In our case, we have some permanent worker processes which should only have one instance of each running across all nodes at any given time. The problem is that we perform k8s rolling deployments with our CD pipeline, thus pods (and therefore nodes) will get destroyed and recreated throughout the day.

With Swarm, we register all above-mentioned worker processes globally, so whenever a pod gets destroyed, its worker processes will be automatically restarted on another healthy pod, ensuring that they are running exactly as envisioned all the time.

(Note: Initially, Swarm contained both the auto-clustering functionality and the process orchestration functionality. Later, the maintainer decided that it would be better to split them into two separate libraries, which become the libcluster and Swarm we see today.)

Clustering with libcluster

In our first step, we’re going to ensure that automatic cluster formation in Kubernetes takes place successfully.

Before we add libcluster to our project, we need to ensure that every Elixir node has a unique name. By default, in the rel/vm.args file generated by Distillery, we have a line just like:

-name <%= release_name %>@

This means every node will be started as yourapp@, which is not what we want.

We could first expose the Kubernetes pod IP as an environment variable in the kube configuration (e.g. kube/deploy.yaml) for the Elixir app:

apiVersion: extensions/v1beta1
kind: Deployment
  name: your-app
  # ...
  # ...
    # ...
      # ...
      - name: your-app
        # ...
        - name: MY_POD_IP
              fieldPath: status.podIP

and then you can change the line in rel/vm.argsso the pod IP address will be substituted at runtime:

-name <%= release_name %>@${MY_POD_IP}

After ensuring unique node names, you can already test clustering manually via your console:

# On your local machine
~ kubectl exec -n your-namespace your-pod -c your-container -- sh

# Launch Elixir console after connecting to your k8s pod.
~ bin/console

iex(app@<POD_1_IP>)> Node.connect(":app@<POD_2_IP>")
iex(app@)> Node.list()

# If things are set up correctly, Node.list() should return [":app@<POD_2_IP>"]

Automatic Clustering

Having tested manual clustering, we can then move on to automatic clustering with libcluster.

You may notice that there are three clustering strategies for Kubernetes in libcluster and wonder which one you should use. In our experience, Cluster.Strategy.Kubernetes.DNS is the easiest to set up. All it requires is that you add a headless service to your cluster and no modifications are needed for the already existing (Elixir) pods.

In your k8s config:

apiVersion: extensions/v1beta1
kind: Deployment
  name: your-app
  # ...
      # The selector for the headless service can
      # find the app by this label
        app: your-app
        role: service
      # ...

apiVersion: v1
kind: Service
  name: your-app-headless
  clusterIP: None
  # The selector is just a way to filter the pods on which the app is deployed.
  # It should match up with the labels specified above
    app: your-app
    role: service

After adding the headless service, we’re finally ready for libcluster. You may add the ClusterSupervisor to the list of supervisors to be started with your app.

### In mix.exs
  defp deps do
      {:libcluster, "~> 3.1"},

### In config/prod.exs
config :libcluster,
  topologies: [
    your_app: [
      strategy: Cluster.Strategy.Kubernetes.DNS,
      config: [
        service: "your-app-headless",
        # The one as seen in node name yourapp@
        application_name: "yourapp",
        polling_interval: 10_000

### In lib/application.ex
  def start(_type, _args) do
    # List all child processes to be supervised
    children =
                Application.get_env(:libcluster, :topologies),
                [name: Yourapp.ClusterSupervisor]
          # Start the Ecto repository
          # Start the endpoint when the application starts

    opts = [strategy: :one_for_one, name: Yourapp.Supervisor]

    Supervisor.start_link(children, opts)

If everything goes well, your pods should already be automatically clustered when you start them. You can verify this by running Node.list() in the console, similar to what we did above.

Optional: Local cluster setup with docker-compose

During development, you will not want to deploy your changes every time to an actual k8s cluster in order to validate them or run a local k8s cluster for that matter. A more lightweight approach would be to make libcluster work together with docker-compose and form a local node cluster. We found Cluster.Strategy.Gossip the easiest to set up for this purpose.

The following assumes the app is started with docker-compose directly, without using Distillery releases.

First, we need to make sure that each Erlang node has a unique name, just as we did for the production environment. We will do it in our entrypoint script for docker-compose:

# In Dockerfile:
ENTRYPOINT ["/opt/app/docker-entrypoint-dev.sh"]

# In docker-entrypoint-dev.sh:
if [ -z ${NODE_IP+x} ]; then
    export NODE_IP="$(hostname -i | cut -f1 -d' ')"

elixir --name yourapp@${NODE_IP} --cookie "your_dev_erlang_cookie" -S mix phx.server

Then, we need to scale the number of containers for our service to 2. You can easily do it by adding another service in your docker-compose.yml file:

  your_app: &app
      context: ./app
      - 4000:4000
    # ...

    <<: *app
      - 4001:4000

(Note: Another way to achieve the same is to use the --scale flag of the docker-compose up command.)

Finally, we just need to specify our clustering strategy correctly:

### In config/config.exs
config :libcluster,
  topologies: [
    your_app: [
      strategy: Cluster.Strategy.Gossip,
      config: [
        port: 45892,
        if_addr: "",
        multicast_addr: "",
        multicast_ttl: 1

By default, the port and the multicast address should already have been available. If not, you can check your docker-compose configurations.

By this point, the two local nodes should be able to automatically find and connect to each other whenever you start your app via docker-compose.

Process registration with Swarm

After the foundation has been laid with libcluster, we may now move on to Swarm.

This sentence from the documentation is key to using Swarm to its maximum potential:

Swarm is intended to be used by registering processes before they are created, and letting Swarm start them for you on the proper node in the cluster.

Therefore, we found the example from the documentation, which shows Swarm being used together with a normal Supervisor, to be slightly confusing: a normal Supervisor must start with some initial child worker processes, which will not be managed by Swarm. DynamicSupervisor seems to suit Swarm’s use case the most: we can start a DynamicSupervisor without any children and ensure all child processes are registered with Swarm before they are dynamically started later.

We can write our DynamicSupervisor module as such:

defmodule Yourapp.YourSupervisor do
  use DynamicSupervisor

  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  def start_link(state) do
    DynamicSupervisor.start_link(__MODULE__, state, name: __MODULE__)

  def init(_) do
    DynamicSupervisor.init(strategy: :one_for_one)

  def register(worker_name) do
    DynamicSupervisor.start_child(__MODULE__, worker_name)

Note that in the init function we didn’t need to provide any actual children to be started.

The register function is a convenience function that needs to be provided to Swarm.register_name/4 whenever we want to start a worker process with Swarm. It simply calls start_child and would return the pid of the started worker.

This is how you would dynamically start your worker process anywhere in your app:


Finally, we come to the definition for the worker process itself. Below is a minimal working example which would simply restart a killed worker process on another node, without preserving its state:

defmodule Yourapp.YourWorker do
  use GenServer

  def start_link(state) do
    GenServer.start_link(__MODULE__, state)

  def init(opts) do
    initial_state = initialize_worker(opts)

    {:ok, initial_state}

  def handle_call({:swarm, :begin_handoff}, _from, state) do
    {:reply, :restart, state}

  def handle_info({:swarm, :die}, state) do
    {:stop, :shutdown, state}

  defp initialize_worker(opts) do
    # ...

Optionally, if you need to make use of the state handover functionality, you would need to make your worker more complicated with these additions:

  # Change the handling of :begin_handoff
  # This is triggered whenever a registered process is to be killed.
  def handle_call({:swarm, :begin_handoff}, _from, current_state) do
    {:reply, {:resume, produce_outgoing_state(current_state)}, current_state}

  # Handle :end_handoff
  # This is triggered whenever a process has been restarted on a new node.
  def handle_call({:swarm, :end_handoff, incoming_state}, _from, current_state) do
    {:noreply, end_handoff_new_state(current_state, incoming_state)}

Now, if you kill a node, you should see all the workers that were originally running on it automatically restarted on another node in the cluster.

Thanks for reading. We hope this walk-through has been of some help to you.

Emacs Setup for Elixir and Vue

I had been an avid user of Spacemacs for a long time. However, it could sometimes be time-consuming to get the setup just right in Emacs land. I would like to share some of my configurations here for a satisfactory editing experience for Elixir and Vue.

Language Server Protocol

Language Server Protocol (LSP) was created by Microsoft to define a common standard for providing editor-agnostic code intelligence support. It has become widely popular since its creation.

Emacs support for lsp comes with the lsp-mode package. Both of the following setups depend on it. Therefore, one needs to install it first, either directly or via the Spacemacs lsp layer.

      # In `dotspacemacs/layers`:


The landscape surrounding Vue support on Emacs has been quite confusing.

There had been an old vue-mode package, which is likely to be the first result on Google. However, the package lacks many features compared to Vetur (a popular VSCode extension) such as autocompletion, and is not being actively developed anymore.

There is also a legacy lsp-vue package, which is supposed to leverage vue-language-server from Vetur. However, this package is not compatible with the the newest version of Emacs’ lsp-mode.

Actually, the new lsp-mode already ships with a lsp-vetur.el, which provides support for Vue via vue-language-server. There is even a non-official Vue layer for Spacemacs that integrates with ESLint and Prettier to provide a complete editing experience similar to Vetur in VSCode. I have been using this layer for a while and it works great. The author has mentioned submitting a MR to Spacemacs to incorporate it into the list of official layers, but hasn’t done so yet. That partly explains why it might take one some digging to find it.

To install the layer:

# Folder for private layers
cd ~/.emacs.d/private
git clone git@github.com:thanhvg/vue.git

# Install the necessary tools manually
# vue-language-server is the language server developed by vetur
npm install -g eslint prettier vue-language-server

To configure the layer in your .spacemacs file:

      # In `dotspacemacs/layers`
      (vue :variables
           vue-backend 'lsp)

       # Depends on whether you already configured this variable in the `javascript` layer
       (node :variable node-add-modules-path)

If you want to format the Vue file with prettier automatically upon saving, add the following:

  # In dotspacemacs/user-config
  (add-hook 'vue-mode-hook
            (lambda ()
              (add-hook 'before-save-hook #'prettier-js nil t)))

You can then enjoy Vetur-like editing capabilities in Emacs. You can find default shortcuts in the layer README.


alchemist.el had been the go-to code intelligence tool for Elixir a couple of years ago. However, its development has stalled, and elixir-ls, which provides more complete code intelligence out of the box, is currently the recommended IDE server for Elixir.

While vscode-elixir-ls downloads and builds elixir-ls automatically, we can also manually clone and build the project, so that it can be used by Emacs (and other editors), similar to what we did with vue-language-server.

git clone git@github.com:elixir-lsp/elixir-ls.git
cd elixir-ls
mix compile
# For example, when the target Elixir version is 1.9.0
mix elixir_ls.release -o release-1.9.0

Since we have several projects with different Elixir versions, I also made sure to compile several versions of elixir-ls using asdf, each corresponding to one of the target projects. One just needs to change the .tool-versions file in elixir-ls folder before compiling and releasing again.

The newest lsp-modeprovides support for elixir-ls already. A recent PR to Spacemacs’ Elixir layer introduced support for lsp backend.

      # In `dotspacemacs/layers`:
      (elixir :variables
              elixir-backend 'lsp
              elixir-ls-path "path-to-elixir-ls/release-1.9.0"

To switch between the different versions of elixir-ls compiled for different Elixir versions, we can make elixir-ls-path a directory local variable, by having a .dir-locals.el file in the project root folder:

  (elixir-ls-path . "path-to-elixir-ls/release-1.8.1")))

To add auto formatting on save:

  # In `dotspacemacs/user-config`:
  (add-hook 'elixir-mode-hook
            (lambda ()
              (add-hook 'before-save-hook #'lsp-format-buffer nil t)))

You could also refer to the comprehensive FAQ on ElixirForum, should you have any questions regarding the setup.

Tweaks and Customizations

On MacOS, you might get an error about exceeding the maximum limit of file descriptors, since lsp-mode tries to watch the folder for changes. Some workarounds are described in this post. If none of them works, you can also disable the file watching altogether:

      (lsp :variables
           lsp-enable-file-watchers nil

By default, lsp-mode displays a floating window that displays documentation following your cursor movement, which I found a bit intrusive. You can turn it off by setting lsp-ui-doc-enable to nil

      (lsp :variables
           lsp-ui-doc-enable nil)

Then one can display the documentation with , h h.

I don’t like the default behavior, where the documentation buffer pops up at the bottom of the editor. After some digging, it turns out that the *Help* buffer is controlled by popwin. By adding the following configuration, I was able to view the documentation buffer to the right of the screen:

  (setcdr (assoc "*Help*" popwin:special-display-config)
          '(:dedicated t :position right :stick t :noselect t :width 0.3))

My XMonad Configuration (with Screenshots of Common XMonad Layouts)

Table of Contents

XMonad offers unparalleled customizability, especially with the extensive xmonad-contrib library. However, as the modules in xmonad-contrib are simply listed in an alphabetical order, and there’s no voting mechanism to help differentiate the usefulness (to most users at least) of them, it took me some time to go through a few of them and find what could best benefit my workflow. Setting up xmobar and trayer for the status bar was also not that straightforward.

Here I’ll list some modules that were helpful to me (accompanied by screenshots), in the hope that some might find this article useful and save them some time. My full configuration files are posted at the end of the article.

This also serves as a note to myself as I keep exploring XMonad. I’m still a learner and I’d appreciate it if you point out mistakes in my configuration.

From i3 to XMonad

I’ve been using Thinkpad X1 Carbon with Arch Linux for a while and my experience has been great. The only two features I miss from MacOS are the built-in Dictionary and the seamless HiDPI support, but I can get by without them just fine. The pleasure of being able to harness the full power of Arch Linux together with a proper window manager far outweighs the inconvenience. The topic of X1 Carbon vs. Macbook is probably best left for another article though.

I started with i3, as it is undoubtedly the most popular WM out there, and perhaps the most beginner-friendly. However, some of i3’s inflexibility constantly gnawed at me. Eventually I decided that I’m comfortable enough with WMs to begin exploring something more customizable.

In comparison to i3, the mental model adopted by XMonad is (unexpectedly) much more intuitive in several aspects, out of the box:

  • The concepts of “screen” and “workspace” are cleanly separate, which is great. You are able to switch to/move an application to a particular screen, without worrying about which exact workspace is currently on that screen. This is a life-changer for me as I now almost exclusively use screen-switching keys (which I’ve conveniently defined as Mod+a, Mod+s, and Mod+d).
  • A screen merely “projects” a workspace. This is to say, any workspace that is not currently shown, does not “belong” to any particular screen either. You can easily put any other workspace onto the current screen just by Mod+workspaceid. This is a massive improvement from the tedious i3 procedure. This also ensures that disconnecting/reconnecting to external monitors remains a smooth experience, as workspaces get automatically projected to available screens.
  • You are free to customize the workspace layout to your heart’s content. The automatic layout algorithms ensure that you won’t need to perform manual splits like those in i3. The default Tall layout is already very sensible in that it is automatically able to use both axes of the screen.

Useful modules from xmonad-contrib

The above are only the beginning, as xmonad-contrib offers many ready-to-use modules which massively enhance the already great defaults.


Layout algorithms are the fundamentals of any window manager. There are tons of layouts in xmonad-contrib, but save for a summary page without screenshots on the Wiki, there doesn’t seem to be much easily accessible information around. I’ve tried out each layout in there. IMO while most of them suit very specific needs and might not be very useful for most users’ daily workflow, a few of them could become indispensable. I’ll list such layouts below, complete with screenshots.

Tabbed (XMonad.Layout.Tabbed)

Tabbed Screenshot

This layout adds tabs to the default Fullscreen layout. It’s in a sense similar to i3’s default fullscreen layout.

This is the essential layout for multi-monitor setups, where each application automatically occupies the whole screen.

TwoPane (XMonad.Layout.TwoPane)

TwoPane Screenshot 1

TwoPane Screenshot 2

This is a frequent use case I had in i3: Divide a window into two panes and cycle between applications within an individual pane. For example, I might have a tech talk playing in one pane, while alternatively programming with a code editor or taking notes with org-mode in the other pane. Another example is keeping Anki/an article open in one pane and cycling between different dictionary apps in the other. The TwoPane layout achieves this by fixing the application in the main pane while allowing you to cycle through other applications in the secondary pane.

By default the split is vertical. However, just like the case in Tall layout, by simply mirroring the layout you can also make the split horizontal, as shown in the screenshot.

Mirror TwoPane Screenshot

There’s also a DragPane layout that allows you to additional resize the split ratio by mouse, and offers more configuration options. However it didn’t seem to work on my system as the pane borders constantly blink.

ResizableTall (XMonad.Layout.ResizableTile)

ResizableTall Screenshot

The default Tall layout only allows for adjusting the ratio of the main split, i.e. all the secondary panes will have the same size. However, there might be a use case where you want to have one relatively large secondary pane (e.g. Emacs) and a relatively small secondary pane (e.g. terminal). ResizableTall extends Tall by allowing for the layout to be extended just fine.

The screenshot shows both the ratio of the main split and that between the secondary panes adjusted.

emptyBSP (XMonad.Layout.BinarySpacePartition)

EmptyBSP Screenshot

This layout will automatically split your focused window in two to make space for the newly created window.

Spiral, Dwindle (XMonad.Layout.Dwindle)

Spiral Screenshot

These two layouts imitate awesomeWM and produce increasingly smaller windows in fixed locations.

I find the above listed layouts able to satisfy almost all of my daily needs for now. However, you can create much more complicated custom layouts by using modules such as Xmonad.Layout.Combo or Xmonad.Layout.LayoutCombinators.


This is an essential module for multi-monitor setups. When multiple monitors are connected, the screen ids get assigned quite arbitrarily by default. However, we’d normally want the screens numbered in a left-to-right order according to their physical locations. This module provides the getScreen and viewScreen functions that help us do just that.

Example usage:

-- mod-{w,e,r}, Switch to physical/Xinerama screens 1, 2, or 3
-- mod-shift-{w,e,r}, Move client to screen 1, 2, or 3
[((modm .|. mask, key), f sc)
    | (key, sc) <- zip [xK_w, xK_e, xK_r] [0..]
    , (f, mask) <- [(viewScreen def, 0), (sendToScreen def, shiftMask)]]


This achieves the same thing as that by i3gaps. XMonad argues that the correct terminology for this should be “spacing” instead of “gaps”, since “gaps” should refer to the gap between a window and the edges, not between panes within a window.

This makes the layout a bit less crowded.

myLayout = avoidStruts $
  ||| tiled
  ||| twopane
     tiled = spacing 3 $ ResizableTall nmaster delta ratio []
     twopane = spacing 3 $ TwoPane delta ratio


You need to add an ewmh hook if you want to correctly use rofi to locate and switch to a running application by its name.

Just import the module and then add ewmh as such:

main = do
    xmonad $ ewmh def


It would be silly to have a border around the window if the window always occupies the whole screen. Use noBorders to avoid that in such layouts (e.g. tabbed, Full).

myLayout = avoidStruts $
  noBorders (tabbed shrinkText myTabConfig)

historyHook (XMonad.Actions.GroupNavigation)

historyHook keeps track of your window history and allows for actions such as going back to the most recent window.

Just append >> historyHook to the end of your logHook, e.g.

        , logHook = dynamicLogWithPP myPP {
                                          ppOutput = hPutStrLn xmproc
                        >> historyHook

xmobar and trayer

Normally one would want to have a status bar and an application/applet tray. The most popular choices for those seems to be xmobar and trayer.

The configuration options for xmobar is stored in .xmobarrc. They are relatively well-documented in the official README.

Note that one would need to manually leave some space to the side of the xmobar so that the trayer can be displayed:

In .xmobarrc:

Config {
       , position = TopW L 85

I spawn xmobar in my xmonad.hs file:

main = do
    xmproc <- spawnPipe "xmobar"
    xmonad $ ewmh def
        , logHook = dynamicLogWithPP myPP {
                                          ppOutput = hPutStrLn xmproc
                        >> historyHook

and spawn trayer in my startup script:

trayer --edge top --align right --SetDockType true --SetPartialStrut true \
       --expand true --width 15 --transparent true --alpha 0 --tint 0x283339 --height 20\
       --monitor 1 &

Note that by setting --transparent true, --alpha 0 --tint 0x283339, I was able to ensure that it has the same background color as what I set in .xmobarrc.

Since xmobar and trayer are completely separate processes, if one of them crashes you can just relaunch it individually without impacting the other one’s normal functioning.

Adding an entry in /usr/share/xsessions for startup applications

Finally, when logging in, one might want to launch some startup applications prior to launching xmonad itself, just as one would do in .i3/config with exec.

You can simply write a bash script run-xmonad which includes all the commands you want to run. For example:


setxkbmap -option "ctrl:nocaps"
xset r rate 200 50 &

trayer --edge top --align right --SetDockType true --SetPartialStrut true \
       --expand true --width 15 --transparent true --alpha 0 --tint 0x283339 --height 20\
       --monitor 1 &
nm-applet &
xfce4-power-manager &
pactl load-module module-bluetooth-discover &
blueman-applet &

# User apps
dropbox start &
thunderbird &
goldendict &
pulseaudio &
pa-applet &


Note that there is a file /usr/share/xsessions/xmonad.desktop already, which allows you to launch xmonad after logging into an xsession. You can simply create a copy and change the line



Name=Xmonad (with startup apps)

You should then be able to choose this new entry from your dm at your next login.

My full configuration files


import XMonad hiding ((|||))
import qualified XMonad.StackSet as W
import qualified Data.Map        as M

-- Useful for rofi
import XMonad.Hooks.EwmhDesktops
import XMonad.Hooks.DynamicLog
import XMonad.Hooks.ManageDocks
import XMonad.Util.Run(spawnPipe)
import XMonad.Util.EZConfig(additionalKeys, additionalKeysP, additionalMouseBindings)
import System.IO
import System.Exit
-- Last window
import XMonad.Actions.GroupNavigation
-- Last workspace. Seems to conflict with the last window hook though so just
-- disabled it.
-- import XMonad.Actions.CycleWS
-- import XMonad.Hooks.WorkspaceHistory (workspaceHistoryHook)
import XMonad.Layout.Tabbed
import XMonad.Hooks.InsertPosition
import XMonad.Layout.SimpleDecoration (shrinkText)
-- Imitate dynamicLogXinerama layout
import XMonad.Util.WorkspaceCompare
import XMonad.Hooks.ManageHelpers
-- Order screens by physical location
import XMonad.Actions.PhysicalScreens
import Data.Default
-- For getSortByXineramaPhysicalRule
import XMonad.Layout.LayoutCombinators
-- smartBorders and noBorders
import XMonad.Layout.NoBorders
-- spacing between tiles
import XMonad.Layout.Spacing
-- Insert new tabs to the right: https://stackoverflow.com/questions/50666868/how-to-modify-order-of-tabbed-windows-in-xmonad?rq=1
-- import XMonad.Hooks.InsertPosition

--- Layouts
-- Resizable tile layout
import XMonad.Layout.ResizableTile
-- Simple two pane layout.
import XMonad.Layout.TwoPane
import XMonad.Layout.BinarySpacePartition
import XMonad.Layout.Dwindle

myTabConfig = def { activeColor = "#556064"
                  , inactiveColor = "#2F3D44"
                  , urgentColor = "#FDF6E3"
                  , activeBorderColor = "#454948"
                  , inactiveBorderColor = "#454948"
                  , urgentBorderColor = "#268BD2"
                  , activeTextColor = "#80FFF9"
                  , inactiveTextColor = "#1ABC9C"
                  , urgentTextColor = "#1ABC9C"
                  , fontName = "xft:Noto Sans CJK:size=10:antialias=true"

myLayout = avoidStruts $
  noBorders (tabbed shrinkText myTabConfig)
  ||| tiled
  ||| Mirror tiled
  -- ||| noBorders Full
  ||| twopane
  ||| Mirror twopane
  ||| emptyBSP
  ||| Spiral L XMonad.Layout.Dwindle.CW (3/2) (11/10) -- L means the non-main windows are put to the left.

     -- The last parameter is fraction to multiply the slave window heights
     -- with. Useless here.
     tiled = spacing 3 $ ResizableTall nmaster delta ratio []
     -- In this layout the second pane will only show the focused window.
     twopane = spacing 3 $ TwoPane delta ratio
     -- The default number of windows in the master pane
     nmaster = 1
     -- Default proportion of screen occupied by master pane
     ratio   = 1/2
     -- Percent of screen to increment by when resizing panes
     delta   = 3/100

myPP = def { ppCurrent = xmobarColor "#1ABC9C" "" . wrap "[" "]"
           , ppTitle = xmobarColor "#1ABC9C" "" . shorten 60
           , ppVisible = wrap "(" ")"
           , ppUrgent  = xmobarColor "red" "yellow"
           , ppSort = getSortByXineramaPhysicalRule def

myManageHook = composeAll [ isFullscreen --> doFullFloat


myKeys conf@(XConfig {XMonad.modMask = modm}) = M.fromList $

    -- launch a terminal
    [ ((modm .|. shiftMask, xK_Return), spawn $ XMonad.terminal conf)

    -- close focused window
    , ((modm .|. shiftMask, xK_q     ), kill)

     -- Rotate through the available layout algorithms
    , ((modm,               xK_space ), sendMessage NextLayout)

    --  Reset the layouts on the current workspace to default
    , ((modm .|. shiftMask, xK_space ), setLayout $ XMonad.layoutHook conf)
    , ((modm .|. shiftMask, xK_h), sendMessage $ JumpToLayout "Mirror Tall")
    , ((modm .|. shiftMask, xK_v), sendMessage $ JumpToLayout "Tall")
    , ((modm .|. shiftMask, xK_f), sendMessage $ JumpToLayout "Full")
    , ((modm .|. shiftMask, xK_t), sendMessage $ JumpToLayout "Tabbed Simplest")

    -- Resize viewed windows to the correct size
    , ((modm,               xK_n     ), refresh)

    -- Move focus to the next window
    , ((modm,               xK_Tab   ), windows W.focusDown)

    -- Move focus to the next window
    , ((modm,               xK_j     ), windows W.focusDown)

    -- Move focus to the previous window
    , ((modm,               xK_k     ), windows W.focusUp  )

    -- Move focus to the master window
    , ((modm,               xK_m     ), windows W.focusMaster  )

    -- Swap the focused window and the master window
    , ((modm,               xK_Return), windows W.swapMaster)

    -- Swap the focused window with the next window
    , ((modm .|. shiftMask, xK_j     ), windows W.swapDown  )

    -- Swap the focused window with the previous window
    , ((modm .|. shiftMask, xK_k     ), windows W.swapUp    )

    -- Shrink the master area
    , ((modm,               xK_h     ), sendMessage Shrink)

    -- Expand the master area
    , ((modm,               xK_l     ), sendMessage Expand)

    -- Shrink and expand ratio between the secondary panes, for the ResizableTall layout
    , ((modm .|. shiftMask,               xK_h), sendMessage MirrorShrink)
    , ((modm .|. shiftMask,               xK_l), sendMessage MirrorExpand)

    -- Push window back into tiling
    , ((modm,               xK_t     ), withFocused $ windows . W.sink)

    -- Increment the number of windows in the master area
    , ((modm              , xK_comma ), sendMessage (IncMasterN 1))

    -- Deincrement the number of windows in the master area
    , ((modm              , xK_period), sendMessage (IncMasterN (-1)))

    -- Toggle the status bar gap
    -- Use this binding with avoidStruts from Hooks.ManageDocks.
    -- See also the statusBar function from Hooks.DynamicLog.
    , ((modm              , xK_b     ), sendMessage ToggleStruts)
    , ((controlMask, xK_Print), spawn "sleep 0.2; scrot -s")
    , ((0, xK_Print), spawn "scrot") -- 0 means no extra modifier key needs to be pressed in this case.
    , ((modm, xK_F3), spawn "pcmanfm")
    , ((modm.|. shiftMask, xK_F3), spawn "gksu pcmanfm")

      [((m .|. modm, k), windows $ f i)
      | (i, k) <- zip (XMonad.workspaces conf) [xK_1 .. xK_9]
      , (f, m) <- [(W.greedyView, 0), (W.shift, shiftMask)]]
      [((m .|. modm, key), f sc)
      | (key, sc) <- zip [xK_a, xK_s, xK_d] [0..]
      -- Order screen by physical order instead of arbitrary numberings.
      , (f, m) <- [(viewScreen def, 0), (sendToScreen def, shiftMask)]]

main = do
    xmproc <- spawnPipe "xmobar"
    xmonad $ ewmh def
        { modMask = mod4Mask
        , keys = myKeys
        , manageHook = manageDocks <+> myManageHook
        , layoutHook = myLayout
        , handleEventHook = handleEventHook def <+> docksEventHook
        , logHook = dynamicLogWithPP myPP {
                                          ppOutput = hPutStrLn xmproc
                        >> historyHook
        , terminal = "lxterminal"
        -- This is the color of the borders of the windows themselves.
        , normalBorderColor  = "#2f3d44"
        , focusedBorderColor = "#1ABC9C"
          ("M1-<Space>", spawn "rofi -combi-modi window,run,drun -show combi -modi combi")
          , ("C-M1-<Space>", spawn "rofi -show run")
          -- Restart xmonad. This is the same keybinding as from i3
          , ("M-S-c", spawn "xmonad --recompile; xmonad --restart")
          , ("M-S-q", kill)
          , ("M-'", windows W.swapMaster)
          , ("M1-<Tab>", nextMatch History (return True))
          , ("M-<Return>", spawn "lxterminal")
          -- Make it really hard to mispress...
          , ("M-M1-S-e", io (exitWith ExitSuccess))
          , ("M-M1-S-l", spawn "i3lock")
          , ("M-M1-S-s", spawn "i3lock && systemctl suspend")
          , ("M-M1-S-h", spawn "i3lock && systemctl hibernate")
        ] `additionalMouseBindings`
        [ ((mod4Mask, button4), (\w -> windows W.focusUp))
        , ((mod4Mask, button5), (\w -> windows W.focusDown))


Config {
       font = "xft:Noto Sans:size=9:antialias=true,Noto Sans CJK SC:size=9:antialias=true"
       , bgColor = "#283339"
       , fgColor = "#F9fAF9"
       , position = TopW L 85
       , commands = [
                    Run Battery [ "--template" , "B: <acstatus>"
                                , "--L" , "15"
                                , "--H" , "75"
                                , "--low"      , "darkred"
                                , "--normal"   , "darkorange"
                                , "--high"     , "#1ABC9C"
                                , "--" -- battery specific options
                                       -- discharging status
                                       , "-o"	, "<left>% (<timeleft>)"
                                       -- AC "on" status
                                       , "-O"	, "<fc=#dAA520>Charging</fc>"
                                       -- charged status
                                       , "-i"	, "<fc=#1ABC9C>Charged</fc>"
                                ] 50
                    -- , Run Cpu [ "--template" , "C: <total>%", "-L","0","-H","50","--normal","#1ABC9C","--high","darkred"] 10
                    -- , Run Memory ["-t","M: <usedratio>%"] 10
                    , Run DiskU [("/", "D: <free>")] ["-L", "20", "-H", "60"] 10
                    -- , Run Swap [] 10
                    , Run Date "%a %d.%m. %H:%M" "date" 10
                    , Run StdinReader
       , sepChar = "%"
       , alignSep = "}{"
       , template = "%StdinReader% }{ %battery% | %disku% | %date%"