Files
lhns--steam-frame-nix/modules/steam-ui-patches.nix
T
Pierre Kisters 1ad0dd751e modules/cleanup.nix: orphan cleanup on every switch
Every module imports cleanup.nix (also exported as
homeManagerModules.cleanup), which
- puts steam-frame-nix-cleanup (install.sh cleanup) on PATH;
- runs `cleanup --orphans --quiet` after linkGeneration on every switch,
  keeping what the configuration still uses (steamFrame.cleanup.keep, set
  by the modules: debugger while steamvrDebugger is on, the Firefox desktop
  profile's user.js while its fix applies); in a dry run it runs with
  --dry-run;
- runs `cleanup --all` instead when Home Manager's `uninstall = true;` is
  set (the manual uninstall route);
- removes links of older versions at paths Home Manager is about to own
  before checkLinkTargets (steamFrame.cleanup.migrateLinks), which would
  count as collisions otherwise.

install.sh cleanup gains --quiet (only actions, deferrals and warnings;
the header only when something is printed). The Jellyfin module's own
removal of the old shim copy goes: cleanup does it.
2026-09-29 00:01:30 +02:00

160 lines
5.5 KiB
Nix

# Runtime patches of Steam's and SteamVR's web UIs over local CEF DevTools
# (Steam 127.0.0.1:8080; SteamVR's vrwebhelper 127.0.0.1:8087, see
# steamvr-debugger.nix). The steam-ui-patches service (injector.mjs) injects
# each registered patch into its target pages, re-injects on reload and every
# 15 s, and runs the unpatches on stop; Steam's files are untouched.
# It exists while `patches` is non-empty, restarts on every switch (the old
# instance reverts removed patches) and is stopped once the list is empty.
# Patches with `state = true` get a persistent JSON value (injector.mjs:
# "Persistent state"). Patch calling convention (mkPatch): lib/default.nix.
{ config, pkgs, lib, ... }:
let
cfg = config.steamFrame.uiPatches;
inherit (lib) mkOption types;
patchType = types.submodule {
options = {
name = mkOption {
type = types.strMatching "[A-Za-z0-9_.-]+";
example = "launcher-menu-order";
description = "Unique name, used in the service's log.";
};
endpoint = mkOption {
type = types.str;
default = "http://127.0.0.1:8080";
example = "http://127.0.0.1:8087";
description = ''
DevTools base URL (its /json/list is polled): Steam 8080, SteamVR
8087.
'';
};
target = {
title = mkOption {
type = types.nullOr types.str;
default = null;
example = "SharedJSContext";
description = "Exact page title of the targets to patch.";
};
titleRegex = mkOption {
type = types.nullOr types.str;
default = null;
description = "JavaScript regex the page title must match.";
};
urlRegex = mkOption {
type = types.nullOr types.str;
default = null;
description = "JavaScript regex the page URL must match.";
};
};
patch = mkOption {
type = types.path;
description = ''
JavaScript evaluated (awaited) in every matching page after reloads
and every 15 s, so it must be idempotent. Its result is logged when
it changes ("unchanged" never is). Build with `lib.mkPatch` to get
the finder library.
'';
};
unpatch = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
JavaScript that reverts the patch when the service stops; must be
safe on an unpatched page.
'';
};
state = mkOption {
type = types.bool;
default = false;
description = ''
Give the patch one persistent JSON value (e.g. choices made in its
UI), kept by the service in
$XDG_STATE_HOME/steam-frame-nix/ui-patches/<name>.json across
SteamVR restarts and reboots: the page reads it with
`window.__sfuiStore.get(name)` and writes it with
`window.__sfuiStore.set(name, value)` (see injector.mjs). The file
is user data and stays when the patch is removed.
'';
};
};
};
entry = p: {
inherit (p) name endpoint patch unpatch state;
target = lib.filterAttrs (_: v: v != null) p.target;
};
configFile = pkgs.writeText "steam-ui-patches.json" (builtins.toJSON {
pollMs = 5000;
reinjectMs = 15000;
debounceMs = 3000;
unpatchTimeoutMs = 2000;
patches = map entry cfg.patches;
});
in {
imports = [ ./session.nix ./cleanup.nix ];
options.steamFrame.uiPatches.lib = mkOption {
type = types.attrsOf types.raw;
readOnly = true;
default = import ./lib { inherit pkgs; };
defaultText = lib.literalMD "the helpers of `modules/lib`";
description = ''
Patch helpers from modules/lib/default.nix: `mkPatch { name, src,
signatures ? …, opts ? { } }` (calls `src` as
`(find, sigs, opts, hooks) => …`; see there), plus `finders`, `hooks`
(paths) and `signatures` (parsed signatures.json).
'';
};
options.steamFrame.uiPatches.patches = mkOption {
type = types.listOf patchType;
default = [ ];
example = lib.literalExpression ''
[ {
name = "my-patch";
target.title = "SharedJSContext";
patch = ./my-patch/patch.js;
unpatch = ./my-patch/unpatch.js;
} ]
'';
description = ''
Runtime patches of Steam's web UIs, kept injected by the
steam-ui-patches service and reverted when it stops. Every page
matching all given target criteria is patched.
'';
};
config = lib.mkMerge [
(lib.mkIf (cfg.patches != [ ]) {
assertions = [ {
assertion = lib.allUnique (map (p: p.name) cfg.patches);
message = "steamFrame.uiPatches.patches: names must be unique.";
} ];
systemd.user.services.steam-ui-patches = {
Unit.Description = "Runtime patches of Steam's UI (CEF DevTools)";
Service = {
ExecStart = lib.escapeShellArgs [
"${pkgs.nodejs}/bin/node"
"${./steam-ui-patches/injector.mjs}"
"${configFile}"
];
Restart = "always";
RestartSec = 5;
TimeoutStopSec = 5; # the injector unpatches Steam's UI on SIGTERM
};
Install.WantedBy = [ "default.target" ];
};
# Restart on switch: re-inject changed patches; the old instance reverts
# removed ones.
steamFrame.session.services.restart = [ "steam-ui-patches.service" ];
})
# No patches: stopping the injector reverts them right away.
(lib.mkIf (cfg.patches == [ ]) {
steamFrame.session.services.stop = [ "steam-ui-patches.service" ];
})
];
}