diff --git a/.gitignore b/.gitignore index 48c5447..f5faa18 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ hosts/configuration.nix +__pycache__ diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/common/__init__.py b/scripts/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/common/common.py b/scripts/common/common.py new file mode 100644 index 0000000..a58705f --- /dev/null +++ b/scripts/common/common.py @@ -0,0 +1,71 @@ +import os +import subprocess +from pathlib import Path +from shutil import which + + +def notify( + application_name: str, + title: str, + message: str, + urgency: str = "low", + category: str | None = None, + icon: Path | None = None, + desktop_entry: str | None = None, +) -> None: + args = ["notify-send", "-a", application_name, "-u", urgency] + if category: + args.append("-c") + args.append(category) + if icon: + args.append("-i") + args.append(str(icon)) + if desktop_entry: + args.append("-h") + args.append(f"string:desktop-entry:{desktop_entry}") + args.append(title) + args.append(message) + print(args) + subprocess.run(args) + + +def read_secret_file(secret: str) -> str: + with open(f"/run/secrets/{secret}", "r") as f: + return f.read().strip() + + +def does_desktop_entry_exist(desktop_entry: str) -> bool: + if not desktop_entry: + raise ValueError("Please provide the full filename of the desktop entry.") + + if not desktop_entry.endswith(".desktop"): + desktop_entry += ".desktop" + + entry_paths = [] + + if which("qtpaths"): + result = subprocess.run( + ["qtpaths", "--paths", "ApplicationsLocation"], + stdout=subprocess.PIPE, + text=True, + ) + entry_paths = result.stdout.strip().split(":") + else: + print("qtpaths is not installed, falling back to XDG_DATA_DIRS.") + xdg_data_dirs = os.getenv("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split( + ":" + ) + entry_paths = [os.path.join(path, "applications") for path in xdg_data_dirs] + entry_paths.append(os.path.expanduser("~/.local/share/applications")) + + print(f"Checking the following paths for {desktop_entry}:\n{entry_paths}\n{'-'*20}") + + for entry_path in entry_paths: + entry_file = Path(entry_path) / f"{desktop_entry}" + print(f"Checking for {entry_file}") + if entry_file.is_file(): + print(f"{desktop_entry} found in {entry_path}") + return True + + print(f"Desktop entry {desktop_entry} does not exist.") + return False diff --git a/scripts/spectacle-screenshot b/scripts/spectacle-screenshot new file mode 100755 index 0000000..80e1ff2 --- /dev/null +++ b/scripts/spectacle-screenshot @@ -0,0 +1,118 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i python -p python312 + +import argparse +import os +import subprocess +import tempfile +from pathlib import Path +from shutil import which + +from common.common import notify + + +def spectacle_screenshot( + url: str | None = None, record: bool = False, file_path: Path | None = None +) -> None: + try: + if not which("spectacle"): + raise FileNotFoundError("spectacle is not installed.") + + if file_path and Path(file_path).exists(): + raise FileExistsError( + 'File already exists. Please provide a different file path, or use the zipline function to upload the file.\nExample: zipline "{file_path}"' + ) + + if not file_path: + # Spectacle actually defaults to .webm for video recordings, + # but Zipline doesn't behave well with .webm files so we use .mp4 instead. + use_temp_file = True + suffix = ".mp4" if record else ".png" + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) + file_path = temp_file.name + temp_file.close() + else: + use_temp_file = False + + command = [ + "spectacle", + "--nonotify", + "--background", + "--pointer", + "--copy-image", + "--output", + file_path, + ] + + if record: + command.append("--record=region") + else: + command.append("--region") + + try: + subprocess.run(command, check=True) + except subprocess.CalledProcessError as e: + if Path(file_path).exists() and use_temp_file: + os.remove(file_path) + raise e + + if not Path(file_path).stat().st_size: + os.remove(file_path) + raise FileNotFoundError("The file was not created properly.") + + try: + opts = [ + "/etc/nixos/scripts/zipline", + file_path, + "--application-name", + "Spectacle", + "--desktop-entry", + "org.kde.spectacle", + ] + if url: + opts.extend(["--url", url]) + subprocess.run(opts) + finally: + if Path(file_path).exists() and use_temp_file: + os.remove(file_path) + except Exception as e: + notify( + application_name="Spectacle", + title="An error occurred", + message=str(e), + urgency="critical", + category="transfer.error", + desktop_entry="org.kde.spectacle", + ) + raise e + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="spectacle-screenshot", + description="Take a screenshot or recording with Spectacle and automatically upload it to a Zipline instance.", + epilog="Example usage: spectacle-screenshot", + ) + parser.add_argument( + "--url", + help="The URL of the Zipline instance to upload the screenshot or recording to. Defaults to 'https://csw.im'.", + default="https://csw.im", + ) + parser.add_argument( + "--record", + help="If this is set, Spectacle will record the region instead of taking a screenshot.", + action="store_true", + ) + parser.add_argument( + "--file-path", + help="The path to save the screenshot or recording to. If not provided, the screenshot or recording will be saved to a temporary file.", + default=None, + ) + + args = parser.parse_args() + + spectacle_screenshot( + url=args.url, + record=args.record, + file_path=args.file_path, + ) diff --git a/scripts/zipline b/scripts/zipline index 1687051..227bf89 100755 --- a/scripts/zipline +++ b/scripts/zipline @@ -1,5 +1,5 @@ #! /usr/bin/env nix-shell -#! nix-shell -i python3 -p python312 python312Packages.tkinter python312Packages.requests libnotify +#! nix-shell -i python -p python312 python312Packages.tkinter python312Packages.requests libnotify import argparse import mimetypes @@ -11,83 +11,24 @@ from tkinter import Tk from typing import Any import requests # type: ignore - - -def read_secret_file(secret: str) -> str: - with open(f"/run/secrets/{secret}", "r") as f: - return f.read().strip() - - -def does_desktop_entry_exist(desktop_entry: str) -> bool: - if not desktop_entry: - raise ValueError("Please provide the full filename of the desktop entry.") - - if not desktop_entry.endswith(".desktop"): - desktop_entry += ".desktop" - - entry_paths = [] - - # Check if qtpaths is available - if which("qtpaths"): - result = subprocess.run( - ["qtpaths", "--paths", "ApplicationsLocation"], - stdout=subprocess.PIPE, - text=True, - ) - entry_paths = result.stdout.strip().split(":") - else: - print("qtpaths is not installed, falling back to XDG_DATA_DIRS.") - xdg_data_dirs = os.getenv("XDG_DATA_DIRS", "/usr/share:/usr/local/share").split( - ":" - ) - entry_paths = [os.path.join(path, "applications") for path in xdg_data_dirs] - entry_paths.append(os.path.expanduser("~/.local/share/applications")) - - print(f"Checking the following paths for {desktop_entry}:\n{entry_paths}\n{'-'*20}") - - # Search for the desktop entry file - for entry_path in entry_paths: - entry_file = Path(entry_path) / f"{desktop_entry}" - print(f"Checking for {entry_file}") - if entry_file.is_file(): - print(f"{desktop_entry} found in {entry_path}") - return True - - print(f"Desktop entry {desktop_entry} does not exist.") - return False +from common.common import does_desktop_entry_exist, notify, read_secret_file def copy_to_clipboard(text: str) -> None: - root = Tk() - root.withdraw() - root.clipboard_clear() - root.clipboard_append(text) - root.update() - root.destroy() - - -def notify( - application_name: str, - title: str, - message: str, - urgency: str = "low", - category: str | None = None, - icon: Path | None = None, - desktop_entry: str | None = None, -) -> None: - args = ["notify-send" "-a", application_name, "-u", urgency] - if category: - args.append("-c") - args.append(category) - if icon: - args.append("-i") - args.append(str(icon)) - if desktop_entry: - args.append("-h") - args.append(f"string:desktop-entry:{desktop_entry}") - args.append(title) - args.append(message) - subprocess.run(args) + if which("xclip"): + subprocess.run( + ["xclip", "-selection", "clipboard", "-t", "text/plain", "-i"], + input=text.encode(), + ) + elif which("wl-copy"): + subprocess.run(["wl-copy", "--type", "text/plain"], input=text.encode()) + else: + root = Tk() + root.withdraw() + root.clipboard_clear() + root.clipboard_append(text) + root.update() + root.destroy() def zipline( @@ -98,23 +39,19 @@ def zipline( ) -> Any: token = read_secret_file("zipline") if not token: - print("Secret file at /run/secrets/zipline either does not exist or is empty.") raise FileNotFoundError( "Secret file at /run/secrets/zipline either does not exist or is empty." ) if not os.path.isfile(file_path): - print(f"File at {file_path} does not exist.") raise FileNotFoundError(f"File at {file_path} does not exist.") use_send_notify = False if application_name and desktop_entry: if not does_desktop_entry_exist(desktop_entry=desktop_entry): - print("Desktop entry does not exist.") raise FileNotFoundError("Desktop entry does not exist.") if not which("notify-send"): - print("notify-send is not installed.") raise FileNotFoundError("notify-send is not installed.") use_send_notify = True @@ -136,6 +73,7 @@ def zipline( if link: copy_to_clipboard(text=link) + print(f"Link copied to clipboard: {link}") if use_send_notify: notify(