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 callswindows (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
- Start XMonad with the supplied configuration file
- Open two terminals on workspace 1, and two terminals on workspace 2 (
M-<Return> M-<Return> M-2 M-<Return> M-<Return>
) - Return to workspace 1 and swap layout to
Full
(M-1 M-<Space>
) - Swap to workspace 2 (
M-2
). Note that workspace 2 is unchanged - Return to workspace 1 (
M-1
) - Swap to workspace two with the alternative keybinding (
M-S-2
). Note that workspace 2 has had its layout changed toFull
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
- With
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!