packages feed

taffybar-4.1.2: src/System/Taffybar/Widget/Bluetooth.hs

{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module      : System.Taffybar.Widget.Bluetooth
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- This module provides a Bluetooth status widget that displays the current
-- Bluetooth state, connected devices, and optionally battery percentages.
--
-- The widget uses the 'TChan'-based system from
-- "System.Taffybar.Information.Bluetooth" for receiving updates.
-----------------------------------------------------------------------------
module System.Taffybar.Widget.Bluetooth
  ( BluetoothWidgetConfig(..)
  , defaultBluetoothWidgetConfig
  , bluetoothIconNew
  , bluetoothIconNewWith
  , bluetoothLabelNew
  , bluetoothLabelNewWith
  , bluetoothNew
  , bluetoothNewWith
  ) where

import Control.Monad (void)
import Control.Monad.IO.Class
import Data.Default (Default(..))
import Data.List (intercalate)
import qualified Data.Text as T
import qualified GI.GLib as G
import qualified GI.Gtk as Gtk
import System.Taffybar.Context (TaffyIO)
import System.Taffybar.Information.Bluetooth
import System.Taffybar.Util (postGUIASync)
import System.Taffybar.Widget.Generic.ChannelWidget
import System.Taffybar.Widget.Util (buildIconLabelBox)
import Text.StringTemplate

-- | Configuration for the Bluetooth widget.
data BluetoothWidgetConfig = BluetoothWidgetConfig
  { -- | Format string when Bluetooth is connected.
    -- Available variables: $status$, $device_alias$, $device_battery$,
    -- $num_connections$, $controller_alias$
    bluetoothFormatConnected :: String
    -- | Format string when Bluetooth is on but not connected.
  , bluetoothFormatOn :: String
    -- | Format string when Bluetooth is off (powered down).
  , bluetoothFormatOff :: String
    -- | Format string when no Bluetooth controller is found.
  , bluetoothFormatNoController :: String
    -- | Optional tooltip format.
    -- Additional variable: $device_list$
  , bluetoothTooltipFormat :: Maybe String
    -- | Format for each device in the tooltip device list.
    -- Variables: $device_alias$, $device_battery$
  , bluetoothDeviceListFormat :: String
    -- | Icon to display when Bluetooth is connected.
  , bluetoothConnectedIcon :: T.Text
    -- | Icon to display when Bluetooth is on but not connected.
  , bluetoothOnIcon :: T.Text
    -- | Icon to display when Bluetooth is off (powered down).
  , bluetoothOffIcon :: T.Text
    -- | Icon to display when no Bluetooth controller is found.
  , bluetoothNoControllerIcon :: T.Text
  }

defaultBluetoothWidgetConfig :: BluetoothWidgetConfig
defaultBluetoothWidgetConfig = BluetoothWidgetConfig
  { bluetoothFormatConnected = "$device_alias$"
  , bluetoothFormatOn = ""
  , bluetoothFormatOff = ""
  , bluetoothFormatNoController = ""
  , bluetoothTooltipFormat = Just "Bluetooth: $status$\nController: $controller_alias$\n$device_list$"
  , bluetoothDeviceListFormat = "$device_alias$ ($device_battery$%)"
  , bluetoothConnectedIcon = T.pack "\xF293"
  , bluetoothOnIcon = T.pack "\xF293"
  , bluetoothOffIcon = T.pack "\xF294"
  , bluetoothNoControllerIcon = T.pack "\xF294"
  }

instance Default BluetoothWidgetConfig where
  def = defaultBluetoothWidgetConfig

-- | Create a Bluetooth icon widget with default configuration.
bluetoothIconNew :: TaffyIO Gtk.Widget
bluetoothIconNew = bluetoothIconNewWith defaultBluetoothWidgetConfig

-- | Create a Bluetooth icon widget with custom configuration.
-- The icon updates dynamically based on the current Bluetooth status.
bluetoothIconNewWith :: BluetoothWidgetConfig -> TaffyIO Gtk.Widget
bluetoothIconNewWith config = do
  chan <- getBluetoothInfoChan
  initialInfo <- getBluetoothInfoState
  liftIO $ do
    label <- Gtk.labelNew Nothing
    let updateIcon info = postGUIASync $ do
          let iconText = case bluetoothStatus info of
                BluetoothConnected -> bluetoothConnectedIcon config
                BluetoothOn -> bluetoothOnIcon config
                BluetoothOff -> bluetoothOffIcon config
                BluetoothNoController -> bluetoothNoControllerIcon config
          Gtk.labelSetText label iconText
          updateStyleClasses label info
    void $ Gtk.onWidgetRealize label $ updateIcon initialInfo
    Gtk.widgetShowAll label
    Gtk.toWidget =<< channelWidgetNew label chan updateIcon

-- | Create a Bluetooth label widget with default configuration.
bluetoothLabelNew :: TaffyIO Gtk.Widget
bluetoothLabelNew = bluetoothLabelNewWith defaultBluetoothWidgetConfig

-- | Create a Bluetooth label widget with custom configuration.
-- The label shows text information (device alias, status, etc.) but no icon.
bluetoothLabelNewWith :: BluetoothWidgetConfig -> TaffyIO Gtk.Widget
bluetoothLabelNewWith config = do
  chan <- getBluetoothInfoChan
  initialInfo <- getBluetoothInfoState

  liftIO $ do
    label <- Gtk.labelNew Nothing

    let updateLabel info = do
          (labelText, tooltipText) <- formatBluetoothWidget config info
          postGUIASync $ do
            Gtk.labelSetMarkup label labelText
            Gtk.widgetSetTooltipMarkup label tooltipText
            -- Add CSS classes based on status
            updateStyleClasses label info

    void $ Gtk.onWidgetRealize label $ updateLabel initialInfo

    Gtk.widgetShowAll label
    Gtk.toWidget =<< channelWidgetNew label chan updateLabel

-- | Create a combined icon+label Bluetooth widget with default configuration.
bluetoothNew :: TaffyIO Gtk.Widget
bluetoothNew = bluetoothNewWith defaultBluetoothWidgetConfig

-- | Create a combined icon+label Bluetooth widget with custom configuration.
bluetoothNewWith :: BluetoothWidgetConfig -> TaffyIO Gtk.Widget
bluetoothNewWith config = do
  iconWidget <- bluetoothIconNewWith config
  labelWidget <- bluetoothLabelNewWith config
  liftIO $ buildIconLabelBox iconWidget labelWidget

-- | Update CSS classes on the widget based on Bluetooth status.
updateStyleClasses :: Gtk.Label -> BluetoothInfo -> IO ()
updateStyleClasses label info = do
  styleCtx <- Gtk.widgetGetStyleContext label

  -- Remove all bluetooth status classes first
  Gtk.styleContextRemoveClass styleCtx "bluetooth-connected"
  Gtk.styleContextRemoveClass styleCtx "bluetooth-on"
  Gtk.styleContextRemoveClass styleCtx "bluetooth-off"
  Gtk.styleContextRemoveClass styleCtx "bluetooth-no-controller"

  -- Add the appropriate class
  case bluetoothStatus info of
    BluetoothConnected -> Gtk.styleContextAddClass styleCtx "bluetooth-connected"
    BluetoothOn -> Gtk.styleContextAddClass styleCtx "bluetooth-on"
    BluetoothOff -> Gtk.styleContextAddClass styleCtx "bluetooth-off"
    BluetoothNoController -> Gtk.styleContextAddClass styleCtx "bluetooth-no-controller"

-- | Format the Bluetooth widget based on current state.
formatBluetoothWidget ::
  BluetoothWidgetConfig ->
  BluetoothInfo ->
  IO (T.Text, Maybe T.Text)
formatBluetoothWidget config info = do
  attrs <- buildAttrs info
  let format = case bluetoothStatus info of
        BluetoothConnected -> bluetoothFormatConnected config
        BluetoothOn -> bluetoothFormatOn config
        BluetoothOff -> bluetoothFormatOff config
        BluetoothNoController -> bluetoothFormatNoController config
      labelText = renderTemplate format attrs
      tooltipText = fmap (`renderTemplate` attrs) (bluetoothTooltipFormat config)
  return (T.pack labelText, T.pack <$> tooltipText)

-- | Build template attributes from Bluetooth info.
buildAttrs :: BluetoothInfo -> IO [(String, String)]
buildAttrs info = do
  let statusText = case bluetoothStatus info of
        BluetoothConnected -> "connected"
        BluetoothOn -> "on"
        BluetoothOff -> "off"
        BluetoothNoController -> "no-controller"

      -- Get the first connected device for primary display
      primaryDevice = case bluetoothConnectedDevices info of
        [] -> Nothing
        (d:_) -> Just d

      deviceAliasText = maybe "" deviceAlias primaryDevice
      deviceBatteryText = maybe "?" (maybe "?" show . deviceBatteryPercentage) primaryDevice
      numConnections = length $ bluetoothConnectedDevices info
      controllerAliasText = maybe "none" controllerAlias (bluetoothController info)

      -- Build device list for tooltip
      deviceListText = intercalate "\n" $
        map formatDeviceEntry (bluetoothConnectedDevices info)

      formatDeviceEntry dev =
        let battery = maybe "?" show (deviceBatteryPercentage dev)
        in deviceAlias dev ++ " (" ++ battery ++ "%)"

  status <- escapeText $ T.pack statusText
  deviceAliasEsc <- escapeText $ T.pack deviceAliasText
  deviceBatteryEsc <- escapeText $ T.pack deviceBatteryText
  controllerAliasEsc <- escapeText $ T.pack controllerAliasText
  deviceListEsc <- escapeText $ T.pack deviceListText

  return
    [ ("status", status)
    , ("device_alias", deviceAliasEsc)
    , ("device_battery", deviceBatteryEsc)
    , ("num_connections", show numConnections)
    , ("controller_alias", controllerAliasEsc)
    , ("device_list", deviceListEsc)
    ]

-- | Render a template with the given attributes.
renderTemplate :: String -> [(String, String)] -> String
renderTemplate template attrs = render $ setManyAttrib attrs (newSTMP template)

-- | Escape text for Pango markup.
escapeText :: T.Text -> IO String
escapeText input = T.unpack <$> G.markupEscapeText input (-1)