v0.38.0 58133ea

clipboard_linux_test.go RawHistoryFolder

1//go:build linux
2
3package tui
4
5import (
6	"bytes"
7	"context"
8	"os"
9	"path/filepath"
10	"testing"
11)
12
13func TestReadClipboardImageWaylandUsesMIMEPriority(t *testing.T) {
14	dir := t.TempDir()
15	state := filepath.Join(dir, "listed")
16	writeClipboardTestHelper(t, dir, "wl-paste", `#!/bin/sh
17if [ "$1" = "--list-types" ]; then
18  : > "$CLIPBOARD_TEST_STATE"
19  printf 'image/jpeg\nimage/png\nimage/bmp\n'
20  exit 0
21fi
22if [ "$1" = "--type" ]; then
23  if [ ! -f "$CLIPBOARD_TEST_STATE" ]; then
24    exit 1
25  fi
26  case "$2" in
27    image/png) printf 'png-bytes' ;;
28    image/jpeg) printf 'jpeg-bytes' ;;
29    *) exit 1 ;;
30  esac
31  exit 0
32fi
33exit 1
34`)
35	t.Setenv("PATH", dir)
36	t.Setenv("WAYLAND_DISPLAY", "wayland-test")
37	t.Setenv("DISPLAY", "")
38	t.Setenv("CLIPBOARD_TEST_STATE", state)
39
40	image, ok, err := ReadClipboardImage(context.Background())
41	if err != nil {
42		t.Fatalf("ReadClipboardImage() error = %v", err)
43	}
44	if !ok {
45		t.Fatal("ReadClipboardImage() reported no image")
46	}
47	if image.MimeType != "image/png" {
48		t.Fatalf("MIME type = %q, want image/png", image.MimeType)
49	}
50	if string(image.Data) != "png-bytes" {
51		t.Fatalf("image data = %q, want png-bytes", image.Data)
52	}
53}
54
55func TestReadClipboardImageFallsBackToX11Helpers(t *testing.T) {
56	dir := t.TempDir()
57	writeClipboardTestHelper(t, dir, "wl-paste", `#!/bin/sh
58if [ "$1" = "--list-types" ]; then
59  printf 'text/plain\n'
60fi
61`)
62	writeClipboardTestHelper(t, dir, "xclip", `#!/bin/sh
63case "$*" in
64  *"image/gif"*) printf 'gif-bytes' ;;
65  *) exit 1 ;;
66esac
67`)
68	t.Setenv("PATH", dir)
69	t.Setenv("WAYLAND_DISPLAY", "wayland-test")
70	t.Setenv("DISPLAY", ":test")
71
72	image, ok, err := ReadClipboardImage(context.Background())
73	if err != nil {
74		t.Fatalf("ReadClipboardImage() error = %v", err)
75	}
76	if !ok {
77		t.Fatal("ReadClipboardImage() reported no image")
78	}
79	if image.MimeType != "image/gif" {
80		t.Fatalf("MIME type = %q, want image/gif", image.MimeType)
81	}
82	if string(image.Data) != "gif-bytes" {
83		t.Fatalf("image data = %q, want gif-bytes", image.Data)
84	}
85}
86
87func TestReadClipboardImageUnavailableIsNoImage(t *testing.T) {
88	dir := t.TempDir()
89	writeClipboardTestHelper(t, dir, "wl-paste", `#!/bin/sh
90printf 'clipboard helper failure' >&2
91exit 1
92`)
93	t.Setenv("PATH", dir)
94	t.Setenv("WAYLAND_DISPLAY", "wayland-test")
95	t.Setenv("DISPLAY", "")
96
97	image, ok, err := ReadClipboardImage(context.Background())
98	if err != nil {
99		t.Fatalf("ReadClipboardImage() error = %v, want nil", err)
100	}
101	if ok {
102		t.Fatalf("ReadClipboardImage() returned image %#v", image)
103	}
104	if image.MimeType != "" || len(image.Data) != 0 {
105		t.Fatalf("empty image = %#v", image)
106	}
107
108	path, data, ok, err := ReadClipboardImagePNG()
109	if err != nil || ok || path != "" || data != nil {
110		t.Fatalf("ReadClipboardImagePNG() = (%q, %v, %v, %v), want empty result", path, data, ok, err)
111	}
112}
113
114func TestWriteClipboardTextWaylandUsesWlCopy(t *testing.T) {
115	dir := t.TempDir()
116	payload := filepath.Join(dir, "clipboard-payload")
117	writeClipboardTestHelper(t, dir, "wl-copy", `#!/bin/sh
118if [ "$1" != "--type" ] || [ "$2" != "text/plain" ]; then
119  exit 1
120fi
121exec /bin/cat > "$CLIPBOARD_TEST_PAYLOAD"
122`)
123	t.Setenv("PATH", dir)
124	t.Setenv("WAYLAND_DISPLAY", "wayland-test")
125	t.Setenv("DISPLAY", "")
126	t.Setenv("CLIPBOARD_TEST_PAYLOAD", payload)
127
128	if err := WriteClipboardText(context.Background(), "https://example.com/oauth?state=xyz"); err != nil {
129		t.Fatal(err)
130	}
131	got, err := os.ReadFile(payload)
132	if err != nil {
133		t.Fatal(err)
134	}
135	if string(got) != "https://example.com/oauth?state=xyz" {
136		t.Fatalf("clipboard payload = %q", got)
137	}
138}
139
140func TestRunClipboardImageCommandEnforcesSizeLimit(t *testing.T) {
141	dir := t.TempDir()
142	payload := filepath.Join(dir, "payload")
143	writeClipboardTestHelper(t, dir, "clipboard-test-helper", `#!/bin/sh
144exec /bin/cat "$CLIPBOARD_TEST_PAYLOAD"
145`)
146	t.Setenv("PATH", dir)
147	t.Setenv("CLIPBOARD_TEST_PAYLOAD", payload)
148
149	if err := os.WriteFile(payload, bytes.Repeat([]byte{'x'}, maxClipboardImageBytes), 0o600); err != nil {
150		t.Fatal(err)
151	}
152	data, ok := runClipboardImageCommand(context.Background(), clipboardImageCommand{name: "clipboard-test-helper"})
153	if !ok || len(data) != maxClipboardImageBytes {
154		t.Fatalf("exact-limit command result = (%d bytes, %v), want (%d bytes, true)", len(data), ok, maxClipboardImageBytes)
155	}
156
157	if err := os.WriteFile(payload, bytes.Repeat([]byte{'x'}, maxClipboardImageBytes+1), 0o600); err != nil {
158		t.Fatal(err)
159	}
160	data, ok = runClipboardImageCommand(context.Background(), clipboardImageCommand{name: "clipboard-test-helper"})
161	if ok || data != nil {
162		t.Fatalf("over-limit command result = (%d bytes, %v), want (0 bytes, false)", len(data), ok)
163	}
164}
165
166func writeClipboardTestHelper(t *testing.T, dir, name, script string) {
167	t.Helper()
168	path := filepath.Join(dir, name)
169	if err := os.WriteFile(path, []byte(script), 0o700); err != nil {
170		t.Fatal(err)
171	}
172}

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 476

↑↓ moveenter openesc close