Nix option docs and module comments: concise

Descriptions say what, default and notable caveats; the mkPatch calling
convention is referenced from lib/default.nix instead of repeated.
This commit is contained in:
Pierre Kisters committed 2026-09-27 23:59:15 +02:00
1 parent 4c843c3ce4
commit 65f5ccde9b
15 files changed
+210 -401

No files matched your search

+1 -3
View File
@@ -24,9 +24,7 @@
steam-close-button = ./modules/steam-close-button.nix;
window-curvature = ./modules/window-curvature.nix;
frame-controls = ./modules/frame-controls.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`.
# Built with the consumer's pkgs; `key` dedups direct + `default` imports.
clipboard-sync = {
key = "steam-frame-nix/clipboard-sync";
_file = ./modules/clipboard-sync.nix;
+10 -16
View File
@@ -1,10 +1,7 @@
# clipboard-sync: bridges the clipboards of the Steam session's X displays and
# the nested Plasma desktop (own Wayland + Xwayland :2).
# Started via KDE autostart, not systemd: the nested desktop has no access to
# the user systemd instance, and clipboard-sync must start after :2 exists.
#
# Built from source with the consumer's pkgs (the upstream flake outputs are
# x86-only); flake.nix passes the source in through this closure.
# clipboard-sync between the Steam session's X displays and the nested desktop
# (Xwayland :2). KDE autostart, not systemd: the nested desktop can't reach the
# user manager, and :2 must exist first. Built from source (upstream flake is
# x86-only); flake.nix passes the source in.
{ clipboard-sync-src }:
{ config, pkgs, lib, ... }:
let
@@ -15,8 +12,8 @@ in {
type = lib.types.bool;
default = true; # without it the nested desktop's clipboard is isolated
description = ''
Whether to run clipboard-sync between the Steam session and the nested
desktop (KDE autostart; on switch, stale builds and duplicates are stopped).
Run clipboard-sync between the Steam session and the nested desktop
(KDE autostart; switch stops stale builds and duplicates).
'';
};
package = lib.mkOption {
@@ -46,13 +43,10 @@ in {
NoDisplay=true
'';
# Keep exactly one instance of the current build, preferably one started in
# the nested desktop (XDG_CURRENT_DESKTOP=KDE): it inherits the process
# environment, so a copy started from a Steam-session terminal runs with
# the wrong session's env. Stale builds and duplicate instances are
# stopped (workers of an instance are left alone). A new
# instance is only started when switching from the nested desktop;
# otherwise the autostart entry starts it with the desktop.
# Keep one instance of the current build, preferably one started in the
# nested desktop (it inherits the environment; one from a Steam-session
# terminal has the wrong one). Kill stale builds and duplicates; start a new
# one only when switching from the nested desktop (else autostart does).
home.activation.startClipboardSync = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
want="${cfg.package}/bin/clipboard-sync"
desktop=() other=()
+11 -20
View File
@@ -1,13 +1,7 @@
# Resize and grab-distance limits of SteamVR dashboard windows (Steam, app
# windows, overlays, the theater screen, the dashboard itself), through a
# runtime patch of SteamVR's dashboard page (vrwebhelper "systemui", DevTools
# 127.0.0.1:8087; see dashboard-windows/patch.js for how it works). It is
# registered with steam-ui-patches.nix only when an option is set, and turns
# on the SteamVR web helper debugger (steamvr-debugger.nix), which needs one
# SteamVR restart the first time. Changes apply immediately (the dashboard
# resends its scene graph); unsetting all options reverts to stock.
# Depends on SteamVR UI internals, found by signature (lib/signatures.json,
# "dashboard-windows"; scripts/check-signatures.mjs checks them offline).
# Resize and grab-distance limits of SteamVR dashboard windows. Dashboard patch
# (dashboard-windows/patch.js; "systemui" on 127.0.0.1:8087), registered only
# when an option is set; enables steamvr-debugger.nix (one SteamVR restart the
# first time). Unsetting all options reverts to stock.
{ config, lib, pkgs, ... }:
let
cfg = config.steamFrame.dashboard;
@@ -26,8 +20,8 @@ let
type = types.nullOr types.number;
default = null;
description = ''
Closest distance in meters ${what} can be pulled in to while grabbed.
null = stock (${toString stockDistance.${kind}.min} m).
Closest distance (m) ${what} can be pulled in to while grabbed; null =
stock (${toString stockDistance.${kind}.min} m).
'';
};
max = mkOption {
@@ -35,8 +29,8 @@ let
default = null;
example = stockDistance.${kind}.max * 2;
description = ''
Farthest distance in meters ${what} can be pushed back to while
grabbed. null = stock (${toString stockDistance.${kind}.max} m).
Farthest distance (m) ${what} can be pushed back to while grabbed;
null = stock (${toString stockDistance.${kind}.max} m).
'';
};
};
@@ -57,12 +51,9 @@ in {
default = null;
example = 4.0;
description = ''
Largest size, relative to their default size, that SteamVR dashboard
windows (Steam, app windows, overlays, the theater screen) can be
enlarged to with the resize handle. null = stock (2; the smallest is
0.25). The theater screen's default size is 2.8x that of a normal
window, so its limit is 2.8x this value. Runtime patch of the SteamVR
dashboard (steamFrame.uiPatches), applied immediately.
Largest resize factor of SteamVR dashboard windows, relative to their
default size; null = stock (2). The theater screen starts 2.8x larger,
so its limit is 2.8x this. Dashboard patch, applied immediately.
'';
};
windowDistance = {
+15 -25
View File
@@ -1,22 +1,13 @@
# Firefox (Flathub Flatpak org.mozilla.firefox) on the Frame.
#
# vrFullscreenFix: real fullscreen is broken in the Steam session: gamescope
# gives the fullscreen X11 window input focus but never shows it, so Firefox
# looks frozen. With ignore-widgets, fullscreen (e.g. YouTube) only fills the
# Firefox window itself, which in VR can be made as large as you like.
# Profile names are random, and flakes can't read $HOME at eval time, so the
# user.js is linked into every existing profile on each switch.
#
# desktopProfile: the Steam session and the nested desktop have separate D-Bus
# buses and displays, so a second Firefox can't find the running one and
# stops at the locked profile. In the nested desktop (XDG_CURRENT_DESKTOP=KDE)
# the launcher uses its own profile instead (the launcher creates the dir,
# Firefox fills it). It gets no user.js: real fullscreen works there.
#
# The launcher's desktop entry shadows the Flatpak's own entry (same ID), so
# MIME/default-browser associations for org.mozilla.firefox.desktop still
# apply, and the Steam "+" menu (which only reads ~/.local/share/applications)
# sees it.
# Firefox Flatpak (org.mozilla.firefox) launcher.
# - vrFullscreenFix: in the Steam session gamescope focuses a fullscreen X11
# window but never shows it (Firefox looks frozen). ignore-widgets keeps
# fullscreen inside the window. Profile names are random and $HOME isn't
# readable at eval time, so user.js is linked into each profile on switch.
# - desktopProfile: the sessions have separate buses/displays, so a second
# Firefox can't reach the running one and hits the profile lock; the nested
# desktop gets its own profile (no user.js: fullscreen works there).
# The entry shadows the Flatpak's (same ID), keeping MIME associations, and is
# seen by the "+" menu (which reads only ~/.local/share/applications).
{ config, pkgs, lib, ... }:
let
cfg = config.steamFrame.firefox;
@@ -48,18 +39,17 @@ in {
type = lib.types.bool;
default = true;
description = ''
Link a user.js setting full-screen-api.ignore-widgets into every
existing Firefox profile (except the desktop profile), so fullscreen
fills only the Firefox window instead of freezing in the Steam session.
Link a user.js (full-screen-api.ignore-widgets) into every existing
profile except the desktop one, so fullscreen fills the window instead
of freezing in the Steam session.
'';
};
desktopProfile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "desktop";
description = ''
Name of the separate profile the launcher uses in the nested desktop
(XDG_CURRENT_DESKTOP=KDE), so both sessions can run Firefox at once.
null uses the default profile in both sessions.
Profile used in the nested desktop, so both sessions can run Firefox at
once; null = default profile in both.
'';
};
};
+16 -31
View File
@@ -1,16 +1,7 @@
# Window control bar (steamFrame.dashboard.frameControls): move the control
# icons under SteamVR dashboard windows between the bottom bar and the More
# Options (three-dot) menu with a long press on the icon or menu row (a popup
# with "Show in bar"). Placement is per control type, for all windows, kept
# across SteamVR restarts; inBar / inMenu set defaults. Optionally gives
# theater windows the "Float" control back. Runtime patch of SteamVR's
# dashboard page (vrwebhelper "systemui", DevTools 127.0.0.1:8087; see
# frame-controls/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, "frame-controls"; scripts/check-signatures.mjs checks
# them offline).
# Move window controls between a dashboard window's bottom bar and its
# three-dot menu (long press). Dashboard patch (frame-controls/patch.js;
# "systemui" on 127.0.0.1:8087), registered only when enabled; enables
# steamvr-debugger.nix (one SteamVR restart the first time).
{ config, lib, pkgs, ... }:
let
cfg = config.steamFrame.dashboard.frameControls;
@@ -31,28 +22,23 @@ in {
options.steamFrame.dashboard.frameControls = {
enable = lib.mkEnableOption ''
moving the control icons under SteamVR dashboard windows between the
window's bottom bar and its More Options (three-dot) menu: a long press
on a bar icon or a menu row opens a popup with "Show in bar"; the choice
applies to that control in all windows and is kept across SteamVR
restarts. Short presses work as usual. Runtime patch of the SteamVR
dashboard (steamFrame.uiPatches), applied immediately; turning it off
restores the stock controls (next switch)'';
moving SteamVR dashboard window controls between the bottom bar and the
three-dot menu: long press an icon or menu row for a "Show in bar"
popup. Applies to that control in all windows, kept across SteamVR
restarts. Dashboard patch; off restores stock (next switch)'';
longPressMs = mkOption {
type = types.ints.between 300 10000;
default = 1500;
description = ''
Hold time (ms) of the long press that opens the popup. A ring around
the icon shows the progress from half the time (at most after 1 s),
so ordinary clicks show nothing.
Long-press time (ms) to open the popup. A progress ring appears after
half of it (at most 1 s), so ordinary clicks show nothing.
'';
};
inBar = controlList ''
Controls that start in the bar (by default: SteamVR's placement).
Names: ${lib.concatStringsSep ", " names}, or "icon:<n>" (the action's
icon number, see window.__sfuiFrameControls.dump() in the dashboard's
DevTools). A choice made in the popup wins until the control's entry
here changes.
Controls that start in the bar (default: SteamVR's placement):
${lib.concatStringsSep ", " names}, or "icon:<n>" (see
window.__sfuiFrameControls.dump() in the dashboard's DevTools). A popup
choice wins until the control's entry here changes.
'';
inMenu = controlList ''
Controls that start in the three-dot menu (same names as inBar).
@@ -61,9 +47,8 @@ in {
type = types.bool;
default = false;
description = ''
Give windows in the theater the "Float" control (SteamVR only shows it
for windows docked in the dashboard): floats the window in the world,
like the stock button. Follows Float's placement.
Give theater windows the "Float" control too (stock: dashboard-docked
windows only). Follows Float's placement.
'';
};
};
+2 -5
View File
@@ -1,8 +1,5 @@
# Hide system apps from menus, including the Steam session's "+" menu (with
# Developer Mode on, or launcherMenu.showAllApps, it lists every desktop entry
# GLib would show). A user
# entry with Hidden=true in ~/.local/share/applications masks the one in
# /usr/share/applications. Also hides them from the nested desktop's KDE menu.
# Hide desktop entries from the "+" menu and the KDE menu: a user entry with
# Hidden=true masks the system one.
{ config, lib, ... }: {
options.steamFrame.hiddenApps = lib.mkOption {
type = lib.types.listOf lib.types.str;
+5 -8
View File
@@ -1,9 +1,7 @@
# Keyboard layout for the Steam session. gamescope (and its Xwayland displays
# :0/:1) uses xkbcommon defaults, i.e. US, unless XKB_DEFAULT_* is set. KDE's
# layout setting only applies to the nested desktop. systemd --user on the
# Frame does not pick up ~/.config/environment.d, so set it directly on the
# unit that launches gamescope. Takes effect the next time the Steam session
# starts.
# Keyboard layout for the Steam session: gamescope uses xkbcommon defaults (US)
# unless XKB_DEFAULT_* is set; KDE's setting only covers the nested desktop.
# environment.d isn't read on the Frame, so it goes on gamescope-session.service
# (next session start).
{ config, lib, ... }:
let
cfg = config.steamFrame;
@@ -14,8 +12,7 @@ in {
default = null;
example = "de";
description = ''
XKB layout for the Steam session (XKB_DEFAULT_LAYOUT). null leaves
gamescope's default (US) and writes no drop-in.
XKB layout for the Steam session (XKB_DEFAULT_LAYOUT); null = US.
'';
};
keyboardVariant = lib.mkOption {
+48 -85
View File
@@ -1,37 +1,19 @@
# The VR dashboard's "+" menu (non-Steam programs, #VRDashboard_LaunchNonSteamApp)
# lists programs in the order SteamClient.Apps.ScanForInstalledNonSteamApps()
# returns them: GLib hash-table order, effectively random, with "Desktop" (the
# nested Plasma session) somewhere in a scrolling list. Runtime patches of
# Steam's UI (steam-ui-patches.nix, evaluated in SharedJSContext), each
# registered only when its option is set:
# - order/: wraps ScanForInstalledNonSteamApps to sort the list by name;
# - 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
# original;
# - 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);
# - grid/: restyles the programs section as a grid of tiles (icon, name
# below), optionally limited to maxRows visible rows;
# - show-all/: empties the list of programs Steam hides without Developer
# Mode (the webpack module holding it is found by signature).
# Options reach the patches through mkPatch's `opts`. All are reverted when
# turned off (next switch). Besides that one module, they depend on Steam UI
# internals such as React props, popup names and the scroll fade's classes
# and CSS; scripts/check-signatures.mjs checks all of them after a Steam
# update (lib/signatures.json). Tested with Steam client 1790377368.
# The VR dashboard's "+" menu (non-Steam programs). Steam lists them in GLib
# hash-table order (random-looking). Runtime patches in SharedJSContext, each
# registered only when its option is set, reverted when unset (next switch):
# - order/: sorts ScanForInstalledNonSteamApps() by name;
# - pinned-desktop/: hides Desktop in the list, pins a proxy above/below it;
# - launch/: wraps LaunchNonSteamApp (menu-only) to close the menu and/or
# debounce repeated launches;
# - grid/: programs as a grid of tiles, optionally maxRows visible;
# - show-all/: empties the list Steam hides without Developer Mode.
# Tested with Steam client 1790377368.
#
# iconFallbacks is not a patch: Steam's scan of host programs resolves a
# desktop entry's Icon= name only in the hicolor icon theme (and pixmaps),
# so programs whose icon exists only in the desktop's Breeze theme (SteamOS'
# Konsole and KDE System Settings) have no icon in the menu. On every switch,
# launcher-menu/icon-fallbacks.sh looks through the desktop entries Steam
# sees and links the missing icons that nixpkgs' Breeze has into
# ~/.local/share/icons/hicolor (see the script). Until 2026-09 iconFallbacks
# was a list of names; setting a list now fails with a message pointing to
# enable/extra.
# iconFallbacks (not a patch): Steam resolves Icon= names only in hicolor (and
# pixmaps), so Breeze-only icons (Konsole, KDE System Settings) are missing;
# icon-fallbacks.sh links them from nixpkgs' Breeze into
# ~/.local/share/icons/hicolor on switch. It was a list of names until 2026-09;
# a list now fails with a pointer to enable/extra.
{ config, lib, pkgs, ... }:
let
cfg = config.steamFrame.launcherMenu;
@@ -51,9 +33,7 @@ in {
type = lib.types.bool;
default = false;
description = ''
Sort the VR dashboard's "+" menu (non-Steam programs) alphabetically
instead of Steam's random-looking order. Runtime patch of Steam's UI
(steamFrame.uiPatches).
Sort the "+" menu (non-Steam programs) by name. Runtime patch.
'';
};
pinDesktop = lib.mkOption {
@@ -61,23 +41,17 @@ in {
default = null;
example = "bottom";
description = ''
Pin "Desktop" (the nested Plasma session) above ("top", below the
menu heading) or below ("bottom") the scrolling program list of the
"+" menu, separated by a thin line, so it is always visible; it is
hidden in the list itself. null leaves it a normal list entry. Runtime
patch of Steam's UI (steamFrame.uiPatches).
Pin "Desktop" (the nested Plasma session) above ("top") or below
("bottom") the "+" menu's scrolling list, so it is always visible;
null = normal list entry. Runtime patch.
'';
};
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.
Close the "+" menu as soon as a program is launched (stock: it stays
open until the window appears, inviting double launches). Runtime patch.
'';
};
launchDebounce = lib.mkOption {
@@ -85,22 +59,21 @@ in {
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).
Ignore another "+" menu launch of the same command line within this
many seconds of the last one that went through (logged); 0 = off.
Runtime patch.
'';
};
grid = {
enable = lib.mkEnableOption ''
the "+" menu's programs as a grid of tiles (large icon, name below)
instead of a list. Runtime patch of Steam's UI (steamFrame.uiPatches)'';
the "+" menu's programs as a grid of tiles (icon, name below) instead
of a list. Runtime patch'';
columns = lib.mkOption {
type = lib.types.ints.between 1 8;
default = 4;
description = ''
Tiles per row. The menu popup is a fixed 300 px wide: 3 columns give
tiles of about 92 px, 4 about 68 px, 5 about 53 px.
Tiles per row (the menu is 300 px wide: 3 → ~92 px tiles, 4 → ~68,
5 → ~53).
'';
};
maxRows = lib.mkOption {
@@ -108,9 +81,8 @@ in {
default = null;
example = 4;
description = ''
Rows of tiles visible at once: the menu shrinks to that many rows and
the rest scrolls. null: the grid fills up to the menu's stock maximum
height (600 px).
Rows visible at once (the rest scrolls); null = up to the menu's
stock max height (600 px).
'';
};
};
@@ -118,24 +90,20 @@ in {
type = lib.types.bool;
default = false;
description = ''
List all programs in the "+" menu without Steam's Developer Mode.
Without it, Steam hides a fixed list (konsole, systemsettings,
dolphin, plasma-discover, vlc, firewall-config, cmake-gui, qrenderdoc,
lxterminal, sh). Only that list is emptied; the Developer Mode setting
itself (used by other settings pages) is untouched. Hide individual
programs with steamFrame.hiddenApps. Runtime patch of Steam's UI
(steamFrame.uiPatches).
List all programs in the "+" menu without Developer Mode (Steam
otherwise hides konsole, systemsettings, dolphin, plasma-discover, vlc,
firewall-config, cmake-gui, qrenderdoc, lxterminal, sh). Developer Mode
itself is untouched; hide single programs with steamFrame.hiddenApps.
Runtime patch.
'';
};
iconFallbacks = lib.mkOption {
default = { };
description = ''
Hicolor fallbacks for program icons only the desktop's Breeze theme
has, so the "+" menu shows them: Steam resolves program icons only in
the hicolor theme.
Hicolor links for program icons only Breeze has, so the "+" menu shows
them (Steam looks only in hicolor).
'';
# The option used to be a list of icon names: keep a list from being
# silently misread and fail with a message instead (see the assertion).
# Formerly a list of names: fail with a message instead (see assertion).
type = lib.types.coercedTo (lib.types.listOf lib.types.str)
(names: { legacyList = names; })
(lib.types.submodule {
@@ -144,16 +112,12 @@ in {
type = lib.types.bool;
default = true;
description = ''
On every switch, look through the desktop entries Steam sees
(XDG data dirs, shadowed and Hidden/NoDisplay entries skipped)
and, for each Icon= name no hicolor theme dir (or pixmaps)
has but nixpkgs' Breeze app icons do (SteamOS: Konsole's
utilities-terminal, KDE System Settings' preferences-system),
link the Breeze SVG as
~/.local/share/icons/hicolor/scalable/apps/<name>.svg. Links
no longer needed are removed; only links made by this option
(listed in ~/.local/state/steam-frame-nix/icon-fallbacks) are
ever touched. false removes them all.
On switch, for each Icon= of the desktop entries Steam sees
that hicolor lacks but nixpkgs' Breeze has (SteamOS:
utilities-terminal, preferences-system), link the Breeze SVG
into ~/.local/share/icons/hicolor/scalable/apps. Only its own
links (listed in ~/.local/state/steam-frame-nix/icon-fallbacks)
are touched; stale ones are removed; false removes all.
'';
};
extra = lib.mkOption {
@@ -161,9 +125,8 @@ in {
default = [ ];
example = [ "system-file-manager" ];
description = ''
Icon names to provide even if no desktop entry the scan sees
uses them (still only if hicolor lacks them). A name Breeze
has no app icon for is reported on switch and skipped.
Extra icon names to provide (if hicolor lacks them); names
Breeze doesn't have are reported and skipped.
'';
};
legacyList = lib.mkOption {
@@ -188,8 +151,8 @@ in {
'';
} ];
# After installPackages, so the new profile's desktop entries and icons count.
# Disabled, the script removes its links (no BREEZE_APPS).
# After installPackages so the new profile counts; without BREEZE_APPS the
# script removes its links.
config.home.activation.steamFrameIconFallbacks =
lib.hm.dag.entryAfter [ "writeBoundary" "installPackages" ] (
if cfg.iconFallbacks.enable then ''
+8 -12
View File
@@ -1,11 +1,8 @@
# Workaround for the Steam Frame image (SteamOS 0.3.0, build 20260922):
# the Steam session's xdg-desktop-portal gets XDG_DESKTOP_PORTAL_DIR pointing at
# /usr/share/xdg-desktop-portal/gamescope-portals, which contains the holo and
# gamescope backends but no gamescope-portals.conf (and no UseIn=). With that
# variable set, the portal only reads config from that dir, so it selects no
# backend and offers no OpenURI: no app in the Steam session can open links.
# Fix: our own portal dir = links to Valve's .portal files + a config.
# Remove once SteamOS ships a gamescope-portals.conf.
# Workaround (SteamOS 0.3.0, build 20260922): the Steam session's portal dir
# /usr/share/xdg-desktop-portal/gamescope-portals has backends but no
# gamescope-portals.conf, so no backend is selected and there is no OpenURI
# (links don't open). We point it at our own dir: Valve's .portal files + a
# config. Remove once SteamOS ships the .conf.
{ config, lib, ... }:
let
sys = "/usr/share/xdg-desktop-portal/gamescope-portals";
@@ -15,8 +12,8 @@ in {
type = lib.types.bool;
default = true;
description = ''
Give the Steam session's xdg-desktop-portal a working config
(gamescope-portals.conf) so apps there can open links (OpenURI).
Give the Steam session's xdg-desktop-portal a config so apps there can
open links (OpenURI).
'';
};
@@ -30,8 +27,7 @@ in {
default=holo;gamescope
'';
# Only affects the systemd-managed (outer) portal; the nested desktop's
# portal is D-Bus-activated on its own bus and keeps using kde-portals.conf.
# Outer (systemd) portal only; the nested desktop's keeps kde-portals.conf.
xdg.configFile."systemd/user/xdg-desktop-portal.service.d/gamescope-portals.conf".text = ''
[Service]
Environment=XDG_DESKTOP_PORTAL_DIR=%h/.local/share/${dir}
+12 -24
View File
@@ -1,19 +1,11 @@
# Shared settings for the two sessions on the Steam Frame, and the
# user-services mechanism.
# Settings shared by both sessions, and user-service handling on switch.
#
# The Frame runs the Steam/VR session (gamescope, systemd user manager, outer
# D-Bus at <runtimeDir>/bus) and a nested Plasma desktop with its own
# XDG_RUNTIME_DIR and private D-Bus. Modules that talk to the outer session use
# `runtimeDir` / `userBus` explicitly.
#
# User services: `home-manager switch` usually runs from the nested desktop,
# whose XDG_RUNTIME_DIR and D-Bus can't reach the user manager, so
# home-manager's own reloadSystemd step is skipped ("User systemd daemon not
# running"). The `steamFrameUserServices` activation entry does it instead,
# pointed at the outer session: it always runs `systemctl --user
# daemon-reload` (so changed unit files are picked up even when both lists are
# empty), then starts the units in `userServices.start`, stops those in
# `userServices.stop` and restarts those in `userServices.restart`.
# The Steam/VR session owns the systemd user manager and the outer D-Bus
# (<runtimeDir>/bus); the nested Plasma desktop has its own XDG_RUNTIME_DIR
# and private bus. `switch` usually runs from the nested desktop, where
# home-manager's reloadSystemd is skipped ("User systemd daemon not running"),
# so `steamFrameUserServices` does it against the outer session: always
# daemon-reload, then start/stop/restart the units in `userServices`.
{ config, lib, ... }:
let
cfg = config.steamFrame;
@@ -30,8 +22,8 @@ in {
default = "unix:path=${cfg.runtimeDir}/bus";
defaultText = lib.literalExpression ''"unix:path=''${config.steamFrame.runtimeDir}/bus"'';
description = ''
Address of the outer session D-Bus. The user systemd manager and the
single running kwalletd6 are only reachable over this bus.
Outer session D-Bus address; the only bus that reaches the user
systemd manager and the running kwalletd6.
'';
};
outerBusEnv = lib.mkOption {
@@ -40,9 +32,8 @@ in {
default = "env DBUS_SESSION_BUS_ADDRESS=${cfg.userBus}";
defaultText = lib.literalExpression ''"env DBUS_SESSION_BUS_ADDRESS=''${config.steamFrame.userBus}"'';
description = ''
Command prefix for launchers (desktop entry Exec= lines) that must use
the outer bus, e.g. so apps started from the nested desktop use the
one kwalletd6 instead of starting a second wallet on the private bus.
Exec= prefix for launchers that must use the outer bus, e.g. so apps
in the nested desktop use the running kwalletd6 instead of a second one.
'';
};
userServices = {
@@ -61,10 +52,7 @@ in {
stop = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
User units stopped on switch if still running, e.g. the service of a
feature that was just disabled (its unit file is already gone).
'';
description = "User units stopped on switch if running (e.g. of a just-disabled feature).";
};
};
};
+7 -19
View File
@@ -1,14 +1,6 @@
# 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).
# Close (X) button on the dashboard's Steam window. Dashboard patch
# (steam-close-button/patch.js; "systemui" on 127.0.0.1:8087), registered only
# when enabled; enables steamvr-debugger.nix (one SteamVR restart the first time).
{ config, lib, pkgs, ... }:
let
cfg = config.steamFrame.dashboard.steamCloseButton;
@@ -17,14 +9,10 @@ in {
imports = [ ./steam-ui-patches.nix ./steamvr-debugger.nix ];
options.steamFrame.dashboard.steamCloseButton.enable = lib.mkEnableOption ''
a 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)'';
a Close (X) button on the dashboard's Steam window: docks it back if it
was in the world, then switches to the last active other window, or
leaves only the dashboard bar (until a window is activated, e.g. via the
Steam tab). Dashboard patch; off removes it (next switch)'';
config = lib.mkIf cfg.enable {
steamFrame.uiPatches.patches = [ {
+14 -26
View File
@@ -1,20 +1,12 @@
# Runtime patch of Steam's on-screen (VR) keyboard. Steam's layouts are
# hardcoded in its UI, and in VR it can only send text (no Ctrl/Alt/Esc;
# SteamClient.Input.ControllerKeyboardSetKeyState throws "Unknown method").
# - patch.js is injected into Steam's running UI through its CEF DevTools port
# (127.0.0.1:8080; SteamOS starts Steam with -cef-enable-debugging); Steam's
# files are untouched. Bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ →, with
# AltGr + arrows = Pos1/PgUp/PgDn/End. Chords, Shift+arrows and characters
# Steam's key emulation turns into "1" (non-ASCII, AltGr/dead keys on the
# de keymap) are handed to the helper; Ctrl/Alt are held while toggled.
# - helper.mjs keeps the injection alive (Steam restarts, popup recreated),
# performs those requests with xdotool on :0 (X focus follows the window
# selected in VR) and reverts the patch when stopped (unpatch.js). Its
# allowlist can't type ASCII text or press Enter.
# Depends on Steam UI internals, found by signature (lib/finders.js, entry
# "steam-keyboard-patch" in lib/signatures.json) rather than webpack module
# ids; after a Steam update, scripts/check-signatures.mjs tells whether they
# still match. Tested with Steam client 1790377368 (UI build 11041156).
# Steam's VR keyboard can only send text (ControllerKeyboardSetKeyState throws
# "Unknown method" in VR), and its layouts are hardcoded.
# - patch.js (injected over CEF DevTools, 127.0.0.1:8080) adds a bottom row
# Esc Ctrl Alt [space] AltGr ← ↑ ↓ → (AltGr+arrows = Pos1/PgUp/PgDn/End) and
# hands chords, Shift+arrows and characters Steam would turn into "1"
# (non-ASCII, AltGr/dead keys) to the helper.
# - helper.mjs keeps it injected, sends those keys with xdotool on :0 (an
# allowlist: no ASCII text, no Enter) and unpatches on stop.
# Tested with Steam client 1790377368 (UI build 11041156).
{ config, pkgs, lib, ... }:
let
patch = (import ./lib { inherit pkgs; }).mkPatch {
@@ -25,11 +17,9 @@ in {
imports = [ ./session.nix ];
options.steamFrame.steamKeyboardPatch.enable = lib.mkEnableOption ''
the runtime patch of Steam's on-screen (VR) keyboard: Esc/Ctrl/Alt and four
separate arrow keys, real Ctrl/Alt chords (held while toggled, e.g. for
Ctrl+scroll), and working AltGr/non-ASCII characters. Injected into Steam's
UI through its CEF DevTools port by an xdotool helper service; disabling
it reverts the patch on the next switch'';
Esc/Ctrl/Alt, arrow keys, Ctrl/Alt chords (held while toggled) and
AltGr/non-ASCII characters on Steam's VR keyboard (runtime patch plus an
xdotool helper service); off reverts it on the next switch'';
config = lib.mkMerge [
(lib.mkIf config.steamFrame.steamKeyboardPatch.enable {
@@ -50,12 +40,10 @@ in {
Install.WantedBy = [ "default.target" ];
};
# Restart on every switch so a changed patch is re-injected (it replaces
# the older version).
# Restart on switch to re-inject a changed patch.
steamFrame.userServices.restart = [ "steam-keyboard-patch.service" ];
})
# Disabled: stop a still-running helper, which reverts the patch, so the
# stock keyboard is back right away (no Steam restart or reboot).
# Disabled: stopping the helper reverts the patch right away.
(lib.mkIf (!config.steamFrame.steamKeyboardPatch.enable) {
steamFrame.userServices.stop = [ "steam-keyboard-patch.service" ];
})
+24 -50
View File
@@ -1,24 +1,11 @@
# Runtime patches of Steam's (and SteamVR's) web UIs through their local CEF
# DevTools ports: Steam's client UI on 127.0.0.1:8080 (SteamOS starts Steam
# with -cef-enable-debugging), SteamVR's vrwebhelper on 127.0.0.1:8087 when
# its debugger is enabled. Steam's files are untouched: the
# `steam-ui-patches` service (injector.mjs, Node) evaluates each registered
# patch in its target pages, re-injects it when a page is reloaded or
# recreated (and every 15 s), and evaluates its unpatch expression when the
# service stops, so the UI is back to stock without a Steam restart.
#
# Other modules (and your own config) register patches in
# `steamFrame.uiPatches.patches`; the service exists only while that list is
# non-empty. It is restarted on every switch (changed patches are re-injected,
# removed ones reverted by the old instance) and stopped once the list is
# empty. The keyboard patch (steam-keyboard-patch.nix) has its own helper.
#
# Patches that need Steam's webpack modules should find them by signature
# rather than by module id or minified export name, which change with Steam
# updates: `steamFrame.uiPatches.lib.mkPatch` wraps a patch written as
# `(find, sigs, opts, hooks) => …` with the finder library (lib/finders.js),
# its signatures and the shared method hooks (lib/hooks.js);
# scripts/check-signatures.mjs checks signatures offline.
# 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.
# Patch calling convention (mkPatch): lib/default.nix.
{ config, pkgs, lib, ... }:
let
cfg = config.steamFrame.uiPatches;
@@ -36,8 +23,7 @@ let
default = "http://127.0.0.1:8080";
example = "http://127.0.0.1:8087";
description = ''
DevTools base URL of the CEF instance (its /json/list is polled).
Steam's client UI is on port 8080, SteamVR's vrwebhelper on 8087.
DevTools base URL (its /json/list is polled): Steam 8080, SteamVR 8087.
'';
};
target = {
@@ -61,21 +47,18 @@ let
patch = mkOption {
type = types.path;
description = ''
JavaScript file evaluated in every matching page (DevTools
Runtime.evaluate, awaited). Must be idempotent: it is re-evaluated
after page reloads and every 15 s. Its result value is logged when it
changes; return e.g. "patched", and "unchanged" (never logged) when
already applied. To use the finder library, build it with
`config.steamFrame.uiPatches.lib.mkPatch`.
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 file evaluated in every patched page when the service
stops (switch, list emptied, logout), reverting the patch. Must be
safe when the page isn't patched.
JavaScript that reverts the patch when the service stops; must be
safe on an unpatched page.
'';
};
};
@@ -102,16 +85,10 @@ in {
default = import ./lib { inherit pkgs; };
defaultText = lib.literalMD "the helpers of `modules/lib`";
description = ''
Helpers for writing patches (see modules/lib/default.nix):
`mkPatch { name, src, signatures ? …, opts ? { } }` returns a patch
file that calls `src`, a JavaScript function expression
`(find, sigs, opts, hooks) => …`, with the finder library `find`
(modules/lib/finders.js: getWebpackRequire, resolveAll, findModule,
findExport, findFiberUp, findInReactTree, …), the module signatures
`sigs` (format: modules/lib/signatures.json; default: its entry
`name`, if any), `opts` and the shared method hooks `hooks`
(modules/lib/hooks.js: before, remove, has). `finders` and `hooks`
are the libraries' paths, `signatures` the parsed signatures.json.
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).
'';
};
@@ -128,9 +105,8 @@ in {
'';
description = ''
Runtime patches of Steam's web UIs, kept injected by the
`steam-ui-patches` user service over the local CEF DevTools ports and
reverted when it stops. Every target (page) of the endpoint matching all
given target criteria is patched.
steam-ui-patches service and reverted when it stops. Every page
matching all given target criteria is patched.
'';
};
@@ -156,13 +132,11 @@ in {
Install.WantedBy = [ "default.target" ];
};
# Restart on every switch so changed patches are re-injected (each
# replaces its older version) and removed ones are reverted by the old
# instance.
# Restart on switch: re-inject changed patches; the old instance reverts
# removed ones.
steamFrame.userServices.restart = [ "steam-ui-patches.service" ];
})
# No patches: stop a still-running injector, which reverts its patches, so
# Steam's UI is stock right away.
# No patches: stopping the injector reverts them right away.
(lib.mkIf (cfg.patches == [ ]) {
steamFrame.userServices.stop = [ "steam-ui-patches.service" ];
})
+18 -36
View File
@@ -1,29 +1,16 @@
# Enables the SteamVR web helper debugger: Chromium DevTools of vrwebhelper,
# which renders the SteamVR dashboard, on 127.0.0.1:8087, so steam-ui-patches
# can patch the dashboard the way it patches Steam's UI on 127.0.0.1:8080.
# Turned on automatically (mkDefault) when a UI patch targets port 8087.
# SteamVR web helper debugger: DevTools of the dashboard (vrwebhelper) on
# 127.0.0.1:8087, for dashboard patches. On automatically (mkDefault) when a
# patch targets port 8087.
#
# SteamVR only opens the port when /settings/VRWebHelper/DebuggerEnabled is
# true (settingsschema.vrsettings: requires_restart; port from
# VRWebHelper/DebuggerPort, default 8087). User settings live in
# ~/.config/openvr/config/steamvr.vrsettings, which SteamVR rewrites at
# runtime and on exit (from memory), so it can't be a read-only store link,
# and editing it while SteamVR runs would be overwritten.
# steamvr-webhelper-debugger.service merges just that key with jq (atomic
# write, other keys untouched, file created if missing), before every start
# of steamvr.service (SteamVR's systemd user unit): a drop-in makes it
# Want + start After the oneshot. (Its own ExecStartPre= chain already starts
# vrserver, so an added ExecStartPre= would run too late.) Hence the setting
# takes effect with the next SteamVR start, once.
#
# The service is installed even when disabled: if this module set the key
# before (marker in $XDG_STATE_HOME/steam-frame-nix), it sets it back to
# false at the next SteamVR start and removes the marker; otherwise it
# leaves the file alone (a setting made by hand is kept).
#
# Security: the port listens on 127.0.0.1 only; Steam's Developer Mode makes
# SteamOS forward it to 0.0.0.0:8088 (README, "DevTools on the LAN"), so keep
# Developer Mode off. Our patches only use 127.0.0.1.
# The port opens only with VRWebHelper/DebuggerEnabled (requires a SteamVR
# restart). steamvr.vrsettings is rewritten by SteamVR from memory, so it
# can't be a store link or be edited while SteamVR runs: a oneshot merges just
# that key with jq before each steamvr.service start (drop-in Wants/After;
# ExecStartPre= would be too late, vrserver starts in the unit's own chain).
# Disabled, it resets the key only if it set it (marker in
# $XDG_STATE_HOME/steam-frame-nix); a manual setting is kept.
# Developer Mode forwards the port to 0.0.0.0:8088 (README, "DevTools on the
# LAN"); our patches use 127.0.0.1 only.
{ config, lib, pkgs, ... }:
let
cfg = config.steamFrame.steamvrDebugger;
@@ -73,15 +60,11 @@ in {
default = false;
defaultText = lib.literalMD "on automatically when a dashboard patch (a `steamFrame.uiPatches.patches` entry on port 8087) is enabled";
description = ''
Enable SteamVR's web helper debugger (DevTools of the SteamVR dashboard
on 127.0.0.1:8087, setting VRWebHelper/DebuggerEnabled in
~/.config/openvr/config/steamvr.vrsettings), needed by patches of the
SteamVR dashboard. Set before each SteamVR start, so it takes effect
after SteamVR is restarted once. Turning it off sets the key back to
false at the next SteamVR start. Normally there is no need to set it:
it is turned on automatically when a dashboard patch is enabled. The
port listens on 127.0.0.1; keep Steam's Developer Mode off, which would
also forward it to the LAN (0.0.0.0:8088).
SteamVR's web helper debugger (dashboard DevTools on 127.0.0.1:8087,
VRWebHelper/DebuggerEnabled in steamvr.vrsettings), needed by dashboard
patches, which turn it on automatically. Takes effect after one SteamVR
restart; off resets the key at the next start. Developer Mode also
forwards the port to the LAN (0.0.0.0:8088).
'';
};
@@ -100,8 +83,7 @@ in {
};
};
# Wants= (not Requires=): if the merge fails (e.g. broken JSON), SteamVR
# still starts, just without the debugger.
# Wants=, not Requires=: SteamVR still starts if the merge fails.
xdg.configFile."systemd/user/steamvr.service.d/webhelper-debugger.conf".text = ''
[Unit]
Wants=steamvr-webhelper-debugger.service
+19 -41
View File
@@ -1,14 +1,6 @@
# Adjustable curvature per SteamVR dashboard window
# (steamFrame.dashboard.windowCurvature): the "Toggle Curvature" control of a
# window (row of its More Options menu, or its bottom-bar button when moved
# there) becomes a wheel (click: toggle, drag up/down: curvature, with
# controller haptics). Runtime patch of SteamVR's dashboard page (vrwebhelper
# "systemui", DevTools 127.0.0.1:8087; see window-curvature/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, "window-curvature";
# scripts/check-signatures.mjs checks them offline).
# Adjustable curvature per dashboard window. Dashboard patch
# (window-curvature/patch.js; "systemui" on 127.0.0.1:8087), registered only
# when enabled; enables steamvr-debugger.nix (one SteamVR restart the first time).
{ config, lib, pkgs, ... }:
let
cfg = config.steamFrame.dashboard.windowCurvature;
@@ -24,29 +16,20 @@ in {
options.steamFrame.dashboard.windowCurvature = {
enable = lib.mkEnableOption ''
adjustable curvature per SteamVR dashboard window: the "Toggle
Curvature" row of a window's More Options (three-dot) menu shows the
window's curvature and becomes a control, and so does its button in
the window's bottom bar when it sits there (no value shown; steps and
snap points are felt as haptics). Click: curved → flat, flat → stock
curve. Press and drag up/down with the laser: set the curvature
live. Values are relative to SteamVR's stock curve (1 = stock, 2 = twice
as curved, i.e. half the radius, 0 = flat) and kept per window until
SteamVR restarts. Runtime patch of the SteamVR dashboard
(steamFrame.uiPatches), applied immediately; turning it off restores the
stock menu and radius (next switch)'';
adjustable curvature per SteamVR dashboard window via its "Toggle
Curvature" menu row (shows the value) or bar button: click toggles
curved/flat, drag up/down sets it live. Values are relative to the stock
curve (1 = stock, 2 = half the radius, 0 = flat), kept per window until
SteamVR restarts. Dashboard patch; off restores stock (next switch)'';
default = value 1.0 ''
Curvature of windows placed in the world or on a hand that have no value
of their own yet, once curved (stock SteamVR shows them flat; the
toggle turns them on). Windows docked in the dashboard or in the theater
start at 1 (stock), so the docked Steam window stays concentric with the
dashboard bar.
Curvature a world/hand window (stock: flat) gets when first curved.
Dashboard and theater windows start at 1, keeping the docked Steam
window concentric with the bar.
'';
max = value 3.0 "Largest curvature the control goes to (relative to the stock curve).";
step = value 0.05 "Step the value is rounded to while dragging.";
snap = value 0.15 ''
While dragging, values within ± this distance of a snap point snap to
it exactly; dragging on moves past. 0 = no snapping.
Snap distance around snap points while dragging; 0 = no snapping.
'';
snapPoints = mkOption {
type = types.listOf types.number;
@@ -57,34 +40,29 @@ in {
type = types.ints.unsigned;
default = 8;
description = ''
Vertical laser travel (menu pixels) before a press on the row becomes
a drag instead of a click.
Vertical laser travel (menu px) that turns a press into a drag.
'';
};
dragPixelsPerUnit = value 60 ''
Drag distance (menu pixels) per 1.0 of curvature. The laser's position
stops at the menu's edge (about 190 px above the row), so 0 to `max`
should fit into that.
Menu pixels per 1.0 of curvature. The laser stops at the menu's edge
(~190 px above the row), so 0 to `max` should fit.
'';
barDragPixelsPerUnit = value 30 ''
Drag distance (bar pixels) per 1.0 of curvature on the bottom-bar
button (when the control sits in the bar).
Bar pixels per 1.0 of curvature on the bottom-bar button.
'';
barDragRoom = mkOption {
type = types.ints.unsigned;
default = 160;
description = ''
Transparent room (pixels) added above and below a window's bottom bar
while its curvature button is dragged, so the laser stays on the bar
panel (its position stops at the pressed panel's edge). 0 = none.
Transparent room (px) added above and below the bar while its button
is dragged, so the laser stays on the panel; 0 = none.
'';
};
haptics = mkOption {
type = types.bool;
default = true;
description = ''
Controller haptics while dragging: a snap at snap points, a stronger
edge at 0 and `max`, a light tick for other steps.
Controller haptics while dragging (snap points, 0/`max` edges, steps).
'';
};
};