clipboard_x11.go raw

   1  // +build linux freebsd openbsd
   2  
   3  package clipboard
   4  
   5  import (
   6  	"fmt"
   7  	"os"
   8  	"time"
   9  	
  10  	"github.com/BurntSushi/xgb"
  11  	"github.com/BurntSushi/xgb/xproto"
  12  )
  13  
  14  // todo: only X is required from this package, the rest runs off the built-in Gio clipboard
  15  
  16  const debugClipboardRequests = false
  17  
  18  var X *xgb.Conn
  19  var win xproto.Window
  20  var clipboardText string
  21  var selnotify chan bool
  22  
  23  var clipboardAtom, primaryAtom, textAtom, targetsAtom, atomAtom xproto.Atom
  24  var targetAtoms []xproto.Atom
  25  var clipboardAtomCache = map[xproto.Atom]string{}
  26  
  27  var RunningX bool
  28  
  29  // Start up the clipboard watcher
  30  func Start() {
  31  	// first, check if X is running as only X has the Primary buffer
  32  	env := os.Environ()
  33  	for i := range env {
  34  		if env[i]=="XDG_SESSION_TYPE=x11" {
  35  			I.Ln("running X11, primary selection buffer enabled")
  36  			RunningX=true
  37  			break
  38  		}
  39  	}
  40  	if !RunningX {
  41  		return
  42  	}
  43  	var e error
  44  	X, e = xgb.NewConnDisplay("")
  45  	if e != nil {
  46  		panic(e)
  47  	}
  48  
  49  	selnotify = make(chan bool, 1)
  50  
  51  	win, e = xproto.NewWindowId(X)
  52  	if e != nil {
  53  		panic(e)
  54  	}
  55  
  56  	setup := xproto.Setup(X)
  57  	s := setup.DefaultScreen(X)
  58  	e = xproto.CreateWindowChecked(
  59  		X,
  60  		s.RootDepth,
  61  		win,
  62  		s.Root,
  63  		100,
  64  		100,
  65  		1,
  66  		1,
  67  		0,
  68  		xproto.WindowClassInputOutput,
  69  		s.RootVisual,
  70  		0,
  71  		[]uint32{},
  72  	).Check()
  73  	if e != nil {
  74  		panic(e)
  75  	}
  76  
  77  	clipboardAtom = internAtom(X, "CLIPBOARD")
  78  	primaryAtom = internAtom(X, "PRIMARY")
  79  	textAtom = internAtom(X, "UTF8_STRING")
  80  	targetsAtom = internAtom(X, "TARGETS")
  81  	atomAtom = internAtom(X, "ATOM")
  82  
  83  	targetAtoms = []xproto.Atom{targetsAtom, textAtom}
  84  
  85  	go eventLoop()
  86  }
  87  
  88  func Set(text string) (e error){
  89  	if !RunningX {
  90  		return
  91  	}
  92  	clipboardText = text
  93  	ssoc := xproto.SetSelectionOwnerChecked(X, win, clipboardAtom, xproto.TimeCurrentTime)
  94  	if e = ssoc.Check(); E.Chk(e) {
  95  		fmt.Fprintf(os.Stderr, "Error setting clipboard: %v", e)
  96  	}
  97  	ssoc = xproto.SetSelectionOwnerChecked(X, win, primaryAtom, xproto.TimeCurrentTime)
  98  	if e = ssoc.Check(); E.Chk(e) {
  99  		fmt.Fprintf(os.Stderr, "Error setting primary selection: %v", e)
 100  	}
 101  	return
 102  }
 103  
 104  func SetPrimary(text string) (e error){
 105  	if !RunningX {
 106  		return
 107  	}
 108  	clipboardText = text
 109  	ssoc := xproto.SetSelectionOwnerChecked(X, win, primaryAtom, xproto.TimeCurrentTime)
 110  	if e = ssoc.Check(); E.Chk(e) {
 111  		fmt.Fprintf(os.Stderr, "Error setting primary selection: %v", e)
 112  	}
 113  	return
 114  }
 115  
 116  func Get() string {
 117  	if !RunningX {
 118  		return ""
 119  	}
 120  	return getSelection(clipboardAtom)
 121  }
 122  
 123  func GetPrimary() string {
 124  	if !RunningX {
 125  		return ""
 126  	}
 127  	return getSelection(primaryAtom)
 128  }
 129  
 130  func getSelection(selAtom xproto.Atom) string {
 131  	csc := xproto.ConvertSelectionChecked(X, win, selAtom, textAtom, selAtom, xproto.TimeCurrentTime)
 132  	e := csc.Check()
 133  	if e != nil {
 134  		fmt.Fprintln(os.Stderr, e)
 135  		return ""
 136  	}
 137  	
 138  	select {
 139  	case r := <-selnotify:
 140  		if !r {
 141  			return ""
 142  		}
 143  		gpc := xproto.GetProperty(X, true, win, selAtom, textAtom, 0, 5*1024*1024)
 144  		gpr, e := gpc.Reply()
 145  		if e != nil {
 146  			fmt.Fprintln(os.Stderr, e)
 147  			return ""
 148  		}
 149  		if gpr.BytesAfter != 0 {
 150  			fmt.Fprintln(os.Stderr, "Clipboard too large")
 151  			return ""
 152  		}
 153  		return string(gpr.Value[:gpr.ValueLen])
 154  	case <-time.After(1 * time.Second):
 155  		fmt.Fprintln(os.Stderr, "Clipboard retrieval failed, timeout")
 156  		return ""
 157  	}
 158  }
 159  
 160  func eventLoop() {
 161  	for {
 162  		ev, e := X.WaitForEvent()
 163  		if e != nil {
 164  			continue
 165  		}
 166  		switch evt := ev.(type) {
 167  		case xproto.SelectionRequestEvent:
 168  			if debugClipboardRequests {
 169  				tgtname := lookupAtom(evt.Target)
 170  				fmt.Fprintln(
 171  					os.Stderr,
 172  					"SelectionRequest",
 173  					ev,
 174  					textAtom,
 175  					tgtname,
 176  					"isPrimary:",
 177  					evt.Selection == primaryAtom,
 178  					"isClipboard:",
 179  					evt.Selection == clipboardAtom,
 180  				)
 181  			}
 182  			t := clipboardText
 183  			
 184  			switch evt.Target {
 185  			case textAtom:
 186  				if debugClipboardRequests {
 187  					fmt.Fprintln(os.Stderr, "Sending as text")
 188  				}
 189  				cpc := xproto.ChangePropertyChecked(
 190  					X,
 191  					xproto.PropModeReplace,
 192  					evt.Requestor,
 193  					evt.Property,
 194  					textAtom,
 195  					8,
 196  					uint32(len(t)),
 197  					[]byte(t),
 198  				)
 199  				e := cpc.Check()
 200  				if e == nil {
 201  					sendSelectionNotify(evt)
 202  				} else {
 203  					fmt.Fprintln(os.Stderr, e)
 204  				}
 205  			
 206  			case targetsAtom:
 207  				if debugClipboardRequests {
 208  					fmt.Fprintln(os.Stderr, "Sending targets")
 209  				}
 210  				buf := make([]byte, len(targetAtoms)*4)
 211  				for i, atom := range targetAtoms {
 212  					xgb.Put32(buf[i*4:], uint32(atom))
 213  				}
 214  				
 215  				_ = xproto.ChangePropertyChecked(
 216  					X,
 217  					xproto.PropModeReplace,
 218  					evt.Requestor,
 219  					evt.Property,
 220  					atomAtom,
 221  					32,
 222  					uint32(len(targetAtoms)),
 223  					buf,
 224  				).Check()
 225  				if e == nil {
 226  					sendSelectionNotify(evt)
 227  				} else {
 228  					fmt.Fprintln(os.Stderr, e)
 229  				}
 230  			
 231  			default:
 232  				if debugClipboardRequests {
 233  					fmt.Fprintln(os.Stderr, "Skipping")
 234  				}
 235  				evt.Property = 0
 236  				sendSelectionNotify(evt)
 237  			}
 238  		
 239  		case xproto.SelectionNotifyEvent:
 240  			selnotify <- (evt.Property == clipboardAtom) || (evt.Property == primaryAtom)
 241  		}
 242  	}
 243  }
 244  
 245  func lookupAtom(at xproto.Atom) string {
 246  	if s, ok := clipboardAtomCache[at]; ok {
 247  		return s
 248  	}
 249  	
 250  	reply, e := xproto.GetAtomName(X, at).Reply()
 251  	if e != nil {
 252  		panic(e)
 253  	}
 254  	
 255  	// If we're here, it means we didn't have the ATOM id cached. So cache it.
 256  	atomName := string(reply.Name)
 257  	clipboardAtomCache[at] = atomName
 258  	return atomName
 259  }
 260  
 261  func sendSelectionNotify(ev xproto.SelectionRequestEvent) {
 262  	sn := xproto.SelectionNotifyEvent{
 263  		Time:      xproto.TimeCurrentTime,
 264  		Requestor: ev.Requestor,
 265  		Selection: ev.Selection,
 266  		Target:    ev.Target,
 267  		Property:  ev.Property,
 268  	}
 269  	var e error
 270  	sec := xproto.SendEventChecked(X, false, ev.Requestor, 0, string(sn.Bytes()))
 271  	if e = sec.Check(); E.Chk(e) {
 272  		fmt.Fprintln(os.Stderr, e)
 273  	}
 274  }
 275  
 276  func internAtom(conn *xgb.Conn, n string) xproto.Atom {
 277  	iac := xproto.InternAtom(conn, true, uint16(len(n)), n)
 278  	iar, e := iac.Reply()
 279  	if e != nil {
 280  		panic(e)
 281  	}
 282  	return iar.Atom
 283  }
 284