aboutsummaryrefslogtreecommitdiff
path: root/src/Rahm/Desktop/WorkspaceWheel.hs
blob: f841bfb14f2ccaafc248c26755f589709c3af242 (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
{-# 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.Function (fix)
import Data.List (find, sortBy)
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)

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

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

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
          (fromIntegral $ x + (fromIntegral w `div` 2))
          (fromIntegral $ y + (fromIntegral h `div` 2))
    )
    =<< pointerScreen

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

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

  let allWorkspaces :: [(W.Workspace WorkspaceId (Layout Window) Window, ScreenType)]
      allWorkspaces =
        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)
      (SD rect) = W.screenDetail screen
      dzenRects = 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) ->
            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 px py)
                    | b == button3 -> do
                        selection <- getAndResetWindowSelectionOrCurrent
                        withWindowsUnpinned selection $
                          windows $
                            appEndo $
                              mconcat (map (Endo . W.shiftWin ws) selection)

                        return (displayWorkspaceWheelAtPoint 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 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

    -- 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