main 5d6fa26

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

Tags 10

↑↓ moveenter openesc close