mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-11-09 20:04:34 +00:00
* fix(packaging): declare the launcher's real window class in its .desktop file (#1026) The window sets its app id (Wayland) and WM_CLASS (X11) to ai.storyteller.artcraft-launcher, but the entry said StartupWMClass=artcraft-launcher. Desktops that find the entry by its file name were fine; an entry under another name (AppImageLauncher, Gear Lever, a hand-made one) never matched, so the dock showed a generic icon and the launcher could not be pinned. * fix(packaging): no second menu entry when an AppImage manager already lists the launcher (#474, #394) appimaged, AppImageLauncher and Gear Lever write their own entry for the AppImage under another file name. AppRun added ai.storyteller.artcraft-launcher.desktop next to it on every start, so the launcher showed up twice in the app menu, and deleting the extra entry only lasted until the next launch. AppRun now installs nothing while another entry's Exec runs this AppImage, and removes the entry it wrote earlier when own_entry says it is still unchanged. own_entry also accepts entries that carry the old StartupWMClass=artcraft-launcher, since that line is the only difference from what earlier releases wrote. * fix(packaging): a hidden manager entry doesn't stand in for ours Menu editors hide an entry with NoDisplay=true or Hidden=true. When a user hid the manager's duplicate that way, AppRun still counted it and removed our entry, leaving the launcher out of the menu. Such an entry no longer counts. Test 15e covers both keys. * fix(packaging): a manager entry stands in for ours only with our window class Read only the [Desktop Entry] group, so an Exec in a [Desktop Action] group doesn't count, and require StartupWMClass=ai.storyteller.artcraft-launcher. Gear Lever doesn't refresh its entry when the launcher replaces the AppImage in place, so its entry can keep the old class, and without ours GNOME would have nothing to match the window against.
142 lines
6.1 KiB
Bash
Executable File
142 lines
6.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# ArtCraft Launcher AppImage entry point (packaging/linux/AppRun; installed by package.sh).
|
|
#
|
|
# On Wayland the compositor resolves a window's dock icon by taking the window's
|
|
# app_id (ai.storyteller.artcraft-launcher) and looking up an installed .desktop file.
|
|
# An AppImage installs nothing, so every AppImage run would otherwise show the
|
|
# generic "Wayland" icon. Best-effort desktop integration: on first run
|
|
# (and whenever the AppImage's path changes) install the .desktop entry and the
|
|
# hicolor icons into $XDG_DATA_HOME, then launch. Every failure here must leave
|
|
# the launch untouched. AppImage managers can opt out with DESKTOPINTEGRATION=1,
|
|
# and users with ARTCRAFT_LAUNCHER_NO_DESKTOP_INTEGRATION=1. Nothing is installed
|
|
# while another entry (appimaged, AppImageLauncher, Gear Lever) runs this AppImage.
|
|
|
|
set -u
|
|
|
|
APPDIR="$(dirname "$(readlink -f "$0")")"
|
|
DESK_ID=ai.storyteller.artcraft-launcher
|
|
|
|
integrate() {
|
|
case "${ARTCRAFT_LAUNCHER_NO_DESKTOP_INTEGRATION:-}" in
|
|
1 | true | yes) return 0 ;;
|
|
esac
|
|
case "${DESKTOPINTEGRATION:-}" in
|
|
1 | true | yes) return 0 ;;
|
|
esac
|
|
# The AppImage runtime exports $APPIMAGE; without it (an extracted AppDir) there
|
|
# is no path to register and nothing to do.
|
|
appimage="${APPIMAGE:-}"
|
|
[ -n "$appimage" ] && [ -f "$appimage" ] || return 0
|
|
appimage="$(readlink -f "$appimage")" || return 0
|
|
# Exec= is parsed by the desktop: a path holding a quote, backquote, dollar, backslash,
|
|
# percent or a line break would need escaping (or could add lines), so skip integration.
|
|
case "$appimage" in
|
|
*[\"\`\$\\%]* | *'
|
|
'*) return 0 ;;
|
|
esac
|
|
home="${HOME:-}"
|
|
[ -n "$home" ] || return 0
|
|
data="${XDG_DATA_HOME:-$home/.local/share}"
|
|
src_desktop="$APPDIR/usr/share/applications/$DESK_ID.desktop"
|
|
[ -f "$src_desktop" ] || return 0
|
|
apps="$data/applications"
|
|
# A second entry would list the app twice; drop the one we wrote if it is unchanged.
|
|
if other_entry_runs "$apps" "$appimage"; then
|
|
if own_entry "$apps/$DESK_ID.desktop" && rm -f "$apps/$DESK_ID.desktop"; then
|
|
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps" >/dev/null 2>&1
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
# Rewrite Exec to this AppImage (the binary is not on the host PATH) and drop
|
|
# TryExec, which would hide the entry; everything else is shipped as packaged.
|
|
mkdir -p "$apps" 2>/dev/null || return 0
|
|
tmp="$apps/.$DESK_ID.tmp.$$"
|
|
esc="$(printf '%s' "$appimage" | sed 's/[\\&|]/\\&/g')"
|
|
if sed -e "s|^Exec=.*|Exec=\"$esc\"|" -e '/^TryExec=/d' "$src_desktop" 2>/dev/null >"$tmp" &&
|
|
printf '%s\n' "X-ArtCraft-Launcher-Generated-Exec=\"$appimage\"" >>"$tmp"; then
|
|
if cmp -s "$tmp" "$apps/$DESK_ID.desktop"; then
|
|
rm -f "$tmp"
|
|
elif [ ! -e "$apps/$DESK_ID.desktop" ] && [ ! -L "$apps/$DESK_ID.desktop" ] || own_entry "$apps/$DESK_ID.desktop"; then
|
|
if mv -f "$tmp" "$apps/$DESK_ID.desktop" 2>/dev/null; then
|
|
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps" >/dev/null 2>&1
|
|
else
|
|
rm -f "$tmp"
|
|
fi
|
|
else
|
|
# Keep manager metadata and user changes, including a custom Exec environment.
|
|
rm -f "$tmp"
|
|
fi
|
|
else
|
|
rm -f "$tmp"
|
|
return 0
|
|
fi
|
|
|
|
# Icons: per-file compare so an unchanged install costs a handful of cmp calls.
|
|
src_icons="$APPDIR/usr/share/icons/hicolor"
|
|
[ -d "$src_icons" ] || return 0
|
|
icons="$data/icons/hicolor"
|
|
(cd "$src_icons" && find . -type f) | {
|
|
changed=false
|
|
while IFS= read -r rel; do
|
|
mkdir -p "$icons/$(dirname "$rel")" 2>/dev/null || continue
|
|
if ! cmp -s "$src_icons/$rel" "$icons/$rel"; then
|
|
if cp "$src_icons/$rel" "$icons/$rel" 2>/dev/null; then
|
|
changed=true
|
|
fi
|
|
fi
|
|
done
|
|
if [ "$changed" = true ] && command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
|
# A per-user hicolor directory need not have index.theme; -t permits that.
|
|
gtk-update-icon-cache -f -t "$icons" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
}
|
|
|
|
# Refresh only an unchanged entry whose Exec still matches the executable we wrote.
|
|
# Unmarked legacy entries and customized or ambiguous executable fields are preserved.
|
|
own_entry() {
|
|
[ -f "$1" ] && [ ! -L "$1" ] || return 1
|
|
[ "$(grep -c '^Exec=' "$1")" = 1 ] || return 1
|
|
[ "$(grep -c '^X-ArtCraft-Launcher-Generated-Exec=' "$1")" = 1 ] || return 1
|
|
grep -Eq '^Exec="/[^"`$\\%]+"$' "$1" || return 1
|
|
existing_exec="$(sed -n 's/^Exec=//p' "$1")" || return 1
|
|
generated_exec="$(sed -n 's/^X-ArtCraft-Launcher-Generated-Exec=//p' "$1")" || return 1
|
|
[ "$existing_exec" = "$generated_exec" ] || return 1
|
|
expected_body="$(sed -e '/^Exec=/d' -e '/^TryExec=/d' "$src_desktop")" || return 1
|
|
existing_body="$(sed -e '/^Exec=/d' -e '/^X-ArtCraft-Launcher-Generated-Exec=/d' "$1")" || return 1
|
|
# Entries written before the StartupWMClass fix differ only in that line.
|
|
old_body="$(printf '%s\n' "$expected_body" | sed 's/^StartupWMClass=.*/StartupWMClass=artcraft-launcher/')"
|
|
[ "$expected_body" = "$existing_body" ] || [ "$old_body" = "$existing_body" ]
|
|
}
|
|
|
|
# True when a visible entry in $1 other than ours runs the AppImage $2 and has our window class.
|
|
other_entry_runs() {
|
|
grep -lF -e "$2" "$1"/*.desktop 2>/dev/null | (
|
|
while IFS= read -r f; do
|
|
[ "$f" = "$1/$DESK_ID.desktop" ] && continue
|
|
main=false runs=false class=false
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
# Only the [Desktop Entry] group; an action's Exec isn't what the entry launches.
|
|
case "$line" in
|
|
"[Desktop Entry]") main=true && continue ;;
|
|
"["*) main=false ;;
|
|
esac
|
|
[ "$main" = true ] || continue
|
|
case "$line" in
|
|
# Menu editors hide an entry this way; it lists nothing.
|
|
Hidden=true | NoDisplay=true) runs=false && break ;;
|
|
# A class from before an in-place update can't match the window, so ours has to stay.
|
|
"StartupWMClass=$DESK_ID") class=true ;;
|
|
Exec=*"$2" | Exec=*"$2 "* | Exec=*"$2\""* | Exec=*"$2'"*) runs=true ;;
|
|
esac
|
|
done <"$f"
|
|
[ "$runs" = true ] && [ "$class" = true ] && exit 0
|
|
done
|
|
exit 1
|
|
)
|
|
}
|
|
|
|
integrate
|
|
exec "$APPDIR/usr/bin/artcraft-launcher" "$@"
|