packages feed

passman-cli-0.2.0.0: src/Passman/CLI/Clipboard/X11.hs

-- Copyright (C) 2017  Matthew Harm Bekkema
--
-- This file is part of passman-cli.
--
-- passman-cli is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- passman-cli is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <https://www.gnu.org/licenses/>.

{-# LANGUAGE RecordWildCards #-}

module Passman.CLI.Clipboard.X11
    ( withInitialSetup
    , setClipboardString
    , clearClipboard
    ) where

import           Control.Monad (when)
import           Data.Function (fix)
import           Data.Maybe    (isNothing)

import           Data.Text         (Text)
import qualified Data.Text.Foreign as T

import           Control.Exception  (bracket)
import           Control.Concurrent (threadDelay)
import           System.Timeout     (timeout)
import           Foreign.Ptr        (castPtr)

import Graphics.X11.Xlib        (Display, XEventPtr, Window, Time, Atom,
                                 destroyWindow, createSimpleWindow, sendEvent,
                                 pending, nextEvent, allocaXEvent, openDisplay,
                                 defaultRootWindow, closeDisplay, internAtom,
                                 getAtomName, selectionNotify)
import Graphics.X11.Xlib.Extras (Event (SelectionClear, SelectionRequest,
                                        ev_requestor, ev_selection, ev_target,
                                        ev_property, ev_time),
                                 xSetSelectionOwner, xChangeProperty,
                                 setSelectionNotify, setEventType,
                                 propModeReplace, none, getEvent, currentTime)


setClipboardString :: (Display, Window, Atom) -> Text -> IO ()
setClipboardString (display, window, clipboard) str = do
    xSetSelectionOwner display clipboard window currentTime
    advertiseSelection display str True

clearClipboard :: (Display, Window, Atom) -> IO ()
clearClipboard (display, window, clipboard) = do
    xSetSelectionOwner display clipboard window currentTime
    _ <- timeout 1000000 $
        advertiseSelection display mempty False
    return ()

interruptableNextEvent :: Display -> XEventPtr -> IO ()
interruptableNextEvent display evPtr = fix $ \r -> do
    p <- pending display
    if p > 0
        then nextEvent display evPtr
        else threadDelay 1000 >> r

advertiseSelection :: Display -> Text -> Bool -> IO ()
advertiseSelection display str loop = allocaXEvent go
  where
    go evPtr = do
        interruptableNextEvent 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
                when (res == none || loop) $ go evPtr

            SelectionClear {..} -> return ()

            _ -> go evPtr

handleOutput :: Display -> Window -> Atom -> Maybe String -> Text -> 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
        _ <- T.withCStringLen str $ \(str', len) ->
            xChangeProperty display req prop target 8 propModeReplace
                            (castPtr 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

withInitialSetup :: ((Display, Window, Atom) -> IO a) -> IO a
withInitialSetup f = bracket (openDisplay "") closeDisplay $ \display ->
    bracket (createSimpleWindow display (defaultRootWindow display)
                                0 0 1 1 0 0 0)
            (destroyWindow display) $ \window -> do
        clipboard <- internAtom display "CLIPBOARD" False
        f (display, window, clipboard)