#!/usr/bin/env python3 # The all-in-one solution to taking and modifying screenshots on sWayland # # Additional dependencies: # - Python's pillow library # - pip install --user pillow-avif-plugin # For avif support (recommended) # - grim # - swappy import argparse, os, time, re, shutil from subprocess import run, Popen, PIPE, DEVNULL from PIL import Image from PIL.ImageFilter import GaussianBlur from pathlib import Path from typing import * DIR = Path(f"{os.environ['HOME']}/Desktop/screenshots_tmp") # Default # Returns the path of the latest screenshot def get_latest_sceenshot_path() -> Path: pics = [f for f in os.listdir(DIR) if os.path.isfile(DIR / f)] pngs = [p for p in pics if re.fullmatch("[0-9_]+\.png", p)] pngs.sort() return Path(pngs[-1]) return pngs[-1] # Returns a path to an unused screenshot in DIR def get_sceenshot_path() -> Path: t = time.time() p = DIR / f"{round(t)}.png" if os.path.isfile(p): return DIR / f"{str(t).replace('.', '_')}.png" else: return p # Copies the image to the wayland clipboard def copy_to_clipboard(pic: Path): with open(pic, 'r') as img: run(["wl-copy"], stdin=img, timeout=4) # Interactively gets user to select an area of the screen # String matches regex /[0-9]+,[0-9]+ [0-9]+x[0-9]+/ # Example: 287,526 474x369 def select_area() -> str: slurp = run(["slurp"], text=True, stdout=PIPE) slurp.check_returncode() return slurp.stdout.strip() # Adds drop-shadow to an image. If `img` and `save` are the same path, the image # is overwritten def add_drop_shadow(img: Path, save: Path, back_color: str, shadow_color: str): with Image.open(img) as img: mode = img.mode border_width = max(img.size) // 20 # 5% of larger dim width = img.size[0] + border_width*2 height = img.size[1] + border_width*2 shadow = Image.new(mode, img.size, color=shadow_color) canvas = Image.new(mode, (width, height), color=back_color) canvas.paste(shadow, box=(border_width, int(border_width*1.16))) canvas = canvas.filter(GaussianBlur(border_width // 4)) canvas.paste(img, box=(border_width, border_width)) canvas.save(save) # Uses grim for wayland to take a screenshot. Default to full screen without # dims. exact_dims must match a slurp regex def take_screenshot(path, exact_dims=None): if exact_dims is not None: run(["grim", "-g", exact_dims, path]).check_returncode() else: run(["grim", path]).check_returncode() # Takes a screenshot as specified by args def take_subcommand(args): save_path = get_sceenshot_path() match args.region: case "full": take_screenshot(save_path, None) case "exact": take_screenshot(save_path, args.dimensions) case "select": take_screenshot(save_path, select_area()) case _: raise Exception(f"Region '{args.region}' not recognized") match args.drop_shadow: case "light": add_drop_shadow(save_path, save_path, 'white', 'black') case "dark": add_drop_shadow(save_path, save_path, '#515e6e', 'black') if args.clipboard: copy_to_clipboard(save_path) if args.file is not None and not os.path.isfile(args.file): shutil.copyfile(save_path, args.file) elif args.file is not None: raise Exception(f"Refusing to overwrite {args.file}") # Provides a drawing editor for the latest image def markup_latest(args): img = get_latest_sceenshot_path() if args.gimp: print("TODO gimp") else: run(["swappy", img]).check_returncode() if args.clipboard: copy_to_clipboard(img) # =================================================================== # Parse args # =================================================================== def parser_dir(s: str): if os.path.isdir(s): return Path(s) else: raise NotADirectoryError(f"`{s}` is not a directory") def slurp_regex(s: str) -> str: s = s.strip() if re.fullmatch('[0-9]+,[0-9]+ [0-9]+x[0-9]+', s): return s else: raise Exception( f"`{s}` does not match pattern /[0-9]+,[0-9]+ [0-9]+x[0-9]+/") parser = argparse.ArgumentParser( prog="Screenshot Wayland v1.0.0", description='Take a screenshot on Sway' ); parser.add_argument( '-s', '--screenshot-dir', type=parser_dir, metavar="