diff --git a/flake.lock b/flake.lock index 3a1aad5..12c2883 100644 --- a/flake.lock +++ b/flake.lock @@ -193,6 +193,26 @@ "type": "github" } }, + "loupe": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1789759456, + "narHash": "sha256-pV2iKbCp4F7ux4E3usYHi/KzC08/fxiDa64cL4DdJfc=", + "ref": "refs/heads/main", + "rev": "f65d5548de5aa77cc14870d5e7a5f24a972d957a", + "revCount": 1671, + "type": "git", + "url": "https://forg.gregersen.it/lazzexus/Loupe" + }, + "original": { + "type": "git", + "url": "https://forg.gregersen.it/lazzexus/Loupe" + } + }, "nix-cachyos-kernel": { "inputs": { "flake-compat": "flake-compat", @@ -371,6 +391,7 @@ "root": { "inputs": { "home-manager": "home-manager", + "loupe": "loupe", "nix-cachyos-kernel": "nix-cachyos-kernel", "nixpkgs": "nixpkgs_2", "proton-cachyos-nix": "proton-cachyos-nix", diff --git a/flake.nix b/flake.nix index 6034e01..e6178d6 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,11 @@ proton-cachyos-nix.url = "github:powerofthe69/proton-cachyos-nix"; + loupe = { + url = "git+https://forg.gregersen.it/lazzexus/Loupe"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # Source only, not this flake's outputs: nixpkgs' redlib predates the wreq # switch and Reddit 403s its rustls TLS fingerprint (see services/redlib.nix). redlib = { diff --git a/hosts/FredOS-Gaming.nix b/hosts/FredOS-Gaming.nix index cbc0a26..9db3d26 100644 --- a/hosts/FredOS-Gaming.nix +++ b/hosts/FredOS-Gaming.nix @@ -20,6 +20,7 @@ protonplus unrar p7zip + inputs.loupe.packages.${pkgs.stdenv.hostPlatform.system}.default ]; # Nova Pro Wireless base station: open up its vendor HID interface so diff --git a/settings/hyprland.nix b/settings/hyprland.nix index e9d1cdf..cccef68 100644 --- a/settings/hyprland.nix +++ b/settings/hyprland.nix @@ -79,6 +79,23 @@ in # land in unrelated hues from the ink the shell frame is drawn in. ink4 = ui.mixHex c.base05 c.base00 ui.inkMuted; ink3 = ui.mixHex c.base05 c.base00 ui.inkFaint; + screenshotWithLoupe = pkgs.writeShellScript "screenshot-with-loupe" '' + set -eu + dir="''${XDG_RUNTIME_DIR:?}/loupe-screenshots" + ${pkgs.coreutils}/bin/mkdir -m 700 -p "$dir" + name="$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S-%N).png" + ${pkgs.hyprshot}/bin/hyprshot -m "$1" -o "$dir" -f "$name" --silent + [ -s "$dir/$name" ] || exit 0 + + # Quickshell retains at most 30 notifications. Keep the same number of + # screenshots so notification clicks work without filling runtime tmpfs. + ${pkgs.findutils}/bin/find "$dir" -maxdepth 1 -type f -name '*.png' -printf '%f\n' \ + | ${pkgs.coreutils}/bin/sort \ + | ${pkgs.coreutils}/bin/head -n -30 \ + | while IFS= read -r old; do ${pkgs.coreutils}/bin/rm -- "$dir/$old"; done + ${pkgs.libnotify}/bin/notify-send -a Hyprshot -i "$dir/$name" \ + 'Screenshot copied' 'Click to annotate in Loupe. Saved temporarily until logout.' + ''; in { # Stylix's Hyprland target injects settings.{general,decoration,group,misc} # as top-level keys, which render as hl.general()/hl.decoration()/… in Lua @@ -506,11 +523,15 @@ in -- Screenshots — Shift+Super+S matches GNOME binding -- Pin/unpin quickshell's focus grab around the region select so an -- open menu survives slurp's input grab (no-ops if qs isn't up). - -- --clipboard-only: nothing hits the disk, so there is no folder to - -- fill and no file for the notification to thumbnail. The bar draws - -- a camera glyph instead of holding a decoded screen-sized texture. + ${if isMacbook then '' hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("sh -c 'qs ipc call screenshot pin; hyprshot -m region --clipboard-only; qs ipc call screenshot unpin'")) hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m output --clipboard-only")) + '' else '' + -- On gaming, save temporary PNGs so the notification can reopen + -- the exact screenshot in Loupe, while hyprshot still copies it. + hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("sh -c 'qs ipc call screenshot pin; ${screenshotWithLoupe} region; qs ipc call screenshot unpin'")) + hl.bind("Print", hl.dsp.exec_cmd("${screenshotWithLoupe} output")) + ''} -- Settings shortcut — Super+I matches GNOME binding hl.bind(mod .. " + I", hl.dsp.exec_cmd("pavucontrol")) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 8f88bdc..f949828 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -887,6 +887,7 @@ in readonly property string xdgOpen: "${pkgs.xdg-utils}/bin/xdg-open" readonly property string screenRec: "${screenRecScript}" readonly property string slurpRegion: "${slurpRegionScript}" + readonly property string loupe: "${if isMacbook then "" else "${inputs.loupe.packages.${pkgs.stdenv.hostPlatform.system}.default}/bin/loupe"}" readonly property string themeApply: "${themeApply}" } ''; @@ -3418,6 +3419,7 @@ in // file-path appIcon (e.g. album art on disk). "" = none. function notifPreviewSource(notif) { if (!notif) return ""; + if (notif.appName === "Hyprshot") return ""; if (notif.image && notif.image !== "") return notif.image; let a = notif.appIcon || ""; if (a.startsWith("file://")) return a; @@ -3426,9 +3428,8 @@ in } // Glyph to fill the preview slot when there is no image to put - // there. Screenshots run --clipboard-only, so nothing is written - // to disk and there is no thumbnail to load — which is the point: - // a decoded 3440x1440 texture cost ~19MB per retained shot. + // there. Gaming screenshots stay on disk for opening in Loupe; + // avoid holding full-screen preview textures in the bar. function notifPreviewIcon(notif) { if (!notif) return ""; return notif.appName === "Hyprshot" ? "camera" : ""; @@ -3459,6 +3460,14 @@ in // behind the default action, so fire that too when they send one. function activateNotif(notif) { if (!notif) return; + if (notif.appName === "Hyprshot" && Commands.loupe !== "") { + let path = notif.appIcon || ""; + if (path.startsWith("file://")) path = decodeURIComponent(path.substring(7)); + if (path.startsWith("/") && path.includes("/loupe-screenshots/") && path.endsWith(".png")) { + Quickshell.execDetached([Commands.loupe, path]); + return; + } + } for (const a of notif.actions) if (a.identifier === "default") { a.invoke(); break; } focusNotifApp(notif);