{-# LANGUAGE UnicodeSyntax, LambdaCase, FlexibleContexts #-} import Control.Monad import Control.Monad.IO.Class import Control.Monad.State.Strict import Control.Lens import Data.List (isPrefixOf) import System.Environment import System.Exit import System.Process import Data.List (intersperse) import Text.Read (readMaybe) import GHC.Exts (sortWith) import qualified DMenu runProc :: MonadIO m => String → [String] → String → m (Either String String) runProc prog args sIn = liftIO $ do (exitCode, sOut, sErr) ← readCreateProcessWithExitCode (proc prog args) sIn pure $ case exitCode of ExitSuccess → Right sOut ExitFailure _ → Left sErr runProcOr :: MonadIO m => String → [String] → String → String → m String runProcOr prog args sIn sDef = either (const sDef) id <$> runProc prog args sIn data ProcInfo = ProcInfo { piUser :: String , piPid :: Integer , piCpuUsage :: Double , piMemoryUsage :: Double , piVSZ :: Integer , piRSS :: Integer , piTTY :: Maybe String , piStat :: String , piStart :: String , piTime :: String , piCommand :: String } readProcInfo :: String → Maybe ProcInfo readProcInfo s = case words s of user:pid:cpu:mem:vsz:rss:tty:stat:start:time:cmdWords | Just pid' ← readMaybe pid , Just cpu' ← readMaybe cpu , Just mem' ← readMaybe mem , Just vsz' ← readMaybe vsz , Just rss' ← readMaybe rss → let tty' | tty == "?" = Nothing | otherwise = Just tty in Just $ ProcInfo user pid' cpu' mem' vsz' rss' tty' stat start time (take 100 $ unwords cmdWords) -- FIXME: unwords . words loses whitespaces of command _ → Nothing showProcInfos :: [ProcInfo] → [String] showProcInfos pis = map f pairs where users = fillWithSP $ map piUser pis pids = fillWithSPR $ map (show . piPid) pis cpus = fillWithSPR $ map (show . piCpuUsage) pis mems = fillWithSPR $ map (show . piMemoryUsage) pis cmds = fillWithSP $ map piCommand pis pairs = zip users $ zip pids $ zip cpus $ zip mems cmds f (user,(pid,(cpu,(mem,cmd)))) = concat $ intersperse " " [ pid, user, cpu ++ "% CPU", mem ++ "% MEM", cmd ] getProcs :: MonadIO m => m [ProcInfo] getProcs = do sOut ← runProcOr "ps" ["aux"] "" "" fmap concat $ forM (drop 1 $ lines sOut) $ \l → do case readProcInfo l of Nothing → pure [] Just pi' → pure [pi'] fillWithSP :: [String] → [String] fillWithSP ss = map f ss where f s = s ++ replicate (maxLength - length s) ' ' maxLength = maximum (map length ss) fillWithSPR :: [String] → [String] fillWithSPR ss = map f ss where f s = replicate (maxLength - length s) ' ' ++ s maxLength = maximum (map length ss) data ProcessOrder = CPU | MEM | PID -- | Parse the command line arguments readArgs :: [String] -- ^ Arguments from 'getArgs' -> IO ProcessOrder readArgs args = execStateT (go $ words $ unwords args) PID where go [] = pure () go (a:as) | a == "--" = pure () -- All arguments after "--" are passed to dmenu later. | a == "-cpu" = do put CPU; go as | a == "-mem" = do put MEM; go as | a == "-pid" = do put PID; go as | a `elem` ["-h", "--help"] = liftIO $ do putStrLn usage; exitFailure | a == "" = go as go _ = liftIO $ do putStrLn usage; exitFailure main :: IO () main = do order ← readArgs =<< getArgs sortProcs ← case order of CPU → pure $ reverse . sortWith piCpuUsage MEM → pure $ reverse . sortWith piMemoryUsage PID → pure id procs ← sortProcs <$> getProcs let procItems = zip (map piPid procs) (showProcInfos procs) let cfg = do DMenu.prompt .= "kill -9" DMenu.forwardExtraArgs DMenu.selectWith cfg snd procItems >>= \case Right (pid,_) → callCommand $ "kill -9 " ++ show pid _ → pure () usage :: String usage = unlines [ "USAGE" , " dmenu-pkill [OPTIONS] [-- DMENUOPTIONS]" , "" , " Get current processes with `ps aux`, optionally sort them by CPU or RAM" , " usage, and ask via dmenu to kill one of the processes via `kill -9 `." , "" , " All arguments, after the first `--` argument, are directly passed to dmenu." , "" , "OPTIONS" , " -cpu" , " Sort process list by CPU usage." , " -mem" , " Sort process list by memory usage." , " -pid" , " Sort process list by pid. (default)" , " -h, --help" , " Display this message." ]