packages feed

prompt-hs 1.0.1.0 → 1.0.2.0

raw patch · 2 files changed

+280/−42 lines, 2 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ System.Prompt: instance System.Prompt.IsPromptChoiceState (System.Prompt.PromptChoiceState requirement a)
+ System.Prompt: instance System.Prompt.IsPromptChoiceState (System.Prompt.PromptMultipleChoiceState a)
+ System.Prompt: promptMultipleChoice :: (Chooseable a, Eq a, MonadIO m) => Proxy a -> Text -> m [a]

Files

prompt-hs.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           prompt-hs-version:        1.0.1.0+version:        1.0.2.0 synopsis:       A user-friendly, dependently-typed library for asking your users questions description:    A library making use of the terminal package to prompt users for answers in a CLI context.                 .
src/System/Prompt.hs view
@@ -40,6 +40,7 @@     promptYesNoWithDefault,     promptChoice,     promptChoiceFromSet,+    promptMultipleChoice,      -- * Data     Chooseable (..),@@ -151,27 +152,69 @@   ChoiceInstructionNormal -> "You can type to search, or use the arrow keys. Press enter to select."   ChoiceInstructionNoOptionSelected -> "No option selected. Try expanding your filter with backspace to see more options." +-- | Gets the text which will be displayed to the user for a given instruction.+instructionTextMulti :: ChoiceInstruction -> Text+instructionTextMulti = \case+  ChoiceInstructionNormal -> "You can type to search, or use the arrow keys. Press enter to select/deselect, and ctrl+enter to confirm."+  ChoiceInstructionNoOptionSelected -> "No options match this search. Try expanding your filter with backspace to see more options."++class IsPromptChoiceState a where+  ipcsGetNumberOfOptions :: a -> Int+  ipcsGetNumberOfFilteredOptions :: a -> Int+ data PromptChoiceState (requirement :: SRequirement) (a :: Type) = PromptChoiceState   { pcsConfirmation :: Confirmation,     pcsFilter :: Text,     pcsInstruction :: ChoiceInstruction,     pcsOptions :: NonEmpty (PerRequirement requirement a),     pcsFilteredOptions :: [PerRequirement requirement a],-    pcsSelectedOption :: Maybe (PerRequirement requirement a)+    pcsHoveredOption :: Maybe (PerRequirement requirement a)   } +instance IsPromptChoiceState (PromptChoiceState requirement a) where+  ipcsGetNumberOfOptions = length . pcsOptions+  ipcsGetNumberOfFilteredOptions = length . pcsFilteredOptions++data PromptMultipleChoiceState (a :: Type) = PromptMultipleChoiceState+  { pmcsFilter :: Text,+    pmcsInstruction :: ChoiceInstruction,+    pmcsOptions :: NonEmpty a,+    pmcsFilteredOptions :: [a],+    pmcsHoveredOption :: Maybe a,+    pmcsSelectedOptions :: [a]+  }++instance IsPromptChoiceState (PromptMultipleChoiceState a) where+  ipcsGetNumberOfOptions = length . pmcsOptions+  ipcsGetNumberOfFilteredOptions = length . pmcsFilteredOptions+ _pcsFilter :: Lens' (PromptChoiceState requirement a) Text _pcsFilter = lens pcsFilter \s x -> s {pcsFilter = x} +_pmcsFilter :: Lens' (PromptMultipleChoiceState a) Text+_pmcsFilter = lens pmcsFilter \s x -> s {pmcsFilter = x}+ _pcsInstruction :: Lens' (PromptChoiceState requirement a) ChoiceInstruction _pcsInstruction = lens pcsInstruction \s x -> s {pcsInstruction = x} +_pmcsInstruction :: Lens' (PromptMultipleChoiceState a) ChoiceInstruction+_pmcsInstruction = lens pmcsInstruction \s x -> s {pmcsInstruction = x}+ _pcsFilteredOptions :: Lens' (PromptChoiceState requirement a) [PerRequirement requirement a] _pcsFilteredOptions = lens pcsFilteredOptions \s x -> s {pcsFilteredOptions = x} -_pcsSelectedOption :: Lens' (PromptChoiceState requirement a) (Maybe (PerRequirement requirement a))-_pcsSelectedOption = lens pcsSelectedOption \s x -> s {pcsSelectedOption = x}+_pmcsFilteredOptions :: Lens' (PromptMultipleChoiceState a) [a]+_pmcsFilteredOptions = lens pmcsFilteredOptions \s x -> s {pmcsFilteredOptions = x} +_pcsHoveredOption :: Lens' (PromptChoiceState requirement a) (Maybe (PerRequirement requirement a))+_pcsHoveredOption = lens pcsHoveredOption \s x -> s {pcsHoveredOption = x}++_pmcsHoveredOption :: Lens' (PromptMultipleChoiceState a) (Maybe a)+_pmcsHoveredOption = lens pmcsHoveredOption \s x -> s {pmcsHoveredOption = x}++_pmcsSelectedOptions :: Lens' (PromptMultipleChoiceState a) [a]+_pmcsSelectedOptions = lens pmcsSelectedOptions \s x -> s {pmcsSelectedOptions = x}+ data PromptTextState (requirement :: SRequirement) = PromptTextState   { ptsConfirmation :: Confirmation,     ptsCurrentInput :: Text,@@ -235,6 +278,30 @@   liftIO . withTerminal . runTerminalT $     promptChoiceInternal Proxy prompt (initialPromptChoiceStateFromSet confirmation $ NE.nub set) +-- | Ask the user to choose zero-to-many of the constructors of a sum type.+--+-- @+-- data Colour = Red | Green | Blue deriving (Bounded, Enum, Eq, Show)+--+-- instance Chooseable Colour where+--   showChooseable = Data.Text.pack . show+--+-- main :: IO ()+-- main = do+--   favouriteColours <- promptMultipleChoice (Proxy :: Proxy Colour) $ "What are your favourite colours?"+--   print favouriteColours+-- @+promptMultipleChoice ::+  (Chooseable a, Eq a, MonadIO m) =>+  -- | Proxy of type you want to get back+  Proxy a ->+  -- | What should the prompt say? (You need not add a space to the end of this text; one will be added)+  Text ->+  m [a]+promptMultipleChoice pxy prompt =+  liftIO . withTerminal . runTerminalT $+    promptMultipleChoiceInternal pxy prompt initialPromptMultipleChoiceState+ -- | Ask the user to enter text. -- -- The answer is stripped of leading and trailing whitespace.@@ -309,7 +376,7 @@       pcsInstruction = ChoiceInstructionNormal,       pcsOptions = universeChooseableNE,       pcsFilteredOptions = NE.toList universeChooseableNE,-      pcsSelectedOption = Just initialSelectionChooseable+      pcsHoveredOption = Just initialSelectionChooseable     }  initialPromptChoiceStateFromSet ::@@ -324,9 +391,22 @@       pcsInstruction = ChoiceInstructionNormal,       pcsOptions = options,       pcsFilteredOptions = NE.toList options,-      pcsSelectedOption = Just initialSelectionChooseableItem+      pcsHoveredOption = Just initialSelectionChooseableItem     } +initialPromptMultipleChoiceState ::+  (Chooseable a) =>+  PromptMultipleChoiceState a+initialPromptMultipleChoiceState =+  PromptMultipleChoiceState+    { pmcsFilter = "",+      pmcsInstruction = ChoiceInstructionNormal,+      pmcsOptions = universeChooseableNE,+      pmcsFilteredOptions = NE.toList universeChooseableNE,+      pmcsHoveredOption = initialSelectionChooseable,+      pmcsSelectedOptions = []+    }+ promptChoiceInternal ::   (MonadInput m, Renderable m requirement a) =>   Proxy a ->@@ -337,6 +417,16 @@   linesRendered <- renderPromptChoicePrompt prompt s   awaitEvent >>= handlePromptChoiceEvent prompt linesRendered s +promptMultipleChoiceInternal ::+  (MonadInput m, Renderable m 'SRequired a) =>+  Proxy a ->+  Text ->+  PromptMultipleChoiceState a ->+  m [a]+promptMultipleChoiceInternal _ prompt s = do+  linesRendered <- renderPromptMultipleChoicePrompt prompt s+  awaitEvent >>= handlePromptMultipleChoiceEvent prompt linesRendered s+ promptTextInternal ::   (MonadColorPrinter m, MonadFormattingPrinter m, MonadInput m, MonadScreen m) =>   PromptTextState requirement ->@@ -355,28 +445,37 @@   m Int renderPromptChoicePrompt prompt s = do   renderPromptLine prompt-  linesRendered <- renderOptionLines s+  linesRendered <- renderChoiceOptionLines s   renderSummaryLine s linesRendered-  renderInstructionLine-  renderFilterLine+  renderInstructionLine instructionText $ pcsInstruction s+  renderFilterLine $ pcsFilter s   pure $ linesRendered + 4-  where-    renderInstructionLine = withAttributes [italic] . putTextLn . instructionText $ pcsInstruction s-    renderFilterLine = do-      withAttributes [bold, foreground magenta] $ putText "> "-      putText $ pcsFilter s-      flush +-- | Renders the prompt for the user and returns the number of lines output+renderPromptMultipleChoicePrompt ::+  (Renderable m 'SRequired a) =>+  Text ->+  PromptMultipleChoiceState a ->+  m Int+renderPromptMultipleChoicePrompt prompt s = do+  renderPromptLine prompt+  linesRendered <- renderMultipleChoiceOptionLines s+  renderSummaryLine s linesRendered+  renderCurrentSelectionsLine s+  renderInstructionLine instructionTextMulti $ pmcsInstruction s+  renderFilterLine $ pmcsFilter s+  pure $ linesRendered + 5+ renderPrompt :: (MonadColorPrinter m, MonadFormattingPrinter m) => Text -> m () renderPrompt = withAttributes [bold, foreground blue] . putText . (<> " ")  renderPromptLine :: (MonadColorPrinter m, MonadFormattingPrinter m) => Text -> m () renderPromptLine = withAttributes [bold, foreground blue] . putTextLn -renderSummaryLine :: (Renderable m requirement a) => PromptChoiceState requirement a -> Int -> m ()+renderSummaryLine :: (IsPromptChoiceState s, MonadColorPrinter m, MonadFormattingPrinter m) => s -> Int -> m () renderSummaryLine s numberOfOptionsRendered = do-  let nAll = length $ pcsOptions s-      nFiltered = length $ pcsFilteredOptions s+  let nAll = ipcsGetNumberOfOptions s+      nFiltered = ipcsGetNumberOfFilteredOptions s   withAttributes [italic, foreground cyan] . putTextLn $     T.pack (show nFiltered)       <> "/"@@ -385,38 +484,89 @@       <> T.pack (show numberOfOptionsRendered)       <> " shown." -renderOptionLines ::+renderCurrentSelectionsLine :: (ChooseableItem a, MonadColorPrinter m, MonadFormattingPrinter m) => PromptMultipleChoiceState a -> m ()+renderCurrentSelectionsLine s = do+  withAttributes [italic, foreground cyan] $ putText "Current Selections: "+  withAttributes [bold, italic, foreground cyan]+    . putTextLn+    . T.intercalate ", "+    . fmap chooseableItemText+    $ pmcsSelectedOptions s++renderChoiceOptionLines ::   forall requirement a m.   (Renderable m requirement a) =>   PromptChoiceState requirement a ->   m Int-renderOptionLines s = do-  let (selectedOption, otherOptions) = getVisibleOptions s-  case selectedOption of+renderChoiceOptionLines s = do+  let (hoveredOption, otherOptions) = getVisibleChoiceOptions s+  case hoveredOption of     Just o -> withAttributes [bold, foreground yellow] . putTextLn . ("* " <>) $ chooseableItemText o     Nothing -> pure ()   forM_ otherOptions (withAttributes [foreground yellow] . putTextLn . ("  " <>) . chooseableItemText)-  pure $ length otherOptions + maybe 0 (const 1) selectedOption+  pure $ length otherOptions + maybe 0 (const 1) hoveredOption --- | Returns a maximum of 6 options, including the one currently selected as the first option.-getVisibleOptions ::+renderMultipleChoiceOptionLines ::+  forall a m.+  (Renderable m 'SRequired a) =>+  PromptMultipleChoiceState a ->+  m Int+renderMultipleChoiceOptionLines s = do+  let (hoveredOption, otherOptions) = getVisibleMultipleChoiceOptions s+  case hoveredOption of+    Just o -> withAttributes [bold, foreground yellow] . putTextLn . ("> " <>) . insertSelectedIndicator o $ chooseableItemText o+    Nothing -> pure ()+  forM_ otherOptions (\o -> withAttributes [foreground yellow] . putTextLn . ("  " <>) . insertSelectedIndicator o $ chooseableItemText o)+  pure $ length otherOptions + maybe 0 (const 1) hoveredOption+  where+    insertSelectedIndicator :: a -> Text -> Text+    insertSelectedIndicator o =+      ( ( if o `elem` pmcsSelectedOptions s+            then "[*] "+            else "[ ] "+        )+          <>+      )++-- | Returns a maximum of 6 options, including the one currently hovered as the first option.+getVisibleChoiceOptions ::   forall requirement a result.   (result ~ PerRequirement requirement a, Eq result) =>   PromptChoiceState requirement a ->   (Maybe result, [result])-getVisibleOptions s = case pcsSelectedOption s of+getVisibleChoiceOptions s = case pcsHoveredOption s of   Just o | o `elem` filteredOptions -> (Just o, take 5 $ filter (/= o) filteredOptions)   _ -> (Nothing, take 6 filteredOptions)   where     maxNumberOfOptions :: Int     maxNumberOfOptions = length $ pcsFilteredOptions s     filteredOptions :: [result]-    filteredOptions = take maxNumberOfOptions . dropWhile isNotSelectedOption . cycle $ pcsFilteredOptions s-    isNotSelectedOption :: result -> Bool-    isNotSelectedOption o = case pcsSelectedOption s of+    filteredOptions = take maxNumberOfOptions . dropWhile isNotHoveredOption . cycle $ pcsFilteredOptions s+    isNotHoveredOption :: result -> Bool+    isNotHoveredOption o = case pcsHoveredOption s of       Just target -> o /= target       Nothing -> False +-- | Returns a maximum of 6 options, including the one currently hovered as the first option.+getVisibleMultipleChoiceOptions :: forall a. (Eq a) => PromptMultipleChoiceState a -> (Maybe a, [a])+getVisibleMultipleChoiceOptions s = case pmcsHoveredOption s of+  Just o | o `elem` filteredOptions -> (Just o, take 5 $ filter (/= o) filteredOptions)+  _ -> (Nothing, take 6 filteredOptions)+  where+    maxNumberOfOptions :: Int+    maxNumberOfOptions = length $ pmcsFilteredOptions s+    filteredOptions :: [a]+    filteredOptions = take maxNumberOfOptions . maybe id (\o -> dropWhile (/= o)) (pmcsHoveredOption s) . cycle $ pmcsFilteredOptions s++renderInstructionLine :: (MonadFormattingPrinter m) => (a -> Text) -> a -> m ()+renderInstructionLine instructionToText = withAttributes [italic] . putTextLn . instructionToText++renderFilterLine :: (MonadFormattingPrinter m, MonadColorPrinter m) => Text -> m ()+renderFilterLine f = do+  withAttributes [bold, foreground magenta] $ putText "> "+  putText f+  flush+ handlePromptChoiceEvent ::   forall m requirement a.   (Renderable m requirement a, MonadInput m) =>@@ -435,15 +585,15 @@         Upwards ->           goAgain $             s-              { pcsSelectedOption =+              { pcsHoveredOption =                   headViaNonEmpty                     $ drop 1-                      . dropWhile (maybe (const False) (/=) (pcsSelectedOption s))+                      . dropWhile (maybe (const False) (/=) (pcsHoveredOption s))                       . cycle                       . reverse                     $ pcsFilteredOptions s               }-        Downwards -> goAgain $ s {pcsSelectedOption = headViaNonEmpty . snd $ getVisibleOptions s}+        Downwards -> goAgain $ s {pcsHoveredOption = headViaNonEmpty . snd $ getVisibleChoiceOptions s}         _ -> goAgain s       BackspaceKey ->         goAgain $@@ -453,9 +603,9 @@             & _pcsInstruction %~ \case               ChoiceInstructionNoOptionSelected -> ChoiceInstructionNormal               currentInstruction -> currentInstruction-      EnterKey -> case pcsSelectedOption s of+      EnterKey -> case pcsHoveredOption s of         Just o -> case pcsConfirmation s of-          RequireConfirmation -> confirmSelection prompt o (goAgain s)+          RequireConfirmation -> confirmSelection prompt o (chooseableItemText o) (goAgain s)           DontConfirm -> do             renderPrompt prompt             withAttributes [bold, foreground yellow] . putTextLn $ chooseableItemText o@@ -474,6 +624,76 @@     goAgain :: PromptChoiceState requirement a -> m (PerRequirement requirement a)     goAgain = promptChoiceInternal Proxy prompt +handlePromptMultipleChoiceEvent ::+  forall m a.+  (Renderable m 'SRequired a, MonadInput m) =>+  Text ->+  Int ->+  PromptMultipleChoiceState a ->+  Either Interrupt Event ->+  m [a]+handlePromptMultipleChoiceEvent prompt linesRendered s e = do+  moveCursorUp (linesRendered - 1)+  deleteLines linesRendered+  case e of+    Left Interrupt -> liftIO exitFailure+    Right (KeyEvent key modifiers) -> case key of+      ArrowKey direction -> case direction of+        Upwards ->+          goAgain $+            s+              { pmcsHoveredOption =+                  headViaNonEmpty+                    $ drop 1+                      . dropWhile (maybe (const False) (/=) (pmcsHoveredOption s))+                      . cycle+                      . reverse+                    $ pmcsFilteredOptions s+              }+        Downwards -> goAgain $ s {pmcsHoveredOption = headViaNonEmpty . snd $ getVisibleMultipleChoiceOptions s}+        _ -> goAgain s+      BackspaceKey ->+        goAgain $+          s+            & _pmcsFilter %~ T.dropEnd 1+            & applyFilterMultiple+            & _pmcsInstruction %~ \case+              ChoiceInstructionNoOptionSelected -> ChoiceInstructionNormal+              currentInstruction -> currentInstruction+      EnterKey ->+        confirmSelection+          prompt+          (pmcsSelectedOptions s)+          ( if null (pmcsSelectedOptions s)+              then "[nothing]"+              else T.intercalate ", " . fmap chooseableItemText $ pmcsSelectedOptions s+          )+          (goAgain s)+      CharKey c+        | c == ' ' ->+            goAgain $+              s+                & _pmcsSelectedOptions+                  %~ ( \opts -> case pmcsHoveredOption s of+                         Just hovered ->+                           if hovered `elem` opts+                             then filter (/= hovered) opts+                             else hovered : opts+                         Nothing -> opts+                     )+      CharKey c ->+        if modifiers == mempty+          then+            if null (pmcsFilteredOptions s)+              then goAgain $ s {pmcsInstruction = ChoiceInstructionNoOptionSelected}+              else goAgain $ s & _pmcsFilter %~ flip T.snoc c & applyFilterMultiple+          else goAgain s+      _ -> goAgain s+    _ -> goAgain s+  where+    goAgain :: PromptMultipleChoiceState a -> m [a]+    goAgain = promptMultipleChoiceInternal Proxy prompt+ handlePromptTextEvent ::   (MonadColorPrinter m, MonadFormattingPrinter m, MonadInput m, MonadScreen m) =>   PromptTextState requirement ->@@ -500,8 +720,8 @@       _ -> promptTextInternal s     _ -> promptTextInternal s --- | Applies the filter in `_pcsFilter` to set `_pcsOptions` and update `_pcsSelectedOption`--- if the previously selected option is no longer included in the filter.+-- | Applies the filter in `_pcsFilter` to set `_pcsOptions` and update `_pcsHoveredOption`+-- if the previously hovered option is no longer included in the filter. applyFilter ::   (result ~ PerRequirement requirement a, ChooseableItem result, Eq result) =>   PromptChoiceState requirement a ->@@ -513,20 +733,38 @@           (NE.toList $ pcsOptions s)    in s         & _pcsFilteredOptions .~ newFilteredOptions-        & _pcsSelectedOption %~ \case+        & _pcsHoveredOption %~ \case           Just o | o `elem` newFilteredOptions -> Just o           _ -> headViaNonEmpty newFilteredOptions +-- | Applies the filter in `_pcsFilter` to set `_pcsOptions` and update `_pcsHoveredOption`+-- if the previously hovered option is no longer included in the filter.+applyFilterMultiple ::+  (ChooseableItem a, Eq a) =>+  PromptMultipleChoiceState a ->+  PromptMultipleChoiceState a+applyFilterMultiple s =+  let newFilteredOptions =+        filter+          ((T.toLower (pmcsFilter s) `T.isInfixOf`) . T.toLower . chooseableItemText)+          (NE.toList $ pmcsOptions s)+   in s+        & _pmcsFilteredOptions .~ newFilteredOptions+        & _pmcsHoveredOption %~ \case+          Just o | o `elem` newFilteredOptions -> Just o+          _ -> headViaNonEmpty newFilteredOptions+ confirmSelection ::-  (ChooseableItem a, MonadColorPrinter m, MonadFormattingPrinter m, MonadInput m, MonadScreen m) =>+  (MonadColorPrinter m, MonadFormattingPrinter m, MonadInput m, MonadScreen m) =>   Text ->   a ->+  Text ->   m a ->   m a-confirmSelection prompt selection onCancel = do+confirmSelection prompt selection selectionText onCancel = do   renderPromptLine prompt   withAttributes [bold, foreground cyan] $ putText "You have chosen: "-  withAttributes [bold, foreground yellow] $ putTextLn (chooseableItemText selection)+  withAttributes [bold, foreground yellow] $ putTextLn selectionText   withAttributes [italic] $ putTextLn "Press enter again to confirm, or escape to go back."   e <- awaitEvent   moveCursorUp 3 *> deleteLines 4@@ -534,10 +772,10 @@     Left Interrupt -> liftIO exitFailure     Right (KeyEvent EnterKey _) -> do       renderPrompt prompt-      withAttributes [bold, foreground yellow] $ putTextLn (chooseableItemText selection)+      withAttributes [bold, foreground yellow] $ putTextLn selectionText       pure selection     Right (KeyEvent EscapeKey _) -> onCancel-    _ -> confirmSelection prompt selection onCancel+    _ -> confirmSelection prompt selection selectionText onCancel  confirmTextInput ::   forall m requirement.