effectful-cli 0.2.4.0 → 0.2.4.1
raw patch · 10 files changed
+220/−33 lines, 10 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +2/−0
- effectful-cli.cabal +1/−1
- src/Data/CLI/Args.hs +21/−4
- src/Data/CLI/Impl.hs +73/−10
- src/Data/Error/CLI.hs +4/−1
- src/Effectful/CLI/Conc.hs +14/−0
- src/Effectful/CLI/Exception.hs +8/−1
- src/Effectful/CLI/IO.hs +60/−10
- src/Effectful/CLI/Sleep.hs +18/−3
- src/Effectful/Logger.hs +19/−3
CHANGELOG.md view
@@ -1,3 +1,5 @@+## [2.4.1](https://github.com/haskell-github-trust/effectful-cli/compare/2.4.0...2.4.1) (2026-10-31)+ ## [2.4.0](https://github.com/haskell-github-trust/effectful-cli/compare/2.3.0...2.4.0) (2026-10-31) ### Features
effectful-cli.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: effectful-cli-version: 0.2.4.0+version: 0.2.4.1 synopsis: CLI helpers for the effectful ecosystem description: Please see the README at https://github.com/haskell-github-trust/effectful-cli category: CLI
src/Data/CLI/Args.hs view
@@ -48,13 +48,19 @@ -- * info otherwise -- -- @since 0.1.0.0-getLogLevel :: CLIArgs c -> LogLevel+getLogLevel+ :: CLIArgs c+ -- ^ parsed options+ -> LogLevel getLogLevel a = fromMaybe (if _debug a then LevelDebug else LevelInfo) (_logLevel a) -- | Print the version string to stdout. -- -- @since 0.1.0.0-showVersion :: String -> IO ()+showVersion+ :: String+ -- ^ version+ -> IO () showVersion = putStrLn -- | Print the version if @--version@ was passed, otherwise run the parsed command.@@ -62,21 +68,32 @@ -- @since 0.1.0.0 versionOrCommand :: String+ -- ^ version -> CLIArgs c+ -- ^ parsed options -> (c -> IO ())+ -- ^ command runner -> IO () versionOrCommand v a run = either (const (showVersion v)) run (_command a) -- | Parser for the common CLI options, wrapping the given command parser. -- -- @since 0.1.0.0-argParser :: Parser c -> Parser (CLIArgs c)+argParser+ :: Parser c+ -- ^ command parser+ -> Parser (CLIArgs c) argParser = argParserWith defaultHelpStyle -- | 'argParser' with a custom t'HelpStyle'. -- -- @since 0.2.2.0-argParserWith :: HelpStyle -> Parser c -> Parser (CLIArgs c)+argParserWith+ :: HelpStyle+ -- ^ help style+ -> Parser c+ -- ^ command parser+ -> Parser (CLIArgs c) argParserWith s cmdParser = CLIArgs <$> optional
src/Data/CLI/Impl.hs view
@@ -76,7 +76,14 @@ -- | Render a string with the selected style of a t'HelpStyle'. -- -- @since 0.2.2.0-styled :: (HelpStyle -> AnsiStyle) -> HelpStyle -> String -> Doc+styled+ :: (HelpStyle -> AnsiStyle)+ -- ^ style selector, e.g. '_styleValue'+ -> HelpStyle+ -- ^ style set to select from+ -> String+ -- ^ text to render+ -> Doc styled f s | f s == mempty = pretty | otherwise = annotate (f s) . pretty@@ -94,7 +101,10 @@ -- 'colorEnabled' is false. -- -- @since 0.2.2.0-execCLIParser :: ParserInfo a -> IO a+execCLIParser+ :: ParserInfo a+ -- ^ top-level parser, e.g. from 'usage'+ -> IO a execCLIParser pinfo = do c <- colorEnabled res <- execParserPure (prefs showHelpOnEmpty) pinfo <$> getArgs@@ -108,13 +118,21 @@ -- | Error for 'succ' called on 'maxBound' of the named enum. -- -- @since 0.1.0.0-unboundEnumError :: String -> a+unboundEnumError+ :: String+ -- ^ enum type name+ -> a unboundEnumError name = error (printf "%s.Enum: succ invoked on maxBound" name) -- | Turn a megaparsec parser into a 'ReadM', requiring the whole input to be consumed. -- -- @since 0.1.0.0-fromParsec :: String -> Parsec Void String a -> ReadM a+fromParsec+ :: String+ -- ^ input name used in parse error messages+ -> Parsec Void String a+ -- ^ parser for the whole value+ -> ReadM a fromParsec name p = eitherReader (first errorBundlePretty . runParser (p <* eof) name) -- | Accept a value either as a positional argument or as a @-c@/@--name@ option.@@ -122,10 +140,15 @@ -- @since 0.1.0.0 argOrOpt :: String+ -- ^ long option name, also the uppercased metavar -> Char+ -- ^ short option -> String+ -- ^ help text -> Either (Parsec Void String a) (ReadM a)+ -- ^ value reader, as a megaparsec parser or a 'ReadM' -> [String]+ -- ^ shell completion candidates -> Parser a argOrOpt name c h p cs = argument r (metavar var <> completeWith cs)@@ -137,7 +160,10 @@ -- | Parser for a command that takes no arguments. -- -- @since 0.1.0.0-noArgs :: a -> Parser a+noArgs+ :: a+ -- ^ command value+ -> Parser a noArgs = pure -- | @-f@/@--force@ switch.@@ -160,7 +186,14 @@ -- | @--name@/@--no-name@ switch pair; the last occurrence wins, otherwise the default. -- -- @since 0.2.2.0-reversibleSwitch :: Bool -> String -> String -> Parser Bool+reversibleSwitch+ :: Bool+ -- ^ default when neither switch is given+ -> String+ -- ^ long switch name, @no-@ prefixed for the negation+ -> String+ -- ^ help text+ -> Parser Bool reversibleSwitch def name h = last <$> some (flag' True (long name <> help h) <|> flag' False (long ("no-" <> name) <> hidden))@@ -175,26 +208,56 @@ -- | Build a subcommand from a 'Command' instance. -- -- @since 0.1.0.0-mkCommand :: Command c e a => a -> Mod CommandFields a+mkCommand+ :: Command c e a+ => a+ -- ^ command value+ -> Mod CommandFields a mkCommand a = command (cmdname a) (info (parser a) (progDesc (doc a))) -- | Build subcommands for every value of an enum, sorted by name and wrapped with the given constructor. -- -- @since 0.1.0.0 mkCommands- :: Bounded a => Command c e a => Command c e b => Enum a => (a -> b) -> Mod CommandFields b+ :: Bounded a+ => Command c e a+ => Command c e b+ => Enum a+ => (a -> b)+ -- ^ wrapping constructor+ -> Mod CommandFields b mkCommands f = foldMap (mkCommand . f) (sortOn cmdname [minBound ..]) -- | Top-level t'ParserInfo' with @--help@ and a header showing name, description and version. -- -- @since 0.1.0.0-usage :: String -> String -> String -> Parser a -> ParserInfo a+usage+ :: String+ -- ^ program name+ -> String+ -- ^ version+ -> String+ -- ^ description+ -> Parser a+ -- ^ arguments parser+ -> ParserInfo a usage = usageWith defaultHelpStyle -- | 'usage' with a custom t'HelpStyle'. -- -- @since 0.2.2.0-usageWith :: HelpStyle -> String -> String -> String -> Parser a -> ParserInfo a+usageWith+ :: HelpStyle+ -- ^ help style+ -> String+ -- ^ program name+ -> String+ -- ^ version+ -> String+ -- ^ description+ -> Parser a+ -- ^ arguments parser+ -> ParserInfo a usageWith s name version hdr p = info (p <**> helper)
src/Data/Error/CLI.hs view
@@ -25,7 +25,10 @@ -- | Render a 'CLIError' as a human-readable message. -- -- @since 0.1.0.0-displayCLIError :: CLIError -> Text+displayCLIError+ :: CLIError+ -- ^ error to render+ -> Text displayCLIError (InputError s) = "input error: " <> s displayCLIError (NotFoundError s) = "not found: " <> s displayCLIError (NotImplemented s) = "not implemented: " <> s
src/Effectful/CLI/Conc.hs view
@@ -36,6 +36,7 @@ :: Concurrent :> es => Traversable t => t (Eff es a)+ -- ^ actions -> Eff es (t a) runConcurrently = mapConcurrently id @@ -46,6 +47,7 @@ :: Concurrent :> es => Foldable t => t (Eff es a)+ -- ^ actions -> Eff es () runConcurrently_ = mapConcurrently_ id @@ -56,8 +58,11 @@ :: Concurrent :> es => Traversable t => Int+ -- ^ maximum number of concurrent actions -> (a -> Eff es b)+ -- ^ action for each element -> t a+ -- ^ elements -> Eff es (t b) batchConcurrently = pooledMapConcurrentlyN @@ -68,8 +73,11 @@ :: Concurrent :> es => Foldable f => Int+ -- ^ maximum number of concurrent actions -> (a -> Eff es b)+ -- ^ action for each element -> f a+ -- ^ elements -> Eff es () batchConcurrently_ = pooledMapConcurrentlyN_ @@ -80,8 +88,11 @@ :: Concurrent :> es => Traversable t => Int+ -- ^ maximum number of concurrent actions -> (Int -> a -> Eff es b)+ -- ^ action for each element -> t a+ -- ^ elements -> Eff es (t b) batchConcurrentlyWithIndex n f = batchConcurrently n (uncurry f) . snd . mapAccumL (\i x -> (i + 1, (i, x))) 1@@ -93,7 +104,10 @@ :: Concurrent :> es => Foldable f => Int+ -- ^ maximum number of concurrent actions -> (Int -> a -> Eff es b)+ -- ^ action for each element -> f a+ -- ^ elements -> Eff es () batchConcurrentlyWithIndex_ n f = batchConcurrently_ n (uncurry f) . zip [1 ..] . toList
src/Effectful/CLI/Exception.hs view
@@ -11,5 +11,12 @@ -- | Catch 'IOException's raised by an action and rethrow them as @e@ via the given constructor. -- -- @since 0.2.1.0-rethrowIO :: Error e :> es => Show e => (IOException -> e) -> Eff es a -> Eff es a+rethrowIO+ :: Error e :> es+ => Show e+ => (IOException -> e)+ -- ^ application error constructor+ -> Eff es a+ -- ^ action that may raise an 'IOException'+ -> Eff es a rethrowIO f = handle (throwError . f)
src/Effectful/CLI/IO.hs view
@@ -40,31 +40,52 @@ -- | Print to stdout without a trailing newline. -- -- @since 0.1.0.0-printOutput :: IOE :> es => Text -> Eff es ()+printOutput+ :: IOE :> es+ => Text+ -- ^ text to print+ -> Eff es () printOutput = liftIO . T.putStr -- | Print to stdout with a trailing newline. -- -- @since 0.1.0.0-printOutputLn :: IOE :> es => Text -> Eff es ()+printOutputLn+ :: IOE :> es+ => Text+ -- ^ text to print+ -> Eff es () printOutputLn = liftIO . T.putStrLn -- | Print to stderr without a trailing newline. -- -- @since 0.1.0.0-printError :: IOE :> es => Text -> Eff es ()+printError+ :: IOE :> es+ => Text+ -- ^ text to print+ -> Eff es () printError = liftIO . T.hPutStr stderr -- | Print lines to stdout, each followed by a newline. -- -- @since 0.1.0.0-printLines :: IOE :> es => Foldable f => f Text -> Eff es ()+printLines+ :: IOE :> es+ => Foldable f+ => f Text+ -- ^ lines to print+ -> Eff es () printLines = printOutput . T.unlines . toList -- | Read a line from stdin after printing an optional prompt to stderr. -- -- @since 0.1.0.0-prompt :: IOE :> es => Maybe Text -> Eff es Text+prompt+ :: IOE :> es+ => Maybe Text+ -- ^ prompt text, followed by @> @+ -> Eff es Text prompt p = liftIO $ do T.hPutStr stderr (fromMaybe "" p <> "> ") hFlush stderr@@ -74,7 +95,13 @@ -- -- @since 0.1.0.0 prompt'- :: Error CLIError :> es => IOE :> es => Maybe Text -> Parsec Void Text a -> Eff es a+ :: Error CLIError :> es+ => IOE :> es+ => Maybe Text+ -- ^ prompt text+ -> Parsec Void Text a+ -- ^ answer parser+ -> Eff es a prompt' p parser = prompt p >>= either (throwError . InputError . T.pack . errorBundlePretty) pure@@ -97,7 +124,13 @@ -- @since 0.1.0.0 withConfirm :: forall c es a- . CLIConfig c => Error CLIError :> es => IOE :> es => Reader c :> es => Eff es a -> Eff es a+ . CLIConfig c+ => Error CLIError :> es+ => IOE :> es+ => Reader c :> es+ => Eff es a+ -- ^ action to confirm+ -> Eff es a withConfirm action = do c <- ask @c when (autoDecline c) $ throwError (UserCanceled (Just "auto-declined"))@@ -109,7 +142,11 @@ -- | Open a file or URL with @xdg-open@. -- -- @since 0.1.0.0-autoOpen :: Process :> es => String -> Eff es ()+autoOpen+ :: Process :> es+ => String+ -- ^ file path or URL+ -> Eff es () autoOpen arg = callProcess "xdg-open" [arg] -- | Let the user pick one item from a table; a single item is returned without prompting, none throws 'NotFoundError'.@@ -119,7 +156,15 @@ :: Error CLIError :> es => IOE :> es => Foldable f- => Text -> [Text] -> (a -> [Text]) -> f a -> Eff es a+ => Text+ -- ^ item kind, used as prompt and in the not-found error+ -> [Text]+ -- ^ table headers+ -> (a -> [Text])+ -- ^ table row of an item+ -> f a+ -- ^ items to choose from+ -> Eff es a chooseItem name headers row = go . toList where go [] = throwError (NotFoundError ("no item found for " <> name))@@ -136,7 +181,12 @@ -- | Turn a 1-based (or negative, from the end) user index into a 0-based one. -- -- @since 0.1.0.0-resolveIndex :: Int -> Int -> Either CLIError Int+resolveIndex+ :: Int+ -- ^ number of items+ -> Int+ -- ^ user index+ -> Either CLIError Int resolveIndex len i | i >= 1 && i <= len = Right (i - 1) | i <= -1 && i >= -len = Right (len + i)
src/Effectful/CLI/Sleep.hs view
@@ -23,7 +23,12 @@ -- | Sleep for the given number of milliseconds. -- -- @since 0.1.0.0-msleep :: IOE :> es => Logger :> es => Int -> Eff es ()+msleep+ :: IOE :> es+ => Logger :> es+ => Int+ -- ^ milliseconds+ -> Eff es () msleep n = do logDebugN (T.pack (printf "sleeping for %dms" n)) liftIO (threadDelay (n * 1000))@@ -31,7 +36,12 @@ -- | Sleep for the given number of seconds. -- -- @since 0.1.0.0-sleep :: IOE :> es => Logger :> es => Int -> Eff es ()+sleep+ :: IOE :> es+ => Logger :> es+ => Int+ -- ^ seconds+ -> Eff es () sleep n = do logDebugN (T.pack (printf "sleeping for %ds" n)) liftIO (threadDelay (n * 1000000))@@ -39,7 +49,12 @@ -- | Sleep until the given local time. -- -- @since 0.1.0.0-sleepUntil :: IOE :> es => Logger :> es => LocalTime -> Eff es ()+sleepUntil+ :: IOE :> es+ => Logger :> es+ => LocalTime+ -- ^ wake-up time+ -> Eff es () sleepUntil t = do logInfoN (T.pack ("sleeping until " <> formatTime defaultTimeLocale "%R" t)) now <- getLocalTime
src/Effectful/Logger.hs view
@@ -51,19 +51,35 @@ -- | Run the t'Logger' effect with the given log callback. -- -- @since 0.1.0.0-runLogger :: IOE :> es => LogFunc -> Eff (Logger : es) a -> Eff es a+runLogger+ :: IOE :> es+ => LogFunc+ -- ^ log callback+ -> Eff (Logger : es) a+ -- ^ action using the logger+ -> Eff es a runLogger = evalStaticRep . Logger -- | Run the t'Logger' effect, writing to stderr. -- -- @since 0.1.0.0-runStderrLogger :: IOE :> es => Eff (Logger : es) a -> Eff es a+runStderrLogger+ :: IOE :> es+ => Eff (Logger : es) a+ -- ^ action using the logger+ -> Eff es a runStderrLogger = runLogger (defaultOutput stderr) -- | Run the t'Logger' effect, emitting OpenTelemetry log records. -- -- @since 0.1.0.0-runOTelLogger :: IOE :> es => OTel.Logger -> Eff (Logger : es) a -> Eff es a+runOTelLogger+ :: IOE :> es+ => OTel.Logger+ -- ^ OpenTelemetry logger+ -> Eff (Logger : es) a+ -- ^ action using the logger+ -> Eff es a runOTelLogger = runLogger . OTel.makeOTelLogCallback instance Logger :> es => MonadLogger (Eff es) where