desktopToDarwinBundle: fix squircle icons

- Convert icons to a single .icns file; and
- Provide an opt-out via X-macOS-Squircle in the desktop item to
  override the squircle behavior when the source icons look bad when
  converted automatically.
This commit is contained in:
Randy Eckenrode 2022-02-19 23:30:16 -05:00
parent 88ac8585ba
commit 30a09ae9ac
No known key found for this signature in database
GPG key ID: 64C1CD4EC2A600D9
3 changed files with 150 additions and 32 deletions

View file

@ -4,33 +4,34 @@ let
pListText = lib.generators.toPlist { } {
CFBundleDevelopmentRegion = "English";
CFBundleExecutable = "$name";
CFBundleIconFiles = [ "$iconPlistArray" ];
CFBundleIconFile = "$icon";
CFBundleIdentifier = "org.nixos.$name";
CFBundleInfoDictionaryVersion = "6.0";
CFBundleName = "$name";
CFBundlePackageType = "APPL";
CFBundleSignature = "???";
};
# The generation of the CFBundleIconFiles array is a bit of a hack, since we
# will always end up with an empty first element (<string></string>) but macOS
# appears to ignore this which allows us to use the nix PList generator.
in writeScriptBin "write-darwin-bundle" ''
shopt -s nullglob
readonly prefix="$1"
readonly name="$2"
readonly exec="$3"
iconPlistArray=""
readonly prefix=$1
readonly name=$2
readonly exec=$3
readonly icon=$4.icns
readonly squircle=''${5:-1}
readonly plist=$prefix/Applications/$name.app/Contents/Info.plist
for icon in "$prefix/Applications/$name.app/Contents/Resources"/*; do
iconPlistArray="$iconPlistArray</string><string>"$(basename "$icon")""
done
cat > "$prefix/Applications/$name.app/Contents/Info.plist" <<EOF
cat > "$plist" <<EOF
${pListText}
EOF
if [[ $squircle != 0 && $squircle != "false" ]]; then
sed "
s|CFBundleIconFile|CFBundleIconFiles|;
s|<string>$icon</string>|<array><string>$icon</string></array>|
" -i "$plist"
fi
cat > "$prefix/Applications/$name.app/Contents/MacOS/$name" <<EOF
#!/bin/bash
exec $prefix/bin/$exec

View file

@ -10,31 +10,147 @@ getDesktopParam() {
awk -F "=" "/${pattern}/ {print \$2}" "${file}"
}
# Convert a freedesktop.org icon theme for a given app to a .icns file. When possible, missing
# icons are synthesized from SVG or rescaled from existing ones (when within the size threshold).
convertIconTheme() {
local -r out=$1
local -r sharePath=$2
local -r iconName=$3
local -r theme=${4:-hicolor}
local -ra iconSizes=(16 32 128 256 512)
local -ra scales=([1]="" [2]="@2")
# Based loosely on the algorithm at:
# https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup
# Assumes threshold = 2 for ease of implementation.
function findIcon() {
local -r iconSize=$1
local -r scale=$2
local scaleSuffix=${scales[$scale]}
local exactSize=${iconSize}x${iconSize}${scaleSuffix}
local -a validSizes=(
${exactSize}
$(expr $iconSize + 1)x$(expr $iconSize + 1)${scaleSuffix}
$(expr $iconSize + 2)x$(expr $iconSize + 2)${scaleSuffix}
$(expr $iconSize - 1)x$(expr $iconSize - 1)${scaleSuffix}
$(expr $iconSize - 2)x$(expr $iconSize - 2)${scaleSuffix}
)
for iconIndex in "${!candidateIcons[@]}"; do
for maybeSize in "${validSizes[@]}"; do
icon=${candidateIcons[$iconIndex]}
if [[ $icon = */$maybeSize/* ]]; then
if [[ $maybeSize = $exactSize ]]; then
echo "fixed $icon"
else
echo "threshold $icon"
fi
return 0
fi
done
done
echo "scalable"
}
function resizeIcon() {
local -r in=$1
local -r out=$2
local -r iconSize=$3
local -r scale=$4
local density=$(expr 72 \* $scale)x$(expr 72 \* $scale)
local dim=$(expr $iconSize \* $scale)
magick convert -scale "${dim}x${dim}" -density "$density" -units PixelsPerInch "$in" "$out"
}
function synthesizeIcon() {
local -r in=$1
local -r out=$2
local -r iconSize=$3
local -r scale=$4
if [[ $in != '-' ]]; then
local density=$(expr 72 \* $scale)x$(expr 72 \* $scale)
local dim=$(expr $iconSize \* $scale)
rsvg-convert --keep-aspect-ratio --width "$dim" --height "$dim" "$in" --output "$out"
magick convert -density "$density" -units PixelsPerInch "$out" "$out"
else
return 1
fi
}
function getIcons() {
local -r sharePath=$1
local -r iconname=$2
local -r theme=$3
local -r resultdir=$(mktemp -d)
local -ar candidateIcons=(
"${sharePath}/icons/${theme}/"*"/${iconname}.png"
"${sharePath}/icons/${theme}/"*"/${iconname}.xpm"
)
local -a scalableIcon=("${sharePath}/icons/${theme}/scalable/${iconname}.svg"*)
if [[ ${#scalableIcon[@]} = 0 ]]; then
scalableIcon=('-')
fi
for iconSize in "${iconSizes[@]}"; do
for scale in "${!scales[@]}"; do
local iconResult=$(findIcon $iconSize $scale)
local type=${iconResult%% *}
local icon=${iconResult#* }
local scaleSuffix=${scales[$scale]}
local result=${resultdir}/${iconSize}x${iconSize}${scales[$scale]}${scaleSuffix:+x}.png
case $type in
fixed)
local density=$(expr 72 \* $scale)x$(expr 72 \* $scale)
magick convert -density "$density" -units PixelsPerInch "$icon" "$result"
;;
threshold)
# Synthesize an icon of the exact size if a scalable icon is available
# instead of scaling one and ending up with a fuzzy icon.
if ! synthesizeIcon "${scalableIcon[0]}" "$result" "$iconSize" "$scale"; then
resizeIcon "$icon" "$result" "$iconSize" "$scale"
fi
;;
scalable)
synthesizeIcon "${scalableIcon[0]}" "$result" "$iconSize" "$scale" || true
;;
esac
done
done
echo "$resultdir"
}
iconsdir=$(getIcons "$sharePath" "apps/${iconName}" "$theme")
if [[ ! -z "$(ls -1 "$iconsdir/"*)" ]]; then
icnsutil compose "$out/${iconName}.icns" "$iconsdir/"*
else
echo "Warning: no icons were found. Creating an empty icon for ${iconName}.icns."
touch "$out/${iconName}.icns"
fi
}
# For a given .desktop file, generate a darwin '.app' bundle for it.
convertDesktopFile() {
local -r file="$1"
local -r file=$1
local -r sharePath=$(dirname "$(dirname "$file")")
local -r name=$(getDesktopParam "${file}" "^Name")
local -r exec=$(getDesktopParam "${file}" "Exec")
local -r iconName=$(getDesktopParam "${file}" "Icon")
local -r iconFiles=$(find "$out/share/icons/" -name "${iconName}.*" 2>/dev/null);
local -r pixMaps=$(find "$out/share/pixmaps/" -name "${iconName}.xpm" 2>/dev/null);
local -r iconName=$(getDesktopParam "${file}" "^Icon")
local -r squircle=$(getDesktopParam "${file}" "X-macOS-SquircleIcon")
mkdir -p "$out/Applications/${name}.app/Contents/MacOS"
mkdir -p "$out/Applications/${name}.app/Contents/Resources"
local i=0;
for icon in $iconFiles; do
ln -s "$icon" "$out/Applications/${name}.app/Contents/Resources/$i-$(basename "$icon")"
(( i +=1 ));
done
convertIconTheme "$out/Applications/${name}.app/Contents/Resources" "$sharePath" "$iconName"
for pixmap in $pixMaps; do
local newIconName="$i-$(basename "$pixmap")";
convert "$pixmap" "$out/Applications/${name}.app/Contents/Resources/${newIconName%.xpm}.png"
(( i +=1 ));
done
write-darwin-bundle "$out" "$name" "$exec"
write-darwin-bundle "$out" "$name" "$exec" "$iconName" "$squircle"
}
convertDesktopFiles() {

View file

@ -821,8 +821,9 @@ with pkgs;
writeDarwinBundle = callPackage ../build-support/make-darwin-bundle/write-darwin-bundle.nix { };
desktopToDarwinBundle = makeSetupHook { deps = [ writeDarwinBundle imagemagick ]; }
../build-support/setup-hooks/desktop-to-darwin-bundle.sh;
desktopToDarwinBundle = makeSetupHook {
deps = [ writeDarwinBundle librsvg imagemagick python3Packages.icnsutil ];
} ../build-support/setup-hooks/desktop-to-darwin-bundle.sh;
keepBuildTree = makeSetupHook { } ../build-support/setup-hooks/keep-build-tree.sh;