From 426fe32a63f0b2e24d6acf652f9d3b99625a17df Mon Sep 17 00:00:00 2001 From: cool-mist Date: Fri, 6 Feb 2026 20:40:57 +0530 Subject: [PATCH] add screenshot --- private_dot_local/bin/executable_grimblast | 395 +++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 private_dot_local/bin/executable_grimblast diff --git a/private_dot_local/bin/executable_grimblast b/private_dot_local/bin/executable_grimblast new file mode 100644 index 0000000..b1d4904 --- /dev/null +++ b/private_dot_local/bin/executable_grimblast @@ -0,0 +1,395 @@ +#!/usr/bin/env bash +## Grimblast: a helper for screenshots within hyprland +## Requirements: +## - `grim`: screenshot utility for wayland +## - `slurp`: to select an area +## - `hyprctl`: to read properties of current window (provided by Hyprland) +## - `hyprpicker`: to freeze the screen when selecting area +## - `wl-copy`: clipboard utility (provided by wl-clipboard) +## - `jq`: json utility to parse hyprctl output +## - `notify-send`: to show notifications (provided by libnotify) +## Those are needed to be installed, if unsure, run `grimblast check` +## +## See `man 1 grimblast` or `grimblast usage` for further details. + +## Author: Misterio (https://github.com/misterio77) + +## This tool is based on grimshot, with swaymsg commands replaced by their +## hyprctl equivalents. +## https://github.com/OctopusET/sway-contrib/blob/master/grimshot/grimshot + +NAME="$(basename "$0")" + +# Check whether another instance is running +GRIMBLASTLOCK="${XDG_RUNTIME_DIR:-${XDG_CACHE_DIR:-$HOME/.cache}}/$NAME.lock" + +killhyprpicker() { + pidof -q hyprpicker && pkill hyprpicker +} + +cleanup() { + rm -f "$GRIMBLASTLOCK" + killhyprpicker +} + +# Entry point +[[ -e $GRIMBLASTLOCK ]] && exit 2 +trap cleanup EXIT +touch "$GRIMBLASTLOCK" + +[[ $HYPRLAND_INSTANCE_SIGNATURE ]] || { + echo "Error: HYPRLAND_INSTANCE_SIGNATURE not set! (is hyprland running?)" >&2 + exit 1 +} + +# Globals +# General settings. These can be set by the user. See man page for more details +[[ $DEFAULT_TARGET_DIR ]] || { + USER_DIRS="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" + [[ -f $USER_DIRS ]] && source "$USER_DIRS" + DEFAULT_TARGET_DIR="${XDG_SCREENSHOTS_DIR:-${XDG_PICTURES_DIR:-$HOME}}" +} +: "${DEFAULT_TMP_EDITOR_DIR:=/tmp}" "${GRIMBLAST_EDITOR:=gimp}" "${DATE_FORMAT:=%Y%m%d_%H%M%S}" + +# Screenshot variables. In addition to these, there's: +# SCALE +EXPIRE_TIME=3000 +FILETYPE=png + +# These have an effect depending on whether they're set +# CURSOR +# FREEZE +NOTIFY=false +SHOW_FILE_NOTIFY=false +WAIT=0 + +# Notification functions +notify() { + notify-send -t "$EXPIRE_TIME" -a "$NAME" "$@" +} + +notify::ok() { + if $NOTIFY; then + notify "$@" + fi +} + +notify::error() { + if $NOTIFY; then + TITLE=${2:-"Screenshot"} + MESSAGE=${1:-"Error taking screenshot with grim"} + notify -u critical "$TITLE" "$MESSAGE" + fi + echo "$1" >&2 +} + +# If invoked with -h, print usage before dying +die() { + killhyprpicker + local msg OPTIND option + while getopts 'h' option; do + case "$option" in + h) usage >&2 ;; + ?) echo "die: Usage: die [-h] MESSAGE" >&2 ;; + esac + done + shift $((OPTIND - 1)) + msg=${1:-Bye} + notify::error "Error: $msg" + exit 2 +} + +notify::showparentdir() { + if $SHOW_FILE_NOTIFY; then + if [[ $(notify::ok -A 'Show file="Show file"' "$@") == "Show file" ]]; then + gdbus call --session --dest org.freedesktop.FileManager1 --object-path /org/freedesktop/FileManager1 --method org.freedesktop.FileManager1.ShowItems "['file://$4']" "" || die "Could not display parent directory with gdbus" + fi + else + notify::ok "$@" + fi +} + +# Miscellaneous functions +grimblast::wait() { + [[ $WAIT == 0 ]] || sleep "$WAIT" +} + +get-mime-type() { + case $FILETYPE in + png | jpeg) echo "image/$FILETYPE" ;; + ppm) echo "image/x-portable-pixmap" ;; + esac +} + +freezescreen() { + hyprpicker -rz & + sleep 0.2 +} + +# Checks whether an individual tool is available +# If invoked with -q (quiet), no output is printed, useful for use in tests +# Exits with 0 if the tool is available, non-zero otherwise +grimblast::check() { + local cmd result OPTIND option status quiet=false + while getopts 'q' option; do + case "$option" in + q) quiet=true ;; + ?) echo 'check: Usage: check [-q] COMMAND' >&2 ;; + esac + done + shift $((OPTIND - 1)) + cmd=$1 + command -v "$cmd" >/dev/null 2>&1 + status=$? + if ((status == 0)); then + result="OK" + else + result="NOT FOUND" + fi + $quiet || echo " $cmd: $result" + return $status +} + +# The actual grim command used is printed to stderr +screenshot() { + local file="$1" geom="$2" output="$3" + xargs --verbose grim <<<"${CURSOR:+-c} ${SCALE:+-s \"$SCALE\"} -t \"$FILETYPE\" ${output:+-o \"$output\"} ${geom:+-g \"$geom\"} \"$file\"" +} + +# Special actions: usage and check. These are special because they allow to exit early +usage() { + cat <] [-c|--cursor] [-f|--freeze] [-w N|--wait N] [-s N|--scale N] [-t TYPE|--filetype TYPE] (copy|save|copysave|edit) [active|screen|output|area] [FILE|-] + $NAME check + $NAME usage + +Commands: + copy: Copy the screenshot data into the clipboard. + save: Save the screenshot to a regular file or '-' to pipe to STDOUT. + copysave: Combine the previous 2 options. + edit: Open screenshot in the image editor of your choice (default is gimp). See man page for info. + check: Verify if required tools are installed and exit. + usage: Show this message and exit. + +Targets: + active: Currently active window. + screen: All visible outputs. + output: Currently active output. + area: Manually select a region or window. +EOF +} + +check() { + local status + echo "Checking if required tools are installed. If something is missing, install it to your system and make it available in PATH..." + for t in grim slurp hyprctl hyprpicker wl-copy jq notify-send; do + grimblast::check "$t" || status=$? + done + exit $status +} + +# Target functions. These calculate global variables depending on the target. +# Specifically: GEOM, WHAT and OUTPUT. Not all would be set by all targets. +# These would later be used by the action functions. +active() { + local focused app_id + grimblast::wait + focused="$(hyprctl activewindow -j)" app_id=$(jq -r '.class' <<<"$focused") + GEOM="$(jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"' <<<"$focused")" + WHAT="$app_id window" +} + +screen() { + grimblast::wait + GEOM="" + WHAT="Screen" +} + +output() { + grimblast::wait + GEOM="" + OUTPUT=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name') + WHAT="$OUTPUT" +} + +area() { + local fullscreen_workspaces workspaces windows + [[ $CURSOR ]] && die "'-c|--cursor' cannot be used with TARGET 'area'" + + if [[ $FREEZE ]] && grimblast::check -q hyprpicker; then + freezescreen + fi + + # disable animation for layer namespace "selection" (slurp) + # this removes the black border seen around screenshots + hyprctl keyword layerrule "match:selection, no_anim on" >/dev/null + + fullscreen_workspaces="$(hyprctl workspaces -j | jq -r 'map(select(.hasfullscreen) | .id)')" + workspaces="$(hyprctl monitors -j | jq -r '[(foreach .[] as $monitor (0; if $monitor.specialWorkspace.name == "" then $monitor.activeWorkspace else $monitor.specialWorkspace end)).id]')" + windows="$(hyprctl clients -j | jq -r --argjson workspaces "$workspaces" --argjson fullscreenWorkspaces "$fullscreen_workspaces" 'map((select(([.workspace.id] | inside($workspaces)) and ([.workspace.id] | inside($fullscreenWorkspaces) | not) or .fullscreen > 0)))')" + # convert SLURP_ARGS to a bash array + IFS=' ' read -ra SLURP_ARGS <<<"$SLURP_ARGS" + GEOM="$(jq -r '.[] | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"' <<<"$windows" | slurp "${SLURP_ARGS[@]}")" + + grimblast::wait + + # Check if user exited slurp without selecting the area + [[ $GEOM ]] || { + killhyprpicker + exit 1 + } + WHAT="Area" +} + +# Action functions. +# These take the global variables set by target functions and take the screenshot. +copy() { + [[ $FILETYPE == "png" ]] || die "Clipboard operations only support PNG format. Use --filetype png or omit the option." + screenshot - "$GEOM" "$OUTPUT" | wl-copy --type "$(get-mime-type)" || die "Clipboard error" + notify::ok "$WHAT copied to buffer" +} + +save() { + local file title message + file="${1:-$DEFAULT_TARGET_DIR/$(date +"$DATE_FORMAT").$FILETYPE}" + screenshot "$file" "$GEOM" "$OUTPUT" || die "Could not take screenshot with grim" + title="Screenshot of $WHAT" message="$(basename "$file")" + killhyprpicker + notify::showparentdir "$title" "$message" -i "$file" + echo "$file" +} + +edit() { + local editor="${GRIMBLAST_EDITOR%% *}" + grimblast::check -q "$editor" || die "$editor is not installed" + local file title message + file="${1:-$DEFAULT_TMP_EDITOR_DIR/$(date +"$DATE_FORMAT").$FILETYPE}" + screenshot "$file" "$GEOM" "$OUTPUT" || die "Could not take screenshot" + title="Screenshot of $WHAT" message="Open screenshot in $editor" + notify::ok "$title" "$message" -i "$file" + $GRIMBLAST_EDITOR "$file" + echo "$file" +} + +copysave() { + [[ $FILETYPE == "png" ]] || die "Clipboard operations only support PNG format. Use --filetype png or omit the option." + local file title message + file="${1:-$DEFAULT_TARGET_DIR/$(date +"$DATE_FORMAT").$FILETYPE}" + if [[ $file = "-" ]]; then + screenshot - "$GEOM" "$OUTPUT" | tee >(wl-copy --type "$(get-mime-type)") || die "Clipboard error" + notify::ok "$WHAT copied to buffer and piped to stdout" + else + screenshot - "$GEOM" "$OUTPUT" | tee "$file" | wl-copy --type "$(get-mime-type)" || die "Clipboard error" + title="Screenshot of $WHAT" + message="$WHAT copied to buffer and saved to $file" + notify::showparentdir "$title" "$message" -i "$file" + echo "$file" + fi +} + +parse-action() { + local action="$1" file="$2" + case "$action" in + copy) copy ;; + save) save "$file" ;; + edit) edit "$file" ;; + copysave) copysave "$file" ;; + *) die -h "Unknown action $action" ;; + esac +} + +parse-target() { + case "$1" in + active) active ;; + screen) screen ;; + output) output ;; + area) area ;; + window) die "$(echo -e "Target 'window' is now included in 'area'.\nSimply run with 'area' and single click over the window you want.")" ;; + *) die -h "Unknown target to take a screen shot from $1" ;; + esac +} + +main() { + local parsed_args + + parsed_args="$(getopt --name "$NAME" --options 'nocfe:w:s:t:' --longoptions 'notify,openparentdir,cursor,freeze,expire-time:,wait:,scale:,filetype:' -- "$@")" || { + usage >&2 + exit 1 + } + eval "set -- $parsed_args" + + while true; do + case $1 in + -n | --notify) + NOTIFY=true + shift + ;; + -o | --openparentdir) + SHOW_FILE_NOTIFY=true + shift + ;; + -e | --expire-time) + [[ $2 =~ ^[0-9]+$ ]] || { + echo "$NAME: ERROR: Invalid or missing argument for '-e|--expire-time'" >&2 + exit 1 + } + EXPIRE_TIME=$2 + shift 2 + ;; + -c | --cursor) + CURSOR=1 + shift + ;; + -f | --freeze) + FREEZE=1 + shift + ;; + -w | --wait) + [[ $2 =~ ^[0-9]*(\.[0-9]+)?$ ]] || { + echo "$NAME: ERROR: Invalid value for '-w|--wait'" >&2 + exit 1 + } + WAIT=$2 + shift 2 + ;; + -s | --scale) + [[ "$2" =~ ^[0-9]*(\.[0-9]+)?$ ]] || { + echo "$NAME: ERROR: Invalid or missing argument for '-s|--scale'" >&2 + exit 1 + } + SCALE=$2 + shift 2 + ;; + -t | --filetype) + [[ "$2" =~ ^(png|ppm|jpeg)$ ]] || { + echo "$NAME: ERROR: Invalid filetype '$2'. Must be png, ppm, or jpeg" >&2 + exit 1 + } + FILETYPE=$2 + shift 2 + ;; + --) + shift + break + ;; + *) + echo "$NAME: ERROR: Invalid option: $1" >&2 + usage >&2 + exit 1 + ;; + esac + done + + ACTION="${1:-usage}" + if [[ $1 =~ ^(usage|check)$ ]]; then + "$1" + exit + fi + shift + parse-target "${1:-screen}" + shift + parse-action "$ACTION" "$@" +} + +main "$@"