packages feed

gibberish-3.0.0.0: app/Main.hs

module Main (main) where

import Data.Gibberish
import Data.Gibberish.Formatting qualified as Fmt

import Data.Text (Text ())
import Data.Text qualified as Text
import Data.Text.IO qualified as Text
import Data.Version (showVersion)
import Options.Applicative hiding (columns)
import Options.Applicative.Help.Pretty
import Paths_gibberish (version)
import System.Environment (getArgs)
import Prelude hiding (Word ())

termLen :: Int
termLen = 80

termHeight :: Int
termHeight = 10

data Options = Options
  { -- | Options that are always used
    optCommon :: CommonOpts,
    -- | Options that only apply to a specific types
    optType :: Either WordOpts PhraseOpts
  }
  deriving stock (Eq, Show)

data CommonOpts = CommonOpts
  { -- | Include capitals?
    optCapitals :: !Bool,
    -- | Include numerals?
    optDigits :: !Bool,
    -- | Include special characters?
    optSpecials :: !Bool,
    -- | How many passwords/phrases to generate
    optNumber :: !(Maybe Int),
    -- | The dictionary to use
    optLanguage :: !LanguageOpt
  }
  deriving stock (Eq, Show)

data WordOpts = WordOpts
  {optLength :: Int}
  deriving stock (Eq, Show)

data PhraseOpts = PhraseOpts
  { optMinLength :: Int,
    optMaxLength :: Int
  }
  deriving stock (Eq, Show)

data LanguageOpt
  = OptEnglish
  | OptSpanish
  | OptLangCustom !TrigraphConfig
  deriving stock (Eq, Show)

data PassType = Passphrase | Password
  deriving stock (Eq, Show)

main :: IO ()
main = run =<< execParser' opts
  where
    opts = info (optsParser <**> parseVersion <**> helper) mods
    mods = briefDesc <> progDesc desc
    desc =
      "Generates pronounceable passwords that are easy-to-remember and\
      \ hard-to-guess."

run :: Options -> IO ()
run (Options {..}) = Text.putStrLn =<< run' optType =<< getStdGen
  where
    run' (Left w) = passwords optCommon w
    run' (Right p) = passphrases optCommon p

passwords :: RandomGen gen => CommonOpts -> WordOpts -> gen -> IO Text
passwords (CommonOpts {..}) (WordOpts {..}) gen = do
  trigraph <- liftIO $ loadTrigraph (toLanguage optLanguage)

  let genOpts =
        GenPasswordOpts
          { woptsCapitals = optCapitals,
            woptsDigits = optDigits,
            woptsSpecials = optSpecials,
            woptsTrigraph = trigraph,
            woptsLength = optLength
          }
      formatOpts =
        Fmt.FormatOpts
          { optMaxLen = Fmt.MaxLen termLen,
            optMaxHeight = Fmt.MaxHeight termHeight,
            optSeparator = Fmt.Separator "  ",
            optExactWords = Fmt.ExactNumberWords <$> optNumber
          }

      (res, _) = usingPass gen (genPasswords genOpts)

  pure (Fmt.formatWords formatOpts res)

passphrases :: RandomGen gen => CommonOpts -> PhraseOpts -> gen -> IO Text
passphrases (CommonOpts {..}) (PhraseOpts {..}) gen = do
  trigraph <- liftIO $ loadTrigraph English

  let genOpts =
        GenPassphraseOpts
          { poptsCapitals = optCapitals,
            poptsDigits = optDigits,
            poptsSpecials = optSpecials,
            poptsTrigraph = trigraph,
            poptsMinLength = optMinLength,
            poptsMaxLength = optMaxLength
          }
      formatOpts =
        Fmt.FormatOpts
          { optMaxLen = Fmt.MaxLen termLen,
            optMaxHeight = Fmt.MaxHeight termHeight,
            optSeparator = Fmt.Separator " ",
            optExactWords = Fmt.ExactNumberWords <$> optNumber
          }

      (res, _) = usingPass gen (genPassphrase genOpts)

  pure (Fmt.formatWords formatOpts res)

execParser' :: ParserInfo a -> IO a
execParser' info' =
  execParserPure defaultPrefs info' <$> getArgs
    >>= handleParseResult . overFailure'

overFailure' :: ParserResult a -> ParserResult a
overFailure' = overFailure $ \help' -> help' {helpUsage = pure usage}

usage :: Doc
usage =
  hsep
    [ pretty ("Usage:" :: Text),
      align $
        vsep
          [ "gibber [option...] length",
            "gibber --passphrase [option...] min-length max-length"
          ]
    ]

optsParser :: Parser Options
optsParser = parseOptions
  where
    parseOptions =
      Options
        <$> parseCommonOpts
        <*> parseTypeOpts

parseCommonOpts :: Parser CommonOpts
parseCommonOpts =
  CommonOpts
    <$> parseCapitals
    <*> parseDigits
    <*> parseSpecials
    <*> parseNumber
    <*> parseLanguage

parseCapitals :: Parser Bool
parseCapitals =
  switch $
    long "capitals"
      <> short 'c'
      <> help "Include at least one capital letter"

parseDigits :: Parser Bool
parseDigits =
  switch $
    long "digits"
      <> short 'd'
      <> help "Include numerals"

parseSpecials :: Parser Bool
parseSpecials =
  switch $
    long "symbols"
      <> short 's'
      <> help "Include special characters"

parseNumber :: Parser (Maybe Int)
parseNumber =
  optional $
    option auto $
      long "number"
        <> short 'n'
        <> metavar "NUMBER"
        <> help "The number of passwords to generate"

parseLanguage :: Parser LanguageOpt
parseLanguage =
  (OptLangCustom . TrigraphConfig <$> parseTrigraph)
    <|> parseSpanish
    <|> parseEnglish
  where
    parseTrigraph =
      option str $
        long "trigraph"
          <> short 't'
          <> help "Use a custom trigraph config"
    parseSpanish =
      flag' OptSpanish $
        long "spanish"
          <> short 'a'
          <> help "Use spanish dictionary"

    parseEnglish =
      flag OptEnglish OptEnglish $
        long "english"
          <> short 'e'
          <> help "Use english dictionary [default]"

parseTypeOpts :: Parser (Either WordOpts PhraseOpts)
parseTypeOpts =
  (Left <$> parseWordOpts) <|> (Right <$> parsePhraseOpts)

parseWordOpts :: Parser WordOpts
parseWordOpts =
  (WordOpts <$> parseLength)

parsePhraseOpts :: Parser PhraseOpts
parsePhraseOpts =
  parseTypePhrase *> (PhraseOpts <$> parseMinLength <*> parseMaxLength)

parseTypePhrase :: Parser PassType
parseTypePhrase =
  flag' Passphrase $
    long "passphrase"
      <> short 'p'
      <> help "Generate passphrases instead of passwords"

parseVersion :: Parser (a -> a)
parseVersion =
  infoOption (Text.unpack showVersion') $
    long "version"
      <> short 'v'
      <> help "Show version information"

parseLength :: Parser Int
parseLength = argument auto (metavar "length")

parseMinLength :: Parser Int
parseMinLength = argument auto (metavar "min-length")

parseMaxLength :: Parser Int
parseMaxLength =
  argument auto $
    metavar "max-length"
      <> value 10

showVersion' :: Text
showVersion' = name' <> " version " <> version'
  where
    version' = Text.pack (showVersion version)
    name' = "Gibberish"

toLanguage :: LanguageOpt -> Language
toLanguage OptEnglish = English
toLanguage OptSpanish = Spanish
toLanguage (OptLangCustom file) = CustomTrigraph file