#!/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" "$@"