Skip to content

Commit 1e2b658

Browse files
committed
fix gh-paste-image; uses public repo :(
1 parent 08fdf04 commit 1e2b658

1 file changed

Lines changed: 147 additions & 120 deletions

File tree

scripts/gh-paste-image

Lines changed: 147 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,147 @@
1-
#!/usr/bin/env bash
2-
# Upload a clipboard image/video (or file) to GitHub and output/copy the markdown snippet.
3-
# Usage: gh-paste-image [--repo owner/repo] [file]
4-
set -euo pipefail
5-
6-
REPO=""
7-
FILE=""
8-
while [[ $# -gt 0 ]]; do
9-
case "$1" in
10-
--repo) REPO="$2"; shift 2 ;;
11-
-*) echo "Usage: gh-paste-image [--repo owner/repo] [file]" >&2; exit 1 ;;
12-
*) FILE="$1"; shift ;;
13-
esac
14-
done
15-
16-
if [[ -z "$REPO" ]]; then
17-
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null) || {
18-
echo "Error: not in a GitHub repo — run from a git checkout or pass --repo owner/repo" >&2
19-
exit 1
20-
}
21-
fi
22-
23-
TOKEN=$(gh auth token)
24-
TMPFILE=""
25-
26-
if [[ -n "$FILE" ]]; then
27-
MIME=$(file --mime-type -b "$FILE")
28-
TMPFILE="$FILE"
29-
OWN_TMP=0
30-
else
31-
# Pick the best type available in the clipboard (images + video)
32-
MIME=$(wl-paste --list-types 2>/dev/null \
33-
| grep -E '^(image/(png|jpeg|gif|webp)|video/mp4)' \
34-
| head -1)
35-
36-
if [[ -z "$MIME" ]]; then
37-
echo "Error: no supported image/video found in clipboard" >&2
38-
echo "Available types: $(wl-paste --list-types 2>/dev/null | tr '\n' ' ')" >&2
39-
exit 1
40-
fi
41-
42-
EXT="${MIME#*/}"
43-
[[ "$EXT" == "jpeg" ]] && EXT="jpg"
44-
TMPFILE=$(mktemp --suffix=".$EXT")
45-
OWN_TMP=1
46-
trap '[[ "$OWN_TMP" == 1 ]] && rm -f "$TMPFILE"' EXIT
47-
48-
wl-paste --type "$MIME" > "$TMPFILE"
49-
50-
if [[ ! -s "$TMPFILE" ]]; then
51-
echo "Error: clipboard content is empty" >&2
52-
exit 1
53-
fi
54-
fi
55-
56-
echo "Uploading $MIME ($(du -h "$TMPFILE" | cut -f1)) to $REPO..." >&2
57-
58-
RESPONSE=$(curl -s \
59-
-X POST \
60-
-H "Authorization: token $TOKEN" \
61-
-F "data=@$TMPFILE;type=$MIME" \
62-
"https://uploads.github.com/repos/$REPO/issues/assets")
63-
64-
URL=$(echo "$RESPONSE" | jq -r '.href // empty')
65-
66-
if [[ -z "$URL" ]]; then
67-
echo "Error: upload failed" >&2
68-
echo "Response: $RESPONSE" >&2
69-
exit 1
70-
fi
71-
72-
# Format output to match what GitHub's web UI produces
73-
case "$MIME" in
74-
image/png|image/jpeg)
75-
DIMS=$(python3 - "$TMPFILE" <<'EOF'
76-
import struct, sys
77-
78-
path = sys.argv[1]
79-
with open(path, 'rb') as f:
80-
header = f.read(24)
81-
82-
# PNG: 8-byte sig, then IHDR: 4 len + 4 type + 4 width + 4 height
83-
if header[:8] == b'\x89PNG\r\n\x1a\n':
84-
w, h = struct.unpack('>II', header[16:24])
85-
print(f"{w} {h}")
86-
sys.exit(0)
87-
88-
# JPEG: scan for SOF0/SOF2 marker (0xFF 0xC0 or 0xFF 0xC2)
89-
with open(path, 'rb') as f:
90-
data = f.read()
91-
for marker in (b'\xff\xc0', b'\xff\xc2'):
92-
i = data.find(marker)
93-
if i != -1 and i + 9 <= len(data):
94-
h, w = struct.unpack('>HH', data[i+5:i+9])
95-
print(f"{w} {h}")
96-
sys.exit(0)
97-
98-
sys.exit(1)
99-
EOF
100-
2>/dev/null) || true
101-
102-
if [[ -n "$DIMS" ]]; then
103-
W=$(echo "$DIMS" | cut -d' ' -f1)
104-
H=$(echo "$DIMS" | cut -d' ' -f2)
105-
MARKDOWN="<img width=\"$W\" height=\"$H\" alt=\"image\" src=\"$URL\" />"
106-
else
107-
MARKDOWN="![image]($URL)"
108-
fi
109-
;;
110-
video/*)
111-
MARKDOWN="$URL"
112-
;;
113-
*)
114-
MARKDOWN="![image]($URL)"
115-
;;
116-
esac
117-
118-
printf '%s\n' "$MARKDOWN"
119-
printf '%s' "$MARKDOWN" | wl-copy
120-
echo "(copied to clipboard)" >&2
1+
#!/usr/bin/env python3
2+
"""Upload a clipboard image/video (or file) to a GitHub assets repo and copy the markdown snippet.
3+
4+
The image is committed to a public GitHub repo via the Contents API so the URL
5+
is publicly accessible. Set GITHUB_PASTE_REPO=owner/repo to override the default
6+
(which is <your-gh-username>/gh-paste-uploads).
7+
8+
Usage: gh-paste-image [--assets-repo owner/repo] [file]
9+
"""
10+
import argparse
11+
import base64
12+
import os
13+
import struct
14+
import subprocess
15+
import sys
16+
import time
17+
18+
import requests
19+
20+
21+
def run(cmd):
22+
return subprocess.check_output(cmd).decode().strip()
23+
24+
25+
def get_token():
26+
return run(["gh", "auth", "token"])
27+
28+
29+
def get_github_user(token):
30+
return run(["gh", "api", "/user", "--jq", ".login"])
31+
32+
33+
def ensure_repo_exists(repo, token):
34+
"""Create the assets repo if it doesn't exist yet."""
35+
owner, name = repo.split("/", 1)
36+
resp = requests.get(
37+
f"https://api.github.com/repos/{repo}",
38+
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github+json"},
39+
)
40+
if resp.status_code == 404:
41+
print(f"Creating public assets repo {repo}...", file=sys.stderr)
42+
create_resp = requests.post(
43+
"https://api.github.com/user/repos",
44+
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github+json"},
45+
json={"name": name, "private": False, "description": "GitHub paste image uploads", "auto_init": True},
46+
)
47+
if not create_resp.ok:
48+
sys.exit(f"Error: could not create repo {repo}: {create_resp.text[:200]}")
49+
elif not resp.ok:
50+
sys.exit(f"Error: could not access repo {repo}: {resp.text[:200]}")
51+
52+
53+
def upload_to_repo(repo, token, filename, data, mime):
54+
"""Commit the file to the assets repo; return the raw URL."""
55+
ts = int(time.time())
56+
ext = filename.rsplit(".", 1)[-1] if "." in filename else "bin"
57+
path = f"uploads/{ts}-{filename}"
58+
content_b64 = base64.b64encode(data).decode()
59+
60+
resp = requests.put(
61+
f"https://api.github.com/repos/{repo}/contents/{path}",
62+
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github+json"},
63+
json={
64+
"message": f"upload: {filename}",
65+
"content": content_b64,
66+
},
67+
)
68+
if not resp.ok:
69+
sys.exit(f"Error: upload failed {resp.status_code}: {resp.text[:200]}")
70+
return resp.json()["content"]["download_url"]
71+
72+
73+
def clipboard_image():
74+
result = subprocess.run(["wl-paste", "--list-types"], capture_output=True, text=True)
75+
if result.returncode != 0:
76+
sys.exit("Error: wl-paste not available")
77+
import re
78+
supported = re.findall(r'^(image/(?:png|jpeg|gif|webp)|video/mp4)$', result.stdout, re.MULTILINE)
79+
if not supported:
80+
sys.exit(f"Error: no supported image/video in clipboard. Available: {result.stdout.strip()}")
81+
mime = supported[0]
82+
ext = mime.split("/")[1].replace("jpeg", "jpg")
83+
data = subprocess.check_output(["wl-paste", "--type", mime])
84+
if not data:
85+
sys.exit("Error: clipboard content is empty")
86+
return data, mime, f"image.{ext}"
87+
88+
89+
def image_dims(data, mime):
90+
try:
91+
if mime == "image/png" and data[:8] == b'\x89PNG\r\n\x1a\n':
92+
w, h = struct.unpack('>II', data[16:24])
93+
return w, h
94+
elif mime == "image/jpeg":
95+
for marker in (b'\xff\xc0', b'\xff\xc2'):
96+
i = data.find(marker)
97+
if i != -1 and i + 9 <= len(data):
98+
h, w = struct.unpack('>HH', data[i+5:i+9])
99+
return w, h
100+
except Exception:
101+
pass
102+
return None, None
103+
104+
105+
def main():
106+
parser = argparse.ArgumentParser(description=__doc__)
107+
parser.add_argument("--assets-repo", default=os.environ.get("GITHUB_PASTE_REPO", ""))
108+
parser.add_argument("file", nargs="?", default="")
109+
args = parser.parse_args()
110+
111+
token = get_token()
112+
113+
if not args.assets_repo:
114+
user = get_github_user(token)
115+
args.assets_repo = f"{user}/gh-paste-uploads"
116+
117+
if args.file:
118+
with open(args.file, "rb") as f:
119+
data = f.read()
120+
mime = subprocess.check_output(["file", "--mime-type", "-b", args.file]).decode().strip()
121+
filename = os.path.basename(args.file)
122+
else:
123+
data, mime, filename = clipboard_image()
124+
125+
print(f"Uploading {mime} ({len(data) // 1024}K) to {args.assets_repo}...", file=sys.stderr)
126+
127+
ensure_repo_exists(args.assets_repo, token)
128+
url = upload_to_repo(args.assets_repo, token, filename, data, mime)
129+
130+
if mime in ("image/png", "image/jpeg"):
131+
w, h = image_dims(data, mime)
132+
if w and h:
133+
markdown = f'<img width="{w}" height="{h}" alt="image" src="{url}" />'
134+
else:
135+
markdown = f"![image]({url})"
136+
elif mime.startswith("video/"):
137+
markdown = url
138+
else:
139+
markdown = f"![image]({url})"
140+
141+
print(markdown)
142+
subprocess.run(["wl-copy"], input=markdown.encode())
143+
print("(copied to clipboard)", file=sys.stderr)
144+
145+
146+
if __name__ == "__main__":
147+
main()

0 commit comments

Comments
 (0)