004

XMonad Workspaces & EWW

I recently switched my window manager back to XMonad. I had been using AwesomeWM for quite some time, but I found the way the widgets are written is complicated and difficult to do anything of substance without spending a lot of time with it.

I’ve beeen keeping my eye on a widget framework written in Rust called ELKowars wacky widgets or EWW. It offers the ability to write custom screen widgets like statusbars and dashboards using a variation of the RON configuration language and they can be styled using the GTK-variant of SCSS.

One of the more difficult pieces was finding a way to integrate the workspaces from XMonad into EWW. At first I used wmctrl to do this by polling the state of the workspaces and using that to fill out the widget. Polling shouldn’t happen too often or it will use unecessary resources and with a 1 second interval, it could take a full second for the widget to update with the new state.

To solve this I used XMonad’s StatusBarPP module. This module will write a property to the root X window with a string that represents the workspaces. The main purpose of this is to output the actual string as it appears in something like xmobar but I can use it to write EWW buttons. The other benefit of this approach is that xprop has a -spy argument which follows the output allowing us to create a listener and updates the state immediately.

My XMonad workspaces are defined like this:

myWorkspaces :: [String]
myWorkspaces = ["0", "1", "2", "3", "4"]

I use numbers for the workspace names because wmctrl switches workspaces based on the index. To have custom names I wrote a matcher that replaces the index with the desired name.

tagIcon :: String -> String
tagIcon icon = case () of
  _
    | icon == "0" -> "\62601"
    | icon == "1" -> "\61574"
    | icon == "2" -> "\61878"
    | icon == "3" -> "\61530"
    | icon == "4" -> "\61664"
    | otherwise -> ""

Note: The names of my workspaces are Unicode icons from a font, so they are written in that format. You can use regular string names as well.

My workspace buttons in EWW are pretty simple. It’s a button wrapped in a box with some classes for styling and a click event to trigger the switch. EWW uses a syntax called yuck, which appears to be an extension of the Rust-based syntax named ron.

Example:

(box :class "tag"
  (button :onclick "wmctrl -s 0"
          :class "ws-visible" "name"))

This is what my StatusBarPP looks like after adding an EWW widget for each:

myPP :: PP
myPP =
  def
    { ppVisible = \name -> "(box :class \"tag\" (button :onclick \"wmctrl -s " ++ name ++ "\" :class \"" ++ "ws-visible" ++ "\" \"" ++ tagIcon name ++ "\"))",
      ppHidden = \name -> "(box :class \"tag\" (button :onclick \"wmctrl -s " ++ name ++ "\" :class \"" ++ "ws-hidden" ++ "\" \"" ++ tagIcon name ++ "\"))",
      ppHiddenNoWindows = \name -> "(box :class \"tag\" (button :onclick \"wmctrl -s " ++ name ++ "\" :class \"" ++ "ws-hidden-no-windows" ++ "\" \"" ++ tagIcon name ++ "\"))",
      ppCurrent = \name -> "(box :class \"tag\" (button :onclick \"wmctrl -s " ++ name ++ "\" :class \"" ++ "ws-current" ++ "\" \"" ++ tagIcon name ++ "\"))",
      ppUrgent = \name -> "(box :class \"tag\" (button :onclick \"wmctrl -s " ++ name ++ "\" :class \"" ++ "ws-urgent" ++ "\" \"" ++ tagIcon name ++ "\"))",
      ppOrder = \(ws : _ : _ : _) -> [ws]
    }

Once I verified that the widgets were successfully being written to the window property, I wrote a sript for EWW to call that extracted that value and wrapped it in a container box.

#!/bin/bash

box='(box :class "workspaces" :orientation "h" :halign "center" :spacing 8'

xprop -notype -spy -root 8t _XMONAD_LOG | stdbuf -o0 cut -d'=' -f 2 | stdbuf -o0 sed -u -e "s/^ \"/$box/" -e 's/"$/)/'

The stdbuf -o0 call before cut and sed makes both commands run unbuffered so they will output results as they receive them instead of when the input ends. Without this, the command will run, but will never output anything.

In EWW, the configuration is as simple as:

# Listens to the script above.
(deflisten wmstate
           :initial ""
           "scripts/getws")

# Renders the widget
(defwidget workspaces []
         (literal :content {wmstate} ))

This was a really rewarding effort. The configuration is flexible and can accommodate pretty much any combo of EWW widgets and the UI updates immediately when switching workspaces.

© 2023 David Benjamin