-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCastle.hs
185 lines (144 loc) · 6.21 KB
/
Castle.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
{-# 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)