Revise XDG handling

Improve handling of the whole XDG situation.  This now looks as follows

  1. If all three of xmonad's environment variables (XMONAD_DATA_DIR,
     XMONAD_CONFIG_DIR, and XMONAD_CACHE_DIR) are set, use them.
  2. If there is a build script `build' or configuration `xmonad.hs' in
     `~/.xmonad', set all three directories to `~/.xmonad'.
  3. Otherwise, use XDG_DATA_HOME, XDG_CONFIG_HOME, and
     XDG_CACHE_HOME (or their respective fallbacks).

If none of the above exist, we default to using the XDG_* variables,
creating the necessary directories if needed.
This commit is contained in:
slotThe 2020-12-27 19:19:21 +01:00
parent e8f48b77f9
commit 735fb58f6c
3 changed files with 116 additions and 143 deletions

View File

@ -1,5 +1,6 @@
{-# LANGUAGE ExistentialQuantification, FlexibleInstances, GeneralizedNewtypeDeriving,
MultiParamTypeClasses, TypeSynonymInstances, DeriveDataTypeable #-}
MultiParamTypeClasses, TypeSynonymInstances, DeriveDataTypeable,
LambdaCase, NamedFieldPuns, DeriveTraversable #-}
-----------------------------------------------------------------------------
-- |
@ -28,7 +29,7 @@ module XMonad.Core (
getAtom, spawn, spawnPID, xfork, recompile, trace, whenJust, whenX,
getXMonadDir, getXMonadCacheDir, getXMonadDataDir, stateFileName,
atom_WM_STATE, atom_WM_PROTOCOLS, atom_WM_DELETE_WINDOW, atom_WM_TAKE_FOCUS, withWindowAttributes,
ManageHook, Query(..), runQuery
ManageHook, Query(..), runQuery, Directories(..), Dirs, getDirs
) where
import XMonad.StackSet hiding (modify)
@ -36,10 +37,12 @@ import XMonad.StackSet hiding (modify)
import Prelude
import Control.Exception.Extensible (fromException, try, bracket, throw, finally, SomeException(..))
import qualified Control.Exception.Extensible as E
import Control.Applicative ((<|>), empty)
import Control.Monad.Fail
import Control.Monad.State
import Control.Monad.Reader
import Data.Semigroup
import Data.Traversable (for)
import Data.Default
import System.FilePath
import System.IO
@ -57,7 +60,6 @@ import Graphics.X11.Xlib.Extras (getWindowAttributes, WindowAttributes, Event)
import Data.Typeable
import Data.List ((\\))
import Data.Maybe (isJust,fromMaybe)
import System.Environment (lookupEnv)
import qualified Data.Map as M
import qualified Data.Set as S
@ -91,8 +93,8 @@ data XConf = XConf
, mousePosition :: !(Maybe (Position, Position))
-- ^ position of the mouse according to
-- the event currently being processed
, currentEvent :: !(Maybe Event)
-- ^ event currently being processed
, currentEvent :: !(Maybe Event) -- ^ event currently being processed
, dirs :: !Dirs -- ^ directories to use
}
-- todo, better name
@ -447,126 +449,97 @@ runOnWorkspaces job = do
$ current ws : visible ws
modify $ \s -> s { windowset = ws { current = c, visible = v, hidden = h } }
-- | Return the path to the xmonad configuration directory. This
-- directory is where user configuration files are stored (e.g, the
-- xmonad.hs file). You may also create a @lib@ subdirectory in the
-- configuration directory and the default recompile command will add
-- it to the GHC include path.
-- | All the directories that xmonad will use. They will be used for
-- the following purposes:
--
-- Several directories are considered. In order of
-- preference:
-- * @dataDir@: This directory is used by XMonad to store data files
-- such as the run-time state file and the configuration binary
-- generated by GHC.
--
-- 1. The directory specified in the @XMONAD_CONFIG_DIR@ environment variable.
-- 2. The @~\/.xmonad@ directory.
-- 3. The @XDG_CONFIG_HOME/xmonad@ directory.
-- * @cfgDir@: This directory is where user configuration files are
-- stored (e.g, the xmonad.hs file). You may also create a @lib@
-- subdirectory in the configuration directory and the default recompile
-- command will add it to the GHC include path.
--
-- The first directory that exists will be used. If none of the
-- directories exist then (1) will be used if it is set, otherwise (2)
-- will be used. Either way, a directory will be created if necessary.
getXMonadDir :: MonadIO m => m String
getXMonadDir =
findFirstDirWithEnv "XMONAD_CONFIG_DIR"
[ getAppUserDataDirectory "xmonad"
, getXDGDirectory XDGConfig "xmonad"
]
-- * @cacheDir@: This directory is used to store temporary files that
-- can easily be recreated. For example, the XPrompt history file.
--
-- For how these directories are chosen, see 'getDirs'.
--
data Directories a = Dirs
{ dataDir :: !a
, cfgDir :: !a
, cacheDir :: !a
}
deriving (Show, Functor, Foldable, Traversable)
-- | Return the path to the xmonad cache directory. This directory is
-- used to store temporary files that can easily be recreated. For
-- example, the XPrompt history file.
--
-- Several directories are considered. In order of preference:
--
-- 1. The directory specified in the @XMONAD_CACHE_DIR@ environment variable.
-- 2. The @~\/.xmonad@ directory.
-- 3. The @XDG_CACHE_HOME/xmonad@ directory.
--
-- The first directory that exists will be used. If none of the
-- directories exist then (1) will be used if it is set, otherwise (2)
-- will be used. Either way, a directory will be created if necessary.
getXMonadCacheDir :: MonadIO m => m String
getXMonadCacheDir =
findFirstDirWithEnv "XMONAD_CACHE_DIR"
[ getAppUserDataDirectory "xmonad"
, getXDGDirectory XDGCache "xmonad"
]
-- | Convenient type alias for the most common case in which one might
-- want to use the 'Directories' type.
type Dirs = Directories FilePath
-- | Return the path to the xmonad data directory. This directory is
-- used by XMonad to store data files such as the run-time state file
-- and the configuration binary generated by GHC.
-- | Build up the 'Dirs' that xmonad will use. They are chosen as
-- follows:
--
-- Several directories are considered. In order of preference:
-- 1. If all three of xmonad's environment variables (@XMONAD_DATA_DIR@,
-- @XMONAD_CONFIG_DIR@, and @XMONAD_CACHE_DIR@) are set, use them.
-- 2. If there is a build script called @build@ or configuration
-- @xmonad.hs@ in @~\/.xmonad@, set all three directories to
-- @~\/.xmonad@.
-- 3. Otherwise, use the @xmonad@ directory in @XDG_DATA_HOME@,
-- @XDG_CONFIG_HOME@, and @XDG_CACHE_HOME@ (or their respective
-- fallbacks). These directories are created if necessary.
--
-- 1. The directory specified in the @XMONAD_DATA_DIR@ environment variable.
-- 2. The @~\/.xmonad@ directory.
-- 3. The @XDG_DATA_HOME/xmonad@ directory.
-- The xmonad configuration file (or the build script, if present) is
-- always assumed to be in @cfgDir@.
--
-- The first directory that exists will be used. If none of the
-- directories exist then (1) will be used if it is set, otherwise (2)
-- will be used. Either way, a directory will be created if necessary.
getXMonadDataDir :: MonadIO m => m String
getXMonadDataDir =
findFirstDirWithEnv "XMONAD_DATA_DIR"
[ getAppUserDataDirectory "xmonad"
, getXDGDirectory XDGData "xmonad"
]
-- | Helper function that will find the first existing directory and
-- return its path. If none of the directories can be found, create
-- and return the first from the list. If the list is empty this
-- function returns the historical @~\/.xmonad@ directory.
findFirstDirOf :: MonadIO m => [IO FilePath] -> m FilePath
findFirstDirOf [] = findFirstDirOf [getAppUserDataDirectory "xmonad"]
findFirstDirOf possibles = do
found <- go possibles
case found of
Just path -> return path
Nothing -> do
primary <- io (head possibles)
io (createDirectoryIfMissing True primary)
return primary
getDirs :: IO Dirs
getDirs = xmEnvDirs <|> xmDirs <|> xdgDirs
where
go [] = return Nothing
go (x:xs) = do
dir <- io x
exists <- io (doesDirectoryExist dir)
if exists then return (Just dir) else go xs
-- | Check for xmonad's environment variables first
xmEnvDirs :: IO Dirs
xmEnvDirs = do
let xmEnvs = Dirs{ dataDir = "XMONAD_DATA_DIR"
, cfgDir = "XMONAD_CONFIG_DIR"
, cacheDir = "XMONAD_CACHE_DIR"
}
maybe empty pure . sequenceA =<< traverse getEnv xmEnvs
-- | Simple wrapper around @findFirstDirOf@ that allows the primary
-- path to be specified by an environment variable.
findFirstDirWithEnv :: MonadIO m => String -> [IO FilePath] -> m FilePath
findFirstDirWithEnv envName paths = do
envPath' <- io (getEnv envName)
-- | Check whether the config file or a build script is in the
-- @~\/.xmonad@ directory
xmDirs :: IO Dirs
xmDirs = do
xmDir <- getAppUserDataDirectory "xmonad"
conf <- doesFileExist $ xmDir </> "xmonad.hs"
build <- doesFileExist $ xmDir </> "build"
case envPath' of
Nothing -> findFirstDirOf paths
Just envPath -> findFirstDirOf (return envPath:paths)
-- Place *everything* in ~/.xmonad if yes
guard $ conf || build
pure Dirs{ dataDir = xmDir, cfgDir = xmDir, cacheDir = xmDir }
-- | Helper function to retrieve the various XDG directories.
-- This has been based on the implementation shipped with GHC version 8.0.1 or
-- higher. Put here to preserve compatibility with older GHC versions.
getXDGDirectory :: XDGDirectory -> FilePath -> IO FilePath
getXDGDirectory xdgDir suffix =
normalise . (</> suffix) <$>
case xdgDir of
XDGData -> get "XDG_DATA_HOME" ".local/share"
XDGConfig -> get "XDG_CONFIG_HOME" ".config"
XDGCache -> get "XDG_CACHE_HOME" ".cache"
where
get name fallback = do
env <- lookupEnv name
case env of
Nothing -> fallback'
Just path
| isRelative path -> fallback'
| otherwise -> return path
where
fallback' = (</> fallback) <$> getHomeDirectory
data XDGDirectory = XDGData | XDGConfig | XDGCache
-- | Use XDG directories as a fallback
xdgDirs :: IO Dirs
xdgDirs =
for Dirs{ dataDir = XdgData, cfgDir = XdgConfig, cacheDir = XdgCache }
$ \dir -> do d <- getXdgDirectory dir "xmonad"
d <$ createDirectoryIfMissing True d
-- | Return the path to the xmonad configuration directory.
getXMonadDir :: X String
getXMonadDir = asks (cfgDir . dirs)
{-# DEPRECATED getXMonadDir "Use `asks (cfgDir . dirs)' instead." #-}
-- | Return the path to the xmonad cache directory.
getXMonadCacheDir :: X String
getXMonadCacheDir = asks (cacheDir . dirs)
{-# DEPRECATED getXMonadCacheDir "Use `asks (cacheDir . dirs)' instead." #-}
-- | Return the path to the xmonad data directory.
getXMonadDataDir :: X String
getXMonadDataDir = asks (dataDir . dirs)
{-# DEPRECATED getXMonadDataDir "Use `asks (dataDir . dirs)' instead." #-}
-- | Get the name of the file used to store the xmonad window state.
stateFileName :: (Functor m, MonadIO m) => m FilePath
stateFileName :: X FilePath
stateFileName = (</> "xmonad.state") <$> getXMonadDataDir
-- | 'recompile force', recompile the xmonad configuration file when
@ -588,16 +561,14 @@ stateFileName = (</> "xmonad.state") <$> getXMonadDataDir
--
-- 'False' is returned if there are compilation errors.
--
recompile :: MonadIO m => Bool -> m Bool
recompile force = io $ do
cfgdir <- getXMonadDir
datadir <- getXMonadDataDir
recompile :: MonadIO m => Dirs -> Bool -> m Bool
recompile Dirs{ cfgDir, dataDir } force = io $ do
let binn = "xmonad-"++arch++"-"++os
bin = datadir </> binn
err = datadir </> "xmonad.errors"
src = cfgdir </> "xmonad.hs"
lib = cfgdir </> "lib"
buildscript = cfgdir </> "build"
bin = dataDir </> binn
err = dataDir </> "xmonad.errors"
src = cfgDir </> "xmonad.hs"
lib = cfgDir </> "lib"
buildscript = cfgDir </> "build"
libTs <- mapM getModTime . Prelude.filter isSource =<< allFiles lib
srcT <- getModTime src
@ -640,8 +611,8 @@ recompile force = io $ do
uninstallSignalHandlers
status <- bracket (openFile err WriteMode) hClose $ \errHandle ->
waitForProcess =<< if useBuildscript
then compileScript bin cfgdir buildscript errHandle
else compileGHC bin cfgdir errHandle
then compileScript bin cfgDir buildscript errHandle
else compileGHC bin cfgDir errHandle
-- re-enable SIGCHLD:
installSignalHandlers

View File

@ -1,4 +1,4 @@
{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts, NamedFieldPuns #-}
----------------------------------------------------------------------------
-- |
-- Module : XMonad.Main
@ -39,7 +39,7 @@ import XMonad.Operations
import System.IO
import System.Directory
import System.Info
import System.Environment
import System.Environment (getArgs, getProgName, withArgs)
import System.Posix.Process (executeFile)
import System.Exit (exitFailure)
import System.FilePath
@ -59,17 +59,18 @@ xmonad :: (LayoutClass l Window, Read (l Window)) => XConfig l -> IO ()
xmonad conf = do
installSignalHandlers -- important to ignore SIGCHLD to avoid zombies
dirs <- getDirs
let launch' args = do
catchIO buildLaunch
catchIO (buildLaunch dirs)
conf'@XConfig { layoutHook = Layout l }
<- handleExtraArgs conf args conf{ layoutHook = Layout (layoutHook conf) }
withArgs [] $ launch (conf' { layoutHook = l })
withArgs [] $ launch (conf' { layoutHook = l }) dirs
args <- getArgs
case args of
("--resume": ws : xs : args') -> migrateState ws xs >> launch' args'
("--resume": ws : xs : args') -> migrateState dirs ws xs >> launch' args'
["--help"] -> usage
["--recompile"] -> recompile True >>= flip unless exitFailure
["--recompile"] -> recompile dirs True >>= flip unless exitFailure
["--restart"] -> sendRestart
["--version"] -> putStrLn $ unwords shortVersion
["--verbose-version"] -> putStrLn . unwords $ shortVersion ++ longVersion
@ -90,7 +91,7 @@ usage = do
"Options:" :
" --help Print this message" :
" --version Print the version number" :
" --recompile Recompile your ~/.xmonad/xmonad.hs" :
" --recompile Recompile your xmonad.hs" :
" --replace Replace the running window manager with xmonad" :
" --restart Request a running xmonad process to restart" :
[]
@ -111,8 +112,8 @@ usage = do
--
-- * Missing XMonad\/XMonadContrib modules due to ghc upgrade
--
buildLaunch :: IO ()
buildLaunch = do
buildLaunch :: Dirs -> IO ()
buildLaunch dirs@Dirs{ dataDir } = do
whoami <- getProgName
let compiledConfig = "xmonad-"++arch++"-"++os
unless (whoami == compiledConfig) $ do
@ -122,10 +123,9 @@ buildLaunch = do
, " but the compiled configuration should be called "
, show compiledConfig
]
recompile False
dir <- getXMonadDataDir
recompile dirs False
args <- getArgs
executeFile (dir </> compiledConfig) False args Nothing
executeFile (dataDir </> compiledConfig) False args Nothing
sendRestart :: IO ()
sendRestart = do
@ -166,8 +166,8 @@ sendReplace = do
-- function instead of 'xmonad'. You probably also want to have a key
-- binding to the 'XMonad.Operations.restart` function that restarts
-- your custom binary with the resume flag set to @True@.
launch :: (LayoutClass l Window, Read (l Window)) => XConfig l -> IO ()
launch initxmc = do
launch :: (LayoutClass l Window, Read (l Window)) => XConfig l -> Dirs -> IO ()
launch initxmc drs = do
-- setup locale information from environment
setLocale LC_ALL (Just "")
-- ignore SIGPIPE and SIGCHLD
@ -216,7 +216,9 @@ launch initxmc = do
, buttonActions = mouseBindings xmc xmc
, mouseFocused = False
, mousePosition = Nothing
, currentEvent = Nothing }
, currentEvent = Nothing
, dirs = drs
}
st = XState
{ windowset = initialWinset

View File

@ -1,5 +1,5 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleContexts, FlexibleInstances, MultiParamTypeClasses, PatternGuards, TypeSynonymInstances #-}
{-# LANGUAGE FlexibleContexts, FlexibleInstances, MultiParamTypeClasses, PatternGuards, TypeSynonymInstances, NamedFieldPuns #-}
-- --------------------------------------------------------------------------
-- |
-- Module : XMonad.Operations
@ -36,6 +36,7 @@ import qualified Control.Exception.Extensible as C
import System.IO
import System.Directory
import System.FilePath ((</>))
import System.Posix.Process (executeFile)
import Graphics.X11.Xlib
import Graphics.X11.Xinerama (getScreenInfo)
@ -524,12 +525,11 @@ readStateFile xmc = do
-- | Migrate state from a previously running xmonad instance that used
-- the older @--resume@ technique.
{-# DEPRECATED migrateState "will be removed some point in the future." #-}
migrateState :: (Functor m, MonadIO m) => String -> String -> m ()
migrateState ws xs = do
migrateState :: (Functor m, MonadIO m) => Dirs -> String -> String -> m ()
migrateState Dirs{ dataDir } ws xs = do
io (putStrLn "WARNING: --resume is no longer supported.")
whenJust stateData $ \s -> do
path <- stateFileName
catchIO (writeFile path $ show s)
whenJust stateData $ \s ->
catchIO (writeFile (dataDir </> "xmonad.state") $ show s)
where
stateData = StateFile <$> maybeRead ws <*> maybeRead xs
maybeRead s = case reads s of