[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -12,5 +12,5 @@
|
||||
],
|
||||
"url": "https://github.com/murar8/junk-notification-cleaner",
|
||||
"uuid": "junk-notification-cleaner@murar8.github.com",
|
||||
"version": 10
|
||||
}
|
||||
"version": 13
|
||||
}
|
||||
@ -1,171 +1,272 @@
|
||||
import Adw from "gi://Adw";
|
||||
import Gio from "gi://Gio";
|
||||
import GioUnix from "gi://GioUnix";
|
||||
import Gtk from "gi://Gtk";
|
||||
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
|
||||
const LOG_LEVELS = ["debug", "info", "warn", "error"];
|
||||
function isValidRegex(pattern) {
|
||||
try {
|
||||
new RegExp(pattern);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const LOG_LEVELS = [
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error",
|
||||
];
|
||||
export default class JunkNotificationCleanerPreferences extends ExtensionPreferences {
|
||||
async fillPreferencesWindow(window) {
|
||||
const settings = this.getSettings();
|
||||
model;
|
||||
getPreferencesWidget() {
|
||||
this.model = new PreferencesModel(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({
|
||||
page.add(this.buildGeneralGroup());
|
||||
page.add(this.buildLoggingGroup());
|
||||
page.add(this.buildExcludedAppsGroup());
|
||||
return page;
|
||||
}
|
||||
buildGeneralGroup() {
|
||||
const group = new Adw.PreferencesGroup();
|
||||
group.set_title("General Settings");
|
||||
group.add(this.buildSwitchRow({
|
||||
key: "delete-on-focus",
|
||||
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({
|
||||
}));
|
||||
group.add(this.buildSwitchRow({
|
||||
key: "delete-on-close",
|
||||
title: "Delete on Close",
|
||||
subtitle: "Delete notifications when an application window is closed.",
|
||||
}));
|
||||
return group;
|
||||
}
|
||||
buildSwitchRow(opts) {
|
||||
const row = new Adw.ActionRow({
|
||||
title: opts.title,
|
||||
subtitle: opts.subtitle,
|
||||
});
|
||||
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({
|
||||
const toggle = new Gtk.Switch({ valign: Gtk.Align.CENTER });
|
||||
this.model.bindDeleteTrigger(opts.key, toggle, "active");
|
||||
row.add_suffix(toggle);
|
||||
return row;
|
||||
}
|
||||
buildLoggingGroup() {
|
||||
const group = new Adw.PreferencesGroup();
|
||||
group.set_title("Logging");
|
||||
const row = 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),
|
||||
const dropdown = 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]);
|
||||
this.bindLogLevelDropdown(dropdown);
|
||||
row.add_suffix(dropdown);
|
||||
group.add(row);
|
||||
return group;
|
||||
}
|
||||
bindLogLevelDropdown(dropdown) {
|
||||
const sync = () => {
|
||||
dropdown.set_selected(this.model.getLogLevelIndex());
|
||||
};
|
||||
sync();
|
||||
const changedId = this.model.onLogLevelChanged(sync);
|
||||
dropdown.connect("notify::selected", (dd) => {
|
||||
this.model.setLogLevelIndex(dd.get_selected());
|
||||
});
|
||||
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,
|
||||
dropdown.connect("destroy", () => {
|
||||
this.model.disconnect(changedId);
|
||||
});
|
||||
const excludedApps = settings.get_strv("excluded-apps");
|
||||
}
|
||||
buildExcludedAppsGroup() {
|
||||
const group = new Adw.PreferencesGroup();
|
||||
group.set_title("Excluded Applications");
|
||||
group.set_description("Applications whose notifications will not be automatically deleted.");
|
||||
const listBox = this.buildExcludedAppsList();
|
||||
const addButton = new Gtk.Button({
|
||||
icon_name: "list-add-symbolic",
|
||||
tooltip_text: "Add application",
|
||||
css_classes: ["flat"],
|
||||
valign: Gtk.Align.CENTER,
|
||||
});
|
||||
addButton.connect("clicked", (btn) => {
|
||||
const parent = btn.get_root();
|
||||
this.openAppSelector(parent, (appId) => {
|
||||
this.model.addExcludedApp(appId);
|
||||
});
|
||||
});
|
||||
group.set_header_suffix(addButton);
|
||||
group.add(listBox);
|
||||
return group;
|
||||
}
|
||||
buildExcludedAppsList() {
|
||||
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,
|
||||
listBox.set_placeholder(new Gtk.Label({
|
||||
label: "No excluded applications.",
|
||||
css_classes: ["dim-label"],
|
||||
margin_top: 12,
|
||||
margin_bottom: 12,
|
||||
}));
|
||||
this.rebuildExcludedAppsList(listBox);
|
||||
const handlerId = this.model.onExcludedAppsChanged(() => {
|
||||
this.rebuildExcludedAppsList(listBox);
|
||||
});
|
||||
const entry = new Gtk.Entry({
|
||||
placeholder_text: "Enter WM Class regex (e.g. .*firefox.*)",
|
||||
hexpand: true,
|
||||
listBox.connect("destroy", () => {
|
||||
this.model.disconnect(handlerId);
|
||||
});
|
||||
const errorLabel = new Gtk.Label({
|
||||
label: "Invalid regular expression",
|
||||
css_classes: ["error"],
|
||||
xalign: 0,
|
||||
visible: false,
|
||||
});
|
||||
const addButton = new Gtk.Button({
|
||||
label: "Add",
|
||||
css_classes: ["suggested-action"],
|
||||
});
|
||||
entry.connect("changed", () => {
|
||||
entry.remove_css_class("error");
|
||||
errorLabel.set_visible(false);
|
||||
});
|
||||
addButton.connect("clicked", () => {
|
||||
const text = entry.get_text().trim();
|
||||
if (!text)
|
||||
return;
|
||||
if (!isValidRegex(text)) {
|
||||
entry.add_css_class("error");
|
||||
errorLabel.set_visible(true);
|
||||
}
|
||||
else {
|
||||
entry.remove_css_class("error");
|
||||
errorLabel.set_visible(false);
|
||||
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);
|
||||
excludedBox.append(errorLabel);
|
||||
excludedGroup.add(excludedBox);
|
||||
return listBox;
|
||||
}
|
||||
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,
|
||||
});
|
||||
rebuildExcludedAppsList(listBox) {
|
||||
listBox.remove_all();
|
||||
for (const appId of this.model.getExcludedApps()) {
|
||||
listBox.append(this.buildExcludedAppRow(appId));
|
||||
}
|
||||
}
|
||||
buildExcludedAppRow(appId) {
|
||||
const app = tryLoadDesktopApp(appId);
|
||||
const row = buildAppRow(app, appId);
|
||||
if (!app)
|
||||
row.set_subtitle("Uninstalled");
|
||||
const removeButton = new Gtk.Button({
|
||||
icon_name: "user-trash-symbolic",
|
||||
tooltip_text: "Remove",
|
||||
css_classes: ["flat"],
|
||||
valign: Gtk.Align.CENTER,
|
||||
});
|
||||
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);
|
||||
this.model.removeExcludedApp(appId);
|
||||
});
|
||||
box.append(label);
|
||||
box.append(removeButton);
|
||||
row.set_child(box);
|
||||
listBox.append(row);
|
||||
row.add_suffix(removeButton);
|
||||
return row;
|
||||
}
|
||||
openAppSelector(parent, onSelected) {
|
||||
const window = new Adw.Window({
|
||||
title: "Add Excluded Application",
|
||||
modal: true,
|
||||
default_width: 420,
|
||||
default_height: 520,
|
||||
});
|
||||
if (parent)
|
||||
window.set_transient_for(parent);
|
||||
const search = new Gtk.SearchEntry({
|
||||
placeholder_text: "Search applications",
|
||||
hexpand: true,
|
||||
});
|
||||
const headerBar = new Adw.HeaderBar();
|
||||
headerBar.set_title_widget(search);
|
||||
const appList = buildSelectableAppList(this.getSelectableApps(), (appId) => {
|
||||
onSelected(appId);
|
||||
window.close();
|
||||
});
|
||||
wireSearchFilter(appList, search);
|
||||
addCloseOnEscape(window);
|
||||
const toolbarView = new Adw.ToolbarView();
|
||||
toolbarView.add_top_bar(headerBar);
|
||||
toolbarView.set_content(new Gtk.ScrolledWindow({ child: appList }));
|
||||
window.set_content(toolbarView);
|
||||
window.present();
|
||||
search.grab_focus();
|
||||
}
|
||||
getSelectableApps() {
|
||||
const excluded = new Set(this.model.getExcludedApps());
|
||||
return Gio.AppInfo.get_all()
|
||||
.filter((a) => a.should_show() && a.get_id() && !excluded.has(getAppId(a)))
|
||||
.sort((a, b) => a.get_name().localeCompare(b.get_name()));
|
||||
}
|
||||
}
|
||||
function buildSelectableAppList(apps, onActivated) {
|
||||
const appList = new Gtk.ListBox({
|
||||
selection_mode: Gtk.SelectionMode.NONE,
|
||||
css_classes: ["boxed-list"],
|
||||
margin_top: 8,
|
||||
margin_bottom: 8,
|
||||
margin_start: 8,
|
||||
margin_end: 8,
|
||||
});
|
||||
for (const app of apps) {
|
||||
const appId = getAppId(app);
|
||||
const row = buildAppRow(app, appId, { activatable: true });
|
||||
appList.append(row);
|
||||
row.connect("activated", () => {
|
||||
onActivated(appId);
|
||||
});
|
||||
}
|
||||
return appList;
|
||||
}
|
||||
// DesktopAppInfo.new is typed as non-null but returns null when the
|
||||
// .desktop file is missing (e.g. the user uninstalled the app).
|
||||
function tryLoadDesktopApp(appId) {
|
||||
return GioUnix.DesktopAppInfo.new(`${appId}.desktop`);
|
||||
}
|
||||
function buildAppRow(app, appId, extra = {}) {
|
||||
const title = app?.get_name() ?? appId;
|
||||
const row = new Adw.ActionRow({ ...extra, title, subtitle: appId });
|
||||
const icon = app?.get_icon();
|
||||
if (icon) {
|
||||
const image = Gtk.Image.new_from_gicon(icon);
|
||||
image.set_pixel_size(32);
|
||||
row.add_prefix(image);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
function wireSearchFilter(appList, search) {
|
||||
appList.set_filter_func((row) => {
|
||||
const query = search.get_text().toLowerCase().trim();
|
||||
if (query === "")
|
||||
return true;
|
||||
const actionRow = row;
|
||||
const title = actionRow.get_title().toLowerCase();
|
||||
const subtitle = actionRow.get_subtitle()?.toLowerCase() ?? "";
|
||||
return title.includes(query) || subtitle.includes(query);
|
||||
});
|
||||
search.connect("search-changed", () => {
|
||||
appList.invalidate_filter();
|
||||
});
|
||||
}
|
||||
function addCloseOnEscape(window) {
|
||||
const controller = new Gtk.ShortcutController();
|
||||
controller.add_shortcut(new Gtk.Shortcut({
|
||||
trigger: Gtk.ShortcutTrigger.parse_string("Escape"),
|
||||
action: Gtk.ShortcutAction.parse_string("action(window.close)"),
|
||||
}));
|
||||
window.add_controller(controller);
|
||||
}
|
||||
function getAppId(app) {
|
||||
return (app.get_id() ?? "").replace(/\.desktop$/, "");
|
||||
}
|
||||
class PreferencesModel {
|
||||
settings;
|
||||
constructor(settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
bindDeleteTrigger(key, target, property) {
|
||||
this.settings.bind(key, target, property, Gio.SettingsBindFlags.DEFAULT);
|
||||
}
|
||||
getExcludedApps() {
|
||||
return this.settings.get_strv("excluded-apps");
|
||||
}
|
||||
addExcludedApp(appId) {
|
||||
const current = this.getExcludedApps();
|
||||
if (current.includes(appId))
|
||||
return;
|
||||
this.settings.set_strv("excluded-apps", [...current, appId]);
|
||||
}
|
||||
removeExcludedApp(appId) {
|
||||
this.settings.set_strv("excluded-apps", this.getExcludedApps().filter((id) => id !== appId));
|
||||
}
|
||||
onExcludedAppsChanged(handler) {
|
||||
return this.settings.connect("changed::excluded-apps", handler);
|
||||
}
|
||||
getLogLevelIndex() {
|
||||
return this.settings.get_enum("log-level");
|
||||
}
|
||||
setLogLevelIndex(idx) {
|
||||
this.settings.set_enum("log-level", idx);
|
||||
}
|
||||
onLogLevelChanged(handler) {
|
||||
return this.settings.connect("changed::log-level", handler);
|
||||
}
|
||||
disconnect(handlerId) {
|
||||
this.settings.disconnect(handlerId);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist>
|
||||
<enum id="org.gnome.shell.extensions.junk-notification-cleaner.log-level">
|
||||
<value nick="debug" value="0"/>
|
||||
<value nick="info" value="1"/>
|
||||
<value nick="warn" value="2"/>
|
||||
<value nick="error" value="3"/>
|
||||
</enum>
|
||||
<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>
|
||||
@ -14,9 +20,9 @@
|
||||
<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>
|
||||
<description>List of application ids (.desktop file names without the suffix) for which notifications should not be automatically deleted.</description>
|
||||
</key>
|
||||
<key name="log-level" type="s">
|
||||
<key name="log-level" enum="org.gnome.shell.extensions.junk-notification-cleaner.log-level">
|
||||
<default>'info'</default>
|
||||
<summary>Log level</summary>
|
||||
<description>Set the logging level for troubleshooting notification matching issues.</description>
|
||||
|
||||
Reference in New Issue
Block a user