diff --git a/flake.nix b/flake.nix index 0c6c20b..ee75c60 100644 --- a/flake.nix +++ b/flake.nix @@ -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; diff --git a/modules/clipboard-sync.nix b/modules/clipboard-sync.nix index 523b9d8..35689bb 100644 --- a/modules/clipboard-sync.nix +++ b/modules/clipboard-sync.nix @@ -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=() diff --git a/modules/dashboard-windows.nix b/modules/dashboard-windows.nix index ca08f5e..6e26c1b 100644 --- a/modules/dashboard-windows.nix +++ b/modules/dashboard-windows.nix @@ -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 = { diff --git a/modules/firefox.nix b/modules/firefox.nix index d615a0a..ebe75e3 100644 --- a/modules/firefox.nix +++ b/modules/firefox.nix @@ -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. ''; }; }; diff --git a/modules/frame-controls.nix b/modules/frame-controls.nix index a32cfda..075c33a 100644 --- a/modules/frame-controls.nix +++ b/modules/frame-controls.nix @@ -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:" (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:" (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. ''; }; }; diff --git a/modules/hidden-apps.nix b/modules/hidden-apps.nix index ede25af..568dc44 100644 --- a/modules/hidden-apps.nix +++ b/modules/hidden-apps.nix @@ -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; diff --git a/modules/keyboard-layout.nix b/modules/keyboard-layout.nix index e8b00e4..1ddf716 100644 --- a/modules/keyboard-layout.nix +++ b/modules/keyboard-layout.nix @@ -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 { diff --git a/modules/launcher-menu.nix b/modules/launcher-menu.nix index d2916ea..afbf63e 100644 --- a/modules/launcher-menu.nix +++ b/modules/launcher-menu.nix @@ -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/.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 '' diff --git a/modules/portal.nix b/modules/portal.nix index 4802690..9cfa519 100644 --- a/modules/portal.nix +++ b/modules/portal.nix @@ -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} diff --git a/modules/session.nix b/modules/session.nix index ec205f2..37ba298 100644 --- a/modules/session.nix +++ b/modules/session.nix @@ -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 /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 +# (/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)."; }; }; }; diff --git a/modules/steam-close-button.nix b/modules/steam-close-button.nix index 30d47dd..b8d9054 100644 --- a/modules/steam-close-button.nix +++ b/modules/steam-close-button.nix @@ -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 = [ { diff --git a/modules/steam-keyboard-patch.nix b/modules/steam-keyboard-patch.nix index ec77a6e..f38ac0e 100644 --- a/modules/steam-keyboard-patch.nix +++ b/modules/steam-keyboard-patch.nix @@ -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" ]; }) diff --git a/modules/steam-ui-patches.nix b/modules/steam-ui-patches.nix index 943faf5..4a5ff31 100644 --- a/modules/steam-ui-patches.nix +++ b/modules/steam-ui-patches.nix @@ -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" ]; }) diff --git a/modules/steamvr-debugger.nix b/modules/steamvr-debugger.nix index 3e35b07..1281e6e 100644 --- a/modules/steamvr-debugger.nix +++ b/modules/steamvr-debugger.nix @@ -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 diff --git a/modules/window-curvature.nix b/modules/window-curvature.nix index b25ed07..8814033 100644 --- a/modules/window-curvature.nix +++ b/modules/window-curvature.nix @@ -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). ''; }; };