The Complete Computing Environment

Trying out XMonad in Home Manager


I haven't been particularly pleased with i3wm's container strategy and basically just miss some of the simplicity of operating with XMonad. I don't miss all the complexity of it though, but maybe I'm in a better position to write Haskell now… Easy enough to fuck around and find out.

Minimal Haskell support in Emacs

Of course this will be included in Arroyo Emacs to make developing this configuration viable.

(use-package haskell-mode)
(provide 'cce/haskell)

xomand.hs configuration file

Because KDE is a Base for my Emacs Desktop, I don't fuck with things like xmobar or R&R support in this layer, let my desktop environment deal with all of that instead of me. It has built in integration to configure it to manage the Plasma windows and whatnot reasonably.

import XMonad
import XMonad.Config.Kde

myMod = mod4Mask

main = xmonad myConfig

myConfig = kde4Config
  { modMask    = myMod
  , layoutHook = desktopLayoutModifiers $ myLayoutHook 
  , manageHook = manageHook kde4Config <+> myManageHook
  , workspaces = myWorkspaces
  } `additionalKeys`
  [ ((myMod, xK_Tab), toggleRecentWS) -- (ref:recentWS)





de-tangling this a bit is probably a fool's errand, but here we go…

XMonad's docs on configuring as well as the quick start guide will make a lot more sense of the hooks but broadly:

The basic kde4Config

"but rrix don't you run Plasma 5?" yes, yes I do, but unlike in the 3->4 transition the KDE developers were able to maintain a large amount of backwards compatibility. I think it would behoove the XMonad developers to recognize this and make, say, a kde3Config for the 30 Trinity users still left but Sir, This is a Wendy's. So all this is broadly defined in XMonad.Config.Kde and XMonad.Config.Desktop except it doesn't talk about Plasma 5 at all.

Make sure if you do make your own kde4Config that it pulls the default manageHook and invokes desktopLayoutModifiers out of the default configuration.

The only really notable things other than wiring up the hooks defined below are some simple appearance changes:

, focusedBorderColor = "#ebbe7b"
, normalBorderColor  = "#707231"
, borderWidth        = 4

EZConfig makes it eazy to define new keybindings

There's not a lot of "there" there additionalKeys is used to add or override keybindings.

import XMonad.Util.EZConfig (additionalKeys)
import XMonad.Actions.CycleRecentWS (toggleRecentWS)

That's right, we're nesting noweb functions!

I have a bunch of little helpers in i3wm and A World Without EXWM which I should collate and expand on for example to add my org-fc function… but anyways, S-a opens my agenda, S-f opens a new Emacs frame, S-g opens my Journal, S-x opens a new frame with the M-x execute-interactive-command open on it. S-z opens my todo list. S-c opens my Flash Cards.

, ((myMod, xK_a), spawn "org-agenda")
, ((myMod, xK_f), spawn "emacsclient -c -n")
, ((myMod, xK_g), spawn "org-journal")
, ((myMod, xK_x), spawn "meta-x")
, ((myMod, xK_z), spawn "org-todos")
, ((myMod, xK_c), spawn "emacsclient -c -e '(srs)' -n")
, ((myMod, xK_p), spawn "bash -c 'pkill krunner; krunner'")

There is also (recentWS) above which toggles between the … most recent workspace … on S-Tab. Useful for jumping between work terminals spread on the high-numbered workspaces when I am working on a high-context activity on my laptop display only. I provide it up there so that the first element of that list doesn't start with a comma, basically. All the other parts which go in to additionalKeys should start with a commas as though they're (rightfully) being spliced in to a list.

myWorkspaces culls down my workspace list a bit

I don't need nine workspaces. If I am working on that many different things it's a sign I should close stuff out. myWorkspaces is included in the kde4Config declaration above. This pattern is lifted from an example xmonad configuration.

myWorkspaces = ["front", "im", "music", "misc1", "misc2", "fullscreen"]

myLayoutHook captures Layout Customization

myLayoutHook customizes each of the layouts you can swap between (by default bound to S-SPC)

myLayoutHook = imw $ fullscr $ tiled ||| Mirror tiled ||| full ||| threeCol 
    tiled    = BW.boringWindows $ smartSpacingWithEdge 4 $ Tall nmaster delta ratio
    full     = BW.boringWindows $ noBorders $ Full
    threeCol = BW.boringWindows $ smartSpacingWithEdge 4 $ ThreeColMid nmaster delta ratio
    imw      = onWorkspace "im" $ threeCol ||| tiled ||| Mirror tiled ||| full
    fullscr  = onWorkspace "fullscreen" full
    nmaster  = 1      -- Default number of windows in the master pane
    ratio    = 2/3    -- Default proportion of screen occupied by master pane
    delta    = 3/100  -- Percent of screen to increment by when resizing panes

This code block I think was ultimately what I left i3wm for. The "do it yourself" container design made it a real pain in the ass especially when swapping between my 3:2 laptop display and 21:9 widescreen desktop. Now it's just a Super-SPACE away.

I modify my layouts with two modules:

The Boring Window commands are spliced in to additionalKeys above:

, ((myMod, xK_j), BW.focusDown)
, ((myMod, xK_k), BW.focusUp)
, ((myMod, xK_m), BW.focusMaster)
, ((myMod, xK_s), BW.markBoringEverywhere)
, ((myMod .|. shiftMask, xK_s), BW.clearBoring)

This relies on these libraries:

import XMonad.Layout.ThreeColumns
import qualified XMonad.Layout.BoringWindows as BW
import XMonad.Layout.Spacing
import XMonad.Layout.NoBorders
import XMonad.Layout.PerWorkspace

myManageHook provides rules that apply to windows when they are opened

This does a fair bit at once, and is based on some examples taken out of the xmonad docs. Seriously go read them and do your best to make sense of all these new and interesting operators. I'm sorry about all the Haskell and all the line-noise operators, I really am. I don't understand them either, they're mostly lifted from folks who took too many math courses at Uni.

myFloats      = ["pinentry", "ksmserver-logout-greeter", "krunner"]
myTitleFloats = []
prodApps      = ["firefox", "emacs"]
commApps      = ["discord", "Signal", "Element", "telegram-desktop"]
mediaApps     = ["cantata"]

myManageHook = composeAll . concat $
    [ [ className =? c --> doFloat           | c <- myFloats]
    , [ title     =? t --> doFloat           | t <- myTitleFloats]
    , [ className =? c --> doF (W.shift "1") | c <- prodApps]
    , [ className =? c --> doF (W.shift "2") | c <- commApps]
    , [ className =? c --> doF (W.shift "3") | c <- mediaApps]
    , [ className =? "plasmashell" <&&> checkSkipTaskbar --> doIgnore] -- (ref:checkSkips)
    , [ className =? "plasmashell" <&&> checkIsDesktop --> doIgnore]
import qualified XMonad.StackSet as W
import XMonad.Hooks.ManageHelpers (isInProperty)

These are the (checkSkips) helpers..:

checkSkipTaskbar :: Query Bool
checkSkipTaskbar = isInProperty "_NET_WM_STATE" "_NET_WM_STATE_SKIP_TASKBAR"

checkIsDesktop :: Query Bool

home-manager configuration

{ pkgs, config, ... }:
rec {
  home.sessionVariables = {
  xsession.windowManager.xmonad = {
    enable = true;
    enableContribAndExtras = true;

    config = ../files/xmonad.hs;
  # <<plasma-xmonad>>

XMonad Auto Start on KDE >= 5.25

After KDE 5.25, Plasma now uses SystemD to start up the desktop. This means that KDEWM is ignored because reasons. Well. Here's a work around. "just write a user unit and do a bunch of BS" = {
  Install.WantedBy = ["plasma-plasmashell.service"];
  Unit.Description = "Start XMonad instead of KWin";
  Unit.Before = "plasma-plasmashell.service";
  Service.ExecStart = "${config.home.homeDirectory}/.xmonad/xmonad-${pkgs.stdenv.hostPlatform.system}";
home.activation.plasma-xmonad = ''
  # systemctl --user mask plasma-kwin_x11.service
  # systemctl --user daemon-reload
  # back to kwin ...
  systemctl --user unmask plasma-kwin_x11.service
  systemctl --user disable plasma-xmonad.service

Thinking about this Thread from Geoffrey Litt….

Along these lines, one of my favorite findings in malleable software research:

Wendy Mackay found in 1991 that a very common reason people customized their Unix setups was to keep things the way they used to be

Kinda ironic, right?