packages feed

greenclip-2.0.1: src/Clipboard.hs

{-# LANGUAGE RecordWildCards #-}

module Clipboard
  ( getClipboardString
  , setClipboardString
  , getPrimaryClipboard
  ) where

import Graphics.X11.Xlib
import Graphics.X11.Xlib.Extras
import System.Posix.Process     (forkProcess)

import Codec.Binary.UTF8.String (decode, encode)
import Control.Monad
import Data.Maybe
import Foreign                  (peekByteOff)
import Foreign.C.Types          (CChar, CUChar)
import Foreign.Marshal.Array    (withArrayLen)
import System.Directory         (setCurrentDirectory)
import System.IO                (hClose, stderr, stdin, stdout)
import System.IO.Unsafe (unsafePerformIO)
import Data.IORef
import Control.Concurrent (threadDelay)

getClipboardString :: IO (Maybe String)
getClipboardString = do
    (display, window, clipboards) <- readIORef initialSetup
    inp <- internAtom display "clipboard_get" False
    target <- internAtom display "UTF8_STRING" True
    xConvertSelection display (head clipboards) target inp window currentTime
    Just <$> clipboardInputWait display window inp

getPrimaryClipboard :: IO (Maybe String)
getPrimaryClipboard = do
    (display, window, clipboards) <- readIORef initialSetup
    inp <- internAtom display "clipboard_get" False
    target <- internAtom display "UTF8_STRING" True
    xConvertSelection display (clipboards !! 1) target inp window currentTime
    Just <$> clipboardInputWait display window inp

clipboardInputWait :: Display -> Window -> Atom -> IO String
clipboardInputWait display' window' inp' = allocaXEvent (go display' window' inp')
  where
  go display window inp evPtr = do
    waitForEvent display
    nextEvent display evPtr
    ev <- getEvent evPtr
    if ev_event_type ev == selectionNotify
       then charsToString . fromMaybe mempty <$> getWindowProperty8 display inp window
       else go display window inp evPtr
  waitForEvent display = do
    nbEvs <- pending display
    when (nbEvs == 0) $ threadDelay 100000 >> waitForEvent display

charsToString :: [CChar] -> String
charsToString = decode . map fromIntegral

setClipboardString :: String -> IO ()
setClipboardString str = void $ forkProcess $ do
        mapM_ hClose [stdin, stdout, stderr]
        setCurrentDirectory "/"
        (display, window, clipboards) <- readIORef initialSetup
        mapM_ (\atom -> xSetSelectionOwner display atom window currentTime) clipboards
        advertiseSelection display clipboards (stringToChars str)
        cleanup display window

advertiseSelection :: Display -> [Atom] -> [CUChar] -> IO ()
advertiseSelection display clipboards' str = allocaXEvent (go clipboards')
  where
    go [] _ = return ()
    go clipboards evPtr = do
      nextEvent display evPtr
      ev <- getEvent evPtr
      case ev of
          SelectionRequest {..} -> do
              target' <- getAtomName display ev_target
              res <- handleOutput display ev_requestor ev_property target' str
              sendSelectionNotify display ev_requestor ev_selection ev_target res ev_time
              go clipboards evPtr

          _ | ev_event_type ev == selectionClear -> do
              target <- peekByteOff evPtr 40 :: IO Atom
              go (filter (/= target) clipboards) evPtr

          _ -> go clipboards evPtr

handleOutput :: Display -> Window -> Atom -> Maybe String -> [CUChar] -> IO Atom
handleOutput display req prop (Just "UTF8_STRING") str = do
    prop' <- getAtomName display prop
    if isNothing prop' then handleOutput display req prop Nothing str else do
        target <- internAtom display "UTF8_STRING" True
        void $ withArrayLen str $ \len str' ->
            xChangeProperty display req prop target 8 propModeReplace str'
                            (fromIntegral len)
        return prop
handleOutput _ _ _ _ _ = return none

sendSelectionNotify :: Display -> Window -> Atom -> Atom -> Atom -> Time -> IO ()
sendSelectionNotify display req sel target prop time = allocaXEvent $ \ev -> do
    setEventType ev selectionNotify
    setSelectionNotify ev req sel target prop time
    sendEvent display req False 0 ev

stringToChars :: String -> [CUChar]
stringToChars = map fromIntegral . encode

initialSetup :: IORef (Display, Window, [Atom])
initialSetup = unsafePerformIO $ do
    display <- openDisplay ""
    window <- createSimpleWindow display (defaultRootWindow display)
                                 0 0 1 1 0 0 0
    clipboards <- internAtom display "CLIPBOARD" False
    newIORef (display, window, [clipboards, pRIMARY])
{-# NOINLINE initialSetup #-}

cleanup :: Display -> Window -> IO ()
cleanup display window = do
    destroyWindow display window
    closeDisplay display