-- boilerplate {{{ {-# LANGUAGE CPP #-} module Main where import Catch (catch) import Control.Arrow (second) import Control.Concurrent (newEmptyMVar, takeMVar, putMVar, forkIO) import Control.Monad (filterM, forM) import Data.Map (empty, intersection, union) import Data.Time (getCurrentTime) import Prelude hiding (catch) import System.Directory (getDirectoryContents) import System.Environment (getArgs, getEnv) import System.Exit (ExitCode(ExitSuccess, ExitFailure), exitWith) import System.FilePath (()) import System.IO (hClose, hGetContents, hPutStr, stderr, stdout) import System.Posix.Files (fileAccess, getFileStatus, isDirectory) import System.Process (runInteractiveProcess, waitForProcess) import Version (CurrentFormat, versionYeganesh) import Yeganesh (Commands, Options, addEntries, deprecate, dmenuOpts, executables, inFileName, parseInput, parseOptions, parsePath, profile, prune, readPossiblyNonExistent, showPriority, stripNewline, updatePriority, writeProfile) import qualified System.IO.Strict as Strict (getContents) -- }}} -- IO stuff {{{ catchList :: IO [a] -> IO [a] catchList = flip catch (const . return $ []) -- }}} -- shell stuff {{{ dmenu :: [String] -> CurrentFormat -> IO (ExitCode, CurrentFormat) #ifdef profiling -- when profiling, it's convenient to skip the call out to dmenu dmenu opts cv = return (ExitSuccess, cv) #endif dmenu opts cv@(_, cmds) = do (hIn, hOut, hErr, p) <- runInteractiveProcess "dmenu" opts Nothing Nothing hPutStr hIn (showPriority cmds) hClose hIn o <- hGetContents hOut e <- hGetContents hErr c <- waitForProcess p hPutStr stdout o hPutStr stderr e cv' <- updateState c o cv return (c, cv') updateState :: ExitCode -> String -> CurrentFormat -> IO CurrentFormat updateState (ExitFailure {}) _ (t, cmds) = return (t, cmds) updateState ExitSuccess cmd (t, cmds) = do now <- getCurrentTime return (now, updatePriority (stripNewline cmd) t now cmds) lsx :: Bool -> IO (IO [String]) lsx False = return (return []) lsx True = do mvar <- newEmptyMVar _ <- forkIO $ do path <- catchList $ getEnv "PATH" execs <- catchList . forM (parsePath path) $ \s -> catchList (getDirectoryContents s) >>= filterM (executable . (s )) putMVar mvar (concat execs) return (takeMVar mvar) where executable file = flip catch (\_ -> return False) $ do status <- getFileStatus file case isDirectory status of True -> return False False -> fileAccess file True False True -- TODO: do people prefer to see files which are executable by -- *someone*, even if not by the current user? ask brisbin on -- Freenode, who is to date the only person to request this feature parseStdin :: Bool -> IO Commands parseStdin False = fmap parseInput Strict.getContents parseStdin True = return empty runWithOptions :: Options -> IO () runWithOptions opts = do future <- lsx (executables opts) inFile <- inFileName (profile opts) cached <- readPossiblyNonExistent inFile new <- parseStdin (executables opts) (code, updated) <- dmenu (dmenuOpts opts) (second (`combine` new) cached) execs <- future writeProfile opts (addEntries execs updated) deprecate inFile (profile opts) exitWith code where combine = if prune opts then \old -> union old >>= intersection else union -- }}} introText :: String introText = unlines $ [ versionYeganesh, "Usage: yeganesh [OPTIONS] [[--] DMENU_OPTIONS]", "OPTIONS are described below, and DMENU_OPTIONS are passed on verbatim to dmenu.", "Profiles are stored in the XDG data home for yeganesh."] main :: IO () main = getArgs >>= either putStr runWithOptions . parseOptions introText versionYeganesh