evdev-2.0.0.0: src/Evdev.hs
{-# OPTIONS_GHC -fno-state-hack #-}
-- | The main module for working with devices and events.
module Evdev (
-- * Devices
Device,
newDevice,
nextEvent,
evdevDir,
-- ** Properties
deviceName,
devicePath,
deviceProperties,
deviceFd,
-- ** Grabbing
grabDevice,
ungrabDevice,
-- * Events
Event(..),
EventData(..),
KeyEvent(..),
EventCode(..),
EventValue(..),
-- * Lower-level types
-- | These correspond more directly to C's /input_event/ and /timeval/.
-- They are used internally, but may be useful for advanced users.
LL.CEvent(..),
toCEvent,
fromCEvent,
LL.CTimeVal(..),
toCTimeVal,
fromCTimeVal,
) where
import Control.Arrow ((&&&))
import Control.Monad (filterM,join)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as BS
import Data.Int (Int32)
import Data.List.Extra (enumerate)
import Data.Map ((!?), Map)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Ratio ((%))
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Time.Clock (DiffTime)
import Data.Word (Word16)
import Foreign ((.|.))
import Foreign.C (CUInt)
import Foreign.C.Error (Errno(Errno),errnoToIOError)
import System.Posix.ByteString (Fd,RawFilePath)
import System.Posix.IO.ByteString (fdToHandle)
import qualified Evdev.LowLevel as LL
import Evdev.Codes
-- stores path that was originally used, as it seems impossible to recover this later
-- We don't allow the user to access the underlying low-level C device.
-- | An input device.
data Device = Device { cDevice :: LL.Device, devicePath :: RawFilePath }
instance Show Device where
show = show . devicePath
-- | An input event, including the timestamp.
data Event = Event
{ eventData :: EventData
, eventTime :: DiffTime
}
deriving (Eq, Ord, Show)
--TODO name?
-- | An input event, without the timestamp.
-- Each constructor corresponds to one [event type](https://www.kernel.org/doc/html/latest/input/event-codes.html#event-types), except for 'UnknownEvent'.
data EventData
= SyncEvent SyncEvent
| KeyEvent Key KeyEvent
| RelativeEvent RelativeAxis EventValue
| AbsoluteEvent AbsoluteAxis EventValue
| MiscEvent MiscEvent EventValue
| SwitchEvent SwitchEvent EventValue
| LEDEvent LEDEvent EventValue
| SoundEvent SoundEvent EventValue
| RepeatEvent RepeatEvent EventValue
| ForceFeedbackEvent EventCode EventValue
| PowerEvent EventCode EventValue
| ForceFeedbackStatusEvent EventCode EventValue
| UnknownEvent Word16 EventCode EventValue {- ^ We include this primarily so that 'fromCEvent' can be well-defined -
let us know if you ever actually see one emitted by a device, as it would likely
indicate a shortcoming in the library. -}
deriving (Eq, Ord, Read, Show)
-- | A direct representation of the /code/ field of the C /input_event/, for when there is no obvious meaningful sum type.
newtype EventCode = EventCode Word16
deriving stock (Eq, Ord, Read, Show)
deriving newtype (Enum, Integral, Real, Num) --TODO all this baggage to make 'toEnum'' slightly easier?
-- | A direct representation of the /value/ field of the C /input_event/, for when there is no obvious meaningful sum type.
newtype EventValue = EventValue Int32
deriving stock (Eq, Ord, Read, Show)
deriving newtype (Enum, Integral, Real, Num)
-- | The status of a key.
data KeyEvent
= Released
| Pressed
| Repeated
deriving (Bounded, Enum, Eq, Ord, Read, Show)
convertFlags :: Set LL.ReadFlag -> CUInt
convertFlags = fromIntegral . foldr ((.|.) . fromEnum) 0
defaultReadFlags :: Set LL.ReadFlag
defaultReadFlags = Set.fromList [LL.Normal, LL.Blocking]
-- | Prevent other clients (including kernel-internal ones) from receiving events. Often a bad idea.
grabDevice :: Device -> IO ()
grabDevice = grabDevice' LL.LibevdevGrab
-- | Release a grabbed device.
ungrabDevice :: Device -> IO ()
ungrabDevice = grabDevice' LL.LibevdevUngrab
-- | Get the next event from the device.
nextEvent :: Device -> IO Event
nextEvent dev =
fromCEvent <$> throwCErrors "nextEvent" (Right dev) (LL.nextEvent (cDevice dev) (convertFlags defaultReadFlags))
fromCEvent :: LL.CEvent -> Event
fromCEvent (LL.CEvent t (EventCode -> c) (EventValue -> v) time) = Event event $ fromCTimeVal time
where
event = fromMaybe unknown $ toEnum' t >>= \case
EvSyn -> SyncEvent <$> toEnum' c
EvKey -> KeyEvent <$> toEnum' c <*> toEnum' v
EvRel -> RelativeEvent <$> toEnum' c <*> pure v
EvAbs -> AbsoluteEvent <$> toEnum' c <*> pure v
EvMsc -> MiscEvent <$> toEnum' c <*> pure v
EvSw -> SwitchEvent <$> toEnum' c <*> pure v
EvLed -> LEDEvent <$> toEnum' c <*> pure v
EvSnd -> SoundEvent <$> toEnum' c <*> pure v
EvRep -> RepeatEvent <$> toEnum' c <*> pure v
EvFf -> Just $ ForceFeedbackEvent c v
EvPwr -> Just $ PowerEvent c v
EvFfStatus -> Just $ ForceFeedbackStatusEvent c v
unknown = UnknownEvent t c v
toCEvent :: Event -> LL.CEvent
toCEvent (Event e time) = case e of
-- from kernel docs, 'EV_SYN event values are undefined' - we always seem to see 0, so may as well use that
SyncEvent (fe -> c) -> LL.CEvent (fe EvSyn) c 0 cTime
KeyEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvKey) c v cTime
RelativeEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvRel) c v cTime
AbsoluteEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvAbs) c v cTime
MiscEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvMsc) c v cTime
SwitchEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvSw) c v cTime
LEDEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvLed) c v cTime
SoundEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvSnd) c v cTime
RepeatEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvRep) c v cTime
ForceFeedbackEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvFf) c v cTime
PowerEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvPwr) c v cTime
ForceFeedbackStatusEvent (fe -> c) (fe -> v) -> LL.CEvent (fe EvFfStatus) c v cTime
UnknownEvent (fe -> t) (fe -> c) (fe -> v) -> LL.CEvent t c v cTime
where
--TODO this isn't entirely safe in general, though it's really no worse than 'fromEnum'
-- if we could tell C2HS which int type each #defined enum corresponded to, we could check this statically
fe :: (Enum a, Integral b) => a -> b
fe = fromIntegral . fromEnum
cTime = toCTimeVal time
fromCTimeVal :: LL.CTimeVal -> DiffTime
fromCTimeVal (LL.CTimeVal s us) =
fromRational $ fromIntegral s + (fromIntegral us % 1_000_000)
--TODO QuickCheck inverse
toCTimeVal :: DiffTime -> LL.CTimeVal
toCTimeVal t = LL.CTimeVal n (round $ f * 1_000_000)
where (n,f) = properFraction t
-- | Create a device from a valid path - usually /\/dev\/input\/eventX/ for some /X/.
newDevice :: RawFilePath -> IO Device
newDevice path = do
dev <- throwCErrors "newDevice" (Left path) $ LL.newDevice path
return $ Device dev path
-- | The usual directory containing devices (/"\/dev\/input"/).
evdevDir :: RawFilePath
evdevDir = "/dev/input"
deviceName :: Device -> IO ByteString
deviceName = join . LL.deviceName . cDevice
deviceFd :: Device -> IO Fd
deviceFd = LL.deviceFd . cDevice
deviceProperties :: Device -> IO [DeviceProperty]
deviceProperties dev = filterM (LL.hasProperty $ cDevice dev) enumerate
{- Util -}
-- run the action, throwing a relevant exception if the C errno is not 0
throwCErrors :: String -> Either ByteString Device -> IO (Errno, a) -> IO a
throwCErrors func pathOrDev x = do
(errno, res) <- x
case errno of
Errno 0 -> return res
Errno n -> do
(handle,path) <- case pathOrDev of
Left path -> return (Nothing,path)
Right dev -> do
h <- fdToHandle =<< deviceFd dev
return (Just h, devicePath dev)
ioError $ errnoToIOError func (Errno $ abs n) handle (Just $ BS.unpack path)
grabDevice' :: LL.GrabMode -> Device -> IO ()
grabDevice' mode dev = throwCErrors "grabDevice" (Right dev) $ LL.grabDevice (cDevice dev) mode
{-
TODO this is a workaround until c2hs has a better story for enum conversions
when we remove it we can get rid of '-fno-state-hack'
based on profiling, and Debug.Trace, it seems that 'enumMap' is computed no more times than necessary
(6 - number of combinations of a and k that it is called with)
but based on https://www.reddit.com/r/haskell/comments/grskne/help_reasoning_about_performance_memoization/,
it's possible that behaviour is worse without profiling on (argh...)
open c2hs issue
we perhaps essentially want the `CEnum` class proposed at: https://github.com/haskell/c2hs/issues/78
but perhaps belonging (at least initially) in c2hs rather than base, for expediency
this doesn't necessarily consider enum defines though - discussion is around capturing the semantics of actual C enums
alternatively, monomorphic functions for each type, as with c2hs's with* functions
-}
toEnum' :: forall k a. (Integral k, Bounded a, Enum a) => k -> Maybe a
toEnum' = (enumMap !?)
where
--TODO HashMap, IntMap?
enumMap :: Map k a
enumMap = Map.fromList $ map (fromIntegral . fromEnum &&& id) enumerate