mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-09 15:22:13 -04:00
Add TrayRecoveryService singleton that re-registers lost tray icons after resume from suspend via a bash DBus scan. The service resolves every registered SNI item (both well-known names and :1.xxx connection IDs) to a canonical connection ID, building a unified REGISTERED_CONN_IDS set before either scan section runs. This prevents duplicates in both directions: - If an app registered via well-known name, the connection-ID section skips its :1.xxx entry. - If an app registered via connection ID, the well-known-name section skips its well-known name (checked through REGISTERED_CONN_IDS). - After successfully registering via well-known name, REGISTERED_CONN_IDS is updated immediately so the connection-ID section won't probe the same app in the same run. A single busctl snapshot (BUSCTL_OUT) is reused across both sections, replacing redundant dbus-send ListNames calls. The SNI Id dedup check inside the connection-ID subshells is now case-insensitive (-i flag) as a secondary fallback.
147 lines
6.2 KiB
QML
147 lines
6.2 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.SystemTray
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// Re-run after resume from suspend
|
|
Connections {
|
|
target: SessionService
|
|
function onSessionResumed() {
|
|
resumeTimer.restart();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: resumeTimer
|
|
interval: 3000
|
|
repeat: false
|
|
running: false
|
|
onTriggered: root.recoverTrayItems()
|
|
}
|
|
|
|
Process {
|
|
id: recoveryProcess
|
|
running: false
|
|
command: ["bash", "-c", `
|
|
REGISTERED=$(dbus-send --session --print-reply \
|
|
--dest=org.kde.StatusNotifierWatcher \
|
|
/StatusNotifierWatcher \
|
|
org.freedesktop.DBus.Properties.Get \
|
|
string:org.kde.StatusNotifierWatcher \
|
|
string:RegisteredStatusNotifierItems 2>/dev/null || echo "")
|
|
|
|
# Single snapshot of all DBus names/connections (reused in both sections)
|
|
BUSCTL_OUT=$(busctl --user list --no-legend 2>/dev/null)
|
|
|
|
# Build the full set of effectively-registered connection IDs by resolving
|
|
# every registered item (well-known name or :1.xxx) to its connection ID.
|
|
# This prevents both directions of false-duplicate registration.
|
|
REGISTERED_CONN_IDS=""
|
|
for ITEM_PATH in $(echo "$REGISTERED" | grep -oP '"[^"]*"' | tr -d '"'); do
|
|
ITEM_NAME=$(echo "$ITEM_PATH" | cut -d/ -f1)
|
|
if [[ "$ITEM_NAME" == :1.* ]]; then
|
|
REGISTERED_CONN_IDS="$REGISTERED_CONN_IDS $ITEM_NAME"
|
|
else
|
|
CONN=$(echo "$BUSCTL_OUT" | awk -v n="$ITEM_NAME" '$1==n {print $5; exit}')
|
|
[ -n "$CONN" ] && REGISTERED_CONN_IDS="$REGISTERED_CONN_IDS $CONN"
|
|
fi
|
|
done
|
|
|
|
# === Well-known names (DinoX, nm-applet, etc.) ===
|
|
NAMES=$(echo "$BUSCTL_OUT" | awk '$1 ~ /^[A-Za-z]/ {print $1}')
|
|
|
|
for NAME in $NAMES; do
|
|
echo "$REGISTERED" | grep -qF "$NAME" && continue
|
|
|
|
# Also skip if this name's connection ID is already in the registered set
|
|
# (handles the case where the app registered via connection ID instead)
|
|
CONN_FOR_NAME=$(echo "$BUSCTL_OUT" | awk -v n="$NAME" '$1==n {print $5; exit}')
|
|
[ -n "$CONN_FOR_NAME" ] && [[ " $REGISTERED_CONN_IDS " == *" $CONN_FOR_NAME "* ]] && continue
|
|
|
|
case "$NAME" in
|
|
org.freedesktop.*|org.gnome.*|org.kde.StatusNotifier*) continue ;;
|
|
com.canonical.AppMenu*|org.mpris.*|org.pipewire.*) continue ;;
|
|
org.pulseaudio*|fi.epitaph*|quickshell*|org.kde.quickshell*) continue ;;
|
|
esac
|
|
|
|
SHORT=$(echo "$NAME" | awk -F. '{print $NF}')
|
|
for OBJ_PATH in "/StatusNotifierItem" "/org/ayatana/NotificationItem/$SHORT"; do
|
|
if timeout 0.3 dbus-send --session --print-reply \
|
|
--dest="$NAME" "$OBJ_PATH" \
|
|
org.freedesktop.DBus.Properties.GetAll \
|
|
string:org.kde.StatusNotifierItem 2>/dev/null | grep -q 'string.*Id' ; then
|
|
dbus-send --session --type=method_call \
|
|
--dest=org.kde.StatusNotifierWatcher \
|
|
/StatusNotifierWatcher \
|
|
org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem \
|
|
string:"$NAME"
|
|
echo "TrayRecovery: re-registered $NAME at $OBJ_PATH"
|
|
# Update set so the connection-ID section won't double-register this app
|
|
[ -n "$CONN_FOR_NAME" ] && REGISTERED_CONN_IDS="$REGISTERED_CONN_IDS $CONN_FOR_NAME"
|
|
break
|
|
fi
|
|
done
|
|
done
|
|
|
|
# === Connection IDs (Vesktop, Electron apps, etc.) ===
|
|
# Probe all :1.xxx connections in parallel with a short timeout.
|
|
# Most non-SNI connections return an error instantly, so this is fast.
|
|
CONN_IDS=$(echo "$BUSCTL_OUT" | awk '$1 ~ /^:1\./ {print $1}')
|
|
|
|
BATCH=0
|
|
for CONN in $CONN_IDS; do
|
|
# Skip if this connection ID is already covered (directly or via well-known name)
|
|
[[ " $REGISTERED_CONN_IDS " == *" $CONN "* ]] && continue
|
|
(
|
|
SNI_ID=$(timeout 0.15 dbus-send --session --print-reply \
|
|
--dest="$CONN" /StatusNotifierItem \
|
|
org.freedesktop.DBus.Properties.Get \
|
|
string:org.kde.StatusNotifierItem string:Id 2>/dev/null \
|
|
| grep -oP '"[^"]+"' | tr -d '"')
|
|
[ -z "$SNI_ID" ] && exit
|
|
# Skip if an item with the same Id is already registered (case-insensitive)
|
|
echo "$REGISTERED" | grep -qiF "$SNI_ID" && exit
|
|
dbus-send --session --type=method_call \
|
|
--dest=org.kde.StatusNotifierWatcher \
|
|
/StatusNotifierWatcher \
|
|
org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem \
|
|
string:"$CONN"
|
|
echo "TrayRecovery: re-registered $CONN (Id: $SNI_ID)"
|
|
) &
|
|
BATCH=$((BATCH + 1))
|
|
[ $((BATCH % 30)) -eq 0 ] && wait && BATCH=0
|
|
done
|
|
wait
|
|
`]
|
|
|
|
stdout: SplitParser {
|
|
onRead: data => {
|
|
if (data.trim().length > 0)
|
|
console.info(data.trim());
|
|
}
|
|
}
|
|
|
|
stderr: SplitParser {
|
|
onRead: data => {
|
|
if (data.trim().length > 0)
|
|
console.warn("TrayRecoveryService:", data.trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
function recoverTrayItems() {
|
|
const count = SystemTray.items.values.length;
|
|
console.info("TrayRecoveryService: scanning DBus for unregistered SNI items (" + count + " already registered)...");
|
|
recoveryProcess.running = false;
|
|
Qt.callLater(() => {
|
|
recoveryProcess.running = true;
|
|
});
|
|
}
|
|
}
|