webos-debloat: reversible debloat for rooted LG webOS TVs (hide bloat tiles, stop preloading, turn off promos/ACR)

webos-debloat

One shell script that makes a rooted LG webOS TV lighter and quieter:

  • hides bloat tiles and background apps from the launcher
  • stops preloading apps the TV keeps in RAM whether or not you open them
  • turns off home-screen promotions, promo videos, sports alerts, content recommendations and Live Plus (ACR)

Nothing is deleted and the root filesystem is never touched. It only edits LG's own app block list, the preload manager's policy and a few system settings. So every change can be undone, and firmware updates install as they would on a stock TV.

Status: tested on one TV, not everywhere. It was written on a 2026 LG OLED running webOS 26 (firmware 11.2, US region). On other models and versions it should work in principle: app IDs that aren't installed and settings keys that don't exist are skipped. But it hasn't been tried there. Start with --plan and --dry-run.

Requirements

  • Root on the TV via Homebrew Channel. Which rooting method works depends on your firmware; see webosbrew.org.
  • A root shell on the TV, either SSH (recommended) or Homebrew Channel's telnet.
  • For --install-boot-hook, Homebrew Channel's startup scripts must be enabled. They run /var/lib/webosbrew/init.d/* at boot.

Get the script onto the TV

With SSH enabled in Homebrew Channel:

ssh root@ 'cat > /tmp/webos-debloat.sh' < webos-debloat.sh

With telnet only, serve this folder from your computer and fetch the script from the TV:

python3 -m http.server 8000                     # on your computer
curl -o /tmp/webos-debloat.sh http://:8000/webos-debloat.sh   # on the TV

/tmp is wiped on reboot. That's fine: --install-boot-hook copies the script to /var/lib/webos-debloat/.

Quick start

On the TV:

sh /tmp/webos-debloat.sh --list               # what's installed
sh /tmp/webos-debloat.sh --plan               # what --apply would hide
sh /tmp/webos-debloat.sh --apply              # hide the default categories
sh /tmp/webos-debloat.sh --preload-off        # the step that actually frees RAM
sh /tmp/webos-debloat.sh --settings-off       # promos, alerts, recommendations, ACR
sh /tmp/webos-debloat.sh --install-boot-hook  # keep it applied across reboots
sh /tmp/webos-debloat.sh --status

To see the effect, run grep MemAvailable /proc/meminfo before and after.

Categories

--apply hides the default categories. Pick others with --only a,b,c, or hide single apps with --id .

category what default
ad ACR, ad overlays, "food delivery" overlays, video ads yes
sdk example apps that ship in the firmware yes
demo store demo, factory and diagnostics apps yes
remote LG remote-support service yes
promo LG Channels, Sports, Music, Photo & Video, user guide, weather no
visible launcher tiles: LG Channels, Home Hub, Premium, Multi View settings, … no
extras AI/voice assistant, membership, family care, customer support, Remote PC, … no
all all of the above no

extras hides the voice assistant and LG account pages, so skip it if you use them. Some apps are never hidden, whatever you ask for (PROTECTED_IDS in the script): Home, Settings, Live TV, inputs, the browser, first-run setup, Homebrew Channel and anything else whose absence is known to break the TV's own UI.

Undo

S=/var/lib/webos-debloat/webos-debloat.sh   # or wherever your copy is
sh $S --remove-boot-hook
sh $S --restore            # un-hide what this script hid (LG's own blocks stay)
sh $S --preload-on
sh $S --settings-restore   # restores the values from before --settings-off

The first run backs up the files it edits to /var/lib/webos-debloat/*.orig and never overwrites those backups.

Things learned along the way

  • Hiding is mostly cosmetic. It removes tiles from the app list but doesn't stop processes or free memory. On webOS 26 it also doesn't clean up the content rows inside LG Home.
  • The memory goes to preloading. The preload manager keeps some apps resident before anyone opens them. List them with luna-send -n 1 luna://com.webos.service.preloadmanager/getPreloadPolicy '{}'.
    • setPreloadPolicy only lasts until the preload manager reloads, which on webOS 26 also happens a few minutes after boot.
    • The allowedApps map in /var/preferences/webos-preload-manager-conf.json is what persists.
    • The script sets both, and the boot hook replays them.
    • It leaves Home, Live TV and the HDMI inputs preloaded. That is what keeps the Home button and input switching fast.
  • "Off" is usually suspend. With the default power settings, turning the TV off suspends it. Boot hooks run only after a real reboot, not on power-on. Everything this script changes is stored in /var, so it doesn't matter here. If you write your own hooks, keep it in mind: on resume webOS launches LG Home with "reason":"boot", which you can watch for with luna-send -i luna://com.webos.service.applicationmanager/getAppLifeEvents '{"subscribe":true}'.
  • Settings keys live in /etc/palm/description.categorykeysmap.json (which category each key belongs to) and /etc/palm/defaultSettings.json. That's the place to look for more things to switch off.

Security note

Homebrew Channel's telnet is an unauthenticated root shell open to your whole network. Once you're set up, enable SSH with a key in Homebrew Channel's settings and turn telnet off.

License

MIT. See LICENSE, or the SPDX header in webos-debloat.sh if you only grabbed the script. Not affiliated with LG. Rooting your TV may void its warranty; use at your own risk.

#!/bin/sh
# webos-debloat.sh: reversible, firmware-update-safe debloat for rooted LG webOS TVs.
# SPDX-License-Identifier: MIT
#
# DESIGN RULES (read this before changing anything):
# 1. NOTHING IS DELETED. No app directory is removed, no rootfs file is
# written, no binary is replaced. The script only edits LG's own
# "blocked system app list" JSON, which the application manager reads
# when it renders the launcher, plus the preload manager's policy and
# a handful of system settings.
# 2. Because the rootfs is untouched, OTA firmware updates, app-store
# updates and the recovery path all behave exactly as stock. That is
# the whole point of using this mechanism instead of `dev/remove`.
# 3. Every change is reversible (--restore, --preload-on, --settings-restore).
# 4. Original files are backed up exactly once and never overwritten, so a
# later bad run cannot quietly destroy the audit trail.
#
# WHAT IT CHANGES
# Hiding: /var/preferences/com.webos.applicationManager/blockedSystemAppList/.json
# contains {"blocked_system_applist":["com.webos.app.x", ...]}
# Any app ID in that array is hidden from the launcher. The binaries stay
# on disk and launch normally if something else calls them. Removing the
# ID from the array brings the tile back. This is LG's own regional app
# blocking mechanism, not a hack. The application manager may rewrite the
# file on its own (e.g. after a component update), so `--install-boot-hook`
# re-applies it at boot.
#
# Preloads: hiding is cosmetic; it does NOT stop a process or free memory.
# The preload manager keeps some apps (LG Channels, the browser) resident
# whether or not they are visible or ever opened. `--preload-off` turns
# that off, which is what actually frees RAM.
#
# Settings: `--settings-off` switches off home-screen promotions, promo
# videos, sports alerts and content recommendations through the settings
# service. These persist on their own.
#
# USAGE
# sh webos-debloat.sh --list # inventory installed apps
# sh webos-debloat.sh --plan # show what --apply would hide
# sh webos-debloat.sh --apply # hide the default categories
# sh webos-debloat.sh --apply --only ad,sdk,demo,remote,promo
# sh webos-debloat.sh --apply --only visible # visible tiles
# sh webos-debloat.sh --apply --only extras # tracking/AI bloat
# sh webos-debloat.sh --apply --only all # everything
# sh webos-debloat.sh --apply --id com.webos.app.lgchannels # single app
# sh webos-debloat.sh --restore --id com.webos.app.lgchannels # undo that one
# sh webos-debloat.sh --restore # un-hide everything we added
# sh webos-debloat.sh --status # what is currently changed
# sh webos-debloat.sh --install-boot-hook # re-apply at every boot
# sh webos-debloat.sh --remove-boot-hook
# sh webos-debloat.sh --preload-off # stop preloading LG Channels + browser
# sh webos-debloat.sh --preload-off --id com.webos.app.livemenu
# sh webos-debloat.sh --preload-on # undo every --preload-off
# sh webos-debloat.sh --settings-off # promos, sports alerts, recommendations
# sh webos-debloat.sh --settings-restore # put those settings back
#
# --dry-run previews --apply, --restore, --preload-* and --settings-*
# without changing anything. --region XX picks the list file when the TV
# has none yet (default USA). --preload-sync is what the boot hook runs.
#
# REQUIREMENTS
# Root on the TV (via Homebrew Channel). Run it on the TV itself, e.g.:
# ssh root@ 'sh /tmp/webos-debloat.sh --list'
#
# SAFETY
# The PROTECTED_IDS list below is never hidden regardless of what you ask
# for. It contains every app whose removal has a known way of breaking the
# TV's own UI (settings panel, input switching, first-run flow, launcher).
# ID lists are whitespace-separated strings and are split on purpose.
# shellcheck disable=SC2086
set -u
APPS_DIR=/var/preferences/com.webos.applicationManager/blockedSystemAppList
BACKUP_DIR=/var/lib/webos-debloat
HOOK=/var/lib/webosbrew/init.d/50-debloat
STATE=$BACKUP_DIR/state
PRELOAD_STATE=$BACKUP_DIR/preload-off
PRELOAD_CONF=/var/preferences/webos-preload-manager-conf.json
PRELOAD_SVC=luna://com.webos.service.preloadmanager
SAM_SVC=luna://com.webos.service.applicationmanager
SETTINGS_SVC=luna://com.webos.settingsservice
SETTINGS_STATE=$BACKUP_DIR/settings-off
APP_BASES="
/media/system/apps/usr/palm/applications
/usr/palm/applications
/mnt/otncabi/usr/palm/applications
/mnt/otycabi/usr/palm/applications
"
DRY_RUN=0
CATS=""
EXTRA_IDS=""
# ---------------------------------------------------------------------------
# Never hide these. Each one has a known failure mode if it disappears.
# ---------------------------------------------------------------------------
PROTECTED_IDS='
com.webos.app.livetv
com.webos.app.home
com.webos.app.launcher
com.palm.app.settings
com.webos.app.settings
com.webos.app.quicksettings
com.webos.app.firstuse
com.webos.app.eula
com.webos.app.webapphost
com.webos.app.container
container
com.webos.app.inputcommon
inputcommon
com.webos.app.inputlist
com.webos.app.notificationcenter
com.webos.app.browser
com.webos.app.mediaplayer
com.webos.app.mvpd
com.webos.app.install
com.webos.app.screensaver
com.webos.app.volume
com.webos.app.epg
com.webos.app.tvguide
com.webos.app.hdmi1
com.webos.app.hdmi2
com.webos.app.hdmi3
com.webos.app.hdmi4
org.webosbrew.hbchannel
com.tvweb.dashboard
com.tvweb.home
'
# ---------------------------------------------------------------------------
# Curated bloat, grouped. Only IDs that are actually installed get written,
# so IDs from other webOS versions are simply skipped.
# ---------------------------------------------------------------------------
IDS_AD_ACR='
com.webos.app.acrcomponent
com.webos.app.acrhdmi1
com.webos.app.acrhdmi2
com.webos.app.acrhdmi3
com.webos.app.acrhdmi4
com.webos.app.acroverlay
com.webos.app.adoverlay
com.webos.app.adoverlayex
com.webos.app.adhdmi1
com.webos.app.adhdmi2
com.webos.app.adhdmi3
com.webos.app.adhdmi4
com.webos.app.fooddelivery
com.webos.app.fooddeliveryex
com.webos.app.fooddeliveryhdmi1
com.webos.app.fooddeliveryhdmi2
com.webos.app.fooddeliveryhdmi3
com.webos.app.fooddeliveryhdmi4
com.webos.app.overlaymembership
com.webos.app.videoads
com.webos.app.cmp-client
com.webos.app.newandhot
'
IDS_REMOTE='
com.webos.app.remoteservice
'
IDS_SDK='
com.webos.exampleapp.enyoapp.epg
com.webos.exampleapp.groupowner
com.webos.exampleapp.nav
com.webos.exampleapp.qmlapp.client.negative.one
com.webos.exampleapp.qmlapp.client.negative.two
com.webos.exampleapp.qmlapp.client.positive.one
com.webos.exampleapp.qmlapp.client.positive.two
com.webos.exampleapp.qmlapp.discover
com.webos.exampleapp.qmlapp.epg
com.webos.exampleapp.qmlapp.hbbtv
com.webos.exampleapp.qmlapp.livetv
com.webos.exampleapp.qmlapp.search
com.webos.exampleapp.systemui
'
IDS_DEMO='
com.webos.app.store-demo
com.webos.app.sync-demo
com.webos.app.factorywin
com.webos.app.svcdiagnostics
com.webos.app.quickrecovery
com.webos.app.renewupdate
'
# Opt-in only: these are promotional / content-discovery tiles. Hiding them is
# safe but some people actually use the guide and the music player, so they
# are not in the default set.
IDS_PROMO='
com.webos.app.discovery
com.webos.app.lgchannels
com.webos.app.channelplus
com.webos.app.sportsevent
com.webos.app.music
com.webos.app.photovideo
com.webos.app.userguide
com.webos.app.help
com.webos.app.today
com.webos.app.recent
com.webos.app.accuweather
com.webos.app.tvplus
com.webos.app.manufacturer
'
# Visible launcher tiles that are safe to hide. These are the tiles people
# actually see; the default categories above mostly target background
# services. Essentials (Home, Settings, Browser, inputs, etc.) are in
# PROTECTED_IDS and are never hidden even if listed here.
IDS_VISIBLE='
com.webos.app.lgchannels
com.lge.app.viewster
com.webos.app.homeconnect
com.webos.app.lglink
com.webos.app.premium
com.webos.app.lifeonscreen
com.webos.app.lifeonscreen-legacy
com.webos.app.quicklinkcopilot
com.webos.app.sportsteamsettings
com.webos.app.discovery
com.webos.app.storagemanager
com.webos.app.camera
com.webos.app.multiviewsettings
com.webos.app.mediadiscovery
'
# Opt-in: additional tracking / AI / promo / support apps that are installed
# but invisible in the launcher. These are safe to hide, but some are
# features (LG Account, Remote PC, mobile connection) so they are not in
# the default set. Inputs, accessibility, setup, keyboard, updates, casting
# and TV tuning are deliberately excluded.
IDS_EXTRAS='
com.webos.app.adapp
com.webos.app.aiagent
com.webos.app.aiplatform
com.webos.app.aiplatformsupport
com.webos.app.aipromotion
com.webos.app.assistant
com.webos.app.voice
com.webos.app.beanbrowser
com.webos.app.crb
com.webos.app.livedmost
com.webos.app.liveginga
com.webos.app.livehbbtv
com.webos.app.liveinteractivecontent
com.webos.app.masterbrand
com.webos.app.magicnum
com.webos.app.cpcover
com.webos.app.store-streamer
com.webos.app.universalqcard
com.webos.app.totalmusic
com.webos.app.tvuserguide
com.webos.app.helpandtips
com.webos.app.membership
com.webos.app.minimalmembership
com.webos.app.familycare
com.webos.app.learning
com.webos.app.homeoffice
com.webos.app.sportsalarm
com.webos.app.self-diagnosis
com.webos.app.customersupport
com.webos.app.remotecontrolguide
com.webos.app.seniorhome
com.webos.app.simplecalendar
com.webos.app.similarlist
com.webos.app.homeconnect-overlay
com.webos.app.rdp
com.webos.app.mobileconnection
'
DEFAULT_CATS="ad,sdk,demo,remote"
# Default --preload-off targets: apps the preload manager keeps resident in
# RAM even when nobody opens them. Disabling a preload only costs a slower
# first launch. Home, Live TV and HDMI preloads are deliberately left alone:
# they are what make the Home button and input switching fast.
PRELOAD_OFF_IDS='
com.webos.app.lgchannels
com.webos.app.browser
'
# --settings-off targets, as settings-service category:key. All take "on" or
# "off"; keys this firmware doesn't have are skipped. livePlus is LG's
# automatic content recognition (ACR). Deliberately left alone: quickStartMode
# (fast power-on) and homeAutoLaunch (open Home at power-on).
SETTINGS_OFF='
general:homePromotion
general:sportsAlarm
option:livePromotion
option:webOSPromotionVideo
option:promotionStreamer
option:livePlus
other:contentRecommendation
'
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
log() { printf '%s\n' "$*"; }
warn() { printf 'WARN: %s\n' "$*" >&2; }
err() { printf 'ERROR: %s\n' "$*" >&2; }
die() { err "$*"; exit 1; }
require_root() {
[ "$(id -u 2>/dev/null)" = "0" ] || die "must run as root on the TV"
}
has_cmd() { command -v "$1" >/dev/null 2>&1; }
# the leading comment block, minus the shebang
usage() { awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "$0"; }
# one ID per line out of the JSON array, tolerant of pretty-printing
read_ids() {
[ -f "$1" ] || return 0
awk '
{ buf = buf $0 "\n" }
END {
i = index(buf, "\"blocked_system_applist\"")
if (i > 0) {
rest = substr(buf, i)
j = index(rest, "[")
if (j > 0) {
rest = substr(rest, j + 1)
k = index(rest, "]")
if (k > 0) {
arr = substr(rest, 1, k - 1)
n = split(arr, parts, ",")
for (p = 1; p <= n; p++) {
s = parts[p]
gsub(/[ \t\r\n]/, "", s)
gsub(/"/, "", s)
if (s != "") print s
}
}
}
}
}' "$1"
}
write_ids() {
# $1 = file, remaining args = ids
file=$1; shift
body=""
for id in "$@"; do
[ -n "$id" ] || continue
if [ -n "$body" ]; then body="$body,\"$id\""; else body="\"$id\""; fi
done
tmp="$file.tmp.$"
printf '{"blocked_system_applist":[%s]}\n' "$body" > "$tmp" || die "cannot write $tmp"
mv -f "$tmp" "$file" || die "cannot replace $file"
}
in_list() {
needle=$1; shift
for x in "$@"; do [ "$x" = "$needle" ] && return 0; done
return 1
}
is_protected() { in_list "$1" $PROTECTED_IDS; }
# does the app actually exist on this TV?
app_present() {
id=$1
for base in $APP_BASES; do
[ -f "$base/$id/appinfo.json" ] && return 0
done
[ -d "/media/cryptofs/apps/usr/palm/applications/$id" ] && return 0
return 1
}
appinfo_field() {
# $1 file, $2 key (string value)
sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$1" 2>/dev/null | head -n 1
}
app_visible() {
# prints yes/no
v=$(sed -n 's/.*"visible"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' "$1" 2>/dev/null | head -n 1)
[ "$v" = "false" ] && printf 'no\n' || printf 'yes\n'
}
pick_list_file() {
if [ -d "$APPS_DIR" ]; then
for f in "$APPS_DIR"/*.json; do
[ -e "$f" ] || continue
printf '%s\n' "$f"
return 0
done
fi
region=${WEBOS_REGION:-USA}
printf '%s/%s.json\n' "$APPS_DIR" "$region"
}
ensure_dir() {
[ -d "$APPS_DIR" ] && return 0
if [ "$DRY_RUN" = "1" ]; then
warn "dry-run: would create $APPS_DIR"
return 0
fi
mkdir -p "$APPS_DIR" 2>/dev/null || die "cannot create $APPS_DIR (is /var writable?)"
}
backup_once() {
file=$1
[ -f "$file" ] || return 0
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
base=$(basename "$file")
if [ ! -f "$BACKUP_DIR/$base.orig" ]; then
cp "$file" "$BACKUP_DIR/$base.orig" || warn "could not back up $file"
log "backed up original to $BACKUP_DIR/$base.orig"
fi
}
# ---------------------------------------------------------------------------
# --list
# ---------------------------------------------------------------------------
cmd_list() {
log "Installed apps (appid | visible | title | source dir)"
log "--------------------------------------------------------------------"
seen=""
for base in $APP_BASES; do
[ -d "$base" ] || continue
for d in "$base"/*; do
[ -d "$d" ] || continue
[ -f "$d/appinfo.json" ] || continue
id=$(appinfo_field "$d/appinfo.json" id)
[ -n "$id" ] || id=$(basename "$d")
case " $seen " in *" $id "*) continue;; esac
seen="$seen $id"
title=$(appinfo_field "$d/appinfo.json" title)
vis=$(app_visible "$d/appinfo.json")
printf '%-42s %-4s %-28s %s\n' "$id" "$vis" "${title:--}" "$(basename "$base")"
done
done
log "--------------------------------------------------------------------"
log "Total: $(printf '%s\n' $seen | wc -l | tr -d ' ') apps"
}
# ---------------------------------------------------------------------------
# category resolution
# ---------------------------------------------------------------------------
resolve_cats() {
# A bare --id means "only this app", not "default categories plus this app".
if [ -z "$CATS" ] && [ -n "$EXTRA_IDS" ]; then
printf '%s\n' $EXTRA_IDS
return 0
fi
[ -n "$CATS" ] || CATS=$DEFAULT_CATS
wanted=""
old_ifs=$IFS; IFS=','
for c in $CATS; do
case "$c" in
ad) wanted="$wanted $IDS_AD_ACR" ;;
remote) wanted="$wanted $IDS_REMOTE" ;;
sdk) wanted="$wanted $IDS_SDK" ;;
demo) wanted="$wanted $IDS_DEMO" ;;
promo) wanted="$wanted $IDS_PROMO" ;;
visible) wanted="$wanted $IDS_VISIBLE" ;;
extras) wanted="$wanted $IDS_EXTRAS" ;;
all) wanted="$wanted $IDS_AD_ACR $IDS_REMOTE $IDS_SDK $IDS_DEMO $IDS_PROMO $IDS_VISIBLE $IDS_EXTRAS" ;;
*) warn "unknown category '$c' (known: ad, remote, sdk, demo, promo, visible, extras, all)" ;;
esac
done
IFS=$old_ifs
printf '%s\n' $wanted $EXTRA_IDS
}
# IDs that --restore/--status treat as ours. With no selection this is every
# category, since earlier runs may have used --only all. IDs that LG itself
# blocked (present in the .orig backup) are never ours, so a restore can't
# un-hide something stock firmware hides.
our_ids() {
orig=$(read_ids "$BACKUP_DIR/$(basename "$1").orig")
if [ -z "$CATS" ] && [ -z "$EXTRA_IDS" ]; then
ids=$(CATS=all resolve_cats)
else
ids=$(resolve_cats)
fi
for id in $ids; do
in_list "$id" $orig || printf '%s\n' "$id"
done
}
# IDs from the chosen categories that exist here and are not protected
present_targets() {
seen=""
for id in $(resolve_cats); do
case " $seen " in *" $id "*) continue;; esac
seen="$seen $id"
if is_protected "$id"; then
continue
fi
if app_present "$id"; then
printf '%s\n' "$id"
fi
done
}
# ---------------------------------------------------------------------------
# --plan
# ---------------------------------------------------------------------------
cmd_plan() {
file=$(pick_list_file)
log "Target list file : $file"
if [ -z "$CATS" ] && [ -n "$EXTRA_IDS" ]; then
log "Selection : --id only ($EXTRA_IDS)"
else
log "Categories : ${CATS:-$DEFAULT_CATS}${EXTRA_IDS:+ + id:$EXTRA_IDS}"
fi
log ""
n=0
for id in $(present_targets); do
title=""
for base in $APP_BASES; do
if [ -f "$base/$id/appinfo.json" ]; then
title=$(appinfo_field "$base/$id/appinfo.json" title)
break
fi
done
printf ' hide %-42s %s\n' "$id" "${title:--}"
n=$((n + 1))
done
log ""
log "Would hide $n app(s). Nothing was changed."
[ "$n" = "0" ] && warn "no target apps found - run --list to see what this TV actually has"
return 0
}
# ---------------------------------------------------------------------------
# --apply
# ---------------------------------------------------------------------------
cmd_apply() {
require_root
file=$(pick_list_file)
ensure_dir
backup_once "$file"
# existing (LG's own regional blocks) must be preserved
existing=$(read_ids "$file" | grep -v '^ || true)
targets=$(present_targets)
added=""
for id in $targets; do
if in_list "$id" $existing; then
continue
fi
added="$added $id"
done
if [ -z "$added" ]; then
log "Nothing new to hide."
return 0
fi
# merge: existing first, then ours, de-duplicated
merged=""
for id in $existing $targets; do
[ -n "$id" ] || continue
in_list "$id" $merged && continue
merged="$merged $id"
done
log "Hiding:"
for id in $added; do printf ' + %s\n' "$id"; done
if [ "$DRY_RUN" = "1" ]; then
warn "dry-run: not writing $file"
return 0
fi
write_ids "$file" $merged
log "Wrote $file"
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
printf 'applied=%s\nlast_run=%s\n' "$(date +%s 2>/dev/null || echo 0)" "$(date 2>/dev/null || echo unknown)" >> "$STATE"
log ""
log "Done. Open the launcher - tiles disappear without a reboot on most"
log "firmware. If they do not, a reboot (or Homebrew Channel restart) applies it."
if [ -z "$CATS" ] && [ -n "$EXTRA_IDS" ]; then
log "Undo with: sh $0 --restore --id $EXTRA_IDS"
else
log "Undo any time with: sh $0 --restore"
fi
}
# ---------------------------------------------------------------------------
# --restore
# ---------------------------------------------------------------------------
cmd_restore() {
require_root
file=$(pick_list_file)
[ -f "$file" ] || { log "No list file at $file - nothing to restore."; return 0; }
ours=$(our_ids "$file")
keep=""
removed=0
for id in $(read_ids "$file"); do
if in_list "$id" $ours; then
removed=$((removed + 1))
continue
fi
keep="$keep $id"
done
if [ "$removed" = "0" ]; then
log "Nothing of ours is currently hidden."
return 0
fi
log "Un-hiding $removed app(s)."
if [ "$DRY_RUN" = "1" ]; then
warn "dry-run: not writing $file"
return 0
fi
write_ids "$file" $keep
log "Wrote $file"
[ -x "$HOOK" ] && warn "boot hook still installed and will re-hide at next boot; run --remove-boot-hook"
return 0
}
# ---------------------------------------------------------------------------
# --status
# ---------------------------------------------------------------------------
cmd_status() {
file=$(pick_list_file)
log "List file: $file"
if [ ! -f "$file" ]; then
log " (does not exist yet - nothing is hidden)"
return 0
fi
ids=$(read_ids "$file")
if [ -z "$ids" ]; then
log " (empty list)"
return 0
fi
ours=$(our_ids "$file")
n=0
for id in $ids; do
mark="LG/other"
in_list "$id" $ours && mark="ours"
printf ' %-42s %s\n' "$id" "$mark"
n=$((n + 1))
done
log "Total hidden: $n"
if [ -f "$BACKUP_DIR/$(basename "$file").orig" ]; then
log "Backup : $BACKUP_DIR/$(basename "$file").orig"
fi
if [ -x "$HOOK" ]; then
log "Boot hook : installed ($HOOK)"
else
log "Boot hook : not installed"
fi
if [ -s "$PRELOAD_STATE" ]; then
log "Preload off : $(tr '\n' ' ' < "$PRELOAD_STATE")"
fi
if [ -s "$SETTINGS_STATE" ]; then
log "Settings : originals saved for $(awk '{ print $2 }' "$SETTINGS_STATE" | tr '\n' ' ')"
fi
}
# ---------------------------------------------------------------------------
# --preload-off / --preload-on
# ---------------------------------------------------------------------------
# Two levers, both needed:
# - setPreloadPolicy "isEnabled" (a missing key means false) acts at once but
# is runtime only; the preload manager resets it when it reloads, which
# on webOS 26 also happens a few minutes after boot.
# - the "allowedApps" map in $PRELOAD_CONF persists; the preload manager
# reads it when it starts, and false there keeps an app from preloading.
# The IDs we disabled are kept in $PRELOAD_STATE and the boot hook replays
# them with --preload-sync.
wait_preload_svc() {
tries=0
until luna-send -n 1 "$PRELOAD_SVC/getPreloadPolicy" '{}' 2>/dev/null | grep -q '"returnValue":[[:space:]]*true'; do
tries=$((tries + 1))
[ "$tries" -ge 30 ] && die "preload manager not responding"
sleep 2
done
}
set_preload() {
# $1 id, $2 true|false
r=$(luna-send -n 1 "$PRELOAD_SVC/setPreloadPolicy" "{\"application\":{\"id\":\"$1\",\"isEnabled\":$2}}" 2>&1)
case "$r" in
*'"returnValue":true'*) return 0 ;;
*) warn "setPreloadPolicy $1: $r"; return 1 ;;
esac
}
# mirror the change into the persisted allowedApps map (only for IDs the
# file already lists; the original is backed up to $BACKUP_DIR)
set_allowed() {
# $1 id, $2 true|false
[ -f "$PRELOAD_CONF" ] || return 0
backup_once "$PRELOAD_CONF"
id_re=$(printf '%s' "$1" | sed 's/\./\\./g')
if [ "$2" = false ]; then from=true; else from=false; fi
sed -i "s/\(\"$id_re\"[[:space:]]*:[[:space:]]*\)$from/\1$2/" "$PRELOAD_CONF"
}
# close an already-preloaded instance, unless the user is looking at it
close_preloaded() {
fg=$(luna-send -n 1 "$SAM_SVC/getForegroundAppInfo" '{}' 2>/dev/null)
case "$fg" in *"\"appId\":\"$1\""*) return 0 ;; esac
luna-send -n 1 "$SAM_SVC/running" '{}' 2>/dev/null | grep -q "\"$1\"" || return 0
luna-send -n 1 "$SAM_SVC/closeByAppId" "{\"id\":\"$1\"}" >/dev/null 2>&1 \
&& log " closed resident $1"
}
cmd_preload_off() {
require_root
has_cmd luna-send || die "luna-send not found"
ids=${EXTRA_IDS:-$PRELOAD_OFF_IDS}
if [ "$DRY_RUN" = "1" ]; then
for id in $ids; do log "dry-run: would disable preload for $id"; done
return 0
fi
wait_preload_svc
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
touch "$PRELOAD_STATE"
for id in $ids; do
set_preload "$id" false || continue
set_allowed "$id" false
log "preload off: $id"
grep -qx "$id" "$PRELOAD_STATE" || printf '%s\n' "$id" >> "$PRELOAD_STATE"
close_preloaded "$id"
done
}
cmd_preload_on() {
require_root
has_cmd luna-send || die "luna-send not found"
[ -f "$PRELOAD_STATE" ] || { log "Nothing of ours has preload disabled."; return 0; }
ids=${EXTRA_IDS:-$(cat "$PRELOAD_STATE")}
if [ "$DRY_RUN" = "1" ]; then
for id in $ids; do log "dry-run: would re-enable preload for $id"; done
return 0
fi
wait_preload_svc
for id in $ids; do
set_preload "$id" true || continue
set_allowed "$id" true
log "preload on: $id"
grep -vx "$id" "$PRELOAD_STATE" > "$PRELOAD_STATE.tmp" || true
mv -f "$PRELOAD_STATE.tmp" "$PRELOAD_STATE"
done
[ -s "$PRELOAD_STATE" ] || rm -f "$PRELOAD_STATE"
}
# boot-time replay of $PRELOAD_STATE; a no-op if nothing was disabled
cmd_preload_sync() {
[ -s "$PRELOAD_STATE" ] || return 0
EXTRA_IDS=$(cat "$PRELOAD_STATE")
cmd_preload_off
}
# ---------------------------------------------------------------------------
# --settings-off / --settings-restore
# ---------------------------------------------------------------------------
# $SETTINGS_STATE keeps "category key original-value" per line, written the
# first time a key is changed, so --settings-restore puts back whatever the
# TV had before (which is not always "on").
get_setting() {
# $1 category, $2 key; prints the value, or nothing if the key is absent
luna-send -n 1 "$SETTINGS_SVC/getSystemSettings" "{\"category\":\"$1\",\"keys\":[\"$2\"]}" 2>/dev/null \
| sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p"
}
set_setting() {
# $1 category, $2 key, $3 value
r=$(luna-send -n 1 "$SETTINGS_SVC/setSystemSettings" "{\"category\":\"$1\",\"settings\":{\"$2\":\"$3\"}}" 2>&1)
case "$r" in
*'"returnValue":true'*) return 0 ;;
*) warn "setSystemSettings $1/$2: $r"; return 1 ;;
esac
}
cmd_settings_off() {
require_root
has_cmd luna-send || die "luna-send not found"
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
for ck in $SETTINGS_OFF; do
category=${ck%%:*} key=${ck#*:}
cur=$(get_setting "$category" "$key")
if [ -z "$cur" ]; then
log " skip $category/$key (not on this firmware)"
continue
fi
if [ "$cur" = off ]; then
log " $category/$key already off"
continue
fi
if [ "$DRY_RUN" = "1" ]; then
log "dry-run: would turn off $category/$key (now $cur)"
continue
fi
grep -q "^$category $key " "$SETTINGS_STATE" 2>/dev/null \
|| printf '%s %s %s\n' "$category" "$key" "$cur" >> "$SETTINGS_STATE"
set_setting "$category" "$key" off && log "setting off: $category/$key (was $cur)"
done
}
cmd_settings_restore() {
require_root
has_cmd luna-send || die "luna-send not found"
[ -s "$SETTINGS_STATE" ] || { log "No settings of ours to restore."; return 0; }
while read -r category key val; do
[ -n "$val" ] || continue
if [ "$DRY_RUN" = "1" ]; then
log "dry-run: would set $category/$key back to $val"
continue
fi
set_setting "$category" "$key" "$val" && log "setting restored: $category/$key = $val"
done < "$SETTINGS_STATE"
[ "$DRY_RUN" = "1" ] || rm -f "$SETTINGS_STATE"
}
# ---------------------------------------------------------------------------
# boot hook
# ---------------------------------------------------------------------------
cmd_install_hook() {
require_root
mkdir -p "$BACKUP_DIR" 2>/dev/null || die "cannot create $BACKUP_DIR"
mkdir -p "$(dirname "$HOOK")" 2>/dev/null || die "cannot create $(dirname "$HOOK")"
# The hook must not point at /tmp, which is wiped on reboot, so keep a
# copy of this script in persistent storage and reference that.
self_src=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)/$(basename -- "$0")
[ -f "$self_src" ] || self_src=$0
persistent="$BACKUP_DIR/webos-debloat.sh"
if [ "$self_src" != "$persistent" ]; then
cp "$self_src" "$persistent" || die "cannot copy script to $persistent"
chmod 0755 "$persistent"
log "Installed script copy at $persistent"
fi
# Bake the current selection into the hook so a full "all" run stays
# full after a reboot instead of silently falling back to the default set.
apply_args=""
[ -n "$CATS" ] && apply_args="--only $CATS"
[ -n "$EXTRA_IDS" ] && apply_args="$apply_args --id $EXTRA_IDS"
cat > "$HOOK" <<EOF
#!/bin/sh
# Re-assert the debloat list at boot: the application manager can rewrite
# blockedSystemAppList on its own after a component update.
# Installed by webos-debloat.sh. Remove with --remove-boot-hook.
sleep 5
[ -x "$persistent" ] && "$persistent" --apply $apply_args >>"$BACKUP_DIR/boot.log" 2>&1
[ -x "$persistent" ] && "$persistent" --preload-sync >>"$BACKUP_DIR/boot.log" 2>&1
exit 0
EOF
chmod 0755 "$HOOK"
log "Installed boot hook at $HOOK (re-applies: ${apply_args:-default categories})"
}
cmd_remove_hook() {
require_root
if [ -f "$HOOK" ]; then
rm -f "$HOOK" && log "Removed $HOOK"
else
log "No boot hook installed."
fi
}
# ---------------------------------------------------------------------------
# arg parsing
# ---------------------------------------------------------------------------
CMD=""
while [ $# -gt 0 ]; do
case "$1" in
--list) CMD=list ;;
--plan) CMD=plan ;;
--apply) CMD=apply ;;
--restore) CMD=restore ;;
--status) CMD=status ;;
--install-boot-hook) CMD=hook ;;
--remove-boot-hook) CMD=unhook ;;
--preload-off) CMD=preload_off ;;
--preload-on) CMD=preload_on ;;
--preload-sync) CMD=preload_sync ;;
--settings-off) CMD=settings_off ;;
--settings-restore) CMD=settings_restore ;;
--dry-run) DRY_RUN=1 ;;
--only) shift; CATS=${1:-} ;;
--id) shift; EXTRA_IDS="$EXTRA_IDS ${1:-}" ;;
--region) shift; WEBOS_REGION=${1:-} ;;
-h|--help)
usage; exit 0 ;;
*) die "unknown option: $1 (try --help)" ;;
esac
shift
done
[ -n "$CMD" ] || { usage; exit 1; }
case "$CMD" in
list) cmd_list ;;
plan) cmd_plan ;;
apply) cmd_apply ;;
restore) cmd_restore ;;
status) cmd_status ;;
hook) cmd_install_hook ;;
unhook) cmd_remove_hook ;;
preload_off) cmd_preload_off ;;
preload_on) cmd_preload_on ;;
preload_sync) cmd_preload_sync ;;
settings_off) cmd_settings_off ;;
settings_restore) cmd_settings_restore ;;
esac
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论