aboutsummaryrefslogtreecommitdiff
path: root/src/Rahm/Desktop/WorkspaceWheel.hs
blob: a89dc6a3ca3e2b77b19a35e312c725853522b581 (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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
{-# LANGUAGE MultiWayIf #-}

module Rahm.Desktop.WorkspaceWheel
  ( displayWorkspaceWheel,
    displayWorkspaceWheelAtPoint,
    displayWorkspaceWheelInCenter,
  )
where

import Control.Arrow (Arrow (first, second))
import Control.Concurrent (threadDelay)
import Control.Monad (forM_)
import Control.Monad.Trans.Maybe (MaybeT (runMaybeT))
import Data.Char (toLower)
import Data.Function (fix)
import Data.List (find, sortBy)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe, isNothing)
import Data.Monoid (Endo (Endo, appEndo))
import Data.Ord (comparing)
import Data.Word
import qualified Graphics.X11 as X
import Rahm.Desktop.Common (click, gotoWorkspace, pointerLocation, pointerScreen)
import Rahm.Desktop.Keys.Wml (getAndResetWindowSelection, getAndResetWindowSelectionOrCurrent)
import Rahm.Desktop.Layout.PinWindow (withWindowsUnpinned)
import Rahm.Desktop.Marking (setAlternateWindows, setAlternateWorkspace)
import Rahm.Desktop.StackSet as W
import Rahm.Desktop.Submap (ButtonOrKeyEvent (ButtonPress, KeyPress), nextButton, nextButtonOrKeyEvent)
import System.IO
import Text.Printf (printf)
import XMonad
import qualified XMonad.Operations as X
import XMonad.Util.Run (spawnPipe)

dzenSize :: (Integral a) => a
dzenSize = 100

data ScreenType = Current | Visible | Hidden deriving (Eq)

data WorkspaceWheelType = Existing | Alphabet Bool Bool | Digits

displayWorkspaceWheel :: X ()
displayWorkspaceWheel =
  uncurry (displayWorkspaceWheelAtPoint workspaceWheelTypeRing)
    =<< pointerLocation

data Ring a = Ring Int [a] (Maybe a) [a]

workspaceWheelTypeRing :: Ring WorkspaceWheelType
workspaceWheelTypeRing =
  ringAdv $
    mkRing
      [ Existing,
        Alphabet False False,
        Alphabet False True,
        Alphabet True False,
        Alphabet True True,
        Digits
      ]

mkRing :: [a] -> Ring a
mkRing lst =
  let as = cycle lst
      rev = cycle (reverse lst)
   in Ring (length lst) rev Nothing as

ringCur :: Ring a -> Maybe a
ringCur (Ring _ _ a _) = a

ringAdv :: Ring a -> Ring a
ringAdv (Ring s p Nothing (n : ns)) = Ring s p (Just n) ns
ringAdv (Ring s p (Just a) (n : ns)) = Ring s (a : p) (Just n) ns

ringPrev :: Ring a -> Ring a
ringPrev (Ring s (p : ps) Nothing ns) = Ring s ps (Just p) ns
ringPrev (Ring s (p : ps) (Just a) ns) = Ring s ps (Just p) (a : ns)

ringFastForwardTo :: (a -> Bool) -> Ring a -> Ring a
ringFastForwardTo fn ring@(Ring s _ a _) =
  case ringFastForwardTo' fn s ring of
    ring'@(Ring s' p' (Just a') n') | isNothing a -> Ring s' (a' : p') Nothing n'
    ring' -> ring'
  where
    ringFastForwardTo' _ 0 ring = ring
    ringFastForwardTo' fn _ ring@(Ring _ _ (Just a) _) | fn a = ring
    ringFastForwardTo' fn n ring = ringFastForwardTo' fn (n - 1) (ringAdv ring)

displayWorkspaceWheelInCenter :: X ()
displayWorkspaceWheelInCenter =
  mapM_
    ( \(W.screenDetail -> (SD (Rectangle x y w h))) ->
        displayWorkspaceWheelAtPoint
          workspaceWheelTypeRing
          (fromIntegral $ x + (fromIntegral w `div` 2))
          (fromIntegral $ y + (fromIntegral h `div` 2))
    )
    =<< pointerScreen

displayWorkspaceWheelAtPoint :: Ring WorkspaceWheelType -> Int -> Int -> X ()
displayWorkspaceWheelAtPoint wheelType px py = do
  click

  (W.StackSet cur vis other _floating) <- gets windowset
  screen <- fromMaybe cur <$> pointerScreen

  allWorkspaces <- getWorkspaces (fromMaybe Existing $ ringCur wheelType)

  let (SD rect) = W.screenDetail screen
      rectsToWorkspaces = calculatePositions px py rect allWorkspaces
  -- rectsToWorkspaces = map (second (first W.tag)) dzenRects

  dzenPipes <- mapM (uncurry launchDzen) rectsToWorkspaces

  after <-
    ( fix $ \recur ring -> do
        evt <- runMaybeT nextButtonOrKeyEvent
        let stop = return ()
        let retry = recur ring

        case evt of
          Nothing -> return stop -- timeout
          Just (ButtonPress _ b)
            | b == button4 ->
                return (displayWorkspaceWheelAtPoint (ringAdv wheelType) px py)
            | b == button5 ->
                return (displayWorkspaceWheelAtPoint (ringPrev wheelType) px py)
          Just (ButtonPress _ b) ->
            withDisplay $ \d -> do
              (_, _, _, fi -> cx, fi -> cy, _, _, _) <-
                io $ X.queryPointer d =<< rootWindow d 0
              case find (\(r, _) -> pointWithin cx cy r) rectsToWorkspaces of
                Just (_, (ws, _)) ->
                  if
                    | b == button1 -> do
                        gotoWorkspace ws
                        return (displayWorkspaceWheelAtPoint wheelType px py)
                    | b == button3 -> do
                        selection <- getAndResetWindowSelectionOrCurrent
                        withWindowsUnpinned selection $
                          windows $
                            appEndo $
                              mconcat (map (Endo . W.shiftWin ws) selection)

                        return (displayWorkspaceWheelAtPoint wheelType px py)
                    | b == 13 -> do
                        winSet <- gets windowset
                        let mWs = find ((== ws) . W.tag) (W.workspaces winSet)
                        forM_ mWs $ \ws -> do
                          let allWindows = W.integrate' (W.stack ws)
                          setAlternateWindows allWindows
                          mapM_ (`setAlternateWorkspace` W.tag ws) allWindows
                          mapM_ killWindow allWindows

                          -- Preemptively delete the windows so we show the updates
                          -- immediately.
                          windows $
                            appEndo $
                              mconcat $
                                map (Endo . W.delete) allWindows

                        X.refresh
                        return (displayWorkspaceWheelAtPoint wheelType px py)
                    | otherwise -> return stop
                Nothing -> return stop
          Just (KeyPress _ sym _ _) -> do
            if
              | sym == xK_l -> do
                  mapM_ deselect (ringCur ring)
                  let ring' = ringAdv ring
                  mapM_ select (ringCur ring')
                  recur ring'
              | sym == xK_h -> do
                  mapM_ deselect (ringCur ring)
                  let ring' = ringPrev ring
                  mapM_ select (ringCur ring')
                  recur ring'
              | sym == xK_space || sym == xK_Return -> do
                  mapM_
                    (\(_, (_, (ws, _))) -> gotoWorkspace ws)
                    (ringCur ring)
                  return stop
              | otherwise -> return stop
      )
      ( ringFastForwardTo (\(_, (_, (_, n))) -> n == Current) $
          mkRing $
            zip dzenPipes rectsToWorkspaces
      )

  io $ do
    mapM_ hClose dzenPipes

  after
  where
    fi :: (Integral a, Num b) => a -> b
    fi = fromIntegral

    toInt :: (Integral a) => a -> Int
    toInt = fromIntegral

    deselect :: (Handle, (Rectangle, (String, ScreenType))) -> X ()
    deselect (pipe, (rect, (wsName, screenType))) = io $ hPutStrLn pipe wsName

    select :: (Handle, (Rectangle, (String, ScreenType))) -> X ()
    select (pipe, (rect, (wsName, screenType))) =
      io $ hPutStrLn pipe $ printf "^fg(#ff0)%s" wsName

    getWorkspaces :: WorkspaceWheelType -> X [(WorkspaceId, ScreenType)]
    getWorkspaces Existing = do
      (W.StackSet cur vis other _floating) <- gets windowset
      return $
        map (first W.tag) $
          sortBy (comparing (W.tag . fst)) $
            ((W.workspace cur, Current) : map ((,Visible) . W.workspace) vis)
              ++ filter
                (\(w, _) -> (not . null . W.integrate' . W.stack) w && W.tag w /= "*")
                (map (,Hidden) other)
    getWorkspaces (Alphabet isGreek b) =
      getWorkspacesForAlphabet
        ( map (if b then id else toLower) $
            if isGreek then "ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ" else "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        )

    getWorkspaces Digits =
      getWorkspacesForAlphabet "0123456789"

    getWorkspacesForAlphabet a = do
      (W.StackSet cur vis other _floating) <- gets windowset
      let screenTypeById =
            Map.fromList $
              map (first (W.tag . W.workspace)) $
                (cur, Current) : map (,Visible) vis
      return $
        map
          (\c -> ([c], fromMaybe Hidden $ Map.lookup [c] screenTypeById))
          a

    -- Returns, starting x, dzen width
    calculatePositions :: Int -> Int -> Rectangle -> [a] -> [(Rectangle, a)]
    calculatePositions
      pointerX'
      pointerY'
      (Rectangle (toInt -> x) (toInt -> y) (toInt -> w) (toInt -> h))
      as =
        let radius :: (Num a) => a
            radius = 300

            nas = max 2 (length as)
            dPhi = 2 * pi / fromIntegral nas

            circDiff = sqrt $ (radius * cos dPhi - radius) ** 2 + (radius * sin dPhi) ** 2

            size :: (Integral a) => a
            size = round $ min 100 (circDiff * 0.5)

            sizeD2 :: (Integral a) => a
            sizeD2 = fi size `div` 2

            pointerX =
              max (radius + sizeD2 + x) $
                min pointerX' (x + w - radius - sizeD2)

            pointerY =
              max (radius + sizeD2 + y) $
                min pointerY' (y + h - radius - sizeD2)
         in zipWith
              ( \phi a ->
                  let xpos = radius * cos phi + fi pointerX
                      ypos = radius * sin phi + fi pointerY
                      r =
                        Rectangle
                          (round xpos - sizeD2)
                          (round ypos - sizeD2)
                          (fi size)
                          (fi size)
                   in (r, a)
              )
              [3 * pi / 2, 3 * pi / 2 + dPhi ..]
              as

    embedRectangle :: Rectangle -> Int -> Int -> Rectangle
    embedRectangle r@(Rectangle x y w h) w' h' | w' > fi w || h' > fi h = r
    embedRectangle (Rectangle x y w h) w' h' =
      Rectangle
        (x + fi (w `div` 2) - fi (w' `div` 2))
        (y + fi (h `div` 2) - fi (h' `div` 2))
        (fi w')
        (fi h')

    launchDzen :: Rectangle -> (String, ScreenType) -> X Handle
    launchDzen (Rectangle x y w h) (wsTag, screenType) = do
      pipe <-
        spawnPipe
          ( printf
              "dzen2 -fn \"Monofur Nerd Font:size=15\" -x %d -y %d -w %d -h %d -bg '#222' -fg '%s'"
              x
              y
              w
              h
              ( case screenType of
                  Current -> "#ff8888"
                  Visible -> "#8888ff"
                  Hidden -> "#aaa"
              )
          )

      io $ do
        hPutStrLn pipe wsTag
        hFlush pipe

      return pipe