diff --git a/patches/vesktop-wayland-share-dialog.patch b/patches/vesktop-wayland-share-dialog.patch index 1e70dbf..8c9064d 100644 --- a/patches/vesktop-wayland-share-dialog.patch +++ b/patches/vesktop-wayland-share-dialog.patch @@ -6,24 +6,26 @@ -import { desktopCapturer, session, Streams } from "electron"; +import { app, desktopCapturer, session, Streams } from "electron"; -+import { writeFileSync } from "fs"; ++import { readFileSync, statSync, writeFileSync } from "fs"; +import { join } from "path"; import type { StreamPick } from "renderer/components/ScreenSharePicker"; import { IpcCommands, IpcEvents } from "shared/IpcEvents"; -@@ -14,8 +16,39 @@ +@@ -14,8 +16,68 @@ const isWayland = process.platform === "linux" && (process.env.XDG_SESSION_TYPE === "wayland" || !!process.env.WAYLAND_DISPLAY); +// renderer's PICK_ANOTHER ("Pick Another Window" in the stream dialog) +const PICK_ANOTHER = "vesktop-pick-another"; + ++const runtimeDir = process.env.XDG_RUNTIME_DIR ?? "/tmp"; ++ +// FredOS: the XDPH custom picker (settings/hyprland.nix) auto-picks a running +// game unless this file holds a fresh timestamp, then shows the full picker. +function forceFullPicker() { + try { + writeFileSync( -+ join(process.env.XDG_RUNTIME_DIR ?? "/tmp", "xdph-pick-manually"), ++ join(runtimeDir, "xdph-pick-manually"), + String(Math.floor(Date.now() / 1000)) + ); + } catch {} @@ -34,6 +36,33 @@ +const PENDING_PREVIEW = "vesktop-pending-preview"; +let pendingPreview: Promise | undefined; + ++// The FredOS picker grim-shots its selection to xdph-preview.jpg ~70ms after ++// answering; use that, else Electron's thumbnail (~1s). No sources at all ++// (picker closed) resolves undefined so the dialog closes. ++function previewFor(sources: Promise, requestedAt: number) { ++ const file = join(runtimeDir, "xdph-preview.jpg"); ++ const jpeg = (data: Buffer) => "data:image/jpeg;base64," + data.toString("base64"); ++ ++ return new Promise(resolve => { ++ let done = false; ++ const finish = (url?: string) => { ++ if (!done) resolve(url); ++ done = true; ++ }; ++ ++ sources.then(s => finish(s?.[0] ? jpeg(s[0].thumbnail.toJPEG(85)) : undefined)); ++ ++ const poll = () => { ++ if (done) return; ++ try { ++ if (statSync(file).mtimeMs >= requestedAt) return finish(jpeg(readFileSync(file))); ++ } catch {} ++ setTimeout(poll, 25); ++ }; ++ poll(); ++ }); ++} ++ +// Shift held while clicking share = full picker +let shiftDown = false; + @@ -51,7 +80,7 @@ const sources = await desktopCapturer.getSources({ types: ["window", "screen"], thumbnailSize: { -@@ -27,14 +60,40 @@ +@@ -27,14 +89,39 @@ }); session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { @@ -63,12 +92,11 @@ + // let it fetch the preview (PENDING_PREVIEW) when the portal answers. + if (shiftDown) forceFullPicker(); + while (true) { ++ const requestedAt = Date.now(); + const sources = desktopCapturer + .getSources({ types: ["window", "screen"], thumbnailSize: { width: 960, height: 540 } }) + .catch(err => console.error("Error during screenshare picker", err)); -+ pendingPreview = sources.then(s => -+ s?.[0] ? "data:image/jpeg;base64," + s[0].thumbnail.toJPEG(85).toString("base64") : undefined -+ ); ++ pendingPreview = previewFor(sources, requestedAt); + + const stream = await sendRendererCommand(IpcCommands.SCREEN_SHARE_PICKER, { + screens: [{ id: PENDING_PREVIEW, name: "", url: "" }], @@ -96,7 +124,7 @@ } }) .catch(err => console.error("Error during screenshare picker", err)); -@@ -47,21 +106,6 @@ +@@ -47,21 +134,6 @@ url: thumbnail.toDataURL() })); diff --git a/settings/hyprland.nix b/settings/hyprland.nix index 6fae509..13a60bb 100644 --- a/settings/hyprland.nix +++ b/settings/hyprland.nix @@ -117,28 +117,90 @@ in # or HDT. Gaming's patched Vesktop writes the time to xdph-pick-manually # right before asking the portal ("Pick Another Window", shift-click) # to get the normal picker instead. + # + # After answering, it grim-shots the selection to xdph-preview.jpg for + # Vesktop's dialog (~70ms vs Electron's ~1s PipeWire thumbnail). grim + # reads the screen, so a window that isn't visible is skipped and Vesktop + # falls back to the thumbnail. xdg.configFile."hypr/xdph.conf".text = '' screencopy { allow_token_by_default = true - custom_picker_binary = ${pkgs.writeShellScript "share-picker-game-first" '' - flag="''${XDG_RUNTIME_DIR:-/tmp}/xdph-pick-manually" - printf -v now '%(%s)T' -1 - { forced=$(< "$flag"); } 2>/dev/null # no trailing newline, so not `read` - echo 0 > "$flag" + custom_picker_binary = ${lib.getExe (pkgs.writeShellApplication { + name = "share-picker-game-first"; + runtimeInputs = with pkgs; [ coreutils gawk jq grim hyprland xdg-desktop-portal-hyprland ]; + excludeShellChecks = [ "SC2016" ]; # $ in awk/jq programs is meant literally + text = '' + run=''${XDG_RUNTIME_DIR:-/tmp} + flag=$run/xdph-pick-manually + preview=$run/xdph-preview.jpg + list=''${XDPH_WINDOW_SHARING_LIST-} + rm -f "$preview" - game=$(printf '%s' "''${XDPH_WINDOW_SHARING_LIST-}" | ${pkgs.gawk}/bin/awk ' - BEGIN { RS = "\\[HA>\\]"; FS = "\\[H[CTE]>\\]" } - $2 ~ /^steam_app_/ && $3 == "Hearthstone" { hs = $1 } - $2 ~ /^steam_app_/ && $3 != "" && !other \ - && $3 !~ /^(Battle\.net|Hearthstone Deck Tracker|HearthstoneOverlay)/ { other = $1 } - END { print hs != "" ? hs : other }') + printf -v now '%(%s)T' -1 + { forced=$(< "$flag"); } 2>/dev/null || forced=0 # no trailing newline, so not `read` + echo 0 > "$flag" - if [ -n "$game" ] && (( now - ''${forced:-0} > 10 )); then - echo "[SELECTION]r/window:$game" - exit 0 - fi - exec ${pkgs.xdg-desktop-portal-hyprland}/bin/hyprland-share-picker "$@" - ''} + # run awk program $2 over the window list as id/class/title/address + # columns, with want=$1 + entries() { + printf '%s' "$list" | awk ' + BEGIN { RS = "\\[HA>\\]"; FS = "\\[H[CTE]>\\]" } + { print $1 "\t" $2 "\t" $3 "\t" $4 }' | awk -F '\t' -v want="$1" "$2" + } + game=$(entries "" ' + $2 ~ /^steam_app_/ && $3 == "Hearthstone" { hs = $1 } + $2 ~ /^steam_app_/ && $3 != "" && !other \ + && $3 !~ /^(Battle\.net|Hearthstone Deck Tracker|HearthstoneOverlay)/ { other = $1 } + END { print hs != "" ? hs : other }') + + if [ -n "$game" ] && (( now - forced > 10 )); then + selection=r/window:$game + else + out=$(hyprland-share-picker "$@") || true + selection=''${out#*"[SELECTION]"} + if [ "$selection" = "$out" ]; then # closed / failed: pass through + printf '%s\n' "$out" + exit 0 + fi + fi + echo "[SELECTION]$selection" + + shoot() { + local target=''${1#*/} where=() + case $target in + window:*) + local id=''${target#window:} addr geom + if (( id < 0 )); then id=$(( id + 4294967296 )); fi # the Qt picker prints it signed + addr=$(entries "$id" '$1 == want { print $4 }') + [ -n "$addr" ] || return 0 + geom=$(hyprctl -j clients | jq -r --arg a "$(printf '0x%x' "$addr")" \ + --argjson mons "$(hyprctl -j monitors)" ' + [$mons[] | .activeWorkspace.id, .specialWorkspace.id] as $shown + | .[] | select(.address == $a and (.hidden | not)) + | select(.workspace.id as $w | any($shown[]; . == $w)) + | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"') + [ -n "$geom" ] || return 0 + where=(-g "$geom") + ;; + screen:*) + where=(-o "''${target#screen:}") + ;; + region:*) + local spec=''${target#region:} x y w h geom + IFS=, read -r x y w h <<< "''${spec#*@}" + geom=$(hyprctl -j monitors | jq -r --arg o "''${spec%@*}" \ + --argjson x "$x" --argjson y "$y" --arg wh "''${w}x$h" ' + .[] | select(.name == $o) | "\(.x + $x),\(.y + $y) \($wh)"') + [ -n "$geom" ] || return 0 + where=(-g "$geom") + ;; + *) return 0 ;; + esac + grim "''${where[@]}" -s 0.5 -t jpeg -q 85 "$preview.tmp" && mv -f "$preview.tmp" "$preview" + } + shoot "$selection" > /dev/null 2>&1 < /dev/null & + ''; + })} } '';