cabal-mon-1.0.2: CabalMon.hs
module Main(main) where
import Graphics.Vty
import Graphics.Vty.Prelude (regionHeight)
import SimpleGetOpt
import Data.Map ( Map )
import qualified Data.Map as Map
import Data.Maybe (mapMaybe, fromMaybe)
import Data.List(isInfixOf, isPrefixOf)
import Data.IORef (IORef, newIORef, modifyIORef', readIORef)
import System.FilePath
import Control.Concurrent (forkIO)
import Control.Monad (unless)
import Control.Exception(handle,catch,SomeException(..))
import System.Process(runInteractiveProcess,terminateProcess)
import System.IO(hGetContents,hSetBuffering,BufferMode(LineBuffering)
, hPutStrLn, stderr )
import System.Directory
(getHomeDirectory,doesFileExist,doesDirectoryExist)
import System.Exit(exitFailure)
import Data.Char(isSpace)
import Text.Read (readMaybe)
import Data.Bits (testBit)
type Name = String
data State = State { buffers :: !(Map FilePath (Status,Buffer))
, pageHeight :: !Int
, watching :: !(Maybe Name)
, prevBufs :: ![Name]
, curBuf :: !(Maybe Name)
, nextBufs :: ![Name]
, logDir :: FilePath
} deriving Show
data Status = Modified Int | Unmodified
deriving Show
data Settings = Settings
{ fsOpts :: [String]
, fsPath :: FilePath
, fsWatchThis :: Maybe FilePath
}
options :: OptSpec Settings
options = OptSpec
{ progDefaults = Settings { fsOpts = []
, fsPath = "fswatch"
, fsWatchThis = Nothing }
, progOptions =
[ Option ['m'] ["monitor"]
"Use this monitor (see: fswatch -m)."
$ ReqArg "MONITOR" $ \a s -> Right s { fsOpts = "-m" : a : fsOpts s }
, Option ['l'] ["latency"]
"Use the latency (see: fswatch -l)."
$ ReqArg "DOUBLE" $ \a s ->
case readMaybe a of
Just n | (n::Double) > 0.1
-> Right s { fsOpts = "-l" : a : fsOpts s }
_ -> Left "Invalid latency."
, Option ['p'] ["path"]
"The path for the `fswatch` executable."
$ ReqArg "PATH" $ \a s -> Right s { fsPath = a }
, Option ['d'] ["dir"]
"Watch this directory of logs."
$ ReqArg "DIR" $ \a s -> Right s { fsWatchThis = Just a }
]
, progParamDocs = []
, progParams = \p _ -> Left ("Unknown parameter: " ++ p)
}
main :: IO ()
main =
do as <- getOpts options
d <- case fsWatchThis as of
Nothing -> guessLogDir
`catch` \SomeException {} ->
do hPutStrLn stderr "Failed to work out directory"
exitFailure
Just d -> return d
(_,hOut,_,p) <- runInteractiveProcess
(fsPath as) ("-r" : "-x" : "-n" : fsOpts as ++ [d]) Nothing Nothing
`catch` \SomeException {} ->
do hPutStrLn stderr ("Failed to start " ++ show (fsPath as))
exitFailure
hSetBuffering hOut LineBuffering
txt <- hGetContents hOut
-- Start VTY
cfg <- standardIOConfig
vty <- mkVty cfg
bnds <- displayBounds (outputIface vty)
st <- newIORef State { buffers = Map.empty
, pageHeight = regionHeight bnds
, prevBufs = []
, curBuf = Nothing
, nextBufs = []
, watching = Nothing
, logDir = d
}
_ <- forkIO $ mapM_ (\x -> do handleUpdate st x
redraw vty st)
$ mapMaybe getUpdate $ lines txt
vtyLoop vty st
terminateProcess p
guessLogDir :: IO FilePath
guessLogDir =
do cfg <- configFile
txt <- readFile cfg
case mapMaybe isLogLine (lines txt) of
l : _ -> return l
-- Default, let's do some guess work
_ ->
do let stackDir = ".stack-work"
yes <- doesDirectoryExist stackDir
if yes
then return (stackDir </> "logs")
else do d <- getHomeDirectory
return (d </> ".cabal" </> "logs")
where
configFile = do let sandbox = "cabal.sandbox.config"
yes <- doesFileExist sandbox
if yes
then return sandbox
else do d <- getHomeDirectory
return (d </> ".cabal" </> "config")
isLogLine x =
case break (== ':') x of
(as,_:bs) | trim as == "logs-dir" -> Just (trim bs)
_ -> Nothing
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace
vtyLoop :: Vty -> IORef State -> IO ()
vtyLoop vty ref =
do redraw vty ref
evt <- nextEvent vty
case evt of
EvResize _ y -> upd $ \st -> st { pageHeight = y }
EvKey k _ ->
case k of
KEsc -> shutdown vty
KChar 'r' ->
do st <- readIORef ref
case curBuf st of
Just b -> do handleUpdate ref b
redraw vty ref
again
Nothing -> again
KEnter ->
upd $ \st ->
case curBuf st of
Nothing -> st
Just x ->
let unmod (_,y) = (Unmodified,y)
in st { watching = Just x
, buffers = case watching st of
Just y -> Map.adjust unmod y (buffers st)
Nothing -> buffers st
}
KUp ->
upd $ \st ->
case prevBufs st of
[] -> st
x : xs -> st { prevBufs = xs
, curBuf = Just x
, nextBufs =
case curBuf st of
Nothing -> nextBufs st
Just a -> a : nextBufs st }
KDown ->
upd $ \st ->
case nextBufs st of
[] -> st
x : xs -> st { nextBufs = xs
, curBuf = Just x
, prevBufs =
case curBuf st of
Nothing -> prevBufs st
Just a -> a : prevBufs st }
KPageUp -> updWatch bufPageUp
KPageDown -> updWatch bufPageDown
KLeft -> updWatch (\_ -> bufLeft)
KRight -> updWatch (\_ -> bufRight)
_ -> again
_ -> again
where
again = vtyLoop vty ref
upd f = do modifyIORef' ref f
again
updWatch f =
upd $ \st ->
case watching st of
Nothing -> st
Just b -> st { buffers = Map.adjust f' b (buffers st) }
where f' (x,y) = (x,f (pageHeight st) y)
redraw :: Vty -> IORef State -> IO ()
redraw vty ref =
do st <- readIORef ref
update vty $ picForImage $ draw st
draw :: State -> Image
draw st = heading <-> (files <|> preview)
where
heading = string (attr black yellow) (logDir st)
files = vertCat $ map (ppItem False) (reverse (prevBufs st)) ++
[ ppItem True (fromMaybe "(no files)" (curBuf st)) ] ++
map (ppItem False) (nextBufs st)
ppItem sel x = case mbN of
Nothing -> string a name
Just n -> string a (name ++ " (" ++ show n ++ ")")
where
name = dropExtension (takeFileName x)
a = if sel then a' `withStyle` reverseVideo else a'
(a',mbN) = case Map.lookup x (buffers st) of
Nothing -> (noramlText, Nothing)
Just (sta,_) ->
case sta of
_ | Just x == watching st -> (watchingTxt, Nothing)
Modified n -> (changedText, Just n)
Unmodified -> (noramlText, Nothing)
preview = case (`Map.lookup` buffers st) =<< watching st of
Just (_,buf) -> vertCat $ map ppLine
$ bufVisiable (pageHeight st) buf
_ -> string noramlText "(not watching)"
attr fg bg = withForeColor (withBackColor defAttr bg) fg
noramlText = attr white black
changedText = attr red black
watchingTxt = attr green black
brightTxt = attr brightGreen black
ppLine (x,i) = string (if i then brightTxt else noramlText) x
--------------------------------------------------------------------------------
updateBit :: Int
updateBit = 2
dropFromEnd :: Int -> [a] -> [a]
dropFromEnd n xs = take (length xs - n) xs
getUpdate :: String -> Maybe FilePath
getUpdate txt =
case words txt of
xs@(_ : _)
| let numStr = last xs
, Just n <- readMaybe numStr
, testBit (n::Int) updateBit -> Just (dropFromEnd (length numStr + 1) txt)
_ -> Nothing
handleUpdate :: IORef State -> FilePath -> IO ()
handleUpdate ref file =
handle (\SomeException {} -> return ()) $
do isDir <- doesDirectoryExist file
unless isDir $
do txt <- readFile file
let ls = lines txt
lNum = length ls
seq lNum $ modifyIORef' ref $ \st ->
case Map.lookup file (buffers st) of
-- Is this a new buffer?
Nothing ->
let bufs1 = Map.insert file
(Modified 1, Buffer { chText = ls
, chStart = Nothing
, chLineNum = lNum
, chLineSt = Nothing
})
(buffers st)
in case curBuf st of
Nothing -> st { buffers = bufs1
, curBuf = Just file
, watching = Just file
}
Just _ -> st { buffers = bufs1
, nextBufs = file : nextBufs st
}
-- Existing buffer
Just (_,b) -> st { buffers = Map.insertWith jn file
(Modified 1, bufSetText
(pageHeight st) ls b)
(buffers st) }
where
jn (Modified x, a) (Modified n, _) = (Modified (x + n), a)
jn a _ = a
--------------------------------------------------------------------------------
data Buffer = Buffer { chText :: ![String]
, chLineNum :: !Int
, chStart :: !(Maybe Int)
, chLineSt :: !(Maybe Int)
} deriving Show
interestingLine :: String -> Bool
interestingLine x = "Linking" `isPrefixOf` x
|| "Registering" `isPrefixOf` x
|| "registering" `isInfixOf` x
bufVisiable :: Int -> Buffer -> [(String,Bool)]
bufVisiable h c =
case chLineSt c of
Nothing -> ls1
Just x -> [ (drop x cs, i) | (cs,i) <- ls1 ]
where
ls1 = [ (l, interestingLine l) | l <- ls ]
ls = case chStart c of
Just n -> take h $ drop n $ chText c
Nothing -> drop (chLineNum c - h) (chText c)
bufRight :: Buffer -> Buffer
bufRight c = c { chLineSt = case chLineSt c of
Nothing -> Just 2
Just n -> Just (n + 2) }
bufLeft :: Buffer -> Buffer
bufLeft c = c { chLineSt = case chLineSt c of
Just n | n > 2 -> Just (n - 2)
_ -> Nothing }
bufPageUp :: Int -> Buffer -> Buffer
bufPageUp h c = c { chStart = Just (max 0 (start - pageStep h)) }
where start = case chStart c of
Just n -> n
Nothing -> max 0 (chLineNum c - h)
bufPageDown :: Int -> Buffer -> Buffer
bufPageDown h c =
case chStart c of
Nothing -> c
Just n -> bufCorrectEndOfText h c { chStart = Just (n + pageStep h) }
pageStep :: Int -> Int
pageStep h = max 1 (div (2 * h) 3)
bufCorrectEndOfText :: Int -> Buffer -> Buffer
bufCorrectEndOfText h c =
case chStart c of
Just n | n >= chLineNum c - h -> c { chStart = Nothing }
_ -> c
bufSetText :: Int -> [String] -> Buffer -> Buffer
bufSetText h xs cs = bufCorrectEndOfText h
cs { chText = xs, chLineNum = length xs }