diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index eb553c8c5..d28b2a5cf 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -3225,10 +3225,11 @@ Singleton { if (!app && !desktop) return -1; return rules.findIndex(rule => { - if (!predicate(rule)) + if (!predicate(rule) || (rule.matchType || "contains").toString().toLowerCase() !== "exact") return false; const pattern = (rule.pattern || "").toString().toLowerCase(); - return pattern !== "" && (pattern === app || pattern === desktop); + const value = !rule.field || rule.field === "appName" ? app : rule.field === "desktopEntry" ? desktop : ""; + return pattern !== "" && pattern === value; }); } @@ -3246,20 +3247,31 @@ Singleton { saveSettings(); } - function _removeAppRule(appName, desktopEntry, predicate) { + function _hasNoAction(rule) { + return (rule.action || "default").toString().toLowerCase() === "default"; + } + + // Edits the first enabled matching rule and drops it once it no longer does anything. + function _updateAppRule(appName, desktopEntry, predicate, changes) { var rules = JSON.parse(JSON.stringify(notificationRules || [])); - const index = _appRuleIndex(rules, appName, desktopEntry, predicate); + const index = _appRuleIndex(rules, appName, desktopEntry, rule => rule.enabled !== false && predicate(rule)); if (index === -1) - return; - rules.splice(index, 1); + return false; + const rule = Object.assign(rules[index], changes); + if (_hasNoAction(rule) && (rule.urgency || "default").toString().toLowerCase() === "default" && !_isDndBypassRule(rule)) + rules.splice(index, 1); notificationRules = rules; saveSettings(); + return true; } function addMuteRuleForApp(appName, desktopEntry) { - _addAppRule(appName, desktopEntry, { - action: "mute" - }); + if (!_updateAppRule(appName, desktopEntry, _hasNoAction, { + action: "mute" + })) + _addAppRule(appName, desktopEntry, { + action: "mute" + }); } function isAppMuted(appName, desktopEntry) { @@ -3267,7 +3279,9 @@ Singleton { } function removeMuteRuleForApp(appName, desktopEntry) { - _removeAppRule(appName, desktopEntry, _isMuteRule); + _updateAppRule(appName, desktopEntry, _isMuteRule, { + action: "default" + }); } function isAppDndBypassed(appName, desktopEntry) { @@ -3276,14 +3290,19 @@ Singleton { function setAppDndBypass(appName, desktopEntry, enabled) { if (!enabled) { - _removeAppRule(appName, desktopEntry, _isDndBypassRule); + _updateAppRule(appName, desktopEntry, _isDndBypassRule, { + bypassDnd: false + }); return; } if (isAppDndBypassed(appName, desktopEntry)) return; - _addAppRule(appName, desktopEntry, { - bypassDnd: true - }); + if (!_updateAppRule(appName, desktopEntry, () => true, { + bypassDnd: true + })) + _addAppRule(appName, desktopEntry, { + bypassDnd: true + }); } function updateNotificationRule(index, ruleData) { diff --git a/quickshell/Services/NotificationService.qml b/quickshell/Services/NotificationService.qml index 6bbf54449..1884f1f6e 100644 --- a/quickshell/Services/NotificationService.qml +++ b/quickshell/Services/NotificationService.qml @@ -504,6 +504,7 @@ Singleton { }; policy.bypassDnd = rules.some(rule => rule.bypassDnd === true && _matchesNotificationRule(rule, info)); + policy.disablePopup = rules.some(rule => (rule.action || "default").toString().toLowerCase() === "mute" && _matchesNotificationRule(rule, info)); for (const rule of rules) { if (!_matchesNotificationRule(rule, info)) @@ -514,9 +515,6 @@ Singleton { case "ignore": policy.drop = true; break; - case "mute": - policy.disablePopup = true; - break; case "popup_only": policy.hideFromCenter = true; policy.disableHistory = true; @@ -699,23 +697,7 @@ Singleton { } } - // Honor the freedesktop "suppress-sound" hint: the sender - // plays its own audio for this notification and asks the - // server not to double up. "sound-name" is the opposite — an - // explicit request for audio — so it plays even when the - // global new-notification sound is off. - const soundHints = notif.hints || {}; - const suppressSound = !!soundHints["suppress-sound"]; - const requestsSound = !!soundHints["sound-name"]; const dndBlocked = SessionData.doNotDisturb && !_allowedInDnd(policy.urgency, policy.bypassDnd); - if (!dndBlocked && SettingsData.soundsEnabled && (SettingsData.soundNewNotification || requestsSound) && !suppressSound) { - if (policy.urgency === NotificationUrgency.Critical) { - AudioService.playCriticalNotificationSound(); - } else { - AudioService.playNormalNotificationSound(); - } - } - const shouldShowPopup = !root.popupsDisabled && !dndBlocked && !policy.disablePopup; const isTransient = notif.transient; const shouldKeepInCenter = !isTransient && !policy.hideFromCenter; @@ -727,6 +709,22 @@ Singleton { return; } + // Honor the freedesktop "suppress-sound" hint: the sender + // plays its own audio for this notification and asks the + // server not to double up. "sound-name" is the opposite — an + // explicit request for audio — so it plays even when the + // global new-notification sound is off. + const soundHints = notif.hints || {}; + const suppressSound = !!soundHints["suppress-sound"]; + const requestsSound = !!soundHints["sound-name"]; + if (!dndBlocked && !policy.disablePopup && SettingsData.soundsEnabled && (SettingsData.soundNewNotification || requestsSound) && !suppressSound) { + if (policy.urgency === NotificationUrgency.Critical) { + AudioService.playCriticalNotificationSound(); + } else { + AudioService.playNormalNotificationSound(); + } + } + const wrapper = notifComponent.createObject(root, { "popup": shouldShowPopup, "notification": notif,