158 lines
5.9 KiB
Bash
Executable File
158 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Creates a Chromium webapp .desktop entry with favicon as icon.
|
|
# Usage: create-webapp.sh <url> [display-name]
|
|
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
echo "Usage: $(basename "$0") <url> [display-name]"
|
|
echo
|
|
echo " Fetches the page title and favicon, then writes a .desktop"
|
|
echo " entry that opens the URL in Chromium app-window mode (no tabs/address bar)."
|
|
exit 1
|
|
}
|
|
|
|
[[ $# -lt 1 ]] && usage
|
|
|
|
URL="$1"
|
|
CUSTOM_NAME="${2:-}"
|
|
|
|
# Prepend https:// if the user omitted the scheme, so curl/grep work correctly.
|
|
[[ "$URL" != http://* && "$URL" != https://* ]] && URL="https://$URL"
|
|
|
|
# Extract just the origin (scheme + host) — needed as a base for resolving
|
|
# relative favicon URLs later (e.g. "/icons/logo.png" → "https://example.com/icons/logo.png").
|
|
BASE_URL=$(echo "$URL" | grep -oE 'https?://[^/]+')
|
|
|
|
# Strip scheme and optional "www." to get a clean domain used as a fallback name.
|
|
DOMAIN=$(echo "$BASE_URL" | sed -E 's|https?://(www\.)?||')
|
|
|
|
echo "Fetching: $URL"
|
|
# -sL : silent + follow redirects. -A : spoof a browser User-Agent so sites
|
|
# don't return a stripped mobile/bot page with no <title> or <link> tags.
|
|
# The "|| echo ''" prevents pipefail from killing the script on network errors.
|
|
PAGE_HTML=$(curl -sL --max-time 15 \
|
|
-A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
|
|
"$URL" 2>/dev/null || echo "")
|
|
|
|
# Resolve app name from <title> or argument
|
|
if [[ -n "$CUSTOM_NAME" ]]; then
|
|
APP_NAME="$CUSTOM_NAME"
|
|
else
|
|
# Python inline: parse the <title> tag and HTML-unescape entities like &
|
|
# so the .desktop Name field looks clean. Falls back to DOMAIN on failure.
|
|
APP_NAME=$(echo "$PAGE_HTML" | python3 -c '
|
|
import sys, re, html as html_mod
|
|
content = sys.stdin.read()
|
|
m = re.search(r"<title[^>]*>([^<]+)</title>", content, re.IGNORECASE)
|
|
print(html_mod.unescape(m.group(1).strip()) if m else "", end="")
|
|
' 2>/dev/null || true)
|
|
APP_NAME="${APP_NAME:-$DOMAIN}"
|
|
fi
|
|
|
|
# Build a filesystem-safe identifier from the app name.
|
|
# tr '[:upper:]' '[:lower:]' : lowercase everything
|
|
# tr -cs 'a-z0-9' '-' : replace any run of non-alphanumeric chars with '-'
|
|
# sed 's/^-*//;s/-*$//' : trim leading/trailing hyphens
|
|
# Used for both the .desktop filename and the StartupWMClass (WM grouping key).
|
|
SAFE_ID=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-*//;s/-*$//')
|
|
[[ -z "$SAFE_ID" ]] && SAFE_ID="$DOMAIN"
|
|
|
|
# Find the best favicon declared in <link rel="icon"> tags.
|
|
# The Python snippet ranks candidates by their declared pixel size (sizes="NxN")
|
|
# and picks the largest — "any" is treated as 999 to rank SVG/vector icons highest.
|
|
# urljoin resolves relative paths against BASE_URL, matching browser behavior.
|
|
FAVICON_URL=$(echo "$PAGE_HTML" | python3 -c '
|
|
import sys, re
|
|
from urllib.parse import urljoin
|
|
|
|
base = sys.argv[1]
|
|
content = sys.stdin.read()
|
|
|
|
candidates = []
|
|
for link in re.findall(r"<link([^>]+?)/??>", content, re.IGNORECASE | re.DOTALL):
|
|
rel_m = re.search(r"rel=[\"'"'"']([^\"'"'"']+)[\"'"'"']", link, re.IGNORECASE)
|
|
if not rel_m or "icon" not in rel_m.group(1).lower():
|
|
continue
|
|
href_m = re.search(r"href=[\"'"'"']([^\"'"'"']+)[\"'"'"']", link, re.IGNORECASE)
|
|
if not href_m:
|
|
continue
|
|
size = 0
|
|
sizes_m = re.search(r"sizes=[\"'"'"']([^\"'"'"']+)[\"'"'"']", link, re.IGNORECASE)
|
|
if sizes_m:
|
|
val = sizes_m.group(1).lower().split("x")[0]
|
|
try:
|
|
size = 999 if val == "any" else int(val)
|
|
except ValueError:
|
|
pass
|
|
candidates.append((size, href_m.group(1)))
|
|
|
|
if candidates:
|
|
candidates.sort(key=lambda x: x[0], reverse=True)
|
|
print(urljoin(base, candidates[0][1]), end="")
|
|
' "$BASE_URL" 2>/dev/null || true)
|
|
|
|
# Fall back to the conventional /favicon.ico path when no <link> tag was found.
|
|
FAVICON_URL="${FAVICON_URL:-$BASE_URL/favicon.ico}"
|
|
echo "Favicon: $FAVICON_URL"
|
|
|
|
# Store webapp icons separately from system icons to keep ~/.local/share/icons clean.
|
|
ICON_DIR="$HOME/.local/share/icons/webapps"
|
|
mkdir -p "$ICON_DIR"
|
|
|
|
# Use a temp file so a failed download doesn't leave a partial file at the final path.
|
|
TMP=$(mktemp /tmp/webapp-icon.XXXXXX)
|
|
trap 'rm -f "$TMP"' EXIT
|
|
|
|
# Default: fall through to the chromium themed icon if download/convert fails.
|
|
ICON_PATH="chromium" # fallback
|
|
|
|
if curl -sL --max-time 10 -A "Mozilla/5.0" -o "$TMP" "$FAVICON_URL" && [[ -s "$TMP" ]]; then
|
|
# Prefer ImageMagick: convert normalizes the format (handles multi-frame .ico
|
|
# via [0]) and caps the size at 128x128 without upscaling (\> flag).
|
|
if command -v convert &>/dev/null \
|
|
&& convert "${TMP}[0]" -resize 128x128\> "${ICON_DIR}/${SAFE_ID}.png" 2>/dev/null; then
|
|
ICON_PATH="${ICON_DIR}/${SAFE_ID}.png"
|
|
echo "Icon: $ICON_PATH (PNG via ImageMagick)"
|
|
else
|
|
# Fallback: keep the file as-is, just fix the .ico MIME type alias so the
|
|
# extension matches what XDG icon loaders expect.
|
|
MIME=$(file -b --mime-type "$TMP")
|
|
EXT="${MIME##*/}"
|
|
[[ "$EXT" == "x-icon" || "$EXT" == "vnd.microsoft.icon" ]] && EXT="ico"
|
|
cp "$TMP" "${ICON_DIR}/${SAFE_ID}.${EXT}"
|
|
ICON_PATH="${ICON_DIR}/${SAFE_ID}.${EXT}"
|
|
echo "Icon: $ICON_PATH ($MIME)"
|
|
fi
|
|
else
|
|
echo "Warning: could not fetch favicon — using chromium default icon"
|
|
fi
|
|
|
|
# Write .desktop file per freedesktop.org Desktop Entry Specification.
|
|
# chromium --app=<url> opens the site in an app window (no address bar/tabs),
|
|
# making it feel like a native application.
|
|
DESKTOP_DIR="$HOME/.local/share/applications"
|
|
mkdir -p "$DESKTOP_DIR"
|
|
DESKTOP_FILE="${DESKTOP_DIR}/webapp-${SAFE_ID}.desktop"
|
|
|
|
cat > "$DESKTOP_FILE" << DESKTOP
|
|
[Desktop Entry]
|
|
Version=1.0
|
|
Type=Application
|
|
Name=${APP_NAME}
|
|
Exec=chromium --app=${URL}
|
|
Icon=${ICON_PATH}
|
|
Terminal=false
|
|
Categories=Network;WebBrowser;
|
|
StartupWMClass=${SAFE_ID}
|
|
DESKTOP
|
|
|
|
# Mark executable so the file manager and XDG launchers treat it as launchable.
|
|
chmod +x "$DESKTOP_FILE"
|
|
|
|
echo
|
|
echo "Created : $DESKTOP_FILE"
|
|
echo " Name : $APP_NAME"
|
|
echo " URL : $URL"
|
|
echo " Icon : $ICON_PATH"
|