v0.3.93 0192a2e

clipboard_darwin.go RawHistoryFolder

1//go:build darwin
2
3package tui
4
5import (
6	"context"
7	"crypto/rand"
8	"encoding/hex"
9	"fmt"
10	"os"
11	"os/exec"
12	"path/filepath"
13	"strings"
14	"time"
15)
16
17const readClipboardImageScript = `
18on run argv
19	set outPath to item 1 of argv
20	try
21		set imgData to the clipboard as «class PNGf»
22		set imgKind to "png"
23	on error
24		try
25			set imgData to the clipboard as «class TIFF»
26			set imgKind to "tiff"
27		on error
28			return "NO_IMAGE"
29		end try
30	end try
31
32	set outFile to POSIX file outPath
33	set fileRef to open for access outFile with write permission
34	try
35		set eof of fileRef to 0
36		write imgData to fileRef
37		close access fileRef
38	on error errMsg number errNum
39		try
40			close access fileRef
41		end try
42		error errMsg number errNum
43	end try
44	return imgKind
45end run
46`
47
48func ReadClipboardImagePNG() (string, []byte, bool, error) {
49	dir := clipboardImageDir()
50	if err := os.MkdirAll(dir, 0o700); err != nil {
51		return "", nil, false, fmt.Errorf("create clipboard image directory: %w", err)
52	}
53	if err := os.Chmod(dir, 0o700); err != nil {
54		return "", nil, false, fmt.Errorf("secure clipboard image directory: %w", err)
55	}
56	path := filepath.Join(dir, "clipboard-"+time.Now().Format("20060102-150405")+"-"+randomHex(4)+".png")
57	rawPath := path + ".raw"
58	defer os.Remove(rawPath)
59
60	kind, err := writeClipboardImageData(rawPath)
61	if err != nil {
62		return "", nil, false, err
63	}
64	if kind == "" {
65		return "", nil, false, nil
66	}
67
68	sourcePath := rawPath
69	if kind != "png" && kind != "tiff" {
70		var ok bool
71		sourcePath, ok = findClipboardImagePath(kind)
72		if !ok {
73			return "", nil, false, fmt.Errorf("unexpected clipboard image kind %q", kind)
74		}
75	}
76	raw, err := readClipboardImageFile(sourcePath)
77	if err != nil {
78		return "", nil, false, fmt.Errorf("read clipboard image: %w", err)
79	}
80	data, err := normalizeClipboardImagePNG(raw)
81	if err != nil {
82		return "", nil, false, fmt.Errorf("invalid clipboard image: %w", err)
83	}
84	if err := writeClipboardPNGFile(path, data); err != nil {
85		return "", nil, false, fmt.Errorf("write clipboard PNG: %w", err)
86	}
87	return path, data, true, nil
88}
89
90func writeClipboardImageData(path string) (string, error) {
91	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
92	defer cancel()
93	cmd := exec.CommandContext(ctx, "/usr/bin/osascript", "-e", readClipboardImageScript, path)
94	out, err := cmd.CombinedOutput()
95	trimmed := strings.TrimSpace(string(out))
96	if ctx.Err() != nil {
97		return "", fmt.Errorf("osascript timed out: %w", ctx.Err())
98	}
99	if err != nil {
100		if strings.Contains(trimmed, "NO_IMAGE") || strings.Contains(trimmed, "Can’t make") || strings.Contains(trimmed, "Can't make") {
101			return "", nil
102		}
103		if kind, ok := clipboardImageKind(trimmed); ok {
104			return kind, nil
105		}
106		if path, ok := findClipboardImagePath(trimmed); ok {
107			return path, nil
108		}
109		if trimmed == "" {
110			return "", fmt.Errorf("osascript failed: %w", err)
111		}
112		return "", fmt.Errorf("osascript failed: %s", trimmed)
113	}
114	if trimmed == "NO_IMAGE" {
115		return "", nil
116	}
117	if kind, ok := clipboardImageKind(trimmed); ok {
118		return kind, nil
119	}
120	return trimmed, nil
121}
122
123func clipboardImageKind(s string) (string, bool) {
124	for _, line := range strings.Split(s, "\n") {
125		switch strings.TrimSpace(line) {
126		case "png":
127			return "png", true
128		case "tiff":
129			return "tiff", true
130		}
131	}
132	return "", false
133}
134
135func findClipboardImagePath(s string) (string, bool) {
136	if p, ok := clipboardImagePath(s); ok {
137		return p, true
138	}
139	for _, ext := range []string{".png", ".tiff", ".tif"} {
140		lower := strings.ToLower(s)
141		end := strings.Index(lower, ext)
142		if end < 0 {
143			continue
144		}
145		end += len(ext)
146		start := strings.LastIndex(s[:end], "/")
147		if start < 0 {
148			continue
149		}
150		for start > 0 {
151			c := s[start-1]
152			if c == '\'' || c == '"' || c == '\n' || c == '\r' || c == '\t' {
153				break
154			}
155			start--
156		}
157		if p, ok := clipboardImagePath(s[start:end]); ok {
158			return p, true
159		}
160	}
161	return "", false
162}
163
164func clipboardImagePath(s string) (string, bool) {
165	p := strings.TrimSpace(s)
166	p = strings.Trim(p, "'\"")
167	if p == "" {
168		return "", false
169	}
170	info, err := os.Stat(p)
171	if err != nil || info.IsDir() {
172		return "", false
173	}
174	switch strings.ToLower(filepath.Ext(p)) {
175	case ".png", ".tif", ".tiff":
176		return p, true
177	default:
178		return "", false
179	}
180}
181
182func clipboardImageDir() string {
183	if info, err := os.Stat("/tmp"); err == nil && info.IsDir() {
184		return filepath.Join("/tmp", "zot-clipboard-images")
185	}
186	return filepath.Join(os.TempDir(), "zot-clipboard-images")
187}
188
189func randomHex(n int) string {
190	buf := make([]byte, n)
191	if _, err := rand.Read(buf); err != nil {
192		return fmt.Sprintf("%d", time.Now().UnixNano())
193	}
194	return hex.EncodeToString(buf)
195}

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