summaryrefslogtreecommitdiff
path: root/src/System/IO/Terminal/Input.hs
blob: efacf4f6248a38784cecc8d5c74f860e21cc1e30 (plain)
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
{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
{-|
Module      : System.IO.Terminal.Input
Copyright   : (c) UU, 2019
License     : MIT
Maintainer  : Tom Smeding
Stability   : experimental
Portability : POSIX, macOS, Windows

Keyboard input for console applications.

A monad transformer is defined that allows reading keys from the
keyboard in symbolic form, i.e. escape sequences have already been
parsed.
-}
module System.IO.Terminal.Input
    (InputT, Input, withInput, liftIO, MonadInput(..)
    ,Key(..)
    ,InfInt(..), readKey)
    where

import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Trans
import Control.Monad.State.Strict
import Data.Maybe
import Numeric.InfInt
import System.IO.Terminal.Render
import System.IO.Terminal.Input.Key
import System.IO.Terminal.Input.SeqParser
-- import qualified Utils.Debug as Debug
import Utils.Time
import Utils.Monad

#if defined(OS_LINUX) || defined(OS_MACOS)
import System.IO.Terminal.Input.Posix
#else
import System.IO.Terminal.Input.Windows
#endif


-- | Escape sequence timeout in milliseconds.
gEscTimeoutMS :: Int
gEscTimeoutMS = 100


-- | The keyboard input monad transformer. The contained monad must be
-- a monad over 'IO'.
newtype InputT m a = InputT (StateT InputState m a)
  deriving newtype (Functor, Applicative, Monad, MonadState InputState, MonadTrans)
  deriving MonadUnliftIO via StateTUnliftIOWrapper InputState m

deriving instance MonadIO m => MonadIO (InputT m)

-- | The non-transformer variant of 'InputT'.
type Input = InputT IO

data InputState = InputState { sKeys :: [Key]
                             , sParser :: SeqParser (Maybe Char) Key
                               -- If the timeout event was already returned, Infinity
                             , sLastKey :: InfInt }

-- | Run an 'Input' computation within an IO-based monad.
withInput :: MonadIO m => InputT m a -> m a
withInput (InputT st) =
    withRawMode $ evalStateT st (InputState [] seqParser Infinity)


-- | The behavior class for the 'InputT' monad transformer.
class Monad m => MonadInput m where
    -- | Timeout in milliseconds; Infinity is no timeout. Returns Nothing
    -- in case the timeout was reached.
    readKeyTimeout :: InfInt -> m (Maybe Key)

instance MonadIO m => MonadInput (InputT m) where
    readKeyTimeout userTimeout =
        get >>= \s -> case sKeys s of
            [] -> do
                let SeqParser parserFunc = sParser s

                beforeKey <- timestampMS <$> liftIO getSystemTime

                -- These timeouts are either 'Finite timeout' in milliseconds, or 'Infinity' in case
                -- no timeout is needed.
                let -- Possibly the amount of time left for the next escape sequence item
                    escTimeout = let delta = Finite beforeKey - sLastKey s
                                     left = Finite gEscTimeoutMS - delta
                                 in if left < 0 then 0 else left
                    -- We wait until either the user timeout or the escape timeout passes, whichever
                    -- is earlier
                    timeoutVal = min userTimeout escTimeout

                -- Debug.logIO ("readKeyTimeout", beforeKey, userTimeout, sLastKey s, escTimeout, timeoutVal)

                ch <- liftIO $ getCharTimeout (toFinite timeoutVal)

                let userTimeoutTriggered = isNothing ch && userTimeout < escTimeout
                -- If we hit the user timeout, return appropriately
                if userTimeoutTriggered
                    then {-Debug.logIO "returning Nothing" >>-} return Nothing
                    else do
                        -- We either got an actual key, or we hit the escape timeout. In either
                        -- case, we advance the escape sequence parser.
                        let (ks, p') = parserFunc ch
                        -- Debug.logIO ("keys", ks)

                        -- If we got an escape timeout, we'll be reporting it so we mark the last
                        -- key as "long ago" to not report an escape timeout twice to the parser.
                        -- Otherwise, we "start the escape timer" by storing the time of receipt of
                        -- this key.
                        afterKey <- timestampMS <$> liftIO getSystemTime
                        let newLastKey = maybe Infinity (const $ Finite afterKey) ch
                        modify $ \s' -> s' { sKeys = ks, sParser = p', sLastKey = newLastKey }

                        -- Either way, we stored some stuff in sKeys, so either that contains keys
                        -- to return, or we got nothing yet and need to retry; recursing handles
                        -- both cases nicely.
                        let userLeft = userTimeout - Finite (afterKey - beforeKey)
                        readKeyTimeout userLeft

            (k:ks) -> do
                put (s { sKeys = ks })
                -- Debug.logIO (Just k)
                return (Just k)

instance MonadInput (RenderT Input) where
    readKeyTimeout = lift . readKeyTimeout

-- | Read a key from standard input without timeout. Shortcut for
-- @readKeyTimeout Infinity@.
readKey :: MonadInput m => m Key
readKey = fromJust <$> readKeyTimeout Infinity