By Morgan Davis
13 Mar 2026
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 --backupbefore 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"
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.