{-# LANGUAGE GADTs #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} -------------------------------------------------------------------- -- | -- Module : System.Hclip -- Copyright : (c) Jens Thomas -- License : BSD3 -- -- Maintainer: Jens Thomas -- Stability : experimental -- Portability: non-portable (GADTs, CPP, DeriveDataTypeable) -- -- A small cross-platform library for reading and modifying the system clipboard. -- -------------------------------------------------------------------- module System.Hclip ( getClipboard, setClipboard, modifyClipboard, modifyClipboard_, clearClipboard, ClipboardException(..) ) where import System.Info (os) import System.Process (runInteractiveCommand, readProcessWithExitCode, waitForProcess) import System.IO (Handle, hPutStr, hClose) import Data.Monoid import System.IO.Strict (hGetContents) -- see http://hackage.haskell.org/package/strict import System.Exit (ExitCode(..)) import Data.List (intercalate, genericLength) import Control.Exception (Exception, throw, throwIO, bracket, bracket_) import Data.Typeable (Typeable) import Control.Applicative ((<$>)) import Control.Monad ((>=>), liftM) -- | for Windows support #if defined(mingw32_HOST_OS) || defined(__MINGW32__) import System.Win32.Mem (globalAlloc, globalLock, globalUnlock, copyMemory, gHND) import Graphics.Win32.GDI.Clip (openClipboard, closeClipboard, emptyClipboard, getClipboardData, setClipboardData, ClipboardFormat, isClipboardFormatAvailable, cF_TEXT) import Foreign.C (withCAString, peekCAString) import Foreign.Ptr (castPtr, nullPtr) #endif type StdIn = Handle type StdOut = Handle type IOAction a = (StdIn, StdOut) -> IO a -- | Clipboard Commands data Command a where GetClipboard :: Command (IO String) SetClipboard :: String -> Command (IO ()) -- | Supported Platforms data Platform = Linux | Darwin | Windows -- | Exceptions data ClipboardException = UnsupportedOS String | NoTextualData | MissingCommands [String] deriving (Typeable) instance Exception ClipboardException instance Show ClipboardException where show (UnsupportedOS s) = "Unsupported Operating System: " ++ s show NoTextualData = "Clipboard doesn't contain textual data." show (MissingCommands cmds) = "Hclip requires " ++ apps ++ " installed." where apps = intercalate " or " cmds -- | Read clipboard contents. getClipboard :: IO String getClipboard = dispatch GetClipboard -- | Set clipboard contents. setClipboard :: String -> IO () setClipboard = dispatch . SetClipboard -- | Apply function to clipboard and return its new contents. modifyClipboard :: (String -> String) -> IO String modifyClipboard f = do modified <- f <$> getClipboard setClipboard modified return modified -- | Apply function to clipboard. modifyClipboard_ :: (String -> String) -> IO () modifyClipboard_ = flip liftM getClipboard >=> setClipboard -- | Delete Clipboard contents. clearClipboard :: IO () clearClipboard = setClipboard "" -- | Dispatch on the type of the Operating System. dispatch cmd = execute (resolveOS os) cmd where resolveOS "linux" = Linux resolveOS "darwin" = Darwin #if defined(mingw32_HOST_OS) || defined(__MINGW32__) resolveOS "mingw32" = Windows #endif resolveOS unknownOS = throw . UnsupportedOS $ unknownOS -- | Platform-specific execution. execute :: Platform -> Command a -> a execute Linux cmd@GetClipboard = resolveLinuxApp cmd >>= flip withExternalApp readOutHandle execute Linux cmd@(SetClipboard s) = resolveLinuxApp cmd >>= flip withExternalApp (writeInHandle s) execute Darwin GetClipboard = withExternalApp "pbpaste" readOutHandle execute Darwin (SetClipboard s) = withExternalApp "pbcopy" $ writeInHandle s -- | Windows: use WinAPI #if defined(mingw32_HOST_OS) || defined(__MINGW32__) execute Windows GetClipboard = bracket_ (openClipboard nullPtr) closeClipboard $ do isText <- isClipboardFormatAvailable cF_TEXT if isText then do h <- getClipboardData cF_TEXT bracket (globalLock h) globalUnlock $ peekCAString . castPtr else throwIO NoTextualData execute Windows (SetClipboard s) = withCAString s $ \cstr -> do mem <- globalAlloc gHND memSize bracket (globalLock mem) globalUnlock $ \space -> do copyMemory space (castPtr cstr) memSize bracket_ (openClipboard nullPtr) closeClipboard $ do emptyClipboard setClipboardData cF_TEXT space return () where memSize = genericLength s + 1 #endif -- | Determine the correct Linux command. resolveLinuxApp :: Command a -> IO String resolveLinuxApp cmd = decode cmd <$> chooseFirstApp ["xsel", "xclip"] where decode :: Command a -> String -> String decode GetClipboard "xsel" = "xsel -b -o" decode (SetClipboard _) "xsel" = "xsel -b -i" decode GetClipboard "xclip" = "xclip -selection c -o" decode (SetClipboard _) "xclip" = "xclip -selection c" -- | Run external app and apply action to the file handles. withExternalApp :: String -> IOAction a -> IO a withExternalApp app action = bracket (runInteractiveCommand app) (\(inp, outp, stderr, pid) -> mapM_ hClose [inp, outp, stderr] >> waitForProcess pid) (\(inp, outp, _, _) -> action (inp, outp)) -- | Search for installed programs and return the first match. chooseFirstApp :: [String] -> IO String chooseFirstApp apps = do results <- mapM whichCommand apps maybe (throwIO $ MissingCommands apps) return (getFirst . mconcat $ map First results) -- | Check if cmd is installed by using the which command. whichCommand :: String -> IO (Maybe String) whichCommand cmd = do (exitCode,_,_) <- readProcessWithExitCode "which" [cmd] "" case exitCode of ExitSuccess -> return $ Just cmd ExitFailure _ -> return Nothing readOutHandle :: IOAction String readOutHandle = hGetContents . stdout writeInHandle :: String -> IOAction () writeInHandle s = flip hPutStr s . stdin stdin, stdout :: (StdIn, StdOut) -> Handle stdin = fst stdout = snd