launcher-menu: icon fallbacks as Home Manager links

The icon fallbacks were links an activation script made (and tracked in
~/.local/state/steam-frame-nix/icon-fallbacks) after scanning the desktop
entries. They are Home Manager links now:
~/.local/share/icons/hicolor/scalable/apps/<name>.svg -> the largest Breeze
app SVG, for utilities-terminal and preferences-system (SteamOS' Konsole and
KDE System Settings) plus iconFallbacks.extra; a name Breeze doesn't have
fails the build. enable = false: none.

- The scan is read-only now (steam-frame-icon-fallbacks --suggest): each
  switch prints icons Steam can't find that Breeze has (for extra) and
  fallbacks hicolor has anyway.
- hicolor's mtime is bumped when the set of links changed between
  generations, so a running Steam rescans (GTK checks theme dir mtimes).
- The script's old links at those paths are removed before
  checkLinkTargets (they would be collisions); any others and the manifest
  go through steam-frame-nix-cleanup.

The assertion for the old list form stays until ~2026-12.
This commit is contained in:
Pierre Kisters committed 2026-09-29 00:04:51 +02:00
1 parent b7b32d0aa9
commit a0111813b3
2 files changed
+87 -86

No files matched your search

+53 -21
View File
@@ -10,21 +10,39 @@
# Tested with Steam client 1790377368. # Tested with Steam client 1790377368.
# #
# iconFallbacks (not a patch): Steam resolves Icon= names only in hicolor (and # iconFallbacks (not a patch): Steam resolves Icon= names only in hicolor (and
# pixmaps), so Breeze-only icons (Konsole, KDE System Settings) are missing; # pixmaps), so Breeze-only icons (Konsole, KDE System Settings) are missing.
# icon-fallbacks.sh links them from nixpkgs' Breeze into # Home Manager links a fixed list of them from nixpkgs' Breeze into
# ~/.local/share/icons/hicolor on switch. It was a list of names until 2026-09; # ~/.local/share/icons/hicolor/scalable/apps (a missing name fails the
# a list now fails with a pointer to enable/extra. # build); on switch, icon-fallbacks.sh --suggest only prints hints (names to
# add, names no longer needed), and hicolor's mtime is bumped when the list
# changed so a running Steam rescans. Until 2026-09 a script made the links
# on switch (removed by steam-frame-nix-cleanup, or before checkLinkTargets
# where Home Manager takes over). iconFallbacks was a list of names until
# 2026-09; a list fails with a pointer to enable/extra (until ~2026-12).
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.steamFrame.launcherMenu; cfg = config.steamFrame.launcherMenu;
inherit (import ./lib { inherit pkgs; }) mkPatch; inherit (import ./lib { inherit pkgs; }) mkPatch;
sharedJSContext = { title = "SharedJSContext"; }; sharedJSContext = { title = "SharedJSContext"; };
iconFallbacks = pkgs.writeShellApplication { iconSuggest = pkgs.writeShellApplication {
name = "steam-frame-icon-fallbacks"; name = "steam-frame-icon-fallbacks";
runtimeInputs = [ pkgs.coreutils pkgs.findutils pkgs.gawk ]; runtimeInputs = [ pkgs.coreutils pkgs.findutils pkgs.gawk ];
text = builtins.readFile ./launcher-menu/icon-fallbacks.sh; text = builtins.readFile ./launcher-menu/icon-fallbacks.sh;
}; };
breezeApps = "${pkgs.kdePackages.breeze-icons}/share/icons/breeze/apps";
defaultIcons = [ "utilities-terminal" "preferences-system" ];
icons = lib.optionals cfg.iconFallbacks.enable (lib.unique (defaultIcons ++ cfg.iconFallbacks.extra));
# The largest Breeze app icon (all SVG); fails the build if there is none.
breezeIcon = name: pkgs.runCommand "icon-fallback-${name}.svg" { } ''
for size in 64 48 32 24 22 16; do
if [ -e ${breezeApps}/$size/${name}.svg ]; then ln -s ${breezeApps}/$size/${name}.svg $out; exit 0; fi
done
echo "steamFrame.launcherMenu.iconFallbacks: Breeze has no app icon \"${name}\" (${breezeApps}/<size>/${name}.svg)" >&2
exit 1
'';
iconDir = "${lib.removePrefix "${config.home.homeDirectory}/" config.xdg.dataHome}/icons/hicolor";
in { in {
imports = [ imports = [
./cleanup.nix ./cleanup.nix
@@ -119,12 +137,12 @@ in {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
On switch, for each Icon= of the desktop entries Steam sees Link nixpkgs' Breeze SVGs of utilities-terminal and
that hicolor lacks but nixpkgs' Breeze has (SteamOS: preferences-system (SteamOS' Konsole and KDE System
utilities-terminal, preferences-system), link the Breeze SVG Settings) and of `extra` into
into ~/.local/share/icons/hicolor/scalable/apps. Only its own ~/.local/share/icons/hicolor/scalable/apps (Home Manager
links (listed in ~/.local/state/steam-frame-nix/icon-fallbacks) links). Each switch prints hints: icons Steam can't find
are touched; stale ones are removed; false removes all. that Breeze has, and fallbacks hicolor has anyway.
''; '';
}; };
extra = lib.mkOption { extra = lib.mkOption {
@@ -132,8 +150,8 @@ in {
default = [ ]; default = [ ];
example = [ "system-file-manager" ]; example = [ "system-file-manager" ];
description = '' description = ''
Extra icon names to provide (if hicolor lacks them); names Further Breeze app icon names to provide; a name Breeze
Breeze doesn't have are reported and skipped. doesn't have fails the build.
''; '';
}; };
legacyList = lib.mkOption { legacyList = lib.mkOption {
@@ -158,15 +176,29 @@ in {
''; '';
} ]; } ];
# After installPackages so the new profile counts; without BREEZE_APPS the config.xdg.dataFile = lib.listToAttrs (map (name: {
# script removes its links. name = "icons/hicolor/scalable/apps/${name}.svg";
value.source = breezeIcon name;
}) icons);
# The script's links of 2026-09 where Home Manager links now.
config.steamFrame.cleanup.migrateLinks = map (name: {
path = "${iconDir}/scalable/apps/${name}.svg";
target = "/nix/store/*-breeze-icons-*/share/icons/breeze/apps/*";
}) icons;
# GTK (Steam) rescans an icon theme only when a theme dir's mtime changes:
# bump hicolor's when the links changed. After installPackages so the new
# profile counts for the hints.
config.home.activation.steamFrameIconFallbacks = config.home.activation.steamFrameIconFallbacks =
lib.hm.dag.entryAfter [ "writeBoundary" "installPackages" ] ( lib.hm.dag.entryAfter [ "writeBoundary" "linkGeneration" "installPackages" ] (''
if cfg.iconFallbacks.enable then '' apps=${lib.escapeShellArg "${iconDir}/scalable/apps"}
run env BREEZE_APPS=${pkgs.kdePackages.breeze-icons}/share/icons/breeze/apps \ if [[ -d $HOME/${lib.escapeShellArg iconDir} ]] &&
${lib.getExe iconFallbacks} ${lib.escapeShellArgs cfg.iconFallbacks.extra} [[ "$(ls "''${oldGenPath:-/nonexistent}/home-files/$apps" 2>/dev/null)" != "$(ls "$newGenPath/home-files/$apps" 2>/dev/null)" ]]; then
'' else '' run touch "$HOME"/${lib.escapeShellArg iconDir}
run ${lib.getExe iconFallbacks} fi
'' + lib.optionalString cfg.iconFallbacks.enable ''
BREEZE_APPS=${breezeApps} ${lib.getExe iconSuggest} --suggest ${lib.escapeShellArgs icons} || true
''); '');
config.steamFrame.uiPatches.patches = config.steamFrame.uiPatches.patches =
+34 -65
View File
@@ -1,16 +1,19 @@
# shellcheck shell=bash # shellcheck shell=bash
# Hicolor fallbacks for desktop entry icons that only Breeze has. # Hints for launcherMenu.iconFallbacks; read-only.
# Usage: steam-frame-icon-fallbacks [EXTRA_NAME...] # Usage: BREEZE_APPS=<breeze-icons>/share/icons/breeze/apps \
# BREEZE_APPS=<breeze-icons>/share/icons/breeze/apps: link each Icon= name # steam-frame-icon-fallbacks --suggest [CONFIGURED_NAME...]
# (of the entries Steam sees, plus EXTRA_NAMEs) that no hicolor dir has # Prints
# but Breeze does as $XDG_DATA_HOME/icons/hicolor/scalable/apps/<name>.svg. # - Icon= names of the desktop entries Steam sees that no hicolor dir (nor
# BREEZE_APPS unset: remove the links made before. # pixmaps) has but Breeze does, and that aren't configured: candidates for
# Only links listed in the manifest and still pointing into breeze-icons are # iconFallbacks.extra;
# ever removed. # - configured names that hicolor has anyway (apart from our links): no
# longer needed.
[[ ${1:-} == --suggest ]] || { echo "usage: steam-frame-icon-fallbacks --suggest [NAME...]" >&2; exit 2; }
shift
: "${BREEZE_APPS:?BREEZE_APPS is not set}"
data_home=${XDG_DATA_HOME:-$HOME/.local/share} 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 # 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. # the caller's: switches usually run from the nested desktop session.
data_dirs=( data_dirs=(
@@ -24,29 +27,26 @@ data_dirs=(
) )
shopt -s nullglob shopt -s nullglob
ours() { [[ -L $1 && $(readlink "$1") == /nix/store/*-breeze-icons-*/* ]]; } # Our fallbacks: links that end up in a Breeze store path.
ours() { [[ -L $1 && $(readlink -f "$1") == /nix/store/*-breeze-icons-*/* ]]; }
# Does Steam already find <name> (hicolor in any data dir, or pixmaps)? # Where Steam finds <name> (hicolor in any data dir, or pixmaps), ours aside.
# Our own links don't count.
resolves() { resolves() {
local d f local d f
for d in "$HOME/.icons" "${data_dirs[@]/%//icons}"; do for d in "$HOME/.icons" "${data_dirs[@]/%//icons}"; do
for f in "$d"/hicolor/*/*/"$1".{png,svg,xpm}; do for f in "$d"/hicolor/*/*/"$1".{png,svg,xpm}; do
ours "$f" || return 0 ours "$f" || { echo "$f"; return 0; }
done done
done done
for d in "${data_dirs[@]}"; do for d in "${data_dirs[@]}"; do
for f in "$d"/pixmaps/"$1".{png,svg,xpm}; do [[ ! -e $f ]] || return 0; done for f in "$d"/pixmaps/"$1".{png,svg,xpm}; do [[ ! -e $f ]] || { echo "$f"; return 0; }; done
done done
return 1 return 1
} }
# Largest Breeze app icon for <name> (Breeze's app icons are all SVG). in_breeze() {
breeze() {
local size local size
for size in 64 48 32 24 22 16; do for size in 64 48 32 24 22 16; do [[ -e $BREEZE_APPS/$size/$1.svg ]] && return 0; done
if [[ -e $BREEZE_APPS/$size/$1.svg ]]; then echo "$BREEZE_APPS/$size/$1.svg"; return; fi
done
return 1 return 1
} }
@@ -64,11 +64,11 @@ entry_icon() {
' "$1" ' "$1"
} }
# Wanted: extra names, and the icons of the entries Steam sees (the first declare -A configured=() seen=() missing=()
# entry with a given desktop-file ID wins, like in the menu). for name in "$@"; do configured[$name]=1; done
declare -A wanted=() seen=()
if [[ -n ${BREEZE_APPS:-} ]]; then # The entries Steam sees (the first with a given desktop-file ID wins, like
for name in "$@"; do wanted[$name]=extra; done # in the menu).
for d in "${data_dirs[@]}"; do for d in "${data_dirs[@]}"; do
[[ -d $d/applications ]] || continue [[ -d $d/applications ]] || continue
while IFS= read -r -d '' f; do while IFS= read -r -d '' f; do
@@ -76,48 +76,17 @@ if [[ -n ${BREEZE_APPS:-} ]]; then
[[ -z ${seen[$id]:-} ]] || continue [[ -z ${seen[$id]:-} ]] || continue
seen[$id]=1 seen[$id]=1
icon=$(entry_icon "$f") icon=$(entry_icon "$f")
[[ -z $icon ]] || wanted[$icon]=${wanted[$icon]:-$id} [[ -n $icon && -z ${configured[$icon]:-} && -z ${missing[$icon]:-} ]] || continue
if ! resolves "$icon" >/dev/null && in_breeze "$icon"; then missing[$icon]=${id%.desktop}; fi
done < <(find -L "$d/applications" -name '*.desktop' -print0 2>/dev/null) done < <(find -L "$d/applications" -name '*.desktop' -print0 2>/dev/null)
done done
fi
changed= for icon in "${!missing[@]}"; do
linked=() echo "icon fallbacks: \"$icon\" (${missing[$icon]}) shows without icon in Steam's \"+\" menu; Breeze has it: add it to steamFrame.launcherMenu.iconFallbacks.extra"
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 done
for name in "${!configured[@]}"; do
# Remove links from earlier runs that are no longer wanted. if f=$(resolves "$name"); then
if [[ -f $manifest ]]; then echo "icon fallbacks: \"$name\" is in hicolor anyway ($f); the fallback is no longer needed"
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 fi
done < "$manifest" done
fi exit 0
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