v0.4.0 b6be3a8

clipboard.go RawHistoryFolder

1package wayland
2
3import (
4	"github.com/bnema/go-wayland-bindings/server/extdatacontrol"
5	"github.com/bnema/go-wayland-bindings/server/primaryselection"
6	"github.com/bnema/go-wayland-bindings/server/wayland"
7	"github.com/bnema/purego-libwayland/server"
8	"golang.org/x/sys/unix"
9)
10
11// The clipboard (wl_data_device) and the primary selection
12// (zwp_primary_selection_v1) each hold one source. Only the client with
13// keyboard focus may set them and sees them; clipboard managers such as
14// cliphist and wl-clipboard use ext_data_control_v1, which sees and sets
15// both without focus. Like wlroots, any client may bind it. Pasting hands the reader's pipe to the source client:
16// the data never goes through neferwl. Drag and drop is not implemented.
17
18// protected is a defensive DISPLAY admission check, not lock ownership.
19func (s *Server) protected() bool {
20	return s.security != nil && s.security.Snapshot().Protected
21}
22
23const (
24	selClipboard = iota
25	selPrimary
26)
27
28// clipSource is a client's offer: its MIME types and how to ask for data.
29type clipSource struct {
30	client server.Client
31	mimes  []string
32	send   func(mime string, fd int)
33	cancel func()
34	alive  func() bool
35	// used: the source was set, rejected or dragged; it cannot be set
36	// again and takes no more MIME types.
37	used bool
38}
39
40// addMime records a MIME type offered before the source is used.
41func (src *clipSource) addMime(mime string) bool {
42	if src.used {
43		return false
44	}
45	src.mimes = append(src.mimes, mime)
46	return true
47}
48
49// clipDevice receives selections for one client.
50type clipDevice struct {
51	client server.Client
52	// control devices (ext_data_control_v1) see every selection.
53	control bool
54	kinds   [2]bool
55	alive   func() bool
56	// offer announces src (nil clears) as the selection of kind.
57	offer func(kind int, src *clipSource)
58}
59
60func registerClipboard(d *server.Display, s *Server) error {
61	for _, register := range []func() error{
62		func() error {
63			return wayland.NewDataDeviceManagerGlobal(d, 3, func(c server.Client, v, id uint32) {
64				_, _ = wayland.NewDataDeviceManager(c, int32(v), id, dataManager{s})
65			})
66		},
67		func() error {
68			return primaryselection.NewZwpPrimarySelectionDeviceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
69				_, _ = primaryselection.NewZwpPrimarySelectionDeviceManagerV1(c, int32(v), id, primaryManager{s})
70			})
71		},
72		func() error {
73			return extdatacontrol.NewExtDataControlManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
74				_, _ = extdatacontrol.NewExtDataControlManagerV1(c, int32(v), id, controlManager{s})
75			})
76		},
77	} {
78		if err := register(); err != nil {
79			return err
80		}
81	}
82	return nil
83}
84
85// focusedClient is the client with keyboard focus.
86func (s *Server) focusedClient() (server.Client, bool) {
87	surf, _ := s.focusTarget(s.seat.focused)
88	if surf == nil {
89		return server.Client{}, false
90	}
91	return surf.Client(), true
92}
93
94// setSelection makes src the selection of kind and announces it. A
95// replaced source is cancelled.
96func (s *Server) setSelection(kind int, src *clipSource) {
97	if s.protected() {
98		if src != nil && src != s.selections[kind] && src.alive() {
99			src.cancel()
100		}
101		return
102	}
103	old := s.selections[kind]
104	if old == src {
105		return
106	}
107	s.selections[kind] = src
108	if old != nil && old.alive() {
109		old.cancel()
110	}
111	s.announceAll(kind)
112}
113
114// announceAll sends the selection of kind to the focused client and to
115// clipboard managers.
116func (s *Server) announceAll(kind int) {
117	c, ok := s.focusedClient()
118	s.announce(kind, func(d *clipDevice) bool { return d.control || ok && d.client == c })
119}
120
121// announce sends the selection of kind to the devices matching want.
122func (s *Server) announce(kind int, want func(*clipDevice) bool) {
123	if s.protected() {
124		return
125	}
126	src := s.selections[kind]
127	if src != nil && !src.alive() {
128		src, s.selections[kind] = nil, nil
129	}
130	alive := s.clipDevices[:0]
131	for _, d := range s.clipDevices {
132		if !d.alive() {
133			continue
134		}
135		alive = append(alive, d)
136		if d.kinds[kind] && want(d) {
137			d.offer(kind, src)
138		}
139	}
140	clear(s.clipDevices[len(alive):])
141	s.clipDevices = alive
142}
143
144// focusSelections gives a client that just got keyboard focus the current
145// selections, as the protocol asks before its keyboard enter.
146func (s *Server) focusSelections() {
147	c, ok := s.focusedClient()
148	if !ok {
149		return
150	}
151	for kind := range s.selections {
152		s.announce(kind, func(d *clipDevice) bool { return !d.control && d.client == c })
153	}
154}
155
156// addClipDevice tracks a device and sends it the selections it may see.
157func (s *Server) addClipDevice(d *clipDevice) {
158	s.clipDevices = append(s.clipDevices, d)
159	if s.protected() {
160		return
161	}
162	c, ok := s.focusedClient()
163	if !d.control && (!ok || d.client != c) {
164		return
165	}
166	for kind, src := range s.selections {
167		if d.kinds[kind] {
168			if src != nil && !src.alive() {
169				src = nil
170			}
171			d.offer(kind, src)
172		}
173	}
174}
175
176// sourceGone clears the selections held by a destroyed source.
177func (s *Server) sourceGone(src *clipSource) {
178	for kind := range s.selections {
179		if s.selections[kind] == src {
180			s.selections[kind] = nil
181			s.announceAll(kind)
182		}
183	}
184}
185
186// maySet reports whether a client may set the selection: only the
187// focused one. Any client holding an offer may read it.
188func (s *Server) maySet(c server.Client) bool {
189	f, ok := s.focusedClient()
190	return !s.protected() && ok && f == c
191}
192
193// setFocused sets a selection from a focus-bound device. A used source is
194// ignored (it was already cancelled unless it is the current one); an
195// unfocused client's source is cancelled.
196func (s *Server) setFocused(c server.Client, kind int, src *clipSource) {
197	if src != nil && src.used {
198		return
199	}
200	if src != nil {
201		src.used = true
202	}
203	if !s.maySet(c) {
204		if src != nil {
205			src.cancel()
206		}
207		return
208	}
209	s.setSelection(kind, src)
210}
211
212// receive hands the reader's pipe to the source if it is still the
213// selection; the fd is always closed here (the event dups it). Protection
214// rejects even previously announced offers. Transfers accepted before protection
215// are client-to-client: their already handed-off pipes cannot be revoked here.
216func (s *Server) receive(kind int, src *clipSource, mime string, fd int) {
217	defer unix.Close(fd)
218	if s.protected() || s.selections[kind] != src || !src.alive() {
219		return
220	}
221	for _, m := range src.mimes {
222		if m == mime {
223			src.send(mime, fd)
224			return
225		}
226	}
227}
228
229// wl_data_device_manager
230
231type dataManager struct{ server *Server }
232
233func (m dataManager) CreateDataSource(r *wayland.DataDeviceManager, id uint32) {
234	src := &clipSource{client: r.Client()}
235	res, err := wayland.NewDataSource(r.Client(), r.Version(), id, dataSource{src})
236	if err != nil {
237		return
238	}
239	src.send = func(mime string, fd int) { res.SendSend(mime, fd) }
240	src.cancel = res.SendCancelled
241	src.alive = res.Resource.Alive
242	m.server.dataSources[res.Resource] = src
243	res.OnDestroy = func() {
244		delete(m.server.dataSources, res.Resource)
245		m.server.sourceGone(src)
246	}
247}
248func (m dataManager) GetDataDevice(r *wayland.DataDeviceManager, id uint32, _ *wayland.Seat) {
249	s := m.server
250	res, err := wayland.NewDataDevice(r.Client(), r.Version(), id, dataDevice{s})
251	if err != nil {
252		return
253	}
254	s.addClipDevice(&clipDevice{client: r.Client(), kinds: [2]bool{selClipboard: true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) {
255		if src == nil {
256			res.SendSelection(nil)
257			return
258		}
259		offer, err := wayland.NewDataOffer(res.Client(), res.Version(), 0, dataOffer{s, src})
260		if err != nil {
261			return
262		}
263		res.SendDataOffer(offer)
264		for _, mime := range src.mimes {
265			offer.SendOffer(mime)
266		}
267		res.SendSelection(offer)
268	}})
269}
270func (dataManager) Release(r *wayland.DataDeviceManager) { r.Destroy() }
271
272type dataSource struct{ src *clipSource }
273
274func (d dataSource) Offer(_ *wayland.DataSource, mime string) { d.src.addMime(mime) }
275func (dataSource) Destroy(*wayland.DataSource)                {}
276func (dataSource) SetActions(*wayland.DataSource, uint32)     {}
277
278type dataDevice struct{ server *Server }
279
280// StartDrag is not implemented: the drag is cancelled.
281func (d dataDevice) StartDrag(_ *wayland.DataDevice, src *wayland.DataSource, _, _ *wayland.Surface, _ uint32) {
282	if src == nil {
283		return
284	}
285	cs := d.server.dataSources[src.Resource]
286	if cs == nil || cs.used {
287		// A used source is the selection or already cancelled.
288		return
289	}
290	cs.used = true
291	if src.Resource.Alive() {
292		src.SendCancelled()
293	}
294}
295func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, _ uint32) {
296	s := d.server
297	var src *clipSource
298	if res != nil {
299		src = s.dataSources[res.Resource]
300		if src == nil {
301			return
302		}
303	}
304	s.setFocused(r.Client(), selClipboard, src)
305}
306func (dataDevice) Release(r *wayland.DataDevice) { r.Destroy() }
307
308type dataOffer struct {
309	server *Server
310	src    *clipSource
311}
312
313func (dataOffer) Accept(*wayland.DataOffer, uint32, string) {}
314func (o dataOffer) Receive(_ *wayland.DataOffer, mime string, fd int) {
315	o.server.receive(selClipboard, o.src, mime, fd)
316}
317func (dataOffer) Destroy(*wayland.DataOffer)                    {}
318func (dataOffer) Finish(*wayland.DataOffer)                     {}
319func (dataOffer) SetActions(*wayland.DataOffer, uint32, uint32) {}
320
321// zwp_primary_selection_device_manager_v1
322
323type primaryManager struct{ server *Server }
324
325func (m primaryManager) CreateSource(r *primaryselection.ZwpPrimarySelectionDeviceManagerV1, id uint32) {
326	src := &clipSource{client: r.Client()}
327	res, err := primaryselection.NewZwpPrimarySelectionSourceV1(r.Client(), r.Version(), id, primarySource{src})
328	if err != nil {
329		return
330	}
331	src.send = func(mime string, fd int) { res.SendSend(mime, fd) }
332	src.cancel = res.SendCancelled
333	src.alive = res.Resource.Alive
334	m.server.primarySources[res.Resource] = src
335	res.OnDestroy = func() {
336		delete(m.server.primarySources, res.Resource)
337		m.server.sourceGone(src)
338	}
339}
340func (m primaryManager) GetDevice(r *primaryselection.ZwpPrimarySelectionDeviceManagerV1, id uint32, _ *wayland.Seat) {
341	s := m.server
342	res, err := primaryselection.NewZwpPrimarySelectionDeviceV1(r.Client(), r.Version(), id, primaryDevice{s})
343	if err != nil {
344		return
345	}
346	s.addClipDevice(&clipDevice{client: r.Client(), kinds: [2]bool{selPrimary: true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) {
347		if src == nil {
348			res.SendSelection(nil)
349			return
350		}
351		offer, err := primaryselection.NewZwpPrimarySelectionOfferV1(res.Client(), res.Version(), 0, primaryOffer{s, src})
352		if err != nil {
353			return
354		}
355		res.SendDataOffer(offer)
356		for _, mime := range src.mimes {
357			offer.SendOffer(mime)
358		}
359		res.SendSelection(offer)
360	}})
361}
362func (primaryManager) Destroy(*primaryselection.ZwpPrimarySelectionDeviceManagerV1) {}
363
364type primarySource struct{ src *clipSource }
365
366func (p primarySource) Offer(_ *primaryselection.ZwpPrimarySelectionSourceV1, mime string) {
367	p.src.addMime(mime)
368}
369func (primarySource) Destroy(*primaryselection.ZwpPrimarySelectionSourceV1) {}
370
371type primaryDevice struct{ server *Server }
372
373func (d primaryDevice) SetSelection(r *primaryselection.ZwpPrimarySelectionDeviceV1, res *primaryselection.ZwpPrimarySelectionSourceV1, _ uint32) {
374	s := d.server
375	var src *clipSource
376	if res != nil {
377		src = s.primarySources[res.Resource]
378		if src == nil {
379			return
380		}
381	}
382	s.setFocused(r.Client(), selPrimary, src)
383}
384func (primaryDevice) Destroy(*primaryselection.ZwpPrimarySelectionDeviceV1) {}
385
386type primaryOffer struct {
387	server *Server
388	src    *clipSource
389}
390
391func (o primaryOffer) Receive(_ *primaryselection.ZwpPrimarySelectionOfferV1, mime string, fd int) {
392	o.server.receive(selPrimary, o.src, mime, fd)
393}
394func (primaryOffer) Destroy(*primaryselection.ZwpPrimarySelectionOfferV1) {}
395
396// ext_data_control_manager_v1
397
398type controlManager struct{ server *Server }
399
400func (m controlManager) CreateDataSource(r *extdatacontrol.ExtDataControlManagerV1, id uint32) {
401	src := &clipSource{client: r.Client()}
402	res, err := extdatacontrol.NewExtDataControlSourceV1(r.Client(), r.Version(), id, controlSource{src})
403	if err != nil {
404		return
405	}
406	src.send = func(mime string, fd int) { res.SendSend(mime, fd) }
407	src.cancel = res.SendCancelled
408	src.alive = res.Resource.Alive
409	m.server.controlSources[res.Resource] = src
410	res.OnDestroy = func() {
411		delete(m.server.controlSources, res.Resource)
412		m.server.sourceGone(src)
413	}
414}
415func (m controlManager) GetDataDevice(r *extdatacontrol.ExtDataControlManagerV1, id uint32, _ *wayland.Seat) {
416	s := m.server
417	res, err := extdatacontrol.NewExtDataControlDeviceV1(r.Client(), r.Version(), id, controlDevice{s})
418	if err != nil {
419		return
420	}
421	s.addClipDevice(&clipDevice{client: r.Client(), control: true, kinds: [2]bool{true, true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) {
422		send := res.SendSelection
423		if kind == selPrimary {
424			send = res.SendPrimarySelection
425		}
426		if src == nil {
427			send(nil)
428			return
429		}
430		offer, err := extdatacontrol.NewExtDataControlOfferV1(res.Client(), res.Version(), 0, controlOffer{s, kind, src})
431		if err != nil {
432			return
433		}
434		res.SendDataOffer(offer)
435		for _, mime := range src.mimes {
436			offer.SendOffer(mime)
437		}
438		send(offer)
439	}})
440}
441func (controlManager) Destroy(*extdatacontrol.ExtDataControlManagerV1) {}
442
443type controlSource struct{ src *clipSource }
444
445func (c controlSource) Offer(r *extdatacontrol.ExtDataControlSourceV1, mime string) {
446	if !c.src.addMime(mime) {
447		r.PostError(uint32(extdatacontrol.ExtDataControlSourceV1ErrorInvalidOffer), "offer after the source was used")
448	}
449}
450func (controlSource) Destroy(*extdatacontrol.ExtDataControlSourceV1) {}
451
452type controlDevice struct{ server *Server }
453
454func (d controlDevice) SetSelection(r *extdatacontrol.ExtDataControlDeviceV1, res *extdatacontrol.ExtDataControlSourceV1) {
455	d.set(r, selClipboard, res)
456}
457func (d controlDevice) SetPrimarySelection(r *extdatacontrol.ExtDataControlDeviceV1, res *extdatacontrol.ExtDataControlSourceV1) {
458	d.set(r, selPrimary, res)
459}
460
461// set lets a clipboard manager set a selection without focus. A source
462// may be used once.
463func (d controlDevice) set(r *extdatacontrol.ExtDataControlDeviceV1, kind int, res *extdatacontrol.ExtDataControlSourceV1) {
464	s := d.server
465	var src *clipSource
466	if res != nil {
467		src = s.controlSources[res.Resource]
468		if src == nil {
469			return
470		}
471		if src.used {
472			r.PostError(uint32(extdatacontrol.ExtDataControlDeviceV1ErrorUsedSource), "source already used")
473			return
474		}
475		src.used = true
476	}
477	s.setSelection(kind, src)
478}
479func (controlDevice) Destroy(*extdatacontrol.ExtDataControlDeviceV1) {}
480
481type controlOffer struct {
482	server *Server
483	kind   int
484	src    *clipSource
485}
486
487func (o controlOffer) Receive(_ *extdatacontrol.ExtDataControlOfferV1, mime string, fd int) {
488	o.server.receive(o.kind, o.src, mime, fd)
489}
490func (controlOffer) Destroy(*extdatacontrol.ExtDataControlOfferV1) {}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close