Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import

  • Loading branch information...
commit 190f14fb3626da2415062e64e9c8f4473a6111ad 0 parents
@travitch authored
1  .gitignore
@@ -0,0 +1 @@
+/dist/
30 LICENSE
@@ -0,0 +1,30 @@
+Copyright (c)2011, Tristan Ravitch
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of Tristan Ravitch nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 README.md
@@ -0,0 +1,38 @@
+This is a desktop information bar intended for use with XMonad and
+similar window managers. It is similar in spirit to xmobar; it is
+different in that it gives up some simplicity for a reasonable helping
+of eye candy. This bar is based on GTK+ (via gtk2hs) and uses fancy
+graphics where doing so is reasonable and useful.
+
+The bar is configured much like XMonad. It uses
+~/.config/taffybar/taffybar.hs as its configuration file. This file
+is just a Haskell program that invokes the real _main_ function with a
+configuration object. The configuration file basically just specifies
+which widgets to use, though any arbitrary Haskell code can be
+executed before the bar is created.
+
+There are some generic pre-defined widgets available:
+
+ * Graph (modeled after the graph widget in Awesome)
+ * Vertical bar (also similar to a widget in Awesome)
+ * Periodically-updating labels, graphs, and vertical bars
+
+There are also several more specialized widgets:
+
+ * Battery widget
+ * Textual clock
+ * Freedesktop.org notifications (via dbus)
+ * MPRIS widget (currently only supports MPRIS1)
+ * Weather widget
+ * XMonad log widget (listens on dbus instead of stdin)
+ * System tray
+
+TODO
+====
+
+An incomplete list of things that would be cool to have:
+
+ * xrandr widget (for dealing changing clone/extend mode and orientation)
+ * MPRIS2 widget
+ * Better behavior when adding/removing monitors (never tried it)
+ * Make MPRIS more configurable
2  Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
9 src/Main.hs
@@ -0,0 +1,9 @@
+-- | This is just a stub executable that uses dyre to read the config
+-- file and recompile itself.
+module Main ( main ) where
+
+import System.Taffybar
+
+main :: IO ()
+main = do
+ defaultTaffybar defaultTaffybarConfig
166 src/System/Information/Battery.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | This is a simple library to query the Linux UPower daemon (via
+-- DBus) for battery information. Currently, it only retrieves
+-- information for the first battery it finds.
+module System.Information.Battery (
+ -- * Types
+ BatteryContext,
+ BatteryInfo(..),
+ BatteryState(..),
+ BatteryTechnology(..),
+ BatteryType(..),
+ -- * Accessors
+ batteryContextNew,
+ getBatteryInfo
+ ) where
+
+import Data.Map ( Map )
+import qualified Data.Map as M
+import Data.Word
+import Data.Int
+import DBus.Client.Simple
+import Data.List ( find )
+import Data.Text ( isInfixOf, Text )
+
+-- | An opaque wrapper around some internal library state
+newtype BatteryContext = BC Proxy
+
+data BatteryType = BatteryTypeUnknown
+ | BatteryTypeLinePower
+ | BatteryTypeBatteryType
+ | BatteryTypeUps
+ | BatteryTypeMonitor
+ | BatteryTypeMouse
+ | BatteryTypeKeyboard
+ | BatteryTypePda
+ | BatteryTypePhone
+ deriving (Show, Ord, Eq, Enum)
+
+data BatteryState = BatteryStateUnknown
+ | BatteryStateCharging
+ | BatteryStateDischarging
+ | BatteryStateEmpty
+ | BatteryStateFullyCharged
+ | BatteryStatePendingCharge
+ | BatteryStatePendingDischarge
+ deriving (Show, Ord, Eq, Enum)
+
+data BatteryTechnology = BatteryTechnologyUnknown
+ | BatteryTechnologyLithiumIon
+ | BatteryTechnologyLithiumPolymer
+ | BatteryTechnologyLithiumIronPhosphate
+ | BatteryTechnologyLeadAcid
+ | BatteryTechnologyNickelCadmium
+ | BatteryTechnologyNickelMetalHydride
+ deriving (Show, Ord, Eq, Enum)
+
+-- | There are a few fields supported by UPower that aren't exposed
+-- here.. could be easily.
+data BatteryInfo = BatteryInfo { batteryNativePath :: Text
+ , batteryVendor :: Text
+ , batteryModel :: Text
+ , batterySerial :: Text
+ -- , batteryUpdateTime :: Time
+ , batteryType :: BatteryType
+ , batteryPowerSupply :: Bool
+ , batteryHasHistory :: Bool
+ , batteryHasStatistics :: Bool
+ , batteryOnline :: Bool
+ , batteryEnergy :: Double
+ , batteryEnergyEmpty :: Double
+ , batteryEnergyFull :: Double
+ , batteryEnergyFullDesign :: Double
+ , batteryEnergyRate :: Double
+ , batteryVoltage :: Double
+ , batteryTimeToEmpty :: Int64
+ , batteryTimeToFull :: Int64
+ , batteryPercentage :: Double
+ , batteryIsPresent :: Bool
+ , batteryState :: BatteryState
+ , batteryIsRechargable :: Bool
+ , batteryCapacity :: Double
+ , batteryTechnology :: BatteryTechnology
+{- , batteryRecallNotice :: Bool
+ , batteryRecallVendor :: Text
+ , batteryRecallUr :: Text
+-}
+ }
+
+-- | Find the first power source that is a battery in the list. The
+-- simple heuristic is a substring search on 'BAT'
+firstBattery :: [ObjectPath] -> Maybe ObjectPath
+firstBattery = find (isInfixOf "BAT" . objectPathText)
+
+-- | The name of the power daemon bus
+powerBusName :: BusName
+powerBusName = "org.freedesktop.UPower"
+
+-- | The base object path
+powerBaseObjectPath :: ObjectPath
+powerBaseObjectPath = "/org/freedesktop/UPower"
+
+-- | A helper to read the variant contents of a dict with a default
+-- value.
+readDict :: (IsVariant a) => Map Text Variant -> Text -> a -> a
+readDict dict key dflt = val
+ where
+ Just val = fromVariant variant
+ variant = M.findWithDefault (toVariant dflt) key dict
+
+-- | Query the UPower daemon about information on a specific battery.
+-- If some fields are not actually present, they may have bogus values
+-- here. Don't bet anything critical on it.
+getBatteryInfo :: BatteryContext -> IO BatteryInfo
+getBatteryInfo (BC batteryProxy) = do
+ -- Grab all of the properties of the battery each call with one
+ -- message.
+ let iface :: Variant
+ iface = toVariant ("org.freedesktop.UPower.Device" :: Text)
+
+ [val] <- call batteryProxy "org.freedesktop.DBus.Properties" "GetAll" [iface]
+
+ let dict :: Map Text Variant
+ Just dict = fromVariant val
+ return BatteryInfo { batteryNativePath = readDict dict "NativePath" ""
+ , batteryVendor = readDict dict "Vendor" ""
+ , batteryModel = readDict dict "Model" ""
+ , batterySerial = readDict dict "Serial" ""
+ , batteryType = toEnum $ fromIntegral $ readDict dict "Type" (0 :: Word64)
+ , batteryPowerSupply = readDict dict "PowerSupply" False
+ , batteryHasHistory = readDict dict "HasHistory" False
+ , batteryHasStatistics = readDict dict "HasStatistics" False
+ , batteryOnline = readDict dict "Online" False
+ , batteryEnergy = readDict dict "Energy" 0.0
+ , batteryEnergyEmpty = readDict dict "EnergyEmpty" 0.0
+ , batteryEnergyFull = readDict dict "EnergyFull" 0.0
+ , batteryEnergyFullDesign = readDict dict "EnergyFullDesign" 0.0
+ , batteryEnergyRate = readDict dict "EnergyRate" 0.0
+ , batteryVoltage = readDict dict "Voltage" 0.0
+ , batteryTimeToEmpty = readDict dict "TimeToEmpty" 0
+ , batteryTimeToFull = readDict dict "TimeToFull" 0
+ , batteryPercentage = readDict dict "Percentage" 0.0
+ , batteryIsPresent = readDict dict "IsPresent" False
+ , batteryState = toEnum $ fromIntegral $ readDict dict "State" (0 :: Word64)
+ , batteryIsRechargable = readDict dict "IsRechargable" True
+ , batteryCapacity = readDict dict "Capacity" 0.0
+ , batteryTechnology =
+ toEnum $ fromIntegral $ readDict dict "Technology" (0 :: Word64)
+ }
+
+-- | Construct a battery context if possible. This could fail if the
+-- UPower daemon is not running. The context can be used to get
+-- actual battery state with 'getBatteryInfo'.
+batteryContextNew :: IO (Maybe BatteryContext)
+batteryContextNew = do
+ systemConn <- connectSystem
+
+ -- First, get the list of devices. For now, we just get the stats
+ -- for the first battery
+ powerProxy <- proxy systemConn powerBusName powerBaseObjectPath
+ [ powerDevicesV ] <- call powerProxy "org.freedesktop.UPower" "EnumerateDevices" []
+ let Just powerDevices = fromVariant powerDevicesV
+ case firstBattery powerDevices of
+ Nothing -> return Nothing
+ Just battPath ->
+ proxy systemConn powerBusName battPath >>= (return . Just . BC)
+
35 src/System/Information/CPU.hs
@@ -0,0 +1,35 @@
+module System.Information.CPU ( cpuLoad ) where
+
+import Control.Concurrent ( threadDelay )
+import System.IO ( IOMode(ReadMode), openFile, hGetLine, hClose )
+
+procData :: IO [Double]
+procData = do
+ h <- openFile "/proc/stat" ReadMode
+ firstLine <- hGetLine h
+ (length firstLine) `seq` return ()
+ hClose h
+ return (procParser firstLine)
+
+procParser :: String -> [Double]
+procParser = map read . tail . words
+
+truncVal :: Double -> Double
+truncVal v
+ | isNaN v || v < 0.0 = 0.0
+ | otherwise = v
+
+-- | Return a pair with (user time, system time, total time) (read
+-- from /proc/stat). The function waits for 50 ms between samples.
+cpuLoad :: IO (Double, Double, Double)
+cpuLoad = do
+ a <- procData
+ threadDelay 50000
+ b <- procData
+ let dif = zipWith (-) b a
+ tot = foldr (+) 0 dif
+ pct = map (/ tot) dif
+ user = foldr (+) 0 $ take 2 pct
+ system = pct !! 2
+ t = user + system
+ return (truncVal user, truncVal system, truncVal t)
34 src/System/Information/Memory.hs
@@ -0,0 +1,34 @@
+module System.Information.Memory (
+ MemoryInfo(..),
+ parseMeminfo
+ ) where
+
+toMB :: [String] -> Double
+toMB line = (read $ line !! 1 :: Double) / 1024
+
+data MemoryInfo = MemoryInfo { memoryUsedRatio :: Double
+ , memoryTotal :: Double
+ , memoryFree :: Double
+ , memoryBuffer :: Double
+ , memoryCache :: Double
+ , memoryRest :: Double
+ , memoryUsed :: Double
+ }
+
+parseMeminfo :: IO MemoryInfo
+parseMeminfo = do
+ s <- readFile "/proc/meminfo"
+ let content = map words $ take 4 $ lines s
+ [total, free, buffer, cache ] = map toMB content
+ rest = free + buffer + cache
+ used = total - rest
+ usedRatio = used / total
+ return MemoryInfo { memoryUsedRatio = usedRatio
+ , memoryTotal = total
+ , memoryFree = free
+ , memoryBuffer = buffer
+ , memoryCache = cache
+ , memoryRest = rest
+ , memoryUsed = used
+ }
+
176 src/System/Taffybar.hs
@@ -0,0 +1,176 @@
+-- | 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.
+--
+-- 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 "<span fgcolor='orange'>%a %b %_d %H:%M</span>" 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.
+--
+-- 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 DBus.Client.Simple
+-- > import System.Taffybar.XMonadLog ( dbusLog )
+-- >
+-- > main = do
+-- > client <- connectSession
+-- > let pp = defaultPP
+-- > xmonad defaultConfig { logHook = dbusLog client pp }
+--
+-- The complexity is handled in the System.Tafftbar.XMonadLog module.
+module System.Taffybar (
+ TaffybarConfig(..),
+ defaultTaffybar,
+ defaultTaffybarConfig
+ ) where
+
+import qualified Config.Dyre as Dyre
+
+import Graphics.UI.Gtk
+import Text.Printf
+
+import System.Taffybar.StrutProperties
+
+data TaffybarConfig =
+ TaffybarConfig { screenNumber :: Int -- ^ The screen number to run the bar on (default is almost always fine)
+ , monitorNumber :: Int -- ^ The xinerama/xrandr monitor number to put the bar on (default: 0)
+ , barHeight :: Int -- ^ Number of pixels to reserve for the bar (default: 25 pixels)
+ , errorMsg :: Maybe String -- ^ Used by the application
+ , startWidgets :: [IO Widget] -- ^ Widgets that are packed in order at the left end of the bar
+ , endWidgets :: [IO Widget] -- ^ Widgets that are packed from right-to-left in the bar
+ }
+
+-- | The default configuration gives an empty bar 25 pixels high on monitor 0.
+defaultTaffybarConfig :: TaffybarConfig
+defaultTaffybarConfig =
+ TaffybarConfig { screenNumber = 0
+ , monitorNumber = 0
+ , barHeight = 25
+ , errorMsg = Nothing
+ , startWidgets = []
+ , endWidgets = []
+ }
+
+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"]
+ }
+
+-- | The entry point of the application. Feed it a custom config.
+defaultTaffybar :: TaffybarConfig -> IO ()
+defaultTaffybar = Dyre.wrapMain defaultParams
+
+realMain :: TaffybarConfig -> IO ()
+realMain cfg = do
+ case errorMsg cfg of
+ Nothing -> taffybarMain cfg
+ Just err -> error ("Error: " ++ err)
+
+taffybarMain :: TaffybarConfig -> IO ()
+taffybarMain cfg = do
+ _ <- initGUI
+ Just disp <- displayGetDefault
+ nscreens <- displayGetNScreens disp
+ screen <- case screenNumber cfg < nscreens of
+ False -> error $ printf "Screen %d is not available in the default display" (screenNumber cfg)
+ True -> displayGetScreen disp (screenNumber cfg)
+ nmonitors <- screenGetNMonitors screen
+ monitorSize <- case monitorNumber cfg < nmonitors of
+ False -> error $ printf "Monitor %d is not available in the selected screen" (monitorNumber cfg)
+ True -> screenGetMonitorGeometry screen (monitorNumber cfg)
+
+ window <- windowNew
+ let Rectangle x y w _ = monitorSize
+ windowSetTypeHint window WindowTypeHintDock
+ windowSetScreen window screen
+ windowSetDefaultSize window w (barHeight cfg)
+ windowMove window x y
+ widgetModifyBg window StateNormal (Color 0 0 0)
+ _ <- onRealize window $ setStrutProperties window (0, 0, barHeight cfg, 0,
+ 0, 0,
+ 0, 0,
+ x, x + w - 10,
+ 0, 0)
+ box <- hBoxNew False 10
+ containerAdd window box
+
+ mapM_ (\io -> do
+ wid <- io
+ widgetSetSizeRequest wid (-1) (barHeight cfg)
+ boxPackStart box wid PackNatural 0) (startWidgets cfg)
+ mapM_ (\io -> do
+ wid <- io
+ widgetSetSizeRequest wid (-1) (barHeight cfg)
+ boxPackEnd box wid PackNatural 0) (endWidgets cfg)
+ widgetShow window
+ widgetShow box
+ mainGUI
+ return ()
93 src/System/Taffybar/Battery.hs
@@ -0,0 +1,93 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | This module provides battery widgets using the UPower system
+-- service.
+--
+-- Currently it reports only the first battery it finds. If it does
+-- not find a batterym it just returns an obnoxious widget with
+-- warning text in it. Battery hotplugging is not supported. These
+-- more advanced features could be supported if there is interest.
+module System.Taffybar.Battery (
+ batteryBarNew,
+ textBatteryNew,
+ defaultBatteryConfig
+ ) where
+
+import Graphics.UI.Gtk
+import Text.Printf
+
+import System.Information.Battery
+import System.Taffybar.Widgets.PollingBar
+import System.Taffybar.Widgets.PollingLabel
+
+battInfo :: BatteryContext -> String -> IO String
+battInfo ctxt fmt = do
+ info <- getBatteryInfo ctxt
+ let battPctNum :: Int
+ battPctNum = floor (batteryPercentage info)
+ return $ printf fmt battPctNum
+
+-- | A simple textual battery widget that auto-updates once every
+-- polling period (specified in seconds). The displayed format is
+-- specified using a printf-style format string. The format string
+-- must have a single format argument: %d (and any number of %%
+-- sequences to insert a literal percent sign).
+--
+-- More, fewer, or different format arguments will result in a runtime
+-- error.
+textBatteryNew :: String -- ^ Display format
+ -> Double -- ^ Poll period in seconds
+ -> IO Widget
+textBatteryNew fmt pollSeconds = do
+ battCtxt <- batteryContextNew
+
+ case battCtxt of
+ Nothing -> labelNew (Just "No battery") >>= return . toWidget
+ Just ctxt -> do
+ l <- pollingLabelNew "" pollSeconds (battInfo ctxt fmt)
+ widgetShowAll l
+ return l
+
+-- | Returns the current battery percent as a double in the range [0,
+-- 1]
+battPct :: BatteryContext -> IO Double
+battPct ctxt = do
+ info <- getBatteryInfo ctxt
+ return (batteryPercentage info / 100)
+
+-- | A default configuration for the graphical battery display. The
+-- bar will be red when power is critical (< 10%), green if it is full
+-- (> 90%), and grey otherwise.
+--
+-- You can customize this with any of the options in 'BarConfig'
+defaultBatteryConfig :: BarConfig
+defaultBatteryConfig =
+ defaultBarConfig colorFunc
+ where
+ colorFunc pct
+ | pct < 0.1 = (1, 0, 0)
+ | pct < 0.9 = (0.5, 0.5, 0.5)
+ | otherwise = (0, 1, 0)
+
+-- | A fancy graphical battery widget that represents the current
+-- charge as a colored vertical bar. There is also a textual
+-- percentage readout next to the bar.
+batteryBarNew :: BarConfig -- ^ Configuration options for the bar display
+ -> Double -- ^ Polling period in seconds
+ -> IO Widget
+batteryBarNew battCfg pollSeconds = do
+ battCtxt <- batteryContextNew
+ case battCtxt of
+ Nothing -> labelNew (Just "No battery") >>= return . toWidget
+ Just ctxt -> do
+ -- This is currently pretty inefficient - each poll period it
+ -- queries the battery twice (once for the label and once for
+ -- the bar).
+ --
+ -- Converting it to combine the two shouldn't be hard.
+ b <- hBoxNew False 1
+ txt <- textBatteryNew "%d%%" pollSeconds
+ bar <- pollingBarNew battCfg pollSeconds (battPct ctxt)
+ boxPackStart b bar PackNatural 0
+ boxPackStart b txt PackNatural 0
+ widgetShowAll b
+ return (toWidget b)
322 src/System/Taffybar/FreedesktopNotifications.hs
@@ -0,0 +1,322 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | This widget listens on DBus for freedesktop notifications
+-- (http://developer.gnome.org/notification-spec/). Currently it is
+-- somewhat ugly, but the format is somewhat configurable. A visual
+-- overhaul of the widget is coming.
+--
+-- The widget only displays one notification at a time and
+-- notifications are cancellable.
+module System.Taffybar.FreedesktopNotifications (
+ -- * Types
+ Notification(..),
+ NotificationConfig(..),
+ -- * Constructor
+ notifyAreaNew,
+ defaultNotificationConfig
+ ) where
+
+import Control.Concurrent
+import Data.Int ( Int32 )
+import Data.IORef
+import Data.Map ( Map )
+import Data.Monoid ( mconcat )
+import qualified Data.Sequence as S
+import Data.Sequence ( Seq, (|>), viewl, ViewL(..) )
+import Data.Text ( Text )
+import qualified Data.Text as T
+import Data.Word ( Word32 )
+import DBus.Client.Simple
+import Graphics.UI.Gtk hiding ( Variant )
+import Web.Encodings ( decodeHtml, encodeHtml )
+
+-- | A simple structure representing a Freedesktop notification
+data Notification = Notification { noteAppName :: Text
+ , noteReplaceId :: Word32
+ , noteSummary :: Text
+ , noteBody :: Text
+ , noteExpireTimeout :: Int32
+ , noteId :: Word32
+ }
+ deriving (Show, Eq)
+
+data WorkType = CancelNote (Maybe Word32)
+ | ReplaceNote Word32 Notification
+ | NewNote
+ | ExpireNote Word32
+
+data NotifyState = NotifyState { noteQueue :: MVar (Seq Notification)
+ , noteIdSource :: MVar Word32
+ , noteWorkerChan :: Chan WorkType
+ , noteWidget :: Label
+ , noteContainer :: InfoBar
+ , noteTimerThread :: MVar (Maybe ThreadId)
+ , noteConfig :: NotificationConfig
+ }
+
+initialNoteState :: InfoBar -> Label -> NotificationConfig -> IO NotifyState
+initialNoteState ib l cfg = do
+ c <- newChan
+ m <- newMVar 1
+ q <- newMVar S.empty
+ t <- newMVar Nothing
+ return NotifyState { noteQueue = q
+ , noteIdSource = m
+ , noteWorkerChan = c
+ , noteWidget = l
+ , noteContainer = ib
+ , noteTimerThread = t
+ , noteConfig = cfg
+ }
+
+getServerInformation :: IO (Text, Text, Text, Text)
+getServerInformation =
+ return ("haskell-notification-daemon",
+ "nochair.net",
+ "0.0.1",
+ "1.1")
+
+getCapabilities :: IO [Text]
+getCapabilities = return ["body", "body-markup"]
+
+closeNotification :: NotifyState -> Word32 -> IO ()
+closeNotification istate nid = do
+ -- FIXME: filter anything with this nid out of the queue before
+ -- posting to the queue so that the worker doesn't need to scan the
+ -- queue
+ writeChan (noteWorkerChan istate) (CancelNote (Just nid))
+
+-- | Apply the user's formatter and truncate the result with the
+-- specified maxlen.
+formatMessage :: NotifyState -> Notification -> String
+formatMessage s = take maxlen . fmt
+ where
+ maxlen = notificationMaxLength $ noteConfig s
+ fmt = notificationFormatter $ noteConfig s
+
+notify :: MVar Int
+ -> NotifyState
+ -> Text -- ^ Application name
+ -> Word32 -- ^ Replaces id
+ -> Text -- ^ App icon
+ -> Text -- ^ Summary
+ -> Text -- ^ Body
+ -> [Text] -- ^ Actions
+ -> Map Text Variant -- ^ Hints
+ -> Int32 -- ^ Expires timeout (milliseconds)
+ -> IO Word32
+notify idSrc istate appName replaceId icon summary body actions hints timeout = do
+ let maxtout = fromIntegral $ notificationMaxTimeout (noteConfig istate)
+ tout = case timeout of
+ 0 -> maxtout
+ (-1) -> maxtout
+ _ -> min maxtout timeout
+ case replaceId of
+ 0 -> do
+ nid <- modifyMVar idSrc (\x -> return (x+1, x))
+ let n = Notification { noteAppName = appName
+ , noteReplaceId = 0
+ , noteSummary = encodeHtml $ decodeHtml summary
+ , noteBody = encodeHtml $ decodeHtml body
+ , noteExpireTimeout = tout
+ , noteId = fromIntegral nid
+ }
+ modifyMVar_ (noteQueue istate) (\x -> return (x |> n))
+ writeChan (noteWorkerChan istate) NewNote
+ return (fromIntegral nid)
+ i -> do
+ let n = Notification { noteAppName = appName
+ , noteReplaceId = i
+ , noteSummary = summary
+ , noteBody = body
+ , noteExpireTimeout = tout
+ , noteId = i
+ }
+ -- First, replace any notes in the note queue with this note, if
+ -- applicable. Next, notify the worker and have it replace the
+ -- current note if that note has this id.
+ modifyMVar_ (noteQueue istate) (\q -> return $ fmap (replaceNote i n) q)
+ writeChan (noteWorkerChan istate) (ReplaceNote i n)
+ return i
+
+replaceNote :: Word32 -> Notification -> Notification -> Notification
+replaceNote nid newNote curNote =
+ case noteId curNote == nid of
+ False -> curNote
+ True -> newNote
+
+notificationDaemon onNote onCloseNote = do
+ client <- connectSession
+ _ <- requestName client "org.freedesktop.Notifications" [AllowReplacement, ReplaceExisting]
+ export client "/org/freedesktop/Notifications"
+ [ method "org.freedesktop.Notifications" "GetServerInformation" getServerInformation
+ , method "org.freedesktop.Notifications" "GetCapabilities" getCapabilities
+ , method "org.freedesktop.Notifications" "CloseNotification" onCloseNote
+ , method "org.freedesktop.Notifications" "Notify" onNote
+ ]
+
+-- When a notification is received, add it to the queue. Post a token to the channel that the
+-- worker blocks on.
+
+-- The worker thread should sit idle waiting on a chan read. When it
+-- wakes up, check to see if the current notification needs to be
+-- expired (due to a cancellation) or just expired on its own. If it
+-- expired on its own, just empty it out and post the next item in the
+-- queue, if any. If posting, start a thread that just calls
+-- theadDelay for the lifetime of the notification.
+
+workerThread :: NotifyState -> IO ()
+workerThread s = do
+ currentNote <- newIORef Nothing
+ workerThread' currentNote
+ where
+ workerThread' currentNote = do
+ work <- readChan (noteWorkerChan s)
+ case work of
+ NewNote -> onNewNote currentNote
+ ReplaceNote nid n -> onReplaceNote currentNote nid n
+ CancelNote Nothing -> userCancelNote currentNote
+ CancelNote nid -> do
+ workerThread' currentNote
+ ExpireNote nid -> expireNote currentNote nid
+ -- | The user closed the notification manually
+ userCancelNote currentNote = do
+ writeIORef currentNote Nothing
+ postGUIAsync $ widgetHideAll (noteContainer s)
+ showNextNoteIfAny currentNote
+
+ onReplaceNote currentNote nid n = do
+ cnote <- readIORef currentNote
+ case cnote of
+ Nothing -> do
+ writeIORef currentNote (Just n)
+ postGUIAsync $ do
+ labelSetMarkup (noteWidget s) (formatMessage s n)
+ widgetShowAll (noteContainer s)
+ timerThreadId <- forkIO $ setExpireTimeout (noteWorkerChan s) (noteId n) (noteExpireTimeout n)
+ modifyMVar_ (noteTimerThread s) $ const (return (Just timerThreadId))
+ workerThread' currentNote
+ Just cnote' -> case noteId cnote' == nid of
+ -- The replaced note was not current and it either does not
+ -- exist or it was already replaced in the note queue
+ False -> workerThread' currentNote
+ -- Otherwise, swap out the current note
+ True -> do
+ withMVar (noteTimerThread s) (maybe (return ()) killThread)
+ writeIORef currentNote (Just n)
+ postGUIAsync $ labelSetMarkup (noteWidget s) (formatMessage s n)
+ timerId <- forkIO $ setExpireTimeout (noteWorkerChan s) (noteId n) (noteExpireTimeout n)
+ modifyMVar_ (noteTimerThread s) $ const $ return (Just timerId)
+ workerThread' currentNote
+
+ -- | If the current note has the ID being expired, clear the
+ -- notification area and see if there is a pending note to post.
+ expireNote currentNote nid = do
+ cnote <- readIORef currentNote
+ case cnote of
+ Nothing -> showNextNoteIfAny currentNote
+ Just cnote' ->
+ case noteId cnote' == nid of
+ False -> workerThread' currentNote -- Already expired
+ True -> do
+ -- Drop the reference and clear the notification area
+ -- before trying to show a new note
+ writeIORef currentNote Nothing
+ postGUIAsync $ widgetHideAll (noteContainer s)
+ showNextNoteIfAny currentNote
+
+ onNewNote currentNote = do
+ maybeCurrent <- readIORef currentNote
+ case maybeCurrent of
+ Nothing -> showNextNoteIfAny currentNote
+ -- Grab the next note, show it, and then start a timer
+ Just note -> do
+ -- Otherwise, the current note isn't expired yet and we need
+ -- to wait for it.
+ workerThread' currentNote
+
+ -- For use when there is no current note, attempt to show the next
+ -- node and then block to wait for the next event. This is
+ -- guarded by a postGUIAsync.
+ showNextNoteIfAny noCurrentNote = do
+ nextNote <- modifyMVar (noteQueue s) takeNote
+ case nextNote of
+ Nothing -> workerThread' noCurrentNote
+ Just nextNote' -> do
+ writeIORef noCurrentNote nextNote
+ postGUIAsync $ do
+ labelSetMarkup (noteWidget s) (formatMessage s nextNote')
+ widgetShowAll (noteContainer s)
+ timerThreadId <- forkIO $ setExpireTimeout (noteWorkerChan s) (noteId nextNote') (noteExpireTimeout nextNote')
+ modifyMVar_ (noteTimerThread s) $ const (return (Just timerThreadId))
+ workerThread' noCurrentNote
+
+
+takeNote :: Monad m => Seq a -> m (Seq a, Maybe a)
+takeNote q =
+ case viewl q of
+ EmptyL -> return (q, Nothing)
+ n :< rest -> return (rest, Just n)
+
+setExpireTimeout :: Chan WorkType -> Word32 -> Int32 -> IO ()
+setExpireTimeout c nid seconds = do
+ threadDelay (fromIntegral seconds * 1000000)
+ writeChan c (ExpireNote nid)
+
+userCancel s _ = writeChan (noteWorkerChan s) (CancelNote Nothing)
+
+data NotificationConfig =
+ NotificationConfig { notificationMaxTimeout :: Int -- ^ Maximum time that a notification will be displayed (in seconds). Default: 10
+ , notificationMaxLength :: Int -- ^ Maximum length displayed, in characters. Default: 50
+ , notificationFormatter :: Notification -> String -- ^ Function used to format notifications
+ }
+
+defaultFormatter :: Notification -> String
+defaultFormatter note = msg
+ where
+ msg = case T.null (noteBody note) of
+ True -> T.unpack $ noteSummary note
+ False -> T.unpack $ mconcat [ noteSummary note, ": ", noteBody note ]
+
+-- | The default formatter is one of
+--
+-- * Summary : Body
+--
+-- * Summary
+--
+-- depending on the presence of a notification body.
+defaultNotificationConfig :: NotificationConfig
+defaultNotificationConfig =
+ NotificationConfig { notificationMaxTimeout = 10
+ , notificationMaxLength = 50
+ , notificationFormatter = defaultFormatter
+ }
+
+-- | Create a new notification area with the given configuration.
+notifyAreaNew :: NotificationConfig -> IO Widget
+notifyAreaNew cfg = do
+ ib <- infoBarNew
+ l <- labelNew Nothing
+ button <- buttonNew -- FromStock stockClose
+ img <- imageNewFromStock stockClose (IconSizeUser 16)
+ ca <- infoBarGetContentArea ib
+ let container = castToContainer ca
+ containerAdd container l
+ containerAdd button img
+ infoBarAddActionWidget ib button 0
+ widgetHideAll ib
+
+
+
+ istate <- initialNoteState ib l cfg
+ _ <- on ib infoBarResponse (userCancel istate)
+ _ <- forkIO (workerThread istate)
+
+ -- This is only available to the notify handler, so it doesn't need
+ -- to be protected from the worker thread. There might be multiple
+ -- notifiation handler threads, though (not sure), so keep it safe
+ -- and use an mvar.
+ idSrc <- newMVar 1
+ notificationDaemon (notify idSrc istate) (closeNotification istate)
+
+ -- Don't show ib by default - it will appear when needed
+ return (toWidget ib)
85 src/System/Taffybar/MPRIS.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | This is a "Now Playing"-style widget that listens for MPRIS
+-- events on DBus. Various media players implement this. This widget
+-- only works with version 1 of the MPRIS protocol
+-- (http://www.mpris.org/1.0/spec.html). Support for version 2 will
+-- be in a separate widget.
+--
+-- This widget isn't as configurable as the others yet - that will be
+-- fixed.
+module System.Taffybar.MPRIS ( mprisNew ) where
+
+import Data.Int ( Int32 )
+import qualified Data.Map as M
+import Data.Text ( Text )
+import qualified Data.Text as T
+import DBus.Client.Simple ( connectSession )
+import DBus.Client
+import DBus.Types
+import DBus.Message
+import Graphics.UI.Gtk hiding ( Signal, Variant )
+import Web.Encodings ( encodeHtml, decodeHtml )
+import Text.Printf
+
+setupDBus :: Label -> IO ()
+setupDBus w = do
+ let trackMatcher = MatchRule { matchSender = Nothing
+ , matchDestination = Nothing
+ , matchPath = Just "/Player"
+ , matchInterface = Just "org.freedesktop.MediaPlayer"
+ , matchMember = Just "TrackChange"
+ }
+ stateMatcher = MatchRule { matchSender = Nothing
+ , matchDestination = Nothing
+ , matchPath = Just "/Player"
+ , matchInterface = Just "org.freedesktop.MediaPlayer"
+ , matchMember = Just "StatusChange"
+ }
+ client <- connectSession
+ listen client trackMatcher (trackCallback w)
+ listen client stateMatcher (stateCallback w)
+
+variantDictLookup :: (IsVariant b, Ord k) => k -> M.Map k Variant -> Maybe b
+variantDictLookup k m = do
+ val <- M.lookup k m
+ fromVariant val
+
+
+trackCallback :: Label -> BusName -> Signal -> IO ()
+trackCallback w _ Signal { signalBody = [variant] } = do
+ let v :: Maybe (M.Map Text Variant)
+ v = fromVariant variant
+ case v of
+ Just m -> do
+ let artist = maybe "[unknown]" id (variantDictLookup "artist" m)
+ track = maybe "[unknown]" id (variantDictLookup "title" m)
+ msg = encodeHtml $ decodeHtml $ printf "%s - %s" (T.unpack artist) (T.unpack track)
+ txt = "<span fgcolor='yellow'>Now Playing:</span> " ++ msg
+ postGUIAsync $ do
+ -- In case the widget was hidden due to a stop/pause, forcibly
+ -- show it again when the track changes.
+ labelSetMarkup w txt
+ widgetShowAll w
+ _ -> return ()
+trackCallback _ _ _ = return ()
+
+stateCallback :: Label -> BusName -> Signal -> IO ()
+stateCallback w _ Signal { signalBody = [bdy] } =
+ case fromVariant bdy of
+ Just st -> case structureItems st of
+ (pstate:_) -> case (fromVariant pstate) :: Maybe Int32 of
+ Just 2 -> postGUIAsync $ widgetHideAll w
+ Just 1 -> postGUIAsync $ widgetHideAll w
+ Just 0 -> postGUIAsync $ widgetShowAll w
+ _ -> return ()
+ _ -> return ()
+ _ -> return ()
+stateCallback _ _ _ = return ()
+
+mprisNew :: IO Widget
+mprisNew = do
+ l <- labelNew Nothing
+
+ setupDBus l
+ widgetShowAll l
+ return (toWidget l)
72 src/System/Taffybar/SimpleClock.hs
@@ -0,0 +1,72 @@
+-- | This module implements a very simple text-based clock widget.
+-- The widget also toggles a calendar widget when clicked. This
+-- calendar is not fancy at all and has no data backend.
+module System.Taffybar.SimpleClock ( textClockNew ) where
+
+import Control.Monad.Trans ( MonadIO, liftIO )
+import Data.Time.Format
+import Data.Time.LocalTime
+import Graphics.UI.Gtk
+import System.Locale
+
+import System.Taffybar.Widgets.PollingLabel
+
+getCurrentTime :: TimeLocale -> String -> IO String
+getCurrentTime timeLocale fmt = do
+ zt <- getZonedTime
+ return $ formatTime timeLocale fmt zt
+
+makeCalendar :: IO Window
+makeCalendar = do
+ container <- windowNew
+ cal <- calendarNew
+ containerAdd container cal
+ return container
+
+toggleCalendar w c = liftIO $ do
+ isVis <- get c widgetVisible
+ case isVis of
+ True -> widgetHideAll c
+ False -> do
+ windowSetKeepAbove c True
+ windowStick c
+ windowSetTypeHint c WindowTypeHintTooltip
+ windowSetSkipTaskbarHint c True
+ windowSetSkipPagerHint c True
+
+ Just topLevel <- widgetGetAncestor w gTypeWindow
+ let topLevelWindow = castToWindow topLevel
+ windowSetTransientFor c topLevelWindow
+
+ widgetShowAll c
+
+ return True
+
+-- | Create the widget. I recommend passing @Nothing@ for the
+-- TimeLocale parameter. The format string can include Pango markup
+-- (http://developer.gnome.org/pango/stable/PangoMarkupFormat.html).
+textClockNew :: Maybe TimeLocale -- ^ An TimeLocale - if not specified, the default is used. This can be used to customize how different aspects of time are localized
+ -> String -- ^ The time format string (see http://www.haskell.org/ghc/docs/6.12.2/html/libraries/time-1.1.4/Data-Time-Format.html)
+ -> Double -- ^ The number of seconds to wait between clock updates
+ -> IO Widget
+textClockNew userLocale fmt updateSeconds = do
+ let timeLocale = maybe defaultTimeLocale id userLocale
+
+ -- Use a label to display the time. Since we want to be able to
+ -- click on it to show a calendar, we need an eventbox wrapper to
+ -- actually receive events.
+ l <- pollingLabelNew "" updateSeconds (getCurrentTime timeLocale fmt)
+
+-- l <- labelNew Nothing
+ ebox <- eventBoxNew
+ containerAdd ebox l
+ eventBoxSetVisibleWindow ebox False
+
+ -- Allocate a hidden calendar and just show/hide it on clicks.
+ cal <- makeCalendar
+
+ _ <- on ebox buttonPressEvent (toggleCalendar l cal)
+ widgetShowAll ebox
+
+ -- The widget in the bar is actuall the eventbox
+ return (toWidget ebox)
32 src/System/Taffybar/StrutProperties.hs
@@ -0,0 +1,32 @@
+module System.Taffybar.StrutProperties ( setStrutProperties ) where
+
+import Graphics.UI.Gtk
+
+import Foreign
+import Foreign.C.Types
+import Unsafe.Coerce ( unsafeCoerce )
+
+foreign import ccall "set_strut_properties"
+ c_set_strut_properties :: Ptr Window -> CLong -> CLong -> CLong -> CLong
+ -> CLong -> CLong
+ -> CLong -> CLong
+ -> CLong -> CLong
+ -> CLong -> CLong
+ -> ()
+
+-- | Reserve EWMH struts
+setStrutProperties :: Window -> (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) -> IO ()
+setStrutProperties gtkWindow (left, right, top, bottom,
+ left_start_y, left_end_y,
+ right_start_y, right_end_y,
+ top_start_x, top_end_x,
+ bottom_start_x, bottom_end_x) = do
+ let ptrWin = unsafeCoerce gtkWindow :: ForeignPtr Window
+ let fi = fromIntegral
+ withForeignPtr ptrWin $ \realPointer -> do
+ return $ c_set_strut_properties realPointer (fi left) (fi right) (fi top) (fi bottom)
+ (fi left_start_y) (fi left_end_y)
+ (fi right_start_y) (fi right_end_y)
+ (fi top_start_x) (fi top_end_x)
+ (fi bottom_start_x) (fi bottom_end_x)
+
26 src/System/Taffybar/Systray.hs
@@ -0,0 +1,26 @@
+-- | This is a very basic system tray widget. That said, it works
+-- very well since it is based on eggtraymanager.
+module System.Taffybar.Systray ( systrayNew ) where
+
+import Graphics.UI.Gtk
+import Graphics.UI.Gtk.Misc.TrayManager
+
+systrayNew :: IO Widget
+systrayNew = do
+ box <- hBoxNew False 5
+ widgetModifyBg box StateNormal (Color 0 0 0)
+
+ trayManager <- trayManagerNew
+ Just screen <- screenGetDefault
+ _ <- trayManagerManageScreen trayManager screen
+
+ _ <- on trayManager trayIconAdded $ \w -> do
+ widgetModifyBg w StateNormal (Color 0 0 0)
+ widgetShowAll w
+ boxPackStart box w PackNatural 0
+
+ _ <- on trayManager trayIconRemoved $ \w -> do
+ putStrLn "Tray icon removed"
+
+ widgetShowAll box
+ return (toWidget box)
272 src/System/Taffybar/Weather.hs
@@ -0,0 +1,272 @@
+-- | This module defines a simple textual weather widget that polls
+-- NOAA for weather data. To find your weather station, you can use
+--
+-- > http://lwf.ncdc.noaa.gov/oa/climate/stationlocator.html
+--
+-- For example, Madison, WI is KMSN.
+--
+-- NOAA provides several pieces of information in each request; you
+-- can control which pieces end up in your weather widget by providing
+-- a _template_ that is filled in with the current information. The
+-- template is just a 'String' with variables between dollar signs.
+-- The variables will be substituted with real data by the widget.
+--
+-- Available variables:
+--
+-- [@stationPlace@] The name of the weather station
+--
+-- [@stationState@] The state that the weather station is in
+--
+-- [@year@] The year the report was generated
+--
+-- [@month@] The month the report was generated
+--
+-- [@day@] The day the report was generated
+--
+-- [@hour@] The hour the report was generated
+--
+-- [@wind@] The direction and strength of the wind
+--
+-- [@visibility@] Description of current visibility conditions
+--
+-- [@skyCondition@] ?
+--
+-- [@tempC@] The temperature in Celcius
+--
+-- [@tempF@] The temperature in Farenheit
+--
+-- [@dewPoint@] The current dew point
+--
+-- [@humidity@] The current relative humidity
+--
+-- [@pressure@] The current pressure
+--
+--
+-- As an example, a template like
+--
+-- > "$tempF$ °F"
+--
+-- would yield a widget displaying the temperature in Farenheit with a
+-- small label after it.
+--
+-- Implementation Note: the weather data parsing code is taken from
+-- xmobar. This version of the code makes direct HTTP requests
+-- instead of invoking a separate cURL process.
+module System.Taffybar.Weather (
+ -- * Types
+ WeatherConfig(..),
+ WeatherInfo(..),
+ -- * Constructor
+ weatherNew,
+ defaultWeatherConfig
+ ) where
+
+import Network.HTTP
+import Network.URI
+import Graphics.UI.Gtk
+import Text.Parsec
+import Text.Printf
+import Text.StringTemplate
+
+import System.Taffybar.Widgets.PollingLabel
+
+data WeatherInfo =
+ WI { stationPlace :: String
+ , stationState :: String
+ , year :: String
+ , month :: String
+ , day :: String
+ , hour :: String
+ , wind :: String
+ , visibility :: String
+ , skyCondition :: String
+ , tempC :: Int
+ , tempF :: Int
+ , dewPoint :: String
+ , humidity :: Int
+ , pressure :: Int
+ } deriving (Show)
+
+
+-- Parsers stolen from xmobar
+
+type Parser = Parsec String ()
+
+pTime :: Parser (String, String, String, String)
+pTime = do
+ y <- getNumbersAsString
+ _ <- char '.'
+ m <- getNumbersAsString
+ _ <- char '.'
+ d <- getNumbersAsString
+ _ <- char ' '
+ (h:hh:mi:mimi) <- getNumbersAsString
+ _ <- char ' '
+ return (y, m, d ,([h]++[hh]++":"++[mi]++mimi))
+
+pTemp :: Parser (Int, Int)
+pTemp = do
+ let num = digit <|> char '-' <|> char '.'
+ f <- manyTill num $ char ' '
+ _ <- manyTill anyChar $ char '('
+ c <- manyTill num $ char ' '
+ _ <- skipRestOfLine
+ return $ (floor (read c :: Double), floor (read f :: Double))
+
+pRh :: Parser Int
+pRh = do
+ s <- manyTill digit $ (char '%' <|> char '.')
+ return $ read s
+
+pPressure :: Parser Int
+pPressure = do
+ _ <- manyTill anyChar $ char '('
+ s <- manyTill digit $ char ' '
+ _ <- skipRestOfLine
+ return $ read s
+
+parseData :: Parser WeatherInfo
+parseData = do
+ st <- getAllBut ","
+ _ <- space
+ ss <- getAllBut "("
+ _ <- skipRestOfLine >> getAllBut "/"
+ (y,m,d,h) <- pTime
+ w <- getAfterString "Wind: "
+ v <- getAfterString "Visibility: "
+ sk <- getAfterString "Sky conditions: "
+ _ <- skipTillString "Temperature: "
+ (tC,tF) <- pTemp
+ dp <- getAfterString "Dew Point: "
+ _ <- skipTillString "Relative Humidity: "
+ rh <- pRh
+ _ <- skipTillString "Pressure (altimeter): "
+ p <- pPressure
+ _ <- manyTill skipRestOfLine eof
+ return $ WI st ss y m d h w v sk tC tF dp rh p
+
+getAllBut :: String -> Parser String
+getAllBut s =
+ manyTill (noneOf s) (char $ head s)
+
+getAfterString :: String -> Parser String
+getAfterString s = pAfter <|> return ("<" ++ s ++ " not found!>")
+ where
+ pAfter = do
+ _ <- try $ manyTill skipRestOfLine $ string s
+ v <- manyTill anyChar $ newline
+ return v
+
+skipTillString :: String -> Parser String
+skipTillString s =
+ manyTill skipRestOfLine $ string s
+
+getNumbersAsString :: Parser String
+getNumbersAsString = skipMany space >> many1 digit >>= \n -> return n
+
+
+skipRestOfLine :: Parser Char
+skipRestOfLine = do
+ _ <- many $ noneOf "\n\r"
+ newline
+
+
+-- | Simple: download the document at a URL. Taken from Real World
+-- Haskell.
+downloadURL :: String -> IO (Either String String)
+downloadURL url = do
+ resp <- simpleHTTP request
+ case resp of
+ Left x -> return $ Left ("Error connecting: " ++ show x)
+ Right r ->
+ case rspCode r of
+ (2,_,_) -> return $ Right (rspBody r)
+ (3,_,_) -> -- A HTTP redirect
+ case findHeader HdrLocation r of
+ Nothing -> return $ Left (show r)
+ Just url' -> downloadURL url'
+ _ -> return $ Left (show r)
+ where
+ request = Request { rqURI = uri
+ , rqMethod = GET
+ , rqHeaders = []
+ , rqBody = ""
+ }
+ Just uri = parseURI url
+
+getWeather :: String -> IO (Either String WeatherInfo)
+getWeather url = do
+ dat <- downloadURL url
+ case dat of
+ Right dat' -> case parse parseData url dat' of
+ Right d -> return (Right d)
+ Left err -> return (Left (show err))
+ Left err -> return (Left (show err))
+
+defaultFormatter :: StringTemplate String -> WeatherInfo -> String
+defaultFormatter tpl wi = render tpl'
+ where
+ tpl' = setManyAttrib [ ("stationPlace", stationPlace wi)
+ , ("stationState", stationState wi)
+ , ("year", year wi)
+ , ("month", month wi)
+ , ("day", day wi)
+ , ("hour", hour wi)
+ , ("wind", wind wi)
+ , ("visibility", visibility wi)
+ , ("skyCondition", skyCondition wi)
+ , ("tempC", show (tempC wi))
+ , ("tempF", show (tempF wi))
+ , ("dewPoint", dewPoint wi)
+ , ("humidity", show (humidity wi))
+ , ("pressure", show (pressure wi))
+ ] tpl
+
+getCurrentWeather :: String -> StringTemplate String -> WeatherConfig -> IO String
+getCurrentWeather url tpl cfg = do
+ dat <- getWeather url
+ case dat of
+ Right wi -> do
+ case weatherFormatter cfg of
+ DefaultWeatherFormatter -> return (defaultFormatter tpl wi)
+ WeatherFormatter f -> return (f wi)
+ Left err -> do
+ putStrLn err
+ return "N/A"
+
+-- | The NOAA URL to get data from
+baseUrl :: String
+baseUrl = "http://weather.noaa.gov/pub/data/observations/metar/decoded"
+
+-- | A wrapper to allow users to specify a custom weather formatter.
+-- The default interpolates variables into a string as described
+-- above. Custom formatters can do basically anything.
+data WeatherFormatter = WeatherFormatter (WeatherInfo -> String)
+ | DefaultWeatherFormatter
+
+data WeatherConfig =
+ WeatherConfig { weatherStation :: String -- ^ The weather station to poll. No default
+ , weatherTemplate :: String -- ^ Template string, as described above. Default: $tempF$ °F
+ , weatherFormatter :: WeatherFormatter -- ^ Default: substitute in all interpolated variables (above)
+ }
+
+-- | A sensible default configuration for the weather widget that just
+-- renders the temperature.
+defaultWeatherConfig :: String -> WeatherConfig
+defaultWeatherConfig station = WeatherConfig { weatherStation = station
+ , weatherTemplate = "$tempF$ °F"
+ , weatherFormatter = DefaultWeatherFormatter
+ }
+
+-- | Create a periodically-updating weather widget that polls NOAA.
+weatherNew :: WeatherConfig -- ^ Configuration to render
+ -> Double -- ^ Polling period in _minutes_
+ -> IO Widget
+weatherNew cfg delayMinutes = do
+ let url = printf "%s/%s.TXT" baseUrl (weatherStation cfg)
+ tpl' = newSTMP (weatherTemplate cfg)
+
+ l <- pollingLabelNew "N/A" (delayMinutes * 60) (getCurrentWeather url tpl' cfg)
+
+ widgetShowAll l
+ return l
204 src/System/Taffybar/Widgets/Graph.hs
@@ -0,0 +1,204 @@
+-- | This is a graph widget inspired by the widget of the same name in
+-- Awesome (the window manager). It plots a series of data points
+-- similarly to a bar graph. This version must be explicitly fed data
+-- with 'graphAddSample'. For a more automated version, see
+-- 'PollingGraph'.
+--
+-- Like Awesome, this graph can plot multiple data sets in one widget.
+-- The data sets are plotted in the order provided by the caller.
+--
+-- Note: all of the data fed to this widget should be in the range
+-- [0,1].
+module System.Taffybar.Widgets.Graph (
+ -- * Types
+ GraphHandle,
+ GraphConfig(..),
+ -- * Functions
+ graphNew,
+ graphAddSample,
+ defaultGraphConfig
+ ) where
+
+import Prelude hiding ( mapM_ )
+import Control.Concurrent
+import Data.Sequence ( Seq, (<|), viewl, ViewL(..) )
+import Data.Foldable ( mapM_ )
+import qualified Data.Sequence as S
+import Graphics.Rendering.Cairo
+import Graphics.UI.Gtk
+
+newtype GraphHandle = GH (MVar GraphState)
+data GraphState =
+ GraphState { graphIsBootstrapped :: Bool
+ , graphHistory :: [Seq Double]
+ , graphCanvas :: DrawingArea
+ , graphConfig :: GraphConfig
+ }
+
+-- | The configuration options for the graph. The padding is the
+-- number of pixels reserved as blank space around the widget in each
+-- direction.
+data GraphConfig =
+ GraphConfig { graphPadding :: Int -- ^ Number of pixels of padding on each side of the graph widget
+ , graphBackgroundColor :: (Double, Double, Double) -- ^ The background color of the graph (default black)
+ , graphBorderColor :: (Double, Double, Double) -- ^ The border color drawn around the graph (default gray)
+ , graphDataColors :: [(Double, Double, Double, Double)] -- ^ Colors for each data set (default [])
+ , graphHistorySize :: Int -- ^ The number of data points to retain for each data set (default 20)
+ , graphLabel :: Maybe String -- ^ May contain Pango markup (default Nothing)
+ , graphWidth :: Int -- ^ The width (in pixels) of the graph widget (default 50)
+ }
+
+defaultGraphConfig :: GraphConfig
+defaultGraphConfig = GraphConfig { graphPadding = 2
+ , graphBackgroundColor = (0.0, 0.0, 0.0)
+ , graphBorderColor = (0.5, 0.5, 0.5)
+ , graphDataColors = []
+ , graphHistorySize = 20
+ , graphLabel = Nothing
+ , graphWidth = 50
+ }
+
+-- | Add a data point to the graph for each of the tracked data sets.
+-- There should be as many values in the list as there are data sets.
+graphAddSample :: GraphHandle -> [Double] -> IO ()
+graphAddSample (GH mv) rawData = do
+ s <- readMVar mv
+ let drawArea = graphCanvas s
+ histSize = graphHistorySize (graphConfig s)
+ histsAndNewVals = zip pcts (graphHistory s)
+ newHists = case graphHistory s of
+ [] -> map S.singleton pcts
+ _ -> map (\(p,h) -> S.take histSize $ p <| h) histsAndNewVals
+ case graphIsBootstrapped s of
+ False -> return ()
+ True -> do
+ modifyMVar_ mv (\s' -> return s' { graphHistory = newHists })
+ postGUIAsync $ widgetQueueDraw drawArea
+ where
+ pcts = map (clamp 0 1) rawData
+
+clamp :: Double -> Double -> Double -> Double
+clamp lo hi d = max lo $ min hi d
+
+outlineData :: (Double -> Double) -> Double -> Double -> Render ()
+outlineData pctToY xStep pct = do
+ (curX,_) <- getCurrentPoint
+ lineTo (curX + xStep) (pctToY pct)
+
+renderFrameAndBackground :: GraphConfig -> Int -> Int -> Render ()
+renderFrameAndBackground cfg w h = do
+ let (backR, backG, backB) = graphBackgroundColor cfg
+ (frameR, frameG, frameB) = graphBorderColor cfg
+ pad = graphPadding cfg
+ fpad = fromIntegral pad
+ fw = fromIntegral w
+ fh = fromIntegral h
+
+ -- Clear the background to match the bar
+ setSourceRGB 0 0 0
+ rectangle 0 0 fw fh
+ fill
+
+ -- Draw the requested background
+ setSourceRGB backR backG backB
+ rectangle fpad fpad (fw - 2 * fpad) (fh - 2 * fpad)
+ fill
+
+ -- Draw a frame around the widget area
+ setLineWidth 1.0
+ setSourceRGB frameR frameG frameB
+ rectangle fpad fpad (fw - 2 * fpad) (fh - 2 * fpad)
+ stroke
+
+
+renderGraph :: [Seq Double] -> GraphConfig -> Int -> Int -> Double -> Render ()
+renderGraph hists cfg w h xStep = do
+ renderFrameAndBackground cfg w h
+
+ setLineWidth 0.1
+
+
+ let pad = graphPadding cfg
+
+ -- Make the new origin be inside the frame and then scale the
+ -- drawing area so that all operations in terms of width and height
+ -- are inside the drawn frame.
+ translate (fromIntegral pad + 1) (fromIntegral pad + 1)
+ let xS = fromIntegral (w - 2 * pad - 2) / fromIntegral w
+ yS = fromIntegral (h - 2 * pad - 2) / fromIntegral h
+ scale xS yS
+
+ let pctToY pct = fromIntegral h * (1 - pct)
+ histsAndColors = zip hists (graphDataColors cfg)
+ renderDataSet (hist, color)
+ | S.length hist <= 1 = return ()
+ | otherwise = do
+ let (r, g, b, a) = color
+ originY = pctToY newestSample
+ originX = 0
+ newestSample :< hist' = viewl hist
+ setSourceRGBA r g b a
+ moveTo originX originY
+
+ mapM_ (outlineData pctToY xStep) hist'
+ (endX, _) <- getCurrentPoint
+ lineTo endX (fromIntegral h)
+ lineTo 0 (fromIntegral h)
+ fill
+
+
+ mapM_ renderDataSet histsAndColors
+
+drawBorder :: MVar GraphState -> DrawingArea -> IO ()
+drawBorder mv drawArea = do
+ (w, h) <- widgetGetSize drawArea
+ drawWin <- widgetGetDrawWindow drawArea
+ s <- readMVar mv
+ let cfg = graphConfig s
+ renderWithDrawable drawWin (renderFrameAndBackground cfg w h)
+ modifyMVar_ mv (\s' -> return s' { graphIsBootstrapped = True })
+ return ()
+
+drawGraph :: MVar GraphState -> DrawingArea -> IO ()
+drawGraph mv drawArea = do
+ (w, h) <- widgetGetSize drawArea
+ drawWin <- widgetGetDrawWindow drawArea
+ s <- readMVar mv
+ let hist = graphHistory s
+ cfg = graphConfig s
+ histSize = graphHistorySize cfg
+ -- Subtract 1 here since the first data point doesn't require
+ -- any movement in the X direction
+ xStep = fromIntegral w / fromIntegral (histSize - 1)
+
+ case hist of
+ [] -> renderWithDrawable drawWin (renderFrameAndBackground cfg w h)
+ _ -> renderWithDrawable drawWin (renderGraph hist cfg w h xStep)
+
+graphNew :: GraphConfig -> IO (Widget, GraphHandle)
+graphNew cfg = do
+ drawArea <- drawingAreaNew
+ mv <- newMVar GraphState { graphIsBootstrapped = False
+ , graphHistory = []
+ , graphCanvas = drawArea
+ , graphConfig = cfg
+ }
+
+ widgetSetSizeRequest drawArea (graphWidth cfg) (-1)
+ _ <- on drawArea exposeEvent $ tryEvent $ liftIO (drawGraph mv drawArea)
+ _ <- on drawArea realize $ liftIO (drawBorder mv drawArea)
+
+ case graphLabel cfg of
+ Nothing -> do
+ widgetShowAll drawArea
+ return (toWidget drawArea, GH mv)
+ Just lbl -> do
+ l <- labelNew Nothing
+ box <- hBoxNew False 1
+ labelSetMarkup l lbl
+ boxPackStart box l PackNatural 0
+ boxPackStart box drawArea PackGrow 0
+
+ widgetShowAll box
+
+ return (toWidget box, GH mv)
27 src/System/Taffybar/Widgets/PollingBar.hs
@@ -0,0 +1,27 @@
+-- | Like the vertical bar, but this widget automatically updates
+-- itself with a callback at fixed intervals.
+module System.Taffybar.Widgets.PollingBar (
+ -- * Types
+ VerticalBarHandle,
+ BarConfig(..),
+ -- * Constructors and accessors
+ pollingBarNew,
+ defaultBarConfig
+ ) where
+
+import Control.Concurrent
+import Control.Monad ( forever )
+import Graphics.UI.Gtk
+
+import System.Taffybar.Widgets.VerticalBar
+
+pollingBarNew :: BarConfig -> Double -> IO Double -> IO Widget
+pollingBarNew cfg pollSeconds action = do
+ (drawArea, h) <- verticalBarNew cfg
+
+ _ <- forkIO $ forever $ do
+ sample <- action
+ verticalBarSetPercent h sample
+ threadDelay $ floor (pollSeconds * 1000000)
+
+ return drawArea
30 src/System/Taffybar/Widgets/PollingGraph.hs
@@ -0,0 +1,30 @@
+-- | A variant of the Graph widget that automatically updates itself
+-- with a callback at a fixed interval.
+module System.Taffybar.Widgets.PollingGraph (
+ -- * Types
+ GraphHandle,
+ GraphConfig(..),
+ -- * Constructors and accessors
+ pollingGraphNew,
+ defaultGraphConfig
+ ) where
+
+import Control.Concurrent
+import Control.Monad ( forever )
+import Graphics.UI.Gtk
+
+import System.Taffybar.Widgets.Graph
+
+pollingGraphNew :: GraphConfig
+ -> Double
+ -> IO [Double]
+ -> IO Widget
+pollingGraphNew cfg pollSeconds action = do
+ (da, h) <- graphNew cfg
+
+ _ <- forkIO $ forever $ do
+ sample <- action
+ graphAddSample h sample
+ threadDelay $ floor (pollSeconds * 1000000)
+
+ return da
32 src/System/Taffybar/Widgets/PollingLabel.hs
@@ -0,0 +1,32 @@
+-- | This is a simple text widget that updates its contents by calling
+-- a callback at a set interval.
+module System.Taffybar.Widgets.PollingLabel ( pollingLabelNew ) where
+
+import Control.Concurrent ( forkIO, threadDelay )
+import Control.Monad ( forever )
+import Graphics.UI.Gtk
+
+-- | Create a new widget that updates itself at regular intervals. The
+-- function
+--
+-- > updatingLabelNew initialString cmd interval
+--
+-- returns a widget with initial text @initialString@. The widget
+-- forks a thread to update its contents every @interval@ seconds.
+-- The command should return a string with any HTML entities escaped.
+-- This is not checked by the function, since Pango markup shouldn't
+-- be escaped. Proper input sanitization is up to the caller.
+pollingLabelNew :: String -- ^ Initial value for the label
+ -> Double -- ^ Update interval (in seconds)
+ -> IO String -- ^ Command to run to get the input string
+ -> IO Widget
+pollingLabelNew initialString interval cmd = do
+ l <- labelNew Nothing
+ labelSetMarkup l initialString
+
+ _ <- forkIO $ forever $ do
+ str <- cmd
+ postGUIAsync $ labelSetMarkup l str
+ threadDelay $ floor (interval * 1000000)
+
+ return (toWidget l)
129 src/System/Taffybar/Widgets/VerticalBar.hs
@@ -0,0 +1,129 @@
+-- | A vertical bar that can plot data in the range [0, 1]. The
+-- colors are configurable.
+module System.Taffybar.Widgets.VerticalBar (
+ -- * Types
+ VerticalBarHandle,
+ BarConfig(..),
+ -- * Accessors/Constructors
+ verticalBarNew,
+ verticalBarSetPercent,
+ defaultBarConfig
+ ) where
+
+import Control.Concurrent
+import Graphics.Rendering.Cairo
+import Graphics.UI.Gtk
+
+newtype VerticalBarHandle = VBH (MVar VerticalBarState)
+data VerticalBarState =
+ VerticalBarState { barIsBootstrapped :: Bool
+ , barPercent :: Double
+ , barCanvas :: DrawingArea
+ , barConfig :: BarConfig
+ }
+
+data BarConfig =
+ BarConfig { barBorderColor :: (Double, Double, Double) -- ^ Color of the border drawn around the widget
+ , barBackgroundColor :: (Double, Double, Double) -- ^ The background color of the widget
+ , barColor :: Double -> (Double, Double, Double) -- ^ A function to determine the color of the widget for the current data point
+ , barPadding :: Int -- ^ Number of pixels of padding around the widget
+ , barWidth :: Int
+ }
+
+-- | A default bar configuration. The color of the active portion of
+-- the bar must be specified.
+defaultBarConfig :: (Double -> (Double, Double, Double)) -> BarConfig
+defaultBarConfig c = BarConfig { barBorderColor = (0.5, 0.5, 0.5)
+ , barBackgroundColor = (0, 0, 0)
+ , barColor = c
+ , barPadding = 2
+ , barWidth = 15
+ }
+
+verticalBarSetPercent :: VerticalBarHandle -> Double -> IO ()
+verticalBarSetPercent (VBH mv) pct = do
+ s <- readMVar mv
+ let drawArea = barCanvas s
+ case barIsBootstrapped s of
+ False -> return ()
+ True -> do
+ modifyMVar_ mv (\s' -> return s' { barPercent = clamp 0 1 pct })
+ postGUIAsync $ widgetQueueDraw drawArea
+
+clamp :: Double -> Double -> Double -> Double
+clamp lo hi d = max lo $ min hi d
+
+renderFrame :: BarConfig -> Int -> Int -> Render ()
+renderFrame cfg width height = do
+ let fwidth = fromIntegral width
+ fheight = fromIntegral height
+ -- Make a full black background for the whole widget
+ setSourceRGB 0 0 0
+ rectangle 0 0 fwidth fheight
+ fill
+
+ -- Now draw the user's requested background, respecting padding
+ let (bgR, bgG, bgB) = barBackgroundColor cfg
+ pad = barPadding cfg
+ fpad = fromIntegral pad
+ setSourceRGB bgR bgG bgB
+ rectangle fpad fpad (fwidth - 2 * fpad) (fheight - 2 * fpad)
+ fill
+
+ -- Now draw a nice frame
+ let (frameR, frameG, frameB) = barBorderColor cfg
+ setSourceRGB frameR frameG frameB
+ setLineWidth 1.0
+ rectangle fpad fpad (fwidth - 2 * fpad) (fheight - 2 * fpad)
+ stroke
+
+-- renderBar :: Double -> (Double, Double, Double) -> Int -> Int -> Render ()
+renderBar :: Double -> BarConfig -> Int -> Int -> Render ()
+renderBar pct cfg width height = do
+-- renderBar pct (r, g, b) width height = do
+ let activeHeight = pct * (fromIntegral height)
+ activeWidth = fromIntegral width
+ newOrigin = fromIntegral height - activeHeight
+ pad = barPadding cfg
+
+ renderFrame cfg width height
+
+ -- After we draw the frame, transform the coordinate space so that
+ -- we only draw within the frame.
+ translate (fromIntegral pad + 1) (fromIntegral pad + 1)
+ let xS = fromIntegral (width - 2 * pad - 2) / fromIntegral width
+ yS = fromIntegral (height - 2 * pad - 2) / fromIntegral height
+ scale xS yS
+
+ let (r, g, b) = (barColor cfg) pct
+ setSourceRGB r g b
+ translate 0 newOrigin
+ rectangle 0 0 activeWidth activeHeight
+ fill
+
+drawBar :: MVar VerticalBarState -> DrawingArea -> IO ()
+drawBar mv drawArea = do
+ (w, h) <- widgetGetSize drawArea
+ drawWin <- widgetGetDrawWindow drawArea
+ s <- readMVar mv
+ let pct = barPercent s
+ modifyMVar_ mv (\s' -> return s' { barIsBootstrapped = True })
+ renderWithDrawable drawWin (renderBar pct (barConfig s) w h)
+
+verticalBarNew :: BarConfig -> IO (Widget, VerticalBarHandle)
+verticalBarNew cfg = do
+ drawArea <- drawingAreaNew
+
+ mv <- newMVar VerticalBarState { barIsBootstrapped = False
+ , barPercent = 0
+ , barCanvas = drawArea
+ , barConfig = cfg
+ }
+
+ widgetModifyBg drawArea StateNormal (Color 0 0 0)
+
+ widgetSetSizeRequest drawArea (barWidth cfg) (-1)
+ _ <- on drawArea exposeEvent $ tryEvent $ liftIO (drawBar mv drawArea)
+
+ widgetShowAll drawArea
+ return (toWidget drawArea, VBH mv)
58 src/System/Taffybar/XMonadLog.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | This widget listens on DBus for Log events from XMonad and
+-- displays the formatted status string. To log to this widget using
+-- the excellent dbus-core library, use code like the following:
+--
+-- > import DBus.Client.Simple
+-- > main = do
+-- > session <- connectSession
+-- > emit session "/org/xmonad/Log" "org.xmonad.Log" "Update" [toVariant "msg"]
+--
+-- There is a more complete example of xmonad integration in the
+-- top-level module.
+module System.Taffybar.XMonadLog ( xmonadLogNew, dbusLog ) where
+
+import DBus.Client.Simple ( connectSession, emit, Client )
+import DBus.Client ( listen, MatchRule(..) )
+import DBus.Types
+import DBus.Message
+import Graphics.UI.Gtk hiding ( Signal )
+
+import XMonad
+import XMonad.Hooks.DynamicLog
+
+-- | This is a DBus-based logger that can be used from XMonad to log
+-- to this widget.
+dbusLog :: Client -> PP -> X ()
+dbusLog client pp = do
+ dynamicLogWithPP pp { ppOutput = outputThroughDBus client }
+
+outputThroughDBus :: Client -> String -> IO ()
+outputThroughDBus client str =
+ emit client "/org/xmonad/Log" "org.xmonad.Log" "Update" [ toVariant str ]
+
+setupDbus :: Label -> IO ()
+setupDbus w = do
+ let matcher = MatchRule { matchSender = Nothing
+ , matchDestination = Nothing
+ , matchPath = Just "/org/xmonad/Log"
+ , matchInterface = Just "org.xmonad.Log"
+ , matchMember = Just "Update"
+ }
+
+ client <- connectSession
+
+ listen client matcher (callback w)
+
+callback :: Label -> BusName -> Signal -> IO ()
+callback w _ sig = do
+ let [bdy] = signalBody sig
+ Just status = fromVariant bdy
+ postGUIAsync $ labelSetMarkup w status
+
+xmonadLogNew :: IO Widget
+xmonadLogNew = do
+ l <- labelNew Nothing
+ _ <- on l realize $ setupDbus l
+ widgetShowAll l
+ return (toWidget l)
28 src/gdk_property_change_wrapper.c
@@ -0,0 +1,28 @@
+////////////////////////////////////////////////////////////////////////////
+// Copyright : (c) Jan Vornberger 2009
+// License : BSD3-style (see LICENSE)
+//
+// Maintainer : jan.vornberger@informatik.uni-oldenburg.de
+////////////////////////////////////////////////////////////////////////////-
+
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+
+void set_strut_properties(GtkWindow *window,
+ long left, long right, long top, long bottom,
+ long left_start_y, long left_end_y,
+ long right_start_y, long right_end_y,
+ long top_start_x, long top_end_x,
+ long bottom_start_x, long bottom_end_x) {
+ gulong data[12] = {0};
+ data[0] = left; data[1] = right; data[2] = top; data[3] = bottom;
+ data[4] = left_start_y; data[5] = left_end_y;
+ data[6] = right_start_y; data[7] = right_end_y;
+ data[8] = top_start_x; data[9] = top_end_x;
+ data[10] = bottom_start_x; data[11] = bottom_end_x;
+
+ gdk_property_change(GTK_WIDGET(window)->window,
+ gdk_atom_intern("_NET_WM_STRUT_PARTIAL", FALSE),
+ gdk_atom_intern ("CARDINAL", FALSE),
+ 32, GDK_PROP_MODE_REPLACE, (unsigned char *)data, 12);
+}
63 taffybar.cabal
@@ -0,0 +1,63 @@
+name: taffybar
+version: 0.1
+synopsis: A desktop bar similar to xmobar, but with more GUI
+license: BSD3
+license-file: LICENSE
+author: Tristan Ravitch
+maintainer: travitch@cs.wisc.edu
+category: System
+build-type: Simple
+cabal-version: >=1.10
+extra-source-files: README.md,
+ taffybar.hs.example
+
+description: A somewhat fancier desktop bar than xmobar. This bar is based on
+ gtk2hs and provides several widgets (including a few graphical ones).
+ It also sports an optional snazzy system tray.
+
+
+library
+ default-language: Haskell2010
+ build-depends: base > 3 && < 5, time, old-locale, containers, text, HTTP,
+ parsec >= 3.1, mtl >= 2, network, web-encodings, cairo,
+ dbus-core >= 0.9.1 && < 1.0, gtk >= 0.12, dyre >= 0.8.6,
+ HStringTemplate, gtk-traymanager, xmonad-contrib, xmonad
+ hs-source-dirs: src
+ pkgconfig-depends: gtk+-2.0
+ exposed-modules: System.Taffybar,
+ System.Taffybar.XMonadLog,
+ System.Taffybar.Systray,
+ System.Taffybar.SimpleClock,
+ System.Taffybar.FreedesktopNotifications,
+ System.Taffybar.Weather,
+ System.Taffybar.MPRIS,
+ System.Taffybar.Battery,
+ System.Taffybar.Widgets.Graph,
+ System.Taffybar.Widgets.PollingBar,
+ System.Taffybar.Widgets.PollingGraph,
+ System.Taffybar.Widgets.PollingLabel,
+ System.Taffybar.Widgets.VerticalBar,
+ System.Information.Battery,
+ System.Information.Memory,
+ System.Information.CPU
+ other-modules: System.Taffybar.StrutProperties
+
+ c-sources: src/gdk_property_change_wrapper.c
+
+ ghc-options: -Wall -funbox-strict-fields
+ ghc-prof-options: -auto-all
+
+
+executable taffybar
+ default-language: Haskell2010
+ build-depends: base > 3 && < 5, dyre >= 0.8.6, gtk >= 0.12
+ hs-source-dirs: src
+ main-is: Main.hs
+ pkgconfig-depends: gtk+-2.0
+ c-sources: src/gdk_property_change_wrapper.c
+ ghc-options: -Wall -rtsopts -threaded
+ ghc-prof-options: -auto-all
+
+source-repository head
+ type: git
+ location: git://github.com/travitch/taffybar.git
43 taffybar.hs.example
@@ -0,0 +1,43 @@
+import System.Taffybar
+
+import System.Taffybar.Systray
+import System.Taffybar.XMonadLog
+import System.Taffybar.SimpleClock
+import System.Taffybar.FreedesktopNotifications
+import System.Taffybar.Weather
+import System.Taffybar.MPRIS
+
+import System.Taffybar.Widgets.PollingBar
+import System.Taffybar.Widgets.PollingGraph
+
+import System.Information.Memory
+import System.Information.CPU
+
+memCallback = do
+ mi <- parseMeminfo
+ return [memoryUsedRatio mi]
+
+cpuCallback = do
+ (userLoad, systemLoad, totalLoad) <- cpuLoad
+ return [totalLoad, systemLoad]
+
+main = do
+ let memCfg = defaultGraphConfig { graphDataColors = [(1, 0, 0, 1)]
+ , graphLabel = Just "mem"
+ }
+ cpuCfg = defaultGraphConfig { graphDataColors = [ (0, 1, 0, 1)
+ , (1, 0, 1, 0.5)
+ ]
+ , graphLabel = Just "cpu"
+ }
+ let clock = textClockNew Nothing "<span fgcolor='orange'>%a %b %_d %H:%M</span>" 1
+ log = xmonadLogNew
+ note = notifyAreaNew defaultNotificationConfig
+ wea = weatherNew (defaultWeatherConfig "KMSN") 10
+ mpris = mprisNew
+ mem = pollingGraphNew memCfg 1 memCallback
+ cpu = pollingGraphNew cpuCfg 0.5 cpuCallback
+ tray = systrayNew
+ defaultTaffybar defaultTaffybarConfig { startWidgets = [ log, note ]
+ , endWidgets = [ tray, wea, clock, mem, cpu, mpris ]
+ }
Please sign in to comment.
Something went wrong with that request. Please try again.