[gnome] Update extensions
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import * as Main from "resource:///org/gnome/shell/ui/main.js";
|
||||
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
|
||||
import { isMatch } from "./isMatch.js";
|
||||
import Shell from "gi://Shell";
|
||||
import { NotificationApplicationPolicy } from "resource:///org/gnome/shell/ui/messageTray.js";
|
||||
export var LogLevel;
|
||||
(function (LogLevel) {
|
||||
LogLevel["DEBUG"] = "debug";
|
||||
@ -10,66 +11,123 @@ export var LogLevel;
|
||||
})(LogLevel || (LogLevel = {}));
|
||||
function getObjectLabel(name, values) {
|
||||
const labels = Object.entries(values)
|
||||
.filter(([_, value]) => value)
|
||||
.filter((entry) => !!entry[1])
|
||||
.map(([label, value]) => `${label}: '${value}'`);
|
||||
return `${name}(${labels.join(", ")})`;
|
||||
}
|
||||
function safeRegexTest(pattern, value) {
|
||||
try {
|
||||
return new RegExp(pattern).test(value);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getWindowLabel(window) {
|
||||
function getWindowLabel(window, app) {
|
||||
return getObjectLabel("Window", {
|
||||
["Title"]: window.title,
|
||||
["WMClass"]: window.wmClass,
|
||||
["GTKAppId"]: window.gtkApplicationId,
|
||||
["SandboxedAppId"]: window.get_sandboxed_app_id(),
|
||||
Title: window.title,
|
||||
AppId: app?.id ?? null,
|
||||
GtkAppId: window.gtkApplicationId,
|
||||
WmClass: window.wmClass,
|
||||
SandboxedAppId: window.get_sandboxed_app_id(),
|
||||
});
|
||||
}
|
||||
function getSourceLabel(source) {
|
||||
return getObjectLabel("Source", {
|
||||
["Title"]: source.title,
|
||||
["Icon"]: source.icon?.to_string(),
|
||||
Title: source.title,
|
||||
Icon: source.icon?.to_string() ?? null,
|
||||
PolicyId: source.policy instanceof NotificationApplicationPolicy
|
||||
? source.policy.id
|
||||
: null,
|
||||
});
|
||||
}
|
||||
// libnotify clients without a desktop-entry hint set source.icon to an
|
||||
// app-identifying string. Compare it against window-side identifiers.
|
||||
function matchByIcon(icon, window) {
|
||||
return (
|
||||
// Ghostty deb: icon matches GTK app id (com.mitchellh.ghostty)
|
||||
icon === window.gtkApplicationId ||
|
||||
// Slack Flatpak: icon matches sandboxed app id (com.slack.Slack)
|
||||
icon === window.get_sandboxed_app_id() ||
|
||||
// Firefox deb: icon matches window manager class (firefox)
|
||||
icon === window.wmClass);
|
||||
}
|
||||
// Snap apps expose icon paths like /snap/firefox/6638/default256.png; their
|
||||
// sandboxed ids often duplicate the snap name (firefox_firefox).
|
||||
function matchBySnapIcon(icon, window) {
|
||||
const snap = /^\/snap\/([^/]+)\//.exec(icon)?.[1];
|
||||
if (!snap)
|
||||
return false;
|
||||
return window.get_sandboxed_app_id() === `${snap}_${snap}`;
|
||||
}
|
||||
function matchByTitle(title, window) {
|
||||
if (window.title == null)
|
||||
return false;
|
||||
return (
|
||||
// Proton Mail Bridge: title matches window title
|
||||
title === window.title ||
|
||||
// Extract app name from composite title separated by " - " or " | ".
|
||||
// `^.+` is greedy, so the rightmost separator wins:
|
||||
// "foo.ts - project - Cursor" -> "Cursor", "doc | App" -> "App".
|
||||
title === /^.+ (-|\|) (.+)$/.exec(window.title)?.[2] ||
|
||||
// Thunderbird: title matches window manager class (thunderbird)
|
||||
title === window.wmClass ||
|
||||
// Discord snap: title duplicated matches sandboxed app id (discord_discord)
|
||||
`${title}_${title}` === window.get_sandboxed_app_id());
|
||||
}
|
||||
// Prefer authoritative identifiers (policy id, source icon) when present.
|
||||
// If an icon is set, do not fall back to title matching; title is a
|
||||
// last-ditch heuristic reserved for sources that expose neither.
|
||||
function sourceMatchesApp(source, window, appId) {
|
||||
if (source.policy instanceof NotificationApplicationPolicy) {
|
||||
return source.policy.id === appId;
|
||||
}
|
||||
const icon = source.icon?.to_string();
|
||||
if (icon)
|
||||
return matchByIcon(icon, window) || matchBySnapIcon(icon, window);
|
||||
if (source.title)
|
||||
return matchByTitle(source.title, window);
|
||||
return false;
|
||||
}
|
||||
export default class JunkNotificationCleaner extends Extension {
|
||||
focusListenerId = null;
|
||||
closeListenerId = null;
|
||||
settings = null;
|
||||
windowTracker = Shell.WindowTracker.get_default();
|
||||
log(level, message) {
|
||||
let minLevel = this.settings.get_string("log-level");
|
||||
const levels = Object.values(LogLevel);
|
||||
if (!levels.includes(minLevel))
|
||||
minLevel = LogLevel.INFO;
|
||||
if (levels.indexOf(level) >= levels.indexOf(minLevel)) {
|
||||
// gschema enum maps debug=0, info=1, warn=2, error=3, matching the
|
||||
// declaration order of LogLevel; compare ints directly.
|
||||
const minLevelIdx = this.settings?.get_enum("log-level") ?? 1;
|
||||
if (Object.values(LogLevel).indexOf(level) >= minLevelIdx) {
|
||||
log(`[${this.metadata.uuid}][${level}] ${message}`);
|
||||
}
|
||||
}
|
||||
clearNotificationsForApp(window, event) {
|
||||
const windowLabel = getWindowLabel(window);
|
||||
const app = this.windowTracker.get_window_app(window);
|
||||
const windowLabel = getWindowLabel(window, app);
|
||||
this.log(LogLevel.DEBUG, `${windowLabel}: received ${event}`);
|
||||
const excludedApps = this.settings.get_strv("excluded-apps");
|
||||
for (const wmClassPattern of excludedApps) {
|
||||
const result = safeRegexTest(wmClassPattern, window.wmClass);
|
||||
if (result === null) {
|
||||
this.log(LogLevel.WARN, `${windowLabel}: invalid regex '${wmClassPattern}'`);
|
||||
}
|
||||
else if (result) {
|
||||
this.log(LogLevel.DEBUG, `${windowLabel}: excluded by '${wmClassPattern}'`);
|
||||
return;
|
||||
}
|
||||
const settings = this.settings;
|
||||
if (!settings) {
|
||||
this.log(LogLevel.ERROR, `${windowLabel}: settings not initialized`);
|
||||
return;
|
||||
}
|
||||
if (!app) {
|
||||
this.log(LogLevel.DEBUG, `${windowLabel}: no app associated with window`);
|
||||
return;
|
||||
}
|
||||
// Shell.App.id is the desktop filename (e.g. "org.gnome.Nautilus.desktop");
|
||||
// NotificationApplicationPolicy.id stores the same id without the
|
||||
// ".desktop" suffix, so normalize before comparison.
|
||||
const appId = app.id.replace(/\.desktop$/, "");
|
||||
const excludedApps = settings.get_strv("excluded-apps");
|
||||
if (excludedApps.includes(appId)) {
|
||||
this.log(LogLevel.DEBUG, `${windowLabel}: excluded by app id '${appId}'`);
|
||||
return;
|
||||
}
|
||||
for (const source of Main.messageTray.getSources()) {
|
||||
const sourceLabel = getSourceLabel(source);
|
||||
const label = `${windowLabel}: ${getSourceLabel(source)}`;
|
||||
const matches = sourceMatchesApp(source, window, appId);
|
||||
for (const notification of [...source.notifications]) {
|
||||
this.log(LogLevel.DEBUG, `${windowLabel}: ${sourceLabel}: found ${notification.isTransient ? "transient" : "persistent"} notification${notification.title ? `: ${notification.title}` : ""}`);
|
||||
if (isMatch(window, source) && !notification.isTransient) {
|
||||
const title = notification.title ?? "(untitled notification)";
|
||||
const kind = notification.isTransient ? "transient" : "persistent";
|
||||
this.log(LogLevel.DEBUG, `${label}: found ${kind} notification: ${title}`);
|
||||
if (notification.isTransient)
|
||||
continue;
|
||||
if (matches) {
|
||||
notification.destroy();
|
||||
this.log(LogLevel.INFO, `${windowLabel}: ${sourceLabel}: removed notification${notification.title ? `: ${notification.title}` : ""}`);
|
||||
this.log(LogLevel.INFO, `${label}: removed notification: ${title}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,17 +135,18 @@ export default class JunkNotificationCleaner extends Extension {
|
||||
enable() {
|
||||
this.settings = this.getSettings();
|
||||
this.focusListenerId = global.display.connect("notify::focus-window", ({ focusWindow }) => {
|
||||
if (this.settings.get_boolean("delete-on-focus") && focusWindow) {
|
||||
if (this.settings?.get_boolean("delete-on-focus") && focusWindow) {
|
||||
this.clearNotificationsForApp(focusWindow, "focus");
|
||||
}
|
||||
});
|
||||
this.closeListenerId = global.window_manager.connect("destroy", (_, { metaWindow }) => {
|
||||
if (this.settings.get_boolean("delete-on-close") && metaWindow) {
|
||||
if (this.settings?.get_boolean("delete-on-close") && metaWindow) {
|
||||
this.clearNotificationsForApp(metaWindow, "close");
|
||||
}
|
||||
});
|
||||
}
|
||||
disable() {
|
||||
this.settings = null;
|
||||
if (this.focusListenerId !== null) {
|
||||
global.display.disconnect(this.focusListenerId);
|
||||
this.focusListenerId = null;
|
||||
@ -96,8 +155,5 @@ export default class JunkNotificationCleaner extends Extension {
|
||||
global.window_manager.disconnect(this.closeListenerId);
|
||||
this.closeListenerId = null;
|
||||
}
|
||||
if (this.settings) {
|
||||
this.settings = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user