packages feed

taffybar-5.2.0: 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.GLib as G
import qualified GI.Gdk.Enums as Gdk
import qualified GI.Gdk.Structs.EventScroll as GdkEvent
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, widgetSetClassGI)
import Text.StringTemplate

-- | Configuration for PulseAudio widgets.
data PulseAudioWidgetConfig = PulseAudioWidgetConfig
  { pulseAudioSink :: String,
    pulseAudioFormat :: String,
    pulseAudioMuteFormat :: String,
    pulseAudioUnknownFormat :: String,
    pulseAudioTooltipFormat :: Maybe String,
    pulseAudioScrollStepPercent :: Maybe Int,
    pulseAudioToggleMuteOnClick :: Bool
  }

-- | Default PulseAudio widget configuration.
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

-- | Create an icon-only PulseAudio widget with default config.
pulseAudioIconNew :: TaffyIO Gtk.Widget
pulseAudioIconNew = pulseAudioIconNewWith defaultPulseAudioWidgetConfig

-- | Create an icon-only PulseAudio widget with custom config.
pulseAudioIconNewWith :: PulseAudioWidgetConfig -> TaffyIO Gtk.Widget
pulseAudioIconNewWith config = do
  let sinkSpec = pulseAudioSink config
  (chan, var) <- getPulseAudioInfoChanAndVar sinkSpec
  liftIO $ do
    label <- Gtk.labelNew Nothing
    _ <- widgetSetClassGI label "pulse-audio-icon"
    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)
      >>= (`widgetSetClassGI` "pulse-audio-icon")

-- | Create a label-only PulseAudio widget with default config.
pulseAudioLabelNew :: TaffyIO Gtk.Widget
pulseAudioLabelNew = pulseAudioLabelNewWith defaultPulseAudioWidgetConfig

-- | Create a label-only PulseAudio widget with custom config.
pulseAudioLabelNewWith :: PulseAudioWidgetConfig -> TaffyIO Gtk.Widget
pulseAudioLabelNewWith config = do
  let sinkSpec = pulseAudioSink config
  (chan, var) <- getPulseAudioInfoChanAndVar sinkSpec

  liftIO $ do
    label <- Gtk.labelNew Nothing
    _ <- widgetSetClassGI label "pulse-audio-label"

    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)
      >>= (`widgetSetClassGI` "pulse-audio-label")

-- | Create a combined icon+label PulseAudio widget with default config.
pulseAudioNew :: TaffyIO Gtk.Widget
pulseAudioNew = pulseAudioNewWith defaultPulseAudioWidgetConfig

-- | Create a combined icon+label PulseAudio widget with custom config.
pulseAudioNewWith :: PulseAudioWidgetConfig -> TaffyIO Gtk.Widget
pulseAudioNewWith config = do
  iconWidget <- pulseAudioIconNewWith config
  labelWidget <- pulseAudioLabelNewWith config
  liftIO $
    buildIconLabelBox iconWidget labelWidget
      >>= (`widgetSetClassGI` "pulse-audio")

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)