shift-shell/tests/check-shift-icon-theme.sh
Marco Allegretti d9943d2e7a Let symbolic icons follow the color scheme
Declare FollowsColorScheme=true for the Shift icon theme so KDE apps recolor symbolic Places glyphs against dark and light surfaces. Document the requirement and guard it in the icon theme coverage test.
2026-05-23 09:30:21 +02:00

285 lines
No EOL
10 KiB
Bash

#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Marco Allegretti <mightymarco4@gmail.com>
# 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"
while IFS= read -r -d '' icon_path; do
[[ -e "$icon_path" ]] || printf '%s\n' "$icon_path"
done < <(find "$theme_dir" -type l -print0) > "$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" '^FollowsColorScheme=true$' \
"org.shift.icons must follow the active color scheme so symbolic Places icons stay legible in dark and light themes"
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
# Quick-setting tiles render with Kirigami.Icon { isMask: true }; if their icon
# name resolves to a colored preferences/scalable tile it collapses to a solid
# rounded square. Require an explicit -symbolic suffix for any quick-setting
# icon name that also exists as a colored tile.
masked_collisions="$(mktemp)"
trap 'rm -f "$required_icons" "$missing_icons" "$broken_icons" "$plasma_files" "$masked_collisions"' EXIT
while IFS= read -r -d '' qml; do
perl -ne 'if (/^\s*icon:\s*"([^"]+)"\s*$/) { print "$1\n"; }' "$qml"
done < <(find quicksettings -name 'main.qml' -print0) \
| sort -u \
| while IFS= read -r icon_name; do
case "$icon_name" in
*-symbolic) continue ;;
esac
[[ -e "$theme_dir/preferences/scalable/$icon_name.svg" ]] || continue
printf '%s\n' "$icon_name"
done > "$masked_collisions"
if [[ -s "$masked_collisions" ]]; then
printf 'Quick-setting icon names must use a -symbolic suffix when a colored preferences/scalable tile exists, otherwise the masked tile renders as a solid square:\n' >&2
sed 's/^/ - /' "$masked_collisions" >&2
exit 1
fi