Skip to main content

max / alloy

4.2 KB · 92 lines History Blame Raw
1 #!/bin/sh
2 # alloy-shot — take a screenshot and say that it worked.
3 #
4 # The four Print binds in the sway config used to be four inline grim calls.
5 # They worked, and they were reported as broken anyway, on 2026-07-29, by the
6 # person who wrote them: pressing Print produced a correct capture and no
7 # notification, no OSD, no shutter, no window. Nothing at all. The only way to
8 # learn it had worked was to go and look in the directory, which is
9 # indistinguishable from a dead keybind. So this exists for the notification
10 # more than for the capture.
11 #
12 # Three things moved in here that were wrong or fragile inline, and are the
13 # reason this is a script rather than `&& notify-send` appended four times:
14 #
15 # 1. The timestamp is taken ONCE. The config's `set $shot` held a literal
16 # `$(date ...)`, expanded by the shell sway's `exec` runs it through. An
17 # appended notification naming `$shot` would have expanded it a second time,
18 # so any capture that straddled a second boundary would have announced a
19 # filename that does not exist. A notification that lies about the path is
20 # worse than no notification, and it would have been intermittent.
21 # 2. Cancelling a region select is not a failure. `slurp` exits non-zero when
22 # Escape is pressed, which inline made `grim -g ""` run and fail; with a
23 # notification appended it would have reported an error for a deliberate
24 # cancel. Here it exits quietly.
25 # 3. One notification, one wording, one place to change it.
26 #
27 # Modes rather than four scripts because the notification and the timestamp are
28 # the shared part, which is exactly what was missing.
29
30 set -eu
31
32 # XDG_PICTURES_DIR is what xdg-user-dirs writes and what a localized install
33 # actually uses; ~/Pictures is the fallback for a session that has no user-dirs
34 # file yet. The sway config also `mkdir -p`s this at startup, and it is repeated
35 # here because this script is reachable without that having run: from a bind on
36 # a hand-edited config, from a shell, or on a first login where the exec order
37 # is not something to depend on.
38 dir="${XDG_PICTURES_DIR:-$HOME/Pictures}/Screenshots"
39 mkdir -p "$dir"
40
41 shot="$dir/screenshot-$(date +%Y%m%d-%H%M%S).png"
42
43 # Told, not guessed: a bind that names a mode this does not know is a typo in
44 # the config, and it should say so rather than silently capture the whole
45 # screen.
46 mode="${1:-}"
47 case "$mode" in
48 output)
49 grim "$shot"
50 ;;
51 region)
52 # `|| exit 0` covers the Escape case above. It also swallows a real
53 # slurp failure, which is the accepted cost: from here the two are the
54 # same non-zero exit, and treating a cancel as an error is the louder
55 # mistake.
56 geometry="$(slurp)" || exit 0
57 grim -g "$geometry" "$shot"
58 ;;
59 window)
60 # The focused node's rect, in grim's `x,y WxH` geometry. jq is a stated
61 # dependency of the image for exactly this line; see the Containerfile's
62 # package list.
63 geometry="$(swaymsg -t get_tree \
64 | jq -r '.. | select(.focused?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"')"
65 grim -g "$geometry" "$shot"
66 ;;
67 annotate)
68 # Annotate the most recent capture rather than taking a new one, which
69 # is the bind's whole point: shoot first, mark it up after. Nothing to
70 # notify about, because satty opens a window and is its own feedback.
71 #
72 # `ls -t` on a glob that matches nothing exits non-zero under `set -e`,
73 # so an empty directory ends here instead of running satty on an empty
74 # filename.
75 latest="$(ls -t "$dir"/*.png 2>/dev/null | head -1)" || exit 0
76 [ -n "$latest" ] || exit 0
77 exec satty --filename "$latest"
78 ;;
79 *)
80 echo "alloy-shot: expected one of output, region, window, annotate" >&2
81 exit 2
82 ;;
83 esac
84
85 # Past here the capture is on disk. The notification is the point of this
86 # script, so a notification daemon that is not running is worth one line on
87 # stderr rather than a failed exit: the screenshot was taken either way, and
88 # `set -e` would otherwise turn a missing daemon into what looks like a failed
89 # capture.
90 notify-send -a alloy -i camera-photo "Screenshot saved" "$shot" \
91 || echo "alloy-shot: saved $shot, but the notification could not be sent" >&2
92