1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-09 15:22:13 -04:00
Files
DankMaterialShell/quickshell/Services/TrayRecoveryService.qml
Dimariqe 1470aa3185 fix: add TrayRecoveryService with bidirectional SNI dedup
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.
2026-03-31 15:18:52 +07:00

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;
});
}
}