mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 05:00:24 +02:00
launcher-menu: closeOnLaunch and launchDebounce options
Stock, clicking a program in the VR dashboard's "+" menu only calls SteamClient.Apps.LaunchNonSteamApp; the menu stays open until the program's window appears, so users click again and launch it twice. New launch/ patch (SharedJSContext) wraps LaunchNonSteamApp, whose only caller is this menu: closeOnLaunch closes the menu through the "+" bar button's own popup handle right after a launch; launchDebounce ignores repeated launches of the same command line within N seconds (logged to the steam-ui-patches journal). Both opt-in, like the other launcherMenu options.
This commit is contained in:
1 parent
24d6504f30
commit
dd6f3602aa
5 files changed
+169
-5
No files matched your search
@@ -127,7 +127,12 @@ the two files below).
|
|||||||
steamFrame = {
|
steamFrame = {
|
||||||
keyboardLayout = "de";
|
keyboardLayout = "de";
|
||||||
steamKeyboardPatch.enable = true;
|
steamKeyboardPatch.enable = true;
|
||||||
launcherMenu = { sort = true; pinDesktop = "bottom"; };
|
launcherMenu = {
|
||||||
|
sort = true;
|
||||||
|
pinDesktop = "bottom";
|
||||||
|
closeOnLaunch = true;
|
||||||
|
launchDebounce = 10;
|
||||||
|
};
|
||||||
firefox.enable = true;
|
firefox.enable = true;
|
||||||
# Only relevant with Steam Developer Mode on (see hidden-apps below).
|
# Only relevant with Steam Developer Mode on (see hidden-apps below).
|
||||||
hiddenApps = [ "lxterminal" "cmake-gui" "firewall-config" "renderdoc" ];
|
hiddenApps = [ "lxterminal" "cmake-gui" "firewall-config" "renderdoc" ];
|
||||||
@@ -174,6 +179,8 @@ menu list every desktop entry, including terminals such as Konsole.
|
|||||||
| `steamFrame.uiPatches.patches` | list of submodules | `[ ]` | Runtime patches of Steam's web UIs over their local DevTools ports, see [UI patches](#ui-patches-uipatchespatches). |
|
| `steamFrame.uiPatches.patches` | list of submodules | `[ ]` | Runtime patches of Steam's web UIs over their local DevTools ports, see [UI patches](#ui-patches-uipatchespatches). |
|
||||||
| `steamFrame.launcherMenu.sort` | bool | `false` | Sort the VR "+" menu alphabetically. |
|
| `steamFrame.launcherMenu.sort` | bool | `false` | Sort the VR "+" menu alphabetically. |
|
||||||
| `steamFrame.launcherMenu.pinDesktop` | null or `"top"` / `"bottom"` | `null` | Pin "Desktop" above or below the "+" menu's scrolling list (always visible). `null`: a normal list entry. |
|
| `steamFrame.launcherMenu.pinDesktop` | null or `"top"` / `"bottom"` | `null` | Pin "Desktop" above or below the "+" menu's scrolling list (always visible). `null`: a normal list entry. |
|
||||||
|
| `steamFrame.launcherMenu.closeOnLaunch` | bool | `false` | Close the "+" menu as soon as a program in it is clicked. |
|
||||||
|
| `steamFrame.launcherMenu.launchDebounce` | unsigned int (seconds) | `0` | Ignore repeated launches of the same program from the "+" menu within this time. `0`: off. |
|
||||||
| `steamFrame.hiddenApps` | list of str | `[ ]` | Desktop entry ids (without `.desktop`) to hide from the "+" and KDE menus. |
|
| `steamFrame.hiddenApps` | list of str | `[ ]` | Desktop entry ids (without `.desktop`) to hide from the "+" and KDE menus. |
|
||||||
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
|
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
|
||||||
| `steamFrame.clipboardSync.package` | package | built from `dnut/clipboard-sync` | The clipboard-sync package. |
|
| `steamFrame.clipboardSync.package` | package | built from `dnut/clipboard-sync` | The clipboard-sync package. |
|
||||||
@@ -346,6 +353,11 @@ GLib hash-table order, effectively random and changing with installed apps,
|
|||||||
with "Desktop" (the nested Plasma session) somewhere in the middle of a
|
with "Desktop" (the nested Plasma session) somewhere in the middle of a
|
||||||
scrolling list.
|
scrolling list.
|
||||||
|
|
||||||
|
Also, clicking a program only calls `SteamClient.Apps.LaunchNonSteamApp()`:
|
||||||
|
the menu stays open until the program's window appears, which can take a
|
||||||
|
while, so it looks as if the click did nothing, and a second click starts
|
||||||
|
the program twice.
|
||||||
|
|
||||||
**Fix:** UI patches (see above) in Steam's `SharedJSContext`:
|
**Fix:** UI patches (see above) in Steam's `SharedJSContext`:
|
||||||
|
|
||||||
- `sort = true`: a wrapper around `ScanForInstalledNonSteamApps` sorts the
|
- `sort = true`: a wrapper around `ScanForInstalledNonSteamApps` sorts the
|
||||||
@@ -360,8 +372,17 @@ scrolling list.
|
|||||||
**Limitation:** the pinned copy is a plain DOM element, not part of Steam's
|
**Limitation:** the pinned copy is a plain DOM element, not part of Steam's
|
||||||
controller navigation: it works with the laser pointer, but thumbstick /
|
controller navigation: it works with the laser pointer, but thumbstick /
|
||||||
D-pad focus can't reach it.
|
D-pad focus can't reach it.
|
||||||
|
- `closeOnLaunch = true` / `launchDebounce = <seconds>`: a wrapper around
|
||||||
|
`LaunchNonSteamApp` (called only by this menu, for list entries and the
|
||||||
|
pinned Desktop alike). With `closeOnLaunch`, it closes the menu right after
|
||||||
|
the launch through the "+" button's own popup handle (as Steam does after
|
||||||
|
adding a desktop window). With `launchDebounce`, another launch of the same
|
||||||
|
command line within that many seconds of the last one that went through is
|
||||||
|
ignored and logged (`journalctl --user -u steam-ui-patches`, on the next
|
||||||
|
re-injection, i.e. within 15 s). Consequence: a program that exits right
|
||||||
|
away can only be started again once the time is up.
|
||||||
|
|
||||||
Both are reverted when the options are turned off (next switch).
|
All are reverted when the options are turned off (next switch).
|
||||||
Tested with Steam client 1790377368.
|
Tested with Steam client 1790377368.
|
||||||
|
|
||||||
### Hidden apps (`hiddenApps`)
|
### Hidden apps (`hiddenApps`)
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
# - pinned-desktop/: hides Desktop in the scrolling list and pins a copy above
|
# - pinned-desktop/: hides Desktop in the scrolling list and pins a copy above
|
||||||
# or below it, with a separator; clicking the copy clicks the hidden
|
# or below it, with a separator; clicking the copy clicks the hidden
|
||||||
# original. The position is passed by calling the patch's function.
|
# original. The position is passed by calling the patch's function.
|
||||||
# Both are reverted when turned off (next switch).
|
# - launch/: wraps SteamClient.Apps.LaunchNonSteamApp (only called by this
|
||||||
|
# menu) to close the menu right after a program is started, and/or to
|
||||||
|
# ignore repeated launches of the same program within a few seconds (stock,
|
||||||
|
# the menu stays open until the program's window appears, inviting double
|
||||||
|
# launches). Options are passed by calling the patch's function.
|
||||||
|
# All are reverted when turned off (next switch).
|
||||||
# Depends on Steam UI internals; tested with Steam client 1790377368.
|
# Depends on Steam UI internals; tested with Steam client 1790377368.
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
let
|
let
|
||||||
@@ -38,6 +43,29 @@ in {
|
|||||||
patch of Steam's UI (steamFrame.uiPatches).
|
patch of Steam's UI (steamFrame.uiPatches).
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
closeOnLaunch = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Close the "+" menu as soon as a program in it is activated (pointer,
|
||||||
|
controller or the pinned Desktop entry). Stock, it stays open until the
|
||||||
|
program's window appears, so it looks as if the click did nothing and
|
||||||
|
programs get launched twice. Runtime patch of Steam's UI
|
||||||
|
(steamFrame.uiPatches); off by default like the other launcherMenu
|
||||||
|
options, so nothing is injected unless asked for.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
launchDebounce = lib.mkOption {
|
||||||
|
type = lib.types.ints.unsigned;
|
||||||
|
default = 0;
|
||||||
|
example = 10;
|
||||||
|
description = ''
|
||||||
|
Seconds during which another launch of the same program (same command
|
||||||
|
line) from the "+" menu is ignored, counted from the last launch that
|
||||||
|
went through; ignored launches are logged (steam-ui-patches journal).
|
||||||
|
0 disables it. Runtime patch of Steam's UI (steamFrame.uiPatches).
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config.steamFrame.uiPatches.patches =
|
config.steamFrame.uiPatches.patches =
|
||||||
@@ -54,5 +82,16 @@ in {
|
|||||||
(${builtins.readFile ./launcher-menu/pinned-desktop/patch.js})(${builtins.toJSON { position = cfg.pinDesktop; }})
|
(${builtins.readFile ./launcher-menu/pinned-desktop/patch.js})(${builtins.toJSON { position = cfg.pinDesktop; }})
|
||||||
'';
|
'';
|
||||||
unpatch = ./launcher-menu/pinned-desktop/unpatch.js;
|
unpatch = ./launcher-menu/pinned-desktop/unpatch.js;
|
||||||
|
}
|
||||||
|
++ lib.optional (cfg.closeOnLaunch || cfg.launchDebounce > 0) {
|
||||||
|
name = "launcher-menu-launch";
|
||||||
|
target = sharedJSContext;
|
||||||
|
patch = pkgs.writeText "launcher-menu-launch.js" ''
|
||||||
|
(${builtins.readFile ./launcher-menu/launch/patch.js})(${builtins.toJSON {
|
||||||
|
inherit (cfg) closeOnLaunch;
|
||||||
|
debounceSeconds = cfg.launchDebounce;
|
||||||
|
}})
|
||||||
|
'';
|
||||||
|
unpatch = ./launcher-menu/launch/unpatch.js;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// launch: what happens when a program of the VR dashboard's "+" menu
|
||||||
|
// (#VRDashboard_LaunchNonSteamApp) is activated.
|
||||||
|
//
|
||||||
|
// This file is a function expression; launcher-menu.nix calls it with the
|
||||||
|
// options: (<this file>)({ closeOnLaunch: true, debounceSeconds: 10 }).
|
||||||
|
//
|
||||||
|
// Stock, an item's onActivate only calls
|
||||||
|
// SteamClient.Apps.LaunchNonSteamApp(strCmdline) (plus the nav sound); the
|
||||||
|
// "+" popup stays open until the new window takes over, so users click again
|
||||||
|
// and start the program twice. That call is the only use of
|
||||||
|
// LaunchNonSteamApp in Steam's UI and is looked up at call time, so this
|
||||||
|
// patch wraps it in SharedJSContext (the dashboard bar's realm):
|
||||||
|
// - debounceSeconds > 0: a launch of the same command line within that many
|
||||||
|
// seconds of the last accepted one is ignored (console.info, and reported
|
||||||
|
// in the steam-ui-patches journal on the next re-injection);
|
||||||
|
// - closeOnLaunch: after a launch (ignored or not), the "+" popup is closed
|
||||||
|
// through the bar button's own popup handle (closePopup(), what stock does
|
||||||
|
// after adding a desktop window). The handle is found in the dashboard bar
|
||||||
|
// document: the button's React fiber ancestors include the "+" component
|
||||||
|
// (prop allowLaunchProgram) and the bar button (prop refBarPopopHandle).
|
||||||
|
// Works for every activation path (pointer, controller, the pinned Desktop
|
||||||
|
// copy, which clicks the stock item). The original is kept as __sfuiOrig
|
||||||
|
// (unpatch.js restores it). Idempotent; bump VERSION when changing the
|
||||||
|
// wrapper.
|
||||||
|
((opts) => {
|
||||||
|
const VERSION = 1;
|
||||||
|
const NAME = 'launcher-menu-launch';
|
||||||
|
const CLOSE = opts.closeOnLaunch === true;
|
||||||
|
const DEBOUNCE_MS = Math.max(0, Number(opts.debounceSeconds) || 0) * 1000;
|
||||||
|
const Apps = window.SteamClient?.Apps;
|
||||||
|
const cur = Apps?.LaunchNonSteamApp;
|
||||||
|
if (typeof cur !== 'function') return 'SteamClient.Apps.LaunchNonSteamApp missing';
|
||||||
|
const st = cur.__sfuiPatch === NAME ? cur.__sfuiState : null;
|
||||||
|
if (st && cur.__sfuiVersion === VERSION && st.close === CLOSE && st.debounceMs === DEBOUNCE_MS) {
|
||||||
|
// Report ignored launches since the last evaluation (the injector logs
|
||||||
|
// results that differ from the previous one).
|
||||||
|
const msgs = st.log.splice(0);
|
||||||
|
return msgs.length ? msgs.join('; ') : 'unchanged';
|
||||||
|
}
|
||||||
|
const orig = cur.__sfuiPatch === NAME ? cur.__sfuiOrig : cur;
|
||||||
|
|
||||||
|
const fiberOf = (el) => { const k = Object.keys(el).find((k) => k.startsWith('__reactFiber$')); return k && el[k]; };
|
||||||
|
// Popup handles of the "+" bar button(s) (normally one).
|
||||||
|
const plusHandles = () => {
|
||||||
|
const hs = [];
|
||||||
|
for (const p of window.g_PopupManager?.GetPopups() ?? []) {
|
||||||
|
if (!/gamepadui\.bar\./.test(p.m_strName ?? '')) continue;
|
||||||
|
let d;
|
||||||
|
try { d = p.window?.document; } catch { continue; }
|
||||||
|
for (const el of d?.querySelectorAll('.VRDashboardBarSmallButton') ?? []) {
|
||||||
|
let h = null;
|
||||||
|
for (let f = fiberOf(el), i = 0; f && i < 20; f = f.return, i++) {
|
||||||
|
const mp = f.memoizedProps;
|
||||||
|
if (!mp || typeof mp !== 'object') continue;
|
||||||
|
if ('refBarPopopHandle' in mp) h = mp.refBarPopopHandle?.current ?? null;
|
||||||
|
if ('allowLaunchProgram' in mp) { if (h && !hs.includes(h)) hs.push(h); break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hs;
|
||||||
|
};
|
||||||
|
const closeMenu = () => {
|
||||||
|
for (const h of plusHandles()) if (h.BPopupOpen?.()) h.closePopup();
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = { close: CLOSE, debounceMs: DEBOUNCE_MS, last: new Map(), log: [], plusHandles, closeMenu };
|
||||||
|
const f = function LaunchNonSteamApp(cmdline, ...rest) {
|
||||||
|
let result;
|
||||||
|
const now = Date.now();
|
||||||
|
const key = String(cmdline);
|
||||||
|
const prev = state.last.get(key);
|
||||||
|
if (DEBOUNCE_MS > 0 && prev !== undefined && now - prev < DEBOUNCE_MS) {
|
||||||
|
const msg = `ignored repeated launch of ${JSON.stringify(key)} (${((now - prev) / 1000).toFixed(1)} s after the last one, at ${new Date(now).toLocaleTimeString()})`;
|
||||||
|
console.info(`sfui ${NAME}: ${msg}`);
|
||||||
|
state.log.push(msg);
|
||||||
|
if (state.log.length > 20) state.log.shift();
|
||||||
|
} else {
|
||||||
|
state.last.set(key, now);
|
||||||
|
for (const [k, t] of state.last) if (now - t >= DEBOUNCE_MS && k !== key) state.last.delete(k);
|
||||||
|
result = orig.call(this, cmdline, ...rest);
|
||||||
|
}
|
||||||
|
if (CLOSE) { try { closeMenu(); } catch (e) { console.error(`sfui ${NAME}:`, e); } }
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
f.__sfuiPatch = NAME; f.__sfuiVersion = VERSION; f.__sfuiOrig = orig; f.__sfuiState = state;
|
||||||
|
Apps.LaunchNonSteamApp = f;
|
||||||
|
const what = [CLOSE && 'close on launch', DEBOUNCE_MS > 0 && `debounce ${DEBOUNCE_MS / 1000} s`].filter(Boolean);
|
||||||
|
return `patched (${what.join(', ') || 'no-op'})`;
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Reverts launch/patch.js (restores the original
|
||||||
|
// SteamClient.Apps.LaunchNonSteamApp). Safe when not patched.
|
||||||
|
(() => {
|
||||||
|
const Apps = window.SteamClient?.Apps;
|
||||||
|
const f = Apps?.LaunchNonSteamApp;
|
||||||
|
if (f?.__sfuiPatch !== 'launcher-menu-launch') return 'not patched';
|
||||||
|
Apps.LaunchNonSteamApp = f.__sfuiOrig;
|
||||||
|
return 'unpatched';
|
||||||
|
})()
|
||||||
+8
-2
@@ -17,8 +17,14 @@
|
|||||||
# steamFrame = {
|
# steamFrame = {
|
||||||
# keyboardLayout = "de"; # XKB layout for the Steam session
|
# keyboardLayout = "de"; # XKB layout for the Steam session
|
||||||
# steamKeyboardPatch.enable = true; # Esc/Ctrl/Alt/arrows on the VR keyboard
|
# steamKeyboardPatch.enable = true; # Esc/Ctrl/Alt/arrows on the VR keyboard
|
||||||
# # VR "+" menu: sorted by name, Desktop pinned below the list:
|
# # VR "+" menu: sorted by name, Desktop pinned below the list, closed
|
||||||
# launcherMenu = { sort = true; pinDesktop = "bottom"; };
|
# # on click, no second launch of the same program within 10 s:
|
||||||
|
# launcherMenu = {
|
||||||
|
# sort = true;
|
||||||
|
# pinDesktop = "bottom";
|
||||||
|
# closeOnLaunch = true;
|
||||||
|
# launchDebounce = 10;
|
||||||
|
# };
|
||||||
# firefox.enable = true; # launcher for the Firefox Flatpak
|
# firefox.enable = true; # launcher for the Firefox Flatpak
|
||||||
# # Hidden from the "+" menu (with Steam Developer Mode on):
|
# # Hidden from the "+" menu (with Steam Developer Mode on):
|
||||||
# hiddenApps = [ "lxterminal" "cmake-gui" "firewall-config" "renderdoc" ];
|
# hiddenApps = [ "lxterminal" "cmake-gui" "firewall-config" "renderdoc" ];
|
||||||
|
|||||||
Reference in new issue
Block a user