mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 06:00:22 +02:00
Add steam-close-button: X on the dashboard's Steam window
steamFrame.dashboard.steamCloseButton.enable (default false) gives the SteamVR dashboard's main Steam window a close button: it docks the window back into the dashboard, then switches to the previously active window or, with none, leaves just the dashboard bar. Bar-only survives dashboard reopens and patch restarts (state in window.__sfuiSteamCloseState, taken over from the earlier personal version 4 of this patch); the stock "no active frame -> show Steam" paths are suppressed while it lasts. MobX, the dock-location enum and the closing component class are found by signature; check-only signatures cover the dashboard internals it uses.
This commit is contained in:
1 parent
681533027d
commit
01645ffcf6
7 files changed
+470
-3
No files matched your search
@@ -4,7 +4,7 @@
|
|||||||
Valve Steam Frame: SteamOS on `aarch64-linux`, standalone home-manager on a
|
Valve Steam Frame: SteamOS on `aarch64-linux`, standalone home-manager on a
|
||||||
non-NixOS system. They work around quirks of the Frame's two graphical
|
non-NixOS system. They work around quirks of the Frame's two graphical
|
||||||
sessions (portal config, keyboard layout, VR keyboard, "+" menu, VR
|
sessions (portal config, keyboard layout, VR keyboard, "+" menu, VR
|
||||||
dashboard windows, clipboard, Firefox)
|
dashboard windows, a close button for Steam's VR window, clipboard, Firefox)
|
||||||
declaratively, so every change can be reverted by activating an older
|
declaratively, so every change can be reverted by activating an older
|
||||||
home-manager generation.
|
home-manager generation.
|
||||||
|
|
||||||
@@ -141,6 +141,7 @@ the two files below).
|
|||||||
dashboard = {
|
dashboard = {
|
||||||
windowMaxScale = 4.0;
|
windowMaxScale = 4.0;
|
||||||
windowDistance.world.max = 10.0;
|
windowDistance.world.max = 10.0;
|
||||||
|
steamCloseButton.enable = true;
|
||||||
};
|
};
|
||||||
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).
|
||||||
@@ -165,7 +166,7 @@ home-manager switch --flake .#steamos
|
|||||||
```
|
```
|
||||||
|
|
||||||
Individual modules are available as
|
Individual modules are available as
|
||||||
`homeManagerModules.{session,portal,keyboard-layout,steam-keyboard-patch,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,dashboard-windows,clipboard-sync,firefox}`;
|
`homeManagerModules.{session,portal,keyboard-layout,steam-keyboard-patch,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,dashboard-windows,steam-close-button,clipboard-sync,firefox}`;
|
||||||
`default` imports all of them.
|
`default` imports all of them.
|
||||||
|
|
||||||
**Steam Developer Mode** (a Steam setting, not managed here) makes the "+"
|
**Steam Developer Mode** (a Steam setting, not managed here) makes the "+"
|
||||||
@@ -197,6 +198,7 @@ menu list every desktop entry, including terminals such as Konsole.
|
|||||||
| `steamFrame.launcherMenu.iconFallbacks` | list of str | `[ "utilities-terminal" "preferences-system" ]` | Icon names installed as hicolor copies of Breeze app icons, so the "+" menu shows them (Konsole, KDE System Settings). `[ ]`: none. Takes effect after a Steam restart. |
|
| `steamFrame.launcherMenu.iconFallbacks` | list of str | `[ "utilities-terminal" "preferences-system" ]` | Icon names installed as hicolor copies of Breeze app icons, so the "+" menu shows them (Konsole, KDE System Settings). `[ ]`: none. Takes effect after a Steam restart. |
|
||||||
| `steamFrame.dashboard.windowMaxScale` | null or number | `null` | Largest resize-handle scale of SteamVR dashboard windows, relative to their default size. `null`: stock (2). See [Dashboard windows](#dashboard-windows-dashboard). |
|
| `steamFrame.dashboard.windowMaxScale` | null or number | `null` | Largest resize-handle scale of SteamVR dashboard windows, relative to their default size. `null`: stock (2). See [Dashboard windows](#dashboard-windows-dashboard). |
|
||||||
| `steamFrame.dashboard.windowDistance.{world,theater,dashboard}.{min,max}` | null or number (m) | `null` | How close / far grabbed windows can be pulled in / pushed back. `null`: stock (world 0.25-5, theater 1-6, dashboard 0.3-4 m). |
|
| `steamFrame.dashboard.windowDistance.{world,theater,dashboard}.{min,max}` | null or number (m) | `null` | How close / far grabbed windows can be pulled in / pushed back. `null`: stock (world 0.25-5, theater 1-6, dashboard 0.3-4 m). |
|
||||||
|
| `steamFrame.dashboard.steamCloseButton.enable` | bool | `false` | Close (X) button on the dashboard's Steam window: switches to the previous window, or leaves just the dashboard bar. See [Steam close button](#steam-close-button-dashboardsteamclosebuttonenable). |
|
||||||
| `steamFrame.steamvrDebugger.enable` | bool | `true` if a UI patch uses port 8087, else `false` | SteamVR dashboard DevTools on `127.0.0.1:8087` (`VRWebHelper/DebuggerEnabled`), needed by dashboard patches. See [SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable). |
|
| `steamFrame.steamvrDebugger.enable` | bool | `true` if a UI patch uses port 8087, else `false` | SteamVR dashboard DevTools on `127.0.0.1:8087` (`VRWebHelper/DebuggerEnabled`), needed by dashboard patches. See [SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable). |
|
||||||
| `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. |
|
||||||
@@ -614,13 +616,78 @@ options silently do nothing (the scale option still applies). The patch's
|
|||||||
state, including counters of rewritten nodes per kind, is
|
state, including counters of rewritten nodes per kind, is
|
||||||
`window.__sfuiDashboardWindows` in the `systemui` page (DevTools).
|
`window.__sfuiDashboardWindows` in the `systemui` page (DevTools).
|
||||||
|
|
||||||
|
### Steam close button (`dashboard.steamCloseButton.enable`)
|
||||||
|
|
||||||
|
**Problem:** every SteamVR dashboard window has a close (X) button except
|
||||||
|
Steam's own (the library, overlay `valve.steam.gamepadui.main`): it can't be
|
||||||
|
put away, and with no other window open the dashboard always shows it.
|
||||||
|
|
||||||
|
**Fix:** a UI patch (see above) of SteamVR's dashboard page (`vrwebhelper`,
|
||||||
|
`127.0.0.1:8087`, title `systemui`; it turns on the
|
||||||
|
[SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable)) gives the Steam
|
||||||
|
window an X. The window can't really be closed, so clicking it:
|
||||||
|
|
||||||
|
- docks the window back into the dashboard if it was placed in the world
|
||||||
|
(or theater / hand);
|
||||||
|
- if it was the dashboard's active window, switches to the most recently
|
||||||
|
active other window that is still open, in the dashboard and has a tab in
|
||||||
|
the dashboard bar (like a tab click);
|
||||||
|
- with none left, leaves the dashboard open with **just its bar** ("bar
|
||||||
|
only"), no window shown.
|
||||||
|
|
||||||
|
Bar-only is kept when the dashboard is closed and reopened (SteamVR would
|
||||||
|
otherwise pick Steam again) and across restarts of the patch service
|
||||||
|
(`home-manager switch`, re-injection); it ends as soon as any window becomes
|
||||||
|
active: the Steam tab or another tab, a Steam menu pick, a launched app or a
|
||||||
|
new window. It does not survive a SteamVR restart (or a reload of the
|
||||||
|
dashboard page): the dashboard then starts with Steam as usual.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
steamFrame.dashboard.steamCloseButton.enable = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
How: the X of a dashboard window is its `closing` component, shown when a
|
||||||
|
close method is possible; the patch adds an `onCloseRequested` to the Steam
|
||||||
|
window's default component props. For bar-only it clears the active frame
|
||||||
|
and overrides two methods of the dashboard instance while bar-only is on:
|
||||||
|
`autoSwitchOverlayIfNeeded` (stock: no active frame → show Steam) does
|
||||||
|
nothing, and `onShowOverlayRequestFromSteam` drops the first
|
||||||
|
`ShowOverlay("valve.steam.gamepadui.main")` Steam sends by itself after each
|
||||||
|
dashboard open (within 10 s). The patch's state (bar-only, recent windows,
|
||||||
|
ignored requests) is `window.__sfuiSteamCloseState` in the `systemui` page;
|
||||||
|
`window.__sfuiSteamClose.plan()` tells what a click would do right now,
|
||||||
|
without doing it. The journal (`journalctl --user -u steam-ui-patches`)
|
||||||
|
shows `bar-only` and the number of ignored Steam requests.
|
||||||
|
|
||||||
|
**Limitations:**
|
||||||
|
|
||||||
|
- If Steam ever doesn't send that automatic request after an open, the
|
||||||
|
first Steam menu pick (e.g. Library) in bar-only is swallowed once; a
|
||||||
|
second one works.
|
||||||
|
- Showing Steam through SteamVR's own `CVRSteamPrivate::SwitchToDashboardOverlay`
|
||||||
|
path (not through the dashboard page) isn't filtered and ends bar-only.
|
||||||
|
- With a VRLink remote dashboard, SteamVR ignores the local active-frame
|
||||||
|
change, so bar-only has no effect there.
|
||||||
|
- Turning the option off (next switch) removes the button but leaves a
|
||||||
|
dashboard that is bar-only at that moment without an active window until
|
||||||
|
the next tab click or dashboard open.
|
||||||
|
|
||||||
|
**Caveats:** depends on SteamVR UI internals: MobX, the dock-location enum
|
||||||
|
and the `closing` component class are found by signature, and the names the
|
||||||
|
patch relies on (`DashboardStore._setActiveFrame`, `mainSteamFrame`,
|
||||||
|
`Dashboard.autoSwitchOverlayIfNeeded`, …) are checked offline
|
||||||
|
(`steam-close-button` in `modules/lib/signatures.json`,
|
||||||
|
`scripts/check-signatures.mjs`); if a lookup fails, the patch reports it and
|
||||||
|
leaves the dashboard stock. Tested with SteamVR build 11008059.
|
||||||
|
|
||||||
### SteamVR debugger (`steamvrDebugger.enable`)
|
### SteamVR debugger (`steamvrDebugger.enable`)
|
||||||
|
|
||||||
Patches of the SteamVR dashboard need its DevTools port, which SteamVR only
|
Patches of the SteamVR dashboard need its DevTools port, which SteamVR only
|
||||||
opens with the setting `VRWebHelper/DebuggerEnabled` (port
|
opens with the setting `VRWebHelper/DebuggerEnabled` (port
|
||||||
`VRWebHelper/DebuggerPort`, default 8087). It is enabled automatically
|
`VRWebHelper/DebuggerPort`, default 8087). It is enabled automatically
|
||||||
(`mkDefault`) as soon as a patch in `steamFrame.uiPatches.patches` uses port
|
(`mkDefault`) as soon as a patch in `steamFrame.uiPatches.patches` uses port
|
||||||
8087 (e.g. [Dashboard windows](#dashboard-windows-dashboard)).
|
8087 (e.g. [Dashboard windows](#dashboard-windows-dashboard),
|
||||||
|
[Steam close button](#steam-close-button-dashboardsteamclosebuttonenable)).
|
||||||
|
|
||||||
SteamVR rewrites `~/.config/openvr/config/steamvr.vrsettings` while it runs
|
SteamVR rewrites `~/.config/openvr/config/steamvr.vrsettings` while it runs
|
||||||
and on exit, so the file can't be managed by Home Manager, and edits while it
|
and on exit, so the file can't be managed by Home Manager, and edits while it
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
launcher-menu = ./modules/launcher-menu.nix;
|
launcher-menu = ./modules/launcher-menu.nix;
|
||||||
steamvr-debugger = ./modules/steamvr-debugger.nix;
|
steamvr-debugger = ./modules/steamvr-debugger.nix;
|
||||||
dashboard-windows = ./modules/dashboard-windows.nix;
|
dashboard-windows = ./modules/dashboard-windows.nix;
|
||||||
|
steam-close-button = ./modules/steam-close-button.nix;
|
||||||
# Built with the consumer's pkgs; only the source comes from our input.
|
# Built with the consumer's pkgs; only the source comes from our input.
|
||||||
# The key lets the module system deduplicate it when it is imported both
|
# The key lets the module system deduplicate it when it is imported both
|
||||||
# directly and through `default`.
|
# directly and through `default`.
|
||||||
|
|||||||
@@ -156,6 +156,66 @@
|
|||||||
"module": { "includes": ["\"grab-scale\"", "\"min-distance\"", "\"max-distance\""] }
|
"module": { "includes": ["\"grab-scale\"", "\"min-distance\"", "\"max-distance\""] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"steam-close-button": {
|
||||||
|
"bundle": "vrwebui-systemui",
|
||||||
|
"modules": {
|
||||||
|
"mobx": {
|
||||||
|
"module": { "includes": ["runInAction:()=>", "makeObservable:()=>", "reaction:()=>"] }
|
||||||
|
},
|
||||||
|
"dock": {
|
||||||
|
"module": { "includes": ["e[e.Dashboard=1]=\"Dashboard\",e[e.LeftHand=2]=\"LeftHand\""] },
|
||||||
|
"exports": {
|
||||||
|
"EDockLocation": { "type": "object", "props": { "Dashboard": 1, "LeftHand": 2, "RightHand": 3, "World": 4, "Theater": 5 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"closing": {
|
||||||
|
"module": { "includes": ["this.componentName=\"closing\"", "closeMethodPriority"] },
|
||||||
|
"exports": {
|
||||||
|
"Closing": { "type": "class", "includes": ["componentName=\"closing\"", "RequestClose()"] }
|
||||||
|
},
|
||||||
|
"expects": ["defaultComponentProps=", "null!=this.componentProps.onCloseRequested", "get showCloseButton()",
|
||||||
|
"m_bShowSpinner=!0"]
|
||||||
|
},
|
||||||
|
"frameComponent": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["this.defaultComponentProps", "FramePageComponent '"] },
|
||||||
|
"expects": ["get componentProps()", ".componentProps)", "[this.componentName]"]
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["e[e.Alive=2]=\"Alive\""] },
|
||||||
|
"expects": ["this.m_eState=", "this.closing=", "this.docking=", "this.tab=", "get isActiveDashboardFrame()",
|
||||||
|
"this.props="]
|
||||||
|
},
|
||||||
|
"frameStore": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["window.FrameStore="] },
|
||||||
|
"expects": ["GetFrame(e){"]
|
||||||
|
},
|
||||||
|
"dashboardStore": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["window.DashboardStore="] },
|
||||||
|
"expects": ["_setActiveFrame(e,t){", "\"_setActiveFrame\"", "get mainSteamFrame()", "m_unMainSteamFrameID",
|
||||||
|
"get activeFrameID()", "get activeFrame()", "get visibilityState()", "get isFullyVisible()"]
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["window.Dashboard=this", "autoSwitchOverlayIfNeeded(){"] },
|
||||||
|
"expects": ["onShowOverlayRequestFromSteam(e){",
|
||||||
|
"SetImplementation(\"ShowOverlay\",(e=>{this.onShowOverlayRequestFromSteam(e)}))",
|
||||||
|
"this.autoSwitchOverlayIfNeeded()", "switchToFrameInternal(e,t,r){", "\"hideDashboard\"",
|
||||||
|
"visibleInDashboardBar"]
|
||||||
|
},
|
||||||
|
"docking": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["get dockLocation()", "SetDockLocation(e"], "excludes": ["get associatedSummonOverlayKeys()"] }
|
||||||
|
},
|
||||||
|
"overlayKeys": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["\"valve.steam.gamepadui.main\"", "\"valve.steam.gamepadui.appoverlay\""] }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Close (X) button on the SteamVR dashboard's main Steam window
|
||||||
|
# (steamFrame.dashboard.steamCloseButton.enable): it switches to the
|
||||||
|
# previously active dashboard window, or leaves just the dashboard bar when
|
||||||
|
# Steam was the last one. Runtime patch of SteamVR's dashboard page
|
||||||
|
# (vrwebhelper "systemui", DevTools 127.0.0.1:8087; see
|
||||||
|
# steam-close-button/patch.js for how it works), registered with
|
||||||
|
# steam-ui-patches.nix only when enabled; it turns on the SteamVR web helper
|
||||||
|
# debugger (steamvr-debugger.nix), which needs one SteamVR restart the first
|
||||||
|
# time. Depends on SteamVR UI internals, found by signature
|
||||||
|
# (lib/signatures.json, "steam-close-button"; scripts/check-signatures.mjs
|
||||||
|
# checks them offline).
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.steamFrame.dashboard.steamCloseButton;
|
||||||
|
inherit (import ./lib { inherit pkgs; }) mkPatch;
|
||||||
|
in {
|
||||||
|
imports = [ ./steam-ui-patches.nix ./steamvr-debugger.nix ];
|
||||||
|
|
||||||
|
options.steamFrame.dashboard.steamCloseButton.enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Close (X) button on the SteamVR dashboard's main Steam window. It
|
||||||
|
docks the window back into the dashboard if it was placed in the world,
|
||||||
|
then switches to the most recently active other dashboard window, or,
|
||||||
|
with none, leaves the dashboard open with just its bar ("bar only";
|
||||||
|
kept when the dashboard is closed and reopened, until a window is
|
||||||
|
activated, e.g. with the Steam tab). Runtime patch of the SteamVR
|
||||||
|
dashboard (steamFrame.uiPatches), applied immediately; turning it off
|
||||||
|
removes the button (next switch).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
steamFrame.uiPatches.patches = [ {
|
||||||
|
name = "steam-close-button";
|
||||||
|
endpoint = "http://127.0.0.1:8087";
|
||||||
|
target.title = "systemui";
|
||||||
|
patch = mkPatch {
|
||||||
|
name = "steam-close-button";
|
||||||
|
src = ./steam-close-button/patch.js;
|
||||||
|
};
|
||||||
|
unpatch = ./steam-close-button/unpatch.js;
|
||||||
|
} ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
// steam-close-button: a Close (X) button on the SteamVR dashboard's main
|
||||||
|
// "Steam" window (Steam's library, overlay valve.steam.gamepadui.main).
|
||||||
|
// Target: SteamVR's dashboard page (vrwebhelper, DevTools 127.0.0.1:8087,
|
||||||
|
// title "systemui").
|
||||||
|
//
|
||||||
|
// This file is a function expression, called by the file lib/default.nix
|
||||||
|
// (mkPatch) generates: (<this file>)(find, sigs, opts), with find the finder
|
||||||
|
// library (lib/finders.js) and sigs this patch's module signatures
|
||||||
|
// (lib/signatures.json, "steam-close-button": MobX, the dock-location enum,
|
||||||
|
// the frame's `closing` component class, plus check-only anchors for the
|
||||||
|
// dashboard internals used below). No options.
|
||||||
|
//
|
||||||
|
// The button: every dashboard frame has a `closing` component; its X renders
|
||||||
|
// when closing.showCloseButton, i.e. when one of closeMethodPriority's methods
|
||||||
|
// is possible. Method 0 is "componentProps.onCloseRequested exists", and
|
||||||
|
// componentProps = {...defaultComponentProps, ...frame.props.componentProps.closing}.
|
||||||
|
// The main frame passes no closing props, so this patch replaces that
|
||||||
|
// instance's plain `defaultComponentProps` field with a copy that adds
|
||||||
|
// onCloseRequested (original kept as the non-enumerable __sfuiOrig), then
|
||||||
|
// re-assigns frame.props (a shallow copy, in a MobX action) so the computed
|
||||||
|
// componentProps re-evaluates and the button appears. React re-assigning
|
||||||
|
// frame.props later keeps it (the defaults are merged every time).
|
||||||
|
//
|
||||||
|
// Clicking X (RequestClose -> onCloseRequested); the frame can't be
|
||||||
|
// destroyed, so it is put out of view:
|
||||||
|
// * undocked (world/theater/hand) -> docked back into the dashboard;
|
||||||
|
// * if it was the active dashboard frame -> the dashboard switches to the
|
||||||
|
// most recently active other frame that is alive, docked in the dashboard
|
||||||
|
// and has a dashboard-bar tab (the call a tab click makes);
|
||||||
|
// * with none -> "bar only": the dashboard stays open with no active frame,
|
||||||
|
// just the dashboard bar (a docked frame renders only while active).
|
||||||
|
// Stock SteamVR has that state (e.g. when a VRLink remote frame vanishes)
|
||||||
|
// but never keeps it, so while bar-only is on and no frame is active:
|
||||||
|
// - an instance override of Dashboard.autoSwitchOverlayIfNeeded returns
|
||||||
|
// early (its onDashboardTabsUpdated autorun, and reopening the dashboard
|
||||||
|
// with the Steam button, would otherwise switch right back to Steam);
|
||||||
|
// - an instance override of Dashboard.onShowOverlayRequestFromSteam drops
|
||||||
|
// the FIRST ShowOverlay("valve.steam.gamepadui.main") from Steam after each
|
||||||
|
// dashboard open, within STEAM_SHOW_CAP_MS (Steam's main VR window sends
|
||||||
|
// it by itself 50 ms .. 1.7 s after the open). Trade-off: should Steam not
|
||||||
|
// send it, the user's first explicit Steam-menu pick (e.g. Library) in
|
||||||
|
// that session is swallowed once; a second one works.
|
||||||
|
// A Steam tab click (DashboardTabClicked -> switchToFrameInternal) and
|
||||||
|
// SteamVR's own CVRSteamPrivate::SwitchToDashboardOverlay path aren't
|
||||||
|
// filtered. Bar-only ends as soon as any frame becomes active.
|
||||||
|
//
|
||||||
|
// State that must outlive this instance (bar-only flag, last open time,
|
||||||
|
// pending Steam request, frame history, suppression log) is kept in
|
||||||
|
// window.__sfuiSteamCloseState (schema 1, shared with the earlier personal
|
||||||
|
// version 4 of this patch, whose instance is handed over seamlessly).
|
||||||
|
// Teardown (unpatch.js, a newer VERSION) only removes overrides, reactions
|
||||||
|
// and markers: it never switches frames and leaves the state, so a
|
||||||
|
// re-injection (service restart, switch) continues bar-only. It is in-page
|
||||||
|
// only: a dashboard (vrwebhelper) reload starts fresh (see loadState).
|
||||||
|
//
|
||||||
|
// Without the Dashboard component, X hides the dashboard. RequestClose shows
|
||||||
|
// a spinner after onCloseRequested (it expects the frame to go away); it is
|
||||||
|
// reset right after. A MobX reaction keeps the history of active frames and
|
||||||
|
// re-applies when the main frame changes (the 15 s re-injection covers the
|
||||||
|
// rest). window.__sfuiSteamClose: plan() (what a click would do now, no side
|
||||||
|
// effects), wouldIgnoreSteamShow(), barOnly, state, teardown().
|
||||||
|
// Results: "patched" / "unchanged", with " (bar-only, N ignored Steam
|
||||||
|
// requests)" while there is something to report, so the injector logs it.
|
||||||
|
((find, sigs, opts) => {
|
||||||
|
const NAME = 'steam-close-button';
|
||||||
|
const VERSION = 5;
|
||||||
|
const MAIN_KEY = 'valve.steam.gamepadui.main'; // anchored by sigs.overlayKeys
|
||||||
|
const FRAME_ALIVE = 2; // frame m_eState Alive (sigs.frame)
|
||||||
|
const STEAM_SHOW_CAP_MS = 10000;
|
||||||
|
const REASON = 'sfui steam-close-button';
|
||||||
|
|
||||||
|
const prev = window.__sfuiSteamClose;
|
||||||
|
if (prev?.version === VERSION) return prev.apply();
|
||||||
|
|
||||||
|
const DS = window.DashboardStore, FS = window.FrameStore;
|
||||||
|
if (!DS || !FS) return 'not patched: window.DashboardStore/FrameStore missing';
|
||||||
|
let mods;
|
||||||
|
try {
|
||||||
|
mods = find.resolveAll(find.getWebpackRequire('webpackChunkvrwebui'), sigs);
|
||||||
|
} catch (e) {
|
||||||
|
return `signature not found, dashboard left unpatched: ${e.message}`;
|
||||||
|
}
|
||||||
|
const mx = mods.mobx.module;
|
||||||
|
if (typeof mx.runInAction !== 'function' || typeof mx.reaction !== 'function')
|
||||||
|
return 'signature not found, dashboard left unpatched: mobx.runInAction/reaction';
|
||||||
|
const Closing = mods.closing.exports.Closing;
|
||||||
|
const DOCK_DASHBOARD = mods.dock.exports.EDockLocation.Dashboard;
|
||||||
|
|
||||||
|
prev?.teardown?.(); // other VERSION; never switches frames
|
||||||
|
|
||||||
|
// Page state. To keep bar-only across dashboard reloads, load/save it here
|
||||||
|
// (e.g. localStorage; shownAt is performance.now() of this page).
|
||||||
|
const loadState = () => {
|
||||||
|
const s = window.__sfuiSteamCloseState;
|
||||||
|
if (s?.schema === 1) return s;
|
||||||
|
return (window.__sfuiSteamCloseState = {
|
||||||
|
schema: 1, barOnly: false, shownAt: -Infinity, steamShowPending: false,
|
||||||
|
history: [], ignored: 0, suppressed: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const S = loadState();
|
||||||
|
if (S.barOnly && DS.activeFrame != null) S.barOnly = false; // a frame became active meanwhile
|
||||||
|
|
||||||
|
// Recently active frame IDs, most recent last.
|
||||||
|
const note = (id) => {
|
||||||
|
if (id == null) return;
|
||||||
|
const h = S.history, i = h.indexOf(id);
|
||||||
|
if (i >= 0) h.splice(i, 1);
|
||||||
|
h.push(id);
|
||||||
|
if (h.length > 32) h.shift();
|
||||||
|
};
|
||||||
|
note(DS.activeFrameID);
|
||||||
|
|
||||||
|
const fallbackFrame = (main) => {
|
||||||
|
for (let i = S.history.length - 1; i >= 0; i--) {
|
||||||
|
const f = FS.GetFrame(S.history[i]);
|
||||||
|
if (f && f.frameID !== main.frameID && f.m_eState === FRAME_ALIVE &&
|
||||||
|
f.tab?.visibleInDashboardBar && f.docking?.dockLocation === DOCK_DASHBOARD) return f;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// What a click on the main frame's X would do: { action, dock, to?, title? }.
|
||||||
|
const plan = (frame = DS.mainSteamFrame) => {
|
||||||
|
if (!frame) return { action: 'none', reason: 'no main frame' };
|
||||||
|
const dock = !!frame.docking && frame.docking.dockLocation !== DOCK_DASHBOARD;
|
||||||
|
if (!frame.isActiveDashboardFrame) return { action: dock ? 'dock' : 'none', dock };
|
||||||
|
const other = fallbackFrame(frame);
|
||||||
|
if (other && window.Dashboard?.switchToFrameInternal) return { action: 'switch', dock, to: other.frameID, title: other.title };
|
||||||
|
if (window.Dashboard && DS._setActiveFrame) return { action: 'bar-only', dock };
|
||||||
|
return { action: DS.isFullyVisible ? 'hide' : 'none', dock };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- bar-only: overrides on the Dashboard instance --------------------------
|
||||||
|
const holding = () => S.barOnly && DS.activeFrame == null;
|
||||||
|
const wouldIgnoreSteamShow = (key = MAIN_KEY) => holding() && key === MAIN_KEY && DS.visibilityState !== 0 &&
|
||||||
|
S.steamShowPending && performance.now() - S.shownAt < STEAM_SHOW_CAP_MS;
|
||||||
|
const wrappers = {
|
||||||
|
autoSwitchOverlayIfNeeded: (orig) => function autoSwitchOverlayIfNeeded(...args) {
|
||||||
|
if (holding()) return;
|
||||||
|
return orig.apply(this, args);
|
||||||
|
},
|
||||||
|
onShowOverlayRequestFromSteam: (orig) => function onShowOverlayRequestFromSteam(e, ...rest) {
|
||||||
|
if (e?.overlay_key === MAIN_KEY) {
|
||||||
|
const ignore = wouldIgnoreSteamShow(MAIN_KEY);
|
||||||
|
S.steamShowPending = false;
|
||||||
|
if (ignore) {
|
||||||
|
S.ignored++;
|
||||||
|
S.suppressed.push({ at: new Date().toISOString(), afterOpenMs: Math.round(performance.now() - S.shownAt), key: MAIN_KEY });
|
||||||
|
if (S.suppressed.length > 10) S.suppressed.shift();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orig.call(this, e, ...rest);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let guarded = null; // Dashboard instance carrying the overrides
|
||||||
|
// Installs the overrides (idempotent); true if autoSwitchOverlayIfNeeded,
|
||||||
|
// the one bar-only needs, is overridden.
|
||||||
|
const guard = () => {
|
||||||
|
const dash = window.Dashboard;
|
||||||
|
if (!dash) return false;
|
||||||
|
if (guarded && guarded !== dash) unguard(); // Dashboard remounted
|
||||||
|
guarded = dash;
|
||||||
|
for (const [name, make] of Object.entries(wrappers)) {
|
||||||
|
const cur = dash[name];
|
||||||
|
if (typeof cur !== 'function' || (cur.__sfuiPatch === NAME && cur.__sfuiVersion === VERSION)) continue;
|
||||||
|
const own = Object.getOwnPropertyDescriptor(dash, name);
|
||||||
|
if (own && !own.writable && !own.configurable) continue;
|
||||||
|
const ours = cur.__sfuiPatch === NAME; // left by another version
|
||||||
|
const orig = ours ? cur.__sfuiOrig : cur;
|
||||||
|
dash[name] = Object.assign(make(orig), {
|
||||||
|
__sfuiPatch: NAME, __sfuiVersion: VERSION, __sfuiOrig: orig, __sfuiHadOwn: ours ? cur.__sfuiHadOwn : !!own,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dash.autoSwitchOverlayIfNeeded?.__sfuiVersion === VERSION && dash.autoSwitchOverlayIfNeeded.__sfuiPatch === NAME;
|
||||||
|
};
|
||||||
|
const unguard = () => {
|
||||||
|
const d = guarded;
|
||||||
|
guarded = null;
|
||||||
|
for (const name of Object.keys(wrappers)) {
|
||||||
|
const f = d?.[name];
|
||||||
|
if (f?.__sfuiPatch !== NAME) continue;
|
||||||
|
if (f.__sfuiHadOwn) d[name] = f.__sfuiOrig;
|
||||||
|
else delete d[name];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = (frame) => {
|
||||||
|
const p = plan(frame);
|
||||||
|
if (p.dock) frame.docking.SetDockLocation(DOCK_DASHBOARD);
|
||||||
|
const dash = window.Dashboard;
|
||||||
|
switch (p.action) {
|
||||||
|
case 'switch':
|
||||||
|
dash.switchToFrameInternal(FS.GetFrame(p.to), undefined, REASON);
|
||||||
|
break;
|
||||||
|
case 'bar-only':
|
||||||
|
if (!guard()) { dash.hideDashboard?.(REASON, false); break; }
|
||||||
|
S.barOnly = true;
|
||||||
|
S.steamShowPending = false;
|
||||||
|
DS._setActiveFrame(undefined, undefined);
|
||||||
|
break;
|
||||||
|
case 'hide':
|
||||||
|
if (dash?.hideDashboard) dash.hideDashboard(REASON, false);
|
||||||
|
else window.VRHTML?.VRDashboardManager?.HideDashboard(REASON, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- the button: defaultComponentProps of the main frame's `closing` --------
|
||||||
|
let closing = null; // the component we changed
|
||||||
|
const nudge = (frame) => mx.runInAction(() => { if (frame?.props) frame.props = { ...frame.props }; });
|
||||||
|
const resetSpinner = (c) => mx.runInAction(() => { c.m_bShowSpinner = false; });
|
||||||
|
|
||||||
|
const patchFrame = (frame) => {
|
||||||
|
const c = frame.closing;
|
||||||
|
if (!(c instanceof Closing)) return 'not patched: main frame has no closing component';
|
||||||
|
const cur = c.defaultComponentProps;
|
||||||
|
closing = c;
|
||||||
|
if (cur?.__sfuiPatch === NAME && cur.__sfuiVersion === VERSION) return 'unchanged';
|
||||||
|
const orig = cur?.__sfuiPatch === NAME ? cur.__sfuiOrig : cur;
|
||||||
|
const props = {
|
||||||
|
...orig,
|
||||||
|
onCloseRequested: () => {
|
||||||
|
try { onClose(frame); } finally { setTimeout(() => resetSpinner(c), 0); } // set after we return
|
||||||
|
},
|
||||||
|
};
|
||||||
|
for (const [k, value] of [['__sfuiPatch', NAME], ['__sfuiVersion', VERSION], ['__sfuiOrig', orig]])
|
||||||
|
Object.defineProperty(props, k, { value, configurable: true }); // non-enumerable: not merged
|
||||||
|
c.defaultComponentProps = props;
|
||||||
|
nudge(frame);
|
||||||
|
return 'patched';
|
||||||
|
};
|
||||||
|
const unpatchFrame = () => {
|
||||||
|
const c = closing;
|
||||||
|
closing = null;
|
||||||
|
const cur = c?.defaultComponentProps;
|
||||||
|
if (cur?.__sfuiPatch !== NAME) return;
|
||||||
|
c.defaultComponentProps = cur.__sfuiOrig;
|
||||||
|
if (c.frame?.m_eState !== FRAME_ALIVE) return;
|
||||||
|
resetSpinner(c);
|
||||||
|
nudge(c.frame);
|
||||||
|
};
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
const f = DS.mainSteamFrame;
|
||||||
|
if (closing && closing.frame !== f) unpatchFrame(); // main frame recreated
|
||||||
|
guard();
|
||||||
|
if (!f) return 'main Steam frame not found';
|
||||||
|
const r = patchFrame(f);
|
||||||
|
const notes = [S.barOnly && 'bar-only', S.ignored && `${S.ignored} ignored Steam request${S.ignored === 1 ? '' : 's'}`]
|
||||||
|
.filter(Boolean);
|
||||||
|
return notes.length ? `${r} (${notes.join(', ')})` : r;
|
||||||
|
};
|
||||||
|
|
||||||
|
const disposers = [
|
||||||
|
mx.reaction(() => DS.activeFrameID, note),
|
||||||
|
mx.reaction(() => DS.activeFrameID != null, (active) => {
|
||||||
|
if (active) { S.barOnly = false; S.steamShowPending = false; }
|
||||||
|
}),
|
||||||
|
mx.reaction(() => DS.visibilityState !== 0, (shown) => {
|
||||||
|
if (shown) { S.shownAt = performance.now(); S.steamShowPending = true; }
|
||||||
|
}),
|
||||||
|
mx.reaction(() => DS.m_unMainSteamFrameID, () => { try { apply(); } catch (e) { console.error(NAME, e); } }),
|
||||||
|
];
|
||||||
|
const teardown = () => {
|
||||||
|
for (const d of disposers.splice(0)) d();
|
||||||
|
unpatchFrame();
|
||||||
|
unguard();
|
||||||
|
if (window.__sfuiSteamClose === self) delete window.__sfuiSteamClose;
|
||||||
|
};
|
||||||
|
const self = window.__sfuiSteamClose = {
|
||||||
|
version: VERSION, apply, teardown, plan, wouldIgnoreSteamShow, state: S,
|
||||||
|
get barOnly() { return S.barOnly; },
|
||||||
|
};
|
||||||
|
return apply().replace(/^unchanged/, 'patched');
|
||||||
|
})
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Reverts steam-close-button/patch.js: removes its overrides, reactions and
|
||||||
|
// markers (the X disappears). Teardown only: it never switches frames and
|
||||||
|
// leaves window.__sfuiSteamCloseState (bar-only flag etc.) in place, so a
|
||||||
|
// re-injection (service restart, switch) continues where it left off; if the
|
||||||
|
// patch is gone for good while bar-only, the dashboard just stays without an
|
||||||
|
// active frame until the next tab click or dashboard open. Safe when not
|
||||||
|
// patched.
|
||||||
|
(() => {
|
||||||
|
const s = window.__sfuiSteamClose;
|
||||||
|
if (!s) return 'not patched';
|
||||||
|
s.teardown();
|
||||||
|
return 'unpatched';
|
||||||
|
})()
|
||||||
@@ -33,6 +33,9 @@
|
|||||||
# windowMaxScale = 4.0;
|
# windowMaxScale = 4.0;
|
||||||
# windowDistance.world.max = 10.0;
|
# windowDistance.world.max = 10.0;
|
||||||
# windowDistance.theater.max = 12.0;
|
# windowDistance.theater.max = 12.0;
|
||||||
|
# # X button on the Steam window: back to the previous window, or
|
||||||
|
# # just the dashboard bar:
|
||||||
|
# steamCloseButton.enable = true;
|
||||||
# };
|
# };
|
||||||
# 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):
|
||||||
|
|||||||
Reference in new issue
Block a user