hbro-1.2.0.0: library/Hbro/Keys/Model.hs
{-# LANGUAGE TemplateHaskell, TupleSections #-}
-- | Key bindings model.
-- Designed to be imported as @qualified@.
module Hbro.Keys.Model (
-- * Key strokes model
KeyStroke(..),
modifiersL,
keyL,
(.|),
-- * Bindings model
Binding,
Bindings,
Tree(..),
ModalBindings,
(-->),
insert,
merge,
Hbro.Keys.Model.lookup,
-- * State machine
Status(Status),
modeL,
keyStrokesL,
bindingsL,
bind,
bind',
setMode,
press,
getBoundFunction,
resetStrokes,
) where
-- {{{ Imports
import Hbro.Prelude hiding(lookup)
import Control.Lens ()
import Control.Lens.Getter
import Control.Lens.Lens
import Control.Lens.Setter
import Control.Lens.TH
import Data.List.NonEmpty (NonEmpty(..), nonEmpty)
import qualified Data.List.NonEmpty as NE (fromList, toList)
import qualified Data.Map as M
import qualified Prelude (lookup)
-- }}}
-- {{{ Key strokes model
-- | A single keystroke is a (possibly empty) set of (unorderered and unique) modifiers, and a single key.
data KeyStroke modifier key = KeyStroke
{ _modifiers :: Set modifier
, _key :: key
} deriving (Eq, Ord)
makeLensesWith ?? ''KeyStroke $ lensRules
& lensField .~ (\name -> Just (tailSafe name ++ "L"))
(.|) :: (ToSet modifier m) => m -> key -> KeyStroke modifier key
(.|) m k = KeyStroke (toSet m) k
-- }}}
-- {{{ Binding model
-- | A binding is simply a (non-empty) list of strokes bound to an action
type Binding keystroke action = (NonEmpty keystroke, action)
-- | A non-empty tree implementation that labels edges and leaves
type BranchedTree b l = [(b, Tree b l)]
data Tree b l = Leaf l | Branch (NonEmpty (b, Tree b l))
-- | Bindings are stored together in a tree structure
type Bindings keystroke action = BranchedTree keystroke action
-- | Modal bindings (à la vi)
type ModalBindings mode keystroke action = Map mode (Bindings keystroke action)
(-->) :: (ToNonEmpty keystroke s) => s -> action -> Binding keystroke action
(-->) s a = (toNonEmpty s, a)
-- | Make a branch out of a single binding
toBranch :: Ord a => (NonEmpty a, b) -> BranchedTree a b
toBranch ((h:|[]), a) = [ (h, Leaf a) ]
toBranch ((h:|h':t), a) = [ (h, Branch $ toBranch' ((h':|t), a)) ]
toBranch' :: (NonEmpty b, l) -> NonEmpty (b, Tree b l)
toBranch' ((h:|[]), a) = (h, Leaf a) :| []
toBranch' ((h:|h':t), a) = (h, Branch $ toBranch' ((h':|t), a)) :| []
-- | Merge 2 trees. In case of conflicts, prefer the rightmost operand.
merge :: Ord a => BranchedTree a b -> BranchedTree a b -> BranchedTree a b
merge [] [] = []
merge [] b = b
merge a [] = a
merge (h:t) (h':t') = NE.toList $ merge' (h:|t) (h':|t')
merge' :: (Ord k, Ord a)
=> NonEmpty (k, Tree a b) -> NonEmpty (k, Tree a b) -> NonEmpty (k, Tree a b)
merge' a b = NE.fromList . M.assocs $ M.unionWith merge'' (M.fromList $ NE.toList a) (M.fromList $ NE.toList b)
merge'' :: Ord a => Tree a b -> Tree a b -> Tree a b
merge'' (Leaf _) (Leaf b) = Leaf b
merge'' (Leaf _) (Branch b) = Branch b
merge'' (Branch _) (Leaf b) = Leaf b
merge'' (Branch a) (Branch b) = Branch $ merge' a b
-- | Is there a leaf at the end of the given path ?
lookup :: Ord a => NonEmpty a -> BranchedTree a b -> Maybe b
lookup a b = case walk a b of
Just (Leaf x) -> Just x
_ -> Nothing
-- | Return the subtree rooted at the end of the given path
walk :: Ord a => NonEmpty a -> BranchedTree a b -> Maybe (Tree a b)
walk _ [] = Nothing
walk (h:|t) tree = Prelude.lookup h tree >>= walk' t
walk' :: (Ord a) => [a] -> Tree a t -> Maybe (Tree a t)
walk' [] tree = Just tree
walk' (h:t) (Branch b) = walk (h:|t) (NE.toList b)
walk' _ _ = Nothing
-- | Insert new binding into a tree-structured bindings list
insert :: (Ord m, Ord s) => Binding s a -> m -> ModalBindings m s a -> ModalBindings m s a
insert binding theMode = M.insertWith (flip merge) theMode (toBranch binding)
-- }}}
-- | Global state including any necessary information to handle key bindings
data Status keystroke mode action = Status
{ _mode :: mode -- ^ Current mode
, _keyStrokes :: [keystroke] -- ^ Previous keystrokes
, _bindings :: ModalBindings mode keystroke action -- ^ Current bindings
}
makeLensesWith ?? ''Status $ lensRules
& lensField .~ (\name -> Just (tail name ++ "L"))
instance (Default mode) => Default (Status keystroke mode action) where
def = Status def [] M.empty
bind' :: (Ord mode, Ord keystroke, ToNonEmpty keystroke s)
=> mode -> s -> action -> Status keystroke mode action -> Status keystroke mode action
bind' theMode theStrokes action (Status a b c) = Status a b $ insert (theStrokes --> action) theMode c
-- | Same as 'bind' with default mode.
bind :: (Ord mode, Default mode, Ord keystroke, ToNonEmpty keystroke s)
=> s -> action -> Status keystroke mode action -> Status keystroke mode action
bind s f = bind' def s f
setMode :: mode -> Status keystroke mode action -> Status keystroke mode action
setMode newMode = set modeL newMode . set keyStrokesL []
press :: (Ord mode, Ord keystroke) => keystroke -> Status keystroke mode action -> Status keystroke mode action
press keystroke status = set keyStrokesL newKeyStrokes status
where
oldKeyStrokes = status^.keyStrokesL
bindings = M.lookup (status^.modeL) (status^.bindingsL)
validKeyStrokes = isJust . join $ walk <$> nonEmpty oldKeyStrokes <*> bindings
boundKeyStroke = isJust . join $ lookup <$> nonEmpty oldKeyStrokes <*> bindings
newKeyStrokes = (oldKeyStrokes ++ [keystroke]) <| (validKeyStrokes && not boundKeyStroke) |> [keystroke]
getBoundFunction :: (Ord mode, Ord keystroke) => Status keystroke mode action -> Maybe action
getBoundFunction status = do
theStrokes <- nonEmpty $ status^.keyStrokesL
theBindings <- M.lookup (status^.modeL) (status^.bindingsL)
lookup theStrokes theBindings
resetStrokes :: Status keystroke mode action -> Status keystroke mode action
resetStrokes = set keyStrokesL []