Skip to content

Commit a043eab

Browse files
committed
screenshot: simplify capture workflow
1 parent 66d67a3 commit a043eab

5 files changed

Lines changed: 240 additions & 81 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
screenshot_raw.png
2+
Session.vim

screenshot.png

-12.5 KB
Loading

scripts/assets/flume_glitch_bg.jpg

871 KB
Loading

scripts/compose_header.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from pathlib import Path
2+
3+
from PIL import Image, ImageDraw, ImageFilter
4+
5+
ROOT_DIR = Path(__file__).resolve().parent.parent
6+
BACKGROUND_SOURCE = ROOT_DIR / "scripts" / "assets" / "flume_glitch_bg.jpg"
7+
RAW_SCREENSHOT = ROOT_DIR / "screenshot_raw.png"
8+
BACKGROUND_OUTPUT = ROOT_DIR / "background.png"
9+
SCREENSHOT_OUTPUT = ROOT_DIR / "screenshot.png"
10+
11+
MAX_WINDOW_WIDTH = 0.88
12+
MAX_WINDOW_HEIGHT = 0.86
13+
CORNER_RADIUS = 16
14+
SHADOW_PADDING = 10
15+
SHADOW_OFFSET_Y = 2
16+
SHADOW_COLOR = (12, 10, 18, 30)
17+
18+
19+
def crop_to_opaque_bounds(image):
20+
alpha = image.split()[-1]
21+
opaque_mask = alpha.point(lambda p: 255 if p == 255 else 0)
22+
bounds = opaque_mask.getbbox()
23+
return image.crop(bounds) if bounds else image
24+
25+
26+
def fit_inside(source_size, target_size):
27+
source_width, source_height = source_size
28+
max_width, max_height = target_size
29+
source_ratio = source_width / source_height
30+
31+
width = max_width
32+
height = int(width / source_ratio)
33+
34+
if height > max_height:
35+
height = max_height
36+
width = int(height * source_ratio)
37+
38+
return width, height
39+
40+
41+
def round_corners(image, radius):
42+
mask = Image.new("L", image.size, 0)
43+
draw = ImageDraw.Draw(mask)
44+
draw.rounded_rectangle((0, 0, image.width, image.height), radius, fill=255)
45+
46+
rounded = Image.new("RGBA", image.size)
47+
rounded.paste(image, (0, 0), mask=mask)
48+
return rounded
49+
50+
51+
def make_shadow(size, radius):
52+
width, height = size
53+
shadow = Image.new(
54+
"RGBA",
55+
(width + SHADOW_PADDING * 2, height + SHADOW_PADDING * 2),
56+
(0, 0, 0, 0),
57+
)
58+
draw = ImageDraw.Draw(shadow)
59+
draw.rounded_rectangle(
60+
(SHADOW_PADDING, SHADOW_PADDING, SHADOW_PADDING + width, SHADOW_PADDING + height),
61+
radius,
62+
fill=SHADOW_COLOR,
63+
)
64+
return shadow.filter(ImageFilter.GaussianBlur(2))
65+
66+
67+
def compose_header(background, window):
68+
max_window_size = (
69+
int(background.width * MAX_WINDOW_WIDTH),
70+
int(background.height * MAX_WINDOW_HEIGHT),
71+
)
72+
window_size = fit_inside(window.size, max_window_size)
73+
window = window.resize(window_size, Image.Resampling.LANCZOS)
74+
window = round_corners(window, CORNER_RADIUS)
75+
shadow = make_shadow(window_size, CORNER_RADIUS)
76+
77+
window_x = (background.width - window.width) // 2
78+
window_y = (background.height - window.height) // 2
79+
shadow_x = window_x - SHADOW_PADDING
80+
shadow_y = window_y - SHADOW_PADDING + SHADOW_OFFSET_Y
81+
82+
background.alpha_composite(shadow, (shadow_x, shadow_y))
83+
background.alpha_composite(window, (window_x, window_y))
84+
return background
85+
86+
87+
def main():
88+
if not RAW_SCREENSHOT.exists():
89+
raise SystemExit(f"Missing {RAW_SCREENSHOT}. Run scripts/screenshot-window.sh first.")
90+
91+
background = Image.open(BACKGROUND_SOURCE).convert("RGBA")
92+
window = Image.open(RAW_SCREENSHOT).convert("RGBA")
93+
window = crop_to_opaque_bounds(window)
94+
95+
background.convert("RGB").save(BACKGROUND_OUTPUT, "PNG", optimize=True)
96+
compose_header(background, window).convert("RGB").save(SCREENSHOT_OUTPUT, "PNG", optimize=True)
97+
98+
print(f"Saved {SCREENSHOT_OUTPUT}")
99+
100+
101+
if __name__ == "__main__":
102+
main()

scripts/screenshot-window.sh

Lines changed: 136 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,143 @@
11
#!/bin/bash
2+
set -euo pipefail
23

3-
# Script to automatically spawn Ghostty with the Flume theme,
4-
# open Neovim with the sample file, position the cursor,
5-
# and take a screenshot of the window.
6-
7-
# Ensure we are in the repository root
84
cd "$(dirname "$0")/.."
95

10-
# Check if Ghostty is installed
11-
if ! osascript -e 'id of application "Ghostty"' &>/dev/null; then
12-
echo "Error: Ghostty is not installed or not in Applications."
6+
GHOSTTY_COLUMNS=102
7+
GHOSTTY_ROWS=26
8+
GHOSTTY_FONT_SIZE=19
9+
GHOSTTY_PID=""
10+
INPUT_FILE=""
11+
12+
fail() {
13+
echo "Error: $*" >&2
1314
exit 1
14-
fi
15-
16-
# Resolve absolute paths
17-
NVIM_PATH=$(command -v nvim)
18-
if [ -z "$NVIM_PATH" ]; then
19-
NVIM_PATH="nvim"
20-
fi
21-
22-
EXAMPLE_DIR="$(pwd)/examples"
23-
FILE_NAME="flume.zig"
24-
WORKING_DIR="$EXAMPLE_DIR"
25-
26-
# Ghostty is launched via macOS `open`, so the terminal process may not inherit
27-
# the PATH from the shell that ran this script. Capture the user's login-shell
28-
# PATH and pass it explicitly so Neovim can find tools such as zig and zls.
29-
TERMINAL_PATH=$(/bin/zsh -lc 'print -r -- "$PATH"')
30-
if [ -z "$TERMINAL_PATH" ]; then
31-
TERMINAL_PATH="$PATH"
32-
fi
33-
34-
# Avoid Ghostty's macOS "Allow Ghostty to execute ...?" prompt by not using
35-
# -e/--command. Instead, start the normal login shell and send the nvim command
36-
# as startup input. Leave the temp file in /tmp long enough for Ghostty to read it.
37-
INPUT_FILE=$(mktemp "${TMPDIR:-/tmp}/flume-screenshot-input.XXXXXX")
38-
printf '%q %q %q %q %q\n' \
39-
"$NVIM_PATH" \
40-
"+18" \
41-
"+normal! w" \
42-
"+lua vim.defer_fn(function() vim.o.autochdir = false; pcall(vim.cmd, 'lcd %:p:h'); pcall(vim.cmd, 'Gitsigns detach') end, 500)" \
43-
"$FILE_NAME" >"$INPUT_FILE"
44-
45-
echo "Opening Ghostty with the Flume theme and screenshot settings..."
46-
open -na Ghostty.app --args \
47-
--window-save-state=never \
48-
--quit-after-last-window-closed=true \
49-
--theme=flume \
50-
--font-size=19 \
51-
--window-width=102 \
52-
--window-height=28 \
53-
--window-padding-x=16 \
54-
--window-padding-y=16 \
55-
--working-directory="$WORKING_DIR" \
56-
--env="PATH=$TERMINAL_PATH" \
57-
--input="path:$INPUT_FILE"
58-
59-
# Give Ghostty and Neovim time to load LSP/Tree-sitter and draw
15+
}
16+
17+
cleanup() {
18+
if [ -n "$GHOSTTY_PID" ] && kill -0 "$GHOSTTY_PID" &>/dev/null; then
19+
echo "Closing Ghostty..."
20+
kill "$GHOSTTY_PID" &>/dev/null || true
21+
fi
22+
23+
if [ -n "$INPUT_FILE" ]; then
24+
rm -f "$INPUT_FILE"
25+
fi
26+
}
27+
28+
screen_recording_allowed() {
29+
local probe
30+
probe=$(mktemp "${TMPDIR:-/tmp}/flume-screenshot-probe.XXXXXX.png")
31+
32+
if screencapture -x -R 0,0,1,1 "$probe" &>/dev/null && [ -s "$probe" ]; then
33+
rm -f "$probe"
34+
return 0
35+
fi
36+
37+
rm -f "$probe"
38+
return 1
39+
}
40+
41+
require_screen_recording() {
42+
if screen_recording_allowed; then
43+
return
44+
fi
45+
46+
echo "macOS denied Screen Recording permission for this terminal."
47+
echo "Grant permission, quit/reopen the terminal, then retry."
48+
echo ""
49+
echo "System Settings -> Privacy & Security -> Screen Recording"
50+
echo ""
51+
echo "sudo/password cannot grant this macOS privacy permission."
52+
open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture" &>/dev/null || true
53+
exit 1
54+
}
55+
56+
capture_ghostty_window() {
57+
local err_file
58+
err_file=$(mktemp "${TMPDIR:-/tmp}/flume-screenshot-error.XXXXXX")
59+
60+
echo ""
61+
echo "=========================================================="
62+
echo " ACTION REQUIRED: click the Ghostty window with the camera."
63+
echo "=========================================================="
64+
echo ""
65+
66+
if screencapture -o -w screenshot_raw.png 2>"$err_file" && [ -s screenshot_raw.png ]; then
67+
rm -f "$err_file"
68+
return 0
69+
fi
70+
71+
cat "$err_file" >&2
72+
rm -f "$err_file" screenshot_raw.png
73+
74+
echo "Retrying with screen-backed window capture. Click Ghostty again."
75+
screencapture -i -W -S -o screenshot_raw.png && [ -s screenshot_raw.png ]
76+
}
77+
78+
launch_ghostty() {
79+
local nvim_bin terminal_path example_dir input_cmd ghostty_app ghostty_bin
80+
81+
nvim_bin=$(command -v nvim || true)
82+
if [ -z "$nvim_bin" ]; then
83+
nvim_bin="nvim"
84+
fi
85+
86+
terminal_path=$(/bin/zsh -lc 'print -r -- "$PATH"')
87+
if [ -z "$terminal_path" ]; then
88+
terminal_path="$PATH"
89+
fi
90+
91+
example_dir="$(pwd)/examples"
92+
INPUT_FILE=$(mktemp "${TMPDIR:-/tmp}/flume-screenshot-input.XXXXXX")
93+
input_cmd="+lua vim.defer_fn(function() vim.o.autochdir = false; pcall(vim.cmd, 'lcd %:p:h'); pcall(vim.cmd, 'Gitsigns detach') end, 500)"
94+
95+
printf '%q %q %q %q %q\n' \
96+
"$nvim_bin" \
97+
"+18" \
98+
"+normal! w" \
99+
"$input_cmd" \
100+
"flume.zig" >"$INPUT_FILE"
101+
102+
ghostty_app=$(osascript -e 'POSIX path of (path to application "Ghostty")')
103+
ghostty_bin="${ghostty_app}Contents/MacOS/ghostty"
104+
105+
echo "Opening Ghostty with the Flume theme and screenshot settings..."
106+
"$ghostty_bin" \
107+
--window-save-state=never \
108+
--quit-after-last-window-closed=true \
109+
--theme=flume \
110+
--font-size="$GHOSTTY_FONT_SIZE" \
111+
--window-width="$GHOSTTY_COLUMNS" \
112+
--window-height="$GHOSTTY_ROWS" \
113+
--window-padding-x=16 \
114+
--window-padding-y=16 \
115+
--working-directory="$example_dir" \
116+
--env="PATH=$terminal_path" \
117+
--input="path:$INPUT_FILE" &
118+
GHOSTTY_PID=$!
119+
disown "$GHOSTTY_PID" 2>/dev/null || true
120+
}
121+
122+
trap cleanup EXIT
123+
trap 'exit 130' INT
124+
trap 'exit 143' TERM
125+
126+
osascript -e 'id of application "Ghostty"' &>/dev/null || fail "Ghostty is not installed or not in Applications."
127+
128+
require_screen_recording
129+
launch_ghostty
130+
131+
rm -f screenshot_raw.png
132+
60133
echo "Waiting for window to render..."
61-
sleep 2.5
62-
63-
# Bring Ghostty to front and capture its window ID
64-
WINDOW_ID=$(osascript -e '
65-
tell application "Ghostty"
66-
activate
67-
try
68-
return id of window 1
69-
on error
70-
return ""
71-
end try
72-
end tell')
73-
74-
if [ -n "$WINDOW_ID" ]; then
75-
echo "Capturing Ghostty window ID $WINDOW_ID..."
76-
# Capture only the window, omitting the drop shadow (-o)
77-
screencapture -o -l "$WINDOW_ID" screenshot.png
78-
echo "Screenshot successfully saved to screenshot.png"
79-
80-
# Gracefully close the Ghostty window
81-
osascript -e '
82-
tell application "Ghostty"
83-
close window 1
84-
end tell'
85-
else
86-
echo "Error: Could not retrieve Ghostty window ID. Please take screenshot manually."
87-
fi
134+
sleep 1.0
135+
osascript -e 'tell application "Ghostty" to activate'
136+
137+
capture_ghostty_window || fail "Capture cancelled or failed."
138+
139+
cleanup
140+
trap - EXIT
88141

142+
python3 "$(dirname "$0")/compose_header.py"
143+
echo "Updated screenshot.png"

0 commit comments

Comments
 (0)