IntroductionThis article is about three things I'm very interested in. I've been a fan of using real programming languages for configuration files for a long time, but haven't written about that recently. I've been using tiling window managers - now in their dynamic version - for a long time as well, and have written about that. Finally, I've been a fan of Haskell for a while, and have written a number of articles about using it.
XMonad is a dynamic, tiling window manager written in Haskell that uses a Haskell module as a configuration file. This has the usual advantages of doing so - you can put values in variables rather than repeating them, and construct new values with Haskell expressions, etc.
One of the features of XMonad is a
Layout, which controls how windows are tiled on the screen. The core of XMonad provides some basic - but very useful -
Layouts, and there are extensions to do things like creating tabbed stacks of windows, nesting
Layouts control how windows are arranged, they are critical components, and changing them is how you change your window managers behavior. I'm going to look at extending the behavior of one of the core
Tall- in a number of ways.
And a credit. The code here was inspired by Devin Mullins, who provided information and code samples while helping me with my XMonad configuration.
Layoutneeds to be an instance of the
LayoutClasstype class. As such, a
Layoutneeds to do three things: run the layout, handle
Messages from the window manager, and optionally provide a
description. You can find details on that in the API documentation.
descriptionis "a human-readable string used for selecting Layout's." Some tools display them for selection, others use
descriptions to select
Layouts programmatically, say from a list of strings in the configuration. These different uses give rise to different needs, so we'll start by just changing it. This would allow us to have two different
Talllayouts, and tell the difference between them.
First, we need to declare our data type:
data MyTall a = MyTall (Tall a) deriving (Show, Read)
LayoutClass. Running will be forwarded to the wrapped
runLayoutmethod. The same will be done with
Messages by the
pureMessagemethod. I'll get into the details of those later.
instance LayoutClass MyTall a where runLayout (W.Workspace id (MyTall tall) ms) r = fmap (second (fmap MyTall)) $ runLayout (W.Workspace id tall ms) r pureMessage (MyTall tall) m = fmap MyTall $ pureMessage tall m
description _ = "MyTall"
Layouts with different names: one is a regular
Talllayout, and the other a
MyTalllayout. Exactly how you do that will depend on your XMonad config file, but you would just add a
Talllayout and wrap it in a
MyTalllike one of these examples:
MyTall (Tall *...*) MyTall $ Tall *...*
Layout. One of the features of
Tallis a master pane, which holds a programmable number of client windows - typically the one or two you're working on now, with other windows dynamically sized in a second pane. The wrapped
Tallhas the format
Tall n delta frac, where
nis the number of clients in the master pane. We can put that count in the description like so:
description (MyTall (Tall n _ _)) = "Tall " ++ show n
Layouts with different names, but now the second one is distinguished by having the number of client windows in the master pane displayed.
Commands for a
Layoutare described by the
Tallhas two messages, one to change the size of the master pane, and one to change the number of windows in it. I tend to use either one or two windows in the master pane, and would like the ability to toggle between those two states.
Toggling the master paneSo we'll create a new
ToggleMasterto toggle the number of clients in the master pane:
data ToggleMaster = ToggleMaster deriving Typeable instance Message ToggleMaster
pureMessagemethod to handle this
Message. Let's dissect the current version first:
pureMessage (MyTall tall) m = fmap MyTall $ pureMessage tall m
m. It returns a
Maybe (layout a). Forwarding the message is easy - we just call
pureMessageon the wrapped
Tall, extracted by pattern matching in the function. The returned
Maybe Tallneeds to be rewrapped to a
fmap MyTalldoes that for us.
To handle the
Messageourselves, we need to get the actual message from the
fromMessagewill do for us. If that returns
Just ToggleMaster, then we want to handle this
Message. Otherwise, it will return
Nothing, and we pass the message as before. So far we have:
pureMessage (MyTall tall) m = case fromMessage m of Nothing -> fmap MyTall $ pureMessage tall m Just ToggleMaster -> undefined
ToggleMastermessage, we need to return a
Tallhas the new number of client windows we want in the master pane:
pureMessage (MyTall tall@(Tall n delta frac)) m = case fromMessage m of Nothing -> fmap MyTall $ pureMessage tall m Just ToggleMaster -> Just . MyTall $ Tall new delta frac where new = if n /= 1 then 1 else 2
Tall. When we get a
Message, we create the new value
if n /= 1 then 1 else 2. While I usually toggle between 1 and 2 clients, it handles all other cases by going back to 1 as well. To finish this, we create a new
Tallthat we wrap with
Just . MyTall.
We can now bind that in our configuration with:
, ((modm , xK_slash), sendMessage ToggleMaster)
Mod-slashto toggle the master window, which seems to work well with
Mod-period, the defaults for incrementing and decrementing the number of clients in the master pane.
Target TogglesIf you also used three client window regularly, you might want a separate toggle for that. We're going to do that in two different ways.
First, we can simply give ToggleMaster an argument and a name change to match:
data ToggleMasterN = ToggleMasterN !Int deriving Typeable
, ((modm , xK_slash), sendMessage $ ToggleMasterN 2) -- Toggle the master window split 3-way. , ((modm , xK_backslash), sendMessage $ ToggleMasterN 3)
pureMessageto use that argument instead of 2:
Just (ToggleMasterN i) -> Just . MyTall $ Tall new delta frac where new = if n /= 1 then 1 else i
Splits as well.But suppose we wanted a command that always split the master window, no matter what it currently was? Let's call it
SetMasterN, and the code to handle it is pretty simple:
Just (SetMasterN new) -> Just . MyTall $ Tall new delta frac
Messageis similar to
data SetMasterN = SetMasterN !Int deriving Typeable instance Message SetMasterN
SetMasterNto be run in
pureMessage. We're going to refactor
pureMessagea bit to do that:
pureMessage (MyTall tall@(Tall n delta frac)) m = msum [fmap MyTall $ pureMessage tall m, fmap toggle (fromMessage m), fmap set (fromMessage m)] where toggle ToggleMaster = MyTall $ Tall new delta frac where new = if n /= 1 then 1 else 2 set (SetMasterN new) = MyTall $ Tall new delta frac
msumhandles a different set of messages. The first line passes all of them to
Tall, and it will return
Nothingif it doesn't handle that message. Each element after that will pass the appropriate messages to the function that handles them, or be
msumthen returns the
Justvalue from the list.
As a final note, you can access the
Xmonad values if you use
pureMessage. This lets you access the
It's type is
handleMessage :: layout a -> SomeMessage -> X (Maybe (layout a))
fmapcalls to work with the values in the
Layout. Details can be found in the API documentation.
The last set of functions in a
LayoutClassare the ones that generate the rectangles that windows wind up in. At this point, you're really past extending the
Layout, and are writing a new one. But I'm going to look at a simple case anyway.
The oddity of 0One of the odder behaviors of the default
Layoutis that putting all the windows in the master pane and putting none of the windows in the master pane gets the same layout. Both wind up with all windows stretching the width of the screen. It's just that in one, they're in the master pane, and in the other they're in the other pane.
The difference between them is that you can keep increasing the master pane count until you hit
maxBound, but once you decrease it to 0, it won't go any lower. So you can use
IncMasterN minBoundto get to 0 from any value. Once you've got
ToggleMaster, you can use those instead, and use
IncMasterN maxBoundto get to the layout that was at 0. Which means 0 can be used for something else.
A full screen modeGive the above, we can extend
Tallso that putting 0 windows in the master pane makes the first window a full screen window. While I think that this makes as much sense as the current behavior, it makes life a bit difficult if the only message you have is
The layout functions are passed a
Rectangle, and a
Stackof windows, and should return a list of tuples of (window,
Rectangle). The simplest of the layout functions is
pureLayout, which does just that. The value we want to return to get a full screen window is a list with a single tuple consisting of the first window and the initial rectangle:
import XMonad.StackSet as W pureLayout _ r s = map (, r) . take 1 $ W.integrate s
W.integratereturns a list of the windows in the
Stack. While this
pureLayoutshould never be called with an empty list of windows - there's a method specifically for handling that - using
[(head $ W.integrate s, r)]insures that we don't generate an exception should that happen. Instead, we return an empty list, which is the default behavior.
Forwarding the non-zero casesWe want the above code to run when the master pane client count is 0, and otherwise we'll let
Tallhandle it. That's done like so:
pureLayout (MyTall tall@(Tall n _ _)) r s = if n != 0 then pureLayout tall r s else map (, r) . take 1 $ W.integrate s
Layoutand passes that and the non-
Layoutarguments along to the
And the rest
pureLayoutis the simplest of the layout functions. If your layout function isn't quite so pure, you can use
doLayoutreturns a value in the
Xmonad, so you can access the
XConfvalues. The value it returns is a tuple consisting of the list returned by
Layoutof the same type that was passed in. The
Layoutto be modified, ala the
While those two are normally sufficient, there's also
runLayout. Both of these return the same type as
doLayout. The default implementation of
emptyLayoutif there are no windows and
doLayoutotherwise. You should only need these if you want special handling for the case where there are no windows. Details can be found in the API documentation.