greenclip-1.3.0: src/Main.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
module Main where
import ClassyPrelude as CP hiding (readFile)
import Data.Binary (decode, encode)
import qualified Data.Text as T
import qualified Data.Vector as V
import Lens.Micro
import Lens.Micro.Mtl
import qualified System.Clipboard as Clip
import qualified System.Directory as Dir
import System.Environment (lookupEnv)
import System.IO (IOMode (..), openFile)
import System.Timeout (timeout)
data Command = DAEMON | PRINT | COPY Text | CLEAR | HELP deriving (Show, Read)
data Config = Config
{ maxHistoryLength :: Int
, historyPath :: String
, staticHistoryPath :: String
, usePrimarySelectionAsInput :: Bool
} deriving (Show, Read)
type ClipHistory = Vector Text
readFile :: FilePath -> IO ByteString
readFile filepath = bracket (openFile filepath ReadMode) hClose $ \h -> do
str <- hGetContents h
let !str' = str
return str'
getHistory :: (MonadIO m, MonadReader Config m) => m ClipHistory
getHistory = do
storePath <- view (to historyPath)
liftIO $ readH storePath `catchAnyDeep` const mempty
where
readH filePath = readFile filePath <&> fromList . decode . fromStrict
getStaticHistory :: (MonadIO m, MonadReader Config m) => m ClipHistory
getStaticHistory = do
storePath <- view (to staticHistoryPath)
liftIO $ readH storePath `catchAnyDeep` const mempty
where
readH filePath = readFile filePath <&> fromList . T.lines . decodeUtf8
storeHistory :: (MonadIO m, MonadReader Config m) => ClipHistory -> m ()
storeHistory history = do
storePath <- view (to historyPath)
liftIO $ writeH storePath history `catchAnyDeep` const mempty
where
writeH storePath = writeFile storePath . toStrict . encode . toList
appendToHistory :: (MonadIO m, MonadReader Config m) => Text -> ClipHistory -> m ClipHistory
appendToHistory sel history =
let selection = T.strip sel in
if selection == mempty || selection == fromMaybe mempty (headMay history)
then return history
else do
maxLen <- view (to maxHistoryLength)
return $ fst . V.splitAt maxLen . cons selection $ filter (/= selection) history
runDaemon :: (MonadIO m, MonadReader Config m, MonadCatch m) => m ()
runDaemon = do
usePrimary <- view (to usePrimarySelectionAsInput)
let getSelections = (getSelectionFrom Clip.getClipboardString, mempty)
: [(getSelectionFrom Clip.getPrimaryClipboard, mempty) | usePrimary]
forever $ (getHistory >>= go getSelections) `catchAnyDeep` handleError
where
_0_5sec :: Int
_0_5sec = 5 * 10^(5::Int)
getSelection [] = return ([], mempty)
getSelection ((getSel, lastSel):getSels) = do
selection <- liftIO getSel
if selection /= lastSel
then return ((getSel, selection) : getSels, selection)
else getSelection getSels >>= \(e, selection) -> return ((getSel, lastSel) : e, selection)
go getSelections history = do
(getSelections', selection) <- liftIO $ getSelection getSelections
history' <- appendToHistory selection history
when (history' /= history) (storeHistory history')
liftIO $ threadDelay _0_5sec
go getSelections' history'
getSelectionFrom fn = timeout 1000000 fn <&> T.pack . fromMaybe mempty . join
handleError ex = do
let displayMissing = "openDisplay" `T.isInfixOf` tshow ex
if displayMissing
then error "X display not available. Please start Xorg before running greenclip"
else sayErrShow ex
toRofiStr :: Text -> Text
toRofiStr = T.map (\c -> if c == '\n' || c == '\r' then '\xA0' else c)
fromRofiStr :: Text -> Text
fromRofiStr = T.map (\c -> if c == '\xA0' then '\n' else c)
printHistoryForRofi :: (MonadIO m, MonadReader Config m) => m ()
printHistoryForRofi = do
history <- mappend <$> getHistory <*> getStaticHistory
_ <- traverse (putStrLn . toRofiStr) history
return ()
advertiseSelection :: Text -> IO ()
advertiseSelection = Clip.setClipboardString . T.unpack . fromRofiStr
getConfig :: IO Config
getConfig = do
home <- Dir.getHomeDirectory
let cfgPath = home </> ".config/greenclip.cfg"
cfgStr <- (readFile cfgPath <&> T.strip . decodeUtf8) `catchAnyDeep` const mempty
let cfg = fromMaybe (defaultConfig home) (readMay cfgStr)
writeFile cfgPath (fromString $ show cfg)
return cfg
where
defaultConfig home = Config 25 (home </> ".cache/greenclip.history") (home </> ".cache/greenclip.staticHistory") False
parseArgs :: [Text] -> Command
parseArgs ("daemon":_) = DAEMON
parseArgs ["clear"] = CLEAR
parseArgs ["print"] = PRINT
parseArgs ["print", sel] = COPY sel
parseArgs _ = HELP
run :: Command -> IO ()
run cmd = do
cfg <- getConfig
case cmd of
DAEMON -> runReaderT runDaemon cfg
PRINT -> runReaderT printHistoryForRofi cfg
CLEAR -> runReaderT (storeHistory mempty) cfg
-- Should rename COPY into ADVERTISE but as greenclip is already used I don't want to break configs
-- of other people
COPY sel -> advertiseSelection sel
HELP -> putStrLn $ "greenclip v1.3 -- Recyle your clipboard selections\n\n" <>
"Available commands\n" <>
"daemon: Spawn the daemon that will listen to selections\n" <>
"print: Display all selections history\n" <>
"clear: Clear history\n" <>
"help: Display this message\n"
main :: IO ()
main = do
displayPresent <- lookupEnv "DISPLAY"
case displayPresent of
Nothing -> putStrLn "X display not available. Please start Xorg before running greenclip"
_ -> getArgs >>= run .parseArgs