packages feed

matterhorn-30802.1.0: src/Login.hs

{-# LANGUAGE MultiWayIf #-}
module Login
  ( interactiveGatherCredentials
  ) where

import Prelude ()
import Prelude.Compat

import Brick
import Brick.Widgets.Edit
import Brick.Widgets.Center
import Brick.Widgets.Border
import Control.Monad.IO.Class (liftIO)
import Data.Maybe (isNothing)
import Text.Read (readMaybe)
import qualified Data.Text as T
import Graphics.Vty hiding (Config)
import System.Exit (exitSuccess)

import Network.Mattermost.Exceptions (LoginFailureException(..))

import Config
import Markdown
import Types (ConnectionInfo(..), AuthenticationException(..))

data Name = Hostname | Port | Username | Password deriving (Ord, Eq, Show)

data State =
    State { hostnameEdit :: Editor T.Text Name
          , portEdit     :: Editor T.Text Name
          , usernameEdit :: Editor T.Text Name
          , passwordEdit :: Editor T.Text Name
          , focus    :: Name
          , previousError :: Maybe AuthenticationException
          }

toPassword :: [T.Text] -> Widget a
toPassword s = txt $ T.replicate (T.length $ T.concat s) "*"

interactiveGatherCredentials :: Config
                             -> Maybe AuthenticationException
                             -> IO ConnectionInfo
interactiveGatherCredentials config authError = do
    let state = State { hostnameEdit = editor Hostname (txt . T.concat) (Just 1) hStr
                      , portEdit = editor Port (txt . T.concat) (Just 1) (T.pack $ show $ configPort config)
                      , usernameEdit = editor Username (txt . T.concat) (Just 1) uStr
                      , passwordEdit = editor Password toPassword     (Just 1) pStr
                      , focus = initialFocus
                      , previousError = authError
                      }
        hStr = maybe "" id $ configHost config
        uStr = maybe "" id $ configUser config
        pStr = case configPass config of
            Just (PasswordString s) -> s
            _                       -> ""
        initialFocus = if | T.null hStr -> Hostname
                          | T.null uStr -> Username
                          | T.null pStr -> Password
                          | otherwise   -> Hostname
    finalSt <- defaultMain app state
    let finalH = T.concat $ getEditContents $ hostnameEdit finalSt
        finalPort = read $ T.unpack $ T.concat $ getEditContents $ portEdit finalSt
        finalU = T.concat $ getEditContents $ usernameEdit finalSt
        finalPass = T.concat $ getEditContents $ passwordEdit finalSt
    return $ ConnectionInfo finalH finalPort finalU finalPass

app :: App State e Name
app = App
  { appDraw         = credsDraw
  , appChooseCursor = showFirstCursor
  , appHandleEvent  = onEvent
  , appStartEvent   = return
  , appAttrMap      = const colorTheme
  }

errorAttr :: AttrName
errorAttr = "errorMessage"

colorTheme :: AttrMap
colorTheme = attrMap defAttr
  [ (editAttr, black `on` white)
  , (editFocusedAttr, black `on` yellow)
  , (errorAttr, fg red)
  ]

credsDraw :: State -> [Widget Name]
credsDraw st =
    [ center (credentialsForm st <=> errorMessageDisplay st)
    ]

errorMessageDisplay :: State -> Widget Name
errorMessageDisplay st = do
    case previousError st of
        Nothing -> emptyWidget
        -- XXX
        Just e -> txt " " <=>
                  (withDefAttr errorAttr $
                   hCenter (str "Error: " <+> renderAuthError e))

renderAuthError :: AuthenticationException -> Widget Name
renderAuthError (ConnectError _) = txt "Could not connect to server"
renderAuthError (ResolveError _) = txt "Could not resolve server hostname"
renderAuthError (OtherAuthError e) = str $ show e
renderAuthError (LoginError (LoginFailureException msg)) = str msg

credentialsForm :: State -> Widget Name
credentialsForm st =
    hCenter $ hLimit 50 $ vLimit 15 $
    border $
    vBox [ renderText "Please enter your MatterMost credentials to log in."
         , txt " "
         , txt "Hostname:" <+> renderEditor (focus st == Hostname) (hostnameEdit st)
         , txt " "
         , txt "Port:    " <+> renderEditor (focus st == Port) (portEdit st)
         , txt " "
         , txt "Username:" <+> renderEditor (focus st == Username) (usernameEdit st)
         , txt " "
         , txt "Password:" <+> renderEditor (focus st == Password) (passwordEdit st)
         , txt " "
         , renderText "Press Enter to log in or Esc to exit."
         ]

onEvent :: State -> BrickEvent Name e -> EventM Name (Next State)
onEvent _  (VtyEvent (EvKey KEsc [])) = liftIO exitSuccess
onEvent st (VtyEvent (EvKey (KChar '\t') [])) =
    continue $ st { focus = if | focus st == Hostname -> Port
                               | focus st == Port     -> Username
                               | focus st == Username -> Password
                               | focus st == Password -> Hostname
                  }
onEvent st (VtyEvent (EvKey KEnter [])) =
    -- check for valid (non-empty) contents
    let h = T.concat $ getEditContents $ hostnameEdit st
        u = T.concat $ getEditContents $ usernameEdit st
        p = T.concat $ getEditContents $ passwordEdit st
        port :: Maybe Int
        port = readMaybe (T.unpack $ T.concat $ getEditContents $ portEdit st)
    in case T.null h || T.null u || T.null p || isNothing port of
        True -> continue st
        False -> halt st
onEvent st (VtyEvent e) =
    case focus st of
        Hostname -> do
            e' <- handleEditorEvent e (hostnameEdit st)
            continue $ st { hostnameEdit = e' }
        Port -> do
            e' <- handleEditorEvent e (portEdit st)
            continue $ st { portEdit = e' }
        Username -> do
            e' <- handleEditorEvent e (usernameEdit st)
            continue $ st { usernameEdit = e' }
        Password -> do
            e' <- handleEditorEvent e (passwordEdit st)
            continue $ st { passwordEdit = e' }
onEvent st _ = continue st