Files
dotfiles/dotfiles/config/taffybar/taffybar.hs

409 lines
14 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Main (main) where
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Int (Int32)
import Data.List (nub)
import qualified Data.Map as M
import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified GI.GdkPixbuf.Objects.Pixbuf as Gdk
import qualified GI.Gtk as Gtk
import Network.HostName (getHostName)
import System.Environment.XDG.BaseDir (getUserConfigFile)
import System.Log.Logger (Priority(WARNING), rootLoggerName, setLevel, updateGlobalLogger)
import System.Taffybar (startTaffybar)
import System.Taffybar.Context (Backend (BackendWayland, BackendX11), TaffyIO, detectBackend)
import System.Taffybar.DBus
import System.Taffybar.DBus.Toggle
import System.Taffybar.Hooks (withLogLevels)
import System.Taffybar.Information.EWMHDesktopInfo (WorkspaceId (..))
import System.Taffybar.Information.X11DesktopInfo
import System.Taffybar.SimpleConfig
import System.Taffybar.Util (getPixbufFromFilePath, (<|||>), maybeTCombine)
import System.Taffybar.Widget
import qualified System.Taffybar.Widget.HyprlandWorkspaces as Hyprland
import qualified System.Taffybar.Widget.NetworkManager as NetworkManager
import System.Taffybar.Widget.SNIMenu (withNmAppletMenu)
import qualified System.Taffybar.Widget.ASUS as ASUS
import qualified System.Taffybar.Widget.PulseAudio as PulseAudio
import qualified System.Taffybar.Widget.ScreenLock as ScreenLock
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
import Data.Ratio ((%))
import System.Taffybar.Widget.SNITray
( sniTrayNewFromParams
, sniTrayThatStartsWatcherEvenThoughThisIsABadWayToDoIt
)
import qualified StatusNotifier.Tray as SNITray (MenuBackend (HaskellDBusMenu), defaultTrayParams, trayMenuBackend, trayOverlayScale)
import System.Taffybar.Widget.Util (buildContentsBox, buildIconLabelBox, loadPixbufByName, widgetSetClassGI)
import qualified System.Taffybar.Widget.Workspaces as X11Workspaces
import System.Taffybar.WindowIcon (pixBufFromColor)
-- | Wrap the widget in a "TaffyBox" (via 'buildContentsBox') and add a CSS class.
decorateWithClassAndBox :: MonadIO m => Text -> Gtk.Widget -> m Gtk.Widget
decorateWithClassAndBox klass widget = do
boxed <- buildContentsBox widget
widgetSetClassGI boxed klass
decorateWithClassAndBoxM :: MonadIO m => Text -> m Gtk.Widget -> m Gtk.Widget
decorateWithClassAndBoxM klass builder =
builder >>= decorateWithClassAndBox klass
-- ** X11 Workspaces
x11FullWorkspaceNames :: X11Property [(WorkspaceId, String)]
x11FullWorkspaceNames =
go <$> readAsListOfString Nothing "_NET_DESKTOP_FULL_NAMES"
where
go = zip [WorkspaceId i | i <- [0 ..]]
x11WorkspaceLabelSetter :: X11Workspaces.Workspace -> X11Workspaces.WorkspacesIO String
x11WorkspaceLabelSetter workspace =
remapNSP . fromMaybe "" . lookup (X11Workspaces.workspaceIdx workspace) <$>
liftX11Def [] x11FullWorkspaceNames
where
remapNSP "NSP" = "S"
remapNSP n = n
-- ** Logging
-- ** Hyprland Icon Finding
iconRemap :: [(Text, [Text])]
iconRemap =
[ ("spotify", ["spotify-client", "spotify"])
]
iconRemapMap :: M.Map Text [Text]
iconRemapMap =
M.fromList [ (T.toLower k, v) | (k, v) <- iconRemap ]
lookupIconRemap :: Text -> [Text]
lookupIconRemap name = fromMaybe [] $ M.lookup (T.toLower name) iconRemapMap
iconNameVariants :: Text -> [Text]
iconNameVariants raw =
let lower = T.toLower raw
stripped = fromMaybe lower (T.stripSuffix ".desktop" lower)
suffixes = ["-gtk", "-client", "-desktop"]
stripSuffixes name =
let variants = mapMaybe (`T.stripSuffix` name) suffixes
in nub $ variants ++ [name]
baseNames = stripSuffixes stripped ++ [raw]
toDash c
| c == ' ' || c == '_' || c == '.' || c == '/' = '-'
| otherwise = c
toUnderscore c
| c == ' ' || c == '-' || c == '.' || c == '/' = '_'
| otherwise = c
variantsFor name =
let dotted =
case T.splitOn "." name of
[] -> name
xs -> last xs
dashed = T.map toDash name
dashedDotted = T.map toDash dotted
underscored = T.map toUnderscore name
underscoredDotted = T.map toUnderscore dotted
in [dotted, dashed, dashedDotted, underscored, underscoredDotted, name]
in nub $ concatMap variantsFor baseNames
-- Hyprland "special" workspaces (e.g. "special:slack") are scratchpad-like and
-- usually not something we want visible in the workspace widget.
isSpecialHyprWorkspace :: Hyprland.HyprlandWorkspace -> Bool
isSpecialHyprWorkspace ws =
let name = T.toLower $ T.pack $ Hyprland.workspaceName ws
in T.isPrefixOf "special" name || Hyprland.workspaceIdx ws < 0
hyprlandIconCandidates :: Hyprland.HyprlandWindow -> [Text]
hyprlandIconCandidates windowData =
let baseNames = map T.pack $ catMaybes
[ Hyprland.windowClass windowData
, Hyprland.windowInitialClass windowData
]
remapped = concatMap lookupIconRemap baseNames
remappedExpanded = concatMap iconNameVariants remapped
baseExpanded = concatMap iconNameVariants baseNames
in nub (remappedExpanded ++ baseExpanded)
isPathCandidate :: Text -> Bool
isPathCandidate name =
T.isInfixOf "/" name ||
any (`T.isSuffixOf` name) [".png", ".svg", ".xpm"]
hyprlandIconFromCandidate :: Int32 -> Text -> TaffyIO (Maybe Gdk.Pixbuf)
hyprlandIconFromCandidate size name
| isPathCandidate name =
liftIO $ getPixbufFromFilePath (T.unpack name)
| otherwise =
maybeTCombine
(Hyprland.getWindowIconFromDesktopEntryByAppId size (T.unpack name))
(liftIO $ loadPixbufByName size name)
hyprlandManualIconGetter :: Hyprland.HyprlandWindowIconPixbufGetter
hyprlandManualIconGetter =
Hyprland.handleIconGetterException $ \size windowData ->
foldl maybeTCombine (return Nothing) $
map (hyprlandIconFromCandidate size) (hyprlandIconCandidates windowData)
fallbackIconPixbuf :: Int32 -> TaffyIO (Maybe Gdk.Pixbuf)
fallbackIconPixbuf size = do
let fallbackNames =
[ "application-x-executable"
, "application"
, "image-missing"
, "gtk-missing-image"
, "dialog-question"
, "utilities-terminal"
, "system-run"
, "window"
]
tryNames =
foldl
maybeTCombine
(return Nothing)
(map (liftIO . loadPixbufByName size) fallbackNames)
result <- tryNames
case result of
Just _ -> return result
Nothing -> Just <$> pixBufFromColor size 0x5f5f5fff
hyprlandFallbackIcon :: Hyprland.HyprlandWindowIconPixbufGetter
hyprlandFallbackIcon size _ =
fallbackIconPixbuf size
-- ** Host Overrides
defaultCssFiles :: [FilePath]
defaultCssFiles = ["palette.css", "taffybar.css"]
cssFilesByHostname :: [(String, [FilePath])]
cssFilesByHostname =
[ ("imalison-home", ["palette.css", "taffybar.css"])
, ("ryzen-shine", ["palette.css", "taffybar.css"])
, ("stevie-nixos", ["palette.css", "taffybar.css"])
]
laptopHosts :: [String]
laptopHosts =
[ "adell"
, "stevie-nixos"
, "strixi-minaj"
, "jay-lenovo"
]
cssFilesForHost :: String -> [FilePath]
cssFilesForHost hostName =
fromMaybe defaultCssFiles $ lookup hostName cssFilesByHostname
-- ** Widgets
audioWidget :: TaffyIO Gtk.Widget
audioWidget =
decorateWithClassAndBoxM "audio" PulseAudio.pulseAudioNew
networkWidget :: TaffyIO Gtk.Widget
networkWidget =
decorateWithClassAndBoxM "network" $
withNmAppletMenu NetworkManager.networkManagerWifiIconLabelNew
layoutWidget :: TaffyIO Gtk.Widget
layoutWidget =
decorateWithClassAndBoxM "layout" (layoutNew defaultLayoutConfig)
windowsWidget :: TaffyIO Gtk.Widget
windowsWidget =
decorateWithClassAndBoxM "windows" (windowsNew defaultWindowsConfig)
x11WorkspacesWidget :: TaffyIO Gtk.Widget
x11WorkspacesWidget =
flip widgetSetClassGI "workspaces" =<<
X11Workspaces.workspacesNew
X11Workspaces.defaultWorkspacesConfig
{ X11Workspaces.minIcons = 1
, X11Workspaces.getWindowIconPixbuf =
X11Workspaces.scaledWindowIconPixbufGetter $
X11Workspaces.getWindowIconPixbufFromChrome <|||>
X11Workspaces.unscaledDefaultGetWindowIconPixbuf <|||>
(\size _ -> fallbackIconPixbuf size)
, X11Workspaces.widgetGap = 0
, X11Workspaces.showWorkspaceFn = X11Workspaces.hideEmpty
, X11Workspaces.updateRateLimitMicroseconds = 100000
, X11Workspaces.labelSetter = x11WorkspaceLabelSetter
, X11Workspaces.widgetBuilder = X11Workspaces.buildLabelOverlayController
}
-- | Like 'buildWorkspaceIconLabelOverlay' but lets you choose the corner.
buildAlignedOverlay ::
Gtk.Align -> Gtk.Align -> Gtk.Widget -> Gtk.Widget -> TaffyIO Gtk.Widget
buildAlignedOverlay halign valign iconsWidget labelWidget = liftIO $ do
base <- buildContentsBox iconsWidget
ebox <- Gtk.eventBoxNew
_ <- widgetSetClassGI ebox "overlay-box"
Gtk.widgetSetHalign ebox halign
Gtk.widgetSetValign ebox valign
Gtk.containerAdd ebox labelWidget
overlayLabel <- Gtk.toWidget ebox
overlay <- Gtk.overlayNew
baseW <- Gtk.toWidget base
Gtk.containerAdd overlay baseW
Gtk.overlayAddOverlay overlay overlayLabel
Gtk.overlaySetOverlayPassThrough overlay overlayLabel True
Gtk.toWidget overlay
hyprlandWorkspacesWidget :: TaffyIO Gtk.Widget
hyprlandWorkspacesWidget =
flip widgetSetClassGI "workspaces" =<<
Hyprland.hyprlandWorkspacesNew cfg
where
cfg = Hyprland.defaultHyprlandWorkspacesConfig
{ Hyprland.widgetGap = 0
, Hyprland.minIcons = 1
, Hyprland.widgetBuilder =
Hyprland.hyprlandBuildButtonController cfg
(Hyprland.hyprlandBuildCustomOverlayController
(buildAlignedOverlay Gtk.AlignStart Gtk.AlignEnd)
cfg)
-- Don't show Hyprland "special:*" workspaces.
, Hyprland.showWorkspaceFn =
\ws ->
Hyprland.workspaceState ws /= X11Workspaces.Empty &&
not (isSpecialHyprWorkspace ws)
, Hyprland.getWindowIconPixbuf =
hyprlandManualIconGetter <|||>
Hyprland.defaultHyprlandGetWindowIconPixbuf <|||>
hyprlandFallbackIcon
}
clockWidget :: TaffyIO Gtk.Widget
clockWidget =
decorateWithClassAndBoxM
"clock"
( textClockNewWith
defaultClockConfig
{ clockUpdateStrategy = RoundedTargetInterval 60 0.0
, clockFormatString = "%a %b %_d, 🕑%I:%M %p"
}
)
mprisWidget :: TaffyIO Gtk.Widget
mprisWidget =
mpris2NewWithConfig
MPRIS2Config
{ mprisWidgetWrapper = decorateWithClassAndBox "mpris"
, updatePlayerWidget =
simplePlayerWidget
defaultPlayerConfig
{ setNowPlayingLabel = playingText 20 20
}
}
batteryWidget :: TaffyIO Gtk.Widget
batteryWidget = do
iconWidget <- batteryTextIconNew
labelWidget <- textBatteryNew "$percentage$%"
decorateWithClassAndBox "battery" =<< liftIO (buildIconLabelBox iconWidget labelWidget)
backlightWidget :: TaffyIO Gtk.Widget
backlightWidget =
decorateWithClassAndBoxM
"backlight"
( backlightLabelNewChanWith
defaultBacklightWidgetConfig
{ backlightFormat = "☀ $percent$%"
, backlightUnknownFormat = "☀ n/a"
, backlightTooltipFormat =
Just "Device: $device$\nBrightness: $brightness$/$max$ ($percent$%)"
}
)
diskUsageWidget :: TaffyIO Gtk.Widget
diskUsageWidget =
decorateWithClassAndBoxM "disk-usage" diskUsageNew
asusWidget :: TaffyIO Gtk.Widget
asusWidget =
decorateWithClassAndBoxM "asus-profile" ASUS.asusWidgetNew
screenLockWidget :: TaffyIO Gtk.Widget
screenLockWidget =
decorateWithClassAndBoxM "screen-lock" $
ScreenLock.screenLockNewWithConfig
ScreenLock.defaultScreenLockConfig
{ ScreenLock.screenLockIcon = T.pack "\xF023" <> " Lock" }
wlsunsetWidget :: TaffyIO Gtk.Widget
wlsunsetWidget =
decorateWithClassAndBoxM "wlsunset" $
Wlsunset.wlsunsetNewWithConfig
Wlsunset.defaultWlsunsetWidgetConfig
{ Wlsunset.wlsunsetWidgetIcon = T.pack "\xF0599" <> " Sun" }
sniTrayWidget :: TaffyIO Gtk.Widget
sniTrayWidget =
decorateWithClassAndBoxM
"sni-tray"
(sniTrayNewFromParams (SNITray.defaultTrayParams { SNITray.trayMenuBackend = SNITray.HaskellDBusMenu, SNITray.trayOverlayScale = 1 % 3 }))
-- ** Layout
startWidgetsForBackend :: Backend -> [TaffyIO Gtk.Widget]
startWidgetsForBackend backend =
case backend of
BackendX11 -> [x11WorkspacesWidget, layoutWidget, windowsWidget]
-- These Wayland widgets are Hyprland-specific.
BackendWayland -> [hyprlandWorkspacesWidget]
endWidgetsForHost :: String -> Backend -> [TaffyIO Gtk.Widget]
endWidgetsForHost hostName backend =
let baseEndWidgets = [audioWidget, diskUsageWidget, networkWidget, screenLockWidget, wlsunsetWidget, mprisWidget, sniTrayWidget]
laptopEndWidgets =
[ batteryWidget
, sniTrayWidget
, asusWidget
, audioWidget
, diskUsageWidget
, backlightWidget
, networkWidget
, screenLockWidget
, wlsunsetWidget
, mprisWidget
]
in if hostName `elem` laptopHosts
then laptopEndWidgets
else baseEndWidgets
mkSimpleTaffyConfig :: String -> Backend -> [FilePath] -> SimpleTaffyConfig
mkSimpleTaffyConfig hostName backend cssFiles =
defaultSimpleTaffyConfig
{ startWidgets = startWidgetsForBackend backend
, endWidgets = endWidgetsForHost hostName backend
, barPosition = Top
, widgetSpacing = 0
, barPadding = 4
, barHeight = ScreenRatio $ 1 / 33
, cssPaths = cssFiles
, centerWidgets = [clockWidget]
}
-- ** Entry Point
main :: IO ()
main = do
updateGlobalLogger rootLoggerName (setLevel WARNING)
hostName <- getHostName
backend <- detectBackend
cssFiles <- mapM (getUserConfigFile "taffybar") (cssFilesForHost hostName)
let simpleTaffyConfig = mkSimpleTaffyConfig hostName backend cssFiles
startTaffybar $
withLogServer $
withLogLevels $
withToggleServer $
toTaffybarConfig simpleTaffyConfig