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
+97 -96

No files matched your search

+54 -22
View File
@@ -10,21 +10,39 @@
# Tested with Steam client 1790377368.
#
# 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.
# pixmaps), so Breeze-only icons (Konsole, KDE System Settings) are missing.
# Home Manager links a fixed list of them from nixpkgs' Breeze into
# ~/.local/share/icons/hicolor/scalable/apps (a missing name fails the
# 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, ... }:
let
cfg = config.steamFrame.launcherMenu;
inherit (import ./lib { inherit pkgs; }) mkPatch;
sharedJSContext = { title = "SharedJSContext"; };
iconFallbacks = pkgs.writeShellApplication {
iconSuggest = pkgs.writeShellApplication {
name = "steam-frame-icon-fallbacks";
runtimeInputs = [ pkgs.coreutils pkgs.findutils pkgs.gawk ];
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 {
imports = [
./cleanup.nix
@@ -119,12 +137,12 @@ in {
type = lib.types.bool;
default = true;
description = ''
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.
Link nixpkgs' Breeze SVGs of utilities-terminal and
preferences-system (SteamOS' Konsole and KDE System
Settings) and of `extra` into
~/.local/share/icons/hicolor/scalable/apps (Home Manager
links). Each switch prints hints: icons Steam can't find
that Breeze has, and fallbacks hicolor has anyway.
'';
};
extra = lib.mkOption {
@@ -132,8 +150,8 @@ in {
default = [ ];
example = [ "system-file-manager" ];
description = ''
Extra icon names to provide (if hicolor lacks them); names
Breeze doesn't have are reported and skipped.
Further Breeze app icon names to provide; a name Breeze
doesn't have fails the build.
'';
};
legacyList = lib.mkOption {
@@ -158,16 +176,30 @@ in {
'';
} ];
# After installPackages so the new profile counts; without BREEZE_APPS the
# script removes its links.
config.xdg.dataFile = lib.listToAttrs (map (name: {
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 =
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}
'');
lib.hm.dag.entryAfter [ "writeBoundary" "linkGeneration" "installPackages" ] (''
apps=${lib.escapeShellArg "${iconDir}/scalable/apps"}
if [[ -d $HOME/${lib.escapeShellArg iconDir} ]] &&
[[ "$(ls "''${oldGenPath:-/nonexistent}/home-files/$apps" 2>/dev/null)" != "$(ls "$newGenPath/home-files/$apps" 2>/dev/null)" ]]; then
run touch "$HOME"/${lib.escapeShellArg iconDir}
fi
'' + lib.optionalString cfg.iconFallbacks.enable ''
BREEZE_APPS=${breezeApps} ${lib.getExe iconSuggest} --suggest ${lib.escapeShellArgs icons} || true
'');
config.steamFrame.uiPatches.patches =
lib.optional cfg.sort {
+43 -74
View File
@@ -1,16 +1,19 @@
# shellcheck shell=bash
# 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 each Icon= name
# (of the entries Steam sees, plus EXTRA_NAMEs) that no hicolor dir has
# but Breeze does as $XDG_DATA_HOME/icons/hicolor/scalable/apps/<name>.svg.
# BREEZE_APPS unset: remove the links made before.
# Only links listed in the manifest and still pointing into breeze-icons are
# ever removed.
# Hints for launcherMenu.iconFallbacks; read-only.
# Usage: BREEZE_APPS=<breeze-icons>/share/icons/breeze/apps \
# steam-frame-icon-fallbacks --suggest [CONFIGURED_NAME...]
# Prints
# - Icon= names of the desktop entries Steam sees that no hicolor dir (nor
# pixmaps) has but Breeze does, and that aren't configured: candidates for
# iconFallbacks.extra;
# - 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}
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=(
@@ -24,29 +27,26 @@ data_dirs=(
)
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)?
# Our own links don't count.
# Where Steam finds <name> (hicolor in any data dir, or pixmaps), ours aside.
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
ours "$f" || { echo "$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
for f in "$d"/pixmaps/"$1".{png,svg,xpm}; do [[ ! -e $f ]] || { echo "$f"; return 0; }; done
done
return 1
}
# Largest Breeze app icon for <name> (Breeze's app icons are all SVG).
breeze() {
in_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
for size in 64 48 32 24 22 16; do [[ -e $BREEZE_APPS/$size/$1.svg ]] && return 0; done
return 1
}
@@ -64,60 +64,29 @@ entry_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
declare -A configured=() seen=() missing=()
for name in "$@"; do configured[$name]=1; done
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
# The entries Steam sees (the first with a given desktop-file ID wins, like
# in the menu).
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")
[[ -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
# 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
for icon in "${!missing[@]}"; do
echo "icon fallbacks: \"$icon\" (${missing[$icon]}) shows without icon in Steam's \"+\" menu; Breeze has it: add it to steamFrame.launcherMenu.iconFallbacks.extra"
done
for name in "${!configured[@]}"; do
if f=$(resolves "$name"); then
echo "icon fallbacks: \"$name\" is in hicolor anyway ($f); the fallback is no longer needed"
fi
done
exit 0