From 77df1ac30fa7af5948f7ff64f5fee9aed64552b3 Mon Sep 17 00:00:00 2001 From: jao Date: Sun, 25 Nov 2018 15:10:29 +0000 Subject: Back to app/src, since it seems they're the default convention for stack --- src/Xmobar/Plugins/Monitors/Alsa.hs | 146 ++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/Xmobar/Plugins/Monitors/Alsa.hs (limited to 'src/Xmobar/Plugins/Monitors/Alsa.hs') diff --git a/src/Xmobar/Plugins/Monitors/Alsa.hs b/src/Xmobar/Plugins/Monitors/Alsa.hs new file mode 100644 index 0000000..21a2786 --- /dev/null +++ b/src/Xmobar/Plugins/Monitors/Alsa.hs @@ -0,0 +1,146 @@ +----------------------------------------------------------------------------- +-- | +-- Module : Plugins.Monitors.Alsa +-- Copyright : (c) 2018 Daniel Schüssler +-- License : BSD-style (see LICENSE) +-- +-- Maintainer : Jose A. Ortega Ruiz +-- Stability : unstable +-- Portability : unportable +-- +-- Event-based variant of the Volume plugin. +-- +----------------------------------------------------------------------------- + +module Xmobar.Plugins.Monitors.Alsa + ( startAlsaPlugin + , withMonitorWaiter + , parseOptsIncludingMonitorArgs + , AlsaOpts(aoAlsaCtlPath) + ) where + +import Control.Concurrent +import Control.Concurrent.Async +import Control.Exception +import Control.Monad +import Xmobar.Plugins.Monitors.Common +import qualified Xmobar.Plugins.Monitors.Volume as Volume; +import System.Console.GetOpt +import System.Directory +import System.Exit +import System.IO +import System.Process + +data AlsaOpts = AlsaOpts + { aoVolumeOpts :: Volume.VolumeOpts + , aoAlsaCtlPath :: Maybe FilePath + } + +defaultOpts :: AlsaOpts +defaultOpts = AlsaOpts Volume.defaultOpts Nothing + +alsaCtlOptionName :: String +alsaCtlOptionName = "alsactl" + +options :: [OptDescr (AlsaOpts -> AlsaOpts)] +options = + Option "" [alsaCtlOptionName] (ReqArg (\x o -> + o { aoAlsaCtlPath = Just x }) "") "" + : fmap (fmap modifyVolumeOpts) Volume.options + where + modifyVolumeOpts f o = o { aoVolumeOpts = f (aoVolumeOpts o) } + +parseOpts :: [String] -> IO AlsaOpts +parseOpts argv = + case getOpt Permute options argv of + (o, _, []) -> return $ foldr id defaultOpts o + (_, _, errs) -> ioError . userError $ concat errs + +parseOptsIncludingMonitorArgs :: [String] -> IO AlsaOpts +parseOptsIncludingMonitorArgs args = + -- Drop generic Monitor args first + case getOpt Permute [] args of + (_, args', _) -> parseOpts args' + +startAlsaPlugin :: String -> String -> [String] -> (String -> IO ()) -> IO () +startAlsaPlugin mixerName controlName args cb = do + opts <- parseOptsIncludingMonitorArgs args + + let run args2 = do + -- Replicating the reparsing logic used by other plugins for now, + -- but it seems the option parsing could be floated out (actually, + -- GHC could in principle do it already since getOpt is pure, but + -- it would have to inline 'runMBD', 'doArgs' and 'parseOpts' to see + -- it, which probably isn't going to happen with the default + -- optimization settings). + opts2 <- io $ parseOpts args2 + Volume.runVolumeWith (aoVolumeOpts opts2) mixerName controlName + + withMonitorWaiter mixerName (aoAlsaCtlPath opts) $ \wait_ -> + runMB args Volume.volumeConfig run wait_ cb + +withMonitorWaiter :: String -> Maybe FilePath -> (IO () -> IO a) -> IO a +withMonitorWaiter mixerName alsaCtlPath cont = do + mvar <- newMVar () + + path <- determineAlsaCtlPath + + bracket (async $ readerThread mvar path) cancel $ \a -> do + + -- Throw on this thread if there's an exception + -- on the reader thread. + link a + + cont $ takeMVar mvar + + where + + readerThread mvar path = + let createProc = (proc "stdbuf" ["-oL", path, "monitor", mixerName]) + {std_out = CreatePipe} + in + withCreateProcess createProc $ \_ (Just alsaOut) _ _ -> do + hSetBuffering alsaOut LineBuffering + + forever $ do + c <- hGetChar alsaOut + when (c == '\n') $ + -- This uses 'tryPutMVar' because 'putMVar' would make 'runVolume' run + -- once for each event. But we want it to run only once after a burst + -- of events. + void $ tryPutMVar mvar () + + defaultPath = "/usr/sbin/alsactl" + + determineAlsaCtlPath = + case alsaCtlPath of + Just path -> do + found <- doesFileExist path + if found + then pure path + else throwIO . ErrorCall $ + "Specified alsactl file " ++ path ++ " does not exist" + + Nothing -> do + (ec, path, err) <- readProcessWithExitCode "which" ["alsactl"] "" + unless (null err) $ hPutStrLn stderr err + case ec of + ExitSuccess -> pure $ trimTrailingNewline path + ExitFailure _ -> do + found <- doesFileExist defaultPath + if found + then pure defaultPath + else throwIO . ErrorCall $ + "alsactl not found in PATH or at " ++ + show defaultPath ++ + "; please specify with --" ++ + alsaCtlOptionName ++ "=/path/to/alsactl" + + +-- This is necessarily very inefficient on 'String's +trimTrailingNewline :: String -> String +trimTrailingNewline x = + case reverse x of + '\n' : '\r' : y -> reverse y + '\n' : y -> reverse y + _ -> x -- cgit v1.2.3