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 +21/−0
- README.md +124/−0
- Setup.hs +2/−0
- examples/ConfirmExample.hs +9/−0
- examples/InputExample.hs +8/−0
- examples/Main.hs +16/−0
- examples/MultiselectExample.hs +11/−0
- examples/PasswordExample.hs +6/−0
- examples/SelectExample.hs +8/−0
- fortytwo.cabal +74/−0
- src/FortyTwo.hs +20/−0
- src/FortyTwo/Constants.hs +41/−0
- src/FortyTwo/Prompts/Confirm.hs +40/−0
- src/FortyTwo/Prompts/Input.hs +28/−0
- src/FortyTwo/Prompts/Multiselect.hs +81/−0
- src/FortyTwo/Prompts/Password.hs +52/−0
- src/FortyTwo/Prompts/Select.hs +78/−0
- src/FortyTwo/Renderers/Confirm.hs +8/−0
- src/FortyTwo/Renderers/Multiselect.hs +29/−0
- src/FortyTwo/Renderers/Password.hs +11/−0
- src/FortyTwo/Renderers/Question.hs +41/−0
- src/FortyTwo/Renderers/Select.hs +21/−0
- src/FortyTwo/TheAnswerToEverything.hs +8/−0
- src/FortyTwo/Types.hs +13/−0
- src/FortyTwo/Utils.hs +106/−0
- test/Main.hs +11/−0
- test/Specs/Core.hs +9/−0
- test/Specs/Utils.hs +22/−0
- test/doctests.hs +12/−0
+ 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"+ ]