v0.3.0 6800c6a

clipboard_test.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"context"
7	"errors"
8	"io"
9	"net"
10	"os"
11	"testing"
12	"time"
13
14	"github.com/bnema/nefergui/internal/edit"
15	"github.com/bnema/wlturbo"
16	"github.com/bnema/wlturbo/protocol/core"
17	"golang.org/x/sys/unix"
18)
19
20func clipboardFixture(t *testing.T) (*Clipboard, *wlturbo.Display, *net.UnixConn) {
21	t.Helper()
22	ca, server := inputPair(t)
23	d, err := wlturbo.ConnectFromConn(ca)
24	if err != nil {
25		t.Fatal(err)
26	}
27	t.Cleanup(func() { _ = d.Close() })
28	inputRequest(t, server)
29	ctx := d.Context()
30	manager := core.NewDataDeviceManager(ctx)
31	if err = d.Registry().Bind(1, core.DataDeviceManagerInterface, 3, manager); err != nil {
32		t.Fatal(err)
33	}
34	inputRequest(t, server)
35	seat := core.NewSeat(ctx)
36	seat.SetID(20)
37	ctx.Register(seat)
38	device, err := manager.GetDataDevice(seat)
39	if err != nil {
40		t.Fatal(err)
41	}
42	inputRequest(t, server)
43	c := &Clipboard{manager: manager, device: device, serial: func() uint32 { return 42 }, pending: make(map[*core.DataOffer]map[string]bool), offers: make(map[uint32]*core.DataOffer)}
44	c.post = func(e ClipboardEvent) { c.ApplyClipboard(e) }
45	return c, d, server
46}
47func TestClipboardReadOverSocketpair(t *testing.T) {
48	c, d, server := clipboardFixture(t)
49	offer := core.NewDataOffer(d.Context())
50	offer.SetID(30)
51	d.Context().Register(offer)
52	c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: offer})
53	for _, m := range []string{mimePlain, mimeX11, mimeUTF8} {
54		c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: offer, mime: m})
55	}
56	c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: offer})
57	if c.bestMIME() != mimeUTF8 {
58		t.Fatal("MIME priority")
59	}
60	results := make(chan ClipboardResult, 2)
61	// The peer reads receive's SCM_RIGHTS and writes to the transferred pipe.
62	go func() {
63		b := make([]byte, 512)
64		oob := make([]byte, 128)
65		_ = server.SetReadDeadline(time.Now().Add(time.Second))
66		_, n, _, _, err := server.ReadMsgUnix(b, oob)
67		if err != nil {
68			results <- ClipboardResult{Err: err}
69			return
70		}
71		messages, err := unix.ParseSocketControlMessage(oob[:n])
72		if err != nil {
73			results <- ClipboardResult{Err: err}
74			return
75		}
76		for _, m := range messages {
77			fd, e := unix.ParseUnixRights(&m)
78			if e != nil || len(fd) == 0 {
79				continue
80			}
81			f := os.NewFile(uintptr(fd[0]), "offer-write")
82			_, e = io.WriteString(f, "hello")
83			_ = f.Close()
84			if e != nil {
85				results <- ClipboardResult{Err: e}
86			}
87			return
88		}
89		results <- ClipboardResult{Err: errors.New("missing pipe FD")}
90	}()
91	if err := c.ReadTextAsync(context.Background(), edit.MaxClipboardBytes, func(b []byte, e error) { results <- ClipboardResult{Data: b, Err: e} }); err != nil {
92		t.Fatal(err)
93	}
94	select {
95	case result := <-results:
96		if result.Err != nil || string(result.Data) != "hello" {
97			t.Fatalf("result: %+v", result)
98		}
99	case <-time.After(2 * time.Second):
100		t.Fatal("read blocked")
101	}
102}
103func TestClipboardBoundAndCancel(t *testing.T) {
104	r, w, err := os.Pipe()
105	if err != nil {
106		t.Fatal(err)
107	}
108	defer w.Close()
109	results := make(chan ClipboardResult, 1)
110	go func() { data, e := io.ReadAll(io.LimitReader(r, 5)); results <- ClipboardResult{data, e} }()
111	_, _ = w.Write([]byte("123456"))
112	select {
113	case result := <-results:
114		if len(result.Data) != 5 {
115			t.Fatal(result)
116		}
117	case <-time.After(time.Second):
118		t.Fatal("limit blocked")
119	}
120	state := edit.State{}
121	if ok, _ := state.PasteAsync([]byte{0xff}, nil); ok {
122		t.Fatal("accepted invalid UTF-8")
123	}
124	if ok, _ := state.PasteAsync(make([]byte, edit.MaxClipboardBytes+1), nil); ok {
125		t.Fatal("accepted oversized paste")
126	}
127}

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 1

Tags 12

↑↓ moveenter openesc close