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