-- | The main module of Taffybar
module System.Taffybar (
-- * Detail
--
-- | This is a system status bar meant for use with window manager
-- like XMonad. It is similar to xmobar, but with more visual flare
-- and a different widget set. Contributed widgets are more than
-- welcome. The bar is drawn using gtk and cairo. It is actually
-- the simplest possible thing that could plausibly work: you give
-- Taffybar a list of GTK widgets and it will render them in a
-- horizontal bar for you (taking care of ugly details like
-- reserving strut space so that window managers don't put windows
-- over it).
--
-- This is the real main module. The default bar should be
-- customized to taste in the config file
-- (~/.config/taffybar/taffybar.hs). Typically, this means adding
-- widgets to the default config. A default configuration file is
-- included in the distribution, but the essentials are covered
-- here.
-- * Config File
--
-- | The config file is just a Haskell source file that is compiled
-- at startup (if it has changed) to produce a custom executable
-- with the desired set of widgets. You will want to import this
-- module along with the modules of any widgets you want to add to
-- the bar. Note, you can define any widgets that you want in your
-- config file or other libraries. Taffybar only cares that you
-- give it some GTK widgets to display.
--
-- Below is a fairly typical example:
--
-- > import System.Taffybar
-- > import System.Taffybar.Systray
-- > import System.Taffybar.XMonadLog
-- > import System.Taffybar.SimpleClock
-- > import System.Taffybar.Widgets.PollingGraph
-- > import System.Information.CPU
-- >
-- > cpuCallback = do
-- > (_, systemLoad, totalLoad) <- cpuLoad
-- > return [ totalLoad, systemLoad ]
-- >
-- > main = do
-- > let cpuCfg = defaultGraphConfig { graphDataColors = [ (0, 1, 0, 1), (1, 0, 1, 0.5)]
-- > , graphLabel = Just "cpu"
-- > }
-- > clock = textClockNew Nothing "%a %b %_d %H:%M" 1
-- > log = xmonadLogNew
-- > tray = systrayNew
-- > cpu = pollingGraphNew cpuCfg 0.5 cpuCallback
-- > defaultTaffybar defaultTaffybarConfig { startWidgets = [ log ]
-- > , endWidgets = [ tray, clock, cpu ]
-- > }
--
-- This configuration creates a bar with four widgets. On the left is
-- the XMonad log. The rightmost widget is the system tray, with a
-- clock and then a CPU graph. The clock is formatted using standard
-- strftime-style format strings (see the clock module). Note that
-- the clock is colored using Pango markup (again, see the clock
-- module).
--
-- The CPU widget plots two graphs on the same widget: total CPU use
-- in green and then system CPU use in a kind of semi-transparent
-- purple on top of the green.
--
-- It is important to note that the widget lists are *not* [Widget].
-- They are actually [IO Widget] since the bar needs to construct them
-- after performing some GTK initialization.
-- * XMonad Integration (via DBus)
--
-- | The XMonadLog widget differs from its counterpart in xmobar: it
-- listens for updates over DBus instead of reading from stdin.
-- This makes it easy to restart Taffybar independently of XMonad.
-- XMonad does not come with a DBus logger, so here is an example of
-- how to make it work. Note: this requires the dbus-core (>0.9)
-- package, which is installed as a dependency of Taffybar.
--
-- > import XMonad.Hooks.DynamicLog
-- > import XMonad.Hooks.ManageDocks
-- > import DBus.Client
-- > import System.Taffybar.XMonadLog ( dbusLog )
-- >
-- > main = do
-- > client <- connectSession
-- > let pp = defaultPP
-- > xmonad $ docks defaultConfig { logHook = dbusLog client pp }
--
-- The complexity is handled in the System.Taffybar.XMonadLog module. Note
-- that the docks wrapper from ManageDocks is required to have XMonad put
-- taffybar in the strut space that it reserves. If you have problems with
-- taffybar appearing almost fullscreen, check to see if you are using this
-- wrapper. Note that the manageDocks hook that previous used to be sufficient
-- for this is no longer so (see
-- https://github.com/travitch/taffybar/issues/185).
-- ** A note about DBus:
-- |
-- * If you start xmonad using a graphical login manager like gdm or
-- kdm, DBus should be started automatically for you.
--
-- * If you start xmonad with a different graphical login manager that
-- does not start DBus for you automatically, put the line @eval
-- \`dbus-launch --auto-syntax\`@ into your ~\/.xsession *before*
-- xmonad and taffybar are started. This command sets some
-- environment variables that the two must agree on.
--
-- * If you start xmonad via @startx@ or a similar command, add the
-- above command to ~\/.xinitrc
-- * Colors
--
-- | While taffybar is based on GTK+, it ignores your GTK+ theme.
-- The default theme that it uses is in
-- @~\/.cabal\/share\/taffybar-\\/taffybar.rc@. You can
-- customize this theme by copying it to
-- @~\/.config\/taffybar\/taffybar.rc@. For an idea of the customizations you can make,
-- see .
-- * Advanced Widget Example
--
-- | The following is an example leveraging GTK+ features that are not exposed
-- by the normal Taffybar widget hooks.
--
-- > import qualified Graphics.UI.Gtk as Gtk
-- > import System.Taffybar.Widgets.PollingGraph
-- > import System.Information.CPU
-- > import XMonad.Util.Run
-- >
-- > main = do
-- > let
-- > cpuReader widget = do
-- > (userLoad, systemLoad, totalLoad) <- cpuLoad
-- > Gtk.postGUIAsync $ do
-- > let
-- > user = round $ 100 * userLoad :: Int
-- > system = round $ 100 * systemLoad :: Int
-- > tooltip = printf "%02i%% User\n%02i%% System" user system :: String
-- > _ <- Gtk.widgetSetTooltipText widget $ Just tooltip
-- > return ()
-- > return [totalLoad, systemLoad]
-- >
-- > cpuButtons = do
-- > e <- Gtk.eventButton
-- > case e of
-- > Gtk.LeftButton -> unsafeSpawn "terminator -e glances"
-- > Gtk.RightButton -> unsafeSpawn "terminator -e top"
-- > Gtk.MiddleButton -> unsafeSpawn "gnome-system-monitor"
-- > _ -> return ()
-- > return True
-- >
-- > cpuCfg = defaultGraphConfig { graphDataColors = [ (0, 1, 0, 1)
-- > , (1, 0, 1, 0.5)
-- > ]
-- > }
-- >
-- >
-- > cpu = do
-- > ebox <- Gtk.eventBoxNew
-- > btn <- pollingGraphNew cpuCfg 0.5 $ cpuReader $ Gtk.toWidget ebox
-- > Gtk.containerAdd ebox btn
-- > _ <- Gtk.on ebox Gtk.buttonPressEvent systemEvents
-- > Gtk.widgetShowAll ebox
-- > return $ Gtk.toWidget ebox
--
-- The resulting widget can be used like normal widgets, but you can use
-- different mouse buttons to run various programs and it has a useful tooltip
-- which shows the concrete numbers, which may not be clear in the graph
-- itself.
TaffybarConfig(..),
TaffybarConfigEQ,
defaultTaffybar,
defaultTaffybarConfig,
Position(..),
taffybarMain,
allMonitors,
useMonitorNumber,
realMain,
usePrimaryMonitor,
) where
import qualified Config.Dyre as Dyre
import qualified Config.Dyre.Params as Dyre
import qualified Control.Concurrent.MVar as MV
import Control.Monad ( when, foldM, void )
import Data.List
import qualified Data.Map as M
import Data.Maybe ( fromMaybe )
import Graphics.UI.Gtk as Gtk
import Graphics.UI.Gtk.General.StyleContext
import Graphics.X11.Xlib.Misc
import Safe ( atMay )
import System.Directory
import System.Environment.XDG.BaseDir ( getUserConfigFile )
import System.Exit ( exitFailure )
import System.FilePath ( (>) )
import System.Information.X11DesktopInfo
import qualified System.IO as IO
import System.Mem.StableName
import Text.Printf ( printf )
import Graphics.UI.Gtk.General.CssProvider
import Paths_taffybar ( getDataDir )
import System.Taffybar.StrutProperties
data Position = Top | Bottom
deriving (Show, Eq)
strutProperties :: Position -- ^ Bar position
-> Int -- ^ Bar height
-> Rectangle -- ^ Current monitor rectangle
-> [Rectangle] -- ^ All monitors
-> StrutProperties
strutProperties pos bh (Rectangle mX mY mW mH) monitors =
propertize pos sX sW sH
where sX = mX
sW = mW - 1
sH = case pos of Top -> bh + mY
Bottom -> bh + totalH - mY - mH
totalH = maximum $ map bottomY monitors
bottomY (Rectangle _ y _ h) = y + h
propertize p x w h = case p of
Top -> (0, 0, h, 0, 0, 0, 0, 0, x, x+w, 0, 0)
Bottom -> (0, 0, 0, h, 0, 0, 0, 0, 0, 0, x, x+w)
data TaffybarConfig =
TaffybarConfig { -- | The screen number to run the bar on (default is almost always fine)
screenNumber :: Int
-- | The xinerama/xrandr monitor number to put the bar on (default: 0)
, monitorNumber :: Int
-- | Provides a way to specify which screens taffybar should appear on.
, getMonitorConfig :: TaffybarConfigEQ -> IO (Int -> Maybe TaffybarConfigEQ)
-- | A function providing a way to call back in to taffybar to
-- refresh its configs/open closed state on each monitor.
, startRefresher :: IO () -> IO ()
-- | Number of pixels to reserve for the bar (default: 25 pixels)
, barHeight :: Int
-- | Number of additional pixels to reserve for the bar strut (default: 0)
, barPadding :: Int
-- | The position of the bar on the screen (default: Top)
, barPosition :: Position
-- | The number of pixels between widgets
, widgetSpacing :: Int
-- | Used by the application
, errorMsg :: Maybe String
-- | Widgets that are packed in order at the left end of the bar
, startWidgets :: [IO Widget]
-- | Widgets that are packed from right-to-left in the bar
, endWidgets :: [IO Widget]
}
type TaffybarConfigEQ = (TaffybarConfig, StableName TaffybarConfig)
-- | The default configuration gives an empty bar 25 pixels high on monitor 0.
defaultTaffybarConfig :: TaffybarConfig
defaultTaffybarConfig =
TaffybarConfig { screenNumber = 0
, monitorNumber = 0
, getMonitorConfig = useMonitorNumber
, startRefresher = const $ return ()
, barHeight = 25
, barPadding = 0
, barPosition = Top
, widgetSpacing = 10
, errorMsg = Nothing
, startWidgets = []
, endWidgets = []
}
useMonitorNumber :: TaffybarConfigEQ -> IO (Int -> Maybe TaffybarConfigEQ)
useMonitorNumber c@(cfg, _) = return umn
where umn mnumber
| mnumber == monitorNumber cfg = Just c
| otherwise = Nothing
-- | Use the primary monitor as set by Xrandr.
usePrimaryMonitor :: TaffybarConfigEQ -> IO (Int -> Maybe TaffybarConfigEQ)
usePrimaryMonitor c@(cfg, _) = do
maybePrimary <- withDefaultCtx getPrimaryOutputNumber
let primary = fromMaybe (monitorNumber cfg) maybePrimary
return $ \mnumber -> if mnumber == primary then Just c else Nothing
allMonitors :: TaffybarConfigEQ -> IO (Int -> Maybe TaffybarConfigEQ)
allMonitors cfg = return $ const $ Just cfg
showError :: TaffybarConfig -> String -> TaffybarConfig
showError cfg msg = cfg { errorMsg = Just msg }
-- | The default parameters need to tell GHC to compile using
-- -threaded so that the GTK event loops doesn't block all of the
-- widgets
defaultParams :: Dyre.Params TaffybarConfig
defaultParams =
Dyre.defaultParams
{ Dyre.projectName = "taffybar"
, Dyre.realMain = realMain
, Dyre.showError = showError
, Dyre.ghcOpts = ["-threaded", "-rtsopts"]
, Dyre.rtsOptsHandling = Dyre.RTSAppend ["-I0", "-V0"]
}
-- | The entry point of the application. Feed it a custom config.
defaultTaffybar :: TaffybarConfig -> IO ()
defaultTaffybar = Dyre.wrapMain defaultParams
realMain :: TaffybarConfig -> IO ()
realMain cfg =
case errorMsg cfg of
Nothing -> taffybarMain cfg
Just err -> do
IO.hPutStrLn IO.stderr ("Error: " ++ err)
exitFailure
getDefaultConfigFile :: String -> IO FilePath
getDefaultConfigFile name = do
dataDir <- getDataDir
return (dataDir > name)
-- | Given a Taffybar configuration and the Taffybar window, this
-- action sets up the window size and strut properties. May be called
-- multiple times, e.g., when the monitor resolution changes.
setTaffybarSize :: TaffybarConfig -> Window -> Int -> IO ()
setTaffybarSize cfg window monNumber = do
screen <- windowGetScreen window
nmonitors <- screenGetNMonitors screen
allMonitorSizes <-
mapM (screenGetMonitorGeometry screen) [0 .. (nmonitors - 1)]
when (monNumber >= nmonitors) $
IO.hPutStrLn IO.stderr $
printf
"Monitor %d is not available in the selected screen"
monNumber
let monitorSize =
fromMaybe (head allMonitorSizes) $
allMonitorSizes `atMay` monNumber
let Rectangle x y w h = monitorSize
strutHeight = barHeight cfg + (2 * barPadding cfg)
yoff =
case barPosition cfg of
Top -> barPadding cfg
Bottom -> h - strutHeight
windowMove window x (y + yoff)
-- Set up the window size using fixed min and max sizes. This
-- prevents the contained horizontal box from affecting the window
-- size.
windowSetGeometryHints
window
(Nothing :: Maybe Widget)
(Just (w, barHeight cfg)) -- Min size.
(Just (w, barHeight cfg)) -- Max size.
Nothing
Nothing
Nothing
let setStrutProps =
setStrutProperties window $
strutProperties
(barPosition cfg)
strutHeight
monitorSize
allMonitorSizes
winRealized <- widgetGetRealized window
if winRealized
then setStrutProps
else void $ on window realize setStrutProps
startCSS :: IO CssProvider
startCSS = do
-- Override the default GTK theme path settings. This causes the
-- bar (by design) to ignore the real GTK theme and just use the
-- provided minimal theme to set the background and text colors.
-- Users can override this default.
taffybarProvider <- cssProviderNew
let loadIfExists filePath =
doesFileExist filePath >>= flip when (cssProviderLoadFromPath taffybarProvider filePath)
loadIfExists =<< getDefaultConfigFile "taffybar.css"
loadIfExists =<< getUserConfigFile "taffybar" "taffybar.css"
Just scr <- screenGetDefault
styleContextAddProviderForScreen scr taffybarProvider 800
return taffybarProvider
taffybarMain :: TaffybarConfig -> IO ()
taffybarMain cfg = do
_ <- initThreads
_ <- initGUI
_ <- startCSS
Just disp <- displayGetDefault
nscreens <- displayGetNScreens disp
screen <- if screenNumber cfg < nscreens
then displayGetScreen disp (screenNumber cfg)
else error $ printf "Screen %d is not available in the default display"
(screenNumber cfg)
cfgEq <- makeStableName cfg
taffyWindowsVar <- MV.newMVar M.empty
let refreshTaffyWindows = do
nmonitors <- screenGetNMonitors screen
getConfig <- getMonitorConfig cfg (cfg, cfgEq)
MV.modifyMVar_ taffyWindowsVar $ \monitorToWindow ->
do
let monitors = union [0 .. (nmonitors - 1)] $ M.keys monitorToWindow
updateBarOnWindow mapToUpdate monNum
| monNum >= nmonitors = maybeDeleteWindow
| otherwise = case M.lookup monNum monitorToWindow of
Just (currentConfig, window) ->
case getConfig monNum of
Just configEq@(_, newConfigEq) ->
if currentConfig == newConfigEq
then
return mapToUpdate
else
widgetDestroy window >>
makeAndAddWindow configEq
Nothing -> maybeDeleteWindow
Nothing ->
case getConfig monNum of
Just configEq -> makeAndAddWindow configEq
Nothing -> return mapToUpdate
where makeAndAddWindow (newConfig, eqcfg) =
do
window <- makeTaffyWindow newConfig monNum
return $ M.insert monNum (eqcfg, window) mapToUpdate
deleteWindow (_, window) =
widgetDestroy window >> return (M.delete monNum mapToUpdate)
maybeDeleteWindow = maybe (return mapToUpdate) deleteWindow $
M.lookup monNum mapToUpdate
foldM updateBarOnWindow monitorToWindow monitors
makeTaffyWindow wcfg monNumber = do
window <- windowNew
let windowName = printf "Taffybar-%s" $ show monNumber :: String
styleContext <- Gtk.widgetGetStyleContext window
styleContextAddClass styleContext "Taffybar"
widgetSetName window windowName
windowSetTypeHint window WindowTypeHintDock
windowSetScreen window screen
setTaffybarSize wcfg window monNumber
box <- hBoxNew False $ widgetSpacing wcfg
containerAdd window box
mapM_
(\io -> do
wid <- io
widgetSetSizeRequest wid (-1) (barHeight wcfg)
boxPackStart box wid PackNatural 0)
(startWidgets wcfg)
mapM_
(\io -> do
wid <- io
widgetSetSizeRequest wid (-1) (barHeight wcfg)
boxPackEnd box wid PackNatural 0)
(endWidgets wcfg)
widgetShow window
widgetShow box
return window
_ <- on screen screenMonitorsChanged refreshTaffyWindows
startRefresher cfg $ postGUIAsync refreshTaffyWindows
refreshTaffyWindows
-- Reset the size of the Taffybar window if the monitor setup has
-- changed, e.g., after a laptop user has attached an external
-- monitor.
mainGUI
return ()