diff --git a/iris.cabal b/iris.cabal index ed8eeb6..6cae547 100644 --- a/iris.cabal +++ b/iris.cabal @@ -46,6 +46,7 @@ common common-options default-language: Haskell2010 default-extensions: ConstraintKinds + DeriveAnyClass DeriveGeneric DerivingStrategies GeneralizedNewtypeDeriving @@ -67,6 +68,10 @@ library exposed-modules: Iris Iris.App + Iris.Browse + Iris.Cli + Iris.Cli.Browse + Iris.Cli.Version Iris.Colour Iris.Colour.Formatting Iris.Colour.Mode @@ -74,9 +79,11 @@ library build-depends: , ansi-terminal ^>= 0.11 + , directory ^>= 1.3 , bytestring >= 0.10 && < 0.12 , mtl >= 2.2 && < 2.4 , optparse-applicative ^>= 0.17 + , process ^>= 1.6 executable iris-example import: common-options diff --git a/src/Iris.hs b/src/Iris.hs index 517e318..47fe80a 100644 --- a/src/Iris.hs +++ b/src/Iris.hs @@ -13,10 +13,14 @@ Haskell CLI framework module Iris ( module Iris.App + , module Iris.Browse + , module Iris.Cli , module Iris.Colour , module Iris.Env ) where import Iris.App +import Iris.Browse +import Iris.Cli import Iris.Colour import Iris.Env diff --git a/src/Iris/Browse.hs b/src/Iris/Browse.hs new file mode 100644 index 0000000..6a4462e --- /dev/null +++ b/src/Iris/Browse.hs @@ -0,0 +1,149 @@ +{- | +Module : Iris.Browse +Copyright : (c) 2020 Kowainik + (c) 2022 Dmitrii Kovanikov +SPDX-License-Identifier : MPL-2.0 +Maintainer : Dmitrii Kovanikov +Stability : Experimental +Portability : Portable + +Implements a function that opens a given file in a browser. + +@since 0.0.0.0 +-} + +module Iris.Browse + ( openInBrowser + , BrowseException (..) + ) where + +import Control.Exception (Exception, throwIO) +import System.Directory (findExecutable) +import System.Environment (lookupEnv) +import System.Info (os) +import System.Process (callCommand, showCommandForUser) + + +{- | Exception thrown by 'openInBrowser' if can't find a +browser. Stores the current OS inside. + +@since 0.0.0.0 +-} +newtype BrowseException + = BrowserNotFoundException String + deriving stock (Show) + deriving newtype (Eq) + deriving anyclass (Exception) + + +{- | Open a given file in a browser. The function has the following algorithm: + +* Check the @BROWSER@ environment variable +* If it's not set, try to guess browser depending on OS +* If unsuccsessful, print a message + +__Throws:__ 'BrowseException' if can't find a browser. + +@since 0.0.0.0 +-} +openInBrowser :: FilePath -> IO () +openInBrowser file = lookupEnv "BROWSER" >>= \case + Just browser -> runCommand browser [file] + Nothing -> case os of + "darwin" -> runCommand "open" [file] + "mingw32" -> runCommand "cmd" ["/c", "start", file] + curOs -> do + browserExe <- findFirstExecutable + [ "xdg-open" + , "cygstart" + , "x-www-browser" + , "firefox" + , "opera" + , "mozilla" + , "netscape" + ] + case browserExe of + Just browser -> runCommand browser [file] + Nothing -> throwIO $ BrowserNotFoundException curOs + +-- | Execute a command with arguments. +runCommand :: FilePath -> [String] -> IO () +runCommand cmd args = do + let cmdStr = showCommandForUser cmd args + putStrLn $ "⚙ " ++ cmdStr + callCommand cmdStr + +findFirstExecutable :: [FilePath] -> IO (Maybe FilePath) +findFirstExecutable = \case + [] -> pure Nothing + exe:exes -> findExecutable exe >>= \case + Nothing -> findFirstExecutable exes + Just path -> pure $ Just path + +{- +------------------------ +-- Original source code: +------------------------ + +{- | +Copyright: (c) 2020 Kowainik +SPDX-License-Identifier: MPL-2.0 +Maintainer: Kowainik + +Contains implementation of a function that opens a given file in a +browser. +-} + +module Stan.Browse + ( openBrowser + ) where + +import Colourista (errorMessage, infoMessage) +import System.Directory (findExecutable) +import System.Info (os) +import System.Process (callCommand, showCommandForUser) + + +{- | Open a given file in a browser. The function has the following algorithm: + +* Check the @BROWSER@ environment variable +* If it's not set, try to guess browser depending on OS +* If unsuccsessful, print a message +-} +openBrowser :: FilePath -> IO () +openBrowser file = lookupEnv "BROWSER" >>= \case + Just browser -> runCommand browser [file] + Nothing -> case os of + "darwin" -> runCommand "open" [file] + "mingw32" -> runCommand "cmd" ["/c", "start", file] + curOs -> do + browserExe <- findFirstExecutable + [ "xdg-open" + , "cygstart" + , "x-www-browser" + , "firefox" + , "opera" + , "mozilla" + , "netscape" + ] + case browserExe of + Just browser -> runCommand browser [file] + Nothing -> do + errorMessage $ "Cannot guess browser for the OS: " <> toText curOs + infoMessage "Please set the $BROWSER environment variable to a web launcher" + exitFailure + +-- | Execute a command with arguments. +runCommand :: FilePath -> [String] -> IO () +runCommand cmd args = do + let cmdStr = showCommandForUser cmd args + putStrLn $ "⚙ " ++ cmdStr + callCommand cmdStr + +findFirstExecutable :: [FilePath] -> IO (Maybe FilePath) +findFirstExecutable = \case + [] -> pure Nothing + exe:exes -> findExecutable exe >>= \case + Nothing -> findFirstExecutable exes + Just path -> pure $ Just path +-} diff --git a/src/Iris/Cli.hs b/src/Iris/Cli.hs new file mode 100644 index 0000000..f8b1264 --- /dev/null +++ b/src/Iris/Cli.hs @@ -0,0 +1,20 @@ +{- | +Module : Iris.Cli +Copyright : (c) 2022 Dmitrii Kovanikov +SPDX-License-Identifier : MPL-2.0 +Maintainer : Dmitrii Kovanikov +Stability : Experimental +Portability : Portable + +CLI options parsing. + +@since 0.0.0.0 +-} + +module Iris.Cli + ( module Iris.Cli.Browse + , module Iris.Cli.Version + ) where + +import Iris.Cli.Browse +import Iris.Cli.Version diff --git a/src/Iris/Cli/Browse.hs b/src/Iris/Cli/Browse.hs new file mode 100644 index 0000000..228bd6c --- /dev/null +++ b/src/Iris/Cli/Browse.hs @@ -0,0 +1,45 @@ +{- | +Module : Iris.Cli.Browse +Copyright : (c) 2022 Dmitrii Kovanikov +SPDX-License-Identifier : MPL-2.0 +Maintainer : Dmitrii Kovanikov +Stability : Experimental +Portability : Portable + +CLI options parsing for @--browse@ and @--browse=@. + +@since 0.0.0.0 +-} + +module Iris.Cli.Browse + ( browseP + , browseFileP + ) where + +import qualified Options.Applicative as Opt + +{- | A CLI option parse a boolean value if a file needs browsing. + +Use 'Iris.Browse.openInBrowser' to open the file of your choice in a +browser. + +@since 0.0.0.0 +-} +browseP :: String -> Opt.Parser Bool +browseP description = Opt.switch $ mconcat + [ Opt.long "browse" + , Opt.help description + ] + +{- | A CLI option parser for a 'FilePath' that needs to be open wit + +Use 'Iris.Browse.openInBrowser' to open the passed file in a browser. + +@since 0.0.0.0 +-} +browseFileP :: String -> Opt.Parser FilePath +browseFileP description = Opt.option Opt.str $ mconcat + [ Opt.long "browse" + , Opt.metavar "FILE_PATH" + , Opt.help description + ] diff --git a/src/Iris/Cli/Version.hs b/src/Iris/Cli/Version.hs new file mode 100644 index 0000000..0909d3d --- /dev/null +++ b/src/Iris/Cli/Version.hs @@ -0,0 +1,74 @@ +{- | +Module : Iris.Cli.Version +Copyright : (c) 2022 Dmitrii Kovanikov +SPDX-License-Identifier : MPL-2.0 +Maintainer : Dmitrii Kovanikov +Stability : Experimental +Portability : Portable + +CLI options parsing for @--version@ and @--numeric-version@ + +**Enabled with config** + +@since 0.0.0.0 +-} + +module Iris.Cli.Version + ( -- * Settings + VersionSettings (..) + , defaultVersionSettings + + -- * CLI parser + , fullVersionP + + -- * Internal helpers + , mkVersionParser + ) where + +import Data.Version (Version, showVersion) + +import qualified Options.Applicative as Opt + + +{- | + +@since 0.0.0.0 +-} +data VersionSettings = VersionSettings + { -- | @since 0.0.0.0 + versionSettingsVersion :: Version + + -- | @since 0.0.0.0 + , versionSettingsMkDesc :: String -> String + } + +{- | + +@since 0.0.0.0 +-} +defaultVersionSettings :: Version -> VersionSettings +defaultVersionSettings version = VersionSettings + { versionSettingsVersion = version + , versionSettingsMkDesc = id + } + +mkVersionParser :: Maybe VersionSettings -> Opt.Parser (a -> a) +mkVersionParser = maybe (pure id) fullVersionP + +fullVersionP :: VersionSettings -> Opt.Parser (a -> a) +fullVersionP VersionSettings{..} = versionP <*> numericVersionP + where + versionStr :: String + versionStr = showVersion versionSettingsVersion + + versionP :: Opt.Parser (a -> a) + versionP = Opt.infoOption (versionSettingsMkDesc versionStr) $ mconcat + [ Opt.long "version" + , Opt.help "Show application version" + ] + + numericVersionP :: Opt.Parser (a -> a) + numericVersionP = Opt.infoOption versionStr $ mconcat + [ Opt.long "numeric-version" + , Opt.help "Show only numeric application version" + ] diff --git a/src/Iris/Env.hs b/src/Iris/Env.hs index d571f97..203bd44 100644 --- a/src/Iris/Env.hs +++ b/src/Iris/Env.hs @@ -16,12 +16,8 @@ Environment of a CLI app. module Iris.Env ( -- * Settings for the CLI app - -- ** Global CLI settings CliEnvSettings (..) , defaultCliEnvSettings - -- ** Application version settings - , VersionSettings (..) - , defaultVersionSettings -- * CLI application environment -- ** Constructing @@ -34,9 +30,9 @@ module Iris.Env import Control.Monad.Reader (MonadReader, asks) import Data.Kind (Type) -import Data.Version (Version, showVersion) import System.IO (stderr, stdout) +import Iris.Cli.Version (VersionSettings, mkVersionParser) import Iris.Colour.Mode (ColourMode, handleColourMode) import qualified Options.Applicative as Opt @@ -77,27 +73,6 @@ defaultCliEnvSettings = CliEnvSettings , cliEnvSettingsVersionSettings = Nothing } -{- | - -@since 0.0.0.0 --} -data VersionSettings = VersionSettings - { -- | @since 0.0.0.0 - versionSettingsVersion :: Version - - -- | @since 0.0.0.0 - , versionSettingsMkDesc :: String -> String - } - -{- | - -@since 0.0.0.0 --} -defaultVersionSettings :: Version -> VersionSettings -defaultVersionSettings version = VersionSettings - { versionSettingsVersion = version - , versionSettingsMkDesc = id - } {- | CLI application environment. It contains default settings for every CLI app and parameter @@ -156,27 +131,6 @@ mkCliEnv CliEnvSettings{..} = do , Opt.progDesc cliEnvSettingsProgDesc ] -mkVersionParser :: Maybe VersionSettings -> Opt.Parser (a -> a) -mkVersionParser = maybe (pure id) fullVersionP - -fullVersionP :: VersionSettings -> Opt.Parser (a -> a) -fullVersionP VersionSettings{..} = versionP <*> numericVersionP - where - versionStr :: String - versionStr = showVersion versionSettingsVersion - - versionP :: Opt.Parser (a -> a) - versionP = Opt.infoOption (versionSettingsMkDesc versionStr) $ mconcat - [ Opt.long "version" - , Opt.help "Show application version" - ] - - numericVersionP :: Opt.Parser (a -> a) - numericVersionP = Opt.infoOption versionStr $ mconcat - [ Opt.long "numeric-version" - , Opt.help "Show only numeric application version" - ] - {- | Get a field from the global environment 'CliEnv'. @since 0.0.0.0