    {-# LANGUAGE LambdaCase #-}
    -----------------------------------------------------------------------------
    -- |
    -- Module      :  XMonad.Prompt.Pass
    -- Description :  A prompt for interacting with @pass(1)@.
    -- Copyright   :  (c) 2014 Igor Babuschkin, Antoine R. Dumont
    -- License     :  BSD3-style (see LICENSE)
    --
    -- Maintainer  :  Antoine R. Dumont <eniotna.t@gmail.com>
    -- Stability   :  unstable
    -- Portability :  unportable
    --
    -- A thin wrapper around the standard @pass(1)@ UNIX utility.
    --
    -- This module provides several prompts to ease password manipulation
    -- (generate, read, edit, remove); all of them benefit from the
    -- completion system provided by "XMonad.Prompt".  Specifically, we
    -- provide
    --
    -- - various functions to lookup passwords in the password-store:
    --
    --     + 'passPrompt' copies the password directly to the clipboard.
    --
    --     + 'passOTPPrompt' copies a one-time-password to the clipboard
    --        (this uses <https://github.com/tadfisher/pass-otp pass-otp>).
    --
    --     + 'passTypePrompt' and 'passOTPTypePrompt' work like the above,
    --       respectively, but use @xdotool@ to type out the password.
    --
    -- - 'passGeneratePrompt' generates a password for a given password
    --   label that the user inputs.
    --
    -- - 'passEditPrompt' edits a password for a given password label that
    --   the user inputs.
    --
    -- - 'passRemovePrompt' deletes a stored password for a given password
    --   label that the user inputs.
    --
    -- The password store is setup through an environment variable
    -- @$PASSWORD_STORE_DIR@, or @$HOME\/.password-store@ if it is unset.
    -- The editor is determined from the environment variable @$EDITOR@.
    --
    -- Source:
    --
    -- - The <https://www.passwordstore.org/ password store>
    --   implementation is <http://git.zx2c4.com/password-store here>.
    --
    -- - Inspired by <http://babushk.in/posts/combining-xmonad-and-pass.html>
    --
    -----------------------------------------------------------------------------

    module XMonad.Prompt.Pass
        ( -- * Usage
          -- $usage

          -- * Retrieving passwords
          passPrompt
        , passPrompt'
        , passTypePrompt

          -- * Editing passwords
        , passEditPrompt
        , passEditPrompt'
        , passRemovePrompt
        , passRemovePrompt'
        , passGeneratePrompt
        , passGeneratePrompt'
        , passGenerateAndCopyPrompt
        , passGenerateAndCopyPrompt'

          -- * One-time-passwords
        , passOTPPrompt
        , passOTPTypePrompt
        ) where

    import System.Directory (getHomeDirectory)
    import System.FilePath (dropExtension, (</>))
    import System.Posix.Env (getEnv)
    import XMonad
    import XMonad.Prelude
    import XMonad.Prompt
      ( XPConfig,
        XPrompt,
        commandToComplete,
        getNextCompletion,
        mkXPrompt,
        nextCompletion,
        searchPredicate,
        showXPrompt,
      )
    import XMonad.Util.Run (runProcessWithInput)

    -- $usage
    -- You can use this module with the following in your @xmonad.hs@:
    --
    -- > import XMonad.Prompt.Pass
    --
    -- Then add a keybinding for 'passPrompt', 'passGeneratePrompt',
    -- 'passRemovePrompt', 'passEditPrompt' or 'passTypePrompt':
    --
    -- >   , ((modMask , xK_p)                              , passPrompt def)
    -- >   , ((modMask .|. controlMask, xK_p)               , passGeneratePrompt def)
    -- >   , ((modMask .|. shiftMask, xK_p)                 , passEditPrompt def)
    -- >   , ((modMask .|. controlMask  .|. shiftMask, xK_p), passRemovePrompt def)
    --
    -- You can also use the versions that let you specify a custom prompt:
    --
    -- >   , ((modMask , xK_p)                              , passPrompt' "Ask 'pass' for" def)
    --
    -- Note that, by default, we do not use fuzzy matching in this module.
    -- To enable this feature, import the "XMonad.Prompt.FuzzyMatch" module
    -- and add the relevant functions to your prompt configuration:
    --
    -- > myXPConfig :: XPConfig
    -- > myXPConfig = def
    -- >   { searchPredicate = fuzzyMatch
    -- >   , sorter          = fuzzySort
    -- >   }
    -- >
    -- > , ((modMask , xK_p), passPrompt myXPConfig)
    --
    -- For detailed instructions on:
    --
    -- - editing your key bindings, see <https://xmonad.org/TUTORIAL.html#customizing-xmonad the tutorial>.
    --
    -- - how to setup the password store, see <http://git.zx2c4.com/password-store/about/>
    --   or @man 1 pass@.
    --

    ---------------------------------------------------------------------------------
    -- Prompt

    type PromptLabel = String

    newtype Pass = Pass PromptLabel

    instance XPrompt Pass where
      showXPrompt :: Pass -> String
    showXPrompt       (Pass String
    prompt) = String
    prompt String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    ": "
      commandToComplete :: Pass -> String -> String
    commandToComplete Pass
    _ String
    c           = String
    c
      nextCompletion :: Pass -> String -> [String] -> String
    nextCompletion      Pass
    _           = String -> [String] -> String
    getNextCompletion

    -- | A prompt to retrieve a password from a given entry.
    --
    passPrompt :: XPConfig -> X ()
    passPrompt :: XPConfig -> X ()
    passPrompt = String -> XPConfig -> X ()
    passPrompt' String
    "Select password"

    -- | The same as 'passPrompt' but with a user-specified prompt.
    passPrompt' :: String -> XPConfig -> X ()
    passPrompt' :: String -> XPConfig -> X ()
    passPrompt' String
    s = String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    s String -> X ()
    selectPassword

    -- | A prompt to retrieve a OTP from a given entry.  Note that you will
    -- need to use the <https://github.com/tadfisher/pass-otp pass-otp>
    -- extension for this to work.
    --
    passOTPPrompt :: XPConfig -> X ()
    passOTPPrompt :: XPConfig -> X ()
    passOTPPrompt = String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    "Select OTP" String -> X ()
    selectOTP

    -- | A prompt to retrieve a OTP from a given entry.  Note that you will
    -- need to use the <https://github.com/tadfisher/pass-otp pass-otp>
    -- extension for this to work.
    --
    passOTPTypePrompt :: XPConfig -> X ()
    passOTPTypePrompt :: XPConfig -> X ()
    passOTPTypePrompt = String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    "Select OTP" String -> X ()
    selectOTPType

    -- | A prompt to generate a password for a given entry.
    -- This can be used to override an already stored entry.
    -- (Beware that no confirmation is asked)
    --
    passGeneratePrompt :: XPConfig -> X ()
    passGeneratePrompt :: XPConfig -> X ()
    passGeneratePrompt = String -> XPConfig -> X ()
    passGeneratePrompt' String
    "Generate password"

    -- | The same as 'passGeneratePrompt' but with a user-specified prompt.
    passGeneratePrompt' :: String -> XPConfig -> X ()
    passGeneratePrompt' :: String -> XPConfig -> X ()
    passGeneratePrompt' String
    s = String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    s String -> X ()
    generatePassword

    -- | A prompt to generate a password for a given entry and immediately copy it
    -- to the clipboard.  This can be used to override an already stored entry.
    -- (Beware that no confirmation is asked)
    --
    passGenerateAndCopyPrompt :: XPConfig -> X ()
    passGenerateAndCopyPrompt :: XPConfig -> X ()
    passGenerateAndCopyPrompt = String -> XPConfig -> X ()
    passGenerateAndCopyPrompt' String
    "Generate and copy password"

    -- | The same as 'passGenerateAndCopyPrompt' but with a user-specified prompt.
    passGenerateAndCopyPrompt' :: String -> XPConfig -> X ()
    passGenerateAndCopyPrompt' :: String -> XPConfig -> X ()
    passGenerateAndCopyPrompt' String
    s = String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    s String -> X ()
    generateAndCopyPassword

    -- | A prompt to remove a password for a given entry.
    -- (Beware that no confirmation is asked)
    --
    passRemovePrompt :: XPConfig -> X ()
    passRemovePrompt :: XPConfig -> X ()
    passRemovePrompt = String -> XPConfig -> X ()
    passRemovePrompt' String
    "Remove password"

    -- | The same as 'passRemovePrompt' but with a user-specified prompt.
    passRemovePrompt' :: String -> XPConfig -> X ()
    passRemovePrompt' :: String -> XPConfig -> X ()
    passRemovePrompt' String
    s = String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    s String -> X ()
    removePassword

    -- | A prompt to type in a password for a given entry.
    -- This doesn't touch the clipboard.
    --
    passTypePrompt :: XPConfig -> X ()
    passTypePrompt :: XPConfig -> X ()
    passTypePrompt = String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    "Type password" String -> X ()
    typePassword

    -- | A prompt to edit a given entry.
    -- This doesn't touch the clipboard.
    --
    passEditPrompt :: XPConfig -> X ()
    passEditPrompt :: XPConfig -> X ()
    passEditPrompt = String -> XPConfig -> X ()
    passEditPrompt' String
    "Edit password"

    -- | The same as 'passEditPrompt' but with a user-specified prompt.
    passEditPrompt' :: String -> XPConfig -> X ()
    passEditPrompt' :: String -> XPConfig -> X ()
    passEditPrompt' String
    s = String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    s String -> X ()
    editPassword

    -- | A pass prompt factory.
    --
    mkPassPrompt :: PromptLabel -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt :: String -> (String -> X ()) -> XPConfig -> X ()
    mkPassPrompt String
    promptLabel String -> X ()
    passwordFunction XPConfig
    xpconfig = do
      [String]
    passwords <- IO [String] -> X [String]
    forall (m :: * -> *) a. MonadIO m => IO a -> m a
    io (IO String
    passwordStoreFolder IO String -> ComplFunction -> IO [String]
    forall a b. IO a -> (a -> IO b) -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= ComplFunction
    getPasswords)
      Pass -> XPConfig -> ComplFunction -> (String -> X ()) -> X ()
    forall p.
    XPrompt p =>
    p -> XPConfig -> ComplFunction -> (String -> X ()) -> X ()
    mkXPrompt (String -> Pass
    Pass String
    promptLabel)
                XPConfig
    xpconfig
                ([String] -> (String -> String -> Bool) -> ComplFunction
    getPassCompl [String]
    passwords ((String -> String -> Bool) -> ComplFunction)
    -> (String -> String -> Bool) -> ComplFunction
    forall a b. (a -> b) -> a -> b
    $ XPConfig -> String -> String -> Bool
    searchPredicate XPConfig
    xpconfig)
                String -> X ()
    passwordFunction
     where
      getPassCompl :: [String] -> (String -> String -> Bool) -> String -> IO [String]
      getPassCompl :: [String] -> (String -> String -> Bool) -> ComplFunction
    getPassCompl [String]
    compls String -> String -> Bool
    p String
    s = [String] -> IO [String]
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return ([String] -> IO [String]) -> [String] -> IO [String]
    forall a b. (a -> b) -> a -> b
    $ (String -> Bool) -> [String] -> [String]
    forall a. (a -> Bool) -> [a] -> [a]
    filter (String -> String -> Bool
    p String
    s) [String]
    compls

      -- Compute the password store's location. Use the @$PASSWORD_STORE_DIR@
      -- environment variable to set the password store. If empty, return the
      -- password store located in user's home.
      passwordStoreFolder :: IO String
      passwordStoreFolder :: IO String
    passwordStoreFolder =
        String -> IO (Maybe String)
    getEnv String
    "PASSWORD_STORE_DIR" IO (Maybe String) -> (Maybe String -> IO String) -> IO String
    forall a b. IO a -> (a -> IO b) -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= Maybe String -> IO String
    computePasswordStoreDir
       where
        -- Default password store folder in @$HOME/.password-store@.
        computePasswordStoreDir :: Maybe String -> IO String
        computePasswordStoreDir :: Maybe String -> IO String
    computePasswordStoreDir = \case
          Maybe String
    Nothing       -> IO String
    getHomeDirectory IO String -> (String -> String) -> IO String
    forall (f :: * -> *) a b. Functor f => f a -> (a -> b) -> f b
    <&> (String -> String -> String
    </> String
    ".password-store")
          Just String
    storeDir -> String -> IO String
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return String
    storeDir

      -- Retrieve the list of passwords from the password store @passwordStoreDir@.
      getPasswords :: FilePath -> IO [String]
      getPasswords :: ComplFunction
    getPasswords String
    passwordStoreDir = do
        String
    files <- String -> [String] -> String -> IO String
    forall (m :: * -> *).
    MonadIO m =>
    String -> [String] -> String -> m String
    runProcessWithInput String
    "find" [
          String
    "-L", -- Traverse symlinks
          String
    passwordStoreDir,
          String
    "-type", String
    "f",
          String
    "-name", String
    "*.gpg",
          String
    "-printf", String
    "%P\n"] []
        [String] -> IO [String]
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return ([String] -> IO [String])
    -> ([String] -> [String]) -> [String] -> IO [String]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . (String -> String) -> [String] -> [String]
    forall a b. (a -> b) -> [a] -> [b]
    map String -> String
    dropExtension ([String] -> IO [String]) -> [String] -> IO [String]
    forall a b. (a -> b) -> a -> b
    $ String -> [String]
    lines String
    files

    ---------------------------------------------------------------------------------
    -- Selecting a password

    -- | Select a password.
    --
    selectPassword :: String -> X ()
    selectPassword :: String -> X ()
    selectPassword = String -> X ()
    forall (m :: * -> *). MonadIO m => String -> m ()
    spawn (String -> X ()) -> (String -> String) -> String -> X ()
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> String -> String
    pass String
    "--clip"

    -- | Select a one-time-password and copy it to the clipboard.
    --
    selectOTP :: String -> X ()
    selectOTP :: String -> X ()
    selectOTP = String -> X ()
    forall (m :: * -> *). MonadIO m => String -> m ()
    spawn (String -> X ()) -> (String -> String) -> String -> X ()
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> String -> String
    pass String
    "otp --clip"

    -- | Select a one-time-password and type it out.
    --
    selectOTPType :: String -> X ()
    selectOTPType :: String -> X ()
    selectOTPType = String -> X ()
    forall (m :: * -> *). MonadIO m => String -> m ()
    spawn (String -> X ()) -> (String -> String) -> String -> X ()
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> String
    typeString (String -> String) -> (String -> String) -> String -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> String -> String
    pass String
    "otp"

    -- | Generate a 30 characters password for a given entry.
    -- If the entry already exists, it is updated with a new password.
    --
    generatePassword :: String -> X ()
    generatePassword :: String -> X ()
    generatePassword String
    passLabel = String -> X ()
    forall (m :: * -> *). MonadIO m => String -> m ()
    spawn (String -> X ()) -> String -> X ()
    forall a b. (a -> b) -> a -> b
    $ String -> String -> String
    pass String
    "generate --force" String
    passLabel String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    " 30"

    -- | Generate a 30 characters password for a given entry.
    -- If the entry already exists, it is updated with a new password.
    -- After generating the password, it is copied to the clipboard.
    --
    generateAndCopyPassword :: String -> X ()
    generateAndCopyPassword :: String -> X ()
    generateAndCopyPassword String
    passLabel = String -> X ()
    forall (m :: * -> *). MonadIO m => String -> m ()
    spawn (String -> X ()) -> String -> X ()
    forall a b. (a -> b) -> a -> b
    $ String -> String -> String
    pass String
    "generate --force -c" String
    passLabel String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    " 30"

    -- | Remove a password stored for a given entry.
    --
    removePassword :: String -> X ()
    removePassword :: String -> X ()
    removePassword = String -> X ()
    forall (m :: * -> *). MonadIO m => String -> m ()
    spawn (String -> X ()) -> (String -> String) -> String -> X ()
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> String -> String
    pass String
    "rm --force"

    -- | Edit a password stored for a given entry.
    --
    editPassword :: String -> X ()
    editPassword :: String -> X ()
    editPassword = String -> X ()
    forall (m :: * -> *). MonadIO m => String -> m ()
    spawn (String -> X ()) -> (String -> String) -> String -> X ()
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> String -> String
    pass String
    "edit"

    -- | Type a password stored for a given entry using xdotool.
    --
    typePassword :: String -> X ()
    typePassword :: String -> X ()
    typePassword = String -> X ()
    forall (m :: * -> *). MonadIO m => String -> m ()
    spawn (String -> X ()) -> (String -> String) -> String -> X ()
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> String
    typeString (String -> String) -> (String -> String) -> String -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String -> String -> String
    pass String
    ""

    -- | Type the given string with @xdotool@.
    --
    -- >>> typeString (pass "" "arXiv")
    -- "pass  \"arXiv\" | head -n1 | tr -d '\n' | xdotool type --clearmodifiers --file -"
    typeString :: String -> String
    typeString :: String -> String
    typeString String
    cmd = String
    cmd String -> String -> String
    forall a. [a] -> [a] -> [a]
    ++ String
    " | head -n1 | tr -d '\n' | xdotool type --clearmodifiers --file -"

    -- | Generate a pass prompt.
    --
    -- >>> pass "otp" "\\n'git'\"hub\""
    -- "pass otp \"\\\\n'git'\\\"hub\\\"\""
    pass :: String -> String -> String
    pass :: String -> String -> String
    pass String
    cmd String
    label = [String] -> String
    forall (t :: * -> *) a. Foldable t => t [a] -> [a]
    concat [String
    "pass ", String
    cmd, String
    " \"", (Char -> String) -> String -> String
    forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
    concatMap Char -> String
    escape String
    label, String
    "\""]
     where
      escape :: Char -> String
      escape :: Char -> String
    escape Char
    '"'  = String
    "\\\""
      escape Char
    '\\' = String
    "\\\\"
      escape Char
    x    = [Char
    x]