Introduction
This 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 - Layout
s, and there are extensions to do things like creating tabbed stacks of windows, nesting Layout
s, etc.Since
Layout
s 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 Layout
s - 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.
LayoutClass
A Layout
needs to be an instance of the LayoutClass
type class. As such, aLayout
needs to do three things: run the layout, handle Message
s from the window manager, and optionally provide a description
. You can find details on that in the API documentation.
Different description
description
is "a human-readable string used for selecting Layout's." Some tools display them for selection, others use description
s to select Layout
s 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 Tall
layouts, 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 Tall
with the runLayout
method. The same will be done with Message
s by the pureMessage
method. 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"
Tall
Layout
s with different names: one is a regular Tall
layout, and the other a MyTall
layout. Exactly how you do that will depend on your XMonad config file, but you would just add a Tall
layout and wrap it in a MyTall
like one of these examples:MyTall (Tall *...*)
MyTall $ Tall *...*
Layout
. One of the features of Tall
is 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 Tall
has the format Tall n delta frac
, where n
is 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
Tall
Layout
s with different names, but now the second one is distinguished by having the number of client windows in the master pane displayed.
More Message
s
Commands for a Layout
are described by the Message
type class. Tall
has 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 pane
So we'll create a newMessage
, ToggleMaster
to toggle the number of clients in the master pane:data ToggleMaster = ToggleMaster deriving Typeable
instance Message ToggleMaster
pureMessage
method to handle this Message
. Let's dissect the current version first:pureMessage (MyTall tall) m = fmap MyTall $ pureMessage tall m
pureMessage
gets a MyTall
Layout
and a SomeMessage
, m
. It returns a Maybe (layout a)
. Forwarding the message is easy - we just call pureMessage
on the wrapped Tall
, extracted by pattern matching in the function. The returned Maybe Tall
needs to be rewrapped to a Maybe MyTall
. fmap MyTall
does that for us.To handle the
Message
ourselves, we need to get the actual message from the SomeMessage
, which fromMessage
will 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
ToggleMaster
message, we need to return a MyTall Tall
where Tall
has 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 ToggleMaster
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 Tall
that we wrap with Just . MyTall
.
Binding ToggleMaster
We can now bind that in our configuration with: , ((modm , xK_slash), sendMessage ToggleMaster)
Mod-slash
to toggle the master window, which seems to work well with Mod-comma
and Mod-period
, the defaults for incrementing and decrementing the number of clients in the master pane.Target Toggles
If 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.
Extending ToggleMaster
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)
pureMessage
to 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 itSetMasterN
, and the code to handle it is pretty simple: Just (SetMasterN new) -> Just . MyTall $ Tall new delta frac
Message
is similar to ToggleMasterN
:data SetMasterN = SetMasterN !Int deriving Typeable
instance Message SetMasterN
SetMasterN
to be run in pureMessage
. We're going to refactor pureMessage
a 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
msum
handles a different set of messages. The first line passes all of them to Tall
, and it will return Nothing
if it doesn't handle that message. Each element after that will pass the appropriate messages to the function that handles them, or be Nothing
. msum
then returns the Just
value from the list.
handleMessage
As a final note, you can access the X
monad values if you use handleMessage
instead of pureMessage
. This lets you access the XConf
and XState
values via ask
and get
.It's type is
handleMessage :: layout a -> SomeMessage -> X (Maybe (layout a))
fmap
calls to work with the values in the Layout
. Details can be found in the API documentation.
Extending the Layout
The last set of functions in a LayoutClass
are 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 0
One of the odder behaviors of the defaultTall
Layout
is 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 minBound
to get to 0 from any value. Once you've got SetMasterN
or ToggleMaster
, you can use those instead, and use IncMasterN maxBound
to get to the layout that was at 0. Which means 0 can be used for something else.A full screen mode
Give the above, we can extendTall
so 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 IncMasterN
.The layout functions are passed a
Rectangle
, and a Stack
of 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.integrate
returns a list of the windows in the Stack
. While this pureLayout
should never be called with an empty list of windows - there's a method specifically for handling that - using map
and take
instead of [(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 cases
We want the above code to run when the master pane client count is 0, and otherwise we'll letTall
handle 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
Tall
Layout
and passes that and the non-Layout
arguments along to the Tall
method.And the rest
pureLayout
is the simplest of the layout functions. If your layout function isn't quite so pure, you can use doLayout
. doLayout
returns a value in the X
monad, so you can access the XState
and XConf
values. The value it returns is a tuple consisting of the list returned by pureLayout
and a Maybe
Layout
of the same type that was passed in. The Maybe
Layout
allows the Layout
to be modified, ala the Message
handling examples.While those two are normally sufficient, there's also
emptyLayout
and runLayout
. Both of these return the same type as doLayout
. The default implementation of runLayout
calls emptyLayout
if there are no windows and doLayout
otherwise. 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.
No comments:
Post a Comment