{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Main where import Data.Maybe (fromMaybe) import Data.IORef import Control.Applicative import Control.Monad (forM_, forM) import Control.Monad.Trans.Class (lift, MonadTrans(..)) import Control.Monad.Trans.Cont import Control.Monad.IO.Class (MonadIO(..)) import Control.Arrow (second) import Control.Monad.Trans.Except (throwE, ExceptT(..), runExceptT) import Text.PrettyPrint.ANSI.Leijen hiding ((), (<>), (<$>)) import Data.Monoid ((<>)) import Data.Map (Map) import System.IO (hFlush, stdout) import System.Environment (getArgs) import System.Exit (exitFailure) import System.FilePath.Posix (()) import Data.Text.Template import Data.Text (Text) import qualified System.Directory as D import qualified System.FilePath.Posix as D import qualified Data.Text.IO as T import qualified Data.Text as T import qualified Data.Map as M usage :: IO a usage = do putStrLn $ unlines ["usage: skeletons " ,"" ," is a folder in $HOME/.config/closet which contain templates" ] exitFailure type Skel a = ExceptT Doc IO a newtype Default = Default { unDefault :: Text } deriving (Show) newtype SkelM a = SkelM { runSkel :: Skel a } deriving (Monad, Functor, Applicative) instance MonadIO SkelM where liftIO = SkelM . lift lastSplit :: [t] -> Maybe (t, t) lastSplit [] = Nothing lastSplit [_] = Nothing lastSplit [x,y] = Just (x, y) lastSplit (_:xs) = lastSplit xs parseDefault :: Text -> Maybe (Text, Default) parseDefault = fmap (second Default) . lastSplit . map T.strip . T.splitOn "?" specialVars :: FilePath -> Text -> IO (Maybe Text) specialVars _ "$basename" = Just . T.pack . D.takeBaseName <$> D.getCurrentDirectory specialVars fp "$filename" = return . Just . T.pack $ fp specialVars _ _ = return Nothing promptWithDefault :: Default -> Text -> IO Text promptWithDefault (unDefault -> def) var = do putStr $ T.unpack var <> " (" <> T.unpack def <> "): " hFlush stdout input <- getLine return $ if null input then def else T.pack input prompt :: Text -> IO Text prompt var = do putStr $ T.unpack var <> ": " hFlush stdout fmap T.pack getLine processVar :: (Monad (t IO), MonadTrans t) => FilePath -> Text -> t IO Text processVar file var = do let mioDef = do (defVar, def) <- parseDefault var return (defVar, def, fmap Default <$> specialVars file (unDefault def)) matchedVar <- lift $ specialVars file var case (matchedVar, mioDef) of -- we got a matched return it (Just val, _) -> lift $ return val -- we got a default variable, check to see if it's a special var (_, Just (defVar, def, ioMatchDef)) -> do matchDef <- lift ioMatchDef case matchDef of -- we got a default variable that was matched with a special var Just matched -> lift $ promptWithDefault matched defVar -- we got a default variable that was NOT matched with a special var Nothing -> lift $ promptWithDefault def defVar -- We just got a regular var, prompt! (_, Nothing) -> lift $ prompt var process :: IORef (Map Text Text) -> FilePath -> Text -> IO Text process ref file contents = do let procs var = evalContT (mapContT (mapper var) (processVar file var)) applyTemplate procs (parseTemplate contents) where mapper var mr = do vals <- readIORef ref case M.lookup var vals of Just cached -> do -- putStrLn $ T.unpack $ "cache hit on " <> var <> " val " <> cached return cached Nothing -> do r <- mr -- cache the result modifyIORef ref (M.insert var r) -- putStrLn $ T.unpack $ "caching " <> T.pack (show var) <> " val " <> r return r info :: String -> Doc info s = white (text s) err :: Doc err = red (text "error") defaultSkelDir :: FilePath -> FilePath defaultSkelDir homeDir = homeDir ".config/closet" skeleton :: Maybe FilePath -> FilePath -> SkelM [(FilePath, Text)] skeleton mcloset skel = do homeDir <- liftIO D.getHomeDirectory let closet = fromMaybe (defaultSkelDir homeDir) mcloset path :: String path = closet skel dirExists <- liftIO (D.doesDirectoryExist path) if not dirExists then SkelM $ throwE $ err <+> squotes (text path) <+> text "does not exist" else do files <- liftIO (D.getDirectoryContents path) let files' = filter (not . (`elem` [".", ".."])) files ref <- liftIO $ newIORef M.empty liftIO $ forM files' $ \file -> do contents <- liftIO (T.readFile $ path file) processed <- process ref file contents return (file, processed) note :: Doc -> IO () note doc = putStrLn (displayS (renderPretty 1.0 80 doc) "") runSkeleton :: FilePath -> IO (Either Doc [(FilePath, Text)]) runSkeleton x = runExceptT $ runSkel $ skeleton Nothing x main :: IO () main = do args <- getArgs case args of -- TODO: configurable closet path (x:_) -> do result <- runSkeleton x case result of Left doc -> note doc Right res -> forM_ res $ \(file, contents) -> do note (info "writing" <+> text file) T.writeFile file contents [] -> usage