#!/bin/sh # sudo pacman -S --needed sway grim slurp wf-recorder wl-clipboard jq ffmpeg libnotify libpulse procps-ng python-gobject gtk3 gtk-layer-shell [ -z "$WAYLAND_DISPLAY" ] && echo "ERROR: Wayland session required" && exit 1 # screenshot directory screenshot_directory="$HOME/Pictures/screenshots" videos_directory="$HOME/Videos" audio_directory="$HOME/Music" mkdir -p "$screenshot_directory" mkdir -p "$videos_directory" mkdir -p "$audio_directory" filename=$(date '+%Y-%m-%d_%H-%M-%S_pc') #2025-08-10_18-53-58 # h264_nvenc libx264rgb VIDEO_CODEC="libx264rgb" FRAMERATE=60 BORDER_WIDTH=2 BORDER_COLOR="#7F8C8D" desktop="$(pactl get-default-sink).monitor" output="$(swaymsg -t get_outputs | jq -r '.[] | select(.focused) | .name')" window_boxes(){ swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | "\(.rect.x + .window_rect.x),\(.rect.y + .window_rect.y) \(.window_rect.width)x\(.window_rect.height)"' } slurp(){ window_boxes | command slurp -c "#FFFFFFFF" -d "$@" } select_area(){ python3 - "$BORDER_WIDTH" "$BORDER_COLOR" "$(window_boxes)" << 'EOF' import subprocess import sys import threading import gi gi.require_version("Gtk", "3.0") gi.require_version("GtkLayerShell", "0.1") gi.require_foreign("cairo") import cairo from gi.repository import Gtk, Gdk, GLib, GtkLayerShell width = int(sys.argv[1]) color = Gdk.RGBA() color.parse(sys.argv[2]) boxes = [] for line in sys.argv[3].splitlines(): try: position, size = line.split(" ") box_x, box_y = map(int, position.split(",")) box_w, box_h = map(int, size.split("x")) boxes.append((box_x, box_y, box_w, box_h)) except ValueError: pass state = {"start": None, "dragged": False, "selection": None, "size": None, "pending": None, "result": None} condition = threading.Condition() windows = [] def redraw(): for window in windows: window.queue_draw() def box_at(px, py): found = [b for b in boxes if b[0] <= px < b[0] + b[2] and b[1] <= py < b[1] + b[3]] return min(found, key=lambda b: b[2] * b[3]) if found else None def set_selection(selection): if selection == state["selection"]: return state["selection"] = selection state["size"] = None if selection: with condition: state["pending"] = selection condition.notify() redraw() def apply_size(selection, size): if selection == state["selection"]: state["size"] = size redraw() return False def measure(): while True: with condition: while state["pending"] is None: condition.wait() selection = state["pending"] state["pending"] = None x, y, w, h = selection size = 0 if w > 0 and h > 0: try: size = len(subprocess.run(["grim", "-g", f"{x},{y} {w}x{h}", "-"], capture_output=True, check=True).stdout) except (subprocess.CalledProcessError, OSError): size = None GLib.idle_add(apply_size, selection, size) def format_size(size): if size is None: return "..." if size >= 1000000000: decimal = f"{size / 1000000000:.2f} GB" elif size >= 1000000: decimal = f"{size / 1000000:.1f} MB" else: decimal = f"{size / 1000:.1f} KB" if size >= 1073741824: binary = f"{size / 1073741824:.2f} GiB" elif size >= 1048576: binary = f"{size / 1048576:.1f} MiB" else: binary = f"{size / 1024:.1f} KiB" return f"{decimal} ({binary})" def draw(widget, cr, geometry): cr.set_operator(cairo.OPERATOR_SOURCE) cr.set_source_rgba(0, 0, 0, 0) cr.paint() cr.set_operator(cairo.OPERATOR_OVER) selection = state["selection"] cr.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) cr.set_source_rgba(0, 0, 0, 0.25) cr.rectangle(0, 0, geometry.width, geometry.height) if selection: cr.rectangle(selection[0] - geometry.x, selection[1] - geometry.y, selection[2], selection[3]) cr.fill() if not selection: return True x, y, w, h = selection x -= geometry.x y -= geometry.y Gdk.cairo_set_source_rgba(cr, color) cr.set_line_width(width) cr.rectangle(x - width / 2, y - width / 2, w + width, h + width) cr.stroke() if not (0 <= x + w - 1 < geometry.width and 0 <= y + h - 1 < geometry.height): return True text = f"{w}x{h} {format_size(state['size'])}" cr.select_font_face("monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) cr.set_font_size(14) ascent, descent = cr.font_extents()[:2] padding = 6 box_width = cr.text_extents(text).x_advance + padding * 2 box_height = ascent + descent + padding * 2 box_x = min(max(x + w + width - box_width, 0), geometry.width - box_width) box_y = y + h + width + 4 if box_y + box_height > geometry.height: box_y = max(y - width - 4 - box_height, 0) cr.set_source_rgba(0, 0, 0, 0.6) cr.rectangle(box_x, box_y, box_width, box_height) cr.fill() cr.set_source_rgba(1, 1, 1, 1) cr.move_to(box_x + padding, box_y + padding + ascent) cr.show_text(text) return True def finish(): selection = state["selection"] state["start"] = None if not selection or selection[2] <= 0 or selection[3] <= 0: return state["result"] = selection Gtk.main_quit() def press(widget, event, geometry): if event.button == 3: Gtk.main_quit() return True if event.button != 1: return True state["start"] = (int(event.x) + geometry.x, int(event.y) + geometry.y) state["dragged"] = False return True def motion(widget, event, geometry): px = int(event.x) + geometry.x py = int(event.y) + geometry.y if state["start"]: sx, sy = state["start"] if state["dragged"] or abs(px - sx) > 2 or abs(py - sy) > 2: state["dragged"] = True set_selection((min(sx, px), min(sy, py), abs(px - sx), abs(py - sy))) else: set_selection(box_at(px, py)) return True def release(widget, event, geometry): if event.button != 1 or not state["start"]: return True if not state["dragged"]: set_selection(box_at(int(event.x) + geometry.x, int(event.y) + geometry.y)) finish() return True def key(widget, event): if event.keyval == Gdk.KEY_Escape: Gtk.main_quit() return True threading.Thread(target=measure, daemon=True).start() display = Gdk.Display.get_default() for index in range(display.get_n_monitors()): monitor = display.get_monitor(index) geometry = monitor.get_geometry() window = Gtk.Window() GtkLayerShell.init_for_window(window) GtkLayerShell.set_namespace(window, "region-select") GtkLayerShell.set_layer(window, GtkLayerShell.Layer.OVERLAY) GtkLayerShell.set_monitor(window, monitor) GtkLayerShell.set_exclusive_zone(window, -1) for edge in (GtkLayerShell.Edge.TOP, GtkLayerShell.Edge.BOTTOM, GtkLayerShell.Edge.LEFT, GtkLayerShell.Edge.RIGHT): GtkLayerShell.set_anchor(window, edge, True) try: GtkLayerShell.set_keyboard_mode(window, GtkLayerShell.KeyboardMode.EXCLUSIVE) except AttributeError: GtkLayerShell.set_keyboard_interactivity(window, True) window.set_app_paintable(True) window.set_visual(window.get_screen().get_rgba_visual()) window.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.KEY_PRESS_MASK) window.connect("draw", draw, geometry) window.connect("button-press-event", press, geometry) window.connect("motion-notify-event", motion, geometry) window.connect("button-release-event", release, geometry) window.connect("key-press-event", key) window.connect("destroy", Gtk.main_quit) window.show_all() window.get_window().set_cursor(Gdk.Cursor.new_from_name(display, "crosshair")) windows.append(window) Gtk.main() for window in windows: window.destroy() display.flush() if not state["result"]: sys.exit(1) x, y, w, h = state["result"] print(f"{x},{y} {w}x{h}") EOF } show_border(){ python3 - "$1" "$BORDER_WIDTH" "$BORDER_COLOR" "$2" << 'EOF' & import os import sys import time import gi gi.require_version("Gtk", "3.0") gi.require_version("GtkLayerShell", "0.1") gi.require_foreign("cairo") import cairo from gi.repository import Gtk, Gdk, GLib, GtkLayerShell position, size = sys.argv[1].split(" ") x, y = map(int, position.split(",")) w, h = map(int, size.split("x")) width = int(sys.argv[2]) color = Gdk.RGBA() color.parse(sys.argv[3]) path = sys.argv[4] start = time.monotonic() monitor = Gdk.Display.get_default().get_monitor_at_point(x, y) geometry = monitor.get_geometry() x -= geometry.x y -= geometry.y window = Gtk.Window() GtkLayerShell.init_for_window(window) GtkLayerShell.set_namespace(window, "region-border") GtkLayerShell.set_layer(window, GtkLayerShell.Layer.OVERLAY) GtkLayerShell.set_monitor(window, monitor) GtkLayerShell.set_exclusive_zone(window, -1) for edge in (GtkLayerShell.Edge.TOP, GtkLayerShell.Edge.BOTTOM, GtkLayerShell.Edge.LEFT, GtkLayerShell.Edge.RIGHT): GtkLayerShell.set_anchor(window, edge, True) window.set_app_paintable(True) window.set_visual(window.get_screen().get_rgba_visual()) def draw(widget, cr): cr.set_operator(cairo.OPERATOR_SOURCE) cr.set_source_rgba(0, 0, 0, 0) cr.paint() cr.set_operator(cairo.OPERATOR_OVER) Gdk.cairo_set_source_rgba(cr, color) cr.set_line_width(width) cr.rectangle(x - width / 2, y - width / 2, w + width, h + width) cr.stroke() try: size_bytes = os.path.getsize(path) except OSError: size_bytes = 0 if size_bytes >= 1000000000: decimal = f"{size_bytes / 1000000000:.2f} GB" elif size_bytes >= 1000000: decimal = f"{size_bytes / 1000000:.1f} MB" else: decimal = f"{size_bytes / 1000:.1f} KB" if size_bytes >= 1073741824: binary = f"{size_bytes / 1073741824:.2f} GiB" elif size_bytes >= 1048576: binary = f"{size_bytes / 1048576:.1f} MiB" else: binary = f"{size_bytes / 1024:.1f} KiB" elapsed = int(time.monotonic() - start) hours, rest = divmod(elapsed, 3600) minutes, seconds = divmod(rest, 60) timer = f"{hours}:{minutes:02}:{seconds:02}" if hours else f"{minutes:02}:{seconds:02}" text = f"{w}x{h} {decimal} ({binary}) {timer}" cr.select_font_face("monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) cr.set_font_size(14) ascent, descent = cr.font_extents()[:2] padding = 6 box_width = cr.text_extents(text).x_advance + padding * 2 box_height = ascent + descent + padding * 2 box_x = min(max(x + w + width - box_width, 0), geometry.width - box_width) box_y = y + h + width + 4 if box_y + box_height > geometry.height: box_y = max(y - width - 4 - box_height, 0) cr.set_source_rgba(0, 0, 0, 0.6) cr.rectangle(box_x, box_y, box_width, box_height) cr.fill() cr.set_source_rgba(1, 1, 1, 1) cr.move_to(box_x + padding, box_y + padding + ascent) cr.show_text(text) return True def tick(): window.queue_draw() return True window.connect("draw", draw) window.connect("destroy", Gtk.main_quit) window.show_all() window.input_shape_combine_region(cairo.Region()) GLib.timeout_add(500, tick) Gtk.main() EOF border_pid=$! } screenshot_selected_area(){ geometry="$(select_area)" || exit 1 grim -g "$geometry" "$screenshot_directory/$filename.png" wl-copy < "$screenshot_directory/$filename.png" notify-send -i "$screenshot_directory/$filename.png" "Screenshot" "Area screenshot taken" } screenshot_full_screen(){ grim "$screenshot_directory/$filename.png" wl-copy < "$screenshot_directory/$filename.png" notify-send -i "$screenshot_directory/$filename.png" "Screenshot" "Full screen screenshot taken" } video_selected_area(){ geometry="$(slurp)" || exit 1 show_border "$geometry" "$videos_directory/$filename.mp4" wf-recorder -g "$geometry" -c "$VIDEO_CODEC" -r "$FRAMERATE" -f "$videos_directory/$filename.mp4" kill "$border_pid" grim -g "$geometry" -t png "/tmp/$filename.png" notify-send -i "/tmp/$filename.png" "Video" "Area video taken" } video_full_screen(){ wf-recorder -o "$output" -c "$VIDEO_CODEC" -r "$FRAMERATE" -f "$videos_directory/$filename.mp4" ffmpeg -i "$videos_directory/$filename.mp4" -ss 00:00:01.000 -vframes 1 "/tmp/$filename.png" 2>/dev/null notify-send -i "/tmp/$filename.png" "Video" "Full screen video taken" } video_selected_area_audio(){ geometry="$(slurp)" || exit 1 show_border "$geometry" "$videos_directory/$filename.mp4" wf-recorder -g "$geometry" -c "$VIDEO_CODEC" -r "$FRAMERATE" --audio="$desktop" -f "$videos_directory/$filename.mp4" kill "$border_pid" grim -g "$geometry" -t png "/tmp/$filename.png" notify-send -i "/tmp/$filename.png" "Video" "Area video with desktop audio taken" } video_full_screen_audio(){ wf-recorder -o "$output" -c "$VIDEO_CODEC" -r "$FRAMERATE" --audio="$desktop" -f "$videos_directory/$filename.mp4" ffmpeg -i "$videos_directory/$filename.mp4" -ss 00:00:01.000 -vframes 1 "/tmp/$filename.png" 2>/dev/null notify-send -i "/tmp/$filename.png" "Video" "Full screen video with desktop audio taken" } stop(){ pkill -fxn '(/\S+)*ffmpeg\s.*\spulse\s.*' pkill -fxn '(/\S+)*wf-recorder\s.*' pkill -fxn '(/\S+)*pw-record\s.*' exit 1 } help(){ cat << EOF -h | -help | --help -s | -stop -sa | -screenshot_selected_area -sf | -screenshot_full_screen -va | -video_selected_area -vf | -video_full_screen -vaad | -video_selected_area_audio -vfad | -video_full_screen_audio EOF } main(){ while [ "$1" != "" ]; do case "$1" in -h|-help|--help) help exit 0 ;; -s|-stop) stop ;; -sa|-screenshot_selected_area) screenshot_selected_area ;; -sf|-screenshot_full_screen) screenshot_full_screen ;; -va|-video_selected_area) video_selected_area ;; -vf|-video_full_screen) video_full_screen ;; -vaad|-video_selected_area_audio) video_selected_area_audio ;; -vfad|-video_full_screen_audio) video_full_screen_audio ;; *) echo "ERROR: unknown parameter $1" help exit 1 ;; esac shift done } main "$@"