taffybar-4.1.2: src/System/Taffybar/Widget/PulseAudio.hs
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module : System.Taffybar.Widget.PulseAudio
-- Copyright : (c) Ivan A. Malison
-- License : BSD3-style (see LICENSE)
--
-- Maintainer : Ivan A. Malison
-- Stability : unstable
-- Portability : unportable
--
-- Simple volume widget using PulseAudio's DBus interface.
--
-- Note: PulseAudio's DBus socket is not always enabled by default.
-- If the widget shows "vol: n/a", ensure the PulseAudio server has loaded
-- @module-dbus-protocol@ (so @/run/user/$UID/pulse/dbus-socket@ exists).
-----------------------------------------------------------------------------
module System.Taffybar.Widget.PulseAudio
( PulseAudioWidgetConfig(..)
, defaultPulseAudioWidgetConfig
, pulseAudioIconNew
, pulseAudioIconNewWith
, pulseAudioLabelNew
, pulseAudioLabelNewWith
, pulseAudioNew
, pulseAudioNewWith
) where
import Control.Concurrent.MVar (readMVar)
import Control.Monad (void, when)
import Control.Monad.IO.Class
import Data.Default (Default(..))
import qualified Data.Text as T
import qualified GI.Gdk.Enums as Gdk
import qualified GI.Gdk.Structs.EventScroll as GdkEvent
import qualified GI.GLib as G
import qualified GI.Gtk as Gtk
import System.Taffybar.Context (TaffyIO)
import System.Taffybar.Information.PulseAudio
import System.Taffybar.Util (postGUIASync)
import System.Taffybar.Widget.Generic.ChannelWidget
import System.Taffybar.Widget.Util (buildIconLabelBox)
import Text.StringTemplate
data PulseAudioWidgetConfig = PulseAudioWidgetConfig
{ pulseAudioSink :: String
, pulseAudioFormat :: String
, pulseAudioMuteFormat :: String
, pulseAudioUnknownFormat :: String
, pulseAudioTooltipFormat :: Maybe String
, pulseAudioScrollStepPercent :: Maybe Int
, pulseAudioToggleMuteOnClick :: Bool
}
defaultPulseAudioWidgetConfig :: PulseAudioWidgetConfig
defaultPulseAudioWidgetConfig =
PulseAudioWidgetConfig
{ pulseAudioSink = "@DEFAULT_SINK@"
, pulseAudioFormat = "$volume$%"
, pulseAudioMuteFormat = "muted"
, pulseAudioUnknownFormat = "n/a"
, pulseAudioTooltipFormat =
Just "Sink: $sink$\nVolume: $volume$%\nMuted: $muted$"
, pulseAudioScrollStepPercent = Just 5
, pulseAudioToggleMuteOnClick = True
}
instance Default PulseAudioWidgetConfig where
def = defaultPulseAudioWidgetConfig
pulseAudioIconNew :: TaffyIO Gtk.Widget
pulseAudioIconNew = pulseAudioIconNewWith defaultPulseAudioWidgetConfig
pulseAudioIconNewWith :: PulseAudioWidgetConfig -> TaffyIO Gtk.Widget
pulseAudioIconNewWith config = do
let sinkSpec = pulseAudioSink config
(chan, var) <- getPulseAudioInfoChanAndVar sinkSpec
liftIO $ do
label <- Gtk.labelNew Nothing
let updateIcon info = do
let iconText = case info of
Nothing -> T.pack "\xF026"
Just i -> pulseAudioTextIcon (pulseAudioMuted i) (pulseAudioVolumePercent i)
postGUIASync $ Gtk.labelSetText label iconText
void $ Gtk.onWidgetRealize label $ readMVar var >>= updateIcon
Gtk.widgetShowAll label
Gtk.toWidget =<< channelWidgetNew label chan updateIcon
pulseAudioLabelNew :: TaffyIO Gtk.Widget
pulseAudioLabelNew = pulseAudioLabelNewWith defaultPulseAudioWidgetConfig
pulseAudioLabelNewWith :: PulseAudioWidgetConfig -> TaffyIO Gtk.Widget
pulseAudioLabelNewWith config = do
let sinkSpec = pulseAudioSink config
(chan, var) <- getPulseAudioInfoChanAndVar sinkSpec
liftIO $ do
label <- Gtk.labelNew Nothing
let
updateLabel info = do
(labelText, tooltipText) <- formatPulseAudioWidget config info
postGUIASync $ do
Gtk.labelSetMarkup label labelText
Gtk.widgetSetTooltipMarkup label tooltipText
refreshNow = getPulseAudioInfo sinkSpec >>= updateLabel
whenToggleMute widget =
when (pulseAudioToggleMuteOnClick config) $
void $ Gtk.onWidgetButtonPressEvent widget $ \_ -> do
void $ togglePulseAudioMute sinkSpec
refreshNow
return True
whenScrollAdjust widget =
case pulseAudioScrollStepPercent config of
Nothing -> return ()
Just step | step <= 0 -> return ()
Just step -> do
_ <- Gtk.onWidgetScrollEvent widget $ \scrollEvent -> do
dir <- GdkEvent.getEventScrollDirection scrollEvent
let doAdjust delta = do
void $ adjustPulseAudioVolume sinkSpec delta
refreshNow
return True
case dir of
Gdk.ScrollDirectionUp -> doAdjust step
Gdk.ScrollDirectionDown -> doAdjust (-step)
Gdk.ScrollDirectionLeft -> doAdjust step
Gdk.ScrollDirectionRight -> doAdjust (-step)
_ -> return False
return ()
void $ Gtk.onWidgetRealize label $ readMVar var >>= updateLabel
whenToggleMute label
whenScrollAdjust label
Gtk.widgetShowAll label
Gtk.toWidget =<< channelWidgetNew label chan updateLabel
pulseAudioNew :: TaffyIO Gtk.Widget
pulseAudioNew = pulseAudioNewWith defaultPulseAudioWidgetConfig
pulseAudioNewWith :: PulseAudioWidgetConfig -> TaffyIO Gtk.Widget
pulseAudioNewWith config = do
iconWidget <- pulseAudioIconNewWith config
labelWidget <- pulseAudioLabelNewWith config
liftIO $ buildIconLabelBox iconWidget labelWidget
formatPulseAudioWidget
:: PulseAudioWidgetConfig
-> Maybe PulseAudioInfo
-> IO (T.Text, Maybe T.Text)
formatPulseAudioWidget config info = do
attrs <- maybe buildUnknownAttrs buildAttrs info
let
labelTemplate =
case info of
Nothing -> pulseAudioUnknownFormat config
Just audio ->
case pulseAudioMuted audio of
Just True -> pulseAudioMuteFormat config
_ -> pulseAudioFormat config
labelText = renderTemplate labelTemplate attrs
tooltipText = fmap (`renderTemplate` attrs) (pulseAudioTooltipFormat config)
return (T.pack labelText, T.pack <$> tooltipText)
buildAttrs :: PulseAudioInfo -> IO [(String, String)]
buildAttrs info = do
let
volumeText = maybe "?" show (pulseAudioVolumePercent info)
mutedText = case pulseAudioMuted info of
Just True -> "yes"
Just False -> "no"
Nothing -> "unknown"
sinkText = pulseAudioSinkName info
volume <- escapeText $ T.pack volumeText
muted <- escapeText $ T.pack mutedText
sink <- escapeText $ T.pack sinkText
return
[ ("volume", volume)
, ("muted", muted)
, ("sink", sink)
]
buildUnknownAttrs :: IO [(String, String)]
buildUnknownAttrs =
return
[ ("volume", "?")
, ("muted", "unknown")
, ("sink", "unknown")
]
pulseAudioTextIcon :: Maybe Bool -> Maybe Int -> T.Text
pulseAudioTextIcon muted volumePercent =
case muted of
Just True -> T.pack "\xF026" --
_ ->
case volumePercent of
Just v | v <= 0 -> T.pack "\xF026" --
Just v | v <= 33 -> T.pack "\xF027" --
Just _ -> T.pack "\xF028" --
Nothing -> T.pack "\xF028" --
renderTemplate :: String -> [(String, String)] -> String
renderTemplate template attrs = render $ setManyAttrib attrs (newSTMP template)
escapeText :: T.Text -> IO String
escapeText input = T.unpack <$> G.markupEscapeText input (-1)