gamgee-1.0.0: app/Gamgee/Program/Effects.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Gamgee.Program.Effects
( runM_
, runGamgeeByteStoreIO
, runOutputStdOut
, runOutputClipboard
, runErrorStdErr
) where
import Control.Exception.Safe (catch)
import qualified Data.Text.IO as TIO
import qualified Gamgee.Effects as Eff
import qualified Gamgee.Token as Token
import Polysemy (Lift, Member, Members, Sem)
import qualified Polysemy as P
import qualified Polysemy.Error as P
import qualified Polysemy.Output as P
import Relude
import qualified System.Directory as Dir
import System.FilePath ((</>))
import qualified System.Hclip as Clip
import qualified System.IO.Error as IO
import qualified System.Posix.Files as Files
-- | A version of runM that ignores its result
runM_ :: Monad m => Sem '[Lift m] a -> m ()
runM_ = void . P.runM
----------------------------------------------------------------------------------------------------
-- Interpret Output by writing it to stdout or clipboard
----------------------------------------------------------------------------------------------------
runOutputStdOut :: Member (Lift IO) r => Sem (P.Output Text : r) a -> Sem r a
runOutputStdOut = P.interpret $ \case
P.Output s -> P.sendM $ putTextLn s
runOutputClipboard :: Member (Lift IO) r => Sem (P.Output Text : r) a -> Sem r a
runOutputClipboard = P.interpret $ \case
P.Output s -> P.sendM $ Clip.setClipboard $ toString s
----------------------------------------------------------------------------------------------------
-- Interpret Error by writing it to stderr
----------------------------------------------------------------------------------------------------
instance ToText Eff.EffError where
toText (Eff.AlreadyExists ident) = "A token named '" <> Token.unTokenIdentifier ident <> "' already exists."
toText (Eff.NoSuchToken ident) = "No such token: '" <> Token.unTokenIdentifier ident <> "'"
toText (Eff.CryptoError ce) = show ce
toText (Eff.CorruptIV _) = "Internal Error: Unable to decode initial vector, your config is probably corrupt"
toText (Eff.CorruptBase64Encoding msg) = msg
toText (Eff.SecretDecryptError _) = "Error decrypting token. Did you provide an incorrect password?"
toText (Eff.InvalidTokenPeriod tp) = "Unsupported token period: " <> show (Token.unTokenPeriod tp)
toText (Eff.UnsupportedConfigVersion v) = "Internal Error: Unsupported config version: " <> show v
toText (Eff.JSONDecodeError msg) = "Internal Error: Could not decode Gamgee config file: " <> msg
data ByteStoreError = ReadError IO.IOError
| WriteError IO.IOError
instance ToText ByteStoreError where
toText (ReadError e) = "Internal Error: Error reading configuration file: " <> show e
toText (WriteError e) = "Internal Error: Error saving configuration file: " <> show e
runErrorStdErr :: Member (Lift IO) r => Sem (P.Error Eff.EffError : P.Error ByteStoreError : r) a -> Sem r (Maybe a)
runErrorStdErr = fmap join . runToTextError . runToTextError
runToTextError :: (Member (Lift IO) r, ToText e) => Sem (P.Error e : r) a -> Sem r (Maybe a)
runToTextError a = P.runError a >>= either (printError . toText) (return . Just)
where
printError :: Member (Lift IO) r => Text -> Sem r (Maybe a)
printError msg = P.sendM (TIO.hPutStrLn stderr msg) $> Nothing
----------------------------------------------------------------------------------------------------
-- Interpret ByteStore using a file
----------------------------------------------------------------------------------------------------
runByteStoreFile :: ( Members [Lift IO, P.Error e] r
, Exception e1
, Exception e2)
=> FilePath
-> (e1 -> Either e (Maybe LByteString)) -- ^ Function to handle read errors
-> (e2 -> Maybe e) -- ^ Function to handle write errors
-> Sem (Eff.ByteStore : r) a
-> Sem r a
runByteStoreFile file handleReadError handleWriteError = P.interpret $ \case
Eff.ReadByteStore -> do
res <- P.sendM $ (Right . Just <$> readFileLBS file) `catch` (return . handleReadError)
either P.throw return res
Eff.WriteByteStore bytes -> do
res <- P.sendM $ (writeFileLBS file bytes $> Nothing) `catch` (return . handleWriteError)
whenJust res P.throw
P.sendM $ Files.setFileMode file $ Files.ownerReadMode `Files.unionFileModes` Files.ownerWriteMode
runGamgeeByteStoreIO :: Members [Lift IO, P.Error ByteStoreError] r
=> Sem (Eff.ByteStore : r) a
-> Sem r a
runGamgeeByteStoreIO prog = do
file <- P.sendM configFilePath
runByteStoreFile file handleReadError handleWriteError prog
where
handleReadError :: IO.IOError -> Either ByteStoreError (Maybe LByteString)
handleReadError e = if IO.isDoesNotExistError e
then Right Nothing
else Left $ ReadError e
handleWriteError :: IO.IOError -> Maybe ByteStoreError
handleWriteError e = Just $ WriteError e
-- | Path under which tokens are stored - typically ~/.config/gamgee/tokens.json
configFilePath :: IO FilePath
configFilePath = do
dir <- Dir.getXdgDirectory Dir.XdgConfig "gamgee"
Dir.createDirectoryIfMissing True dir
return $ dir </> "tokens.json"