packages feed

wacom-daemon-0.1.0.0: System/Wacom/Config.hs

{-# LANGUAGE OverloadedStrings, FlexibleInstances #-}

module System.Wacom.Config
  (
    Config (..),
    Profile (..),
    RingMode (..),
    TabletAction (..),
    Area,
    dfltConfig,
    buildProfiles
  )
  where

import Control.Applicative
import Data.Char (isDigit)
import qualified Data.Map as M
import qualified Data.HashMap.Strict as H
import Data.Aeson.Types (typeMismatch)
import Data.Yaml
import qualified Data.Text as T

-- | Tablet configuration
data Config = Config {
      tStylusKey :: String              -- ^ Keyword to be used to detect Wacom Stylus device name. Default is @Pen@.
    , tPadKey :: String                 -- ^ Keyword to be used to detect Wacom Pad device name. Default is @Pad@.
    , tTouchKey :: String               -- ^ Keyword to be used to detect Wacom Touch device name. Default is @Touch@.
    , tTouch :: Bool                    -- ^ Whether to enable touchpad functionality (if supported by tablet).
    , tMapAreas :: [Area]               -- ^ List of predefined tablet mapping areas (for example, different monitors).
                                        --   Specified in X11 Geometry format, for example @1920x1200+0+0@.
    , tProfiles :: [(String, Profile)]  -- ^ Map of named settings profiles.
  }
  deriving (Show)

instance FromJSON Config where
  parseJSON (Object v) =
    Config
      <$> v .:? "stylus-device-key" .!= "Pen"
      <*> v .:? "pad-device-key" .!= "Pad"
      <*> v .:? "touch-device-key" .!= "Touch"
      <*> v .:? "enable-touch" .!= False
      <*> v .:? "mapping-areas" .!= []
      <*> (buildProfiles <$> (v .: "profiles"))
  parseJSON invalid = typeMismatch "Config" invalid

type Area = String

-- | Tablet settings profile
data Profile = Profile {
      pName :: String                    -- ^ Profile name
    , pRing :: [RingMode]                -- ^ Ring modes (for Intuos Pro models)
    , pButtons :: M.Map Int TabletAction -- ^ Tablet buttons bindings
  }
  deriving (Show)

instance FromJSON (M.Map Int TabletAction) where
  parseJSON (Object v) = do
      let lst = H.toList v
      lst' <- mapM go lst
      return $ M.fromList lst'
    where
      go (key,val) = do
        let keyStr = T.unpack key
        keyInt <- if all isDigit keyStr
                    then return $ read keyStr
                    else fail $ "Invalid button number " ++ keyStr
        val' <- parseJSON val
        return (keyInt, val')
          

instance FromJSON Profile where
  parseJSON (Object v) =
    Profile
      <$> v .: "name"
      <*> v .:? "ring" .!= []
      <*> v .:? "buttons" .!= M.empty
  parseJSON invalid = typeMismatch "Profile" invalid

-- | Intuos Pro ring mode
data RingMode = RingMode {
      rName :: String           -- ^ Mode name
    , ringUp :: TabletAction    -- ^ Action to be toggled at scrolling ring counterclockwise
    , ringDown :: TabletAction  -- ^ Action to be toggled at scrolling ring clockwise
  }
  deriving (Show)

instance FromJSON RingMode where
  parseJSON (Object v) =
    RingMode
      <$> v .: "name"
      <*> v .: "up"
      <*> v .: "down"
  parseJSON invalid = typeMismatch "RingMode" invalid

-- | Supported actions for binding
data TabletAction =
    Key String   -- ^ Keyboard shortcut, for example @shift@, @ctrl z@ etc
  | Click Int    -- ^ Mouse button click
  | DblClick Int -- ^ Mouse button double click

instance FromJSON TabletAction where
  parseJSON (String text) =
    case words (T.unpack text) of
      [] -> fail "empty tablet action"
      ("key":ws) -> return $ Key $ unwords ws
      ["button", s] ->
        if all isDigit s
          then return $ Click $ read s
          else fail $ "Invalid button number " ++ s
      ["dblclick", s] ->
        if all isDigit s
          then return $ DblClick $ read s
          else fail $ "Invalid button number " ++ s
      xs -> fail $ "Invalid tablet action specification: " ++ unwords xs
  parseJSON invalid = typeMismatch "TabletAction" invalid

instance Show TabletAction where
  show (Key s) = "key " ++ s
  show (Click b) = "button " ++ show b
  show (DblClick b) = "dblclick " ++ show b

-- | Default config
dfltConfig :: Config
dfltConfig = Config {
  tStylusKey = "Pen",
  tPadKey = "Pad",
  tTouchKey = "Finger",
  tMapAreas = [],
  tTouch = False,
  tProfiles = []
}

buildProfiles :: [Profile] -> [(String, Profile)]
buildProfiles lst = [(pName p, p) | p <- lst]

readConfig :: FilePath -> IO (Either ParseException Config)
readConfig path = decodeFileEither path