summaryrefslogtreecommitdiff
path: root/src/System/IO/Terminal/Input/Posix.hs
blob: 85f02680816aef7212162711062324e6471b0909 (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
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
179
180
181
182
183
184
185
186
187
{-|
Module      : System.IO.Terminal.Input.Posix
Copyright   : (c) UU, 2019
License     : MIT
Maintainer  : Tom Smeding
Stability   : experimental
Portability : POSIX, macOS

Platform-specific submodule of "System.IO.Terminal.Input" that works on POSIX-like platforms.
-}
module System.IO.Terminal.Input.Posix
    (withRawMode, getCharTimeout
    ,seqParser)
    where

import Control.Monad
import Control.Monad.IO.Class
import Data.Char
import Data.Function
import Data.List
import Data.Maybe
import System.IO
import System.IO.Terminal.Input.Key
import System.IO.Terminal.Input.SeqParser
import System.Timeout


-- | Run an IO-monadic computation with terminal input buffering and echo
-- turned off.
withRawMode :: MonadIO m => m a -> m a
withRawMode = withNoBuffering . withNoEcho
  where
    withNoBuffering = with (hGetBuffering stdin `takeLeft` hSetBuffering stdin NoBuffering) (hSetBuffering stdin)
    withNoEcho      = with (hGetEcho stdin      `takeLeft` hSetEcho stdin False)            (hSetEcho stdin)
    with start stop act = do
        val <- liftIO start
        res <- act
        _ <- liftIO $ stop val
        return res
    takeLeft a b = a >>= \x -> b >> return x


peekIO :: Show a => IO a -> IO a
-- peekIO act = act >>= \x -> print ("peekIO", x) >> return x
peekIO = id

-- | Get a character from standard input, returning Nothing if no character
-- has appeared after the specified number of milliseconds.
getCharTimeout :: Maybe Int -> IO (Maybe Char)
getCharTimeout (Just timeoutms) = peekIO (timeout (1000 * timeoutms) getChar)
getCharTimeout Nothing = peekIO (Just <$> getChar)


-- | An ANSI escape sequence parser. Pass Nothing to the input if the time
-- between the surrounding characters was "very long", for some definition
-- of "very long" (a reasonable value is ~10ms). This timeout value is used
-- to determine whether certain characters belong together in an escape
-- sequence or are just unrelated keys.
seqParser :: SeqParser (Maybe Char) Key
seqParser = SeqParser $ \case
    Nothing     -> ([], seqParser)
    Just '\ESC' -> ([], escParser)
    Just c      -> ([recogSingle c], seqParser)
  where
    escParser :: SeqParser (Maybe Char) Key
    escParser = SeqParser $ \case
        Nothing     -> ([KEsc], seqParser)
        Just '\ESC' -> ([], escEscParser)
        Just '['    -> ([], ansiParser)
        Just c      -> ([addAlt (recogSingle c)], seqParser)

    escEscParser :: SeqParser (Maybe Char) Key
    escEscParser = SeqParser $ \case
        Nothing     -> ([addAlt KEsc], seqParser)
        Just '['    -> ([], fmap1st addAlt ansiParser)
        Just '\ESC' -> ([addAlt KEsc], escParser)
        Just c      -> ([addAlt KEsc, KChar c], seqParser)

    ansiParser :: SeqParser (Maybe Char) Key
    ansiParser = fmap recogSequence (untilRangeParser (chr 0x40, chr 0x7e) 64)
                    `followedBy` seqParser
      where
        recogSequence :: Maybe [Char] -> Key
        recogSequence mstr = fromMaybe (KInvalid (show mstr)) $ do
            str <- mstr
            guard (not $ null str)
            guard (length str < 64)
            args <- parseArguments (init str)
            recognise args (last str)

    parseArguments :: String -> Maybe [Int]
    parseArguments = collect . groupBy ((==) `on` isDigit)
      where
        collect :: [String] -> Maybe [Int]
        collect []
            = Just []
        collect [num] | Just val <- parseNum num
            = Just [val]
        collect (num:sep:args) | Just val <- parseNum num, validSep sep
            = (val:) <$> collect args
        collect _
            = Nothing

    parseNum :: String -> Maybe Int
    parseNum "" = Nothing
    parseNum str
        | all isDigit str = Just (read str)
        | otherwise       = Nothing

    validSep :: String -> Bool
    validSep ";" = True
    validSep ":" = True
    validSep _   = False

    -- Terminating character is included in string
    -- If a timeout occurs, returns Nothing
    untilRangeParser :: (Char, Char) -> Int -> SeqParserOnce (Maybe Char) (Maybe [Char])
    untilRangeParser (from, to) maxlen = SeqParserOnce (charHandler 0)
      where
        charHandler :: Int -> Maybe Char -> SeqParserOnce' (Maybe Char) (Maybe [Char])
        charHandler _   Nothing    = SeqParserVal Nothing
        charHandler len (Just c)
            | len >= maxlen        = SeqParserVal Nothing
            | from <= c && c <= to = SeqParserVal (Just [c])
            | otherwise            = SeqParserOnce' $ \mc' -> let rec = charHandler (len + 1) mc'
                                                              in fmap (fmap (c :)) rec

    recognise :: [Int] -> Char -> Maybe Key
    recognise args   'A' = Just (arrowMod args KUp)
    recognise args   'B' = Just (arrowMod args KDown)
    recognise args   'C' = Just (arrowMod args KRight)
    recognise args   'D' = Just (arrowMod args KLeft)
    recognise []     'H' = Just KHome
    recognise [1,2]  'H' = Just (addShift KHome)
    recognise [1,5]  'H' = Just (addCtrl KHome)
    recognise [1,6]  'H' = Just (addShift $ addCtrl KHome)
    recognise [1,9]  'H' = Just (addAlt KHome)
    recognise [1,13] 'H' = Just (addCtrl $ addAlt KHome)
    recognise [1,10] 'H' = Just (addShift $ addAlt KHome)
    recognise [1,14] 'H' = Just (addCtrl $ addShift $ addAlt KHome)
    recognise []     'F' = Just KEnd
    recognise [1,2]  'F' = Just (addShift KEnd)
    recognise [1,5]  'F' = Just (addCtrl KEnd)
    recognise [1,6]  'F' = Just (addShift $ addCtrl KEnd)
    recognise [1,9]  'F' = Just (addAlt KEnd)
    recognise [1,13] 'F' = Just (addCtrl $ addAlt KEnd)
    recognise [1,10] 'F' = Just (addShift $ addAlt KEnd)
    recognise [1,14] 'F' = Just (addCtrl $ addShift $ addAlt KEnd)
    recognise _      'Z' = Just (addShift KTab)
    recognise []     '~' = Just KDelete
    recognise [1]    '~' = Just KHome
    recognise [3]    '~' = Just KDelete
    recognise [4]    '~' = Just KEnd
    recognise [5]    '~' = Just KPageUp
    recognise [6]    '~' = Just KPageDown
    recognise [n,2]  '~' = addShift <$> recognise [n] '~'
    recognise [n,5]  '~' = addCtrl  <$> recognise [n] '~'
    recognise _      _   = Nothing

    recogSingle :: Char -> Key
    recogSingle '\HT'  = KTab
    recogSingle '\LF'  = KEnter
    recogSingle '\CR'  = KEnter
    recogSingle '\DEL' = KBackspace
    recogSingle '\NUL' = addCtrl (KChar ' ')
    recogSingle c
        | ord c < 27                = addCtrl (KChar (chr $ ord 'a' - 1 + ord c))
        | 32 <= ord c, ord c <= 126 = KChar c
        | otherwise                 = KInvalid [c]

    arrowMod :: [Int] -> Key -> Key
    arrowMod [1,2] = addShift
    arrowMod [1,3] = addAlt
    arrowMod [1,5] = addCtrl
    arrowMod [1,6] = addCtrl . addShift
    arrowMod [1,7] = addCtrl . addAlt
    arrowMod _     = id  -- in particular, []

    addCtrl, addShift, addAlt :: Key -> Key
    addCtrl  = kmodify (\km -> km { kCtrl  = True })
    addShift = kmodify (\km -> km { kShift = True })
    addAlt   = kmodify (\km -> km { kAlt   = True })

    kmodify :: (Key -> Key) -> Key -> Key
    kmodify f k@(KMod {})  = f k
    kmodify _ (KInvalid s) = KInvalid s
    kmodify f k            = f (KMod False False False k)