teleport-0.0.0.9: app/Main.hs
--Copyright (c) 2015 Siddharth Bhat
--Permission is hereby granted, free of charge, to any person obtaining
--a copy of this software and associated documentation files (the "Software")
--to deal in the Software without restriction, including without limitation the
--rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
--sell copies of the Software, and to permit persons to whom the Software is
--furnished to do so, subject to the following conditions:
-- The above copyright notice and this permission notice shall
-- be included in all copies or substantial portions of the Software.
--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
--OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
--FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
--OTHER DEALINGS IN THE SOFTWARE.
#!/usr/bin/env stack
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
import Control.Monad
import Data.Traversable
import Data.Maybe
import Data.List
import Prelude hiding (FilePath)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T.Encoding
import Options.Applicative
import Filesystem.Path.CurrentOS as Path
import qualified Turtle
--import qualified Data.Configurator as Config
import qualified Data.Aeson as JSON
import Data.Aeson ((.=), (.:))
import qualified Data.ByteString.Lazy as B
import qualified System.Console.ANSI as ANSI
import Debug.Trace
-- options passed to 'warp list'
data ListOptions = ListOptions deriving (Show)
-- options pased to 'warp add'
data AddOptions = AddOptions {
folderPath :: FilePath,
addname :: String
} deriving (Show)
-- options passed to 'warp remove'
data RemoveOptions = RemoveOptions {
removename :: String
} deriving (Show)
-- options parrsed to 'warp goto'
data GotoOptions = GotoOptions {
gotoname :: String
} deriving(Show)
-- the combined datatype representing all warp commands
data Command = CommandList ListOptions |
CommandAdd AddOptions |
CommandRemove RemoveOptions |
CommandGoto GotoOptions
deriving (Show)
-- an abstract entity representing a point to which we can warp to
data WarpPoint = WarpPoint {
name :: String,
absFolderPath :: String
} deriving (Show)
-- the main data that is loaded from JSON
data WarpData = WarpData {
warpPoints :: [WarpPoint]
} deriving (Show)
defaultWarpData :: WarpData
defaultWarpData = WarpData {
warpPoints = []
}
-- flip function for ease of chaining computations
(|>) :: a -> (a -> b) -> b
(|>) = flip ($)
filePathToString :: FilePath -> String
filePathToString = Path.encodeString
warpProgDesc :: String
warpProgDesc = "use warp to quickly setup warp points and move to these " ++
"when needed"
warpHeader :: String
warpHeader = "Warp: move around your filesystem"
main :: IO ()
main = do
command <- execParser (info (helper <*> parseCommand)
(fullDesc <>
progDesc warpProgDesc <>
header warpHeader))
run command
-- Data Loading
-- """"""""""""
-- parse warpPoint
instance JSON.FromJSON WarpPoint where
parseJSON (JSON.Object v) =
WarpPoint <$> v .: "name"
<*> v .: "absFolderPath"
instance JSON.ToJSON WarpPoint where
toJSON (WarpPoint {..}) =
JSON.object [ "name" .= name
,"absFolderPath" .= absFolderPath]
-- parse warpData
instance JSON.FromJSON WarpData where
parseJSON (JSON.Object v) =
WarpData <$> v .: "warpPoints"
instance JSON.ToJSON WarpData where
toJSON(WarpData{..}) =
JSON.object ["warpPoints" .= warpPoints]
dieJSONParseError :: FilePath -> String -> IO WarpData
dieJSONParseError jsonFilePath err =
("parse error in: " ++ (show jsonFilePath) ++
"\nerror:------\n" ++ err) |>
T.pack |>
Turtle.die
decodeWarpData :: FilePath -> IO WarpData
decodeWarpData jsonFilePath = do
rawInput <- B.readFile (filePathToString jsonFilePath)
let jsonResult = JSON.eitherDecode' rawInput
case jsonResult of
Left err -> dieJSONParseError jsonFilePath err
Right json -> return json
createWarpDataFile :: FilePath -> IO ()
createWarpDataFile jsonFilePath = saveWarpData jsonFilePath defaultWarpData
loadWarpData :: FilePath -> IO WarpData
loadWarpData jsonFilePath = do
exists <- (Turtle.testfile jsonFilePath)
if exists then
decodeWarpData jsonFilePath
else
do
createWarpDataFile jsonFilePath
return defaultWarpData
saveWarpData :: FilePath -> WarpData -> IO ()
saveWarpData jsonFilePath warpData = do
let dataBytestring = JSON.encode warpData
Turtle.touch jsonFilePath
B.writeFile (filePathToString jsonFilePath) dataBytestring
getWarpDataPath :: IO FilePath
getWarpDataPath = do
homeFolder <- Turtle.home
return $ homeFolder </> ".warpdata"
-- Common parsers
-- """"""""""""""
readFolderPath :: String -> ReadM FilePath
readFolderPath s = T.pack s |>
Path.fromText |>
(\path -> if Path.valid path
then return path
else readerError ("invalid path: " ++ (show path)))
warpnameParser :: Parser String
warpnameParser = argument str
(metavar "NAME" <>
help "name of the warp point for usage")
-- Command parsers
-- """""""""""""""
parseAddCommand :: Parser Command
parseAddCommand =
CommandAdd <$> (AddOptions <$> folderParser <*> warpnameParser) where
folderParser = option
(str >>= readFolderPath)
--(long "path" <>
--short 'p' <>
(value "./" <>
metavar "FOLDERPATH" <>
help "path of the warp folder to warp to")
parseListCommand :: Parser Command
parseListCommand = pure (CommandList ListOptions)
parseRemoveCommand :: Parser Command
parseRemoveCommand = CommandRemove <$> (RemoveOptions <$> warpnameParser)
parseGotoCommand :: Parser Command
parseGotoCommand = CommandGoto <$> (GotoOptions <$> warpnameParser)
parseCommand :: Parser Command
parseCommand = subparser
-- add command
((command "add" (info parseAddCommand (progDesc "add a warp point"))) <>
-- list command
(command "list"
(info parseListCommand (progDesc "list all warp points"))) <>
-- remove command
(command "remove"
(info parseRemoveCommand (progDesc "remove a warp point"))) <>
-- goto command
(command "goto"
(info parseGotoCommand (progDesc "go to a created warp point"))))
-- Stream Helpers
-- """"""""""""""
setErrorColor :: IO ()
setErrorColor = ANSI.setSGR [ANSI.SetColor ANSI.Foreground ANSI.Vivid ANSI.Red]
warpPointPrint :: WarpPoint -> IO ()
warpPointPrint warpPoint = do
ANSI.setSGR [ANSI.SetColor ANSI.Foreground ANSI.Dull ANSI.White]
putStr (name warpPoint)
ANSI.setSGR [ANSI.SetColor ANSI.Foreground ANSI.Vivid ANSI.Blue]
putStr "\t"
putStr (absFolderPath warpPoint)
putStr "\n"
-- Add command runner
-- """"""""""""""""""
folderNotFoundError :: FilePath -> IO ()
folderNotFoundError path =
setErrorColor >>
("unable to find folder: " ++ (show path)) |>
T.pack |>
Turtle.die
needFolderNotFileError :: FilePath -> IO ()
needFolderNotFileError path =
setErrorColor >>
("expected folder, not file: " ++ (show path)) |>
T.pack |>
Turtle.die
dieIfFolderNotFound :: FilePath -> IO ()
dieIfFolderNotFound path =
do
folderExists <- Turtle.testdir path
fileExists <- Turtle.testfile path
-- error checking
when fileExists (needFolderNotFileError path)
unless folderExists (folderNotFoundError path)
-- we know the folder exists
dieWarpPointExists :: WarpPoint -> IO ()
dieWarpPointExists warpPoint = do
setErrorColor
putStrLn ("warp point " ++ (name warpPoint) ++ " already exists:\n")
warpPointPrint warpPoint
Turtle.die ""
runAdd :: AddOptions -> IO ()
runAdd AddOptions{..} = do
dieIfFolderNotFound folderPath
print "yay, folder exists"
print "loding warp data"
warpDataPath <- getWarpDataPath
warpData <- loadWarpData warpDataPath
absFolderPath <- Turtle.realpath folderPath
let existingWarpPoint = find (\warp -> name warp == addname) (warpPoints warpData)
case existingWarpPoint of
Just warpPoint -> dieWarpPointExists warpPoint
Nothing -> do
let newWarpPoint = WarpPoint {
name = addname,
absFolderPath = filePathToString absFolderPath
}
putStrLn "creating warp point: \n"
warpPointPrint newWarpPoint
let newWarpData = WarpData {
warpPoints = newWarpPoint:(warpPoints warpData)
}
saveWarpData warpDataPath newWarpData
-- List Command
-- """"""""""""
runList :: ListOptions -> IO ()
runList ListOptions = do
warpDataPath <- getWarpDataPath
warpData <- loadWarpData warpDataPath
forM_ (warpPoints warpData) warpPointPrint
-- Remove Command
-- """""""""""""""
dieWarpPointNotFound :: String ->IO ()
dieWarpPointNotFound name =
setErrorColor >>
(name ++ " warp point not found") |>
T.pack |>
Turtle.die
runRemove :: RemoveOptions -> IO ()
runRemove RemoveOptions{..} = do
warpDataPath <- getWarpDataPath
warpData <- loadWarpData warpDataPath
let wantedWarpPoint = find (\warp -> name warp == removename) (warpPoints warpData)
case wantedWarpPoint of
Nothing -> dieWarpPointNotFound removename
Just _ -> do
let newWarpPoints = filter (\warp -> name warp /= removename)
(warpPoints warpData)
let newWarpData = warpData {
warpPoints = newWarpPoints
}
saveWarpData warpDataPath newWarpData
runGoto :: GotoOptions -> IO ()
runGoto GotoOptions{..} = do
warpDataPath <- getWarpDataPath
warpData <- loadWarpData warpDataPath
let wantedWarpPoint = find (\warp -> name warp == gotoname) (warpPoints warpData)
case wantedWarpPoint of
Nothing -> dieWarpPointNotFound gotoname
Just warpPoint -> do
Turtle.echo (T.pack (absFolderPath warpPoint))
Turtle.exit (Turtle.ExitFailure 2)
run :: Command -> IO ()
run command =
case command of
CommandAdd addOpt -> runAdd addOpt
CommandList listOpt -> runList listOpt
CommandRemove removeOpt -> runRemove removeOpt
CommandGoto gotoOpt -> runGoto gotoOpt
other @ _ -> print other