198 lines
4.9 KiB
Haskell
198 lines
4.9 KiB
Haskell
module Rofi.Command
|
|
( RofiConf (..)
|
|
, RofiMenu (..)
|
|
, RofiAction
|
|
, RofiActions
|
|
, RofiIO
|
|
, RofiGroup
|
|
, Hotkey (..)
|
|
, io
|
|
, emptyMenu
|
|
, runRofiIO
|
|
, toRofiActions
|
|
, rofiActionKeys
|
|
, untitledGroup
|
|
, titledGroup
|
|
, selectAction
|
|
, readPassword
|
|
, readPassword'
|
|
, readCmdSuccess
|
|
, readCmdEither
|
|
, readCmdEither'
|
|
, dmenuArgs
|
|
, joinNewline
|
|
, stripWS
|
|
)
|
|
where
|
|
|
|
import Data.Char
|
|
import qualified Data.Map.Ordered as M
|
|
import RIO
|
|
import qualified RIO.List as L
|
|
import qualified RIO.Text as T
|
|
import System.Process
|
|
|
|
class RofiConf c where
|
|
defArgs :: c -> [T.Text]
|
|
|
|
type RofiAction c = (T.Text, RofiIO c ())
|
|
|
|
type RofiActions c = M.OMap T.Text (RofiIO c ())
|
|
|
|
data RofiGroup c = RofiGroup
|
|
{ actions :: RofiActions c
|
|
, title :: Maybe T.Text
|
|
}
|
|
|
|
untitledGroup :: RofiActions c -> RofiGroup c
|
|
untitledGroup a = RofiGroup {actions = a, title = Nothing}
|
|
|
|
titledGroup :: T.Text -> RofiActions c -> RofiGroup c
|
|
titledGroup t a = (untitledGroup a) {title = Just t}
|
|
|
|
data Hotkey c = Hotkey
|
|
{ keyCombo :: !T.Text
|
|
, -- only 1-10 are valid
|
|
keyIndex :: !Int
|
|
, keyDescription :: !T.Text
|
|
, keyActions :: RofiActions c
|
|
}
|
|
|
|
hotkeyBinding :: Hotkey c -> [T.Text]
|
|
hotkeyBinding Hotkey {keyIndex = e, keyCombo = c} = [k, c]
|
|
where
|
|
k = T.append "-kb-custom-" $ T.pack $ show e
|
|
|
|
hotkeyMsg1 :: Hotkey c -> T.Text
|
|
hotkeyMsg1 Hotkey {keyCombo = c, keyDescription = d} =
|
|
T.concat [c, ": <i>", d, "</i>"]
|
|
|
|
hotkeyMsg :: [Hotkey c] -> [T.Text]
|
|
hotkeyMsg [] = []
|
|
hotkeyMsg hs = ["-mesg", T.intercalate " | " $ fmap hotkeyMsg1 hs]
|
|
|
|
hotkeyArgs :: [Hotkey c] -> [T.Text]
|
|
hotkeyArgs hks = hotkeyMsg hks ++ concatMap hotkeyBinding hks
|
|
|
|
data RofiMenu c = RofiMenu
|
|
{ groups :: [RofiGroup c]
|
|
, prompt :: Maybe T.Text
|
|
, hotkeys :: [Hotkey c]
|
|
}
|
|
|
|
emptyMenu :: RofiMenu c
|
|
emptyMenu =
|
|
RofiMenu
|
|
{ groups = []
|
|
, prompt = Nothing
|
|
, hotkeys = []
|
|
}
|
|
|
|
newtype RofiIO c a = RofiIO (ReaderT c IO a)
|
|
deriving (Functor, Applicative, Monad, MonadIO, MonadReader c, MonadUnliftIO)
|
|
|
|
io :: MonadIO m => IO a -> m a
|
|
io = liftIO
|
|
|
|
runRofiIO :: c -> RofiIO c a -> IO a
|
|
runRofiIO c (RofiIO r) = runReaderT r c
|
|
|
|
toRofiActions :: [(T.Text, RofiIO c ())] -> RofiActions c
|
|
toRofiActions = M.fromList
|
|
|
|
rofiActionKeys :: RofiActions c -> T.Text
|
|
rofiActionKeys = joinNewline . map fst . M.assocs
|
|
|
|
lookupRofiAction :: T.Text -> RofiActions c -> RofiIO c ()
|
|
lookupRofiAction key ras = fromMaybe (return ()) $ M.lookup key ras
|
|
|
|
groupEntries :: RofiGroup c -> T.Text
|
|
groupEntries RofiGroup {actions = a, title = t}
|
|
| null a = ""
|
|
| otherwise = T.append title' $ rofiActionKeys a
|
|
where
|
|
title' = maybe "" (`T.append` "\n") t
|
|
|
|
menuActions :: RofiMenu c -> RofiActions c
|
|
menuActions = L.foldr (M.<>|) M.empty . fmap actions . groups
|
|
|
|
menuEntries :: RofiMenu c -> T.Text
|
|
menuEntries = T.intercalate "\n\n" . filter (not . T.null) . fmap groupEntries . groups
|
|
|
|
selectAction :: RofiConf c => RofiMenu c -> RofiIO c ()
|
|
selectAction rm = do
|
|
let p = maybeOption "-p" $ prompt rm
|
|
let hArgs = hotkeyArgs $ hotkeys rm
|
|
res <- readRofi (p ++ hArgs) $ menuEntries rm
|
|
case res of
|
|
Right key -> lookupRofiAction key $ menuActions rm
|
|
Left (n, key, _) ->
|
|
mapM_ (lookupRofiAction key . keyActions) $
|
|
L.find ((==) n . (+ 9) . keyIndex) $
|
|
hotkeys rm
|
|
|
|
maybeOption :: T.Text -> Maybe T.Text -> [T.Text]
|
|
maybeOption switch = maybe [] (\o -> [switch, o])
|
|
|
|
dmenuArgs :: [T.Text]
|
|
dmenuArgs = ["-dmenu"]
|
|
|
|
readRofi
|
|
:: RofiConf c
|
|
=> [T.Text]
|
|
-> T.Text
|
|
-> RofiIO c (Either (Int, T.Text, T.Text) T.Text)
|
|
readRofi uargs input = do
|
|
dargs <- asks defArgs
|
|
io $ readCmdEither "rofi" (dmenuArgs ++ dargs ++ uargs) input
|
|
|
|
readCmdSuccess :: T.Text -> [T.Text] -> T.Text -> IO (Maybe T.Text)
|
|
readCmdSuccess cmd args input =
|
|
either (const Nothing) Just
|
|
<$> readCmdEither cmd args input
|
|
|
|
readCmdEither
|
|
:: T.Text
|
|
-> [T.Text]
|
|
-> T.Text
|
|
-> IO (Either (Int, T.Text, T.Text) T.Text)
|
|
readCmdEither cmd args input =
|
|
resultToEither
|
|
<$> readProcessWithExitCode (T.unpack cmd) (fmap T.unpack args) (T.unpack input)
|
|
|
|
readCmdEither'
|
|
:: T.Text
|
|
-> [T.Text]
|
|
-> T.Text
|
|
-> [(T.Text, T.Text)]
|
|
-> IO (Either (Int, T.Text, T.Text) T.Text)
|
|
readCmdEither' cmd args input environ =
|
|
resultToEither
|
|
<$> readCreateProcessWithExitCode p (T.unpack input)
|
|
where
|
|
p =
|
|
(proc (T.unpack cmd) (fmap T.unpack args))
|
|
{ env = Just $ fmap (bimap T.unpack T.unpack) environ
|
|
}
|
|
|
|
resultToEither
|
|
:: (ExitCode, String, String)
|
|
-> Either (Int, T.Text, T.Text) T.Text
|
|
resultToEither (ExitSuccess, out, _) = Right $ stripWS $ T.pack out
|
|
resultToEither (ExitFailure n, out, err) =
|
|
Left (n, stripWS $ T.pack out, stripWS $ T.pack err)
|
|
|
|
stripWS :: T.Text -> T.Text
|
|
stripWS = T.reverse . T.dropWhile isSpace . T.reverse
|
|
|
|
joinNewline :: [T.Text] -> T.Text
|
|
joinNewline = T.intercalate "\n"
|
|
|
|
readPassword :: IO (Maybe T.Text)
|
|
readPassword = readPassword' "Password"
|
|
|
|
readPassword' :: T.Text -> IO (Maybe T.Text)
|
|
readPassword' p = readCmdSuccess "rofi" args ""
|
|
where
|
|
args = dmenuArgs ++ ["-p", p, "-password"]
|