forked from kmonad/kmonad
-
Notifications
You must be signed in to change notification settings - Fork 0
/
UinputSink.hs
178 lines (150 loc) · 5.41 KB
/
UinputSink.hs
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
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# OPTIONS_GHC -Wno-dodgy-imports #-}
{-|
Module : KMonad.Keyboard.IO.Linux.UinputSink
Description : Using Linux's uinput interface to emit events
Copyright : (c) David Janssen, 2019
License : MIT
Maintainer : [email protected]
Stability : experimental
Portability : portable
-}
module KMonad.Keyboard.IO.Linux.UinputSink
( UinputSink
, UinputCfg(..)
, keyboardName
, vendorCode
, productCode
, productVersion
, postInit
, uinputSink
, defUinputCfg
)
where
import KMonad.Prelude
import Data.Time.Clock.System (getSystemTime)
import Foreign.C.String
import Foreign.C.Types
import System.Posix hiding (sync)
import UnliftIO.Async (async)
import UnliftIO.Process (spawnCommand)
import KMonad.Keyboard.IO.Linux.Types
import KMonad.Util
--------------------------------------------------------------------------------
-- $err
type SinkId = String
-- | A collection of everything that can go wrong with the 'UinputSink'
data UinputSinkError
= UinputRegistrationError SinkId -- ^ Could not register device
| UinputReleaseError SinkId -- ^ Could not release device
| SinkEncodeError SinkId LinuxKeyEvent -- ^ Could not decode event
| EmptyNameError -- ^ Invalid name
deriving Exception
instance Show UinputSinkError where
show (UinputRegistrationError snk) = "Could not register sink with OS: " <> snk
show (UinputReleaseError snk) = "Could not unregister sink with OS: " <> snk
show (SinkEncodeError snk a) = unwords
[ "Could not encode Keyaction"
, show a
, "to bytes for writing to"
, snk
]
show EmptyNameError = "Provided empty name for Uinput keyboard"
makeClassyPrisms ''UinputSinkError
--------------------------------------------------------------------------------
-- $cfg
-- | Configuration of the Uinput keyboard to instantiate
data UinputCfg = UinputCfg
{ _vendorCode :: !CInt
, _productCode :: !CInt
, _productVersion :: !CInt
, _keyboardName :: !String
, _postInit :: !(Maybe String)
} deriving (Eq, Show)
makeClassy ''UinputCfg
-- | Default Uinput configuration
defUinputCfg :: UinputCfg
defUinputCfg = UinputCfg
{ _vendorCode = 0x1235
, _productCode = 0x5679
, _productVersion = 0x0000
, _keyboardName = "KMonad simulated keyboard"
, _postInit = Nothing
}
-- | UinputSink is an MVar to a filehandle
data UinputSink = UinputSink
{ _cfg :: UinputCfg
, _st :: MVar Fd
}
makeLenses ''UinputSink
-- | Return a new uinput 'KeySink' with extra options
uinputSink :: HasLogFunc e => UinputCfg -> RIO e (Acquire KeySink)
uinputSink c = mkKeySink (usOpen c) usClose usWrite
--------------------------------------------------------------------------------
-- FFI calls and type-friendly wrappers
foreign import ccall "acquire_uinput_keysink"
c_acquire_uinput_keysink
:: CInt -- ^ Posix handle to the file to open
-> CString -- ^ Name to give to the keyboard
-> CInt -- ^ Vendor ID
-> CInt -- ^ Product ID
-> CInt -- ^ Version ID
-> IO Int
foreign import ccall "release_uinput_keysink"
c_release_uinput_keysink :: CInt -> IO Int
foreign import ccall "send_event"
c_send_event :: CInt -> CInt -> CInt -> CInt -> CInt -> CInt -> IO Int
-- | Create and acquire a Uinput device
acquire_uinput_keysink :: MonadIO m => Fd -> UinputCfg -> m Int
acquire_uinput_keysink (Fd h) c = liftIO $ do
cstr <- newCString $ c^.keyboardName
c_acquire_uinput_keysink h cstr
(c^.vendorCode) (c^.productCode) (c^.productVersion)
-- | Release a Uinput device
release_uinput_keysink :: MonadIO m => Fd -> m Int
release_uinput_keysink (Fd h) = liftIO $ c_release_uinput_keysink h
-- | Using a Uinput device, send a LinuxKeyEvent to the Linux kernel
send_event :: ()
=> UinputSink
-> Fd
-> LinuxKeyEvent
-> RIO e ()
send_event u (Fd h) e@(LinuxKeyEvent (s', ns', typ, c, val)) = do
liftIO (c_send_event h typ c val s' ns')
`onErr` SinkEncodeError (u^.cfg.keyboardName) e
--------------------------------------------------------------------------------
-- | Create a new UinputSink
usOpen :: HasLogFunc e => UinputCfg -> RIO e UinputSink
usOpen c = do
when (null $ c ^. keyboardName) $ throwM EmptyNameError
fd <- liftIO $ openFd "/dev/uinput"
WriteOnly
#if !MIN_VERSION_unix(2,8,0)
Nothing
#endif
defaultFileFlags
logInfo "Registering Uinput device"
acquire_uinput_keysink fd c `onErr` UinputRegistrationError (c ^. keyboardName)
flip (maybe $ pure ()) (c^.postInit) $ \cmd -> do
logInfo $ "Running UinputSink command: " <> displayShow cmd
void . async . spawnCommand $ cmd
UinputSink c <$> newMVar fd
-- | Close a 'UinputSink'
usClose :: HasLogFunc e => UinputSink -> RIO e ()
usClose snk = withMVar (snk^.st) $ \h -> finally (release h) (close h)
where
release h = do
logInfo "Unregistering Uinput device"
release_uinput_keysink h
`onErr` UinputReleaseError (snk^.cfg.keyboardName)
close h = do
logInfo "Closing Uinput device file"
liftIO $ closeFd h
-- | Write a keyboard event to the sink and sync the driver state. Using an MVar
-- ensures that we can never have 2 threads try to write at the same time.
usWrite :: HasLogFunc e => UinputSink -> KeyEvent -> RIO e ()
usWrite u e = withMVar (u^.st) $ \fd -> do
now <- liftIO getSystemTime
send_event u fd . toLinuxKeyEvent e $ now
send_event u fd . sync $ now