Vesktop: Pick Another Window button + shift-click for full picker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-12 20:19:45 +01:00
parent a7c4513d4e
commit 2eeb29ac80
4 changed files with 140 additions and 39 deletions

View file

@ -7,13 +7,13 @@
gamescope gamescope
spotify spotify
# Preselect the last stream's audio source (e.g. Hearthstone) when it's # Preselect the last stream's audio source (e.g. Hearthstone) when it's
# running again, instead of resetting to "None" every Go Live; Cancel in # running again, instead of resetting to "None" every Go Live.
# the stream dialog makes the next share show the full window picker # "Pick Another Window" in the stream dialog or shift-clicking share
# (see xdph.conf in settings/hyprland.nix). # bypasses the game auto-pick (see xdph.conf in settings/hyprland.nix).
(vesktop.overrideAttrs (old: { (vesktop.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [ patches = (old.patches or [ ]) ++ [
../patches/vesktop-remember-stream-audio.patch ../patches/vesktop-remember-stream-audio.patch
../patches/vesktop-cancel-shows-picker.patch ../patches/vesktop-pick-another-window.patch
]; ];
})) }))
lsfg-vk lsfg-vk

View file

@ -1,30 +0,0 @@
--- a/src/main/screenShare.ts
+++ b/src/main/screenShare.ts
@@ -5,6 +5,8 @@
*/
import { desktopCapturer, session, Streams } from "electron";
+import { writeFileSync } from "fs";
+import { join } from "path";
import type { StreamPick } from "renderer/components/ScreenSharePicker";
import { IpcCommands, IpcEvents } from "shared/IpcEvents";
@@ -55,7 +57,17 @@
skipPicker: true
}).catch(() => null);
- if (stream === null) return callback({});
+ if (stream === null) {
+ // Cancelled: tell the auto-picking XDPH picker (FredOS settings/hyprland.nix)
+ // to show the full picker on the next share.
+ try {
+ writeFileSync(
+ join(process.env.XDG_RUNTIME_DIR ?? "/tmp", "xdph-pick-manually"),
+ String(Math.floor(Date.now() / 1000))
+ );
+ } catch {}
+ return callback({});
+ }
}
callback(video ? { video: sources[0] } : {});

View file

@ -0,0 +1,131 @@
--- a/src/main/screenShare.ts
+++ b/src/main/screenShare.ts
@@ -4,7 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import { desktopCapturer, session, Streams } from "electron";
+import { app, desktopCapturer, session, Streams } from "electron";
+import { writeFileSync } from "fs";
+import { join } from "path";
import type { StreamPick } from "renderer/components/ScreenSharePicker";
import { IpcCommands, IpcEvents } from "shared/IpcEvents";
@@ -14,7 +16,31 @@
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";
+
+// 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"),
+ String(Math.floor(Date.now() / 1000))
+ );
+ } catch {}
+}
+
+// Shift held while clicking share = full picker
+let shiftDown = false;
+
export function registerScreenShareHandler() {
+ app.on("browser-window-created", (_, win) => {
+ win.webContents.on("before-input-event", (_, input) => {
+ if (input.key === "Shift") shiftDown = input.type === "keyDown";
+ });
+ win.on("blur", () => (shiftDown = false));
+ });
+
handle(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, async (_, id: string) => {
const sources = await desktopCapturer.getSources({
types: ["window", "screen"],
@@ -29,15 +55,19 @@
session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => {
// request full resolution on wayland right away because we always only end up with one result anyway
const width = isWayland ? 1920 : 176;
- const sources = await desktopCapturer
- .getSources({
- types: ["window", "screen"],
- thumbnailSize: {
- width,
- height: width * (9 / 16)
- }
- })
- .catch(err => console.error("Error during screenshare picker", err));
+ const getSources = () =>
+ desktopCapturer
+ .getSources({
+ types: ["window", "screen"],
+ thumbnailSize: {
+ width,
+ height: width * (9 / 16)
+ }
+ })
+ .catch(err => console.error("Error during screenshare picker", err));
+
+ if (isWayland && shiftDown) forceFullPicker();
+ let sources = await getSources();
if (!sources) return callback({});
@@ -48,14 +78,22 @@
}));
if (isWayland) {
- const video = data[0];
- if (video) {
+ let video = data[0];
+ while (video) {
const stream = await sendRendererCommand<StreamPick>(IpcCommands.SCREEN_SHARE_PICKER, {
screens: [video],
skipPicker: true
}).catch(() => null);
if (stream === null) return callback({});
+ if (stream.id !== PICK_ANOTHER) break;
+
+ // re-run the portal, this time with the full picker
+ forceFullPicker();
+ sources = await getSources();
+ if (!sources) return callback({});
+ const [source] = sources;
+ video = source && { id: source.id, name: source.name, url: source.thumbnail.toDataURL() };
}
callback(video ? { video: sources[0] } : {});
--- a/src/renderer/components/ScreenSharePicker.tsx
+++ b/src/renderer/components/ScreenSharePicker.tsx
@@ -82,6 +82,10 @@
url: string;
}
+// Returned instead of a source id when "Pick Another Window" is clicked; main
+// re-runs the portal with the full picker (keep in sync with main/screenShare.ts).
+export const PICK_ANOTHER = "vesktop-pick-another";
+
export let currentSettings: StreamSettings | null = null;
const logger = new Logger("VesktopScreenShare");
@@ -829,6 +833,18 @@
Go Live
</Button>
+ {skipPicker && (
+ <Button
+ variant="secondary"
+ onClick={() => {
+ submit({ id: PICK_ANOTHER, audio: false });
+ close();
+ }}
+ >
+ Pick Another Window
+ </Button>
+ )}
+
{selected && !skipPicker ? (
<Button variant="secondary" onClick={() => setSelected(void 0)}>
Back

View file

@ -114,16 +114,16 @@ in
# list as id[HC>]class[HT>]title[HE>]address[HA>]… and reads back # list as id[HC>]class[HT>]title[HE>]address[HA>]… and reads back
# "[SELECTION]<flags>/window:<id>" (flag r = token allowed). Hearthstone # "[SELECTION]<flags>/window:<id>" (flag r = token allowed). Hearthstone
# wins, else any other Proton window (steam_app_*) that isn't a launcher # wins, else any other Proton window (steam_app_*) that isn't a launcher
# or HDT. Escape hatch: Cancel in Vesktop's dialog (Gaming's patched # or HDT. Gaming's patched Vesktop writes the time to xdph-pick-manually
# Vesktop writes the time to xdph-pick-manually) and the next share # right before asking the portal ("Pick Another Window", shift-click)
# within a minute shows the normal picker. # to get the normal picker instead.
xdg.configFile."hypr/xdph.conf".text = '' xdg.configFile."hypr/xdph.conf".text = ''
screencopy { screencopy {
allow_token_by_default = true allow_token_by_default = true
custom_picker_binary = ${pkgs.writeShellScript "share-picker-game-first" '' custom_picker_binary = ${pkgs.writeShellScript "share-picker-game-first" ''
flag="''${XDG_RUNTIME_DIR:-/tmp}/xdph-pick-manually" flag="''${XDG_RUNTIME_DIR:-/tmp}/xdph-pick-manually"
printf -v now '%(%s)T' -1 printf -v now '%(%s)T' -1
{ cancelled=$(< "$flag"); } 2>/dev/null # no trailing newline, so not `read` { forced=$(< "$flag"); } 2>/dev/null # no trailing newline, so not `read`
echo 0 > "$flag" echo 0 > "$flag"
game=$(printf '%s' "''${XDPH_WINDOW_SHARING_LIST-}" | ${pkgs.gawk}/bin/awk ' game=$(printf '%s' "''${XDPH_WINDOW_SHARING_LIST-}" | ${pkgs.gawk}/bin/awk '
@ -133,7 +133,7 @@ in
&& $3 !~ /^(Battle\.net|Hearthstone Deck Tracker|HearthstoneOverlay)/ { other = $1 } && $3 !~ /^(Battle\.net|Hearthstone Deck Tracker|HearthstoneOverlay)/ { other = $1 }
END { print hs != "" ? hs : other }') END { print hs != "" ? hs : other }')
if [ -n "$game" ] && (( now - ''${cancelled:-0} > 60 )); then if [ -n "$game" ] && (( now - ''${forced:-0} > 10 )); then
echo "[SELECTION]r/window:$game" echo "[SELECTION]r/window:$game"
exit 0 exit 0
fi fi