greenclip 3.4.0 → 4.0.0
raw patch · 4 files changed
+128/−51 lines, 4 filesdep +base16-bytestringdep +cryptohash-md5dep +tomland
Dependencies added: base16-bytestring, cryptohash-md5, tomland
Files
- README.md +28/−10
- greenclip.cabal +6/−3
- src/Clipboard.hs +8/−1
- src/Main.hs +86/−37
README.md view
@@ -4,7 +4,7 @@ ## Description -Recyle your clipboard selections with greenclip and don't waste your time anymore+Recycle your clipboard selections with greenclip and don't waste your time anymore to reselect things over and over. **Purpose:**@@ -16,24 +16,42 @@ **Features:** + Integrated with [rofi](https://github.com/DaveDavenport/rofi)- + Permanently set some selections to added at the end (set `staticHistoryPath = your/file/with/static/entries` in the config file)- + Merge X Primary selection with clipboard selection (set `usePrimarySelectionAsInput = True` in the config file)+ + Permanently set some selections to added at the end (set `static_history = []` in the config file)+ + Merge X Primary selection with clipboard selection (set `use_primary_selection_as_input = true` in the config file) + Blacklist some applications (see `I want to blacklist some applications !` in the FAQ section)- + Copy small images+ + Copy small images (you can disable it in the config) ## Installation -1. It's a static binary so drop it anywhere in your $PATH env+- It's a static binary so drop it anywhere in your $PATH env -```wget https://github.com/erebe/greenclip/releases/download/3.3/greenclip```+```wget https://github.com/erebe/greenclip/releases/download/v4.2/greenclip``` -Alternatively if you are using Archlinux you can install the package from AUR +- Alternatively if you are using Archlinux you can install the package from AUR+ ``yay rofi-greenclip`` PS: If you want, you can add a permanent list of selections to be added to your current history. Go see the config file +Configuration file can be found at: +```+❯ cat ~/.config/greenclip.toml +[greenclip]+ history_file = "/home/erebe/.cache/greenclip.history"+ max_history_length = 50+ max_selection_size_bytes = 0+ trim_space_from_selection = true+ use_primary_selection_as_input = false+ blacklisted_applications = []+ enable_image_support = true+ image_cache_directory = "/tmp/greenclip"+ static_history = [+ '''¯\_(ツ)_/¯''',+]+```+ ## Usage Greenclip is intended to be used with [rofi](https://github.com/DaveDavenport/rofi)@@ -41,7 +59,7 @@ 1. Spawn the daemon ``` greenclip daemon ``` 2. When ever you need to get your selections history ``` rofi -modi "clipboard:greenclip print" -show clipboard -run-command '{cmd}' ``` 3. The entry that you have selected will be in your clipboard now-4. Configuration file can be found in ```.config/greenclip.cfg```+4. Configuration file can be found in ```.config/greenclip.toml``` ## Migrating from 2.x version to 3.x one @@ -109,9 +127,9 @@ A. You can also use greenclip with [dmenu](http://tools.suckless.org/dmenu) or [fzf](https://github.com/junegunn/fzf). Example usage: - `greenclip print | sed '/^$/d' | dmenu -i -l 10 -p clipboard | xargs -r -d'\n' -I '{}' greenclip print '{}'`+ `greenclip print | grep . | dmenu -i -l 10 -p clipboard | xargs -r -d'\n' -I '{}' greenclip print '{}'` - `greenclip print | sed '/^$/d' | fzf -e | xargs -r -d'\n' -I '{}' greenclip print '{}'`+ `greenclip print | grep . | fzf -e | xargs -r -d'\n' -I '{}' greenclip print '{}'` ----------
greenclip.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack name: greenclip-version: 3.4.0+version: 4.0.0 synopsis: Simple clipboard manager to be integrated with rofi description: Simple clipboard manager to be integrated with rofi - Please see README.md category: Linux Desktop@@ -23,7 +23,7 @@ main-is: Main.hs hs-source-dirs: src- ghc-options: -Wall -O3+ ghc-options: -Wall -O3 -Wno-name-shadowing build-depends: base >= 4 && < 5 , protolude@@ -39,9 +39,12 @@ , unix , directory , wordexp+ , tomland+ , base16-bytestring+ , cryptohash-md5 , X11 >= 1.6 other-modules: Clipboard default-language: Haskell2010 default-extensions: Strict- pkgconfig-depends: x11, xcb, xau, xdmcp+ pkgconfig-depends: x11, xcb, xau, xdmcp, xscrnsaver
src/Clipboard.hs view
@@ -16,6 +16,7 @@ import Data.Binary (Binary) import qualified Data.ByteString as B+import qualified Data.Text as T import Lens.Micro import System.Directory (setCurrentDirectory)@@ -33,6 +34,13 @@ | BITMAP ByteString deriving (Show, Eq, Generic, Binary) +selectionLength :: Selection -> Int+selectionLength (Selection _ (UTF8 a)) = T.length a+selectionLength (Selection _ (PNG a)) = B.length a+selectionLength (Selection _ (JPEG a)) = B.length a+selectionLength (Selection _ (BITMAP a)) = B.length a++ data Selection = Selection { appName :: Text , selection :: SelectionType@@ -47,7 +55,6 @@ , mimesPriorities :: [Atom] , defaultMime :: Atom } deriving (Show)- test :: IO () test =
src/Main.hs view
@@ -1,3 +1,4 @@+ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -13,37 +14,59 @@ import Protolude.Conv (toS) import Control.Monad.Catch (MonadCatch, catchAll)+import qualified Crypto.Hash.MD5 as H import Data.Binary (decodeFile, encode) import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as BB+import qualified Data.ByteString.Char8 as BC import Data.List (dropWhileEnd) import qualified Data.Text as T+import qualified Data.Text.Encoding as TE import Data.Vector (Vector) import qualified Data.Vector as V import Lens.Micro-import Lens.Micro.Mtl+import Lens.Micro.Mtl hiding ((.=)) import qualified System.Directory as Dir import System.Posix.Files (setFileMode) import System.Environment (lookupEnv) import System.IO (hClose, hGetContents) import System.Timeout (timeout) import System.Wordexp.Simple (wordexp)+--import System.Posix.Temp (mkdtemp) +import Toml (TomlCodec, (.=))+import qualified Toml+ import qualified Clipboard as Clip -data Command = DAEMON | PRINT | COPY Text | CLEAR | HELP deriving (Show, Read)+data Command = DAEMON | PRINT | COPY Text | CLEAR | PRUNE FilePath | HELP deriving (Show, Read) data Config = Config { maxHistoryLength :: Int+ , maxItemSizeBytes :: Int , historyPath :: Text- , staticHistoryPath :: Text , imageCachePath :: Text , usePrimarySelectionAsInput :: Bool , blacklistedApps :: [Text] , trimSpaceFromSelection :: Bool , enableImageSupport :: Bool+ , staticHistory :: [Text] } deriving (Show, Read) +configCodec :: TomlCodec Config+configCodec = Config+ <$> Toml.int "max_history_length" .= maxHistoryLength+ <*> Toml.int "max_selection_size_bytes" .= maxItemSizeBytes+ <*> Toml.text "history_file" .= historyPath+ <*> Toml.text "image_cache_directory" .= imageCachePath+ <*> Toml.bool "use_primary_selection_as_input" .= usePrimarySelectionAsInput+ <*> Toml.arrayOf Toml._Text "blacklisted_applications" .= blacklistedApps+ <*> Toml.bool "trim_space_from_selection" .= trimSpaceFromSelection+ <*> Toml.bool "enable_image_support" .= enableImageSupport+ <*> Toml.arrayOf Toml._Text "static_history" .= staticHistory++ type ClipHistory = Vector Clip.Selection readFile :: FilePath -> IO ByteString@@ -60,13 +83,11 @@ getStaticHistory :: (MonadIO m, MonadReader Config m) => m ClipHistory getStaticHistory = do- storePath <- view $ to (toS . staticHistoryPath)- liftIO $ readH storePath `catchAll` const mempty- where- readH filePath = readFile filePath <&> V.fromList . fmap toSelection . T.lines . toS- toSelection txt = Clip.Selection "greenclip" (Clip.UTF8 txt)+ history <- view $ to (staticHistory)+ return . V.fromList $ Clip.Selection "greenclip" . Clip.UTF8 <$> history + storeHistory :: (MonadIO m, MonadReader Config m) => ClipHistory -> m () storeHistory history = do storePath <- view $ to (toS . historyPath)@@ -75,6 +96,17 @@ writeH storePath = B.writeFile storePath . toS . encode . V.toList +pruneHistory :: (MonadIO m, MonadReader Config m) => FilePath -> m ()+pruneHistory path = do+ targets <- liftIO $ BC.lines <$> readFile path+ history <- getHistory+ storeHistory $ V.filter (go targets) history+ where+ go ts (Clip.Selection _ (Clip.UTF8 x)) = hashText x `notElem` ts+ go _ _ = False+ hashText = BB.encode . H.hash . TE.encodeUtf8++ appendToHistory :: (MonadIO m, MonadReader Config m) => Clip.Selection -> ClipHistory -> m (ClipHistory, ClipHistory) appendToHistory sel history' = do trimSelection <- view $ to trimSpaceFromSelection@@ -151,8 +183,16 @@ innerloop :: (MonadIO m, MonadReader Config m) => [(IO (Maybe Clip.Selection), Maybe Clip.Selection)] -> ClipHistory -> m ClipHistory innerloop getSelections history = do -- Get selection from enabled clipboards- (getSelections', sel) <- liftIO $ getSelection getSelections+ (getSelections', rawSelection) <- liftIO $ getSelection getSelections + -- Do not store selection items above threshold size+ maxItemSize <- view (to maxItemSizeBytes)+ let sel = case rawSelection of+ Nothing -> Nothing+ Just selection -> if maxItemSize > 0 && Clip.selectionLength selection >= maxItemSize+ then Nothing+ else Just selection+ -- Do not use selection coming from blacklisted app liftIO $ when (isJust sel) (print (Clip.appName <$> sel)) blacklist <- view (to blacklistedApps)@@ -224,40 +264,47 @@ getConfig :: IO Config getConfig = do home <- Dir.getHomeDirectory- let cfgPath = home <> "/.config/greenclip.cfg"-- cfgStr <- readFile cfgPath `catchAll` const mempty-- let unprettyCfg' = cfgStr & T.strip . T.replace "\n" "" . toS- let unprettyCfg = if "trimSpaceFromSelection" `T.isInfixOf` unprettyCfg'- then unprettyCfg'- else T.replace "}" ", trimSpaceFromSelection = True }" unprettyCfg'- let cfgMaybe = readMaybe $ toS unprettyCfg- let cfg = fromMaybe defaultConfig cfgMaybe-- -- Write back the config file if the current one was invalid- _ <- if isNothing cfgMaybe || unprettyCfg /= unprettyCfg'- then let prettyCfg = show cfg & T.replace "," ",\n" . T.replace "{" "{\n " . T.replace "}" "\n}"- in writeFile cfgPath prettyCfg- else return ()-- historyPath' <- expandHomeDir $ cfg ^. to historyPath- staticHistoryPath' <- expandHomeDir $ cfg ^. to staticHistoryPath- imageCachePath' <- expandHomeDir $ cfg ^. to imageCachePath-- return $ cfg { historyPath = historyPath'- , staticHistoryPath = staticHistoryPath'- , imageCachePath = imageCachePath'- }+ let configPath = home <> "/.config/greenclip.toml"+ + configExist <- Dir.doesFileExist configPath+ when (not configExist) $ do + config <- Toml.encode (Toml.table configCodec "greenclip") <$> defaultConfig+ writeFile configPath config+ return ()+ + tomlRes <- Toml.decodeFileEither (Toml.table configCodec "greenclip") configPath+ when (isLeft tomlRes) $ do+ die . toS $ "Error parsing the config file at " <> (show configPath) <> "\n" <> Toml.prettyTomlDecodeErrors (fromLeft mempty tomlRes)+ + let cfg = fromRight (Config 50 0 "" "" False [] True True []) tomlRes + + -- Replace $HOME|~|*... in config path+ cfg <- do+ imgCachePath <- wordexp . toS $ imageCachePath cfg+ historyP <- wordexp . toS $ historyPath cfg + return $ cfg { imageCachePath = (toS $ headDef "" imgCachePath), historyPath = (toS $ headDef "" historyP)}+ + -- if it ends with / we don't create a temp directory+ -- user is responsible for it+ -- cfg <- if (lastDef ' ' (toS $ imageCachePath cfg) /= '/')+ -- then do+ -- dirPath <- mkdtemp $ (toS $ imageCachePath cfg)+ -- return $ cfg { imageCachePath = toS dirPath }+ -- else return cfg+ + return cfg where- defaultConfig = Config 50 "~/.cache/greenclip.history" "~/.cache/greenclip.staticHistory" "/tmp/greenclip/" False [] True True- expandHomeDir str = (toS . fromMaybe (toS str) . listToMaybe <$> wordexp (toS str)) `catchAll` (\_ -> return $ toS str)+ defaultConfig = do + homeDir <- toS . fromMaybe mempty . listToMaybe <$> wordexp "~/"+ return $ Config 50 0 (homeDir <> ".cache/greenclip.history") "/tmp/greenclip" False [] True True + ["Greenclip has been updated to v4.1, update your new config file at ~/.config/greenclip.toml"] parseArgs :: [Text] -> Command parseArgs ("daemon":_) = DAEMON parseArgs ["clear"] = CLEAR+parseArgs ["prune", p] = PRUNE $ T.unpack p parseArgs ["print"] = PRINT parseArgs ["print", sel] = COPY sel parseArgs _ = HELP@@ -269,13 +316,15 @@ DAEMON -> runReaderT runDaemon cfg PRINT -> runReaderT printHistoryForRofi cfg CLEAR -> runReaderT (storeHistory mempty) cfg+ PRUNE p -> runReaderT (pruneHistory p) cfg -- Should rename COPY into ADVERTISE but as greenclip is already used I don't want to break configs -- of other people COPY sel -> runReaderT (advertiseSelection sel) cfg- HELP -> putText $ "greenclip v3.3 -- Recyle your clipboard selections\n\n" <>+ HELP -> putText $ "greenclip v4.2 -- 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" <>+ "prune FILE: Remove selections in list of md5 hashes in FILE\n" <> "clear: Clear history\n" <> "help: Display this message\n"