v0.3.91 9a5c565

clipboard_image_test.go RawHistoryFolder

1package tui
2
3import (
4	"bytes"
5	"errors"
6	"image"
7	"image/color"
8	"image/jpeg"
9	"image/png"
10	"os"
11	"path/filepath"
12	"runtime"
13	"testing"
14)
15
16func testClipboardPNG(t *testing.T) []byte {
17	t.Helper()
18	img := image.NewRGBA(image.Rect(0, 0, 2, 2))
19	img.Set(0, 0, color.RGBA{R: 255, A: 255})
20	img.Set(1, 0, color.RGBA{G: 255, A: 255})
21	var buf bytes.Buffer
22	if err := png.Encode(&buf, img); err != nil {
23		t.Fatal(err)
24	}
25	return buf.Bytes()
26}
27
28func TestNormalizeClipboardImagePNGValidatesPNG(t *testing.T) {
29	got, err := normalizeClipboardImagePNG(testClipboardPNG(t))
30	if err != nil {
31		t.Fatal(err)
32	}
33	if _, format, err := image.Decode(bytes.NewReader(got)); err != nil || format != "png" {
34		t.Fatalf("decoded format = %q, err = %v", format, err)
35	}
36}
37
38func TestNormalizeClipboardImagePNGConvertsJPEG(t *testing.T) {
39	img := image.NewRGBA(image.Rect(0, 0, 2, 2))
40	var jpegData bytes.Buffer
41	if err := jpeg.Encode(&jpegData, img, nil); err != nil {
42		t.Fatal(err)
43	}
44
45	got, err := normalizeClipboardImagePNG(jpegData.Bytes())
46	if err != nil {
47		t.Fatal(err)
48	}
49	if _, format, err := image.Decode(bytes.NewReader(got)); err != nil || format != "png" {
50		t.Fatalf("decoded format = %q, err = %v", format, err)
51	}
52}
53
54func TestNormalizeClipboardImagePNGRejectsMalformedData(t *testing.T) {
55	if _, err := normalizeClipboardImagePNG([]byte("not an image")); err == nil {
56		t.Fatal("normalizeClipboardImagePNG accepted malformed data")
57	}
58}
59
60func TestNormalizeClipboardImagePNGEnforcesLimits(t *testing.T) {
61	data := testClipboardPNG(t)
62	if _, err := normalizeClipboardImagePNGWithLimits(data, len(data)-1, 100, 1024); !errors.Is(err, errClipboardImageTooLarge) {
63		t.Fatalf("input limit error = %v", err)
64	}
65	if _, err := normalizeClipboardImagePNGWithLimits(data, len(data), 3, 1024); !errors.Is(err, errClipboardImageTooLarge) {
66		t.Fatalf("pixel limit error = %v", err)
67	}
68	if _, err := normalizeClipboardImagePNGWithLimits(data, len(data), 100, 1); !errors.Is(err, errClipboardImageTooLarge) {
69		t.Fatalf("output limit error = %v", err)
70	}
71}
72
73func TestWriteClipboardPNGFileUsesRestrictivePermissions(t *testing.T) {
74	if runtime.GOOS == "windows" {
75		t.Skip("Windows FileMode permission bits do not represent ACL access")
76	}
77	path := filepath.Join(t.TempDir(), "clipboard.png")
78	if err := writeClipboardPNGFile(path, testClipboardPNG(t)); err != nil {
79		t.Fatal(err)
80	}
81	info, err := os.Stat(path)
82	if err != nil {
83		t.Fatal(err)
84	}
85	if got := info.Mode().Perm(); got&0o077 != 0 {
86		t.Fatalf("file permissions = %o, want no group or other access", got)
87	}
88}
89
90func TestRunClipboardImageCommandSeparatesStderr(t *testing.T) {
91	if runtime.GOOS == "windows" {
92		t.Skip("test helper is a shell script")
93	}
94	dir := t.TempDir()
95	path := filepath.Join(dir, "clipboard-image-test")
96	if err := os.WriteFile(path, []byte("#!/bin/sh\nprintf payload\nprintf diagnostic >&2\n"), 0o700); err != nil {
97		t.Fatal(err)
98	}
99	t.Setenv("PATH", dir)
100
101	got, err := runClipboardImageCommand("clipboard-image-test", 100)
102	if err != nil {
103		t.Fatal(err)
104	}
105	if string(got) != "payload" {
106		t.Fatalf("output = %q, want payload", got)
107	}
108}
109
110func TestRunClipboardImageCommandCapsOutput(t *testing.T) {
111	if runtime.GOOS == "windows" {
112		t.Skip("test helper is a shell script")
113	}
114	dir := t.TempDir()
115	path := filepath.Join(dir, "clipboard-image-test")
116	if err := os.WriteFile(path, []byte("#!/bin/sh\nprintf 'too much data'\n"), 0o700); err != nil {
117		t.Fatal(err)
118	}
119	t.Setenv("PATH", dir)
120
121	if _, err := runClipboardImageCommand("clipboard-image-test", 3); err == nil {
122		t.Fatal("runClipboardImageCommand accepted oversized output")
123	}
124}

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