-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMain.hs
218 lines (185 loc) · 8.64 KB
/
Main.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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import Words (wordBank)
import Text.Read (readMaybe)
import Data.Char (isSpace)
import Data.Time (NominalDiffTime, UTCTime, getCurrentTime, addUTCTime, diffUTCTime)
import Data.List (intercalate)
import System.IO (BufferMode (LineBuffering), BufferMode (NoBuffering), hSetBuffering, stdin, hSetEcho)
import System.Random (randomRIO)
import GHC.IO.Handle (hWaitForInput)
-------------------- IO Functions
main :: IO ()
main = do
typingDuration <- promptUserForTypingDuration
case typingDuration of
Just duration -> startTypingLoop duration
Nothing -> do
putStrLn ""
putStrLn "Error: An invalid input was given."
return ()
promptUserForTypingDuration :: IO (Maybe Int)
promptUserForTypingDuration = do
hSetBuffering stdin LineBuffering
hSetEcho stdin True
putStr clearScreen
putStrLn ""
putStrLn "Please enter the duration you would like to type for."
putStrLn ""
readMaybe <$> getLine
startTypingLoop :: Int -> IO ()
startTypingLoop typingDuration = do
hSetBuffering stdin NoBuffering
hSetEcho stdin False
typingLoop typingDuration
{-
| The main loop function which takes a UTCTime deadline value.
This value represents the time limit in which the user can type.
The typingDuration parameter is the amount of seconds the main
typing loop should run for.
-}
typingLoop :: Int -> IO ()
typingLoop typingDuration = do
randomNums <- getListOfRandomInts (getNumOfWords typingDuration) (length wordBank - 1)
let initialWordList = getWordsFromWordBank randomNums
startTime <- getCurrentTime
let deadline = addUTCTime (convertToNominalDiffTime typingDuration) startTime
typingLoop' initialWordList 0 0 typingDuration deadline
typingLoop' :: [String] -> Int -> Int -> Int -> UTCTime -> IO ()
typingLoop' wordList wordIndex numOfCorrectWords typingDuration deadline = do
let allStringGroups = breakStringsIntoGroups $ highlightStringInList wordList wordIndex
let desiredGroups = getDesiredStringGroups allStringGroups wordIndex
putStr clearScreen
putStrLn ""
putStrLn (modifiedUnwords (intercalate ["\n"] desiredGroups))
putStrLn ""
possibleWordFromUser <- getUserInputWithTimer deadline
case possibleWordFromUser of
Just wordFromUser -> do
let wordFromList = wordList !! wordIndex
if wordFromList == wordFromUser then
typingLoop' wordList (wordIndex + 1) (numOfCorrectWords + 1) typingDuration deadline
else
typingLoop' wordList (wordIndex + 1) numOfCorrectWords typingDuration deadline
Nothing -> do
putStr clearScreen
putStrLn ""
putStrLn ("Your typing speed is " ++ show (calcWordsPerMinute numOfCorrectWords typingDuration) ++ " wpm")
-------------------- Getting inputs from the user character by character
{-
| This function gets the input word from the user. This function has a deadline
parameter which is the amount of time the user has to input the next word before
the function times out.
-}
getUserInputWithTimer :: UTCTime -> IO (Maybe String)
getUserInputWithTimer = getUserInputWithTimer' ""
getUserInputWithTimer' :: String -> UTCTime -> IO (Maybe String)
getUserInputWithTimer' xs deadline = do
now <- getCurrentTime
let remainingTime = round (diffUTCTime deadline now * 1000)
inputReady <- hWaitForInput stdin remainingTime
if inputReady then do
inputChar <- getChar
case inputChar of
c | isSpace c -> return (Just xs)
'\DEL' ->
if null xs then
getUserInputWithTimer' "" deadline
else do
putStr removeLastCharacter
getUserInputWithTimer' (init xs) deadline
_ -> do
putChar inputChar
getUserInputWithTimer' (xs ++ [inputChar]) deadline
else return Nothing
-------------------- For generating random numbers
getRandomInt :: Int -> IO Int
getRandomInt upperRange = do randomRIO (0, upperRange) :: IO Int
-- | First argument is the length of the list. The second argument is the upper range for the list values (0 - upperRange)
getListOfRandomInts :: Int -> Int -> IO [Int]
getListOfRandomInts 0 _ = return []
getListOfRandomInts x upperRange = do
num <- getRandomInt upperRange
nums <- getListOfRandomInts (x - 1) upperRange
return (num:nums)
-------------------- Pure Functions
-- | Takes a list of indexes and returns the Strings at thoes indexes in the wordBank
getWordsFromWordBank :: [Int] -> [String]
getWordsFromWordBank = map (wordBank !!)
-- | Returns a the given list with the word at the given index highlighted using the textColorCyan function
highlightStringInList :: [String] -> Int -> [String]
highlightStringInList words wordIndex = highlightStringInList' words wordIndex 0
highlightStringInList' :: [String] -> Int -> Int-> [String]
highlightStringInList' [] _ _ = []
highlightStringInList' (word:words) wordIndex count
| wordIndex == count = (textColorCyan ++ word ++ textColorReset) : words
| otherwise = word : highlightStringInList' words wordIndex (count + 1)
{-
| Takes a list of strings and splits them into multiple lists, each list's length is determined by
the line character limit function.
-}
breakStringsIntoGroups :: [String] -> [[String]]
breakStringsIntoGroups [] = []
breakStringsIntoGroups (word:words) = breakStringsIntoGroups' words [word] 0
breakStringsIntoGroups' :: [String] -> [String] -> Int -> [[String]]
breakStringsIntoGroups' [] currentSubList _ = [currentSubList]
breakStringsIntoGroups' (word:words) currentSubList count
| count + wordLength >= lineCharacterLimit = currentSubList : breakStringsIntoGroups' words [word] wordLength
| otherwise = breakStringsIntoGroups' words (currentSubList ++ [word]) (count + wordLength)
where
wordLength = length (removeEscapeSequenceFromString word)
{-
| Takes a list of lists of strings and a word index. Returns both the list that the corresponding word to
the given word index is contained in, and the following list.
-}
getDesiredStringGroups :: [[String]] -> Int -> [[String]]
getDesiredStringGroups groups wordIndex = getDesiredStringGroups' groups wordIndex 0
getDesiredStringGroups' :: [[String]] -> Int -> Int -> [[String]]
getDesiredStringGroups' [] _ _ = []
getDesiredStringGroups' (group:groups) wordIndex count
| count + length group <= wordIndex = getDesiredStringGroups' groups wordIndex (count + length group)
| otherwise = group : [nextGroup]
where
nextGroup
| null groups = []
| otherwise = head groups
-- | Returns the given string will all Escape sequences (eg. \ESC***m) removed
removeEscapeSequenceFromString :: String -> String
removeEscapeSequenceFromString [] = []
removeEscapeSequenceFromString (x:xs)
| x == '\ESC' = removeEscapeSequenceFromString (getAllElementsAfterPoint xs 'm')
| otherwise = x : removeEscapeSequenceFromString xs
-- | A modified version of unwords with one distinction: any strings that are simply a line break, do not get spaces appended after them
modifiedUnwords :: [String] -> String
modifiedUnwords [] = []
modifiedUnwords (word:words)
| word == "\n" = word ++ modifiedUnwords words
| otherwise = word ++ " " ++ modifiedUnwords words
-- | Returns the entire list up until and including the given value
getAllElementsUpToPoint :: Eq a => [a] -> a -> [a]
getAllElementsUpToPoint xs val = takeWhile (/= val) xs ++ [val]
-- | Returns the remainder of the list after the given value
getAllElementsAfterPoint :: Eq a => [a] -> a -> [a]
getAllElementsAfterPoint xs val = tail (dropWhile (/= val) xs)
calcWordsPerMinute :: Int -> Int -> Int
calcWordsPerMinute numOfWords typingDuration = floor ((numOfWords `divideIntToFloat` typingDuration) * 60.0)
divideIntToFloat :: Int -> Int -> Float
divideIntToFloat x y = fromIntegral x / fromIntegral y
convertToNominalDiffTime :: Int -> NominalDiffTime
convertToNominalDiffTime seconds = realToFrac (toRational seconds) :: NominalDiffTime
-- | This function takes the typing duration and from it gets the number of words to generate.
getNumOfWords :: Int -> Int
getNumOfWords typingDuration
| typingDuration < 10 = 50
| otherwise = typingDuration * 5
removeLastCharacter :: String
removeLastCharacter = "\b \b"
-- | The character limit per line when printing the word list.
lineCharacterLimit :: Int
lineCharacterLimit = 80
-------------------- ANSI escape sequences
-- all excape codes can be found here https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
textColorReset :: String
textColorReset = "\ESC[0m"
textColorCyan :: String
textColorCyan = "\ESC[36m"
clearScreen :: String
clearScreen = "\ESC[2J"