[gnome] Add new extensions

This commit is contained in:
2026-02-11 16:14:31 -05:00
parent 5b86d17bab
commit 2c2be21fef
25 changed files with 4047 additions and 0 deletions

View File

@ -0,0 +1,89 @@
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";
export var LogLevel;
(function (LogLevel) {
LogLevel["DEBUG"] = "debug";
LogLevel["INFO"] = "info";
LogLevel["WARN"] = "warn";
LogLevel["ERROR"] = "error";
})(LogLevel || (LogLevel = {}));
function getObjectLabel(name, values) {
const labels = Object.entries(values)
.filter(([_, value]) => value)
.map(([label, value]) => `${label}: '${value}'`);
return `${name}(${labels.join(", ")})`;
}
function getWindowLabel(window) {
return getObjectLabel("Window", {
["Title"]: window.title,
["WMClass"]: window.wmClass,
["GTKAppId"]: window.gtkApplicationId,
["SandboxedAppId"]: window.get_sandboxed_app_id(),
});
}
function getSourceLabel(source) {
return getObjectLabel("Source", {
["Title"]: source.title,
["Icon"]: source.icon?.to_string(),
});
}
export default class JunkNotificationCleaner extends Extension {
focusListenerId = null;
closeListenerId = null;
settings = null;
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)) {
log(`[${this.metadata.uuid}][${level}] ${message}`);
}
}
clearNotificationsForApp(window, event) {
const windowLabel = getWindowLabel(window);
this.log(LogLevel.DEBUG, `${windowLabel}: received ${event}`);
const excludedApps = this.settings.get_strv("excluded-apps");
for (const wmClassPattern of excludedApps) {
if (new RegExp(wmClassPattern).test(window.wmClass)) {
this.log(LogLevel.DEBUG, `${windowLabel}: excluded by '${wmClassPattern}'`);
return;
}
}
for (const source of Main.messageTray.getSources()) {
const sourceLabel = getSourceLabel(source);
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) {
notification.destroy();
this.log(LogLevel.INFO, `${windowLabel}: ${sourceLabel}: removed notification${notification.title ? `: ${notification.title}` : ""}`);
}
}
}
}
enable() {
this.settings = this.getSettings();
this.focusListenerId = global.display.connect("notify::focus-window", ({ 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) {
this.clearNotificationsForApp(metaWindow, "close");
}
});
}
disable() {
if (this.focusListenerId !== null) {
global.display.disconnect(this.focusListenerId);
}
if (this.closeListenerId !== null) {
global.window_manager.disconnect(this.closeListenerId);
}
if (this.settings) {
this.settings = null;
}
}
}

View File

@ -0,0 +1,36 @@
export function isMatch(window, source) {
if (source.icon) {
const icon = source.icon.to_string();
if (
// 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) {
return true;
}
// Snap apps have icon paths like /snap/firefox/6638/default256.png
const snapAppName = icon?.match(/^\/snap\/([^/]+)\//)?.at(1);
// Snap sandboxed ids use format appname_appname (firefox_firefox)
if (snapAppName) {
if (window.get_sandboxed_app_id() === `${snapAppName}_${snapAppName}`) {
return true;
}
}
}
if (source.title) {
if (
// Proton Mail Bridge: title matches window title
source.title === window.title ||
// Extract app name from composite title (isMatch.ts - junk-notification-cleaner - Cursor)
source.title === window.title?.match(/^.+ (-|\|) (.+)$/)?.[2] ||
// Thunderbird: title matches window manager class (thunderbird)
source.title === window.wmClass ||
// Discord snap: title duplicated matches sandboxed app id (discord_discord)
`${source.title}_${source.title}` === window.get_sandboxed_app_id()) {
return true;
}
}
return false;
}

View File

@ -0,0 +1,15 @@
{
"_generated": "Generated by SweetTooth, do not edit",
"description": "Delete notifications for an application when its window is focused or closed.",
"name": "Junk Notification Cleaner",
"settings-schema": "org.gnome.shell.extensions.junk-notification-cleaner",
"shell-version": [
"46",
"47",
"48",
"49"
],
"url": "https://github.com/murar8/junk-notification-cleaner",
"uuid": "junk-notification-cleaner@murar8.github.com",
"version": 6
}

View File

@ -0,0 +1,143 @@
import Adw from "gi://Adw";
import Gio from "gi://Gio";
import Gtk from "gi://Gtk";
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
const LOG_LEVELS = ["debug", "info", "warn", "error"];
export default class JunkNotificationCleanerPreferences extends ExtensionPreferences {
async fillPreferencesWindow(window) {
const settings = this.getSettings();
const page = new Adw.PreferencesPage();
page.set_title("Settings");
page.set_icon_name("preferences-system-symbolic");
window.add(page);
const generalGroup = new Adw.PreferencesGroup();
generalGroup.set_title("General Settings");
page.add(generalGroup);
const focusRow = new Adw.ActionRow({
title: "Delete on Focus",
subtitle: "Delete notifications when an application window is focused.",
});
const focusSwitch = new Gtk.Switch({
active: settings.get_boolean("delete-on-focus"),
valign: Gtk.Align.CENTER,
});
settings.bind("delete-on-focus", focusSwitch, "active", Gio.SettingsBindFlags.DEFAULT);
focusRow.add_suffix(focusSwitch);
generalGroup.add(focusRow);
const closeRow = new Adw.ActionRow({
title: "Delete on Close",
subtitle: "Delete notifications when an application window is closed.",
});
const closeSwitch = new Gtk.Switch({
active: settings.get_boolean("delete-on-close"),
valign: Gtk.Align.CENTER,
});
settings.bind("delete-on-close", closeSwitch, "active", Gio.SettingsBindFlags.DEFAULT);
closeRow.add_suffix(closeSwitch);
generalGroup.add(closeRow);
const debugGroup = new Adw.PreferencesGroup();
debugGroup.set_title("Logging");
page.add(debugGroup);
const logLevelRow = new Adw.ActionRow({
title: "Log Level",
subtitle: "Set the logging level for troubleshooting notification matching.",
});
const logLevelDropdown = new Gtk.DropDown({
model: Gtk.StringList.new(LOG_LEVELS),
valign: Gtk.Align.CENTER,
});
let currentLogLevel = settings.get_string("log-level") || "info";
const currentIndex = LOG_LEVELS.indexOf(currentLogLevel);
if (currentIndex !== -1) {
logLevelDropdown.set_selected(currentIndex);
}
logLevelDropdown.connect("notify::selected", () => {
const selectedIndex = logLevelDropdown.get_selected();
settings.set_string("log-level", LOG_LEVELS[selectedIndex]);
});
logLevelRow.add_suffix(logLevelDropdown);
debugGroup.add(logLevelRow);
const excludedGroup = new Adw.PreferencesGroup();
excludedGroup.set_title("Excluded WM Classes");
excludedGroup.set_description([
"Window Manager Classes whose notifications will not be automatically deleted.",
"Will be matched against the wm_class property of the window, supports ECMAScript regular expressions.",
].join("\n"));
page.add(excludedGroup);
const excludedBox = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
margin_top: 8,
margin_bottom: 8,
margin_start: 8,
margin_end: 8,
spacing: 8,
});
const excludedApps = settings.get_strv("excluded-apps");
const listBox = new Gtk.ListBox({
selection_mode: Gtk.SelectionMode.NONE,
css_classes: ["boxed-list"],
});
excludedBox.append(listBox);
for (const app of excludedApps) {
this.addExcludedAppRow(app, listBox, settings);
}
const addBox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 8,
margin_top: 8,
});
const entry = new Gtk.Entry({
placeholder_text: "Enter WM Class regex (e.g. .*firefox.*)",
hexpand: true,
});
const addButton = new Gtk.Button({
label: "Add",
css_classes: ["suggested-action"],
});
addButton.connect("clicked", () => {
const text = entry.get_text().trim();
if (!text)
return;
const currentApps = settings.get_strv("excluded-apps");
if (currentApps.includes(text))
return;
settings.set_strv("excluded-apps", [...currentApps, text]);
this.addExcludedAppRow(text, listBox, settings);
entry.set_text("");
});
addBox.append(entry);
addBox.append(addButton);
excludedBox.append(addBox);
excludedGroup.add(excludedBox);
}
addExcludedAppRow(app, listBox, settings) {
const row = new Gtk.ListBoxRow();
const box = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 8,
margin_top: 8,
margin_bottom: 8,
margin_start: 8,
margin_end: 8,
});
const label = new Gtk.Label({
label: app,
hexpand: true,
xalign: 0,
});
const removeButton = new Gtk.Button({
icon_name: "user-trash-symbolic",
tooltip_text: "Remove",
});
removeButton.connect("clicked", () => {
const currentApps = settings.get_strv("excluded-apps");
const newApps = currentApps.filter((a) => a !== app);
settings.set_strv("excluded-apps", newApps);
listBox.remove(row);
});
box.append(label);
box.append(removeButton);
row.set_child(box);
listBox.append(row);
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.junk-notification-cleaner" path="/org/gnome/shell/extensions/junk-notification-cleaner/">
<key name="delete-on-focus" type="b">
<default>true</default>
<summary>Delete notifications when focusing app</summary>
<description>Delete notifications when an application window is focused.</description>
</key>
<key name="delete-on-close" type="b">
<default>true</default>
<summary>Delete notifications when closing app</summary>
<description>Delete notifications when an application window is closed.</description>
</key>
<key name="excluded-apps" type="as">
<default>[]</default>
<summary>Excluded applications</summary>
<description>List of application regex patterns for which notifications should not be automatically deleted.</description>
</key>
<key name="log-level" type="s">
<default>'info'</default>
<summary>Log level</summary>
<description>Set the logging level for troubleshooting notification matching issues.</description>
</key>
</schema>
</schemalist>