aboutsummaryrefslogtreecommitdiffstats
path: root/XMonad/Actions/Plane.hs
blob: 46684bf42ab9639b95e61d4a953553662aefc376 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
-----------------------------------------------------------------------------
-- |
-- Module      :  XMonad.Actions.Plane
-- Copyright   :  (c) Marco Túlio Gontijo e Silva <marcot@riseup.net>,
--                    Leonardo Serra <leoserra@minaslivre.org>
-- License     :  BSD3-style (see LICENSE)
--
-- Maintainer  :  Marco Túlio Gontijo e Silva <marcot@riseup.net>
-- Stability   :  unstable
-- Portability :  unportable
--
-- This module has functions to navigate through workspaces in a bidimensional
-- manner.  It allows the organization of workspaces in lines, and provides
-- functions to move and shift windows in all four directions (left, up, right
-- and down) possible in a surface.
--
-- This functionality was inspired by GNOME (finite) and KDE (infinite)
-- keybindings for workspace navigation, and by "XMonad.Actions.CycleWS" for
-- the idea of applying this approach to XMonad.
-----------------------------------------------------------------------------

module XMonad.Actions.Plane
    (
    -- * Usage
    -- $usage

    -- * Data types
    Direction (..)
    , Limits (..)
    , Lines (..)

    -- * Key bindings
    , planeKeys

    -- * Navigating through workspaces
    , planeShift
    , planeMove
    )
    where

import Control.Monad
import Data.List
import Data.Map hiding (split)
import Data.Maybe

import XMonad
import XMonad.StackSet hiding (workspaces)
import XMonad.Util.Run

-- $usage
-- You can use this module with the following in your @~\/.xmonad\/xmonad.hs@ file:
--
-- > import XMonad.Actions.Plane
-- > import Data.Map (union)
-- >
-- > main = xmonad def {keys = myKeys}
-- >
-- > myKeys conf = union (keys def conf) $ myNewKeys conf
-- >
-- > myNewKeys (XConfig {modMask = modm}) = planeKeys modm (Lines 3) Finite
--
-- For detailed instructions on editing your key bindings, see
-- "XMonad.Doc.Extending#Editing_key_bindings".

-- | Direction to go in the plane.
data Direction =  ToLeft | ToUp | ToRight | ToDown deriving Enum

-- | Defines the behaviour when you're trying to move out of the limits.
data Limits
    = Finite   -- ^ Ignore the function call, and keep in the same workspace.
    | Circular -- ^ Get on the other side, like in the Snake game.
    | Linear   -- ^ The plan comes as a row, so it goes to the next or prev if
               -- the workspaces were numbered.
    deriving Eq

-- | The number of lines in which the workspaces will be arranged.  It's
-- possible to use a number of lines that is not a divisor of the number of
-- workspaces, but the results are better when using a divisor.  If it's not a
-- divisor, the last line will have the remaining workspaces.
data Lines
    = GConf     -- ^ Use @gconftool-2@ to find out the number of lines.
    | Lines Int -- ^ Specify the number of lines explicitly.

-- | This is the way most people would like to use this module.  It attaches the
-- 'KeyMask' passed as a parameter with 'xK_Left', 'xK_Up', 'xK_Right' and
-- 'xK_Down', associating it with 'planeMove' to the corresponding 'Direction'.
-- It also associates these bindings with 'shiftMask' to 'planeShift'.
planeKeys :: KeyMask -> Lines -> Limits -> Map (KeyMask, KeySym) (X ())
planeKeys modm ln limits =
  fromList $
  [ ((keyMask, keySym), function ln limits direction)
  | (keySym, direction) <- zip [xK_Left .. xK_Down] $ enumFrom ToLeft
  , (keyMask, function) <- [(modm, planeMove), (shiftMask .|. modm, planeShift)]
  ]

-- | Shift a window to the next workspace in 'Direction'.  Note that this will
-- also move to the next workspace.  It's a good idea to use the same 'Lines'
-- and 'Limits' for all the bindings.
planeShift :: Lines -> Limits -> Direction -> X ()
planeShift = plane shift'

shift' ::
    (Eq s, Eq i, Ord a) => i -> StackSet i l a s sd -> StackSet i l a s sd
shift' area = greedyView area . shift area

-- | Move to the next workspace in 'Direction'.
planeMove :: Lines -> Limits -> Direction -> X ()
planeMove = plane greedyView

plane ::
    (WorkspaceId -> WindowSet -> WindowSet) -> Lines -> Limits -> Direction ->
    X ()
plane function numberLines_ limits direction = do
    st <- get
    xconf <- ask

    numberLines <-
        liftIO $
        case numberLines_ of
            Lines numberLines__ ->
                return numberLines__
            GConf               ->
                do
                    numberLines__ <-
                        runProcessWithInput gconftool parameters ""
                    case reads numberLines__ of
                        [(numberRead, _)] -> return numberRead
                        _                 ->
                            do
                                trace $
                                    "XMonad.Actions.Plane: Could not parse the output of " ++ gconftool ++
                                    unwords parameters ++ ": " ++ numberLines__ ++ "; assuming 1."
                                return 1

    let
        notBorder :: Bool
        notBorder = (replicate 2 (circular_ < currentWS) ++ replicate 2 (circular_ > currentWS)) !! fromEnum direction

        circular_ :: Int
        circular_ = circular currentWS

        circular :: Int -> Int
        circular =
            [ onLine   pred
            , onColumn pred
            , onLine   succ
            , onColumn succ
            ]
            !! fromEnum direction

        linear :: Int -> Int
        linear =
            [ onLine   pred . onColumn pred
            , onColumn pred . onLine pred
            , onLine   succ . onColumn succ
            , onColumn succ . onLine succ
            ]
            !! fromEnum direction

        onLine :: (Int -> Int) -> Int -> Int
        onLine f currentWS_
            | line < areasLine = mod_ columns
            | otherwise        = mod_ areasColumn
            where
                line, column :: Int
                (line, column) = split currentWS_

                mod_ :: Int -> Int
                mod_ columns_ = compose line $ mod (f column) columns_

        onColumn :: (Int -> Int) -> Int -> Int
        onColumn f currentWS_
            | column < areasColumn || areasColumn == 0  = mod_ numberLines
            | otherwise                                 = mod_ $ pred numberLines
            where
                line, column :: Int
                (line, column) = split currentWS_

                mod_ :: Int -> Int
                mod_ lines_ = compose (mod (f line) lines_) column

        compose :: Int -> Int -> Int
        compose line column = line * columns + column

        split :: Int -> (Int, Int)
        split currentWS_ =
            (operation div, operation mod)
            where
                operation :: (Int -> Int -> Int) -> Int
                operation f = f currentWS_ columns

        areasLine :: Int
        areasLine = div areas columns

        areasColumn :: Int
        areasColumn = mod areas columns

        columns :: Int
        columns =
            if mod areas numberLines == 0 then preColumns else preColumns + 1

        currentWS :: Int
        currentWS = fromJust mCurrentWS

        preColumns :: Int
        preColumns = div areas numberLines

        mCurrentWS :: Maybe Int
        mCurrentWS = elemIndex (currentTag $ windowset st) areaNames

        areas :: Int
        areas = length areaNames

        run :: (Int -> Int) -> X ()
        run f = windows $ function $ areaNames !! f currentWS

        areaNames :: [String]
        areaNames = workspaces $ config xconf

    when (isJust mCurrentWS) $
        case limits of
        Finite   -> when notBorder $ run circular
        Circular -> run circular
        Linear -> if notBorder then run circular else run linear

gconftool :: String
gconftool = "gconftool-2"

parameters :: [String]
parameters = ["--get", "/apps/panel/applets/workspace_switcher_screen0/prefs/num_rows"]