launcher-menu: detect missing icon fallbacks on switch (iconFallbacks.enable/extra)

This commit is contained in:
Pierre Kisters committed 2026-09-27 21:16:51 +02:00
1 parent cfa9ca7e15
commit 8d7094916b
4 files changed
+229 -45

No files matched your search

+76 -32
View File
@@ -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/<name>.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}/<size>/$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/<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 {
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 {
+125
View File
@@ -0,0 +1,125 @@
# Hicolor fallbacks for desktop entry icons that only Breeze has.
#
# Usage: steam-frame-icon-fallbacks [EXTRA_NAME...]
# BREEZE_APPS=<breeze-icons>/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/<name>.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 <name> (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 <name> (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