#!/usr/bin/env bash # SPDX-FileCopyrightText: 2026 Marco Allegretti # SPDX-License-Identifier: EUPL-1.2 set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repo_dir" theme_dir="icons/org.shift.icons" required_icons="$(mktemp)" missing_icons="$(mktemp)" broken_icons="$(mktemp)" plasma_files="$(mktemp)" trap 'rm -f "$required_icons" "$missing_icons" "$broken_icons" "$plasma_files"' EXIT icon_name_prefix_re='^(accessories|activities|applications?|application-|appointment|archive|arrow-|audio|battery|bluetooth|bookmarks|call|camera-|chronometer|color|configure|computer|device-|dialog-|document|drive|edit-|emblem-|entry|favorites|flashlight|folder|format|games|games-|get-hot-new-stuff|go-|help|high-brightness|input-|internet|kcm|keyboard|klipper|krunner|layer|list-|low-brightness|mail|media-|mobile-|network-|notifications|object|office|org\.kde\.|osd-|phone|plasma|preferences-|printer|process|redshift|rotation-|search|security|settings-|software|speedometer|starred|start-here|system-|tablet|tools|touchpad|trash-|unknown|user|utilities|video|view|view-|virtual-|weather|widget-|window|zoom)' fail() { printf '%s\n' "$1" >&2 exit 1 } require_line() { local file="$1" local pattern="$2" local message="$3" grep -Eq "$pattern" "$file" || fail "$message" } icon_exists() { local icon_name="$1" [[ -e "$theme_dir/preferences/scalable/$icon_name.svg" ]] && return 0 [[ -e "$theme_dir/actions/symbolic/$icon_name.svg" ]] && return 0 [[ -e "$theme_dir/places/symbolic/$icon_name.svg" ]] && return 0 [[ -e "$theme_dir/apps/scalable/$icon_name.svg" ]] && return 0 return 1 } require_directory_order() { local directories directories="$(awk -F= '$1 == "Directories" { print $2 }' "$theme_dir/index.theme")" [[ "$directories" == preferences/scalable,*actions/symbolic* ]] \ || fail "org.shift.icons must list preferences/scalable before actions/symbolic so System Settings category icons do not resolve to symbolic action aliases" } check_systemsettings_category_icons() { [[ -d /usr/share/systemsettings/categories ]] || return 0 local missing_preferences missing_preferences="$(grep -Rh '^Icon=' /usr/share/systemsettings/categories 2>/dev/null \ | cut -d= -f2- \ | sort -u \ | while IFS= read -r icon_name; do case "$icon_name" in preferences-*) [[ -e "$theme_dir/preferences/scalable/$icon_name.svg" ]] || printf '%s\n' "$icon_name" ;; esac done)" [[ -z "$missing_preferences" ]] || { printf 'System Settings category icons must exist in preferences/scalable so they are not shadowed by symbolic action aliases:\n' >&2 printf '%s\n' "$missing_preferences" | sed 's/^/ - /' >&2 exit 1 } } check_systemsettings_module_icons() { compgen -G '/usr/share/applications/kcm_*.desktop' >/dev/null || return 0 local missing_preferences missing_preferences="$({ grep -Rh '^Icon=' /usr/share/applications/kcm_*.desktop 2>/dev/null || true; } \ | cut -d= -f2- \ | sort -u \ | while IFS= read -r icon_name; do [[ -e "$theme_dir/preferences/scalable/$icon_name.svg" ]] || printf '%s\n' "$icon_name" done)" [[ -z "$missing_preferences" ]] || { printf 'System Settings module icons must exist in preferences/scalable so KCM list entries do not resolve to dark symbolic glyphs:\n' >&2 printf '%s\n' "$missing_preferences" | sed 's/^/ - /' >&2 exit 1 } } hicolor_fallback_exists() { local icon_name="$1" case "$icon_name" in start-here-shift) [[ -e icons/sc-places-start-here-shift.svg ]] ;; *) return 1 ;; esac } filter_icon_names() { awk 'length > 0 && $0 !~ /[[:space:]]/ && $0 !~ /\// && $0 !~ /[][{}();:%>]/ && $0 !~ /^#/ && $0 !~ /\.(qml|png|jpg|jpeg|svg|desktop|service|json|xml|wav|ogg)$/ && $0 !~ /-$/ { print }' \ | grep -E "$icon_name_prefix_re" } collect_installed_plasma_icon_names() { command -v rpm >/dev/null || return 0 local package for package in \ plasma6-desktop \ plasma6-workspace \ plasma6-mobile \ plasma6-nm \ plasma6-pa \ powerdevil6 \ kscreen6 \ bluedevil6 \ systemsettings6 \ kde-cli-tools6 \ kwin6 \ kactivitymanagerd6; do rpm -ql "$package" 2>/dev/null || true done \ | awk '/\.(qml|js|json|desktop|actions)$/ || /metadata\.json$/ { print }' \ | { grep -v '^/usr/share/icons/' || true; } \ | sort -u > "$plasma_files" [[ -s "$plasma_files" ]] || return 0 xargs -r perl -ne 'if (/^(?:Icon|IconName|X-KDE-PluginInfo-Icon)=(.+)$/) { print "$1\n"; } while (/"Icon"\s*:\s*"([^"]+)"/g) { print "$1\n"; } while (/(?:\bicon\b|icon\.name|iconName|iconSource|fallbackIconName|compactRepresentationIcon|categoryIcon|source)\s*:\s*["\x27]([^"\x27]+)["\x27]/g) { print "$1\n"; } while (/(?:setIconName|iconName)\s*\(\s*["\x27]([^"\x27]+)["\x27]/g) { print "$1\n"; }' < "$plasma_files" \ | filter_icon_names || true } collect_repo_icon_names() { find \ components/mobileshell/qml \ containments/homescreens/folio/qml \ containments/homescreens/halcyon/qml \ containments/taskpanel/qml \ initialstart \ kcms \ kwin \ lookandfeel/contents/logout \ quicksettings \ -type f \( -name '*.qml' -o -name '*.js' -o -name '*.json' -o -name '*.desktop' -o -name '*.actions' -o -name '*.notifyrc' \) -print0 \ | xargs -0 perl -ne 'if (/^(?:Icon|IconName|X-KDE-PluginInfo-Icon)=(.+)$/) { print "$1\n"; } while (/"Icon"\s*:\s*"([^"]+)"/g) { print "$1\n"; } while (/(?:\bicon\b|icon\.name|iconName|iconSource|fallbackIconName|compactRepresentationIcon|categoryIcon|source)\s*:\s*["\x27]([^"\x27]+)["\x27]/g) { print "$1\n"; } while (/(?:setIconName|iconName)\s*\(\s*["\x27]([^"\x27]+)["\x27]/g) { print "$1\n"; }' \ | filter_icon_names || true } [[ -d "$theme_dir" ]] || fail "Missing Shift icon theme directory: $theme_dir" find "$theme_dir" -xtype l -print > "$broken_icons" if [[ -s "$broken_icons" ]]; then printf 'Broken Shift icon aliases:\n' >&2 sed 's/^/ - /' "$broken_icons" >&2 exit 1 fi require_line lookandfeel/contents/defaults '^Theme=org\.shift\.icons$' \ "look-and-feel defaults must select org.shift.icons" require_line "$theme_dir/index.theme" '^Inherits=hicolor$' \ "org.shift.icons must inherit only hicolor" require_line "$theme_dir/index.theme" '^Directories=.*actions/symbolic.*places/symbolic.*apps/scalable' \ "org.shift.icons index.theme must list actions, places, and apps directories" require_directory_order require_line "$theme_dir/index.theme" '^\[actions/symbolic\]$' \ "org.shift.icons index.theme is missing [actions/symbolic]" require_line "$theme_dir/index.theme" '^\[places/symbolic\]$' \ "org.shift.icons index.theme is missing [places/symbolic]" require_line "$theme_dir/index.theme" '^\[apps/scalable\]$' \ "org.shift.icons index.theme is missing [apps/scalable]" require_line "$theme_dir/index.theme" '^\[preferences/scalable\]$' \ "org.shift.icons index.theme is missing [preferences/scalable]" check_systemsettings_category_icons check_systemsettings_module_icons { collect_repo_icon_names collect_installed_plasma_icon_names if [[ -d /usr/share/icons ]]; then find /usr/share/icons -name 'network-*.svg' -o -name 'network-*.png' 2>/dev/null \ | sed 's#.*/##; s/\.[^.]*$//' \ | awk 'length > 0 { print }' fi cat <<'ICONS' applications-all applications-development applications-education applications-games applications-graphics applications-internet applications-multimedia applications-office applications-other applications-science applications-system applications-utilities battery-000 battery-000-charging battery-010 battery-010-charging battery-020 battery-020-charging battery-030 battery-030-charging battery-040 battery-040-charging battery-050 battery-050-charging battery-060 battery-060-charging battery-070 battery-070-charging battery-080 battery-080-charging battery-090 battery-090-charging battery-100 battery-100-charging battery-missing network-bluetooth network-bluetooth-activated network-mobile-0 network-mobile-20 network-mobile-40 network-mobile-60 network-mobile-80 network-mobile-100 preferences-system systemsettings ICONS } | sort -u > "$required_icons" while IFS= read -r icon_name; do [[ -n "$icon_name" ]] || continue if icon_exists "$icon_name" || hicolor_fallback_exists "$icon_name"; then continue fi printf '%s\n' "$icon_name" >> "$missing_icons" done < "$required_icons" if [[ -s "$missing_icons" ]]; then printf 'Missing required Shift icons:\n' >&2 sed 's/^/ - /' "$missing_icons" >&2 exit 1 fi