pvd-1.1.0: Pvd.hs
module Main (
main
) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad (when)
import Data.Maybe (maybe)
import Data.List
import Data.Ord (comparing)
import Data.Function (on)
import Network.Socket
import qualified Codec.Image.DevIL as IL (ilInit)
import qualified Graphics.X11.Xlib as X
import qualified Graphics.X11.Xlib.Extras as X
import System.Exit (exitSuccess)
import System.IO
import System (getArgs)
import System.Console.GetOpt
import XUtils
type Index = TVar Int
type Playlist = TVar [String]
type Cache = TVar [(String, CachedImg)]
data CachedImg = CachedImg XImg | LoadingImg | LoadFailed
data State = State {
stIdx :: Index,
stPlaylist :: Playlist,
stDpy :: X.Display,
stWin :: X.Window,
stImgCache :: Cache,
stImgCacheSize :: Int,
stSocket :: Socket
}
data Flag =
Port String | CacheSize Int | Playlist String | Help
deriving (Eq)
main = do
args <- getArgs
(flags, files1) <- parseOptions args
files2 <- readPlaylist flags
let cacheSize = last [c | CacheSize c <- CacheSize 15 : flags]
port = last [p | Port p <- Port "4245" : flags]
IL.ilInit
(dpy,win) <- initX
socket <- initSocket port
cache <- newTVarIO []
idx <- newTVarIO 0
playlist <- newTVarIO (files1++files2)
let state = State {
stIdx = idx, stPlaylist = playlist, stDpy = dpy, stWin = win,
stImgCache = cache, stImgCacheSize = cacheSize, stSocket = socket
}
forkIO $ cacheLoop state
forkIO $ eventLoop state
socketLoop state
readPlaylist fs = fmap (concatMap words) $ sequence [readFile pl | (Playlist pl) <- fs]
printHelp = mapM_ putStrLn
[ "Usage:\n pvd [OPTION...] [FILE...]\n"
, "Photo Viewer Daemon - a daemon for viewing photos.\n"
, usageInfo "Available options:" options
]
options =
[ Option "h" ["help"] (NoArg Help) "print this help text"
, Option "p" ["port"] (ReqArg Port "PORT") "photo viewer daemon port"
, Option "c" ["cache"] (ReqArg (CacheSize . read) "SIZE") "photo cache size"
, Option "l" ["playlist"] (ReqArg Playlist "PLAYLIST") "playlist file"
]
parseOptions argv = case getOpt Permute options argv of
(o,n,[] ) -> if Help `elem` o then printHelp >> exitSuccess else return (o, n)
(_,_,errs) -> printHelp >> fail (concatMap (filter ('\n' /=)) errs)
initSocket port = withSocketsDo $ do
addrinfos <- getAddrInfo
(Just (defaultHints {addrFlags = [AI_PASSIVE]}))
Nothing (Just port)
let serveraddr = head addrinfos
sock <- socket (addrFamily serveraddr) Stream defaultProtocol
bindSocket sock (addrAddress serveraddr)
listen sock 5
return sock
socketLoop state = do
(connsock, clientaddr) <- accept (stSocket state)
forkIO $ processMessages connsock clientaddr
socketLoop state
where
processMessages connsock clientaddr = do
connhdl <- socketToHandle connsock ReadMode
hSetBuffering connhdl LineBuffering
messages <- hGetContents connhdl
redraw <- fmap or $ mapM (atomically . parseCmd state) (lines messages)
when redraw $ sendExposeEvent (stDpy state) (stWin state)
hClose connhdl
eventLoop state = do
img <- atomically (fetchImage state)
drawImg (stDpy state) (stWin state) img
X.allocaXEvent $ \e -> X.nextEvent (stDpy state) e
eventLoop state
cacheLoop state = do
path <- atomically (fetchNextPath state >>= putImgInCache state LoadingImg)
img <- fmap (maybe LoadFailed CachedImg) (loadXImg (stDpy state) path)
atomically (putImgInCache state img path)
cacheLoop state
fetchImage state = do
idx <- readTVar (stIdx state)
pl <- readTVar (stPlaylist state)
path <- if idx >= 0 && idx < length pl then return (pl !! idx) else retry
cache <- readTVar (stImgCache state)
case lookup path cache of
Just (CachedImg img) -> return img
_ -> retry
fetchNextPath state = do
pl <- readTVar (stPlaylist state)
cache <- readTVar (stImgCache state)
idx <- readTVar (stIdx state)
let paths = take sz (sortBy (comparePaths pl idx) pl) \\ fst (unzip cache)
sz = stImgCacheSize state
if null paths then retry else return (head paths)
putImgInCache state img path = do
cache <- readTVar (stImgCache state)
pl <- readTVar (stPlaylist state)
idx <- readTVar (stIdx state)
let cache' = (path,img) : filter ((path /=) . fst) cache
scache = sortBy (comparePaths pl idx `on` fst) cache'
writeTVar (stImgCache state) (take (stImgCacheSize state) scache)
return path
comparePaths playlist idx p1 p2 = case (i1,i2) of
(Nothing, Nothing) -> EQ
(Nothing, Just _) -> GT
(Just _, Nothing) -> LT
(Just n1, Just n2) | idx `elem` [n1,n2] -> comparing abs (n1-idx) (n2-idx)
(Just n1, Just n2) -> comparing abs (n1-idx-1) (n2-idx-1)
where
i1 = elemIndex p1 playlist
i2 = elemIndex p2 playlist
parseCmd :: State -> String -> STM Bool
parseCmd s@(State {stIdx = idx, stPlaylist = pl}) cmd = case words cmd of
["next"] -> gotoIdx s (+ 1)
["prev"] -> gotoIdx s (+ (-1))
["first"] -> gotoIdx s (\_ -> 0)
["last"] -> readTVar pl >>= (\p -> gotoIdx s (\_ -> length p - 1))
"playlist":"add":imgs -> do
fmap (++ imgs) (readTVar pl) >>= writeTVar pl
return True
"playlist":"replace":imgs -> do
writeTVar idx 0
writeTVar pl imgs
return True
"playlist":"insert":"0":imgs -> do
p <- readTVar pl
fmap (+ length p) (readTVar idx) >>= writeTVar idx
writeTVar pl (imgs++p)
return True
_ -> return False
gotoIdx (State {stIdx = idx, stPlaylist = pl}) f = do
i <- readTVar idx
let i' = f i
l <- fmap length (readTVar pl)
when (i' /= i && i' >= 0 && i' < l) (writeTVar idx i')
return (i' /= i)