content: -----------------------------------------------------------------------------
  -- |
  -- Module      :  XMonad.Util.Loggers
  -- Copyright   :  (c) Brent Yorgey, Wirt Wolff
  -- License     :  BSD-style (see LICENSE)
  --
  -- Maintainer  :  <byorgey@gmail.com>
  -- Stability   :  unstable
  -- Portability :  unportable
  --
  -- A collection of simple logger functions and formatting utilities
  -- which can be used in the 'XMonad.Hooks.DynamicLog.ppExtras' field of
  -- a pretty-printing status logger format. See "XMonad.Hooks.DynamicLog"
  -- for more information.
  -----------------------------------------------------------------------------
  
  module XMonad.Util.Loggers (
      -- * Usage
      -- $usage
  
        Logger
  
      -- * System Loggers
      -- $system
      , aumixVolume
      , battery
      , date
      , loadAvg
      , maildirNew, maildirUnread
      , logCmd , logFileCount
  
      -- * XMonad Loggers
      -- $xmonad
      , logCurrent, logLayout, logTitle
  
      -- * Formatting Utilities
      -- $format
      , onLogger
      , wrapL, fixedWidthL
      , logSp, padL
      , shortenL
      , dzenColorL, xmobarColorL
  
      , (<$>)
  
    ) where
  
  import XMonad (liftIO)
  import XMonad.Core
  import qualified XMonad.StackSet as W
  import XMonad.Hooks.DynamicLog
  import XMonad.Util.Font (Align (..))
  import XMonad.Util.NamedWindows (getName)
  
  import Control.Applicative ((<$>))
  import Control.Exception as E
  import Data.List (isPrefixOf, isSuffixOf)
  import Data.Maybe (fromMaybe)
  import Data.Traversable (traverse)
  import System.Directory (getDirectoryContents)
  import System.IO
  import System.Locale
  import System.Process (runInteractiveCommand)
  import System.Time
  
  econst :: Monad m => a -> IOException -> m a
  econst = const . return
  
  -- $usage
  -- Use this module by importing it into your @~\/.xmonad\/xmonad.hs@:
  --
  -- > import XMonad.Util.Loggers
  --
  -- Then, add one or more loggers to the
  -- 'XMonad.Hooks.DynamicLog.ppExtras' field of your
  -- 'XMonad.Hooks.DynamicLoc.PP', possibly with extra formatting .
  -- For example:
  --
  -- >   -- display load averages and a pithy quote along with xmonad status.
  -- >   , logHook = dynamicLogWithPP $ def {
  -- >                  ppExtras = [ padL loadAvg, logCmd "fortune -n 40 -s" ]
  -- >                }
  -- >   -- gives something like " 3.27 3.52 3.26 Drive defensively.  Buy a tank."
  --
  -- See the formatting section below for another example using
  -- a @where@ block to define some formatted loggers for a top-level
  -- @myLogHook@.
  --
  -- Loggers are named either for their function, as in 'battery',
  -- 'aumixVolume', and 'maildirNew', or are prefixed with \"log\" when
  -- making use of other functions or by analogy with the pp* functions.
  -- For example, the logger version of 'XMonad.Hooks.DynamicLog.ppTitle'
  -- is 'logTitle', and 'logFileCount' loggerizes the result of file
  -- counting code.
  --
  -- Formatting utility names are generally as short as possible and
  -- carry the suffix \"L\". For example, the logger version of
  -- 'XMonad.Hooks.DynamicLog.shorten' is 'shortenL'.
  --
  -- Of course, there is nothing really special about these so-called
  -- \"loggers\": they are just @X (Maybe String)@ actions.  So you can
  -- use them anywhere you would use an @X (Maybe String)@, not just
  -- with DynamicLog.
  --
  -- Additional loggers welcome!
  
  
  
  -- | 'Logger' is just a convenient synonym for @X (Maybe String)@.
  type Logger = X (Maybe String)
  
  -- $system
  
  -- | Get the current volume with @aumix@. <http://jpj.net/~trevor/aumix.html>
  aumixVolume :: Logger
  aumixVolume = logCmd "aumix -vq"
  
  -- | Get the battery status (percent charge and charging\/discharging
  --   status). This is an ugly hack and may not work for some people.
  --   At some point it would be nice to make this more general\/have
  --   fewer dependencies (assumes @\/usr\/bin\/acpi@ and @sed@ are installed.)
  battery :: Logger
  battery = logCmd "/usr/bin/acpi | sed -r 's/.*?: (.*%).*/\\1/; s/[dD]ischarging, ([0-9]+%)/\\1-/; s/[cC]harging, ([0-9]+%)/\\1+/; s/[cC]harged, //'"
  
  -- | Get the current date and time, and format them via the
  --   given format string.  The format used is the same as that used
  --   by the C library function strftime; for example,
  --   @date \"%a %b %d\"@ might display something like @Tue Feb 19@.
  --   For more information see something like
  --   <http://www.cplusplus.com/reference/clibrary/ctime/strftime.html>.
  date :: String -> Logger
  date fmt = io $ do cal <- (getClockTime >>= toCalendarTime)
                     return . Just $ formatCalendarTime defaultTimeLocale fmt cal
  
  -- | Get the load average.  This assumes that you have a
  --   utility called @\/usr\/bin\/uptime@ and that you have @sed@
  --   installed; these are fairly common on GNU\/Linux systems but it
  --   would be nice to make this more general.
  loadAvg :: Logger
  loadAvg = logCmd "/usr/bin/uptime | sed 's/.*: //; s/,//g'"
  
  -- | Create a 'Logger' from an arbitrary shell command.
  logCmd :: String -> Logger
  logCmd c = io $ do (_, out, _, _) <- runInteractiveCommand c
                     fmap Just (hGetLine out) `E.catch` econst Nothing
                     -- no need to waitForProcess, we ignore SIGCHLD
  
  -- | Get a count of filtered files in a directory.
  -- See 'maildirUnread' and 'maildirNew' source for usage examples.
  logFileCount :: FilePath          -- ^ directory in which to count files
               -> (String -> Bool)  -- ^ predicate to match if file should be counted
               -> Logger
  logFileCount d p = do
      c <- liftIO ( getDirectoryContents d)
      let n = length $ Prelude.filter p c
      case n of
          0 -> return Nothing
          _ -> return $ Just $ show n
  
  -- | Get a count of unread mails in a maildir. For maildir format
  -- details, to write loggers for other classes of mail, see
  -- <http://cr.yp.to/proto/maildir.html> and 'logFileCount'.
  maildirUnread :: FilePath -> Logger
  maildirUnread mdir = logFileCount (mdir ++ "/cur/") (isSuffixOf ",")
  
  -- | Get a count of new mails in a maildir.
  maildirNew :: FilePath -> Logger
  maildirNew mdir = logFileCount (mdir ++ "/new/") (not . isPrefixOf ".")
  
  -- $xmonad
  --
  -- A very small sample of what you can log since you have access to X. For
  -- example you can loggerize the number of windows on each workspace, or
  -- titles on other workspaces, or the id of the previously focused workspace....
  
  -- | Get the title (name) of the focused window.
  logTitle :: Logger
  logTitle = withWindowSet $ traverse (fmap show . getName) . W.peek
  
  -- | Get the name of the current layout.
  logLayout :: Logger
  logLayout = withWindowSet $ return . Just . ld
    where ld = description . W.layout . W.workspace . W.current
  
  -- | Get the name of the current workspace.
  logCurrent :: Logger
  logCurrent = withWindowSet $ return . Just . W.currentTag
  
  -- $format
  -- Combine logger formatting functions to make your
  -- 'XMonad.Hooks.DynamicLog.ppExtras' more colorful and readable.
  -- (For convenience this module exports 'Control.Applicative.<$>' to
  -- use instead of \'.\' or \'$\' in hard to read formatting lines.
  -- For example:
  --
  -- > myLogHook = dynamicLogWithPP def {
  -- >     -- skipped
  -- >     , ppExtras = [lLoad, lTitle, logSp 3, wrapL "[" "]" $ date "%a %d %b"]
  -- >     , ppOrder = \(ws,l,_,xs) -> [l,ws] ++ xs
  -- >     }
  -- >   where
  -- >     -- lTitle = fixedWidthL AlignCenter "." 99 . dzenColorL "cornsilk3" "" . padL . shortenL 80 $ logTitle
  -- >     -- or something like:
  -- >     lTitle = fixedWidthL AlignCenter "." 99 <$> dzenColorL "cornsilk3" "" <$> padL . shortenL 80 $ logTitle
  -- >
  -- >     lLoad = dzenColorL "#6A5ACD" "" . wrapL loadIcon "   " . padL $ loadAvg
  -- >     loadIcon = " ^i(/home/me/.dzen/icons/load.xbm)"
  --
  -- Note: When applying 'shortenL' or 'fixedWidthL' to logger strings
  -- containing colors or other formatting commands, apply the formatting
  -- /after/ the length adjustment, or include \"invisible\" characters
  -- in the length specification, e.g. in the above \'^fg(cornsilk3)\' and
  -- \'^fg()' yields 19 invisible and 80 visible characters.
  
  -- | Use a string formatting function to edit a 'Logger' string.
  -- For example, to create a tag function to prefix or label loggers,
  -- as in \'tag: output\', use:
  --
  -- > tagL l = onLogger $ wrap (l ++ ": ") ""
  -- >
  -- >    tagL "bat" battery
  -- >    tagL "load" loadAvg
  --
  -- If you already have a (String -> String) function you want to
  -- apply to a logger:
  --
  -- > revL = onLogger trim
  --
  -- See formatting utility source code for more 'onLogger' usage examples.
  onLogger :: (String -> String) -> Logger -> Logger
  onLogger = fmap . fmap
  
  -- | Wrap a logger's output in delimiters, unless it is @X (Nothing)@
  -- or @X (Just \"\")@. Some examples:
  --
  -- >    wrapL " | " " | " (date "%a %d %b") -- ' | Tue 19 Feb | '
  -- >
  -- >    wrapL "bat: " "" battery            -- ' bat: battery_logger_output'
  wrapL :: String -> String -> Logger -> Logger
  wrapL l r = onLogger $ wrap l r
  
  -- | Make a logger's output constant width by padding with the given string,
  -- /even if the logger is/ @X (Nothing)@ /or/ @X (Just \"\")@. Useful to
  -- reduce visual noise as a title logger shrinks and grows, to use a fixed
  -- width for a logger that sometimes becomes Nothing, or even to create
  -- fancy spacers or character based art effects.
  --
  -- It fills missing logger output with a repeated character like \".\",
  -- \":\" or pattern, like \" -.-\". The cycling padding string is reversed on
  -- the left of the logger output. This is mainly useful with AlignCenter.
  fixedWidthL :: Align  -- ^ AlignCenter, AlignRight, or AlignLeft
              -> String -- ^ String to cycle to pad missing logger output
              -> Int    -- ^ Fixed length to output (including invisible formatting characters)
              -> Logger -> Logger
  fixedWidthL a str n logger = do
      mbl <- logger
      let l = fromMaybe "" mbl
      case a of
         AlignCenter -> toL (take n $ padhalf l ++ l ++ cs)
         AlignRight -> toL (reverse (take n $ reverse l ++ cs))
         _ -> toL (take n $ l ++ cs)
    where
      toL = return . Just
      cs  = cycle str
      padhalf x = reverse $ take ((n - length x) `div` 2) cs
  
  -- | Create a \"spacer\" logger, e.g. @logSp 3 -- loggerizes \'   \'@.
  -- For more complex \"spacers\", use 'fixedWidthL' with @return Nothing@.
  logSp :: Int -> Logger
  logSp n = return . Just . take n $ cycle " "
  
  -- | Pad a logger's output with a leading and trailing space, unless it
  -- is @X (Nothing)@ or @X (Just \"\")@.
  padL :: Logger -> Logger
  padL = onLogger pad
  
  -- | Limit a logger's length, adding \"...\" if truncated.
  shortenL :: Int -> Logger -> Logger
  shortenL = onLogger . shorten
  
  -- | Color a logger's output with dzen foreground and background colors.
  --
  -- >  dzenColorL "green" "#2A4C3F" battery
  dzenColorL :: String -> String -> Logger -> Logger
  dzenColorL fg bg = onLogger $ dzenColor fg bg
  
  -- | Color a logger's output with xmobar foreground and background colors.
  --
  -- >  xmobarColorL "#6A5ACD" "gray6" loadAverage
  xmobarColorL :: String -> String -> Logger -> Logger
  xmobarColorL fg bg = onLogger $ xmobarColor fg bg
  
  -- todo
  -- * dynamicLogXinerama logger? Or sorted onscreen Id's with "current" indicator?
  -- is logCurrent really useful at all?
  --
  -- * ppVisible, etc. Resolve code dup. somehow. Refactor DynamicLog so can
  -- be used for regular PP stuff /and/ loggers?
  --
  -- * fns for "ppExtras as a whole", combine loggers more nicely.
  --
  -- * parsers  to use with fixedWidthL to be smarter about invisible characters?
  