Search code examples
haskellxmonadxmobar

Sort workspaces numerically


I'm using XMonad in combination with xmobar, and I'm having an issue with workspaces displayed on xmobar not being sorted numerically. Because workspace ID's are ultimately strings, they get sorted lexicographically. So, provided that I have 12 workspaces, they are sorted as 1 10 11 12 3 4 5 6 7 8 9, instead of 1 2 3 4 5 6 7 8 9 10 11 12. I think marshallPP is the culprit because prior to introducing independent screens, the workspaces were displayed correctly without ppSort. I know there is mkWsSort that creates a sorting function from a comparison function, however, I'm not sure how would I write the comparison function. This is my config:

import System.IO
import XMonad
import XMonad.Hooks.DynamicLog
import XMonad.Hooks.ManageDocks
import XMonad.Hooks.ServerMode
import XMonad.Hooks.SetWMName
import XMonad.Layout.IndependentScreens
import XMonad.Layout.Gaps
import XMonad.Layout.Spacing
import XMonad.Util.EZConfig (additionalKeysP)
import XMonad.Util.Run (spawnPipe)
import Data.List
import Data.Function
import qualified XMonad.StackSet as W
import qualified Data.Map as M
import XMonad.Util.WorkspaceCompare

myLayout = gaps [(U, 10), (R, 10), (L, 10), (D, 10)] $ spacingRaw True (Border 0 10 10 10) True (Border 10 10 10 10) True $
             layoutHook def

myWorkspaces = 
  [ (xK_1, "1")
  , (xK_2, "2")
  , (xK_3, "3")
  , (xK_4, "4")
  , (xK_5, "5")
  , (xK_6, "6")
  , (xK_7, "7")
  , (xK_8, "8")
  , (xK_9, "9")
  , (xK_0, "10")
  , (xK_minus, "11")
  , (xK_equal, "12")
  ]

clickable' :: WorkspaceId -> String
clickable' w = xmobarAction ("xmonadctl view\\\"" ++ w ++ "\\\"") "1" w

myKeys conf@(XConfig {XMonad.modMask = modMask}) = M.fromList $
    [ ((modMask, key), windows $ onCurrentScreen W.greedyView ws)
      | (key, ws) <- myWorkspaces
    ]
    ++
    [ ((modMask .|. shiftMask, key), windows $ onCurrentScreen W.shift ws)
      | (key, ws) <- myWorkspaces
    ]
    ++
    [
    -- Spawn the terminal
      ((modMask .|. shiftMask, xK_Return), spawn $ XMonad.terminal conf)
    
    -- Spawn dmenu
    , ((modMask, xK_p), spawn "dmenu_run")

    -- Close focused window 
    , ((modMask .|. shiftMask, xK_c), kill)
 
     -- Rotate through the available layout algorithms
    , ((modMask, xK_space ), sendMessage NextLayout)
 
    --  Reset the layouts on the current workspace to default
    , ((modMask .|. shiftMask, xK_space), setLayout $ XMonad.layoutHook conf)
 
    -- Resize viewed windows to the correct size
    , ((modMask, xK_n), refresh)
 
    -- Move focus to the next window
    , ((modMask, xK_Tab), windows W.focusDown)
 
    -- Move focus to the next window
    , ((modMask, xK_j), windows W.focusDown)
 
    -- Move focus to the previous window
    , ((modMask, xK_k), windows W.focusUp)
 
    -- Move focus to the master window
    , ((modMask, xK_m), windows W.focusMaster)
 
    -- Swap the focused window and the master window
    , ((modMask, xK_Return), windows W.swapMaster)
 
    -- Swap the focused window with the next window
    , ((modMask .|. shiftMask, xK_j), windows W.swapDown)
 
    -- Swap the focused window with the previous window
    , ((modMask .|. shiftMask, xK_k), windows W.swapUp)
 
    -- Shrink the master area
    , ((modMask, xK_h), sendMessage Shrink)
 
    -- Expand the master area
    , ((modMask, xK_l), sendMessage Expand)
 
    -- Push window back into tiling
    , ((modMask, xK_t), withFocused $ windows . W.sink)
 
    -- Increment the number of windows in the master area
    , ((modMask, xK_comma), sendMessage (IncMasterN 1))
 
    -- Deincrement the number of windows in the master area
    , ((modMask, xK_period), sendMessage (IncMasterN (-1)))
 
    -- toggle the status bar gap
    , ((modMask, xK_b), sendMessage ToggleStruts)
 
    -- Restart xmonad
    , ((modMask,  xK_q), broadcastMessage ReleaseResources >> restart "xmonad" True)
    ]

myAdditionalKeysP =
    [
      ("M-<F2>", spawn "thunar")
    , ("M-<F3>", spawn "firefox")
    , ("M-<F4>", spawn "code")
    , ("M-<F5>", spawn "thunderbird")
    , ("M-<Escape>", spawn "xfce4-appfinder")
    , ("M4-<Print>", spawn "xfce4-screenshooter")
    , ("M4-<KP_Add>", spawn "amixer -D pulse sset Master 5%+")
    , ("M4-<KP_Subtract>", spawn "amixer -D pulse sset Master 5%-")
    , ("M-C-p", spawn "passmenu") 
    , ("M-C-c", spawn "clipmenu")
    , ("M-C-m", spawn "mailwatch_restart")
    , ("M-C-x", spawn "xfce4-panel -r")
    , ("M-C-<Left>", spawn "playerctl previous")
    , ("M-C-<Right>", spawn "playerctl next")
    , ("M-C-<Space>", spawn "playerctl play-pause")
    ]

main = do
    xmprocs <- mapM (\i -> spawnPipe $ "xmobar ~/.config/xmobar/xmobarrc-" ++ show i ++ " -x" ++ show i) [0..1]
    xmonad $ docks def
        {
          workspaces = withScreens 2 (map show [1..12])
          , keys = myKeys
          , borderWidth = 2
          , focusedBorderColor = "#226fa5"
          , normalBorderColor = "#191919"
          , handleEventHook = serverModeEventHookCmd
                            <+> serverModeEventHook
                            <+> serverModeEventHookF "XMONAD_PRINT" (io . putStrLn)
          , layoutHook = avoidStruts myLayout
          , logHook = mapM_ dynamicLogWithPP $ zipWith pp xmprocs [0..1]
          , startupHook = setWMName "LG3D"
          , manageHook = manageDocks
        } `additionalKeysP` myAdditionalKeysP

pp h s = marshallPP s def 
    { ppOutput = hPutStrLn h
    , ppCurrent = xmobarColor "blue" "" . wrap "[" "]"
    , ppHiddenNoWindows = xmobarColor "grey" "" . clickable'
    , ppVisible = wrap "(" ")"
    , ppUrgent  = xmobarColor "red" "yellow"
    , ppOrder = \(ws:_:_:_) -> [pad ws]
    , ppHidden = clickable'
    }

I have tried to hard code an ordered list, but it didn't work. I also tried sorting what I pass to workspaces using this function:

sortNumeric = sortBy (compare `on` (read :: String -> Int))

However, that didn't work out as well.

How can I overcome the issue?


Solution

  • You can see that the type signature of mkWsSort is X WorkspaceCompare -> X WorkspaceSort. ppSort needs a X WorkspaceSort, so you just need to provide a X WorkspaceCompare. WorkspaceCompare is an alias for WorkspaceId -> WorkspaceId -> Ordering, and WorkspaceId is an alias for String. So basically, this is just a long-winded path to say you need a string comparison function. You can make one that compares strings by trying to read them into Ints first, then comparing the two:

    import Text.Read
    import Data.Ord
    
    compareNumbers :: String -> String -> Ordering
    compareNumbers a b =
        case (readMaybe a :: Maybe Int, readMaybe b :: Maybe Int) of
            -- if they're both valid numbers then compare them
            (Just x, Just y) -> compare x y
            -- push numbers to the front of strings
            (Just _, Nothing) -> LT
            (Nothing, Just _) -> GT
            -- strings get normal string comparison
            (Nothing, Nothing) -> compare a b
    

    If all your workspaces are numbers then compareNumbers could be reduced to compareNumbers = comparing (read :: String -> Int), or if you want to push non-numbers to the front then it could be reduced to comparing (readMaybe :: String -> Maybe Int).

    Then, you wrap this in the X monad with return, and pass it to mkWsSort then set ppSort to that:

    -- ...
    , ppSort = mkWsSort $ return compareNumbers
    -- ...