xmonad / xmonad-contrib

Contributed modules for xmonad

Home Page:https://xmonad.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Using 'windows' within 'handleMess' has strange behaviour some conditions

Quelklef opened this issue · comments

Problem Description

If:

  • we define a custom layout and give it a handleMess method
  • the handleMess implementation calls windows (W.greedyView someWorkspaceId)
  • handleMess then returns a non-Nothing value

then:

  • the workspace we swap to will wrongly inherit the selected layout of the workspace we are swapping from

Steps to Reproduce

  1. Start XMonad with the supplied configuration file
  2. Open two terminals on workspace 1, and two terminals on workspace 2 (M-<Return> M-<Return> M-2 M-<Return> M-<Return>)
  3. Return to workspace 1 and swap layout to Full (M-1 M-<Space>)
  4. Swap to workspace 2 (M-2). Note that workspace 2 is unchanged
  5. Return to workspace 1 (M-1)
  6. Swap to workspace two with the alternative keybinding (M-S-2). Note that workspace 2 has had its layout changed to Full as well!

Configuration File

Please include the smallest full configuration file that reproduces
the problem you are experiencing:

{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE MultiParamTypeClasses      #-}

import           Control.Monad.Writer               (Writer, execWriter, tell)
import           Data.Foldable                      (for_)
import           Data.Map                           (Map)
import qualified Data.Map                           as Map
import           XMonad
import qualified XMonad.Layout.BinarySpacePartition as LBSP
import           XMonad.Layout.LayoutModifier       (LayoutModifier (handleMess),
                                                     ModifiedLayout (..))
import qualified XMonad.StackSet                    as W

main :: IO ()
main = xmonad myConfig
  where
  myConfig = def
    { modMask = mod4Mask  -- super
    , terminal = "alacritty"
    , keys = myKeys
    , workspaces = show <$> [1..9]
    , layoutHook = myLayout (LBSP.emptyBSP ||| Full)
    }

myKeys :: XConfig Layout -> Map (KeyMask, KeySym) (X ())
myKeys conf@(XConfig { XMonad.modMask = mod }) =
  execWriter $ do
    -- launch terminal
    bind mod xK_Return $ spawn (XMonad.terminal conf)
    -- rotate layout
    bind mod xK_space $ sendMessage NextLayout
    -- move up/down window stack (n for next)
    bind mod                   xK_n   $ windows W.focusDown
    bind (mod .|. controlMask) xK_n $ windows W.focusUp
    -- workspaces
    for_ (zip (XMonad.workspaces conf) [xK_1 .. xK_9]) $ \(workspace, key) -> do
      bind mod                 key (sendMessage $ MoveTo_Working workspace)
      bind (mod .|. shiftMask) key (sendMessage $ MoveTo_Broken workspace)

  where
  bind :: KeyMask -> KeySym -> X () -> Writer (Map (KeyMask, KeySym) (X ())) ()
  bind mask key act = tell $ Map.singleton (mask, key) act

data MyMessage
  = MoveTo_Working WorkspaceId
  | MoveTo_Broken WorkspaceId

instance Message MyMessage

data MyState a = MyState
  deriving (Show, Read)

myLayout :: l a -> ModifiedLayout MyState l a
myLayout = ModifiedLayout MyState

instance LayoutModifier MyState a where
  handleMess _ msg =
    case fromMessage msg of

      Nothing -> pure Nothing

      Just (MoveTo_Working ws) -> do
        windows (W.greedyView ws)
        pure Nothing

      Just (MoveTo_Broken ws) -> do
        windows (W.greedyView ws)
        pure (Just MyState)

Checklist

  • I've read CONTRIBUTING.md

  • I tested my configuration

    • With xmonad version 0.17.0
    • With xmonad-contrib version 0.17.0

Thanks for the nice report and example! It's a cool find. Didn't see the keys defined as a Writer Monad before, cool trick.

As for the weird behavior, in my opinion, this is a bit of an abuse of the X monad. This is my understanding of what's happening here: the handleMessage implementation for the LayoutClass instance of layout modifiers, found here, returns the layout that was active when the message was received, modified depending to the result of handleMessOrMaybeModifyIt (which just calls handleMess in the default implementation). As specified in LayoutClass, the layout that is returned by handleMessage is applied to the active workspace. In your case, since the original layout is taken from workspace 1 and by the end of handleMessage workspace 2 is active, then the modified layout from workspace 1 is used for workspace 2 (and the layout of workspace 1 won't change). I think one should be able to create the same behavior with normal layouts, using a layout modifier isn't necessary.

Changing this behavior is almost certainly a breaking change: I think this is a core change: remember the workspace that was active before the message is handled and apply the new layout to that workspace (disclaimer: I haven't looked at that part of the code yet). I'm not sure whether it's really an issue or just a quirky feature.

Anyways, this was just a fun and shallow 15 minutes dive into the code, let's see what others think.

I personally consider any use of windows in a handleMessage to be risky business, to be honest. If it breaks, you get to keep the pieces. 😄 (It's admittedly not as risky as doing so in a layout method proper; that absolutely would break things, since those are called from inside windows.)

Thank you both for the responses! It's sounding like this is an XY problem and I shouldn't be using layouts to do this in the first place 😅! I was able to re-implement my feature using ExtensibleConfig and ExtensibleState instead of layouts. I'll leave the issue open since it sounds like it still might be valuable to have some discussion about?

Anyone can reopen if they think this needs further discussion. Thank you all!