From 8d7094916b49e41d0c452ccd3edf017a4ae75d36 Mon Sep 17 00:00:00 2001 From: Pierre Kisters <1524059+lhns@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:15:27 +0200 Subject: [PATCH] launcher-menu: detect missing icon fallbacks on switch (iconFallbacks.enable/extra) --- README.md | 38 ++++--- modules/launcher-menu.nix | 108 ++++++++++++++------ modules/launcher-menu/icon-fallbacks.sh | 125 ++++++++++++++++++++++++ template/home.nix | 3 + 4 files changed, 229 insertions(+), 45 deletions(-) create mode 100644 modules/launcher-menu/icon-fallbacks.sh diff --git a/README.md b/README.md index 6a5072c..c99d62d 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,8 @@ menu list every desktop entry, including terminals such as Konsole. | `steamFrame.launcherMenu.grid.enable` | bool | `false` | Show the "+" menu's programs as a grid of tiles (large icon, name below) instead of a list. | | `steamFrame.launcherMenu.grid.columns` | int, 1-8 | `4` | Tiles per row (popup is 300 px wide: 3 ≈ 92 px, 4 ≈ 68 px, 5 ≈ 53 px tiles). | | `steamFrame.launcherMenu.grid.maxRows` | null or positive int | `null` | Rows visible at once, the rest scrolls. `null`: fill up to the menu's max height (600 px). | -| `steamFrame.launcherMenu.iconFallbacks` | list of str | `[ "utilities-terminal" "preferences-system" ]` | Icon names installed as hicolor copies of Breeze app icons, so the "+" menu shows them (Konsole, KDE System Settings). `[ ]`: none. Takes effect after a Steam restart. | +| `steamFrame.launcherMenu.iconFallbacks.enable` | bool | `true` | On every switch, link hicolor fallbacks (Breeze app icons) for desktop entries whose icon Steam can't find, so the "+" menu shows them (SteamOS: Konsole, KDE System Settings). `false` removes the links. See [Icon fallbacks](#icon-fallbacks-launchermenuiconfallbacks). | +| `steamFrame.launcherMenu.iconFallbacks.extra` | list of str | `[ ]` | Further icon names to provide even if no desktop entry the scan sees uses them. | | `steamFrame.dashboard.windowMaxScale` | null or number | `null` | Largest resize-handle scale of SteamVR dashboard windows, relative to their default size. `null`: stock (2). See [Dashboard windows](#dashboard-windows-dashboard). | | `steamFrame.dashboard.windowDistance.{world,theater,dashboard}.{min,max}` | null or number (m) | `null` | How close / far grabbed windows can be pulled in / pushed back. `null`: stock (world 0.25-5, theater 1-6, dashboard 0.3-4 m). | | `steamFrame.dashboard.steamCloseButton.enable` | bool | `false` | Close (X) button on the dashboard's Steam window: switches to the previous window, or leaves just the dashboard bar. See [Steam close button](#steam-close-button-dashboardsteamclosebuttonenable). | @@ -551,19 +552,30 @@ desktop's icon theme. Konsole (`Icon=utilities-terminal`) and KDE System Settings (`Icon=preferences-system`), which SteamOS ships, have their icons only in Breeze, so they show up without an icon in the "+" menu. -**Fix:** for each name in `iconFallbacks` (default: those two), the largest -Breeze app icon from nixpkgs' `kdePackages.breeze-icons` is installed as -`~/.nix-profile/share/icons/hicolor/scalable/apps/.svg` -(`~/.nix-profile/share` is on Steam's `XDG_DATA_DIRS`); desktop entries are -left alone. Add names for other programs that lack an icon (the `Icon=` -value of their desktop entry), e.g. -`iconFallbacks = [ "utilities-terminal" "preferences-system" "system-file-manager" ];`; -`[ ]` installs nothing. A name Breeze has no app icon for fails the build -with an error naming it. +**Fix:** on every `home-manager switch`, a small activation script looks +through the desktop entries Steam sees (`applications/` in +`~/.local/share` and Steam's `XDG_DATA_DIRS`; an entry shadowed by one with +the same desktop-file ID, and `Hidden`/`NoDisplay` entries, are skipped). +For each `Icon=` name that no hicolor theme dir (or `pixmaps`) has but +nixpkgs' `kdePackages.breeze-icons` app icons do, it links the largest +Breeze SVG as `~/.local/share/icons/hicolor/scalable/apps/.svg`. +Desktop entries are left alone. `iconFallbacks.extra` adds names to +provide even if no scanned entry uses them (a name Breeze lacks is +reported and skipped). The links made are listed in +`~/.local/state/steam-frame-nix/icon-fallbacks`; links no longer needed are +removed, and `iconFallbacks.enable = false` removes them all. Only those +links (still pointing into a breeze-icons store path) are ever removed; +other files in the icon dir are never touched. Programs installed later get +their fallback on the next switch. -**Caveat:** Steam caches the icon lookup (GTK icon cache) for its lifetime, -and files in the Nix store all have the same 1970 mtime, so new icons show -up only after Steam is restarted (e.g. a reboot), not on the next switch. +`iconFallbacks` used to be a list of icon names; setting a list now fails +evaluation with a message: use `iconFallbacks.extra` for additional names, +`iconFallbacks.enable = false` for what `[ ]` meant. + +A running Steam picks up the change without a restart: it caches the icon +theme (GTK) and rescans it only when a theme directory's mtime changes, so +the script bumps the mtime of `~/.local/share/icons/hicolor` whenever it +adds or removes links. ### Dashboard windows (`dashboard.*`) diff --git a/modules/launcher-menu.nix b/modules/launcher-menu.nix index e04685d..4738241 100644 --- a/modules/launcher-menu.nix +++ b/modules/launcher-menu.nix @@ -23,32 +23,23 @@ # 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. Hicolor copies -# from nixpkgs' Breeze are installed into the profile (~/.nix-profile/share -# is on Steam's XDG_DATA_DIRS). +# 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. { config, lib, pkgs, ... }: let cfg = config.steamFrame.launcherMenu; inherit (import ./lib { inherit pkgs; }) mkPatch; sharedJSContext = { title = "SharedJSContext"; }; - # Largest Breeze app icon (all are SVG) as hicolor/scalable/apps/.svg. - breezeApps = "${pkgs.kdePackages.breeze-icons}/share/icons/breeze/apps"; - iconFallbacks = pkgs.runCommand "launcher-menu-icon-fallbacks" { } '' - dir=$out/share/icons/hicolor/scalable/apps - mkdir -p $dir - for name in ${lib.escapeShellArgs cfg.iconFallbacks}; do - src= - for size in 64 48 32 24 22 16; do - if [ -e "${breezeApps}/$size/$name.svg" ]; then src="${breezeApps}/$size/$name.svg"; break; fi - done - if [ -z "$src" ]; then - echo "steamFrame.launcherMenu.iconFallbacks: no Breeze app icon \"$name\" (${breezeApps}//$name.svg)" >&2 - exit 1 - fi - cp -L "$src" "$dir/$name.svg" - done - ''; + iconFallbacks = pkgs.writeShellApplication { + name = "steam-frame-icon-fallbacks"; + runtimeInputs = [ pkgs.coreutils pkgs.findutils pkgs.gawk ]; + text = builtins.readFile ./launcher-menu/icon-fallbacks.sh; + }; in { imports = [ ./steam-ui-patches.nix ]; @@ -122,23 +113,76 @@ in { }; }; iconFallbacks = lib.mkOption { - type = lib.types.listOf (lib.types.strMatching "[A-Za-z0-9._+-]+"); - default = [ "utilities-terminal" "preferences-system" ]; - example = [ "utilities-terminal" "preferences-system" "system-file-manager" ]; + default = { }; description = '' - Icon names (Icon= of desktop entries) installed as hicolor icons, - copied from nixpkgs' Breeze app icons, so the "+" menu shows them: - Steam resolves program icons only in the hicolor theme, not in the - desktop's Breeze theme. The default covers the entries SteamOS ships - whose icons exist only in Breeze: Konsole (utilities-terminal) and - KDE System Settings (preferences-system). A name Breeze has no app - icon for fails the build, naming it. [] installs nothing. Steam - picks up new icons only after a restart. + 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. ''; + # 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). + type = lib.types.coercedTo (lib.types.listOf lib.types.str) + (names: { legacyList = names; }) + (lib.types.submodule { + options = { + enable = lib.mkOption { + 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. + ''; + }; + extra = lib.mkOption { + type = lib.types.listOf (lib.types.strMatching "[A-Za-z0-9._+-]+"); + 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. + ''; + }; + legacyList = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + internal = true; + visible = false; + }; + }; + }); }; }; - config.home.packages = lib.optional (cfg.iconFallbacks != [ ]) iconFallbacks; + config.assertions = [ { + assertion = cfg.iconFallbacks.legacyList == null; + message = '' + steamFrame.launcherMenu.iconFallbacks is no longer a list of icon names + (set to ${builtins.toJSON cfg.iconFallbacks.legacyList}). Use + iconFallbacks.enable = true; # default: utilities-terminal, preferences-system + iconFallbacks.extra = [ ... ]; # further names + or iconFallbacks.enable = false; for none (what [ ] used to mean). + ''; + } ]; + + # After installPackages, so the new profile's desktop entries and icons count. + # Disabled, the script removes its links (no BREEZE_APPS). + config.home.activation.steamFrameIconFallbacks = + lib.hm.dag.entryAfter [ "writeBoundary" "installPackages" ] ( + if cfg.iconFallbacks.enable then '' + run env BREEZE_APPS=${pkgs.kdePackages.breeze-icons}/share/icons/breeze/apps \ + ${lib.getExe iconFallbacks} ${lib.escapeShellArgs cfg.iconFallbacks.extra} + '' else '' + run ${lib.getExe iconFallbacks} + ''); config.steamFrame.uiPatches.patches = lib.optional cfg.sort { diff --git a/modules/launcher-menu/icon-fallbacks.sh b/modules/launcher-menu/icon-fallbacks.sh new file mode 100644 index 0000000..b79571e --- /dev/null +++ b/modules/launcher-menu/icon-fallbacks.sh @@ -0,0 +1,125 @@ +# Hicolor fallbacks for desktop entry icons that only Breeze has. +# +# Usage: steam-frame-icon-fallbacks [EXTRA_NAME...] +# BREEZE_APPS=/share/icons/breeze/apps: link the Icon= names +# of the desktop entries Steam sees (plus EXTRA_NAMEs) that no hicolor +# theme dir has but Breeze does, as +# $XDG_DATA_HOME/icons/hicolor/scalable/apps/.svg -> Breeze's SVG. +# BREEZE_APPS unset: remove all links made before (disabled). +# Links made are listed in a manifest; only those, and only while they still +# point into a breeze-icons store path, are ever removed. Nothing else in the +# icon dir is touched. + +data_home=${XDG_DATA_HOME:-$HOME/.local/share} +dest=$data_home/icons/hicolor/scalable/apps +manifest=${XDG_STATE_HOME:-$HOME/.local/state}/steam-frame-nix/icon-fallbacks +# Steam's data dirs (XDG_DATA_HOME, then its XDG_DATA_DIRS on SteamOS), not +# the caller's: switches usually run from the nested desktop session. +data_dirs=( + "$data_home" + "$HOME/.local/share/flatpak/exports/share" + /var/lib/flatpak/exports/share + /usr/local/share + /usr/share + "$HOME/.nix-profile/share" + /nix/var/nix/profiles/default/share +) +shopt -s nullglob + +ours() { [[ -L $1 && $(readlink "$1") == /nix/store/*-breeze-icons-*/* ]]; } + +# Does Steam already find (hicolor in any data dir, or pixmaps)? +# Our own links don't count. +resolves() { + local d f + for d in "$HOME/.icons" "${data_dirs[@]/%//icons}"; do + for f in "$d"/hicolor/*/*/"$1".{png,svg,xpm}; do + ours "$f" || return 0 + done + done + for d in "${data_dirs[@]}"; do + for f in "$d"/pixmaps/"$1".{png,svg,xpm}; do [[ ! -e $f ]] || return 0; done + done + return 1 +} + +# Largest Breeze app icon for (Breeze's app icons are all SVG). +breeze() { + local size + for size in 64 48 32 24 22 16; do + if [[ -e $BREEZE_APPS/$size/$1.svg ]]; then echo "$BREEZE_APPS/$size/$1.svg"; return; fi + done + return 1 +} + +# Icon= of a desktop entry, unless it is Hidden/NoDisplay or not a plain icon +# name (e.g. an absolute path). +entry_icon() { + awk -F= ' + /^\[/ { group = ($0 ~ /^\[Desktop Entry\][[:space:]]*$/); next } + !group { next } + { key = $1; sub(/[[:space:]]+$/, "", key); val = substr($0, index($0, "=") + 1) + gsub(/^[[:space:]]+|[[:space:]\r]+$/, "", val) } + (key == "Hidden" || key == "NoDisplay") && val == "true" { skip = 1 } + key == "Icon" { icon = val } + END { if (!skip && icon ~ /^[A-Za-z0-9._+-]+$/) print icon } + ' "$1" +} + +# Wanted: extra names, and the icons of the entries Steam sees (the first +# entry with a given desktop-file ID wins, like in the menu). +declare -A wanted=() seen=() +if [[ -n ${BREEZE_APPS:-} ]]; then + for name in "$@"; do wanted[$name]=extra; done + for d in "${data_dirs[@]}"; do + [[ -d $d/applications ]] || continue + while IFS= read -r -d '' f; do + id=${f#"$d/applications/"}; id=${id//\//-} + [[ -z ${seen[$id]:-} ]] || continue + seen[$id]=1 + icon=$(entry_icon "$f") + [[ -z $icon ]] || wanted[$icon]=${wanted[$icon]:-$id} + done < <(find -L "$d/applications" -name '*.desktop' -print0 2>/dev/null) + done +fi + +changed= +linked=() +for name in "${!wanted[@]}"; do + resolves "$name" && continue + if ! src=$(breeze "$name"); then + [[ ${wanted[$name]} != extra ]] || echo "icon fallbacks: Breeze has no app icon \"$name\" (extra)" >&2 + continue + fi + link=$dest/$name.svg + if [[ -e $link || -L $link ]] && ! ours "$link"; then continue; fi # not ours + linked+=("$name") + [[ $(readlink "$link") == "$src" ]] && continue + mkdir -p "$dest" + ln -sfn "$src" "$link" + echo "icon fallbacks: linked $name (${wanted[$name]})" + changed=1 +done + +# Remove links from earlier runs that are no longer wanted. +if [[ -f $manifest ]]; then + while IFS= read -r name; do + [[ -n $name && " ${linked[*]} " != *" $name "* ]] || continue + if ours "$dest/$name.svg"; then + rm -f "$dest/$name.svg" + echo "icon fallbacks: removed $name" + changed=1 + fi + done < "$manifest" +fi + +if (( ${#linked[@]} )); then + mkdir -p "${manifest%/*}" + printf '%s\n' "${linked[@]}" | sort > "$manifest" +else + rm -f "$manifest" +fi + +# GTK rescans an icon theme only when a theme dir's mtime changes; bump it +# so a running Steam sees the change. +if [[ -n $changed && -d $data_home/icons/hicolor ]]; then touch "$data_home/icons/hicolor"; fi diff --git a/template/home.nix b/template/home.nix index aae8aa2..e807491 100644 --- a/template/home.nix +++ b/template/home.nix @@ -26,6 +26,9 @@ # closeOnLaunch = true; # launchDebounce = 10; # grid = { enable = true; columns = 4; maxRows = 4; }; + # # Icon fallbacks for programs without an icon in the menu are on by + # # default; names to provide in addition to what the scan finds: + # # iconFallbacks.extra = [ "system-file-manager" ]; # }; # # SteamVR dashboard windows: resizable up to 4x (stock 2x), pushed # # back up to 10 m in the world / 12 m in theater mode (stock 5 / 6 m):