packages feed

fortytwo (empty) → 0.0.1

raw patch · 29 files changed

+910/−0 lines, 29 filesdep +ansi-terminaldep +asyncdep +basesetup-changed

Dependencies added: ansi-terminal, async, base, doctest, fortytwo, hspec, process, terminfo, text

Files

+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2017-Present Gianluca Guarini++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,124 @@+# fortytwo++_Interactive terminal prompt_++[![Build Status][travis-image]][travis-url]+[![MIT License][license-image]][license-url]++<img src="fortytwo.jpg" width="100%" />++## Installation++```sh+cabal install fortytwo+```++## Demo++<img src="demo.gif" />++## API++### Prompts++#### Input+Ask a simple question requiring the user to type any string. It returns always a `String`++```hs+import FortyTwo (input)++main :: IO String+main = input "What's your name?"+```++`inputWithDefault` will let you define a fallback answer if no answer will be provided+```hs+import FortyTwo (inputWithDefault)++main :: IO String+main = inputWithDefault "What's your name?" "Undefined"+```++#### Confirm+Confirm prompt returning a boolean either `True` or `False`. If no answer will be provided it will return `False`++```hs+import FortyTwo (confirm)++main :: IO Bool+main = confirm "Are you old enough to see this?"+```++`confirmWithDefault` will let you define a fallback answer if no answer will be provided+```hs+import FortyTwo (confirmWithDefault)++main :: IO Bool+main = confirmWithDefault "What's your name?" "Undefined"+```++#### Password+Password prompt hiding the values typed by the user. It will always return a `String`++```hs+import FortyTwo (password)++main :: IO String+main = password "What's your secret password?"+```++#### Select+Select prompt letting users decide between one of many possible answers. It will always return a `String`++```hs+import FortyTwo (select)++main :: IO Bool+main = select+         "What's your favourite color?"+         ["Red", "Yellow", "Blue"]+```++`selectWithDefault` will let you define a fallback answer if no answer will be provided+```hs+import FortyTwo (selectWithDefault)++main :: IO Bool+main = selectWithDefault+         "What's your favourite color?"+         ["Red", "Yellow", "Blue"]+         "Red"+```++#### Multiselect+Multiselect prompt letting users decide between multiple possible choices. It will always return a collection `[String]`++```hs+import FortyTwo (multiselect)++main :: IO Bool+main = multiselect+         "What are your favourite films?"+         ["Titanic", "Matrix", "The Gladiator"]+```++`multiselectWithDefault` will let you define a fallback answer if no answer will be provided+```hs+import FortyTwo (multiselectWithDefault)++main :: IO Bool+main = multiselectWithDefault+         "What are your favourite films?"+         ["Titanic", "Matrix", "The Gladiator"]+         ["The Gladiator"]+```++# Inspiration++This script is heavily inspired by [survey (golang)](https://github.com/AlecAivazis/survey)++[travis-image]:https://img.shields.io/travis/GianlucaGuarini/fortytwo.svg?style=flat-square+[travis-url]:https://travis-ci.org/GianlucaGuarini/fortytwo++[license-image]:http://img.shields.io/badge/license-MIT-000000.svg?style=flat-square+[license-url]:LICENSE
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/ConfirmExample.hs view
@@ -0,0 +1,9 @@+module ConfirmExample where++import FortyTwo (confirm, confirmWithDefault)++main :: IO Bool+main = do+  confirm "Do you like music?"+  confirmWithDefault "Do you like pop music?" False+  confirmWithDefault "Do you play an instrument?" True
+ examples/InputExample.hs view
@@ -0,0 +1,8 @@+module InputExample where++import FortyTwo (input, inputWithDefault)++main :: IO String+main = do+  input "What's your name?"+  inputWithDefault "How old are you?" "22"
+ examples/Main.hs view
@@ -0,0 +1,16 @@+module Main where++import qualified InputExample+import qualified ConfirmExample+import qualified PasswordExample+import qualified SelectExample+import qualified MultiselectExample++main :: IO()+main = do+  InputExample.main+  ConfirmExample.main+  MultiselectExample.main+  SelectExample.main+  PasswordExample.main+  return ()
+ examples/MultiselectExample.hs view
@@ -0,0 +1,11 @@+module MultiselectExample where++import FortyTwo (multiselect, multiselectWithDefault)++main :: IO [String]+main = do+  multiselect "Which kind of sports do you like?" ["Soccer", "Tennis", "Golf"]+  multiselectWithDefault+    "What are your favourite books?"+    ["1984", "Moby Dick", "The Hitchhiker's Guide to the Galaxy"]+    ["1984", "The Hitchhiker's Guide to the Galaxy"]
+ examples/PasswordExample.hs view
@@ -0,0 +1,6 @@+module PasswordExample where++import FortyTwo (password)++main :: IO String+main = password "What's your secret password?"
+ examples/SelectExample.hs view
@@ -0,0 +1,8 @@+module SelectExample where++import FortyTwo (select, selectWithDefault)++main :: IO String+main = do+  select "What's your favourite color?" ["Red", "Yellow", "Blue"]+  selectWithDefault "What's your favourite film?" ["28 Days Later", "Blade Runner", "Matrix"] "28 Days Later"
+ fortytwo.cabal view
@@ -0,0 +1,74 @@+name:                fortytwo+version:0.0.1+synopsis:            Interactive terminal prompt+homepage:            https://github.com/gianlucaguarini/fortytwo#readme+license:             MIT+license-file:        LICENSE+author:              Gianluca Guarini+maintainer:          gianluca.guarini@gmail.com+copyright:           Gianlua Guarini+category:            Web+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:     FortyTwo,+                       FortyTwo.Types,+                       FortyTwo.Constants,+                       FortyTwo.Utils,+                       FortyTwo.TheAnswerToEverything,+                       FortyTwo.Prompts.Input,+                       FortyTwo.Prompts.Confirm,+                       FortyTwo.Prompts.Password,+                       FortyTwo.Prompts.Select,+                       FortyTwo.Prompts.Multiselect,+                       FortyTwo.Renderers.Question,+                       FortyTwo.Renderers.Password,+                       FortyTwo.Renderers.Confirm,+                       FortyTwo.Renderers.Select+                       FortyTwo.Renderers.Multiselect+  build-depends:       base >= 4.7 && < 5,+                       text <= 1.2.2.2,+                       terminfo <= 0.4.1.0,+                       ansi-terminal >= 0.6.0.0 && <= 0.7.1.1+  default-language:    Haskell2010++executable             examples+  main-is:             Main.hs+  other-modules:       InputExample,+                       ConfirmExample,+                       PasswordExample,+                       SelectExample,+                       MultiselectExample+  hs-source-dirs:      examples+  build-depends:       base >= 4.7 && < 5,+                       fortytwo+  default-language:    Haskell2010++Test-Suite spec+  type:              exitcode-stdio-1.0+  default-language:  Haskell2010+  main-is:           Main.hs+  other-modules:     Specs.Core,+                     Specs.Utils+  hs-source-dirs:    test+  build-depends:     fortytwo,+                     base >=4.9 && <4.10,+                     process >=1.4 && <1.5,+                     async >=2.1 && <2.2,+                     hspec >= 2.2++Test-Suite doctest+  type:              exitcode-stdio-1.0+  default-language:  Haskell2010+  main-is:           doctests.hs+  hs-source-dirs:    test+  build-depends:     fortytwo,+                     base,+                     doctest >= 0.8.0++source-repository head+  type:     git+  location: https://github.com/gianlucaguarini/fortytwo
+ src/FortyTwo.hs view
@@ -0,0 +1,20 @@+module FortyTwo+    (+      theAnswerToEverything,+      input,+      inputWithDefault,+      confirm,+      confirmWithDefault,+      password,+      select,+      selectWithDefault,+      multiselect,+      multiselectWithDefault+    ) where++import FortyTwo.Prompts.Input+import FortyTwo.Prompts.Confirm+import FortyTwo.Prompts.Password+import FortyTwo.Prompts.Select+import FortyTwo.Prompts.Multiselect+import FortyTwo.TheAnswerToEverything
+ src/FortyTwo/Constants.hs view
@@ -0,0 +1,41 @@+module FortyTwo.Constants where++upKey :: String+upKey = "\ESC[A"++downKey :: String+downKey = "\ESC[B"++rightKey :: String+rightKey = "\ESC[C"++leftKey :: String+leftKey = "\ESC[D"++delKey :: String+delKey = "\DEL"++enterKey :: String+enterKey = "\n"++spaceKey :: String+spaceKey = " "++emptyString :: String+emptyString = ""++-- | Char used to highlight a option+focusIcon :: Char+focusIcon = '❯'++-- | Char to identify the options selected+selectedIcon :: Char+selectedIcon = '◉'++-- | Char to identify the options not yet selected+unselectedIcon :: Char+unselectedIcon = '◯'++-- | Char used to hide the password letters+passwordHiddenChar :: Char+passwordHiddenChar = '*'
+ src/FortyTwo/Prompts/Confirm.hs view
@@ -0,0 +1,40 @@+module FortyTwo.Prompts.Confirm (confirm, confirmWithDefault) where++import qualified Data.Text as T++import FortyTwo.Renderers.Confirm (renderConfirm)+import FortyTwo.Renderers.Question (renderQuestion)+import FortyTwo.Utils (clearLines, flush)+import FortyTwo.Constants (emptyString)++-- | Normalize a string transforming it to lowercase and trimming it and getting either n or y+normalizeString :: String -> String+normalizeString s = take 1 $ T.unpack $ T.strip . T.toLower $ T.pack s++-- | Get a clean user input string+getCleanConfirm :: IO String+getCleanConfirm = do s <- getLine; return $ normalizeString s++-- | Ask a confirm falling back to a default value if no answer will be provided+-- confirmWithDefault "Do you like music?" True+confirmWithDefault :: String -> Bool -> IO Bool+confirmWithDefault question defaultAnswer = do+  putStrLn emptyString+  renderQuestion question defaultAnswerHumanized emptyString+  renderConfirm+  flush+  answer <- getCleanConfirm+  clearLines 1+  if answer == "n" || (answer /= "y" && not defaultAnswer) then do+    renderQuestion question emptyString "no"+    return False+  else do+    renderQuestion question emptyString "yes"+    return True+  where+    defaultAnswerHumanized = if defaultAnswer then "yes" else "no"++-- | Ask a confirm question by default it will be true+-- confirm "Do you like music?"+confirm :: String -> IO Bool+confirm question = confirmWithDefault question False
+ src/FortyTwo/Prompts/Input.hs view
@@ -0,0 +1,28 @@+module FortyTwo.Prompts.Input (inputWithDefault, input) where++import FortyTwo.Renderers.Question (renderQuestion)+import FortyTwo.Utils (clearLines, flush)+import FortyTwo.Constants (emptyString)++-- | Ask a simple input question falling back to a default value if no answer will be provided+-- inputWithDefault "What is your name?" "The Dude"+inputWithDefault :: String -> String -> IO String+inputWithDefault question defaultAnswer = do+  putStrLn emptyString+  renderQuestion question defaultAnswer emptyString+  putStr " "+  flush+  answer <- getLine+  clearLines 1+  -- return the default answer if no answer was given+  if null answer then do+    renderQuestion question emptyString defaultAnswer+    return defaultAnswer+  else do+    renderQuestion question emptyString answer+    return answer++-- | Simple input question+-- input "What is your name?"+input :: String -> IO String+input question = inputWithDefault question emptyString
+ src/FortyTwo/Prompts/Multiselect.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE NamedFieldPuns #-}++module FortyTwo.Prompts.Multiselect (multiselect, multiselectWithDefault) where++import System.Console.ANSI (hideCursor, showCursor)+import FortyTwo.Renderers.Multiselect (renderOptions)+import FortyTwo.Renderers.Question (renderQuestion)+import FortyTwo.Types(Option(..), Options)+import FortyTwo.Utils+import FortyTwo.Constants++-- | Loop to let the users select an single option+loop :: Options -> IO [Int]+loop options = do+  noEcho+  renderOptions options+  key <- getKey+  clearLines $ length options+  res <- handleEvent options key+  restoreEcho+  return res++-- | Handle a user event+handleEvent :: Options -> String -> IO [Int]+handleEvent options key+  | key `elem` [upKey, leftKey]  = loop $ moveUp options $ getOptionsMeta options+  | key `elem` [downKey, rightKey]  = loop $ moveDown options $ getOptionsMeta options+  | key == spaceKey = loop $ toggle options $ getOptionsMeta options+  | key == enterKey = return $ getSelecteOptionsIndexes options+  | otherwise = loop options++toggle :: Options -> (Int, Int, Maybe Int) -> Options+toggle options (minVal, maxVal, focusedIndex) = case focusedIndex of+  Just x -> toggleFocusedOption x options+  Nothing -> options++-- | Handle an arrow up event+moveUp :: Options -> (Int, Int, Maybe Int) -> Options+moveUp options (minVal, maxVal, focusedIndex) = case focusedIndex of+  Just x -> if x == minVal then+             focusOption x options+            else+             focusOption (x - 1) options+  Nothing -> focusOption maxVal options++-- | Handle an arrow down event+moveDown :: Options -> (Int, Int, Maybe Int) -> Options+moveDown options (minVal, maxVal, focusedIndex) = case focusedIndex of+  Just x -> if x == maxVal then+             focusOption x options+            else+             focusOption (x + 1) options+  Nothing -> focusOption minVal options++-- | Multi Select prompt, falling back to a default list of values if no answer will be provided+-- multiselectWithDefault "What's your favourite color?" ["Red", "Yellow", "Blue"] ["Red", "Blue"]+multiselectWithDefault :: String -> [String] -> [String] -> IO [String]+multiselectWithDefault question options defaultAnswer = do+  putStrLn emptyString+  renderQuestion question (toCommaSeparatedString defaultAnswer) emptyString+  putStrLn emptyString+  hideCursor+  flush+  noBuffering+  res <- loop $ stringsToOptions options+  restoreBuffering+  showCursor+  clearLines 1++  if null res then do+    renderQuestion question emptyString (toCommaSeparatedString defaultAnswer)+    return defaultAnswer+  else do+    let answer = filter' (\ i o -> elem i res) options+    renderQuestion question emptyString (toCommaSeparatedString answer)+    return answer++-- | Multi Select prompt+-- multiselect "What's your favourite color?" ["Red", "Yellow", "Blue"]+multiselect :: String -> [String] -> IO [String]+multiselect question options = multiselectWithDefault question options []
+ src/FortyTwo/Prompts/Password.hs view
@@ -0,0 +1,52 @@+module FortyTwo.Prompts.Password (password) where++import System.Console.ANSI (cursorBackward, clearFromCursorToScreenEnd, setCursorColumn, clearFromCursorToLineEnd)+import Control.Monad (unless)+import FortyTwo.Renderers.Password (renderPassword, hideLetters)+import FortyTwo.Renderers.Question (renderQuestion)+import FortyTwo.Constants (emptyString, enterKey, delKey)+import FortyTwo.Utils++-- | Ask a user password+-- password "What your secret password?"+password :: String -> IO String+password question = do+  putStrLn emptyString+  renderQuestion question emptyString emptyString+  putStr " "+  flush+  noBuffering+  answer <- loop emptyString+  restoreBuffering+  -- for the password prompt we need to clear the prompt a bit differently+  setCursorColumn 0+  clearFromCursorToLineEnd+  renderQuestion question emptyString $ hideLetters answer+  return answer++-- | Loop to let the users select an single option+loop :: String -> IO String+loop pass = do+  noEcho+  renderPassword pass+  key <- getKey++  -- Cancel part of the password avoiding to cancel the question as well+  -- the password and the question are located on the same line+  unless (null pass) $ do+    cursorBackward $ length pass+    clearFromCursorToScreenEnd++  -- Try to filter the control keys+  res <- handleEvent pass (if length key > 1 then emptyString else key)+  restoreEcho+  return res++-- | Handle a user event+handleEvent :: String -> String -> IO String+handleEvent pass key+  | key == enterKey = return pass+  | key == delKey =+    if null pass then return loop emptyString pass+    else return loop emptyString (init pass)+  | otherwise = loop $ pass ++ key
+ src/FortyTwo/Prompts/Select.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE NamedFieldPuns #-}++module FortyTwo.Prompts.Select (select, selectWithDefault) where++import System.Console.ANSI (hideCursor, showCursor)+import FortyTwo.Renderers.Select (renderOptions)+import FortyTwo.Renderers.Question (renderQuestion)+import FortyTwo.Types(Option(..), Options)+import FortyTwo.Utils+import FortyTwo.Constants++-- | Loop to let the users select an single option+loop :: Options -> IO (Maybe Int)+loop options = do+  noEcho+  renderOptions options+  key <- getKey+  clearLines $ length options+  res <- handleEvent options key+  restoreEcho+  return res++-- | Handle a user event+handleEvent :: Options -> String -> IO (Maybe Int)+handleEvent options key+  | key `elem` [upKey, leftKey]  = loop $ moveUp options $ getOptionsMeta options+  | key `elem` [downKey, rightKey]  = loop $ moveDown options $ getOptionsMeta options+  | key `elem` [enterKey, spaceKey] = return $ getFocusedOptionIndex options+  | otherwise = loop options++-- | Handle an arrow up event+moveUp :: Options -> (Int, Int, Maybe Int) -> Options+moveUp options (minVal, maxVal, focusedIndex) = case focusedIndex of+  Just x -> if x == minVal then+             focusOption x options+            else+             focusOption (x - 1) options+  Nothing -> focusOption maxVal options++-- | Handle an arrow down event+moveDown :: Options -> (Int, Int, Maybe Int) -> Options+moveDown options (minVal, maxVal, focusedIndex) = case focusedIndex of+  Just x -> if x == maxVal then+             focusOption x options+            else+             focusOption (x + 1) options+  Nothing -> focusOption minVal options++-- | Select prompt from a list of options falling back to a default value if no answer will be provided+-- selectWithDefault "What's your favourite color?" ["Red", "Yellow", "Blue"] "Red"+selectWithDefault :: String -> [String] -> String -> IO String+selectWithDefault question options defaultAnswer = do+  putStrLn emptyString+  renderQuestion question defaultAnswer emptyString+  putStrLn emptyString+  hideCursor+  flush+  noBuffering+  res <- loop $ stringsToOptions options+  restoreBuffering+  showCursor+  clearLines 1++  case res of+    Just x  -> do+      let answer = (!!) options x+      renderQuestion question emptyString answer+      return answer+    -- If no user input will be provided..+    Nothing -> do+      -- ..let's return the default answer+      renderQuestion question emptyString defaultAnswer+      return defaultAnswer++-- | Select prompt from a list of options+-- select "What's your favourite color?" ["Red", "Yellow", "Blue"]+select :: String -> [String] -> IO String+select question options = selectWithDefault question options emptyString
+ src/FortyTwo/Renderers/Confirm.hs view
@@ -0,0 +1,8 @@+module FortyTwo.Renderers.Confirm+    (+      renderConfirm+    ) where++-- | Render the helper text+renderConfirm :: IO ()+renderConfirm = putStr " (y/N) "
+ src/FortyTwo/Renderers/Multiselect.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE NamedFieldPuns #-}++module FortyTwo.Renderers.Multiselect (renderOptions, renderOption) where++import FortyTwo.Types (Option(..), Options)+import System.Console.ANSI+import Control.Monad (when)+import FortyTwo.Constants++-- | Render all the options collection+renderOptions :: Options -> IO ()+renderOptions = mapM_ renderOption++-- | Render a single option items+renderOption :: Option -> IO()+renderOption Option { isSelected, isFocused, value } = do+  if isFocused then do+    setSGR [SetColor Foreground Dull Cyan]+    putStr $ focusIcon : " "+    setSGR [Reset]+  else+    putStr "  "+  if isSelected then do+    setSGR [SetColor Foreground Dull Green]+    putStr [selectedIcon]+    setSGR [Reset]+  else putStr [unselectedIcon]+  putStr $ " " ++ value+  putStrLn emptyString
+ src/FortyTwo/Renderers/Password.hs view
@@ -0,0 +1,11 @@+module FortyTwo.Renderers.Password (renderPassword, hideLetters) where++import FortyTwo.Constants (passwordHiddenChar)++-- | Print the password value to the user hiding it+renderPassword :: String -> IO ()+renderPassword letters = putStr $ hideLetters letters++-- | Hide any string replacing its letters with the passwordHiddenChar+hideLetters :: String -> String+hideLetters letters = [passwordHiddenChar | x <- letters]
+ src/FortyTwo/Renderers/Question.hs view
@@ -0,0 +1,41 @@+module FortyTwo.Renderers.Question (renderQuestion, renderMessage) where++import FortyTwo.Types (Message(..))+import System.Console.ANSI++-- | Print any message depending on its type+renderMessage :: Message -> String -> IO()+renderMessage messageType message+  | messageType == Question = do+      setSGR [SetColor Foreground Dull Green]+      putStr "? "+      setSGR [Reset]+      setSGR [SetColor Foreground Dull White]+      putStr message+      setSGR [Reset]+  | messageType == Answer = do+      setSGR [SetColor Foreground Dull Cyan]+      putStr $ " " ++ message+      setSGR [Reset]+  | messageType == DefaultAnswer = do+    setSGR [SetColor Foreground Vivid Yellow]+    putStr $ " (" ++ message ++ ")"+    setSGR [Reset]++-- | Print the question message+renderQuestion :: String -> String -> String -> IO ()+renderQuestion question defaultAnswer answer+  | hasDefaultAnswer && hasAnswer = do+      renderMessage Question question+      renderMessage DefaultAnswer defaultAnswer+      renderMessage Answer answer+  | hasDefaultAnswer = do+      renderMessage Question question+      renderMessage DefaultAnswer defaultAnswer+  | hasAnswer = do+      renderMessage Question question+      renderMessage Answer answer+  | otherwise = renderMessage Question question+  where+      hasDefaultAnswer = not $ null defaultAnswer+      hasAnswer = not $ null answer
+ src/FortyTwo/Renderers/Select.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE NamedFieldPuns #-}++module FortyTwo.Renderers.Select (renderOptions, renderOption) where++import FortyTwo.Types (Option(..), Options)+import System.Console.ANSI+import FortyTwo.Constants++-- | Render all the options collection+renderOptions :: Options -> IO ()+renderOptions = mapM_ renderOption++-- | Render a single option items+renderOption :: Option -> IO()+renderOption Option { isSelected, isFocused, value } =+  if isFocused then do+    setSGR [SetColor Foreground Dull Cyan]+    putStrLn $ unwords [[focusIcon], value]+    setSGR [Reset]+  else+    putStrLn $ "  " ++ value
+ src/FortyTwo/TheAnswerToEverything.hs view
@@ -0,0 +1,8 @@+module FortyTwo.TheAnswerToEverything+    (+      theAnswerToEverything+    ) where++-- | The answer to everything in the universe+theAnswerToEverything :: Int+theAnswerToEverything = 42
+ src/FortyTwo/Types.hs view
@@ -0,0 +1,13 @@+module FortyTwo.Types where++-- | UI message types+data Message = Question | DefaultAnswer | Answer deriving (Eq)++-- | Struct needed for rendering the select prompts+data Option = Option {+  isSelected :: Bool,+  isFocused :: Bool,+  value :: String+} deriving (Eq, Show)++type Options = [Option]
+ src/FortyTwo/Utils.hs view
@@ -0,0 +1,106 @@+{-# LANGUAGE NamedFieldPuns #-}++module FortyTwo.Utils where++import System.Console.ANSI (cursorUpLine, clearFromCursorToScreenEnd)+import System.IO (hSetBuffering, hFlush, hSetEcho, hReady, stdin, stdout, BufferMode(..))+import Data.List (findIndex, findIndices, elemIndex, intercalate)+import Control.Monad (when)+import Data.Maybe (fromJust)+import FortyTwo.Types(Option(..), Options)+import FortyTwo.Constants (emptyString)++noBuffering :: IO()+noBuffering = do+  hSetBuffering stdin NoBuffering+  hSetBuffering stdout NoBuffering++restoreBuffering :: IO()+restoreBuffering = do+  hSetBuffering stdin LineBuffering+  hSetBuffering stdout LineBuffering++noEcho :: IO ()+noEcho = hSetEcho stdin False++restoreEcho :: IO ()+restoreEcho = hSetEcho stdin True++clearLines :: Int -> IO()+clearLines lines' = do+  -- move up of 1 line...+  cursorUpLine lines'+  -- and clear them+  clearFromCursorToScreenEnd++-- | Map a collection with an index+map' :: (Int -> a -> b) -> [a] -> [b]+map' f = zipWith f [0..]++-- | Filter a collection with index+filter' :: Eq a => (Int -> a -> Bool) -> [a] -> [a]+filter' f xs = [x | x <- xs, f (fromJust (elemIndex x xs)) x]++getKey = reverse <$> getKey' emptyString+  where+    getKey' chars = do+      char <- getChar+      more <- hReady stdin+      (if more then getKey' else return) (char:chars)++-- | Get useful informations from the options collection, like minVal, maxVal, activeIndex+getOptionsMeta :: Options -> (Int, Int, Maybe Int)+getOptionsMeta options = (0, length options - 1, getFocusedOptionIndex options)++-- | Convert a string array to+stringsToOptions :: [String] -> Options+stringsToOptions options = [+    Option { value = o, isFocused = False, isSelected = False } | o <- options+  ]++-- | Give the focus to a single option in the collection+focusOption :: Int -> Options -> Options+focusOption focusedIndex = map' $ \ i o ->+  Option {+    value = getOptionValue o,+    isSelected = getOptionIsSelected o,+    isFocused = focusedIndex == i+  }+-- | Toggle the isSelected flag for a single option+toggleFocusedOption :: Int -> Options -> Options+toggleFocusedOption focusedIndex = map' $ \ i o ->+  Option {+    value = getOptionValue o,+    isFocused = focusedIndex == i,+    isSelected = if focusedIndex == i then+      not $ getOptionIsSelected o+      else getOptionIsSelected o+  }++-- | Print a list to comma separated+toCommaSeparatedString :: [String] -> String+toCommaSeparatedString = intercalate ", "++-- | Flush the output buffer+flush :: IO()+flush = hFlush stdout++-- | Get the value of any option+getOptionValue :: Option -> String+getOptionValue Option { value } = value++-- | Get the is focused attribute of any option+getOptionIsFocused :: Option -> Bool+getOptionIsFocused Option { isFocused } = isFocused++-- | Get the is selected attribute of any option+getOptionIsSelected :: Option -> Bool+getOptionIsSelected Option { isSelected } = isSelected++-- | Get the index of the option selected+getFocusedOptionIndex :: Options -> Maybe Int+getFocusedOptionIndex = findIndex getOptionIsFocused++-- | Filter the indexes of the options selected+getSelecteOptionsIndexes :: Options -> [Int]+getSelecteOptionsIndexes = findIndices getOptionIsSelected
+ test/Main.hs view
@@ -0,0 +1,11 @@+module Main (main) where++import qualified Specs.Core+import qualified Specs.Utils+import Test.Hspec++main :: IO ()+main = hspec $+  describe "FortyTwo" $ do+    Specs.Core.main+    Specs.Utils.main
+ test/Specs/Core.hs view
@@ -0,0 +1,9 @@+module Specs.Core (main) where+import FortyTwo+import Test.Hspec++main :: SpecWith ()+main =+  describe "theAnswerToEverything" $+    it "Get the answer to everything" $+      theAnswerToEverything `shouldBe` (42 :: Int)
+ test/Specs/Utils.hs view
@@ -0,0 +1,22 @@+module Specs.Utils (main) where++import Data.Maybe+import FortyTwo.Utils+import FortyTwo.Types+import Test.Hspec++main :: SpecWith ()+main =+  describe "Utils" $ do+    let list = ["one", "two", "three"]+    it "It converts strings to valid options" $ do+      let options = stringsToOptions list+      getOptionValue ((!!) options 0) `shouldBe` (!!) list 0++    it "Can update properly an options collection with a defined index" $ do+      let selectedIndex = 1+      let options = focusOption selectedIndex $ stringsToOptions list+      fromJust (getFocusedOptionIndex options) `shouldBe` selectedIndex++    it "Return 0 if no option will be selected" $+        fromMaybe 0 (getFocusedOptionIndex $ stringsToOptions list) `shouldBe` 0
+ test/doctests.hs view
@@ -0,0 +1,12 @@+module Main where++import Test.DocTest++main :: IO ()+main = doctest [+    "src/FortyTwo.hs",+    "src/FortyTwo/TheAnswerToEverything.hs",+    "src/FortyTwo/Types.hs",+    "src/FortyTwo/Prompts/Input.hs",+    "src/FortyTwo/Renderers/Question.hs"+  ]