packages feed

hdmenu-0.3.0: src/Core/Util.hs

module Core.Util
    ( -- * Better prelude
      module BasePrelude
    , ByteString
    , Map
    , XdgDirectory(XdgConfig)
    , Type
    , ifM
    , doesFileExist
    , proc
    , getXdgDirectory
    , spawnCommand
    , createDirectoryIfMissing

      -- * Types
    , ShowBS              -- type alias: ByteString -> ByteString
    , OpenIn(Term, Open)
    , Items               -- type alias: Map ByteString Int

      -- * Combining file paths
    , tryAddPrefix        -- :: ByteString -> ByteString -> ByteString
    , (<</>>)             -- :: Functor f => f FilePath -> FilePath -> f FilePath
    , (</>)               -- :: FilePath -> FilePath -> FilePath

      -- * System file paths
    , hdmenuPath          -- :: IO FilePath
    , histFile            -- :: IO FilePath

      -- * Running commands
    , spawn               -- :: ByteString -> IO ()
    , openWith            -- :: OpenIn -> ShowBS -> ByteString -> ByteString
    ) where

import BasePrelude
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as BS
import Data.ByteString.UTF8  (toString)
import Data.Kind (Type)
import Data.Map.Strict (Map)
import System.Directory (XdgDirectory (XdgConfig), createDirectoryIfMissing, doesFileExist, getXdgDirectory)
import System.Posix.FilePath qualified as BS -- used for ByteString version of </>
import System.Process (proc, spawnCommand)

-- | Like 'if', but in a monadic context.
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM p x y = p >>= \b -> if b then x else y
{-# INLINE ifM #-}

-- | Type for an Map that describes all of the executables with their
-- ratings.
type Items :: Type
type Items = Map ByteString Double

-- | Type for helping to decide how to open something.
type OpenIn :: Type
data OpenIn = Term ShowBS | Open ShowBS

-- | 'ShowS' for 'ByteString' because it is shorter :>.
type ShowBS :: Type
type ShowBS = ByteString -> ByteString

{- | Add a prefix to a string if it does not start with "/".

This will ensure the user can specify absolute paths to files, but also
conveniently use relative paths (starting from @$HOME@) if that is
desired.
-}
tryAddPrefix :: ByteString -> ByteString -> ByteString
tryAddPrefix prefix xs
    | BS.null xs  = ""
    | isSpecial x = xs
    | otherwise   = prefix BS.</> xs
  where
    x         :: Char         = BS.head xs
    isSpecial :: Char -> Bool = (`elem` ['/', '~'])

-- | Spawn a command and forget about it.
spawn :: ByteString -> IO ()
spawn = void . spawnCommand . toString

-- | Open something.
openWith
    :: OpenIn      -- ^ How (and with what) to open something.
    -> ByteString  -- ^ The thing to open.
    -> ByteString
openWith = \case
    Term t -> t . (" -e " <>) . escape
    Open o -> o . (" "    <>) . escape
  where
    escape :: ByteString -> ByteString
    escape = ("\"" <>) . (<> "\"") . BS.concatMap \case
        '('  -> "\\("
        ')'  -> "\\)"
        '\'' -> "'\"'\"'"
        '\"' -> "\\\""
        x    -> BS.singleton x

-- | XDG_CONFIG_HOME
xdgConfig :: IO FilePath
xdgConfig = getXdgDirectory XdgConfig ""

-- | Path to the hdmenu directory.
-- @XDG_CONFIG_HOME\/hdmenu@, so probably @~\/.config\/hdmenu@.
hdmenuPath :: IO FilePath
hdmenuPath = xdgConfig <</>> "hdmenu"

-- | Path to the history file.
-- @~\/.config\/hdmenu\/histFile@
histFile :: IO FilePath
histFile = hdmenuPath <</>> "histFile"

-- | Functorial path-append operation.
infixr 5 <</>>  -- same as </>
(<</>>) :: Functor f => f FilePath -> FilePath -> f FilePath
liftedFp <</>> fp = liftedFp <&> (</> fp)

-- | Combine two paths into a new path.
-- Adapted from: <https://hackage.haskell.org/package/filepath>
infixr 5 </>
(</>) :: FilePath -> FilePath -> FilePath
(</>) a b
    | Just '/' == listToMaybe b = b  -- leading path separator
    | otherwise = combineAlways a b
  where
    -- Combine two paths, assuming rhs is NOT absolute.
    combineAlways :: FilePath -> FilePath -> FilePath
    combineAlways z w
        | null z        = w
        | null w        = z
        | last z == '/' = z ++ w
        | otherwise     = z ++ "/" ++ w