packages feed

spartacon-0.1.0.0: app/Main.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilyDependencies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}

module Main where

import Control.Applicative
import Control.Exception
import Control.Monad
import Control.Monad.Reader
import Control.Monad.State
import Data.Bits
import Data.Either.Validation
import Data.Foldable
import Data.Functor.Identity
import Data.List
import Data.Map (Map)
import Data.Maybe
import Data.Relation (Relation)
import Data.Set (Set)
import Data.Text (Text)
import Data.Traversable
import Graphics.X11.Xlib hiding (refreshKeyboardMapping)
import Graphics.X11.Xlib.Extras
import System.Exit
import System.IO
import Text.Printf

import qualified Config as C
import qualified Config.Schema as C
import qualified Data.Map as M
import qualified Data.Relation as R
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Text.IO as T

main :: IO ()
main = do
	config <- readConfiguration
	runSpartacon config $ do
		root <- askRoot
		refreshBindings
		x selectInput root keyPressMask
		suspend $ \ctx s -> allocaXEvent $ \evPtr -> resume ctx s . forever $ do
			x nextEvent evPtr
			ev <- io getEvent evPtr
			case ev of
				MappingNotifyEvent{} -> do
					io refreshKeyboardMapping ev
					km <- x reloadKeymap
					modify (\s -> s { keymap = km })
					refreshBindings
				KeyEvent{}
					| ev_event_type ev /= keyPress -> pure ()
					| otherwise -> do
						bs <- gets bindings
						io $ case M.lookup (ev_state ev, ev_keycode ev) bs of
							Just Quit -> exitWith ExitSuccess
							Just (Print t) -> T.putStr t >> hFlush stdout
							Nothing -> hPrintf stderr "WARNING: got key event with no corresponding binding: %s\n" (show ev)
				_ -> io hPrintf stderr "WARNING: ignoring unexpected X event %s\n" (show ev)

readConfiguration :: IO Configuration
readConfiguration = do
	t <- T.getContents
	v <- case C.parse t of
		Left err -> do
			hPrintf
				stderr
				"Formatting error in configuration. Check the syntax reference at:\nhttp://hackage.haskell.org/package/config-value/docs/Config.html\n\t%s\n"
				(displayException err)
			exitWith (ExitFailure 1)
		Right v -> pure v
	case C.loadValue C.anySpec v of
		Left err -> hPutStrLn stderr (displayException err) >> exitWith (ExitFailure 2)
		Right config -> pure config

data Configuration = Configuration
	{ ignoreMasks :: [KeyMask]
	, bindingRequests :: [(BindingRequest, Action)]
	} deriving (Eq, Ord, Read, Show)

data BindingRequest = BindingRequest
	{ modifier :: KeyMask
	, key :: Either KeySym KeyCode
	} deriving (Eq, Ord, Read, Show)

data Action = Quit | Print Text
	deriving (Eq, Ord, Read, Show)

data Context = Context
	{ configuration :: Configuration
	, display :: Display
	} deriving (Eq, Ord, Show)

data KeyboardState = KeyboardState
	{ keymap :: Relation KeyCode KeySym
	, bindings :: Map (KeyMask, KeyCode) Action
	} deriving (Eq, Ord, Show)

type Spartacon = ReaderT Context (StateT KeyboardState IO)

runSpartacon :: Configuration -> Spartacon a -> IO a
runSpartacon config act = do
	dpy <- openDisplay ""
	keys <- reloadKeymap dpy
	evalStateT (runReaderT act (Context config dpy)) (KeyboardState keys M.empty)

suspend :: (Context -> KeyboardState -> IO (a, KeyboardState)) -> Spartacon a
suspend f = ReaderT $ StateT . f

resume :: Context -> KeyboardState -> Spartacon a -> IO (a, KeyboardState)
resume ctx s act = runStateT (runReaderT act ctx) s

reloadKeymap :: Display -> IO (Relation KeyCode KeySym)
reloadKeymap dpy = do
	mapping <- for [lo..hi] $ \keycode ->
		for [0..3] $ \i ->
			(,) keycode <$> keycodeToKeysym dpy keycode i
	pure . R.fromList $
		[ (keycode, keysym)
		| pairs <- mapping
		, (keycode, keysym) <- pairs
		, keysym /= noSymbol
		]
	where
	(lo_, hi_) = displayKeycodes dpy
	[lo, hi] = map fromIntegral [lo_, hi_]

expandCodes ::
	Configuration ->
	Relation KeyCode KeySym ->
	[((BindingRequest, Action), (KeyMask, KeyCode))]
expandCodes conf mapping =
	[ (reqAct, (modifier req .|. extraMask, code))
	| extraMasks <- subsequences (ignoreMasks conf)
	, let extraMask = foldl' (.|.) 0 extraMasks
	, reqAct@(req, act) <- bindingRequests conf
	, code <- case key req of
		Right code -> [code]
		Left sym -> S.toList . R.lookupRan sym $ mapping
	]

checkCodes ::
	[(BindingRequest, Action)] ->
	[((BindingRequest, Action), (KeyMask, KeyCode))] ->
	( [(BindingRequest, Action)]
	, Map (KeyMask, KeyCode) (Set (BindingRequest, Action))
	, Map (KeyMask, KeyCode) Action
	)
checkCodes reqs expanded =
	( S.toList $ reqSet S.\\ foldMap sourceRequests mappings
	, runIdentity $ M.traverseMaybeWithKey keepDuplicates mappings
	, runIdentity $ M.traverseMaybeWithKey keepActions mappings
	) where
	mappings :: Map (KeyMask, KeyCode) (Map Action (Set BindingRequest))
	mappings = M.fromListWith (M.unionWith S.union)
		[ (binding, M.singleton act (S.singleton req))
		| ((req, act), binding) <- expanded
		]

	keepActions :: (KeyMask, KeyCode) -> Map Action (Set BindingRequest) -> Identity (Maybe Action)
	keepActions _ = Identity . fmap fst . M.lookupMin

	keepDuplicates :: (KeyMask, KeyCode) -> Map Action (Set BindingRequest) -> Identity (Maybe (Set (BindingRequest, Action)))
	keepDuplicates _ m = Identity $ if M.size m > 1
		then Just (sourceRequests m)
		else Nothing

	sourceRequests :: Map Action (Set BindingRequest) -> Set (BindingRequest, Action)
	sourceRequests = M.foldMapWithKey (\act -> S.map (flip (,) act))

	reqSet :: Set (BindingRequest, Action)
	reqSet = S.fromList reqs

computeCodes :: Configuration -> Relation KeyCode KeySym ->
	( [(BindingRequest, Action)]
	, Map (KeyMask, KeyCode) (Set (BindingRequest, Action))
	, Map (KeyMask, KeyCode) Action
	)
computeCodes conf = checkCodes (bindingRequests conf) . expandCodes conf

computeBindings :: Spartacon (Map (KeyMask, KeyCode) Action)
computeBindings = do
	conf <- asks configuration
	(unbound, ambiguous, bs) <- computeCodes conf <$> gets keymap
	unless (null unbound) $ do
		io hPutStrLn stderr "WARNING: No keys found for the following binding requests:"
		forM_ unbound $ io hPrintf stderr "\t%s\n" . pp
	unless (M.null ambiguous) $ do
		io hPutStrLn stderr "WARNING: Some binding requests conflict."
		flip M.traverseWithKey ambiguous $ \binding@(mask, code) reqs -> io $ do
			hPrintf stderr "\tThese binding requests are competing for mask %s, keycode %d"
				(reverse . drop 1 . reverse . pp $ mask)
				code
			for_ (M.lookup binding bs) $ hPrintf stderr " (winner: %s)" . pp
			hPutStrLn stderr ":"
			for_ reqs $ hPrintf stderr "\t\t%s\n" . pp
		pure ()
	pure bs

refreshBindings :: Spartacon ()
refreshBindings = do
	bs <- gets bindings
	bs' <- computeBindings
	root <- askRoot
	for_ (M.keysSet (bs M.\\ bs')) $ \(mask, code) -> x ungrabKey code mask root
	for_ (M.keysSet (bs' M.\\ bs)) $ \(mask, code) -> x grabKey code mask root False grabModeAsync grabModeAsync
	modify (\s -> s { bindings = bs' })

class PP a where pp :: a -> String

instance PP Action where
	pp Quit = "quit"
	pp (Print t) = show t

instance PP BindingRequest where
	pp (BindingRequest mask code) = case (pp mask, pp code) of
		-- atoms must start with a letter
		("", s@(c:_)) | c < 'A' || c > 'z' || (c > 'Z' && c < 'a')
			-> "B-" ++ s
		(s, s') -> s ++ s'

instance PP (Either KeySym KeyCode) where
	pp (Left sym) = keysymToString sym
	pp (Right code) = "." ++ show code

instance PP (BindingRequest, Action) where
	pp (req, action) = pp req ++ ": " ++ pp action

instance PP KeyMask where
	pp mask = concat $ zipWith
		(\label mask' -> if mask .&. mask' == 0 then "" else label ++ "-")
		-- TODO: numlock/alt might not be mod2Mask/mod1Mask
		["NL", "L", "C", "A", "S", "M3", "M4", "M5"]
		[mod2Mask, lockMask, controlMask, mod1Mask, shiftMask, mod3Mask, mod4Mask, mod5Mask]

askRoot :: Spartacon Window
askRoot = asks (defaultRootWindow . display)

instance C.HasSpec Configuration where
	anySpec = C.sectionsSpec "configuration" $ pure Configuration
		-- TODO: NL might not always be mod2Mask
		<*> (nub . fromMaybe [mod2Mask] <$> C.optSection' "ignore" (C.listSpec C.anySpec) "keymasks to ignore when deciding whether a keypress matches (default: NL)")
		<*> (fold <$> C.optSection' "bindings" bindingRequestsSpec "keys to watch for and the actions to take when they're pressed (default: no bindings)")

bindingRequestsSpec :: C.ValueSpec [(BindingRequest, Action)]
bindingRequestsSpec = C.customSpec
	"binding requests"
	(C.assocSpec C.anySpec)
	( ppParseError "binding requests"
	. traverse (\(req, act) -> flip (,) act <$> parseBindingRequest req)
	)

instance C.HasSpec Action where
	anySpec = (Quit <$ C.atomSpec "quit") C.<!> (Quit <$ C.atomSpec "exit")
	    C.<!> (Print <$> C.anySpec)

instance C.HasSpec KeyMask where
	anySpec = foldr1 (C.<!>) [mask <$ C.atomSpec label | (label, mask) <- modifierMap]

parseModifier :: Text -> Validation [(Text, [Text])] (Text, KeyMask)
parseModifier t = head $
	[ pure (T.drop (T.length t') t, m)
	| (t', m) <- modifierMap
	, t' `T.isPrefixOf` t
	] ++ [Failure [(t, fst <$> modifierMap)]]

parseOnlyModifier :: Text -> Validation [(Text, [Text])] KeyMask
parseOnlyModifier t = case parseModifier t of
	Success (t', m)
		| T.null t' -> pure m
		| otherwise -> Failure [(t', ["<end of string>"])]
	Failure errs -> Failure errs

parseKey :: Text -> Validation [(Text, [Text])] (Either KeySym KeyCode)
parseKey t
	| T.isPrefixOf "." t = case reads . tail . T.unpack $ t of
		(code, ""):_ -> pure (Right code)
		(_, unparsed):_ -> Failure [(T.pack unparsed, ["<end of string>"])]
		[] -> Failure [(T.drop 1 t, ["<number>"])]
	| otherwise = case stringToKeysym . T.unpack $ t of
		0 -> Failure [(t, ["<keysym string>"])]
		sym -> pure (Left sym)

parseBindingRequest :: Text -> Validation [(Text, [Text])] BindingRequest
parseBindingRequest t = case T.splitOn "-" t of
	[] -> Failure [(t, ["<anything>"])]
	ts -> pure BindingRequest
		<*> (foldr (.|.) noModMask <$> traverse parseOnlyModifier (init ts))
		<*> parseKey (last ts)

-- TODO: alt/numlock aren't necessarily M1/M2
modifierMap :: [(Text, KeyMask)]
modifierMap = concat . tail $ [undefined
	, "S" ~> shiftMask
	, "L" ~> lockMask
	, "C" ~> controlMask
	, "A" ~> mod1Mask
	, "M1" ~> mod1Mask
	, "NL" ~> mod2Mask
	, "M2" ~> mod2Mask
	, "M3" ~> mod3Mask
	, "M4" ~> mod4Mask
	, "M5" ~> mod5Mask
	, "B" ~> noModMask -- blank; needed to bind 1-9 or keycodes with no modifier, since atoms must start with a letter
	]
	where
	t ~> m = [(t, m), (T.toLower t, m)]

ppParseError :: Text -> Validation [(Text, [Text])] a -> Either Text a
ppParseError ty (Success a) = Right a
ppParseError ty (Failure errs) = Left . T.unlines . ("\tParsing of " <> ty <> " failed":) $
	[ msg
	| (t, expecteds) <- errs
	, msg <- ("\tat " <> t <> ", expected one of these:") : map ("\t\t"<>) expecteds
	]

class SpartaClass a where
	type Rebel a = r | r -> a
	x :: (Display -> a) -> Rebel a
	io :: a -> Rebel a

instance SpartaClass (IO a) where
	type Rebel (IO a) = Spartacon a
	x f = ask >>= liftIO . f . display
	io = liftIO

instance SpartaClass b => SpartaClass (a -> b) where
	type Rebel (a -> b) = a -> Rebel b
	x f = x . flip f
	io f = io . f