{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TemplateHaskell #-} module Main where import Control.Monad (filterM, when) import Data.Char (isAscii, isPrint) import Data.FileEmbed (embedStringFile) import Data.List.Split (splitOn) import qualified Data.Text as T import Data.Word (Word8) import System.Console.CmdArgs (Data, Typeable, args, cmdArgs, def, details, help, name, program, summary, typ, (&=)) import System.Directory (doesFileExist) import System.Random (randomRIO) import Text.Wrap (WrapSettings (..), wrapText) import UI (run) data Config = Config { fg_empty :: Word8 , fg_error :: Word8 , files :: [FilePath] , height :: Int , max_paragraph_len :: Int , min_paragraph_len :: Int , nonsense_len :: Int , paragraph :: Bool , reflow_ :: Bool , tab :: Int , width :: Int } deriving (Show, Data, Typeable) toAscii :: Int -> String -> String toAscii tabWidth = concatMap toAscii' where toAscii' c | c == '\t' = replicate tabWidth ' ' | c == '‘' || c == '’' = "'" | c == '“' || c == '”' = "\"" | c == '–' || c == '—' = "-" | c == '…' = "..." | isAscii c && (isPrint c || c == '\n') = [c] | otherwise = "" trimEmptyLines :: String -> String trimEmptyLines = (++ "\n") . f . f where f = reverse . dropWhile (== '\n') config :: Config config = Config { fg_empty = 8 &= typ "COLOUR" &= help "The ANSI colour code for empty (not yet typed) text" , fg_error = 1 &= typ "COLOUR" &= help "The ANSI colour code for errors" , height = 20 &= typ "LINES" &= help "The maximum number of lines to sample (default: 20)" , max_paragraph_len = 750 &= typ "WORDS" &= help "The maximum length of a sampled paragraph (default: 750)" , min_paragraph_len = 250 &= typ "WORDS" &= help "The minimum length of a sampled paragraph (default: 250)" , nonsense_len = 500 &= name "l" &= typ "WORDS" &= help "The length of nonsense to generate (default: 500)" , paragraph = def &= help "Sample a paragraph from the input files" , reflow_ = def &= help "Reflow paragraph to the target width" , tab = 4 &= typ "SIZE" &= help "The size of a tab in spaces (default: 4)" , width = 80 &= typ "CHARS" &= help "The width at which to wrap lines (default: 80)" , files = def &= args &= typ "FILES" } &= summary "Gotta Go Fast 0.3.0.6" &= help "Practice typing and measure your WPM and accuracy." &= program "gotta-go-fast" &= details (lines $(embedStringFile "details.txt")) wrap :: Int -> String -> String wrap width = T.unpack . wrapText wrapSettings width . T.pack wrapSettings = WrapSettings {preserveIndentation = True, breakLongWords = True} -- wordWeights.txt is taken from -- https://en.wiktionary.org/wiki/Wiktionary:Frequency_lists#TV_and_movie_scripts -- (and cleaned up a little with some throwaway sed) wordWeights :: [(String, Int)] wordWeights = map ((\[w, f] -> (w, read f)) . words) . lines $ $(embedStringFile "wordWeights.txt") totalWeight :: Int totalWeight = sum . map snd $ wordWeights weightedRandomWord :: IO String weightedRandomWord = do r <- randomRIO (0, totalWeight - 1) return $ go r wordWeights where go r ((w, f):rest) | r < f = w | otherwise = go (r - f) rest loopWhile :: Monad m => (a -> Bool) -> m a -> m a loopWhile p mx = do x <- mx if p x then loopWhile p mx else return x -- Generates nonsense which is superficially similar to English. Similar in the -- sense that the frequency of words in the generated text is approximately the -- same as the frequency of words in actual usage. nonsense :: Config -> IO String nonsense c = do words <- go (nonsense_len c) Nothing return $ (wrap (width c) . unwords $ words) ++ "\n" where go n lastWord | n <= 0 = return [] | otherwise = do word <- loopWhile ((== lastWord) . Just) weightedRandomWord rest <- go (n - length word - 1) (Just word) -- extra 1 to count the space return $ word : rest sample :: Config -> String -> IO String sample c file = if paragraph c && not (null paragraphs) then sampleParagraph else sampleLines where sampleParagraph = do r <- randomRIO (0, length paragraphs - 1) return $ (if reflow_ c then reflow else wrap (width c)) (paragraphs !! r) ++ "\n" sampleLines = do r <- randomRIO (0, max 0 $ length (lines ascii) - height c) return . trimEmptyLines . chop . wrap (width c) . chop . unlines . drop r $ lines ascii paragraphs = filter ((\l -> l >= min_paragraph_len c && l <= max_paragraph_len c) . length) . map unlines . splitOn [""] . lines $ ascii reflow = wrap (width c) . map (\case '\n' -> ' ' c -> c) ascii = toAscii (tab c) file chop = unlines . take (height c) . lines main :: IO () main = do c <- cmdArgs config fs <- filterM doesFileExist $ files c target <- case fs of [] -> nonsense c _ -> do r <- randomRIO (0, length fs - 1) file <- readFile $ fs !! r sample c file loop <- run (fg_empty c) (fg_error c) target when loop main