← Writings

Rebuilding scrt for Hyprland

How I rebuilt and overengineered my tiny scrot screenshot wrapper for Hyprland using grim, slurp, wl-copy, and a little hyprctl glue.

One of my most used tools is a tiny script called scrt. It takes a screenshot and puts it in clipboard. That’s it. I use it all-the-dang-time. When I moved from AwesomeWM to Hyprland, it stopped working.

The old version was just a small wrapper around scrot and xclip.

scrot -s '/tmp/scrt_temp_%Y%m%d%H%M%S.png' \
    -e 'xclip -selection clipboard -t image/png -i "$f" && rm "$f"'

Under X11, scrot -s had exactly the behaviour I wanted:

  • Click a window and it captures the whole window.
  • Drag a rectangle and it captures that region.
  • Copy the result to the clipboard.

That is the whole workflow. No save dialog, no image editor, no naming files, no remembering where screenshots went. I can then just paste the screenshot into slack or (dog forbid) an email.

TL;DR: Wayland made the old script stop being the right script. The replacement is an overengineered vibecoded combo of grim, slurp, wl-copy, and a bit of hyprctl/jq to get the old click-a-window behaviour back.

The Script

The script lives in my dotfiles here: bin/bin/scrt.

#!/bin/bash

# scrt - Take a screenshot selection and copy it to the clipboard

set -euo pipefail

if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
    for cmd in grim hyprctl jq slurp wl-copy; do
        if ! command -v "$cmd" >/dev/null 2>&1; then
            echo "Error: '$cmd' is not installed."
            exit 1
        fi
    done

selection_file="$(mktemp -t scrt-selection.XXXXXX)"
trap 'rm -f "$selection_file"' EXIT

while true; do
    workspace_id="$(hyprctl activeworkspace -j | jq -r '.id')"
    windows="$(hyprctl clients -j | jq -r --argjson workspace_id "$workspace_id" '
        .[]
        | select(.mapped and .workspace.id == $workspace_id)
        | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1]) \(.title)"
    ')"

    refresh_selection=false
    slurp > "$selection_file" <<< "$windows" &
    slurp_pid="$!"

    while kill -0 "$slurp_pid" >/dev/null 2>&1; do
        current_workspace_id="$(hyprctl activeworkspace -j | jq -r '.id')"
        if [ "$workspace_id" != "$current_workspace_id" ]; then
            refresh_selection=true
            kill "$slurp_pid" >/dev/null 2>&1 || true
            wait "$slurp_pid" >/dev/null 2>&1 || true
            break
        fi
        sleep 0.1
    done

    if [ "$refresh_selection" = true ]; then
        continue
    fi

    wait "$slurp_pid" || exit 1
    geometry="$(cat "$selection_file")"
    [ -n "$geometry" ] && break
done

grim -g "$geometry" - | wl-copy --type image/png
else
    for cmd in scrot xclip; do
        if ! command -v "$cmd" >/dev/null 2>&1; then
            echo "Error: '$cmd' is not installed."
            exit 1
        fi
    done

    scrot -s '/tmp/scrt_temp_%Y%m%d%H%M%S.png' -e 'xclip -selection clipboard -t image/png -i "$f" && rm "$f"'
fi

echo "Screenshot copied to clipboard!"

How It Works

On Wayland, the script uses hyprctl clients -j to get the windows on the current workspace. jq turns that JSON into rectangle lines that slurp understands, so I can click a window to capture the whole thing or drag a custom region.

slurp only knows about the rectangles it was given when it started. To handle workspace changes, the script watches hyprctl activeworkspace -j while slurp is running. If the workspace changes, it kills slurp and starts it again with the new workspace’s windows. Once a selection is made, grim captures that geometry and pipes the PNG into wl-copy. I’m pretty sure my AwesomeWM version just exited when changing tags, so this is a nice improvement.

Bonus: on X11, it falls back to the old scrot and xclip version.

The End

Long live scrt.