detached ece7682

wlpaste.go RawHistoryFolder

1//go:build linux
2
3// Package clipboard implements ports.ClipboardReader over Wayland's
4// wl-clipboard CLI (wl-paste). It lets a remote attach forward a locally
5// clipped image to the daemon.
6package clipboard
7
8import (
9	"bytes"
10	"context"
11	"os/exec"
12	"strings"
13	"time"
14
15	"github.com/bnema/vev/internal/ports"
16)
17
18// mimePreference orders candidate image MIME types the same way pi (the
19// reference TUI agent whose local Ctrl+V behavior this replicates) does:
20// png first, then the other common web/image formats.
21var mimePreference = []string{"image/png", "image/jpeg", "image/webp", "image/gif"}
22
23// listTypesTimeout bounds the "what's on the clipboard" probe so a hung or
24// missing wl-paste never stalls the stdin pump for long.
25const listTypesTimeout = 1 * time.Second
26
27// defaultReadTimeout bounds the actual clipboard data read. Without this, a
28// wl-paste that hangs mid-read (as opposed to during the --list-types probe,
29// which listTypesTimeout already covers) would stall the client's stdin pump
30// indefinitely, since ReadImage is called with context.Background() from the
31// interceptor. Keep this short so a slow clipboard read cannot noticeably
32// freeze typing for longer than the image-transfer MVP's small payload cap.
33const defaultReadTimeout = time.Second
34
35// WlPaste implements ports.ClipboardReader by shelling out to wl-paste. It
36// holds no state and is safe for concurrent use; every call runs a fresh
37// subprocess.
38type WlPaste struct {
39	// execCommand constructs the command to run; overridable in tests so
40	// command construction can be verified without requiring wl-paste (or
41	// even Wayland) to be installed.
42	execCommand func(ctx context.Context, name string, args ...string) *exec.Cmd
43	// readTimeout bounds the data-read command; zero means defaultReadTimeout
44	// (a zero-value WlPaste{execCommand: ...}, as tests construct directly,
45	// still gets a sane default). Tests needing a short deadline set it
46	// explicitly.
47	readTimeout time.Duration
48}
49
50// New returns a WlPaste clipboard reader that shells out to the real
51// wl-paste binary.
52func New() ports.ClipboardReader {
53	return &WlPaste{execCommand: exec.CommandContext, readTimeout: defaultReadTimeout}
54}
55
56func (w *WlPaste) readTimeoutOrDefault() time.Duration {
57	if w.readTimeout <= 0 {
58		return defaultReadTimeout
59	}
60	return w.readTimeout
61}
62
63// ReadImage implements ports.ClipboardReader. It lists the clipboard's
64// offered MIME types, picks the best supported image type (if any), and
65// reads it. Any failure (wl-paste missing, no image on the clipboard, a
66// read error or timeout) is reported as ports.ErrNoClipboardImage so callers
67// uniformly fall back to forwarding the keystroke that triggered the read.
68func (w *WlPaste) ReadImage(ctx context.Context) (string, []byte, error) {
69	listCtx, cancel := context.WithTimeout(ctx, listTypesTimeout)
70	defer cancel()
71
72	listCmd := w.execCommand(listCtx, "wl-paste", "--list-types")
73	out, err := listCmd.Output()
74	if err != nil {
75		return "", nil, ports.ErrNoClipboardImage
76	}
77
78	mime := pickImageMime(string(out))
79	if mime == "" {
80		return "", nil, ports.ErrNoClipboardImage
81	}
82
83	readCtx, readCancel := context.WithTimeout(ctx, w.readTimeoutOrDefault())
84	defer readCancel()
85
86	readCmd := w.execCommand(readCtx, "wl-paste", "--type", mime, "--no-newline")
87	var buf bytes.Buffer
88	readCmd.Stdout = &buf
89	if err := readCmd.Run(); err != nil {
90		return "", nil, ports.ErrNoClipboardImage
91	}
92	return mime, buf.Bytes(), nil
93}
94
95// pickImageMime scans wl-paste --list-types output (one MIME type per line)
96// and returns the first match in mimePreference order, or "" if the
97// clipboard offers no supported image type.
98func pickImageMime(listTypesOutput string) string {
99	offered := make(map[string]bool)
100	for _, line := range strings.Split(listTypesOutput, "\n") {
101		line = strings.TrimSpace(line)
102		if line != "" {
103			offered[line] = true
104		}
105	}
106	for _, mime := range mimePreference {
107		if offered[mime] {
108			return mime
109		}
110	}
111	return ""
112}

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 12

Tags 11

↑↓ moveenter openesc close