castle-0.1.0.1: Castle.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Main where
import Control.Monad
import Data.Maybe
import Data.Monoid
import qualified Data.Text as T
import Data.Tree
import Filesystem
import Filesystem.Path.CurrentOS as FS
import Options.Applicative
import qualified Options.Applicative as O
import Prelude hiding (FilePath)
import Shelly
import qualified Shelly as S
-- Settings
castleDir :: IO FilePath
castleDir = (FS.</> ".castle") <$> getHomeDirectory
-- Shell utilities
cabal_ :: T.Text -> [T.Text] -> Sh ()
cabal_ = command1_ "cabal" []
sandbox_ :: T.Text -> [T.Text] -> Sh ()
sandbox_ cmd = cabal_ "sandbox" . (cmd:)
-- Workflow and utility functions
installCastle :: Sh ()
installCastle = do
castle <- liftIO castleDir
chdir (parent castle) $ do
mkdirTree $ (filename castle) # leaves ["castles"]
where (#) = Node
leaves = map (# [])
getSandboxDir :: T.Text -> Sh FilePath
getSandboxDir name = liftIO $ fmap subdir castleDir
where subdir base = base FS.</> "castles" FS.</> S.fromText name
withSandbox :: T.Text -> (FilePath -> Sh ()) -> (FilePath -> Sh ()) -> Sh ()
withSandbox name onExists onFail = do
sandboxDir <- getSandboxDir name
exists <- test_d sandboxDir
if exists
then onExists sandboxDir
else onFail sandboxDir
withSandbox' :: T.Text -> (FilePath -> Sh ()) -> Sh ()
withSandbox' name onExists = withSandbox name onExists noop
where noop = const $ return ()
getConfigFile :: Sh FilePath
getConfigFile = (FS.</> "cabal.sandbox.config") <$> pwd
listCastles :: Sh [T.Text]
listCastles = liftIO (fmap (FS.</> "castles") castleDir)
>>= ls
>>= fmap (map (toTextIgnore . basename)) . filterM test_d
-- Command function
castle :: CastleCmd -> Sh ()
castle ListCmd = mapM_ echo =<< listCastles
castle NewCmd{..} = withSandbox castleName
(const $ errorExit $ "Sandbox " <> castleName <> " already exists.")
(\d -> mkdir_p d >> chdir d
(sandbox_ "init" ["--sandbox=" <> toTextIgnore d]))
castle UseCmd{..} = withSandbox castleName
(\d -> pwd >>= cp (d FS.</> "cabal.sandbox.config"))
(const $ errorExit $ "Sandbox " <> castleName <> " does not exist.\
\ Create it with 'sandbox new'.")
castle CurrentCmd = do
configFile <- getConfigFile
whenM (not <$> test_f configFile) $
errorExit "No sandbox in this directory."
config <- T.lines <$> readfile configFile
maybe (errorExit "No 'prefix:' line in configuration file.")
(echo . toTextIgnore . FS.basename . FS.fromText . T.drop 10)
. listToMaybe
$ filter (T.isPrefixOf " prefix: ") config
castle RemoveCmd = do
configFile <- getConfigFile
whenM (not <$> test_f configFile) $
errorExit "No sandbox in this directory."
rm configFile
castle DeleteCmd{..} = withSandbox castleName
rm_rf
(const $ errorExit $ "Sandbox " <> castleName <> " does not exist.")
castle ClearCmd{..} =
withSandbox' castleName rm_rf >> castle (NewCmd castleName)
castle SearchCmd{..} =
mapM_ echo =<< filter (T.isInfixOf searchQuery) <$> listCastles
-- Main
main :: IO ()
main = do
CastleOpts{..} <- execParser opts
shelly $ verbosely $ do
installCastle
castle mode
where
opts' = CastleOpts
<$> subparser ( O.command "list" listCmd
<> O.command "new" newCmd
<> O.command "use" useCmd
<> O.command "current" currCmd
<> O.command "remove" rmCmd
<> O.command "delete" delCmd
<> O.command "clear" clrCmd
<> O.command "search" srchCmd
)
listCmd = pinfo (pure ListCmd) "List sand castles." mempty
newCmd = pinfo (NewCmd <$> castleNameArg "The name of the castle to create.")
"Create a new castle." mempty
useCmd = pinfo (UseCmd <$> castleNameArg "The name of the castle to use.")
"Use an existing castle." mempty
currCmd = pinfo (pure CurrentCmd) "Display the current castle name."
mempty
rmCmd = pinfo (pure RemoveCmd) "Removes the sandbox from the current directory."
mempty
delCmd = pinfo (DeleteCmd <$> castleNameArg "The name of the castle to delete.")
"Deletes the castle." mempty
clrCmd = pinfo (ClearCmd <$> castleNameArg "The name of the castle to clear.")
"Clears a castle by deleting and re-creating it." mempty
srchCmd = pinfo (SearchCmd <$> textArg "QUERY" "Search the castles\
\ for one matching the name.")
"Searches for a castle with a name containing the QUERY." mempty
opts = pinfo opts' "Manage shared cabal sandboxes."
(header "castle - manage shared cabal sandboxes.")
-- Command-line parsing
-- | This is a builder utility for ParserInfo instances.
pinfo :: Parser a -> String -> InfoMod a -> ParserInfo a
pinfo p desc imod = info (helper <*> p) (fullDesc <> progDesc desc <> imod)
textOption :: Mod OptionFields T.Text -> Parser T.Text
textOption fields = option (T.pack <$> str) fields
fileOption :: Mod OptionFields FilePath -> Parser FilePath
fileOption fields = option (decodeString <$> str) fields
textArg :: String -> String -> Parser T.Text
textArg meta helpText = argument (T.pack <$> str) (metavar meta <> help helpText)
castleNameArg :: String -> Parser T.Text
castleNameArg = textArg "CASTLE_NAME"
data CastleOpts
= CastleOpts
{ mode :: CastleCmd
} deriving (Show)
data CastleCmd
= ListCmd
| NewCmd { castleName :: T.Text }
| UseCmd { castleName :: T.Text }
| CurrentCmd
| RemoveCmd
| DeleteCmd { castleName :: T.Text }
| ClearCmd { castleName :: T.Text }
| SearchCmd { searchQuery :: T.Text }
deriving (Show)