Custom Search

Thursday, October 16, 2014

Extending the behavior of XMonad Layouts


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 - Layouts, and there are extensions to do things like creating tabbed stacks of windows, nesting Layouts, etc.
Since 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 Layouts - 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.


A Layout needs to be an instance of the LayoutClass type class. As such, aLayout needs 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.

Different description

description is "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 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)
Now we need to make this an instance of LayoutClass. Running will be forwarded to the wrapped Tall with the runLayout method. The same will be done with Messages 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
And the critical part is to change the description:
  description _ = "MyTall"
So now we can create two different Tall Layouts 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 *...*
Instead of just displaying the name, we could display information from the 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
We can still have two Tall Layouts with different names, but now the second one is distinguished by having the number of client windows in the master pane displayed.

More Messages

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 new Message, ToggleMaster to toggle the number of clients in the master pane:
data ToggleMaster = ToggleMaster deriving Typeable
instance Message ToggleMaster
Now, we need to change the existing 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
To handle the 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
This uses pattern matching to get the values in the 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)
This uses 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
And the changed bindings, including using backslash for the 3-way split:
    , ((modm              , xK_slash), sendMessage $ ToggleMasterN 2)

    -- Toggle the master window split 3-way.
    , ((modm              , xK_backslash), sendMessage $ ToggleMasterN 3)
And then we change the handling in 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 it SetMasterN, and the code to handle it is pretty simple:
      Just (SetMasterN new) -> Just . MyTall $ Tall new delta frac
I'll omit the bindings. The new Message is similar to ToggleMasterN:
data SetMasterN = SetMasterN !Int deriving Typeable
instance Message SetMasterN
So all we have to do is get the code for 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
Each element of the list passed to 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.


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))
so requires another level of 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 default Tall 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 extend Tall 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
The function 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 let Tall 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
Forwarding simply unwraps the 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