RSS Amplifier

Morgan Davis · Mar 14, 2026

Managing Web App Launchers on Linux Mint

0
Sign in to vote or save

Morgan Davis · Morgan Davis

By Morgan Davis

13 Mar 2026

Web Apps icon

The Web Apps Tool is Excellent — Until You Want to Switch Browsers

If you use Linux Mint, you've probably discovered the Web Apps tool. It's one of those small, thoughtful utilities that makes Mint feel polished: point it at a URL, pick a browser and an icon, and it creates a dedicated .desktop launcher that opens the site in its own window — no tabs, no toolbar, just the app. It's a clean way to turn frequently-used websites into first-class desktop citizens.

I use it for everything — Home Assistant, Jellyfin, Google Voice, Speedtest, and a dozen others. It works great. Right up until you want to switch browsers.

The Problem With Switching

ICE (the underlying engine behind the Web Apps tool) hardcodes the browser binary and its profile path directly into each launcher's Exec= line. That's fine when you set things up, but if you later want to switch from Vivaldi to Firefox — or the other way around — you're staring down a list of launcher files that all need updating, one at a time. The Web Apps user interface doesn't even allow you to change the browser on an existing entry. You're left with editing the .desktop files by hand, or deleting the entries replacing them one at a time. What a pain!

Editing them by hand is tedious and error-prone. The Exec= format is also completely different between Firefox and Chromium-based browsers. Firefox uses a sh -c wrapper with a dedicated --profile directory and --no-remote. Chromium-based browsers use --app= with --user-data-dir. It's not just swapping a binary name — it's reconstructing the entire command.

Sure, you can tell Mint that you have a new default browser, but it doesn't update all your Web Apps to make use of it as you'd expect. I even checked to see if there is an existing tool that made switching to a new browser easier, but found nothing. So, as usual, it has come to this…

Fine! I'll Do It Myself!

I wrote manage-webapps.sh to handle all of it. Rather than trying to parse and transform existing Exec= lines — which is fragile — it reads the metadata fields ICE already writes into each file (X-WebApp-URL, StartupWMClass, X-WebApp-Isolated, Icon) and reconstructs the Exec= line from scratch in the correct format for the target browser.

It supports Chromium, Firefox, Vivaldi, Brave, and Edge. It handles profile isolation, the XAPP custom icon wrapper, and the per-browser profile path conventions ICE uses. Run it with no arguments and it shows you the current state of all your launchers. Point it at a browser and it converts everything in one shot.

Usage: manage-webapps.sh [ options ]
  --browser <n>   Set browser to chromium, firefox, vivaldi, brave, edge
  --isolate-on    Enable sandboxed profile isolation
  --isolate-off   Disable sandboxed profile isolation
  --backup        Copy all WebApp .desktop files to .desktop.bak
  --restore       Move .bak files back and refresh the desktop database
  --clean         Remove all .desktop.bak backup files
  --help, -h      Show this help

Options combine freely. --backup before a conversion gives you a safety net. --restore brings everything back if something goes sideways. The output always ends with a full status listing — -> for files that were just changed, = for everything else — so you can see exactly what happened.

A Note on Compatibility

The script requires ICE/Web Apps Manager, which ships with Linux Mint and is installable on other Ubuntu-based distros. Beyond that, it works on any freedesktop.org-compliant Linux desktop (GNOME, KDE, XFCE, Cinnamon, MATE) — anything that uses ~/.local/share/applications and update-desktop-database. It's POSIX shell, no dependencies beyond what any standard Linux system already has.

Back up first. Run manage-webapps.sh --backup before doing any conversion. ICE launchers are easy to regenerate from scratch through the Web Apps UI, but having a one-command restore is nicer.

manage-webapps.sh

#!/bin/sh
# manage-webapps.sh
#
# Manage ICE/WebApp Manager .desktop launchers. Converts between browser
# formats, toggles profile isolation, backs up/restores launchers, and
# displays current settings.
#
# Exec= lines are reconstructed from metadata fields (X-WebApp-URL,
# StartupWMClass, X-WebApp-Isolated, Icon) rather than transformed in place.
#
# If a launcher Exec= contains XAPP_FORCE_GTKWINDOW_ICON, the sh -c wrapper
# is preserved -- it is an ICE custom-icon feature, not browser-specific.
#
# Firefox profiles: ~/.local/share/ice/firefox/<AppID>
# Chromium-family profiles: ~/.local/share/ice/profiles/<AppID>
# Isolation omits profile flags entirely when disabled.
#
# Compatibility: requires ICE/Web Apps Manager (ships with Linux Mint;
# installable on other Ubuntu-based distros). Works on any freedesktop.org-
# compliant desktop (GNOME, KDE, XFCE, Cinnamon, MATE, etc.) that uses
# ~/.local/share/applications and update-desktop-database.
set -eu
# ANSI colour codes -- set via printf to ensure escape interpretation
R=$(printf '\033[0;31m')
G=$(printf '\033[0;32m')
Y=$(printf '\033[0;33m')
B=$(printf '\033[0;34m')
C=$(printf '\033[0;36m')
W=$(printf '\033[1;37m')
N=$(printf '\033[0m')
SELF=$(basename "$0")
print_help() {
    printf "%sUsage:%s %s [ options ]\n" "$W" "$N" "$SELF"
    printf "\n"
    printf "  %s--browser <n>   %sSet browser to chromium, firefox, vivaldi, brave, edge\n"  "$C" "$N"
    printf "  %s--isolate-on    %sEnable sandboxed profile isolation\n"                      "$C" "$N"
    printf "  %s--isolate-off   %sDisable sandboxed profile isolation\n"                     "$C" "$N"
    printf "  %s--backup        %sCopy all WebApp .desktop files to .desktop.bak\n"          "$C" "$N"
    printf "  %s--restore       %sMove .bak files back and refresh the desktop database\n"   "$C" "$N"
    printf "  %s--clean         %sRemove all .desktop.bak backup files\n"                    "$C" "$N"
    printf "  %s--help, -h      %sShow this help\n"                                          "$C" "$N"
}
help()  { print_help;     exit 0; }
usage() { print_help >&2; exit 1; }
die()    { printf "%serror:%s %s\n" "$R" "$N" "$1" >&2; exit 1; }
warn()   { printf "%swarn:%s  %s\n" "$Y" "$N" "$1" >&2; }
header() { printf "%s==>%s %s%s%s\n" "$B" "$N" "$W" "$1" "$N"; }
# get_field <file> <FieldName>
get_field() { sed -n "s|^${2}=||p" "$1" | head -n1; }
# Resolve the brave binary: prefer brave-browser, fall back to brave
brave_cmd() {
    command -v brave-browser >/dev/null 2>&1 && printf "brave-browser" || printf "brave"
}
# Parse arguments
ISOLATE_OVERRIDE=""
BROWSER=""
BACKUP=0
RESTORE=0
CLEAN=0
while [ $# -gt 0 ]; do
    case "$1" in
        --isolate-on)  ISOLATE_OVERRIDE="true";  shift ;;
        --isolate-off) ISOLATE_OVERRIDE="false"; shift ;;
        --backup)      BACKUP=1;                 shift ;;
        --restore)     RESTORE=1;                shift ;;
        --clean)       CLEAN=1;                  shift ;;
        --browser)
            [ $# -lt 2 ] && die "--browser requires a value: chromium, firefox, vivaldi, brave, or edge"
            case "$2" in
                chromium|firefox|vivaldi|brave|edge) BROWSER="$2" ;;
                *) die "--browser: unknown browser '$2'. Valid: chromium, firefox, vivaldi, brave, edge" ;;
            esac
            shift 2
            ;;
        -h|--help) help ;;
        *) printf "%serror:%s unknown argument: %s\n\n" "$R" "$N" "$1" >&2; usage ;;
    esac
done
# Validate flag combinations
if [ "$BACKUP" -eq 1 ] && [ "$RESTORE" -eq 1 ]; then
    die "--backup and --restore are mutually exclusive"
fi
APPS_DIR="${HOME}/.local/share/applications"
cd "$APPS_DIR" || die "cannot cd to $APPS_DIR"
CHANGED=""
SUMMARY=""
# ---------------------------------------------------------------------------
# --backup: copy .desktop files to .bak
# ---------------------------------------------------------------------------
if [ "$BACKUP" -eq 1 ]; then
    backed=0
    for file in WebApp-*.desktop webapp-*.desktop; do
        [ -f "$file" ] || continue
        cp "$file" "${file}.bak" || { warn "could not back up $file"; continue; }
        backed=$((backed+1))
    done
    if [ "$backed" -eq 0 ]; then
        warn "no matching .desktop files found in $APPS_DIR"
    else
        SUMMARY="${SUMMARY}    ✓ ${backed} file(s) backed up\n"
    fi
fi
# ---------------------------------------------------------------------------
# --restore: move .bak files back into place and refresh the database
# ---------------------------------------------------------------------------
if [ "$RESTORE" -eq 1 ]; then
    restored=0
    for bak in WebApp-*.desktop.bak webapp-*.desktop.bak; do
        [ -f "$bak" ] || continue
        target="${bak%.bak}"
        mv "$bak" "$target" || { warn "could not restore $bak"; continue; }
        CHANGED="${CHANGED}${target}
"
        restored=$((restored+1))
    done
    if [ "$restored" -eq 0 ]; then
        warn "no .bak files found in $APPS_DIR"
    else
        update-desktop-database "$APPS_DIR" || die "update-desktop-database failed"
        SUMMARY="${SUMMARY}    ✓ ${restored} file(s) restored\n"
    fi
fi
# ---------------------------------------------------------------------------
# --clean: remove .bak files
# ---------------------------------------------------------------------------
if [ "$CLEAN" -eq 1 ]; then
    cleaned=0
    for bak in WebApp-*.desktop.bak webapp-*.desktop.bak; do
        [ -f "$bak" ] || continue
        rm "$bak" || { warn "could not remove $bak"; continue; }
        cleaned=$((cleaned+1))
    done
    if [ "$cleaned" -eq 0 ]; then
        warn "no .bak files found in $APPS_DIR"
    else
        SUMMARY="${SUMMARY}    ✓ ${cleaned} backup(s) removed\n"
    fi
fi
# ---------------------------------------------------------------------------
# Conversion: update browser and/or isolation state
# ---------------------------------------------------------------------------
if [ -n "$BROWSER" ] || [ -n "$ISOLATE_OVERRIDE" ]; then
    TARGET_CMD=""
    TARGET_DISPLAY=""
    case "$BROWSER" in
        chromium) TARGET_CMD="chromium";       TARGET_DISPLAY="Chromium" ;;
        firefox)  TARGET_CMD="firefox";        TARGET_DISPLAY="Firefox"  ;;
        vivaldi)  TARGET_CMD="vivaldi-stable"; TARGET_DISPLAY="Vivaldi"  ;;
        brave)    TARGET_CMD=$(brave_cmd);     TARGET_DISPLAY="Brave"    ;;
        edge)     TARGET_CMD="microsoft-edge"; TARGET_DISPLAY="Edge"     ;;
    esac
    ICE_DIR="${HOME}/.local/share/ice"
    processed=0
    skipped=0
    if [ -n "$TARGET_DISPLAY" ]; then
        printf "%s==>%s %sConverting to %s%s%s (%s)%s\n" \
            "$B" "$N" "$W" "$C" "$TARGET_DISPLAY" "$N" "$TARGET_CMD" "$N"
    else
        header "Updating WebApp launchers"
    fi
    [ -n "$ISOLATE_OVERRIDE" ] && printf "    %sIsolate:%s %s\n" "$W" "$N" "$ISOLATE_OVERRIDE"
    for file in WebApp-*.desktop webapp-*.desktop; do
        [ -f "$file" ] || continue
        WM_CLASS=$(get_field "$file" "StartupWMClass")
        URL=$(get_field "$file" "X-WebApp-URL")
        ICON=$(get_field "$file" "Icon")
        EXEC_LINE=$(get_field "$file" "Exec")
        ISOLATED_FIELD=$(get_field "$file" "X-WebApp-Isolated")
        CUR_BROWSER_FIELD=$(get_field "$file" "X-WebApp-Browser")
        if [ -z "$WM_CLASS" ] || [ -z "$URL" ]; then
            warn "$file: missing StartupWMClass or X-WebApp-URL, skipping"
            skipped=$((skipped+1))
            continue
        fi
        # Resolve effective browser: global arg takes precedence, else keep existing
        if [ -n "$TARGET_CMD" ]; then
            FILE_CMD="$TARGET_CMD"
            FILE_DISPLAY="$TARGET_DISPLAY"
        else
            case "$CUR_BROWSER_FIELD" in
                Chromium) FILE_CMD="chromium";       FILE_DISPLAY="Chromium" ;;
                Firefox)  FILE_CMD="firefox";        FILE_DISPLAY="Firefox"  ;;
                Vivaldi)  FILE_CMD="vivaldi-stable"; FILE_DISPLAY="Vivaldi"  ;;
                Brave)    FILE_CMD=$(brave_cmd);     FILE_DISPLAY="Brave"    ;;
                Edge)     FILE_CMD="microsoft-edge"; FILE_DISPLAY="Edge"     ;;
                *)
                    warn "$file: unrecognised X-WebApp-Browser '$CUR_BROWSER_FIELD', skipping"
                    skipped=$((skipped+1))
                    continue
                    ;;
            esac
        fi
        # Resolve effective isolation: override takes precedence, else keep existing
        if [ -n "$ISOLATE_OVERRIDE" ]; then
            ISOLATED="$ISOLATE_OVERRIDE"
        else
            case "$ISOLATED_FIELD" in
                true|True|1) ISOLATED="true"  ;;
                *)           ISOLATED="false" ;;
            esac
        fi
        APP_ID="${WM_CLASS#WebApp-}"
        # Build Exec= line for the target browser format
        case "$FILE_CMD" in
            firefox)
                PROFILE_DIR="${ICE_DIR}/firefox/${APP_ID}"
                if [ "$ISOLATED" = "true" ]; then
                    BROWSER_ARGS="${FILE_CMD} --class ${WM_CLASS} --name ${WM_CLASS} --profile ${PROFILE_DIR} --no-remote \"${URL}\""
                else
                    BROWSER_ARGS="${FILE_CMD} --class ${WM_CLASS} --name ${WM_CLASS} \"${URL}\""
                fi
                ;;
            chromium|vivaldi-stable|brave-browser|brave|microsoft-edge)
                PROFILE_DIR="${ICE_DIR}/profiles/${APP_ID}"
                if [ "$ISOLATED" = "true" ]; then
                    BROWSER_ARGS="${FILE_CMD} --app=\"${URL}\" --class=${WM_CLASS} --name=${WM_CLASS} --user-data-dir=${PROFILE_DIR}"
                else
                    BROWSER_ARGS="${FILE_CMD} --app=\"${URL}\" --class=${WM_CLASS} --name=${WM_CLASS}"
                fi
                ;;
        esac
        # Wrap in XAPP_FORCE_GTKWINDOW_ICON sh -c if the original used it (ICE custom icon feature)
        case "$EXEC_LINE" in
            *XAPP_FORCE_GTKWINDOW_ICON*)
                NEW_EXEC="sh -c 'XAPP_FORCE_GTKWINDOW_ICON=\"${ICON}\" ${BROWSER_ARGS}'" ;;
            *)
                NEW_EXEC="$BROWSER_ARGS" ;;
        esac
        sed -i \
            -e "s|^Exec=.*|Exec=${NEW_EXEC}|" \
            -e "s|^X-WebApp-Browser=.*|X-WebApp-Browser=${FILE_DISPLAY}|" \
            -e "s|^X-WebApp-Isolated=.*|X-WebApp-Isolated=${ISOLATED}|" \
            "$file"
        if ! grep -q '^X-WebApp-Browser=' "$file"; then
            sed -i "/^\[Desktop Entry\]/a X-WebApp-Browser=${FILE_DISPLAY}" "$file"
        fi
        if ! grep -q '^X-WebApp-Isolated=' "$file"; then
            sed -i "/^\[Desktop Entry\]/a X-WebApp-Isolated=${ISOLATED}" "$file"
        fi
        CHANGED="${CHANGED}${file}
"
        processed=$((processed+1))
    done
    if [ "$processed" -eq 0 ]; then
        warn "no matching .desktop files found in $APPS_DIR"
    else
        update-desktop-database "$APPS_DIR" || die "update-desktop-database failed"
        SUMMARY="${SUMMARY}    ✓ ${processed} file(s) updated\n"
    fi
fi
# ---------------------------------------------------------------------------
# Final listing: always shown; -> for changed files, = for unchanged
# ---------------------------------------------------------------------------
header "WebApp launcher settings in $APPS_DIR"
found=0
for file in WebApp-*.desktop webapp-*.desktop; do
    [ -f "$file" ] || continue
    CUR_BROWSER=$(get_field "$file" "X-WebApp-Browser")
    CUR_ISOLATED=$(get_field "$file" "X-WebApp-Isolated")
    [ -z "$CUR_BROWSER" ] && CUR_BROWSER="unknown"
    case "$CUR_ISOLATED" in
        true|True|1)   CUR_ISOLATE="on"      ;;
        false|False|0) CUR_ISOLATE="off"     ;;
        *)             CUR_ISOLATE="unknown" ;;
    esac
    case "$CHANGED" in
        *"${file}"*) MC="$G" ; MK="->" ;;
        *)           MC="$C" ; MK="= " ;;
    esac
    printf "  %s%s%s %-44s %s=%s %s%s%s %s[%sisolate: %s%s]%s\n" \
        "$MC" "$MK" "$N" "$file" "$B" "$N" "$C" "$CUR_BROWSER" "$N" "$B" "$N" "$CUR_ISOLATE" "$B" "$N"
    found=$((found+1))
done
[ "$found" -eq 0 ] && warn "no matching .desktop files found in $APPS_DIR"
[ -n "$SUMMARY" ] && printf "\n%s==>%s %sDone.%s In %s:\n%b" "$B" "$N" "$G" "$N" "$APPS_DIR" "$SUMMARY"
Explore

Read the original on morgandavis.net

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.