github.com/jmigpin/editor@v1.6.0/driver/xdriver/copypaste/paste.go (about)

     1  package copypaste
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"math"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/BurntSushi/xgb"
    11  	"github.com/BurntSushi/xgb/xproto"
    12  	"github.com/jmigpin/editor/driver/xdriver/xutil"
    13  	"github.com/jmigpin/editor/util/syncutil"
    14  	"github.com/jmigpin/editor/util/uiutil/event"
    15  )
    16  
    17  type Paste struct {
    18  	conn *xgb.Conn
    19  	win  xproto.Window
    20  	sw   *syncutil.WaitForSet // selectionnotify
    21  	pw   *syncutil.WaitForSet // propertynotify
    22  }
    23  
    24  func NewPaste(conn *xgb.Conn, win xproto.Window) (*Paste, error) {
    25  	if err := xutil.LoadAtoms(conn, &PasteAtoms, false); err != nil {
    26  		return nil, err
    27  	}
    28  	p := &Paste{
    29  		conn: conn,
    30  		win:  win,
    31  	}
    32  	p.sw = syncutil.NewWaitForSet()
    33  	p.pw = syncutil.NewWaitForSet()
    34  	return p, nil
    35  }
    36  
    37  //----------
    38  
    39  func (p *Paste) Get(index event.ClipboardIndex) (string, error) {
    40  	switch index {
    41  	case event.CIPrimary:
    42  		return p.request(PasteAtoms.Primary)
    43  	case event.CIClipboard:
    44  		return p.request(PasteAtoms.Clipboard)
    45  	default:
    46  		return "", fmt.Errorf("unhandled index")
    47  	}
    48  }
    49  
    50  //----------
    51  
    52  func (p *Paste) request(selection xproto.Atom) (string, error) {
    53  	// TODO: handle timestamps to force only one paste at a time?
    54  
    55  	p.sw.Start(1500 * time.Millisecond)
    56  	p.requestData(selection)
    57  	v, err := p.sw.WaitForSet()
    58  	if err != nil {
    59  		return "", err
    60  	}
    61  	ev := v.(*xproto.SelectionNotifyEvent)
    62  
    63  	//log.Printf("%#v", ev)
    64  
    65  	return p.extractData(ev)
    66  }
    67  
    68  func (p *Paste) requestData(selection xproto.Atom) {
    69  	_ = xproto.ConvertSelection(
    70  		p.conn,
    71  		p.win,
    72  		selection,
    73  		PasteAtoms.Utf8String, // target/type
    74  		PasteAtoms.XSelData,   // property
    75  		xproto.TimeCurrentTime)
    76  }
    77  
    78  //----------
    79  
    80  func (p *Paste) OnSelectionNotify(ev *xproto.SelectionNotifyEvent) {
    81  	// not a a paste event
    82  	switch ev.Property {
    83  	case xproto.AtomNone, PasteAtoms.XSelData:
    84  	default:
    85  		return
    86  	}
    87  
    88  	err := p.sw.Set(ev)
    89  	if err != nil {
    90  		log.Print(fmt.Errorf("onselectionnotify: %w", err))
    91  	}
    92  }
    93  
    94  //----------
    95  
    96  func (p *Paste) OnPropertyNotify(ev *xproto.PropertyNotifyEvent) {
    97  	// not a a paste event
    98  	switch ev.Atom {
    99  	case PasteAtoms.XSelData: // property used on requestData()
   100  	default:
   101  		return
   102  	}
   103  
   104  	//log.Printf("%#v", ev)
   105  
   106  	err := p.pw.Set(ev)
   107  	if err != nil {
   108  		//log.Print(errors.Wrap(err, "onpropertynotify"))
   109  	}
   110  }
   111  
   112  //----------
   113  
   114  func (p *Paste) extractData(ev *xproto.SelectionNotifyEvent) (string, error) {
   115  	switch ev.Property {
   116  	case xproto.AtomNone:
   117  		// nothing to paste (no owner exists)
   118  		return "", nil
   119  	case PasteAtoms.XSelData:
   120  		if ev.Target != PasteAtoms.Utf8String {
   121  			s, _ := xutil.GetAtomName(p.conn, ev.Target)
   122  			return "", fmt.Errorf("paste: unexpected type: %v %v", ev.Target, s)
   123  		}
   124  		return p.extractData3(ev)
   125  	default:
   126  		return "", fmt.Errorf("unhandled property: %v", ev.Property)
   127  	}
   128  }
   129  
   130  func (p *Paste) extractData3(ev *xproto.SelectionNotifyEvent) (string, error) {
   131  	w := []string{}
   132  	incrMode := false
   133  	for {
   134  		cookie := xproto.GetProperty(
   135  			p.conn,
   136  			true, // delete
   137  			ev.Requestor,
   138  			ev.Property,    // property that contains the data
   139  			ev.Target,      // type
   140  			0,              // long offset
   141  			math.MaxUint32) // long length
   142  		reply, err := cookie.Reply()
   143  		if err != nil {
   144  			return "", err
   145  		}
   146  
   147  		if reply.Type == PasteAtoms.Utf8String {
   148  			str := string(reply.Value)
   149  			w = append(w, str)
   150  
   151  			if incrMode {
   152  				if reply.ValueLen == 0 {
   153  					xproto.DeleteProperty(p.conn, ev.Requestor, ev.Property)
   154  					break
   155  				}
   156  			} else {
   157  				break
   158  			}
   159  		}
   160  
   161  		// incr mode
   162  		// https://tronche.com/gui/x/icccm/sec-2.html#s-2.7.2
   163  		if reply.Type == PasteAtoms.Incr {
   164  			incrMode = true
   165  			xproto.DeleteProperty(p.conn, ev.Requestor, ev.Property)
   166  			continue
   167  		}
   168  		if incrMode {
   169  			err := p.waitForPropertyNewValue(ev)
   170  			if err != nil {
   171  				return "", err
   172  			}
   173  			continue
   174  		}
   175  	}
   176  
   177  	return strings.Join(w, ""), nil
   178  }
   179  
   180  func (p *Paste) waitForPropertyNewValue(ev *xproto.SelectionNotifyEvent) error {
   181  	for {
   182  		p.pw.Start(1500 * time.Millisecond)
   183  		v, err := p.pw.WaitForSet()
   184  		if err != nil {
   185  			return err
   186  		}
   187  		pev := v.(*xproto.PropertyNotifyEvent)
   188  		if pev.Atom == ev.Property && pev.State == xproto.PropertyNewValue {
   189  			return nil
   190  		}
   191  	}
   192  }
   193  
   194  //----------
   195  
   196  var PasteAtoms struct {
   197  	Primary   xproto.Atom `loadAtoms:"PRIMARY"`
   198  	Clipboard xproto.Atom `loadAtoms:"CLIPBOARD"`
   199  	XSelData  xproto.Atom `loadAtoms:"XSEL_DATA"`
   200  	Incr      xproto.Atom `loadAtoms:"INCR"`
   201  
   202  	Utf8String xproto.Atom `loadAtoms:"UTF8_STRING"`
   203  }