#!/bin/bash # fix-lomiri-spinner-install.sh # # Fixes Lomiri's 30-40% idle CPU usage when X11/XMir apps (e.g. Firefox) run. # NO ROOT REQUIRED. Persists across reboots. Only touches files in $HOME. # # Usage: # ./fix-lomiri-spinner-install.sh # install and restart lomiri # ./fix-lomiri-spinner-install.sh uninstall # remove fix and restart lomiri # ./fix-lomiri-spinner-install.sh status # check fix state # # ─── THE BUG ──────────────────────────────────────────────────────────────── # # When an X11 app (e.g. Firefox) runs via XMir/Xwayland, Lomiri creates a # placeholder ApplicationWindow for it. The Xwayland surface arrives under a # different app identity ("xwayland.qtmir" instead of the real app ID), so # the placeholder never gets filled with the actual surface. Its splash screen # spins an ActivityIndicator forever — invisible behind Firefox, but its # RotationAnimator still drives the entire Qt render loop at 60fps (~33% CPU). # # ─── THE FIX ──────────────────────────────────────────────────────────────── # # We override ActivityIndicatorStyle.qml (the Lomiri theme file that defines # the spinning animation) to add a 15-second safety timeout. Normal apps # finish loading in <5 seconds, so the timeout only fires for stuck splash # screens. After 15 seconds the spinner stops, the animation driver goes idle, # and Lomiri drops to 0% CPU. # # ─── HOW IT WORKS ─────────────────────────────────────────────────────────── # # Qt's QML engine resolves "import Lomiri.Components 1.3" by searching # QML2_IMPORT_PATH directories (left to right) for a matching qmldir file. # We prepend our override directory, so Qt finds OUR qmldir first: # # QML2_IMPORT_PATH=/home/phablet/.local/share/lomiri-qml-fix # ↓ # .../lomiri-qml-fix/Lomiri/Components/Themes/Ambiance/qmldir # (found first → this module "wins" over the system one) # # Our qmldir is a copy of the system's, so it declares the same module # with the same type mappings. Since qmldir uses RELATIVE paths (e.g. # "./1.3/ActivityIndicatorStyle.qml"), Qt resolves them relative to OUR # directory — where we have our patched file. All other files are symlinks # back to the system originals, so they stay up to date. # # ─── DIRECTORY LAYOUT ─────────────────────────────────────────────────────── # # ~/.local/share/lomiri-qml-fix/Lomiri/Components/Themes/Ambiance/ # ├── qmldir ← copy of system's (module definition file) # ├── artwork/ ← symlink → system's artwork/ # │ └── spinner@{8,16,32}.png (DPI variants of the spinner image) # ├── 1.2/ ← symlink → system's 1.2/ (entire directory) # └── 1.3/ ← real directory (NOT a symlink, explained below) # ├── ActivityIndicatorStyle.qml ← OUR PATCHED FILE (the only real file) # ├── ButtonStyle.qml ← symlink → system's 1.3/ButtonStyle.qml # ├── SliderStyle.qml ← symlink → system's 1.3/SliderStyle.qml # └── ... (47 more symlinks to system 1.3/ files) # # WHY each part is needed: # # qmldir (copy, not symlink): # Must be a real file in our directory for Qt to "claim" this module. # If it were a symlink, Qt would follow it and resolve relative paths # against the system directory, defeating the whole override. # # artwork/ (symlink to system): # ActivityIndicatorStyle.qml loads "Qt.resolvedUrl("../artwork/spinner.png")". # The "../" resolves relative to the QML file's location, which is now our # 1.3/ dir. Without the artwork symlink at our level, the image 404s. # Other styles (ButtonStyle, ScrollbarStyle, etc.) also reference ../artwork/. # # 1.2/ (symlink to system): # The qmldir maps many types (API versions 0.1 through 1.2) to files in # the ./1.2/ subdirectory. These are older Lomiri Components API versions # that apps compiled against earlier SDKs still use. Without this symlink, # any app using "import Lomiri.Components 1.2" would get broken styles # (no buttons, no sliders, no text fields, etc.). # # 1.3/ (real dir with per-file symlinks): # We need to replace ONE file (ActivityIndicatorStyle.qml) while keeping # all ~48 other files from the system's 1.3/. We can't symlink the whole # directory (that would include the original file), so we create a real # directory and symlink each file individually — except our override. # # ─── UPDATE ROBUSTNESS ────────────────────────────────────────────────────── # # When the system lomiri-ui-toolkit package updates: # # ✓ Changes to any 1.2/ file: Transparent — our 1.2/ is a symlink to system. # ✓ Changes to any 1.3/ file OTHER than ActivityIndicatorStyle.qml: # Transparent — our per-file symlinks point to the system originals. # ✓ New files added to 1.3/: Harmless — they won't have symlinks in our dir, # but they'll only be missing from our override module, not from the system. # This only matters if the qmldir is also updated to reference them. # ⚠ Changes to 1.3/ActivityIndicatorStyle.qml: Our override masks the update. # This is extremely unlikely (the file hasn't changed in years), and even if # it did, our version is a strict superset (only adds a Timer). Use the # "status" command to detect this. # ⚠ Changes to qmldir (new types, new versions like 1.4): Our copy becomes # stale. The "status" command detects this. Re-running install refreshes it. # ⚠ If a new 1.4/ directory is added: Needs re-install to create symlinks. # The "status" command detects this too. # # TL;DR: Run "./fix-lomiri-spinner-install.sh status" after OTA updates. If # it says "stale", re-run install. In practice updates to this theme are rare. # set -euo pipefail # ─── Configuration ────────────────────────────────────────────────────────── SYSDIR=/usr/lib/aarch64-linux-gnu/qt5/qml/Lomiri/Components/Themes/Ambiance FIXBASE="$HOME/.local/share/lomiri-qml-fix" FIXDIR="$FIXBASE/Lomiri/Components/Themes/Ambiance" DROPIN_DIR="$HOME/.config/systemd/user/lomiri-full-shell.service.d" DROPIN_FILE="$DROPIN_DIR/fix-spinner.conf" HASH_FILE="$FIXBASE/.system-qmldir-sha256" OVERRIDE_FILE="ActivityIndicatorStyle.qml" TIMEOUT_SECS=15 # ─── Helpers ──────────────────────────────────────────────────────────────── die() { echo "ERROR: $*" >&2; exit 1; } # Compute sha256 of a file (portable: works with sha256sum or shasum) file_hash() { sha256sum "$1" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$1" | cut -d' ' -f1 } # ─── Status ───────────────────────────────────────────────────────────────── status() { echo "=== Lomiri spinner fix status ===" local issues=0 # Check override directory if [ -d "$FIXDIR/1.3" ] && [ -f "$FIXDIR/1.3/$OVERRIDE_FILE" ]; then echo "✓ Override directory exists" else echo "✗ Override directory missing — fix is NOT installed" issues=$((issues + 1)) fi # Check systemd drop-in if [ -f "$DROPIN_FILE" ]; then echo "✓ Systemd drop-in exists" else echo "✗ Systemd drop-in missing — fix is NOT installed" issues=$((issues + 1)) fi # Check if lomiri actually has the env var local lpid lpid=$(pgrep -f "lomiri --mode=full-shell" | head -1) if [ -n "$lpid" ] && cat /proc/"$lpid"/environ 2>/dev/null | tr '\0' '\n' | grep -q "QML2_IMPORT_PATH=.*lomiri-qml-fix"; then echo "✓ Running lomiri has QML2_IMPORT_PATH set" elif [ -n "$lpid" ]; then echo "⚠ Running lomiri does NOT have QML2_IMPORT_PATH (restart needed?)" issues=$((issues + 1)) else echo "- Lomiri not running, can't check env" fi # Check if system qmldir has changed since install if [ -f "$HASH_FILE" ] && [ -f "$SYSDIR/qmldir" ]; then local saved_hash current_hash saved_hash=$(cat "$HASH_FILE") current_hash=$(file_hash "$SYSDIR/qmldir") if [ "$saved_hash" = "$current_hash" ]; then echo "✓ System qmldir unchanged since install" else echo "⚠ System qmldir has CHANGED — re-run install to update" issues=$((issues + 1)) fi fi # Check if system 1.3/ has new files we don't have symlinks for if [ -d "$FIXDIR/1.3" ] && [ -d "$SYSDIR/1.3" ]; then local missing=0 for f in "$SYSDIR/1.3/"*; do local name name=$(basename "$f") if [ ! -e "$FIXDIR/1.3/$name" ]; then echo "⚠ Missing from override: 1.3/$name (new file in system?)" missing=$((missing + 1)) fi done if [ "$missing" -eq 0 ]; then echo "✓ All system 1.3/ files accounted for" fi issues=$((issues + missing)) fi echo "" if [ "$issues" -eq 0 ]; then echo "Status: INSTALLED and UP TO DATE" else echo "Status: $issues issue(s) found — consider re-running install" fi } # ─── Uninstall ────────────────────────────────────────────────────────────── uninstall() { echo "=== Uninstalling lomiri spinner fix ===" echo "Removing QML override directory..." rm -rf "$FIXBASE" echo "Removing systemd drop-in..." rm -f "$DROPIN_FILE" rmdir "$DROPIN_DIR" 2>/dev/null || true echo "Reloading systemd..." systemctl --user daemon-reload echo "Restarting lomiri (screen will flash)..." systemctl --user restart lomiri-full-shell.service echo "" echo "✓ Fix removed. Lomiri is back to stock behavior." } # ─── Install ──────────────────────────────────────────────────────────────── install() { echo "=== Installing lomiri spinner fix ===" # ── Preflight checks ──────────────────────────────────────────────── [ -d "$SYSDIR/1.3" ] || die "System QML directory not found at $SYSDIR/1.3 — is this Ubuntu Touch with Lomiri Components 1.3?" [ -f "$SYSDIR/qmldir" ] || die "System qmldir not found at $SYSDIR/qmldir" [ -f "$SYSDIR/1.3/$OVERRIDE_FILE" ] || die "System $OVERRIDE_FILE not found — nothing to override" # ── Step 1: Create override directory structure ────────────────────── echo "" echo "Step 1: Creating QML override directory..." echo " (This shadows the system Ambiance theme module so we can replace one file)" rm -rf "$FIXBASE" mkdir -p "$FIXDIR/1.3" # Copy qmldir — the module definition that tells Qt which QML types exist # and where their .qml files are. Must be a real copy (not symlink) so that # Qt resolves the relative paths (./1.2/*, ./1.3/*) against OUR directory. echo " Copying qmldir (module definition)..." cp "$SYSDIR/qmldir" "$FIXDIR/qmldir" # Save a hash so we can detect if the system qmldir changes in an update file_hash "$SYSDIR/qmldir" > "$HASH_FILE" # Symlink artwork/ — contains spinner@{8,16,32}.png (DPI variants). # Our patched QML file loads Qt.resolvedUrl("../artwork/spinner.png"), # which resolves relative to 1.3/, so artwork/ must exist at our level. echo " Symlinking artwork/ (spinner images, referenced as ../artwork/ from QML)..." ln -s "$SYSDIR/artwork" "$FIXDIR/artwork" # Symlink 1.2/ — the qmldir maps ~25 types (API versions 0.1–1.2) to files # in ./1.2/. These are older Lomiri Components APIs that existing apps still # use. Without this, any app using "import Lomiri.Components 1.2" would get # broken styles (no buttons, text fields, sliders, etc.). echo " Symlinking 1.2/ (older API versions — used by existing apps)..." ln -s "$SYSDIR/1.2" "$FIXDIR/1.2" # Populate 1.3/ with per-file symlinks. We can't symlink the whole 1.3/ # directory because we need to replace one file inside it. So we create a # real directory and symlink each file individually — except our override. echo " Symlinking 1.3/ files (one-by-one, so we can replace $OVERRIDE_FILE)..." local count=0 for f in "$SYSDIR/1.3/"*; do local name name=$(basename "$f") if [ "$name" != "$OVERRIDE_FILE" ]; then ln -s "$f" "$FIXDIR/1.3/$name" count=$((count + 1)) fi done echo " $count symlinks to system files" # ── Step 2: Write the patched ActivityIndicatorStyle.qml ───────────── echo "" echo "Step 2: Writing patched $OVERRIDE_FILE..." echo " (Adds a ${TIMEOUT_SECS}-second timeout to the infinite RotationAnimator)" cat > "$FIXDIR/1.3/$OVERRIDE_FILE" << 'QML' /* * Copyright 2012 Canonical Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * PATCHED: Added safety timeout to stop the RotationAnimator after 15 seconds. * Without this, Lomiri burns 30-40% CPU when an X11/XMir app's splash screen * placeholder never gets its surface filled (Xwayland surface matching bug in * TopLevelWindowModel::prependSurface). The spinner is invisible behind the * real app window but its animator still drives the Qt render loop at 60fps. */ import QtQuick 2.4 import Lomiri.Components 1.3 Image { id: container smooth: true visible: styledItem.running && styledItem.visible fillMode: Image.PreserveAspectFit horizontalAlignment: Image.AlignHCenter verticalAlignment: Image.AlignVCenter source: visible ? Qt.resolvedUrl("../artwork/spinner.png") : "" asynchronous: true RotationAnimator on rotation { id: rotationAnim running: styledItem.running from: 0 to: 360 loops: Animation.Infinite duration: LomiriAnimation.SleepyDuration } // Safety timeout: stop spinning after 15 seconds. // Normal apps finish loading in <5s. This only fires for stuck splash // screens (e.g. X11 apps whose Xwayland surface never fills the placeholder). Timer { interval: 15000 running: rotationAnim.running onTriggered: styledItem.running = false } } QML echo " 1 patched file" # ── Step 3: Create systemd user drop-in ────────────────────────────── echo "" echo "Step 3: Creating systemd user drop-in..." echo " (Sets QML2_IMPORT_PATH so lomiri finds our override before the system's)" mkdir -p "$DROPIN_DIR" cat > "$DROPIN_FILE" << EOF # Lomiri spinner fix: prepend our QML override directory so Qt finds our # patched ActivityIndicatorStyle.qml before the system's. # See: fix-lomiri-spinner-install.sh [Service] Environment=QML2_IMPORT_PATH=$FIXBASE EOF echo " Written to: $DROPIN_FILE" # ── Step 4: Apply ──────────────────────────────────────────────────── echo "" echo "Step 4: Reloading systemd and restarting lomiri..." echo " (Screen will flash briefly as the shell restarts)" systemctl --user daemon-reload systemctl --user restart lomiri-full-shell.service echo "" echo "════════════════════════════════════════════════════════════════════" echo " ✓ Fix installed!" echo "" echo " • Applies automatically on every boot (no manual steps needed)" echo " • To verify: launch Firefox, wait 30s, check CPU with:" echo " top -p \$(pgrep -f 'lomiri --mode=full-shell' | head -1)" echo " (should be ~0% instead of 30-40%)" echo " • After OTA updates: $0 status" echo " • To remove: $0 uninstall" echo "════════════════════════════════════════════════════════════════════" } # ─── Main ─────────────────────────────────────────────────────────────────── case "${1:-install}" in install) install ;; uninstall|remove) uninstall ;; status|check) status ;; -h|--help|help) echo "Usage: $0 [install|uninstall|status]" echo "" echo "Fixes Lomiri burning 30-40% CPU when X11 apps (Firefox) run via Xwayland." echo "Overrides ActivityIndicatorStyle.qml to add a 15s safety timeout to the" echo "splash screen spinner. No root required; persists across reboots." echo "" echo "Commands:" echo " install Install the fix and restart lomiri (default)" echo " uninstall Remove the fix and restart lomiri" echo " status Check if fix is installed and up to date" ;; *) echo "Unknown command: $1" >&2 echo "Usage: $0 [install|uninstall|status]" >&2 exit 1 ;; esac