REF wrap everything in simpleapp

This commit is contained in:
Nathan Dwarshuis 2023-02-22 22:44:44 -05:00
parent 05ecda045e
commit c3fc38d785
8 changed files with 310 additions and 257 deletions

View File

@ -10,17 +10,17 @@ import Bitwarden.Internal
import qualified Data.Text.IO as TI import qualified Data.Text.IO as TI
import Data.Yaml import Data.Yaml
import RIO import RIO
import RIO.Directory
import qualified RIO.List as L import qualified RIO.List as L
import qualified RIO.Text as T import qualified RIO.Text as T
import System.Directory
import System.Environment
import System.FilePath.Posix import System.FilePath.Posix
import System.Posix.Process import System.Posix.Process
import UnliftIO.Environment
main :: IO () main :: IO ()
main = do main = runSimpleApp $ do
hSetBuffering stdout LineBuffering hSetBuffering stdout LineBuffering
TI.putStrLn "OK Pleased to meet you" logInfo "OK Pleased to meet you"
pinentryLoop =<< readPinConf pinentryLoop =<< readPinConf
newtype PinConf = PinConf {pcBwName :: T.Text} deriving (Eq, Show) newtype PinConf = PinConf {pcBwName :: T.Text} deriving (Eq, Show)
@ -29,25 +29,27 @@ instance FromJSON PinConf where
parseJSON (Object o) = PinConf <$> o .:? "bitwarden-name" .!= "gnupg" parseJSON (Object o) = PinConf <$> o .:? "bitwarden-name" .!= "gnupg"
parseJSON _ = fail "pinentry yaml parse error" parseJSON _ = fail "pinentry yaml parse error"
readPinConf :: IO PinConf readPinConf :: RIO SimpleApp PinConf
readPinConf = do readPinConf = do
c <- decodeFileEither =<< pinConfDir c <- liftIO . decodeFileEither =<< pinConfDir
case c of case c of
Left e -> TI.putStrLn (T.pack $ show e) >> exitWith (ExitFailure 1) Left e -> do
logError $ displayShow e
exitWith (ExitFailure 1)
Right r -> return r Right r -> return r
pinConfDir :: IO FilePath pinConfDir :: RIO SimpleApp FilePath
pinConfDir = maybe defHome (return . (</> confname)) =<< lookupEnv "GNUPGHOME" pinConfDir = maybe defHome (return . (</> confname)) =<< lookupEnv "GNUPGHOME"
where where
defHome = (</> ".gnupg" </> confname) <$> getHomeDirectory defHome = (</> ".gnupg" </> confname) <$> getHomeDirectory
confname = "pinentry-rofi.yml" confname = "pinentry-rofi.yml"
pinentryLoop :: PinConf -> IO () pinentryLoop :: PinConf -> RIO SimpleApp ()
pinentryLoop p = do pinentryLoop p = do
processLine p . T.words =<< TI.getLine processLine p . T.words =<< liftIO TI.getLine
pinentryLoop p pinentryLoop p
processLine :: PinConf -> [T.Text] -> IO () processLine :: PinConf -> [T.Text] -> RIO SimpleApp ()
processLine _ [] = noop processLine _ [] = noop
processLine _ ["BYE"] = exitSuccess processLine _ ["BYE"] = exitSuccess
processLine p ["GETPIN"] = getPin p processLine p ["GETPIN"] = getPin p
@ -66,33 +68,36 @@ processLine _ ["CONFIRM"] = noop
processLine _ ["CONFIRM", "--one-button", _] = noop processLine _ ["CONFIRM", "--one-button", _] = noop
processLine _ ss = unknownCommand $ T.unwords ss processLine _ ss = unknownCommand $ T.unwords ss
unknownCommand :: T.Text -> IO () unknownCommand :: T.Text -> RIO SimpleApp ()
unknownCommand c = TI.putStrLn $ T.append "ERR 275 Unknown command " c unknownCommand c = putStrLnT $ T.append "ERR 275 Unknown command " c
getPin :: PinConf -> IO () getPin :: PinConf -> RIO SimpleApp ()
getPin p = do getPin p = do
its <- getItems its <- getItems
let w = (password . login) =<< L.find (\i -> pcBwName p == name i) its let w = (password . login) =<< L.find (\i -> pcBwName p == name i) its
maybe err send w maybe err send w
where where
err = TI.putStrLn "ERR 83886179 Operation canceled <rofi>" err = putStrLnT "ERR 83886179 Operation canceled <rofi>"
-- these are the only supported options for GETINFO; anything else is an error -- these are the only supported options for GETINFO; anything else is an error
processGetInfo :: T.Text -> IO () processGetInfo :: T.Text -> RIO SimpleApp ()
processGetInfo "pid" = send . T.pack . show =<< getProcessID processGetInfo "pid" = send . T.pack . show =<< liftIO getProcessID
processGetInfo "version" = noop processGetInfo "version" = noop
processGetInfo "flavor" = noop processGetInfo "flavor" = noop
processGetInfo "ttyinfo" = noop processGetInfo "ttyinfo" = noop
processGetInfo _ = TI.putStrLn "ERR 83886360 IPC parameter error <rofi>" processGetInfo _ = putStrLnT "ERR 83886360 IPC parameter error <rofi>"
processOption :: T.Text -> IO () processOption :: T.Text -> RIO SimpleApp ()
processOption _ = noop processOption _ = noop
send :: T.Text -> IO () send :: T.Text -> RIO SimpleApp ()
send s = TI.putStrLn (T.append "D " s) >> ok send s = putStrLnT (T.append "D " s) >> ok
noop :: IO () noop :: RIO SimpleApp ()
noop = ok noop = ok
ok :: IO () ok :: RIO SimpleApp ()
ok = TI.putStrLn "OK" ok = putStrLnT "OK"
putStrLnT :: MonadIO m => T.Text -> m ()
putStrLnT = liftIO . TI.putStrLn

View File

@ -5,29 +5,27 @@
module Main (main) where module Main (main) where
import Control.Monad
import Data.Maybe
import qualified Data.Text.IO as TI
import RIO import RIO
import RIO.Directory
import qualified RIO.Text as T import qualified RIO.Text as T
import Rofi.Command import Rofi.Command
import System.Directory
import System.Environment
import System.FilePath.Posix import System.FilePath.Posix
import System.Process import System.Process
import UnliftIO.Environment
main :: IO () main :: IO ()
main = runChecks >> getArgs >>= runPrompt main = runSimpleApp $ do
runChecks
getArgs >>= runPrompt
-- TOOD not DRY runChecks :: (MonadReader c m, HasLogFunc c, MonadIO m) => m ()
runChecks :: IO ()
runChecks = checkExe "autorandr" >> checkExe "rofi" runChecks = checkExe "autorandr" >> checkExe "rofi"
checkExe :: String -> IO () checkExe :: (MonadReader c m, HasLogFunc c, MonadIO m) => String -> m ()
checkExe cmd = do checkExe cmd = do
res <- findExecutable cmd res <- findExecutable cmd
unless (isJust res) $ do unless (isJust res) $ do
TI.putStrLn $ T.append "Could not find executable: " $ T.pack cmd logError $ displayBytesUtf8 $ encodeUtf8 $ T.append "Could not find executable: " $ T.pack cmd
exitWith $ ExitFailure 1 exitWith $ ExitFailure 1
newtype ARClientConf = ARClientConf [T.Text] newtype ARClientConf = ARClientConf [T.Text]
@ -35,7 +33,7 @@ newtype ARClientConf = ARClientConf [T.Text]
instance HasRofiConf ARClientConf where instance HasRofiConf ARClientConf where
defArgs (ARClientConf a) = a defArgs (ARClientConf a) = a
runPrompt :: [String] -> IO () runPrompt :: MonadIO m => [String] -> m ()
runPrompt a = do runPrompt a = do
let c = ARClientConf $ fmap T.pack a let c = ARClientConf $ fmap T.pack a
staticProfs <- getAutoRandrProfiles staticProfs <- getAutoRandrProfiles
@ -54,13 +52,13 @@ virtProfs :: [T.Text]
virtProfs = ["off", "common", "clone-largest", "horizontal", "vertical"] virtProfs = ["off", "common", "clone-largest", "horizontal", "vertical"]
-- TODO filter profiles based on which xrandr outputs are actually connected -- TODO filter profiles based on which xrandr outputs are actually connected
getAutoRandrProfiles :: IO [T.Text] getAutoRandrProfiles :: MonadIO m => m [T.Text]
getAutoRandrProfiles = do getAutoRandrProfiles = do
dir <- getAutoRandrDir dir <- getAutoRandrDir
contents <- listDirectory dir contents <- listDirectory dir
(fmap T.pack) <$> filterM (doesDirectoryExist . (dir </>)) contents (fmap T.pack) <$> filterM (doesDirectoryExist . (dir </>)) contents
getAutoRandrDir :: IO FilePath getAutoRandrDir :: MonadIO m => m FilePath
getAutoRandrDir = do getAutoRandrDir = do
c <- getXdgDirectory XdgConfig "autorandr" c <- getXdgDirectory XdgConfig "autorandr"
e <- doesDirectoryExist c e <- doesDirectoryExist c
@ -69,6 +67,7 @@ getAutoRandrDir = do
appendToHome p = (</> p) <$> getHomeDirectory appendToHome p = (</> p) <$> getHomeDirectory
selectProfile :: T.Text -> RIO ARClientConf () selectProfile :: T.Text -> RIO ARClientConf ()
selectProfile name = liftIO $ do selectProfile name =
TI.putStrLn name liftIO $
void $ spawnProcess "autorandr" ["--change", T.unpack name] void $
spawnProcess "autorandr" ["--change", T.unpack name]

View File

@ -8,46 +8,53 @@ import DBus
import DBus.Client import DBus.Client
import qualified Data.Map as M import qualified Data.Map as M
import Data.Maybe import Data.Maybe
import qualified Data.Text.IO as TI
import RIO import RIO
import qualified RIO.List as L import qualified RIO.List as L
import qualified RIO.Text as T import qualified RIO.Text as T
import Rofi.Command import Rofi.Command
import System.Environment import UnliftIO.Environment
main :: IO () main :: IO ()
main = getArgs >>= runPrompt main = runSimpleApp $ getArgs >>= runPrompt
data RofiBTConf = RofiBTConf [T.Text] ObjectPath data RofiBTConf = RofiBTConf
{ btArgs :: ![T.Text]
, btAdapter :: !ObjectPath
, btEnv :: !SimpleApp
}
instance HasRofiConf RofiBTConf where instance HasRofiConf RofiBTConf where
defArgs (RofiBTConf as _) = as defArgs = btArgs
instance HasLogFunc RofiBTConf where
logFuncL = lens btEnv (\x y -> x {btEnv = y}) . logFuncL
type BTAction = RofiAction RofiBTConf type BTAction = RofiAction RofiBTConf
runPrompt :: [String] -> IO () runPrompt :: [String] -> RIO SimpleApp ()
runPrompt args = do runPrompt args = do
c <- getClient c <- getClient
maybe (TI.putStrLn "could not get DBus client") run c maybe (logError "could not get DBus client") run c
where where
run client = do run client = do
paths <- M.keys <$> getObjectTree client paths <- M.keys <$> getObjectTree client
maybe (TI.putStrLn "could not get DBus adapter") (actions client paths) $ case getAdapter paths of
getAdapter paths Nothing -> logError "could not get DBus adapter"
actions client paths adapter = do Just adapter -> do
ras <- getRofiActions client paths ras <- getRofiActions client paths
runRofi (RofiBTConf (fmap T.pack args) adapter) $ mapRIO (RofiBTConf (fmap T.pack args) adapter) $
selectAction $
emptyMenu emptyMenu
{ groups = [untitledGroup $ toRofiActions ras] { groups = [untitledGroup $ toRofiActions ras]
, prompt = Just "Select Device" , prompt = Just "Select Device"
} }
getRofiActions :: Client -> [ObjectPath] -> IO [BTAction] getRofiActions :: MonadIO m => Client -> [ObjectPath] -> m [BTAction]
getRofiActions client os = do getRofiActions client os = do
devs <- getDevices client os devs <- getDevices client os
catMaybes <$> mapM (deviceToRofiAction client) devs catMaybes <$> mapM (deviceToRofiAction client) devs
deviceToRofiAction :: Client -> ObjectPath -> IO (Maybe BTAction) deviceToRofiAction :: MonadIO m => Client -> ObjectPath -> m (Maybe BTAction)
deviceToRofiAction client dev = do deviceToRofiAction client dev = do
c <- getDeviceConnected client dev c <- getDeviceConnected client dev
n <- getDeviceName client dev n <- getDeviceName client dev
@ -64,11 +71,11 @@ deviceToRofiAction client dev = do
powerAdapterMaybe :: Client -> RIO RofiBTConf () powerAdapterMaybe :: Client -> RIO RofiBTConf ()
powerAdapterMaybe client = do powerAdapterMaybe client = do
(RofiBTConf _ adapter) <- ask adapter <- asks btAdapter
let mc = btMethodCall adapter i m let mc = btMethodCall adapter i m
let powerOnMaybe = flip unless $ void $ setProperty client mc value let powerOnMaybe = flip unless $ void $ liftIO $ setProperty client mc value
powered <- io $ getBTProperty client adapter i m powered <- getBTProperty client adapter i m
io $ maybe (TI.putStrLn "could not get adapter powered status") powerOnMaybe powered maybe (logError "could not get adapter powered status") powerOnMaybe powered
where where
i = interfaceName_ "org.bluez.Adapter1" i = interfaceName_ "org.bluez.Adapter1"
m = memberName_ "Powered" m = memberName_ "Powered"
@ -85,12 +92,12 @@ formatDeviceEntry connected name = T.unwords [prefix connected, name]
getAdapter :: [ObjectPath] -> Maybe ObjectPath getAdapter :: [ObjectPath] -> Maybe ObjectPath
getAdapter = L.find pathIsAdaptor getAdapter = L.find pathIsAdaptor
getDevices :: Client -> [ObjectPath] -> IO [ObjectPath] getDevices :: MonadIO m => Client -> [ObjectPath] -> m [ObjectPath]
getDevices client = filterM (getDevicePaired client) . filter pathIsDevice getDevices client = filterM (getDevicePaired client) . filter pathIsDevice
type ObjectTree = M.Map ObjectPath (M.Map T.Text (M.Map T.Text Variant)) type ObjectTree = M.Map ObjectPath (M.Map T.Text (M.Map T.Text Variant))
getObjectTree :: Client -> IO ObjectTree getObjectTree :: MonadIO m => Client -> m ObjectTree
getObjectTree client = getObjectTree client =
fromMaybe M.empty . eitherMaybe from <$> callBTMethod client o i m fromMaybe M.empty . eitherMaybe from <$> callBTMethod client o i m
where where
@ -99,19 +106,19 @@ getObjectTree client =
m = memberName_ "GetManagedObjects" m = memberName_ "GetManagedObjects"
from = fromVariant <=< listToMaybe . methodReturnBody from = fromVariant <=< listToMaybe . methodReturnBody
getDeviceConnected :: Client -> ObjectPath -> IO (Maybe Bool) getDeviceConnected :: MonadIO m => Client -> ObjectPath -> m (Maybe Bool)
getDeviceConnected = getDevProperty "Connected" getDeviceConnected = getDevProperty "Connected"
getDeviceName :: Client -> ObjectPath -> IO (Maybe T.Text) getDeviceName :: MonadIO m => Client -> ObjectPath -> m (Maybe T.Text)
getDeviceName = getDevProperty "Name" getDeviceName = getDevProperty "Name"
getDevicePaired :: Client -> ObjectPath -> IO Bool getDevicePaired :: MonadIO m => Client -> ObjectPath -> m Bool
getDevicePaired c = fmap (fromMaybe False) . getDevProperty "Paired" c getDevicePaired c = fmap (fromMaybe False) . getDevProperty "Paired" c
callDeviceConnect :: Client -> ObjectPath -> IO () callDeviceConnect :: MonadIO m => Client -> ObjectPath -> m ()
callDeviceConnect = callDevMethod "Connect" callDeviceConnect = callDevMethod "Connect"
callDeviceDisconnect :: Client -> ObjectPath -> IO () callDeviceDisconnect :: MonadIO m => Client -> ObjectPath -> m ()
callDeviceDisconnect = callDevMethod "Disconnect" callDeviceDisconnect = callDevMethod "Disconnect"
pathIsAdaptor :: ObjectPath -> Bool pathIsAdaptor :: ObjectPath -> Bool
@ -130,39 +137,42 @@ pathIsAdaptorPrefix a b c = a == "org" && b == "bluez" && "hci" `T.isPrefixOf` c
splitPath :: ObjectPath -> [T.Text] splitPath :: ObjectPath -> [T.Text]
splitPath = T.split (== '/') . T.dropWhile (== '/') . T.pack . formatObjectPath splitPath = T.split (== '/') . T.dropWhile (== '/') . T.pack . formatObjectPath
getClient :: IO (Maybe Client) getClient :: (MonadReader c m, HasLogFunc c, MonadUnliftIO m) => m (Maybe Client)
getClient = either warn (return . Just) =<< try connectSystem getClient = either warn (return . Just) =<< try (liftIO connectSystem)
where where
warn e = TI.putStrLn (T.pack $ clientErrorMessage e) >> return Nothing warn e = do
logWarn $ displayBytesUtf8 $ encodeUtf8 $ (T.pack $ clientErrorMessage e)
return Nothing
callDevMethod :: T.Text -> Client -> ObjectPath -> IO () callDevMethod :: MonadIO m => T.Text -> Client -> ObjectPath -> m ()
callDevMethod mem client dev = callDevMethod mem client dev =
void $ callBTMethod client dev btDevInterface $ memberName_ $ T.unpack mem void $ callBTMethod client dev btDevInterface $ memberName_ $ T.unpack mem
getDevProperty :: IsVariant a => T.Text -> Client -> ObjectPath -> IO (Maybe a) getDevProperty :: (MonadIO m, IsVariant a) => T.Text -> Client -> ObjectPath -> m (Maybe a)
getDevProperty mem client dev = getDevProperty mem client dev =
getBTProperty client dev btDevInterface $ memberName_ $ T.unpack mem getBTProperty client dev btDevInterface $ memberName_ $ T.unpack mem
callBTMethod callBTMethod
:: Client :: MonadIO m
=> Client
-> ObjectPath -> ObjectPath
-> InterfaceName -> InterfaceName
-> MemberName -> MemberName
-> IO (Either MethodError MethodReturn) -> m (Either MethodError MethodReturn)
callBTMethod client o i m = call client (btMethodCall o i m) callBTMethod client o i m = liftIO $ call client (btMethodCall o i m)
-- eitherMaybe (fromVariant <=< listToMaybe . methodReturnBody) -- eitherMaybe (fromVariant <=< listToMaybe . methodReturnBody)
-- <$> call client (btMethodCall o i m) -- <$> call client (btMethodCall o i m)
getBTProperty getBTProperty
:: IsVariant a :: (MonadIO m, IsVariant a)
=> Client => Client
-> ObjectPath -> ObjectPath
-> InterfaceName -> InterfaceName
-> MemberName -> MemberName
-> IO (Maybe a) -> m (Maybe a)
getBTProperty client o i m = getBTProperty client o i m =
eitherMaybe fromVariant <$> getProperty client (btMethodCall o i m) eitherMaybe fromVariant <$> (liftIO $ getProperty client (btMethodCall o i m))
btMethodCall :: ObjectPath -> InterfaceName -> MemberName -> MethodCall btMethodCall :: ObjectPath -> InterfaceName -> MemberName -> MethodCall
btMethodCall o i m = (methodCall o i m) {methodCallDestination = Just btBus} btMethodCall o i m = (methodCall o i m) {methodCallDestination = Just btBus}

View File

@ -17,36 +17,37 @@
module Main (main) where module Main (main) where
import Bitwarden.Internal import Bitwarden.Internal
import qualified Data.Text.IO as TI
import RIO import RIO
import RIO.Directory import RIO.Directory
import qualified RIO.Text as T import qualified RIO.Text as T
import Rofi.Command import UnliftIO.Environment
import System.Environment
main :: IO () main :: IO ()
main = runChecks >> getArgs >>= parse main = runSimpleApp $ runChecks >> getArgs >>= parse
-- TODO check if daemon is running when running client -- TODO check if daemon is running when running client
parse :: [String] -> IO () parse :: HasLogFunc c => [String] -> RIO c ()
parse ["-d", t] = case readMaybe t of Just t' -> runDaemon t'; _ -> usage parse ["-d", t] = case readMaybe t of Just t' -> runDaemon t'; _ -> usage
parse ("-c" : args) = runClient $ fmap T.pack args parse ("-c" : args) = runClient $ fmap T.pack args
parse _ = usage parse _ = usage
usage :: IO () usage :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => m ()
usage = usage =
TI.putStrLn $ logInfo $
joinNewline displayBytesUtf8 $
encodeUtf8 $
T.unlines
[ "daemon mode: rofi-bw -d TIMEOUT" [ "daemon mode: rofi-bw -d TIMEOUT"
, "client mode: rofi-bw -c [ROFI-ARGS]" , "client mode: rofi-bw -c [ROFI-ARGS]"
] ]
runChecks :: IO () runChecks :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => m ()
runChecks = checkExe "bw" >> checkExe "rofi" runChecks = checkExe "bw" >> checkExe "rofi"
checkExe :: String -> IO () -- TODO not DRY
checkExe :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => String -> m ()
checkExe cmd = do checkExe cmd = do
res <- findExecutable cmd res <- findExecutable cmd
unless (isJust res) $ do unless (isJust res) $ do
TI.putStrLn $ T.append "Could not find executable: " $ T.pack cmd logError $ displayBytesUtf8 $ encodeUtf8 $ T.append "Could not find executable: " $ T.pack cmd
exitWith $ ExitFailure 1 exitWith $ ExitFailure 1

View File

@ -12,7 +12,6 @@
module Main (main) where module Main (main) where
import Bitwarden.Internal import Bitwarden.Internal
import qualified Data.Text.IO as TI
import Dhall hiding (maybe, sequence, void) import Dhall hiding (maybe, sequence, void)
import Dhall.TH import Dhall.TH
import RIO import RIO
@ -23,10 +22,10 @@ import qualified RIO.NonEmpty as NE
import qualified RIO.Text as T import qualified RIO.Text as T
import Rofi.Command import Rofi.Command
import System.Console.GetOpt import System.Console.GetOpt
import System.Environment
import System.FilePath.Posix import System.FilePath.Posix
import System.Posix.User (getEffectiveUserName) import System.Posix.User (getEffectiveUserName)
import System.Process import System.Process
import UnliftIO.Environment
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Static device configuration (dhall) -- Static device configuration (dhall)
@ -51,12 +50,17 @@ makeHaskellTypesWith
] ]
main :: IO () main :: IO ()
main = getArgs >>= parse main = runSimpleApp $ getArgs >>= parse
parse :: [String] -> IO () parse :: [String] -> RIO SimpleApp ()
parse args = case getOpt Permute options args of parse args = case getOpt Permute options args of
(o, n, []) -> runMounts $ L.foldl (flip id) (defaultOpts (fmap T.pack n)) o (o, n, []) -> runMounts $ L.foldl (flip id) (defaultOpts (fmap T.pack n)) o
(_, _, errs) -> TI.putStrLn $ T.pack $ concat errs ++ usageInfo h options (_, _, errs) ->
logError $
displayBytesUtf8 $
encodeUtf8 $
T.pack $
concat errs ++ usageInfo h options
where where
h = "Usage: rofi-dev [OPTIONS] [-- ROFI-OPTIONS]" h = "Usage: rofi-dev [OPTIONS] [-- ROFI-OPTIONS]"
defaultOpts r = defaultOpts r =
@ -101,29 +105,35 @@ data Opts = Opts
-- mounts grouped by device type (eg removable, sshfs, cifs, etc). I like -- mounts grouped by device type (eg removable, sshfs, cifs, etc). I like
-- pretty things, so ensure the entries are aligned properly as well -- pretty things, so ensure the entries are aligned properly as well
runMounts :: Opts -> IO () runMounts :: Opts -> RIO SimpleApp ()
runMounts opts = do runMounts opts = do
static <- join <$> traverse parseStaticConfig (optsConfig opts) static <- join <$> traverse parseStaticConfig (optsConfig opts)
defaultTmpPath <- ("/tmp/media" </>) <$> getEffectiveUserName defaultTmpPath <- ("/tmp/media" </>) <$> liftIO getEffectiveUserName
let tmpPath = fromMaybe defaultTmpPath $ (fmap T.unpack . scTmpPath) =<< static let tmpPath = fromMaybe defaultTmpPath $ (fmap T.unpack . scTmpPath) =<< static
let staticDevs = maybe M.empty (M.fromList . fmap (\(TreeMap k v) -> (k, v)) . scDevices) static let staticDevs = maybe M.empty (M.fromList . fmap (\(TreeMap k v) -> (k, v)) . scDevices) static
let verbose = fromMaybe False $ scVerbose =<< static let verbose = fromMaybe False $ scVerbose =<< static
let mountconf = let mountconf e =
MountConf MountConf
{ mountconfVolatilePath = tmpPath { mountconfVolatilePath = tmpPath
, mountconfRofiArgs = optsRofiArgs opts , mountconfRofiArgs = optsRofiArgs opts
, mountconfStaticDevs = staticDevs , mountconfStaticDevs = staticDevs
, mountconfVerbose = verbose , mountconfVerbose = verbose
, mountconfEnv = e
} }
let byAlias = mountByAlias $ optsUnmount opts let byAlias = mountByAlias $ optsUnmount opts
let byPrompt = runPrompt =<< getGroups let byPrompt = runPrompt =<< getGroups
runRIO mountconf $ maybe byPrompt byAlias $ optsAlias opts mapRIO mountconf $ maybe byPrompt byAlias $ optsAlias opts
parseStaticConfig :: FilePath -> IO (Maybe StaticConfig) parseStaticConfig
:: (HasLogFunc c, MonadReader c m, MonadUnliftIO m)
=> FilePath
-> m (Maybe StaticConfig)
parseStaticConfig p = do parseStaticConfig p = do
res <- tryIO $ inputFile auto p res <- tryIO $ liftIO $ inputFile auto p
case res of case res of
Left e -> TI.putStrLn (T.pack $ show e) >> return Nothing Left e -> do
logError $ displayBytesUtf8 $ encodeUtf8 $ T.pack $ show e
return Nothing
Right c -> return $ Just c Right c -> return $ Just c
runPrompt :: HasRofiConf c => [RofiGroup c] -> RIO c () runPrompt :: HasRofiConf c => [RofiGroup c] -> RIO c ()
@ -134,7 +144,7 @@ runPrompt gs =
, prompt = Just "Select Device" , prompt = Just "Select Device"
} }
getGroups :: RofiMountIO [RofiGroup MountConf] getGroups :: MIO [RofiGroup MountConf]
getGroups = do getGroups = do
actions <- sequence [getStaticActions, getRemovableActions, getMTPActions] actions <- sequence [getStaticActions, getRemovableActions, getMTPActions]
return $ return $
@ -147,17 +157,17 @@ getGroups = do
titledGroup "Meta Actions" $ titledGroup "Meta Actions" $
toRofiActions [(" Dismount All", dismountAll)] toRofiActions [(" Dismount All", dismountAll)]
dismountAll :: RofiMountIO () dismountAll :: MIO ()
dismountAll = do dismountAll = do
umount =<< asks (configToTree' . mountconfStaticDevs) umount =<< asks (configToTree' . mountconfStaticDevs)
umount =<< getRemovableDevices umount =<< getRemovableDevices
umount =<< getMTPDevices umount =<< getMTPDevices
return () return ()
where where
umount :: Mountable a => [a] -> RofiMountIO () umount :: Mountable a => [a] -> MIO ()
umount = mapM_ (`mountMaybe` True) umount = mapM_ (`mountMaybe` True)
mountByAlias :: Bool -> T.Text -> RofiMountIO () mountByAlias :: Bool -> T.Text -> MIO ()
mountByAlias unmountFlag alias = do mountByAlias unmountFlag alias = do
static <- asks mountconfStaticDevs static <- asks mountconfStaticDevs
mapM_ (`mountMaybe` unmountFlag) $ configToTree static <$> M.lookup alias static mapM_ (`mountMaybe` unmountFlag) $ configToTree static <$> M.lookup alias static
@ -170,7 +180,7 @@ mkGroup as = titledGroup h $ toRofiActions $ NE.toList $ alignEntries $ snd <$>
alignSep :: T.Text alignSep :: T.Text
alignSep = " | " alignSep = " | "
alignEntries :: NE.NonEmpty (ProtoAction) -> NE.NonEmpty (T.Text, RofiMountIO ()) alignEntries :: NE.NonEmpty (ProtoAction) -> NE.NonEmpty (T.Text, MIO ())
alignEntries ps = NE.zip (align es) as alignEntries ps = NE.zip (align es) as
where where
(es, as) = NE.unzip $ fmap (\(ProtoAction e a) -> (e, a)) ps (es, as) = NE.unzip $ fmap (\(ProtoAction e a) -> (e, a)) ps
@ -195,6 +205,7 @@ data MountConf = MountConf
, mountconfRofiArgs :: [T.Text] , mountconfRofiArgs :: [T.Text]
, mountconfStaticDevs :: M.Map T.Text TreeConfig , mountconfStaticDevs :: M.Map T.Text TreeConfig
, mountconfVerbose :: Bool , mountconfVerbose :: Bool
, mountconfEnv :: !SimpleApp
} }
-- deriving (Show) -- deriving (Show)
@ -202,6 +213,9 @@ data MountConf = MountConf
instance HasRofiConf MountConf where instance HasRofiConf MountConf where
defArgs MountConf {mountconfRofiArgs = a} = a defArgs MountConf {mountconfRofiArgs = a} = a
instance HasLogFunc MountConf where
logFuncL = lens mountconfEnv (\x y -> x {mountconfEnv = y}) . logFuncL
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Mountable typeclass -- Mountable typeclass
-- --
@ -215,9 +229,9 @@ mountedState _ = False
class Mountable a where class Mountable a where
-- | Mount the given type (or dismount if False is passed) -- | Mount the given type (or dismount if False is passed)
mount :: a -> Bool -> RofiMountIO MountResult mount :: a -> Bool -> MIO MountResult
mountMaybe :: a -> Bool -> RofiMountIO () mountMaybe :: a -> Bool -> MIO ()
mountMaybe dev mountFlag = do mountMaybe dev mountFlag = do
let lab = getLabel dev let lab = getLabel dev
mounted <- isMounted dev mounted <- isMounted dev
@ -232,16 +246,16 @@ class Mountable a where
return () return ()
-- | Check if the mounting utilities are present -- | Check if the mounting utilities are present
allInstalled :: a -> RofiMountIO Bool allInstalled :: a -> MIO Bool
-- | Return a string representing the label of the device -- | Return a string representing the label of the device
getLabel :: a -> T.Text getLabel :: a -> T.Text
-- | Determine if the given type is mounted or not -- | Determine if the given type is mounted or not
isMounted :: a -> RofiMountIO Bool isMounted :: a -> MIO Bool
isMounted dev = mountedState <$> mountState dev isMounted dev = mountedState <$> mountState dev
mountState :: a -> RofiMountIO MountState mountState :: a -> MIO MountState
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Actionable typeclass -- Actionable typeclass
@ -261,7 +275,7 @@ class Mountable a => Actionable a where
-- | Given a mountable type, return a rofi action (string to go in the -- | Given a mountable type, return a rofi action (string to go in the
-- Rofi prompt and an action to perform when it is selected) -- Rofi prompt and an action to perform when it is selected)
mkAction :: a -> RofiMountIO (Header, ProtoAction) mkAction :: a -> MIO (Header, ProtoAction)
mkAction dev = do mkAction dev = do
m <- mountState dev m <- mountState dev
i <- allInstalled dev i <- allInstalled dev
@ -278,11 +292,11 @@ class Mountable a => Actionable a where
mountableToAction mountableToAction
:: Actionable a :: Actionable a
=> RofiMountIO [a] => MIO [a]
-> RofiMountIO [(Header, ProtoAction)] -> MIO [(Header, ProtoAction)]
mountableToAction ms = mapM mkAction =<< ms mountableToAction ms = mapM mkAction =<< ms
type RofiMountIO a = RIO MountConf a type MIO a = RIO MountConf a
-- headers appear in the order listed here (per Enum) -- headers appear in the order listed here (per Enum)
data Header data Header
@ -303,7 +317,7 @@ instance Show Header where
where where
suffix = (++ " Devices") suffix = (++ " Devices")
data ProtoAction = ProtoAction (NE.NonEmpty T.Text) (RofiMountIO ()) data ProtoAction = ProtoAction (NE.NonEmpty T.Text) (MIO ())
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Static devices trees -- Static devices trees
@ -367,7 +381,6 @@ instance Mountable DeviceConfig where
mount DeviceConfig {deviceMount = m, deviceData = devData} False = do mount DeviceConfig {deviceMount = m, deviceData = devData} False = do
m' <- getAbsMountpoint m m' <- getAbsMountpoint m
withTmpMountDir m' $ withTmpMountDir m' $
io $
case devData of case devData of
SSHFSConfig (SSHFSData {sshfsRemote = r, sshfsPassword = p}) -> SSHFSConfig (SSHFSData {sshfsRemote = r, sshfsPassword = p}) ->
mountSSHFS m' p r mountSSHFS m' p r
@ -416,19 +429,25 @@ instance Mountable DeviceConfig where
{ deviceMount = MountConfig {mpPath = p, mpLabel = l} { deviceMount = MountConfig {mpPath = p, mpLabel = l}
} = fromMaybe (T.pack $ takeFileName $ T.unpack p) l } = fromMaybe (T.pack $ takeFileName $ T.unpack p) l
mountSSHFS :: FilePath -> Maybe PasswordConfig -> T.Text -> IO MountResult mountSSHFS
:: (HasLogFunc c, MonadReader c m, MonadUnliftIO m)
=> FilePath
-> Maybe PasswordConfig
-> T.Text
-> m MountResult
mountSSHFS mountpoint pwdConfig remote = mountSSHFS mountpoint pwdConfig remote =
withPasswordGetter pwdConfig (run ["-o", "password_stdin"]) $ run [] "" withPasswordGetter pwdConfig (run ["-o", "password_stdin"]) $ run [] ""
where where
run other = runMount "sshfs" (other ++ [remote, T.pack mountpoint]) run other = runMount "sshfs" (other ++ [remote, T.pack mountpoint])
mountCIFS mountCIFS
:: Bool :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m)
=> Bool
-> T.Text -> T.Text
-> FilePath -> FilePath
-> Maybe CIFSOpts -> Maybe CIFSOpts
-> Maybe PasswordConfig -> Maybe PasswordConfig
-> IO MountResult -> m MountResult
mountCIFS useSudo remote mountpoint opts pwdConfig = mountCIFS useSudo remote mountpoint opts pwdConfig =
withPasswordGetter pwdConfig runPwd run withPasswordGetter pwdConfig runPwd run
where where
@ -448,7 +467,12 @@ fromCIFSOpts o = T.intercalate "," $ mapMaybe concatMaybe fs
] ]
concatMaybe (k, f) = (\v -> T.concat [k, "=", v]) <$> f o concatMaybe (k, f) = (\v -> T.concat [k, "=", v]) <$> f o
mountVeracrypt :: FilePath -> Maybe PasswordConfig -> T.Text -> IO MountResult mountVeracrypt
:: (HasLogFunc c, MonadReader c m, MonadUnliftIO m)
=> FilePath
-> Maybe PasswordConfig
-> T.Text
-> m MountResult
mountVeracrypt mountpoint pwdConfig volume = mountVeracrypt mountpoint pwdConfig volume =
withPasswordGetter pwdConfig (runVeraCrypt (args ++ ["--stdin"])) $ withPasswordGetter pwdConfig (runVeraCrypt (args ++ ["--stdin"])) $
runVeraCrypt args "" runVeraCrypt args ""
@ -457,12 +481,12 @@ mountVeracrypt mountpoint pwdConfig volume =
-- NOTE: the user is assumed to have added themselves to the sudoers file so -- NOTE: the user is assumed to have added themselves to the sudoers file so
-- that this command will work -- that this command will work
runVeraCrypt :: [T.Text] -> T.Text -> IO MountResult runVeraCrypt :: MonadIO m => [T.Text] -> T.Text -> m MountResult
runVeraCrypt args = runMount "sudo" (defaultArgs ++ args) runVeraCrypt args = runMount "sudo" (defaultArgs ++ args)
where where
defaultArgs = ["/usr/bin/veracrypt", "--text", "--non-interactive"] defaultArgs = ["/usr/bin/veracrypt", "--text", "--non-interactive"]
veracryptMountState :: MountConfig -> RofiMountIO MountState veracryptMountState :: MountConfig -> MIO MountState
veracryptMountState mc = do veracryptMountState mc = do
mp <- getAbsMountpoint mc mp <- getAbsMountpoint mc
primary <- io $ lookupSpec mp primary <- io $ lookupSpec mp
@ -479,29 +503,29 @@ veracryptMountState mc = do
Just (i, _) -> if i `elem` ['0' .. '9'] then Just i else Nothing Just (i, _) -> if i `elem` ['0' .. '9'] then Just i else Nothing
_ -> Nothing _ -> Nothing
getAbsMountpoint :: MountConfig -> RofiMountIO FilePath getAbsMountpoint :: MountConfig -> MIO FilePath
getAbsMountpoint MountConfig {mpPath = m} = getAbsMountpoint MountConfig {mpPath = m} =
asks $ flip appendRoot (T.unpack m) . mountconfVolatilePath asks $ flip appendRoot (T.unpack m) . mountconfVolatilePath
getStaticActions :: RofiMountIO [(Header, ProtoAction)] getStaticActions :: MIO [(Header, ProtoAction)]
getStaticActions = mountableToAction $ asks $ configToTree' . mountconfStaticDevs getStaticActions = mountableToAction $ asks $ configToTree' . mountconfStaticDevs
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Password-getting functions for static devices -- Password-getting functions for static devices
type PasswordGetter = IO (Maybe T.Text) type PasswordGetter m = m (Maybe T.Text)
runSecret :: M.Map T.Text T.Text -> PasswordGetter runSecret :: MonadUnliftIO m => M.Map T.Text T.Text -> PasswordGetter m
runSecret kvs = readCmdSuccess "secret-tool" ("lookup" : kvs') "" runSecret kvs = readCmdSuccess "secret-tool" ("lookup" : kvs') ""
where where
kvs' = concatMap (\(k, v) -> [k, v]) $ M.toList kvs kvs' = concatMap (\(k, v) -> [k, v]) $ M.toList kvs
runBitwarden :: T.Text -> PasswordGetter runBitwarden :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => T.Text -> PasswordGetter m
runBitwarden pname = runBitwarden pname =
((password . login) <=< L.find (\i -> name i == pname)) ((password . login) <=< L.find (\i -> name i == pname))
<$> getItems <$> getItems
runPromptLoop :: Natural -> PasswordGetter -> PasswordGetter runPromptLoop :: MonadUnliftIO m => Natural -> PasswordGetter m -> PasswordGetter m
runPromptLoop n pwd = do runPromptLoop n pwd = do
res <- pwd res <- pwd
if isNothing res if isNothing res
@ -523,17 +547,18 @@ runPromptLoop n pwd = do
-- getPrompt = maybe (return Nothing) (flip runPromptLoop readPassword . promptTries) -- getPrompt = maybe (return Nothing) (flip runPromptLoop readPassword . promptTries)
-- runMaybe x y = (\r -> if isNothing r then y else return r) =<< x -- runMaybe x y = (\r -> if isNothing r then y else return r) =<< x
configToPwd :: PasswordConfig -> PasswordGetter configToPwd :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => PasswordConfig -> PasswordGetter m
configToPwd (PwdBW (BitwardenConfig {bwKey = k, bwTries = n})) = configToPwd (PwdBW (BitwardenConfig {bwKey = k, bwTries = n})) =
runPromptLoop n $ runBitwarden k runPromptLoop n $ runBitwarden k
configToPwd (PwdLS s) = runSecret $ M.fromList $ fmap (\(SecretMap k v) -> (k, v)) $ secretAttributes s configToPwd (PwdLS s) = runSecret $ M.fromList $ fmap (\(SecretMap k v) -> (k, v)) $ secretAttributes s
configToPwd (PwdPr p) = flip runPromptLoop readPassword $ promptTries p configToPwd (PwdPr p) = flip runPromptLoop readPassword $ promptTries p
withPasswordGetter withPasswordGetter
:: Maybe PasswordConfig :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m)
-> (T.Text -> IO MountResult) => Maybe PasswordConfig
-> IO MountResult -> (T.Text -> m MountResult)
-> IO MountResult -> m MountResult
-> m MountResult
withPasswordGetter (Just pwdConfig) runPwd _ = withPasswordGetter (Just pwdConfig) runPwd _ =
maybe (return $ MountError "Password could not be obtained") runPwd maybe (return $ MountError "Password could not be obtained") runPwd
=<< configToPwd pwdConfig =<< configToPwd pwdConfig
@ -590,7 +615,7 @@ getRemovableDevices =
_ -> Nothing _ -> Nothing
mk d l = Just $ Removable {removablePath = d, removableLabel = l} mk d l = Just $ Removable {removablePath = d, removableLabel = l}
getRemovableActions :: RofiMountIO [(Header, ProtoAction)] getRemovableActions :: MIO [(Header, ProtoAction)]
getRemovableActions = mountableToAction getRemovableDevices getRemovableActions = mountableToAction getRemovableDevices
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -625,7 +650,7 @@ instance Mountable MTPFS where
getLabel = mtpfsDescription getLabel = mtpfsDescription
-- | Return list of all available MTP devices -- | Return list of all available MTP devices
getMTPDevices :: RofiMountIO [MTPFS] getMTPDevices :: MIO [MTPFS]
getMTPDevices = do getMTPDevices = do
i <- io mtpExeInstalled i <- io mtpExeInstalled
if i then go else return [] if i then go else return []
@ -656,7 +681,7 @@ getMTPDevices = do
| c == ' ' = Just '-' | c == ' ' = Just '-'
| otherwise = Just c | otherwise = Just c
getMTPActions :: RofiMountIO [(Header, ProtoAction)] getMTPActions :: MIO [(Header, ProtoAction)]
getMTPActions = mountableToAction getMTPDevices getMTPActions = mountableToAction getMTPDevices
mtpExeInstalled :: IO Bool mtpExeInstalled :: IO Bool
@ -697,19 +722,19 @@ notify icon summary body =
data MountResult = MountSuccess | MountError T.Text deriving (Show, Eq) data MountResult = MountSuccess | MountError T.Text deriving (Show, Eq)
runMount :: T.Text -> [T.Text] -> T.Text -> IO MountResult runMount :: MonadIO m => T.Text -> [T.Text] -> T.Text -> m MountResult
runMount cmd args stdin_ = eitherToMountResult <$> readCmdEither cmd args stdin_ runMount cmd args stdin_ = eitherToMountResult <$> readCmdEither cmd args stdin_
runMount' :: T.Text -> [T.Text] -> T.Text -> [(T.Text, T.Text)] -> IO MountResult runMount' :: MonadIO m => T.Text -> [T.Text] -> T.Text -> [(T.Text, T.Text)] -> m MountResult
runMount' cmd args stdin_ environ = runMount' cmd args stdin_ environ =
eitherToMountResult eitherToMountResult
<$> readCmdEither' cmd args stdin_ environ <$> readCmdEither' cmd args stdin_ environ
runMountSudoMaybe :: Bool -> T.Text -> [T.Text] -> IO MountResult runMountSudoMaybe :: MonadIO m => Bool -> T.Text -> [T.Text] -> m MountResult
runMountSudoMaybe useSudo cmd args = runMountSudoMaybe useSudo cmd args =
runMountSudoMaybe' useSudo cmd args [] runMountSudoMaybe' useSudo cmd args []
runMountSudoMaybe' :: Bool -> T.Text -> [T.Text] -> [(T.Text, T.Text)] -> IO MountResult runMountSudoMaybe' :: MonadIO m => Bool -> T.Text -> [T.Text] -> [(T.Text, T.Text)] -> m MountResult
runMountSudoMaybe' useSudo cmd args environ = runMountSudoMaybe' useSudo cmd args environ =
maybe maybe
(runMount' cmd args "" environ) (runMount' cmd args "" environ)
@ -720,7 +745,7 @@ runMountSudoMaybe' useSudo cmd args environ =
-- runSudoMount :: T.Text -> T.Text -> [T.Text] -> T.Text -> IO MountResult -- runSudoMount :: T.Text -> T.Text -> [T.Text] -> T.Text -> IO MountResult
-- runSudoMount rootpass cmd args stdin = runSudoMount' rootpass cmd args stdin [] -- runSudoMount rootpass cmd args stdin = runSudoMount' rootpass cmd args stdin []
runSudoMount' :: T.Text -> T.Text -> [T.Text] -> [(T.Text, T.Text)] -> IO MountResult runSudoMount' :: MonadIO m => T.Text -> T.Text -> [T.Text] -> [(T.Text, T.Text)] -> m MountResult
runSudoMount' rootpass cmd args environ = runMount "sudo" args' rootpass runSudoMount' rootpass cmd args environ = runMount "sudo" args' rootpass
where where
args' = ["-S"] ++ environ' ++ [cmd] ++ args args' = ["-S"] ++ environ' ++ [cmd] ++ args
@ -758,7 +783,7 @@ lookupSpec mountpoint = M.lookup mountpoint <$> mountMap
-- base path in /tmp, so all this is saying is that umounting everything will -- base path in /tmp, so all this is saying is that umounting everything will
-- leave /tmp/media/USER without removing all the way down to /tmp) -- leave /tmp/media/USER without removing all the way down to /tmp)
rmDirOnMountError :: FilePath -> RofiMountIO MountResult -> RofiMountIO MountResult rmDirOnMountError :: FilePath -> MIO MountResult -> MIO MountResult
rmDirOnMountError d f = do rmDirOnMountError d f = do
res <- f res <- f
unless (res == MountSuccess) $ rmDirMaybe d unless (res == MountSuccess) $ rmDirMaybe d
@ -766,22 +791,22 @@ rmDirOnMountError d f = do
-- | Run a mount command and create the mountpoint if it does not exist, and -- | Run a mount command and create the mountpoint if it does not exist, and
-- remove the mountpoint if a mount error occurs -- remove the mountpoint if a mount error occurs
withTmpMountDir :: FilePath -> RofiMountIO MountResult -> RofiMountIO MountResult withTmpMountDir :: FilePath -> MIO MountResult -> MIO MountResult
withTmpMountDir m = withTmpMountDir m =
rmDirOnMountError m rmDirOnMountError m
. bracketOnError_ (mkDirMaybe m) (rmDirMaybe m) . bracketOnError_ (mkDirMaybe m) (rmDirMaybe m)
-- | Run an unmount command and remove the mountpoint if no errors occur -- | Run an unmount command and remove the mountpoint if no errors occur
runAndRemoveDir :: FilePath -> RofiMountIO MountResult -> RofiMountIO MountResult runAndRemoveDir :: FilePath -> MIO MountResult -> MIO MountResult
runAndRemoveDir m f = do runAndRemoveDir m f = do
res <- catch f (return . MountError . (T.pack . displayException :: SomeException -> T.Text)) res <- catch f (return . MountError . (T.pack . displayException :: SomeException -> T.Text))
when (res == MountSuccess) $ rmDirMaybe m when (res == MountSuccess) $ rmDirMaybe m
return res return res
mkDirMaybe :: FilePath -> RofiMountIO () mkDirMaybe :: FilePath -> MIO ()
mkDirMaybe fp = whenInMountDir fp $ io $ createDirectoryIfMissing True fp mkDirMaybe fp = whenInMountDir fp $ io $ createDirectoryIfMissing True fp
rmDirMaybe :: FilePath -> RofiMountIO () rmDirMaybe :: FilePath -> MIO ()
rmDirMaybe fp = rmDirMaybe fp =
whenInMountDir fp $ whenInMountDir fp $
unlessMountpoint fp $ unlessMountpoint fp $
@ -791,7 +816,7 @@ rmDirMaybe fp =
removePathForcibly cur removePathForcibly cur
rmUntil (takeDirectory cur) target rmUntil (takeDirectory cur) target
whenInMountDir :: FilePath -> RofiMountIO () -> RofiMountIO () whenInMountDir :: FilePath -> MIO () -> MIO ()
whenInMountDir fp f = do whenInMountDir fp f = do
mDir <- asks mountconfVolatilePath mDir <- asks mountconfVolatilePath
when (mDir `L.isPrefixOf` fp) f when (mDir `L.isPrefixOf` fp) f

View File

@ -4,17 +4,16 @@
module Main (main) where module Main (main) where
import Data.Maybe
import RIO import RIO
import qualified RIO.Text as T import qualified RIO.Text as T
import Rofi.Command import Rofi.Command
import System.Environment
import System.Process import System.Process
import UnliftIO.Environment
main :: IO () main :: IO ()
main = getArgs >>= runPrompt main = runSimpleApp $ getArgs >>= runPrompt
runPrompt :: [String] -> IO () runPrompt :: [String] -> RIO SimpleApp ()
runPrompt args = do runPrompt args = do
servers <- getServers servers <- getServers
maybe (return ()) run servers maybe (return ()) run servers
@ -42,19 +41,19 @@ type VPNServer = (T.Text, T.Text)
data VPNStatus = VPNStatus (Maybe T.Text) [VPNServer] deriving (Show) data VPNStatus = VPNStatus (Maybe T.Text) [VPNServer] deriving (Show)
getServers :: IO (Maybe VPNStatus) getServers :: MonadIO m => m (Maybe VPNStatus)
getServers = do getServers = do
running <- daemonIsRunning running <- daemonIsRunning
if running if running
then Just <$> getStatus then Just <$> getStatus
else notify IconError "ExpressVPN daemon not running" >> return Nothing else notify IconError "ExpressVPN daemon not running" >> return Nothing
getStatus :: IO VPNStatus getStatus :: MonadIO m => m VPNStatus
getStatus = do getStatus = do
connected <- getConnectedServer connected <- getConnectedServer
VPNStatus connected <$> getAvailableServers VPNStatus connected <$> getAvailableServers
getConnectedServer :: IO (Maybe T.Text) getConnectedServer :: MonadIO m => m (Maybe T.Text)
getConnectedServer = (procStatus =<<) <$> readCmdSuccess eVPN ["status"] "" getConnectedServer = (procStatus =<<) <$> readCmdSuccess eVPN ["status"] ""
where where
procStatus = listToMaybe . mapMaybe procLine . T.lines procStatus = listToMaybe . mapMaybe procLine . T.lines
@ -63,7 +62,7 @@ getConnectedServer = (procStatus =<<) <$> readCmdSuccess eVPN ["status"] ""
("\ESC[1;32;49mConnected" : "to" : server) -> Just $ T.unwords server ("\ESC[1;32;49mConnected" : "to" : server) -> Just $ T.unwords server
_ -> Nothing _ -> Nothing
getAvailableServers :: IO [VPNServer] getAvailableServers :: MonadIO m => m [VPNServer]
getAvailableServers = procOut =<< readCmdSuccess eVPN ["ls"] "" getAvailableServers = procOut =<< readCmdSuccess eVPN ["ls"] ""
where where
procOut Nothing = do procOut Nothing = do
@ -91,7 +90,7 @@ getAvailableServers = procOut =<< readCmdSuccess eVPN ["ls"] ""
matchLine [i, _, _, _, l] = Just (i, l) matchLine [i, _, _, _, l] = Just (i, l)
matchLine _ = Nothing matchLine _ = Nothing
daemonIsRunning :: IO Bool daemonIsRunning :: MonadIO m => m Bool
daemonIsRunning = isJust <$> readCmdSuccess "pgrep" [eVPND] "" daemonIsRunning = isJust <$> readCmdSuccess "pgrep" [eVPND] ""
getDisconnectAction :: T.Text -> VPNAction getDisconnectAction :: T.Text -> VPNAction
@ -119,7 +118,7 @@ eVPN = "expressvpn"
eVPND :: T.Text eVPND :: T.Text
eVPND = "expressvpnd" eVPND = "expressvpnd"
connect :: VPNServer -> IO () connect :: MonadIO m => VPNServer -> m ()
connect (sid, sname) = do connect (sid, sname) = do
res <- readCmdSuccess' eVPN ["connect", sid] res <- readCmdSuccess' eVPN ["connect", sid]
notifyIf notifyIf
@ -127,7 +126,7 @@ connect (sid, sname) = do
(T.append "connected to " sname) (T.append "connected to " sname)
(T.append "failed to connect to " sname) (T.append "failed to connect to " sname)
disconnect :: T.Text -> IO Bool disconnect :: MonadIO m => T.Text -> m Bool
disconnect server = do disconnect server = do
res <- readCmdSuccess' eVPN ["disconnect"] res <- readCmdSuccess' eVPN ["disconnect"]
notifyIf notifyIf
@ -136,7 +135,7 @@ disconnect server = do
(T.append "failed to disconnect from " server) (T.append "failed to disconnect from " server)
return res return res
readCmdSuccess' :: T.Text -> [T.Text] -> IO Bool readCmdSuccess' :: MonadIO m => T.Text -> [T.Text] -> m Bool
readCmdSuccess' cmd args = isJust <$> readCmdSuccess cmd args "" readCmdSuccess' cmd args = isJust <$> readCmdSuccess cmd args ""
-- TODO not DRY -- TODO not DRY
@ -146,12 +145,12 @@ instance Show NotifyIcon where
show IconError = "dialog-error-symbolic" show IconError = "dialog-error-symbolic"
show IconInfo = "dialog-information-symbolic" show IconInfo = "dialog-information-symbolic"
notifyIf :: Bool -> T.Text -> T.Text -> IO () notifyIf :: MonadIO m => Bool -> T.Text -> T.Text -> m ()
notifyIf True s _ = notify IconInfo s notifyIf True s _ = notify IconInfo s
notifyIf False _ s = notify IconError s notifyIf False _ s = notify IconError s
notify :: NotifyIcon -> T.Text -> IO () notify :: MonadIO m => NotifyIcon -> T.Text -> m ()
notify icon body = void $ spawnProcess "notify-send" $ args ++ [T.unpack body] notify icon body = liftIO $ void $ spawnProcess "notify-send" $ args ++ [T.unpack body]
where where
args = ["-i", show icon, summary] args = ["-i", show icon, summary]
summary = "ExpressVPN" summary = "ExpressVPN"

View File

@ -12,7 +12,6 @@ where
import DBus import DBus
import DBus.Client import DBus.Client
import Data.Aeson import Data.Aeson
import qualified Data.Text.IO as TI
import Data.UnixTime import Data.UnixTime
import GHC.Generics import GHC.Generics
import RIO hiding (timeout) import RIO hiding (timeout)
@ -37,23 +36,23 @@ newtype BWServerConf = BWServerConf
-- TODO add a cache so the browse list will load faster -- TODO add a cache so the browse list will load faster
data CurrentSession = CurrentSession data CurrentSession = CurrentSession
{ timestamp :: UnixTime { timestamp :: !UnixTime
, hash :: T.Text , hash :: !T.Text
} }
type Session = MVar (Maybe CurrentSession) type Session = MVar (Maybe CurrentSession)
runDaemon :: Int -> IO () runDaemon :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => Int -> m ()
runDaemon t = do runDaemon t = do
ses <- newMVar Nothing ses <- newMVar Nothing
let c = BWServerConf {timeout = UnixDiffTime (fromIntegral t) 0} let c = BWServerConf {timeout = UnixDiffTime (fromIntegral t) 0}
startService c ses startService c ses
forever $ threadDelay 1000000 forever $ threadDelay 1000000
lockSession :: Session -> IO () lockSession :: MonadIO m => Session -> m ()
lockSession ses = void $ swapMVar ses Nothing lockSession ses = void $ swapMVar ses Nothing
syncSession :: BWServerConf -> Session -> IO () syncSession :: MonadUnliftIO m => BWServerConf -> Session -> m ()
syncSession conf ses = notify =<< fmap join . mapM cmd =<< getSession' conf ses syncSession conf ses = notify =<< fmap join . mapM cmd =<< getSession' conf ses
where where
cmd h = readCmdSuccess "bw" ["sync", "--session", h] "" cmd h = readCmdSuccess "bw" ["sync", "--session", h] ""
@ -61,9 +60,9 @@ syncSession conf ses = notify =<< fmap join . mapM cmd =<< getSession' conf ses
let j = isJust res let j = isJust res
in notifyStatus j $ if j then "sync succeeded" else "sync failed" in notifyStatus j $ if j then "sync succeeded" else "sync failed"
getSession' :: BWServerConf -> Session -> IO (Maybe T.Text) getSession' :: MonadUnliftIO m => BWServerConf -> Session -> m (Maybe T.Text)
getSession' BWServerConf {timeout = t} ses = do getSession' BWServerConf {timeout = t} ses = do
ut <- getUnixTime ut <- liftIO $ getUnixTime
modifyMVar ses $ \s -> case s of modifyMVar ses $ \s -> case s of
Just CurrentSession {timestamp = ts, hash = h} -> Just CurrentSession {timestamp = ts, hash = h} ->
if diffUnixTime ut ts > t then getNewSession else return (s, Just h) if diffUnixTime ut ts > t then getNewSession else return (s, Just h)
@ -74,18 +73,18 @@ getSession' BWServerConf {timeout = t} ses = do
newHash <- join <$> mapM readSession pwd newHash <- join <$> mapM readSession pwd
(,newHash) <$> mapM newSession newHash (,newHash) <$> mapM newSession newHash
newSession h = do newSession h = do
ut <- getUnixTime ut <- liftIO $ getUnixTime
return CurrentSession {timestamp = ut, hash = h} return CurrentSession {timestamp = ut, hash = h}
getSession :: BWServerConf -> Session -> IO T.Text getSession :: MonadUnliftIO m => BWServerConf -> Session -> m T.Text
getSession conf ses = fromMaybe "" <$> getSession' conf ses getSession conf ses = fromMaybe "" <$> getSession' conf ses
readSession :: T.Text -> IO (Maybe T.Text) readSession :: MonadIO m => T.Text -> m (Maybe T.Text)
readSession pwd = readCmdSuccess "bw" ["unlock", pwd, "--raw"] "" readSession pwd = readCmdSuccess "bw" ["unlock", pwd, "--raw"] ""
notifyStatus :: Bool -> T.Text -> IO () notifyStatus :: MonadIO m => Bool -> T.Text -> m ()
notifyStatus succeeded msg = notifyStatus succeeded msg =
void $ spawnProcess "notify-send" ["-i", i, T.unpack msg] void $ liftIO $ spawnProcess "notify-send" ["-i", i, T.unpack msg]
where where
i = i =
if succeeded if succeeded
@ -108,15 +107,21 @@ notifyStatus succeeded msg =
-- - username (if applicable) -> copy to clipboard -- - username (if applicable) -> copy to clipboard
-- - password (if applicable) -> copy to clipboard -- - password (if applicable) -> copy to clipboard
-- - anything else (notes and such) -> copy to clipboard -- - anything else (notes and such) -> copy to clipboard
newtype BWClientConf = BWClientConf [T.Text] data BWClientConf c = BWClientConf
{ bwArgs :: ![T.Text]
, bwEnv :: !c
}
instance HasRofiConf BWClientConf where instance HasRofiConf (BWClientConf c) where
defArgs (BWClientConf a) = a defArgs = bwArgs
runClient :: [T.Text] -> IO () instance HasLogFunc c => HasLogFunc (BWClientConf c) where
runClient a = do logFuncL = lens bwEnv (\x y -> x {bwEnv = y}) . logFuncL
let c = BWClientConf a
runRofi c $ runClient :: HasLogFunc c => [T.Text] -> RIO c ()
runClient a =
mapRIO (BWClientConf a) $
selectAction $
emptyMenu emptyMenu
{ groups = [untitledGroup $ toRofiActions ras] { groups = [untitledGroup $ toRofiActions ras]
, prompt = Just "Action" , prompt = Just "Action"
@ -124,19 +129,19 @@ runClient a = do
where where
ras = ras =
[ ("Browse Logins", browseLogins) [ ("Browse Logins", browseLogins)
, ("Sync Session", io callSyncSession) , ("Sync Session", callSyncSession)
, ("Lock Session", io callLockSession) , ("Lock Session", callLockSession)
] ]
browseLogins :: HasRofiConf c => RIO c () browseLogins :: (HasLogFunc c, HasRofiConf c) => RIO c ()
browseLogins = io getItems >>= selectItem browseLogins = getItems >>= selectItem
getItems :: IO [Item] getItems :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => m [Item]
getItems = maybe (return []) getItems' =<< callGetSession getItems = maybe (return []) getItems' =<< callGetSession
getItems' :: T.Text -> IO [Item] getItems' :: MonadIO m => T.Text -> m [Item]
getItems' session = do getItems' session = do
items <- io $ readProcess "bw" ["list", "items", "--session", T.unpack session] "" items <- liftIO $ readProcess "bw" ["list", "items", "--session", T.unpack session] ""
return $ filter notEmpty $ fromMaybe [] $ decode $ fromString items return $ filter notEmpty $ fromMaybe [] $ decode $ fromString items
where where
notEmpty Item {login = Login {username = Nothing, password = Nothing}} = notEmpty Item {login = Login {username = Nothing, password = Nothing}} =
@ -166,7 +171,7 @@ instance FromJSON Login
-- TODO make menu buttons here to go back and to copy without leaving -- TODO make menu buttons here to go back and to copy without leaving
-- the current menu -- the current menu
selectItem :: HasRofiConf c => [Item] -> RIO c () selectItem :: (HasLogFunc c, HasRofiConf c) => [Item] -> RIO c ()
selectItem items = selectItem items =
selectAction $ selectAction $
emptyMenu emptyMenu
@ -174,10 +179,10 @@ selectItem items =
, prompt = Just "Login" , prompt = Just "Login"
} }
itemsToRofiActions :: HasRofiConf c => [Item] -> RofiActions c itemsToRofiActions :: (HasLogFunc c, HasRofiConf c) => [Item] -> RofiActions c
itemsToRofiActions = toRofiActions . fmap (\i -> (name i, selectCopy $ login i)) itemsToRofiActions = toRofiActions . fmap (\i -> (name i, selectCopy $ login i))
selectCopy :: HasRofiConf c => Login -> RIO c () selectCopy :: (HasLogFunc c, HasRofiConf c) => Login -> RIO c ()
selectCopy l = selectCopy l =
selectAction $ selectAction $
emptyMenu emptyMenu
@ -213,17 +218,18 @@ loginToRofiActions Login {username = u, password = p} a =
user = copyIfJust fmtUsername u user = copyIfJust fmtUsername u
pwd = copyIfJust fmtPassword p pwd = copyIfJust fmtPassword p
getItemPassword' :: BWServerConf -> Session -> T.Text -> IO (Maybe T.Text) getItemPassword' :: MonadUnliftIO m => BWServerConf -> Session -> T.Text -> m (Maybe T.Text)
getItemPassword' conf session item = mapM getPwd =<< getSession' conf session getItemPassword' conf session item = mapM getPwd =<< getSession' conf session
where where
getPwd s = getPwd = fmap T.pack . pr
T.pack pr s =
<$> readProcess liftIO $
readProcess
"bw" "bw"
["get", "password", T.unpack item, "--session", T.unpack s] ["get", "password", T.unpack item, "--session", T.unpack s]
"" ""
getItemPassword :: BWServerConf -> Session -> T.Text -> IO T.Text getItemPassword :: MonadUnliftIO m => BWServerConf -> Session -> T.Text -> m T.Text
getItemPassword conf session item = getItemPassword conf session item =
fromMaybe "" fromMaybe ""
<$> getItemPassword' conf session item <$> getItemPassword' conf session item
@ -231,22 +237,23 @@ getItemPassword conf session item =
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- | DBus -- | DBus
startService :: BWServerConf -> Session -> IO () startService :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => BWServerConf -> Session -> m ()
startService c ses = do startService c ses = do
client <- connectSession client <- liftIO $ connectSession
let flags = [nameAllowReplacement, nameReplaceExisting] let flags = [nameAllowReplacement, nameReplaceExisting]
_ <- requestName client busname flags _ <- liftIO $ requestName client busname flags
TI.putStrLn "Started rofi bitwarden dbus client" logInfo "Started rofi bitwarden dbus client"
withRunInIO $ \runIO ->
export export
client client
path path
defaultInterface defaultInterface
{ interfaceName = interface { interfaceName = interface
, interfaceMethods = , interfaceMethods =
[ autoMethod memGetSession $ getSession c ses [ autoMethod memGetSession $ runIO $ getSession c ses
, autoMethod memLockSession $ lockSession ses , autoMethod memLockSession $ runIO $ lockSession ses
, autoMethod memSyncSession $ syncSession c ses , autoMethod memSyncSession $ runIO $ syncSession c ses
, autoMethod memGetPassword $ getItemPassword c ses , autoMethod memGetPassword $ runIO . getItemPassword c ses
] ]
} }
@ -271,20 +278,25 @@ memSyncSession = "SyncSession"
memGetPassword :: MemberName memGetPassword :: MemberName
memGetPassword = "GetPassword" memGetPassword = "GetPassword"
callMember :: MemberName -> IO [Variant] callMember :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => MemberName -> m [Variant]
callMember m = do callMember m = do
reply <- callMethod $ methodCall path interface m reply <- callMethod $ methodCall path interface m
case reply of case reply of
Left err -> TI.putStrLn (T.pack (methodErrorMessage err)) >> return [] Left err -> do
logError $
displayBytesUtf8 $
encodeUtf8 $
(T.pack (methodErrorMessage err))
return []
Right body -> return body Right body -> return body
callLockSession :: IO () callLockSession :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => m ()
callLockSession = void $ callMember memLockSession callLockSession = void $ callMember memLockSession
callSyncSession :: IO () callSyncSession :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => m ()
callSyncSession = void $ callMember memSyncSession callSyncSession = void $ callMember memSyncSession
callGetSession :: IO (Maybe T.Text) callGetSession :: (HasLogFunc c, MonadReader c m, MonadUnliftIO m) => m (Maybe T.Text)
callGetSession = getBodyString <$> callMember memGetSession callGetSession = getBodyString <$> callMember memGetSession
-- TODO maybe will need to add a caller for getItemPassword -- TODO maybe will need to add a caller for getItemPassword
@ -295,8 +307,8 @@ getBodyString [b] = case fromVariant b :: Maybe T.Text of
s -> s s -> s
getBodyString _ = Nothing getBodyString _ = Nothing
callMethod :: MethodCall -> IO (Either MethodError [Variant]) callMethod :: MonadIO m => MethodCall -> m (Either MethodError [Variant])
callMethod mc = do callMethod mc = liftIO $ do
client <- connectSession client <- connectSession
reply <- call client mc {methodCallDestination = Just busname} reply <- call client mc {methodCallDestination = Just busname}
disconnect client disconnect client

View File

@ -149,27 +149,29 @@ readRofi uargs input = do
dargs <- asks defArgs dargs <- asks defArgs
io $ readCmdEither "rofi" (dmenuArgs ++ dargs ++ uargs) input io $ readCmdEither "rofi" (dmenuArgs ++ dargs ++ uargs) input
readCmdSuccess :: T.Text -> [T.Text] -> T.Text -> IO (Maybe T.Text) readCmdSuccess :: MonadIO m => T.Text -> [T.Text] -> T.Text -> m (Maybe T.Text)
readCmdSuccess cmd args input = readCmdSuccess cmd args input =
either (const Nothing) Just either (const Nothing) Just
<$> readCmdEither cmd args input <$> readCmdEither cmd args input
readCmdEither readCmdEither
:: T.Text :: MonadIO m
=> T.Text
-> [T.Text] -> [T.Text]
-> T.Text -> T.Text
-> IO (Either (Int, T.Text, T.Text) T.Text) -> m (Either (Int, T.Text, T.Text) T.Text)
readCmdEither cmd args input = readCmdEither' cmd args input [] readCmdEither cmd args input = readCmdEither' cmd args input []
readCmdEither' readCmdEither'
:: T.Text :: MonadIO m
=> T.Text
-> [T.Text] -> [T.Text]
-> T.Text -> T.Text
-> [(T.Text, T.Text)] -> [(T.Text, T.Text)]
-> IO (Either (Int, T.Text, T.Text) T.Text) -> m (Either (Int, T.Text, T.Text) T.Text)
readCmdEither' cmd args input environ = readCmdEither' cmd args input environ =
resultToEither resultToEither
<$> readCreateProcessWithExitCode p (T.unpack input) <$> (liftIO $ readCreateProcessWithExitCode p (T.unpack input))
where where
e = case environ of e = case environ of
[] -> Nothing [] -> Nothing
@ -187,10 +189,10 @@ resultToEither (ExitFailure n, out, err) =
joinNewline :: [T.Text] -> T.Text joinNewline :: [T.Text] -> T.Text
joinNewline = T.intercalate "\n" joinNewline = T.intercalate "\n"
readPassword :: IO (Maybe T.Text) readPassword :: MonadIO m => m (Maybe T.Text)
readPassword = readPassword' "Password" readPassword = readPassword' "Password"
readPassword' :: T.Text -> IO (Maybe T.Text) readPassword' :: MonadIO m => T.Text -> m (Maybe T.Text)
readPassword' p = readCmdSuccess "rofi" args "" readPassword' p = readCmdSuccess "rofi" args ""
where where
args = dmenuArgs ++ ["-p", p, "-password"] args = dmenuArgs ++ ["-p", p, "-password"]