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:
Pierre Kisters committed 2026-09-27 20:55:39 +02:00
1 parent 681533027d
commit 01645ffcf6
7 files changed
+470 -3

No files matched your search

+70 -3
View File
@@ -4,7 +4,7 @@
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
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
home-manager generation.
@@ -141,6 +141,7 @@ the two files below).
dashboard = {
windowMaxScale = 4.0;
windowDistance.world.max = 10.0;
steamCloseButton.enable = true;
};
firefox.enable = true;
# 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
`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.
**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.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.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.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. |
@@ -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
`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`)
Patches of the SteamVR dashboard need its DevTools port, which SteamVR only
opens with the setting `VRWebHelper/DebuggerEnabled` (port
`VRWebHelper/DebuggerPort`, default 8087). It is enabled automatically
(`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
and on exit, so the file can't be managed by Home Manager, and edits while it
+1
View File
@@ -21,6 +21,7 @@
launcher-menu = ./modules/launcher-menu.nix;
steamvr-debugger = ./modules/steamvr-debugger.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.
# The key lets the module system deduplicate it when it is imported both
# directly and through `default`.
+60
View File
@@ -156,6 +156,66 @@
"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\""] }
}
}
}
}
}
+46
View File
@@ -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;
} ];
};
}
+277
View File
@@ -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');
})
+13
View File
@@ -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';
})()
+3
View File
@@ -33,6 +33,9 @@
# windowMaxScale = 4.0;
# windowDistance.world.max = 10.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
# # Hidden from the "+" menu (with Steam Developer Mode on):