diff --git a/modules/launcher-menu.nix b/modules/launcher-menu.nix index c60fa01..b92a9cb 100644 --- a/modules/launcher-menu.nix +++ b/modules/launcher-menu.nix @@ -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}//${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 { diff --git a/modules/launcher-menu/icon-fallbacks.sh b/modules/launcher-menu/icon-fallbacks.sh index 03b5927..b4fc391 100644 --- a/modules/launcher-menu/icon-fallbacks.sh +++ b/modules/launcher-menu/icon-fallbacks.sh @@ -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=/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/.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=/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 (hicolor in any data dir, or pixmaps)? -# Our own links don't count. +# Where Steam finds (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 (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