Search code examples
haskellxmonad

Use IO when creating Xmonad configuration (keymap depends on number of connected monitors)


I'm trying to set up different Xmonad key mappings depending on the number of connected monitors. The reason is that I use the same Xmonad config file on multiple systems (desktops, a laptop with different monitor configurations including 3 displays). Displays are listed in a different order on different systems, that's why I need to hardcode display indices when using a 3 monitor setup.

My current best try is something like that (everything that is not relevant has been removed):

import qualified Graphics.X11.Xlib as X11
import qualified Graphics.X11.Xinerama as X11

screenKeysFor2Monitors conf@(XConfig {XMonad.modMask = modMask}) = M.fromList $
  [((m .|. mod4Mask, key), screenWorkspace sc >>= flip whenJust (windows . f)) -- Replace 'mod1Mask' with your mod key of choice.
      | (key, sc) <- zip [xK_w, xK_e] [0, 1] -- Usual screen order
      , (f, m) <- [(W.view, 0), (W.shift, shiftMask), (W.greedyView, mod1Mask)]]

screenKeysFor3Monitors conf@(XConfig {XMonad.modMask = modMask}) = M.fromList $
  [((m .|. mod4Mask, key), screenWorkspace sc >>= flip whenJust (windows . f)) -- Replace 'mod1Mask' with your mod key of choice.
      | (key, sc) <- zip [xK_w, xK_e, xK_q] [0, 2, 1] -- hardcoded according to laptop driver
      , (f, m) <- [(W.view, 0), (W.shift, shiftMask), (W.greedyView, mod1Mask)]]


screenKeys x = do
        numberOfScreens <- getScreens
        keyConfig <- case numberOfScreens of
                        3 -> screenKeysFor3Monitors x
                        _ -> screenKeysFor2Monitors x
        return keyConfig

-- | Get number of screens
getScreens = do
  screens <- do
    dpy <- X11.openDisplay ""
    rects <- X11.getScreenInfo dpy
    X11.closeDisplay dpy
    return rects
  pure $ length screens

xmonadConfig = ewmh xfceConfig{
          modMask = mod4Mask
          keys = MyKeys.screenKeys
}

I get this error

Error detected while loading xmonad configuration file: /home/me/.xmonad/xmonad.hs

lib/MyXMonad/Keys.hs:51:64: error:
    * Couldn't match expected type `M.Map (KeyMask, KeySym) (X ())`
                  with actual type `IO (X ())`
    * In the expression: (screenKeys x)
      In the second argument of `($)`, namely
        `[(myKeysToAdd x), (workspaceKeys x), (screenKeys x)]`
      In the expression:
        M.unions $ [(myKeysToAdd x), (workspaceKeys x), (screenKeys x)]
   |
51 | keysToAdd x = M.unions $ [(myKeysToAdd x), (workspaceKeys x), (screenKeys x)]
   |                                                                ^^^^^^^^^^^^

lib/MyXMonad/Keys.hs:242:30: error:
    * Couldn't match type `M.Map (KeyMask, KeySym)` with `IO`
      Expected type: IO (X ())
        Actual type: M.Map (KeyMask, KeySym) (X ())
    * In the expression: screenKeysFor3Monitors x
      In a case alternative: 3 -> screenKeysFor3Monitors x
      In a stmt of a 'do' block:
        keyConfig <- case numberOfScreens of
                       3 -> screenKeysFor3Monitors x
                       _ -> screenKeysFor2Monitors x
    |
242 |                         3 -> screenKeysFor3Monitors x
    |                              ^^^^^^^^^^^^^^^^^^^^^^^^

lib/MyXMonad/Keys.hs:243:30: error:
    * Couldn't match type `M.Map (KeyMask, KeySym)` with `IO`
      Expected type: IO (X ())
        Actual type: M.Map (KeyMask, KeySym) (X ())
    * In the expression: screenKeysFor2Monitors x
      In a case alternative: _ -> screenKeysFor2Monitors x
      In a stmt of a 'do' block:
        keyConfig <- case numberOfScreens of
                       3 -> screenKeysFor3Monitors x
                       _ -> screenKeysFor2Monitors x
    |
243 |                         _ -> screenKeysFor2Monitors x
    |                              ^^^^^^^^^^^^^^^^^^^^^^^^

Please check the file for errors.

If I understand correctly, the problem here is that my code depends on side effects (working with monitor configuration uses IO monad) and becomes non-pure. I can convert IO monad to X monad using liftIO. But the X monad is accessible only inside key binding handlers. The code that creates key bindings for Xmonad configuration has to be pure, and X monad is not expected here.

In other words, if I get the situation right, it's not possible to define key bindings using non-pure functions (e.g. by looking on connected monitors). Maybe there is some workaround? I lack a decent understanding of Haskell and maybe I'm missing something obvious for regular Haskell programmers.


Solution

  • not too familiar with Xmonad but you can easily do the following I guess. create a pure function mkConfig which takes the number of screens and returns the desired key mapping. Then, in your main pass it to xmonad function. I haven't tried to compile any of this but probably you can modify it easily

    mkConfig numberOfScreens  = -- Notice that this is a pure function
     case numberOfScreens of
       3 -> screenKeysFor3Monitors x
       _ -> screenKeysFor2Monitors x
    
    
    main :: IO ()
    main = do
     numberOfScreens <- getScreens                                              -- Retrive the number of screens from the system
     let keyConfig = mkConfig numberOfScreens                                   -- Makes a key mapping out of this
         xmonadConfig = ewmh xfceConfig{ modMask = mod4Mask, keys = keyConfig } -- Creates a Xmonad configuration
    
     xmonad xmonadConfig  -- Launch Xmonad.