module System.Taffybar.Information.XDG.Protocol
( XDGMenu(..)
, DesktopEntryCondition(..)
, getApplicationEntries
, getDirectoryDirs
, getPreferredLanguages
, getXDGDesktop
, getXDGMenuFilenames
, matchesCondition
, readXDGMenu
) where
import Control.Applicative
import Control.Monad.Trans.Class
import Control.Monad.Trans.Maybe
import Data.Char (toLower)
import Data.List
import Data.Maybe
import qualified Debug.Trace as D
import GHC.IO.Encoding
import Prelude
import Safe (headMay)
import System.Directory
import System.Environment
import System.FilePath.Posix
import System.Log.Logger
import System.Taffybar.Information.XDG.DesktopEntry
import System.Taffybar.Util
import Text.XML.Light
import Text.XML.Light.Helpers
getXDGMenuPrefix :: IO (Maybe String)
getXDGMenuPrefix = lookupEnv "XDG_MENU_PREFIX"
getXDGMenuFilenames
:: Maybe String
-> IO [FilePath]
getXDGMenuFilenames mMenuPrefix = do
configDirs <- getXdgDirectoryList XdgConfigDirs
maybePrefix <- (mMenuPrefix <|>) <$> getXDGMenuPrefix
let maybeAddDash t = if last t == '-' then t else t ++ "-"
dashedPrefix = maybe "" maybeAddDash maybePrefix
return $ map (</> "menus" </> dashedPrefix ++ "applications.menu") configDirs
data XDGMenu = XDGMenu
{ xmAppDir :: Maybe String
, xmDefaultAppDirs :: Bool
, xmDirectoryDir :: Maybe String
, xmDefaultDirectoryDirs :: Bool
, xmLegacyDirs :: [String]
, xmName :: String
, xmDirectory :: String
, xmOnlyUnallocated :: Bool
, xmDeleted :: Bool
, xmInclude :: Maybe DesktopEntryCondition
, xmExclude :: Maybe DesktopEntryCondition
, xmSubmenus :: [XDGMenu]
, xmLayout :: [XDGLayoutItem]
} deriving (Show)
data XDGLayoutItem =
XliFile String | XliSeparator | XliMenu String | XliMerge String
deriving(Show)
getApplicationEntries
:: [String]
-> XDGMenu
-> IO [DesktopEntry]
getApplicationEntries langs xm = do
defEntries <- if xmDefaultAppDirs xm
then do dataDirs <- getXDGDataDirs
concat <$> mapM (listDesktopEntries ".desktop" .
(</> "applications")) dataDirs
else return []
return $ sortBy (\de1 de2 -> compare (map toLower (deName langs de1))
(map toLower (deName langs de2))) defEntries
parseMenu :: Element -> Maybe XDGMenu
parseMenu elt =
let appDir = getChildData "AppDir" elt
defaultAppDirs = isJust $ getChildData "DefaultAppDirs" elt
directoryDir = getChildData "DirectoryDir" elt
defaultDirectoryDirs = isJust $ getChildData "DefaultDirectoryDirs" elt
name = fromMaybe "Name?" $ getChildData "Name" elt
dir = fromMaybe "Dir?" $ getChildData "Directory" elt
onlyUnallocated =
case ( getChildData "OnlyUnallocated" elt
, getChildData "NotOnlyUnallocated" elt) of
(Nothing, Nothing) -> False
(Nothing, Just _) -> False
(Just _, Nothing) -> True
(Just _, Just _) -> False
deleted = False
include = parseConditions "Include" elt
exclude = parseConditions "Exclude" elt
layout = parseLayout elt
subMenus = fromMaybe [] $ mapChildren "Menu" elt parseMenu
in Just
XDGMenu
{ xmAppDir = appDir
, xmDefaultAppDirs = defaultAppDirs
, xmDirectoryDir = directoryDir
, xmDefaultDirectoryDirs = defaultDirectoryDirs
, xmLegacyDirs = []
, xmName = name
, xmDirectory = dir
, xmOnlyUnallocated = onlyUnallocated
, xmDeleted = deleted
, xmInclude = include
, xmExclude = exclude
, xmSubmenus = subMenus
, xmLayout = layout
}
parseConditions :: String -> Element -> Maybe DesktopEntryCondition
parseConditions key elt = case findChild (unqual key) elt of
Nothing -> Nothing
Just inc -> doParseConditions (elChildren inc)
where doParseConditions :: [Element] -> Maybe DesktopEntryCondition
doParseConditions [] = Nothing
doParseConditions [e] = parseSingleItem e
doParseConditions elts = Just $ Or $ mapMaybe parseSingleItem elts
parseSingleItem e = case qName (elName e) of
"Category" -> Just $ Category $ strContent e
"Filename" -> Just $ Filename $ strContent e
"And" -> Just $ And $ mapMaybe parseSingleItem
$ elChildren e
"Or" -> Just $ Or $ mapMaybe parseSingleItem
$ elChildren e
"Not" -> case parseSingleItem (head (elChildren e)) of
Nothing -> Nothing
Just rule -> Just $ Not rule
unknown -> D.trace ("Unknown Condition item: " ++ unknown) Nothing
data DesktopEntryCondition = Category String
| Filename String
| Not DesktopEntryCondition
| And [DesktopEntryCondition]
| Or [DesktopEntryCondition]
| All
| None
deriving (Read, Show, Eq)
parseLayout :: Element -> [XDGLayoutItem]
parseLayout elt = case findChild (unqual "Layout") elt of
Nothing -> []
Just lt -> mapMaybe parseLayoutItem (elChildren lt)
where parseLayoutItem :: Element -> Maybe XDGLayoutItem
parseLayoutItem e = case qName (elName e) of
"Separator" -> Just XliSeparator
"Filename" -> Just $ XliFile $ strContent e
unknown -> D.trace ("Unknown layout item: " ++ unknown) Nothing
matchesCondition :: DesktopEntry -> DesktopEntryCondition -> Bool
matchesCondition de (Category cat) = deHasCategory de cat
matchesCondition de (Filename fn) = fn == deFilename de
matchesCondition de (Not cond) = not $ matchesCondition de cond
matchesCondition de (And conds) = all (matchesCondition de) conds
matchesCondition de (Or conds) = any (matchesCondition de) conds
matchesCondition _ All = True
matchesCondition _ None = False
getPreferredLanguages :: IO [String]
getPreferredLanguages = do
mLcMessages <- lookupEnv "LC_MESSAGES"
lang <- case mLcMessages of
Nothing -> lookupEnv "LANG"
Just lm -> return (Just lm)
case lang of
Nothing -> return []
Just l -> return $
let woEncoding = takeWhile (/= '.') l
(language, _cm) = span (/= '_') woEncoding
(country, _m) = span (/= '@') (if null _cm then "" else tail _cm)
modifier = if null _m then "" else tail _m
in dgl language country modifier
where dgl "" "" "" = []
dgl l "" "" = [l]
dgl l c "" = [l ++ "_" ++ c, l]
dgl l "" m = [l ++ "@" ++ m, l]
dgl l c m = [l ++ "_" ++ c ++ "@" ++ m, l ++ "_" ++ c,
l ++ "@" ++ m]
getXDGDesktop :: IO String
getXDGDesktop = do
mCurDt <- lookupEnv "XDG_CURRENT_DESKTOP"
return $ fromMaybe "???" mCurDt
getDirectoryDirs :: IO [FilePath]
getDirectoryDirs = do
dataDirs <- getXDGDataDirs
existingDirs $ map (</> "desktop-directories") dataDirs
readXDGMenu :: Maybe String -> IO (Maybe (XDGMenu, [DesktopEntry]))
readXDGMenu mMenuPrefix = do
setLocaleEncoding utf8
filenames <- getXDGMenuFilenames mMenuPrefix
headMay . catMaybes <$> traverse maybeMenu filenames
maybeMenu :: FilePath -> IO (Maybe (XDGMenu, [DesktopEntry]))
maybeMenu filename =
ifM (doesFileExist filename)
(do
contents <- readFile filename
langs <- getPreferredLanguages
runMaybeT $ do
m <- MaybeT $ return $ parseXMLDoc contents >>= parseMenu
des <- lift $ getApplicationEntries langs m
return (m, des))
(do
logM "System.Taffybar.Information.XDG.Protocol" WARNING $
"Menu file '" ++ filename ++ "' does not exist!"
return Nothing)