aboutsummaryrefslogtreecommitdiff
path: root/src/Rahm/Desktop/Keys/Dsl2.hs
blob: cd0035a5484be9448dc7cb8030728184a56cdd8c (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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
{-# LANGUAGE UndecidableInstances #-}

-- | This module creates a DSL for binding keys in a succinct and expressive
-- way. This DSL follows the pattern:
--
-- bind <key> $ do
--    <mask> $ <doc string $?> binding
--    <mask> $ <doc string $?> binding
--    <mask> $ <doc string $?> binding
--
-- for example:
--
-- bind xK_x $ do
--   justMod $ doc "Kill the current window" (withFocused X.kill)
--   shiftMod $ doc "Restart xmonad" restart
--
-- bind xK_v $ do
--   justMod $
--     continuous $ do
--       bind xK_plus $ doc "increase volume" increaseVolume
--       bind xK_minus $ doc "decrease volume" decreaseVolume
module Rahm.Desktop.Keys.Dsl2 where

import Control.Applicative ((<|>))
import Control.Monad.Fix (fix)
import Control.Monad.RWS (All (All), MonadTrans (lift), MonadWriter, forM, forM_, when)
import Control.Monad.Reader (Reader, ask, runReader)
import Control.Monad.State (MonadTrans, StateT (StateT))
import Control.Monad.Trans.Maybe (MaybeT (..))
import Control.Monad.Trans.Writer (Writer, WriterT, execWriter, execWriterT, runWriter)
import Control.Monad.Writer.Class (tell)
import Data.Bits ((.&.))
import Data.Functor.Identity (Identity)
import Data.List (intercalate)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Rahm.Desktop.Common (pointerWindow, runMaybeT_)
import Rahm.Desktop.Keys.Grab
import Rahm.Desktop.Keys.KeyCodeMapping (setupKeycodeMapping)
import Rahm.Desktop.Logger (LogLevel (Debug, Info), logs)
import Rahm.Desktop.Submap (ButtonOrKeyEvent (ButtonPress, KeyPress, event_keycode, event_mask), getStringForKey, nextButtonOrKeyEvent)
import Rahm.Desktop.XMobarLog (spawnXMobar)
import Rahm.Desktop.XMobarLog.PendingBuffer (pushAddPendingBuffer, pushPendingBuffer, clearPendingBuffer)
import XMonad

-- | A documented "thing." It is essentially an item with a string attached to
-- it. A glorified tuple (String, t)
data Documented t = Documented
  { docString :: String,
    undocument :: t
  }

-- | The Documented type is a functor.
instance Functor Documented where
  fmap fn (Documented s t) = Documented s (fn t)

-- | Type family for an action associated with a type. This type family
-- indicates what type of action a keytype can be bound to.
type family Action t where
  -- KeySyms are bound to contextless actions with type X ()
  Action KeySymOrKeyCode = X ()
  -- Buttons are associated with actions with type Window -> X (). In other
  -- words, actions bound to a button have windows associated with it.
  Action Button = Window -> X ()

class (Bind (Super k)) => LiftBinding k where
  type Super k :: *
  doLift :: k -> Super k

instance LiftBinding KeySymOrKeyCode where
  type Super KeySymOrKeyCode = KeySymOrKeyCode
  doLift = id

instance LiftBinding KeySym where
  type Super KeySym = Super KeySymOrKeyCode
  doLift = doLift . Ks

instance LiftBinding KeyCode where
  type Super KeyCode = Super KeySymOrKeyCode
  doLift = doLift . Kc

instance LiftBinding Button where
  type Super Button = Button
  doLift = id

-- | An GADT for XConfig that hides the 'l' parameter. This keeps type
-- signatures clean by not having to carry around a superfluous type variable.
data XConfigH where
  XConfigH :: forall l. XConfig l -> XConfigH

-- | A type for binding. These are the type of bindings keys and buttons can be
-- bound to The type 't' refers to the key-type of the binding: either `KeySym`
-- or `Button`.
data Binding t
  = -- | Just an action. When this key is pressed, just do some action.
    -- This uses the type-family defined above to determine what type the action
    -- should be.
    Action (Action t)
  | -- | Sub-bindings. These bindings require another key to resolve.
    Submap (forall l. XConfig l -> BindingsMap)
  | -- | Like submap, but these bindings are repeatable. This operates as a
    -- self-loop in the keybinding graph.
    Repeat (Binding t) (forall l. XConfig l -> BindingsMap)
  | -- | No action bound to the key.
    NoBinding

-- | Compiled bindings map. This is the type built up by the Binder monad.
data BindingsMap = BindingsMap
  { -- | Bindings for keys
    key_bindings :: Map (KeyMask, KeySymOrKeyCode) (Documented (Binding KeySymOrKeyCode)),
    -- | Bindings for buttons.
    button_bindings :: Map (KeyMask, Button) (Documented (Binding Button)),
    -- | If no mapping for a key sym exists, this function is called to handle
    -- the no match key.
    no_match_catch_key :: (KeyMask, KeySym, String) -> X (),
    -- | If no mapping for a button exists, this function is called to handle
    -- the unbound button.
    no_match_catch_button :: (KeyMask, Button) -> Window -> X ()
  }

-- | BindingMaps are combined by combining the bindings map and combining the
-- handling functio
instance Semigroup BindingsMap where
  (BindingsMap mk1 mb1 fk1 fb1) <> (BindingsMap mk2 mb2 fk2 fb2) =
    BindingsMap
      (mk1 <> mk2)
      (mb1 <> mb2)
      (\a -> fk1 a >> fk2 a)
      (\a b -> fb1 a b >> fb2 a b)

instance Monoid BindingsMap where
  mempty = BindingsMap mempty mempty (\_ -> return ()) (\_ _ -> return ())

-- | This type is an intermediate monad in the DSL. It binds an action to a key
-- mask. This is all bound to a key or button in the Binder monad.
newtype MaskBinder k a = MaskBinder
  { unMaskBinder :: WriterT (Map KeyMask (Documented (Binding k))) (Reader XConfigH) a
  }
  deriving
    ( Functor,
      Applicative,
      Monad,
      MonadWriter (Map KeyMask (Documented (Binding k))),
      MonadReader XConfigH
    )

-- | The ultimate monad for this DSL. This Binder monad builds up a BindingsMap
newtype Binder a = Binder (WriterT BindingsMap (Reader XConfigH) a)
  deriving (Functor, Applicative, Monad, MonadWriter BindingsMap, MonadReader XConfigH)

-- | This typeclas esentiallly defines what types can be bound in the DSL. I.e.
-- what types are allowed to be arguments to the xMod, rawMask, etc. functions.
class BindingType a where
  -- | What key this action belongs to.
  type BoundTo a :: *

  -- | Convert the action into a Documented binding
  toBinding :: a -> Documented (Binding (BoundTo a))

-- | Bindings are trivially a BindingType.
instance BindingType (Binding t) where
  type BoundTo (Binding t) = t
  toBinding = Documented ""

-- | An X () can be bound to a Binding KeySym
instance BindingType (X ()) where
  type BoundTo (X ()) = KeySymOrKeyCode
  toBinding = Documented "" . Action

-- | A Window -> X () can be bound to a Binding Button.
instance BindingType (Window -> X ()) where
  type BoundTo (Window -> X ()) = Button
  toBinding = Documented "" . Action

-- | Any Documented BindingType is also a BindingType.
instance (BindingType a) => BindingType (Documented a) where
  type BoundTo (Documented a) = BoundTo a
  toBinding (Documented s (toBinding -> (Documented _ a))) = Documented s a

-- | This typeclass is responsible for converting a compiled MaskBinder into a
-- BindingsMap using a key type 'k'.
class Bind k where
  doBinding :: k -> Map KeyMask (Documented (Binding k)) -> BindingsMap
  rawMaskRaw :: KeyMask -> Documented (Binding k) -> MaskBinder k ()

-- | For this, it adds the bindings to the buttonMap
instance Bind Button where
  doBinding but mp = mempty {button_bindings = Map.mapKeys (,but) mp}
  rawMaskRaw mask act = tell (Map.singleton mask act)

-- | For this, it adds the bindings to the keysMap
instance Bind KeySymOrKeyCode where
  doBinding key mp = mempty {key_bindings = Map.mapKeys (,key) mp}
  rawMaskRaw mask act = tell (Map.singleton mask act)

rawMask :: (Bind (BoundTo a), BindingType a) => KeyMask -> a -> MaskBinder (BoundTo a) ()
rawMask mask act = rawMaskRaw mask (toBinding act)

withMod :: (Bind (BoundTo a), BindingType a) => KeyMask -> a -> MaskBinder (BoundTo a) ()
withMod m act = do
  (XConfigH (modMask -> mm)) <- ask
  rawMask (mm .|. m) act

-- | Mask bindings.
noMod, justMod, shiftMod, controlMod, altMod :: (Bind (BoundTo a), BindingType a) => a -> MaskBinder (BoundTo a) ()
justMod = withMod 0
noMod = rawMask 0
shiftMod = withMod shiftMask
controlMod = withMod controlMask
altMod = withMod mod1Mask

-- | allows easy assigning multiple masks to the same action.
(-|-) ::
  (Bind (BoundTo a), BindingType a) =>
  (a -> MaskBinder (BoundTo a) ()) ->
  (a -> MaskBinder (BoundTo a) ()) ->
  a ->
  MaskBinder (BoundTo a) ()
m1 -|- m2 = \act -> m1 act >> m2 act

-- | Bind a key to a maksBinder.
bind :: (LiftBinding k) => k -> MaskBinder (Super k) () -> Binder ()
bind k h =
  tell . doBinding (doLift k) . runReader (execWriterT $ unMaskBinder h) =<< ask

-- | Bind multiple keys to the same mask binder.
bindL :: (LiftBinding k) => [k] -> MaskBinder (Super k) () -> Binder ()
bindL ks h = mapM_ (`bind` h) ks

-- | Convenience function for Documented.
doc :: String -> a -> Documented a
doc = Documented

-- | A concrete-typed version of 'const'
noWindow :: X () -> Window -> X ()
noWindow fn _ = fn

data Bindings where
  Bindings ::
    (forall l. XConfig l -> Map (KeyMask, KeySym) (X ())) ->
    (forall l. XConfig l -> Map (KeyMask, KeyCode) (X ())) ->
    (forall l. XConfig l -> Map (ButtonMask, Button) (Window -> X ())) ->
    Bindings

-- | Turn a BindingsMap into two values usable values for the XMonad config.
resolveBindings ::
  BindingsMap -> Bindings
resolveBindings (BindingsMap keyAndKeyCodeBindings buttonBindings _ _) =
  Bindings
    (\c -> Map.mapWithKey (\k v -> pushK k (bindingToX c) (undocument v)) keyBindings)
    (\c -> Map.mapWithKey (\k v -> bindingToX c (undocument v)) keycodeBindings)
    (\c -> Map.mapWithKey (\k v -> pushB k (bindingToWinX c) (undocument v)) buttonBindings)
  where
    (keyBindings, keycodeBindings) =
      partitionMap
        ( \case
            (m, Kc keyCode) -> Right (m, keyCode)
            (m, Ks keySym) -> Left (m, keySym)
        )
        keyAndKeyCodeBindings

    pushB (_, b) fn binding win = do
      if isRepeatOrSubmap binding
        then pushPendingBuffer ("b" ++ show b ++ " ") $ fn binding win
        else fn binding win
      clearPendingBuffer

    pushK (m, k) fn binding = do
      if isRepeatOrSubmap binding
        then do
          let s = getStringForKey (m, k)
          pushPendingBuffer (s ++ " ") $ fn binding
        else fn binding
      clearPendingBuffer

    bindingToX :: forall l. XConfig l -> Binding KeySymOrKeyCode -> X ()
    bindingToX conf = \case
      NoBinding -> return ()
      Action a -> a
      Submap sm -> doSubmap conf (sm conf) (return ())
      Repeat a sm -> bindingToX conf a >> fix (doSubmap conf (sm conf))

    bindingToWinX :: forall l. XConfig l -> Binding Button -> Window -> X ()
    bindingToWinX conf binding win = case binding of
      NoBinding -> return ()
      Action fn -> fn win
      Submap sm -> doSubmap conf (sm conf) (return ())
      Repeat a sm -> bindingToWinX conf a win >> fix (doSubmap conf (sm conf))

    doSubmap :: forall l. XConfig l -> BindingsMap -> X () -> X ()
    doSubmap conf (BindingsMap kbind bbind catk catb) after = do
      nextPressEvent $ \str ->
        \case
          (ButtonPress m b) -> do
            win <- pointerWindow
            case Map.lookup (m, b) bbind of
              (Just binding) ->
                pushAddPendingBuffer (str ++ " ") $ do
                  bindingToWinX conf (undocument binding) win
                  after
              Nothing -> catb (m, b) win
          (KeyPress m k c s) -> do
            case Map.lookup (m, Kc c) kbind <|> Map.lookup (m, Ks k) kbind of
              (Just binding) ->
                pushAddPendingBuffer (str ++ " ") $ do
                  bindingToX conf (undocument binding)
                  after
              Nothing -> catk (m, k, s)

    isRepeatOrSubmap = \case
      Repeat {} -> True
      Submap {} -> True
      _ -> False

    nextPressEvent fn = runMaybeT_ $ do
      ev <- nextButtonOrKeyEvent
      let str = case ev of
            ButtonPress m b -> "b" ++ show b
            KeyPress _ _ _ s -> s
      lift $
        fn str ev

-- Create a submap in place of an action.
subbind :: Binder () -> Binding t
subbind (Binder b) =
  Submap $ \config ->
    runReader (execWriterT b) (XConfigH config)

repeatable :: Binder () -> Binding t
repeatable (Binder b) =
  Repeat NoBinding $ \config ->
    runReader (execWriterT b) (XConfigH config)

-- Similar to repeatable, but all the keys in the binder start the loop.
continuous :: Binder () -> Binder ()
continuous (Binder b) = do
  conf <- ask
  let bm@(BindingsMap keyBinds mouseBinds _ _) =
        runReader (execWriterT b) conf

  forM_ (Map.toList keyBinds) $ \((m, k), Documented _ b) ->
    bind k $ rawMask m $ Repeat b $ const bm

  forM_ (Map.toList mouseBinds) $ \((m, k), Documented _ b) ->
    bind k $ rawMask m $ Repeat b $ const bm

runBinder :: XConfig l -> Binder a -> BindingsMap
runBinder conf (Binder binder) = runReader (execWriterT binder) (XConfigH conf)

withBindings :: Binder a -> XConfig l -> XConfig l
withBindings b config =
  let (Bindings keyBinds keycodeBinds buttonBinds) =
        resolveBindings $ runBinder config b
   in setupKeycodeMapping keycodeBinds $
        config
          { keys = keyBinds,
            mouseBindings = buttonBinds
          }

documentation :: XConfig l -> Binder () -> String
documentation conf binder =
  documentation' $ runBinder conf binder
  where
    documentation' :: BindingsMap -> String
    documentation' (BindingsMap kmap bmap _ _) = execWriter $ do
      forM_
        ( Map.toList $
            invert $
              Map.union
                (Map.map documentBinding (Map.mapKeys keyToStr kmap))
                (Map.map documentBinding (Map.mapKeys buttonToStr bmap))
        )
        $ \(doc, keys) ->
          when (doc /= "") $ do
            when (length keys > 1) (tell "\n")
            tell (intercalate ",\n" keys)
            tell " -> "
            tell (tindent doc)
            when (length keys > 1) (tell "\n")

    documentBinding :: Documented (Binding r) -> String
    documentBinding = \case
      (Documented s (Action _)) -> s
      (Documented s (Submap mp)) ->
        s ++ "\n" ++ indent (documentation' (mp conf))
      (Documented s (Repeat _ mp)) ->
        s ++ " (repeatable)\n" ++ indent (documentation' (mp conf))
      _ -> ""

    indent = unlines . map ("  " ++) . lines
    tindent (lines -> (h : t)) = unlines (h : map ("  " ++) t)
    tindent x = x

    keyToStr (m, k) = showMask m ++ keysymOrKeyCodeToString k
    keysymOrKeyCodeToString (Kc code) = show code
    keysymOrKeyCodeToString (Ks sym) = keysymToString sym

    showMask mask =
      let masks =
            [ (shiftMask, "S"),
              (mod1Mask, "A"),
              (mod3Mask, "H"),
              (mod4Mask, "M"),
              (controlMask, "C")
            ]
       in concatMap ((++ "-") . snd) $ filter ((/= 0) . (.&. mask) . fst) masks

    invert :: (Ord a, Ord b) => Map a b -> Map b [a]
    invert = Map.fromListWith (++) . map (\(a, b) -> (b, [a])) . Map.toList

    buttonToStr (m, b) = showMask m ++ buttonNumToStr b
    buttonNumToStr = \case
      1 -> "Left Click"
      2 -> "Middle Click"
      3 -> "Right Click"
      4 -> "Wheel Up"
      5 -> "Wheel Down"
      6 -> "Wheel Left"
      7 -> "Wheel Right"
      8 -> "Browser Back"
      9 -> "Browser Forward"
      13 -> "Thumb Target"
      14 -> "Index Forward"
      15 -> "Index Back"
      b -> "Button " ++ show b

bindOtherKeys :: ((KeyMask, KeySym, String) -> X ()) -> Binder ()
bindOtherKeys fn = Binder $ tell (mempty {no_match_catch_key = fn})

bindOtherButtons :: ((KeyMask, Button) -> Window -> X ()) -> Binder ()
bindOtherButtons fn = Binder $ tell (mempty {no_match_catch_button = fn})

partitionMap :: (Ord k, Ord k1, Ord k2) => (k -> Either k1 k2) -> Map k a -> (Map k1 a, Map k2 a)
partitionMap f mp =
  foldl
    ( \(mp1, mp2) (k, v) ->
        case f k of
          Left k1 -> (Map.insert k1 v mp1, mp2)
          Right k2 -> (mp1, Map.insert k2 v mp2)
    )
    (mempty, mempty)
    (Map.toList mp)