428 lines
11 KiB
Haskell
428 lines
11 KiB
Haskell
module Main (main) where
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- | Xmobar binary
|
|
--
|
|
-- Features:
|
|
-- * Uses the 'UnsafeStdinReader' to receive the current workspace/layout config
|
|
-- from xmonad
|
|
-- * FontAwesome and Symbol fonts for icons
|
|
-- * Some custom plugins (imported below)
|
|
-- * Theme integration with xmonad (shared module imported below)
|
|
-- * A custom Locks plugin from my own forked repo
|
|
|
|
import Control.Monad
|
|
|
|
import Data.Either
|
|
import Data.List
|
|
import Data.Maybe
|
|
|
|
import DBus.Client
|
|
import DBus.Internal
|
|
|
|
import System.Directory
|
|
import System.Exit
|
|
import System.IO
|
|
import System.IO.Error
|
|
import System.Process
|
|
( readProcessWithExitCode
|
|
)
|
|
|
|
import Xmobar.Plugins.Bluetooth
|
|
import Xmobar.Plugins.ClevoKeyboard
|
|
import Xmobar.Plugins.Device
|
|
import Xmobar.Plugins.IntelBacklight
|
|
import Xmobar.Plugins.Screensaver
|
|
import Xmobar.Plugins.VPN
|
|
|
|
import XMonad.Core
|
|
( cfgDir
|
|
, getDirectories
|
|
)
|
|
import XMonad.Hooks.DynamicLog (wrap)
|
|
import XMonad.Internal.Command.Power (hasBattery)
|
|
import XMonad.Internal.DBus.Brightness.ClevoKeyboard
|
|
import XMonad.Internal.DBus.Brightness.IntelBacklight
|
|
import XMonad.Internal.DBus.Screensaver (ssSignalDep)
|
|
import XMonad.Internal.Dependency
|
|
import XMonad.Internal.Shell
|
|
import qualified XMonad.Internal.Theme as T
|
|
import Xmobar
|
|
import Xmobar.Plugins.Common
|
|
|
|
main :: IO ()
|
|
main = do
|
|
sysClient <- getDBusClient True
|
|
sesClient <- getDBusClient False
|
|
cs <- getAllCommands =<< rightPlugins sysClient sesClient
|
|
d <- cfgDir <$> getDirectories
|
|
-- this is needed to see any printed messages
|
|
hFlush stdout
|
|
mapM_ (maybe skip disconnect) [sysClient, sesClient]
|
|
xmobar $ config cs d
|
|
|
|
config :: BarRegions -> String -> Config
|
|
config br confDir = defaultConfig
|
|
-- TODO head makes me feel icky
|
|
{ font = head allFontStrings
|
|
, additionalFonts = drop 1 allFontStrings
|
|
, textOffset = head allFontOffsets
|
|
, textOffsets = drop 1 allFontOffsets
|
|
, bgColor = T.bgColor
|
|
, fgColor = T.fgColor
|
|
, position = BottomSize C 100 24
|
|
, border = NoBorder
|
|
, borderColor = T.bordersColor
|
|
|
|
, sepChar = pSep
|
|
, alignSep = [lSep, rSep]
|
|
, template = fmtRegions br
|
|
|
|
, lowerOnStart = False
|
|
, hideOnStart = False
|
|
, allDesktops = True
|
|
, overrideRedirect = True
|
|
, pickBroadest = False
|
|
, persistent = True
|
|
-- store the icons with the xmonad/xmobar stack project
|
|
, iconRoot = confDir ++ "/icons"
|
|
|
|
, commands = csRunnable <$> concatRegions br
|
|
}
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- | command specifications
|
|
|
|
data BarRegions = BarRegions
|
|
{ brLeft :: [CmdSpec]
|
|
, brCenter :: [CmdSpec]
|
|
, brRight :: [CmdSpec]
|
|
} deriving Show
|
|
|
|
data CmdSpec = CmdSpec
|
|
{ csAlias :: String
|
|
, csRunnable :: Runnable
|
|
} deriving Show
|
|
|
|
concatRegions :: BarRegions -> [CmdSpec]
|
|
concatRegions (BarRegions l c r) = l ++ c ++ r
|
|
|
|
wirelessCmd :: String -> CmdSpec
|
|
wirelessCmd iface = CmdSpec
|
|
{ csAlias = iface ++ "wi"
|
|
, csRunnable = Run
|
|
$ Wireless iface
|
|
[ "-t", "<qualityipat><essid>"
|
|
, "--"
|
|
, "--quality-icon-pattern", "<icon=wifi_%%.xpm/>"
|
|
] 5
|
|
}
|
|
|
|
ethernetCmd :: String -> CmdSpec
|
|
ethernetCmd iface = CmdSpec
|
|
{ csAlias = iface
|
|
, csRunnable = Run
|
|
$ Device (iface, fontifyText IconMedium "\xf0e8", colors)
|
|
}
|
|
|
|
batteryCmd :: CmdSpec
|
|
batteryCmd = CmdSpec
|
|
{ csAlias = "battery"
|
|
, csRunnable = Run
|
|
$ Battery
|
|
[ "--template", "<acstatus><left>"
|
|
, "--Low", "10"
|
|
, "--High", "80"
|
|
, "--low", "red"
|
|
, "--normal", T.fgColor
|
|
, "--high", T.fgColor
|
|
, "--"
|
|
, "-a", notify
|
|
, "-P"
|
|
, "-o" , fontify "\xf0e7"
|
|
, "-O" , fontify "\xf1e6"
|
|
, "-i" , fontify "\xf1e6"
|
|
] 50
|
|
}
|
|
where
|
|
fontify = fontifyText IconSmall
|
|
-- TODO format this with standardized notification library from xmonad.internal
|
|
notify = fmtCmd "notify-send"
|
|
[ "-u"
|
|
, "critical"
|
|
, "-i"
|
|
, "'dialog-information-symbolic'"
|
|
, "'low battery'"
|
|
]
|
|
|
|
vpnCmd :: CmdSpec
|
|
vpnCmd = CmdSpec
|
|
{ csAlias = vpnAlias
|
|
, csRunnable = Run $ VPN (fontifyText IconMedium "\xf023", colors)
|
|
}
|
|
|
|
btCmd :: CmdSpec
|
|
btCmd = CmdSpec
|
|
{ csAlias = btAlias
|
|
, csRunnable = Run
|
|
$ Bluetooth (fontify "\xf5b0", fontify "\xf5ae") colors
|
|
}
|
|
where
|
|
fontify = fontifyText IconLarge
|
|
|
|
alsaCmd :: CmdSpec
|
|
alsaCmd = CmdSpec
|
|
{ csAlias = "alsa:default:Master"
|
|
, csRunnable = Run
|
|
$ Alsa "default" "Master"
|
|
[ "-t", "<status><volume>%"
|
|
, "--"
|
|
, "-O", fontifyText IconSmall "\xf028"
|
|
, "-o", fontifyText IconSmall "\xf026 "
|
|
, "-c", T.fgColor
|
|
, "-C", T.fgColor
|
|
]
|
|
}
|
|
|
|
blCmd :: CmdSpec
|
|
blCmd = CmdSpec
|
|
{ csAlias = blAlias
|
|
, csRunnable = Run $ IntelBacklight $ fontifyText IconSmall "\xf185"
|
|
}
|
|
|
|
ckCmd :: CmdSpec
|
|
ckCmd = CmdSpec
|
|
{ csAlias = ckAlias
|
|
, csRunnable = Run $ ClevoKeyboard $ fontifyText IconSmall "\xf40b"
|
|
}
|
|
|
|
ssCmd :: CmdSpec
|
|
ssCmd = CmdSpec
|
|
{ csAlias = ssAlias
|
|
, csRunnable = Run
|
|
$ Screensaver (fontifyText IconSmall "\xf254", colors)
|
|
}
|
|
|
|
lockCmd :: CmdSpec
|
|
lockCmd = CmdSpec
|
|
{ csAlias = "locks"
|
|
, csRunnable = Run
|
|
$ Locks
|
|
[ "-N", numIcon
|
|
, "-n", disabledColor numIcon
|
|
, "-C", capIcon
|
|
, "-c", disabledColor capIcon
|
|
, "-s", ""
|
|
, "-S", ""
|
|
, "-d", " "
|
|
]
|
|
}
|
|
where
|
|
numIcon = fontify "\xf8a5"
|
|
capIcon = fontify "\xf657"
|
|
fontify = fontifyText IconXLarge
|
|
disabledColor = xmobarFGColor T.backdropFgColor
|
|
|
|
dateCmd :: CmdSpec
|
|
dateCmd = CmdSpec
|
|
{ csAlias = "date"
|
|
, csRunnable = Run $ Date "%Y-%m-%d %H:%M:%S " "date" 10
|
|
}
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- | command runtime checks and setup
|
|
--
|
|
-- some commands depend on the presence of interfaces that can only be
|
|
-- determined at runtime; define these checks here
|
|
--
|
|
-- in the case of network interfaces, assume that the system uses systemd in
|
|
-- which case ethernet interfaces always start with "en" and wireless
|
|
-- interfaces always start with "wl"
|
|
|
|
type BarFeature = Feature CmdSpec
|
|
|
|
isWireless :: String -> Bool
|
|
isWireless ('w':'l':_) = True
|
|
isWireless _ = False
|
|
|
|
isEthernet :: String -> Bool
|
|
isEthernet ('e':'n':_) = True
|
|
isEthernet _ = False
|
|
|
|
listInterfaces :: IO [String]
|
|
listInterfaces = fromRight [] <$> tryIOError (listDirectory sysfsNet)
|
|
|
|
sysfsNet :: FilePath
|
|
sysfsNet = "/sys/class/net"
|
|
|
|
readInterface :: (String -> Bool) -> IO (Either [String] String)
|
|
readInterface f = do
|
|
ns <- filter f <$> listInterfaces
|
|
case ns of
|
|
[] -> return $ Left ["no interfaces found"]
|
|
(x:xs) -> do
|
|
unless (null xs) $
|
|
putStrLn $ "WARNING: extra interfaces found, using " ++ x
|
|
return $ Right x
|
|
|
|
vpnPresent :: IO (Maybe String)
|
|
vpnPresent = do
|
|
res <- tryIOError $ readProcessWithExitCode "nmcli" args ""
|
|
-- TODO provide some error messages
|
|
return $ case res of
|
|
(Right (ExitSuccess, out, _)) -> if "vpn" `elem` lines out then Nothing else Just "vpn not found"
|
|
_ -> Just "puke"
|
|
where
|
|
args = ["-c", "no", "-t", "-f", "TYPE", "c", "show"]
|
|
|
|
rightPlugins :: Maybe Client -> Maybe Client -> IO [MaybeAction CmdSpec]
|
|
rightPlugins sysClient sesClient = mapM evalFeature
|
|
[ getWireless
|
|
, getEthernet sysClient
|
|
, getVPN sysClient
|
|
, getBt sysClient
|
|
, getAlsa
|
|
, getBattery
|
|
, getBl sesClient
|
|
, getCk sesClient
|
|
, getSs sesClient
|
|
, ConstFeature lockCmd
|
|
, ConstFeature dateCmd
|
|
]
|
|
|
|
getWireless :: BarFeature
|
|
getWireless = feature "wireless status indicator" Default
|
|
-- TODO this is stupid
|
|
$ GenTree (Double wirelessCmd $ readInterface isWireless) (Only $ exe "ls")
|
|
|
|
getEthernet :: Maybe Client -> BarFeature
|
|
getEthernet client = feature "ethernet status indicator" Default
|
|
$ DBusTree action client (Only $ fullDep devDep)
|
|
where
|
|
action = Double (\i _ -> ethernetCmd i) (readInterface isEthernet)
|
|
|
|
getBattery :: BarFeature
|
|
getBattery = feature "battery level indicator" Default
|
|
$ GenTree (Single batteryCmd) (Only $ fullDep $ IOTest desc hasBattery)
|
|
where
|
|
desc = "Test if battery is present"
|
|
|
|
getVPN :: Maybe Client -> BarFeature
|
|
getVPN client = feature "VPN status indicator" Default
|
|
$ DBusTree (Single (const vpnCmd)) client $ And (Only $ fullDep vpnDep) (Only dp)
|
|
where
|
|
dp = fullDep $ DBusGenDep $ IOTest desc vpnPresent
|
|
desc = "Use nmcli to test if VPN is present"
|
|
|
|
getBt :: Maybe Client -> BarFeature
|
|
getBt client = feature "bluetooth status indicator" Default
|
|
$ DBusTree (Single (const btCmd)) client (Only $ fullDep btDep)
|
|
|
|
getAlsa :: BarFeature
|
|
getAlsa = feature "volume level indicator" Default
|
|
$ GenTree (Single alsaCmd) (Only $ exe "alsactl")
|
|
|
|
getBl :: Maybe Client -> BarFeature
|
|
getBl client = feature "Intel backlight indicator" Default
|
|
$ DBusTree (Single (const blCmd)) client (Only $ fullDep intelBacklightSignalDep)
|
|
|
|
getCk :: Maybe Client -> BarFeature
|
|
getCk client = feature "Clevo keyboard indicator" Default
|
|
$ DBusTree (Single (const ckCmd)) client (Only $ fullDep clevoKeyboardSignalDep)
|
|
|
|
getSs :: Maybe Client -> BarFeature
|
|
getSs client = feature "screensaver indicator" Default
|
|
$ DBusTree (Single (const ssCmd)) client (Only $ fullDep ssSignalDep)
|
|
|
|
getAllCommands :: [MaybeAction CmdSpec] -> IO BarRegions
|
|
getAllCommands right = do
|
|
let left =
|
|
[ CmdSpec
|
|
{ csAlias = "UnsafeStdinReader"
|
|
, csRunnable = Run UnsafeStdinReader
|
|
}
|
|
]
|
|
return $ BarRegions
|
|
{ brLeft = left
|
|
, brCenter = []
|
|
, brRight = catMaybes right
|
|
}
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- | fonts
|
|
|
|
data Font = Text
|
|
| IconSmall
|
|
| IconMedium
|
|
| IconLarge
|
|
| IconXLarge
|
|
deriving (Eq, Enum, Bounded, Show)
|
|
|
|
-- font data ~ (offset, fontification string)
|
|
fontData :: Font -> (Int, String)
|
|
fontData Text = (16, barFont)
|
|
fontData IconSmall = (16, nerdFont 13)
|
|
fontData IconMedium = (17, nerdFont 15)
|
|
fontData IconLarge = (17, nerdFont 18)
|
|
fontData IconXLarge = (18, nerdFont 20)
|
|
|
|
fontString :: Font -> String
|
|
fontString = snd . fontData
|
|
|
|
fontOffset :: Font -> Int
|
|
fontOffset = fst . fontData
|
|
|
|
allFonts :: [Font]
|
|
allFonts = enumFrom minBound
|
|
|
|
allFontOffsets :: [Int]
|
|
allFontOffsets = fontOffset <$> allFonts
|
|
|
|
allFontStrings :: [String]
|
|
allFontStrings = fontString <$> allFonts
|
|
|
|
barFont :: String
|
|
barFont = T.fmtFontXFT T.font
|
|
{ T.family = "DejaVu Sans Mono"
|
|
, T.size = Just 11
|
|
, T.weight = Just T.Bold
|
|
}
|
|
|
|
nerdFont :: Int -> String
|
|
nerdFont size = T.fmtFontXFT T.font
|
|
{ T.family = "Symbols Nerd Font"
|
|
, T.size = Nothing
|
|
, T.pixelsize = Just size
|
|
}
|
|
|
|
fontifyText :: Font -> String -> String
|
|
fontifyText fnt txt = concat ["<fn=", show $ fromEnum fnt, ">", txt, "</fn>"]
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- | various formatting things
|
|
|
|
colors :: Colors
|
|
colors = Colors { colorsOn = T.fgColor, colorsOff = T.backdropFgColor }
|
|
|
|
sep :: String
|
|
sep = xmobarFGColor T.backdropFgColor " : "
|
|
|
|
lSep :: Char
|
|
lSep = '}'
|
|
|
|
rSep :: Char
|
|
rSep = '{'
|
|
|
|
pSep :: String
|
|
pSep = "%"
|
|
|
|
fmtSpecs :: [CmdSpec] -> String
|
|
fmtSpecs = intercalate sep . fmap go
|
|
where
|
|
go CmdSpec { csAlias = a } = wrap pSep pSep a
|
|
|
|
fmtRegions :: BarRegions -> String
|
|
fmtRegions BarRegions { brLeft = l, brCenter = c, brRight = r } =
|
|
fmtSpecs l ++ [lSep] ++ fmtSpecs c ++ [rSep] ++ fmtSpecs r
|
|
|