diff --git a/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/extension.js b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/extension.js
new file mode 100644
index 0000000..727b06d
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/extension.js
@@ -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;
+ }
+ }
+}
diff --git a/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/isMatch.js b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/isMatch.js
new file mode 100644
index 0000000..b96d150
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/isMatch.js
@@ -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;
+}
diff --git a/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/metadata.json b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/metadata.json
new file mode 100644
index 0000000..279cd61
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/metadata.json
@@ -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
+}
\ No newline at end of file
diff --git a/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/prefs.js b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/prefs.js
new file mode 100644
index 0000000..23d7ceb
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/prefs.js
@@ -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);
+ }
+}
diff --git a/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/schemas/gschemas.compiled b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/schemas/gschemas.compiled
new file mode 100644
index 0000000..72cf47b
Binary files /dev/null and b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/schemas/gschemas.compiled differ
diff --git a/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/schemas/org.gnome.shell.extensions.junk-notification-cleaner.gschema.xml b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/schemas/org.gnome.shell.extensions.junk-notification-cleaner.gschema.xml
new file mode 100644
index 0000000..cb75d93
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/junk-notification-cleaner@murar8.github.com/schemas/org.gnome.shell.extensions.junk-notification-cleaner.gschema.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ true
+ Delete notifications when focusing app
+ Delete notifications when an application window is focused.
+
+
+ true
+ Delete notifications when closing app
+ Delete notifications when an application window is closed.
+
+
+ []
+ Excluded applications
+ List of application regex patterns for which notifications should not be automatically deleted.
+
+
+ 'info'
+ Log level
+ Set the logging level for troubleshooting notification matching issues.
+
+
+
\ No newline at end of file
diff --git a/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/extension.js b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/extension.js
new file mode 100644
index 0000000..4916ba9
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/extension.js
@@ -0,0 +1,245 @@
+/* extension.js
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import Clutter from 'gi://Clutter';
+
+
+import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import { MessageTray } from 'resource:///org/gnome/shell/ui/messageTray.js';
+import * as Utils from './utils.js';
+
+const BannerBin = Main.messageTray._bannerBin;
+
+/* Imports necessary by the code pulled in from messageTray.js */
+import { State, Urgency } from 'resource:///org/gnome/shell/ui/messageTray.js';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Calendar from 'resource:///org/gnome/shell/ui/calendar.js'
+import * as MessageList from 'resource:///org/gnome/shell/ui/messageList.js'
+
+
+const NOTIFICATION_TIMEOUT = 4000;
+const HIDE_TIMEOUT = 200;
+const LONGER_HIDE_TIMEOUT = 600;
+const IDLE_TIME = 1000;
+
+let ANIMATION_TIME = 200;
+let ANIMATION_DIRECTION = 2;
+let ANCHOR_VERTICAL = 0;
+let ANCHOR_HORIZONTAL = 2;
+let PADDING_VERTICAL = 0;
+let PADDING_HORIZONTAL = 0;
+let ALWAYS_MINIMIZE = 0;
+
+function patcher(obj, live, method, original, patch) {
+ const body = eval(`${obj}.prototype.${method}.toString()`);
+ const newBody = body.replace(original, patch).replace(method + "(", "function(")
+ eval(`${obj}.prototype.${method} = ${newBody}`);
+ eval(`${live}.${method} = ${newBody}`);
+}
+
+const getMessageTraySize = () => {
+ const { width, height } = Main.layoutManager.getWorkAreaForMonitor(global.display.get_primary_monitor());
+ return {width, height};
+}
+
+const originalShow = MessageTray.prototype._showNotification;
+const originalHide = MessageTray.prototype._hideNotification;
+const originalUpdateShowing = MessageTray.prototype._updateShowingNotification;
+
+function calcTarget(self) {
+ let x = 0, y = 0;
+ switch (ANCHOR_HORIZONTAL) {
+ case 0: // left
+ x = 0 + PADDING_HORIZONTAL;
+ break;
+ case 1: // right
+ x = getMessageTraySize().width - self._banner.width - PADDING_HORIZONTAL;
+ break;
+ case 2: // center
+ x = (getMessageTraySize().width - self._banner.width) / 2.0;
+ break;
+ }
+ switch (ANCHOR_VERTICAL) {
+ case 0: // top
+ y = 0 + PADDING_VERTICAL;
+ break;
+ case 1: // bottom
+ y = getMessageTraySize().height - self._banner.height - PADDING_VERTICAL;
+ break;
+ case 2: // center
+ y = (getMessageTraySize().height - self._banner.height) / 2.0;
+ break;
+ }
+ return { x, y }
+}
+
+function calcHide(self) {
+ let { x, y } = calcTarget(self)
+ switch (ANIMATION_DIRECTION) {
+ case 0: // from left
+ x = -self._banner.width;
+ break;
+ case 1: // from right
+ x = getMessageTraySize().width;
+ break;
+ case 2: // from top
+ y = -self._banner.height
+ break;
+ case 3: // from bottom
+ y = getMessageTraySize().height
+ break;
+ }
+ return { x, y }
+}
+
+function calcStart(self) {
+ const { x, y } = calcHide(self);
+ self._bannerBin.x = x;
+ self._bannerBin.y = y;
+
+
+ // if banner is not expanded and anchored to the bottom
+ // it won't have enough vertical space to expand
+ // in such case, move it up enough to fit the expanded banner
+ if (!self._banner.expanded && ANCHOR_VERTICAL == 1) {
+ const unexpandedHeight = self._banner.height
+ // expand without animation to measure height
+ self._banner.expand(false);
+ const expandedDifference = self._banner.height - unexpandedHeight;
+ // go back to unexpanded
+ self._banner.unexpand(false);
+
+ // move up when needed
+ self._banner.connect('expanded', () => {
+ self._bannerBin.ease({
+ y: self._bannerBin.y - expandedDifference,
+ duration: ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ });
+ });
+ }
+}
+
+// each of the methods has hardcoded values for showing banners
+// at the top. instead of rewriting whole functions, just patch the
+// relevant lines
+const patches = [
+ {
+ "obj": "MessageTray", "method": "_updateShowingNotification",
+ "live": "Main.messageTray",
+ "original": 'y: 0',
+ "patch": '...calcTarget(this)'
+ },
+ {
+ "obj": "MessageTray", "method": "_showNotification",
+ "live": "Main.messageTray",
+ "original": 'this._bannerBin.y = -this._banner.height',
+ "patch": 'calcStart(this)'
+ },
+ {
+ "obj": "MessageTray", "method": "_hideNotification",
+ "live": "Main.messageTray",
+ "original": 'y: -this._bannerBin.height',
+ "patch": '...calcHide(this)'
+ }
+];
+
+const always_minimize_patch = {
+ obj: "MessageTray",
+ live: "Main.messageTray",
+ method: "_updateShowingNotification",
+ original: "this._expandBanner(true)",
+ patch: "// always minimized setting enabled by notification-banner-reloaded ... this._expandBanner(true)",
+};
+
+export default class NotificationExtension extends Extension {
+ constructor(metadata) {
+ super(metadata);
+ this._previous_y_align = BannerBin.get_y_align();
+ this._previous_x_align = BannerBin.get_x_align();
+ }
+
+ _loadSettings() {
+ this._settings = this.getSettings();
+ this._settingsChangedId = this._settings.connect('changed', this._onSettingsChange.bind(this));
+ this._fetchSettings();
+ }
+
+ _fetchSettings() {
+ ANCHOR_VERTICAL = this._settings.get_int(Utils.PrefFields.ANCHOR_VERTICAL);
+ ANCHOR_HORIZONTAL = this._settings.get_int(Utils.PrefFields.ANCHOR_HORIZONTAL);
+ PADDING_VERTICAL = this._settings.get_int(Utils.PrefFields.PADDING_VERTICAL);
+ PADDING_HORIZONTAL = this._settings.get_int(Utils.PrefFields.PADDING_HORIZONTAL);
+ ANIMATION_DIRECTION = this._settings.get_int(Utils.PrefFields.ANIMATION_DIRECTION);
+ ANIMATION_TIME = this._settings.get_int(Utils.PrefFields.ANIMATION_TIME);
+ ALWAYS_MINIMIZE = this._settings.get_int(Utils.PrefFields.ALWAYS_MINIMIZE);
+ }
+
+ _onSettingsChange() {
+ this._fetchSettings();
+ this.enable();
+ }
+
+ enable() {
+ this._loadSettings();
+ // generally alignment can be controller with START/CENTER/END
+ // but CENTER and END are problematic to implement animations with
+ // (especially x -> END and animations from left/right)
+ // all positions will then be calculated in relation to the top-left
+ // corner (START/START).
+ let x_align = Clutter.ActorAlign.START;
+ let y_align = Clutter.ActorAlign.START;
+ BannerBin.set_x_align(x_align);
+ BannerBin.set_y_align(y_align);
+ this.restore();
+ for (const { obj, live, method, original, patch } of patches) {
+ patcher(obj, live, method, original, patch)
+ }
+
+ if (ALWAYS_MINIMIZE) {
+ const { obj, live, method, original, patch } = always_minimize_patch;
+ patcher(obj, live, method, original, patch);
+ }
+ }
+
+ disable() {
+ BannerBin.set_x_align(this._previous_x_align);
+ BannerBin.set_y_align(this._previous_y_align);
+ BannerBin.x = 0
+ BannerBin.y = 0
+ this.restore()
+ if (this._settingsChangedId) {
+ this._settings.disconnect(this._settingsChangedId);
+ this._settingsChangedId = null;
+ }
+ this._settings = null;
+
+ }
+
+ restore() {
+ MessageTray.prototype._hideNotification = originalHide;
+ MessageTray.prototype._showNotification = originalShow;
+ MessageTray.prototype._updateShowingNotification = originalUpdateShowing;
+ }
+}
diff --git a/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/metadata.json b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/metadata.json
new file mode 100644
index 0000000..f8ecd87
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/metadata.json
@@ -0,0 +1,16 @@
+{
+ "_generated": "Generated by SweetTooth, do not edit",
+ "description": "Configure notification banner position and animation to your liking.\nVersion 9: Gnome 45 changes by mannjani@github\nVersion 10: mannjani@github added a test button inside prefs",
+ "name": "Notification Banner Reloaded",
+ "settings-schema": "org.gnome.shell.extensions.notification-banner-reloaded",
+ "shell-version": [
+ "45",
+ "46",
+ "47",
+ "48",
+ "49"
+ ],
+ "url": "https://github.com/marcinjakubowski/notification-position-reloaded",
+ "uuid": "notification-banner-reloaded@marcinjakubowski.github.com",
+ "version": 17
+}
\ No newline at end of file
diff --git a/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/prefs.js b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/prefs.js
new file mode 100644
index 0000000..250ebcf
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/prefs.js
@@ -0,0 +1,248 @@
+/* extension.js
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+/*
+ Swaths of pref related code borrowed from Clipboard Indicator, an amazing extension
+ https://github.com/Tudmotu/gnome-shell-extension-clipboard-indicator
+ https://extensions.gnome.org/extension/779/clipboard-indicator/
+*/
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import * as Utils from './utils.js';
+
+import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+export default class NotificationExtensionPreferences extends ExtensionPreferences {
+ constructor(metadata) {
+ super(metadata);
+ }
+
+ getPreferencesWidget() {
+ let frame = new Gtk.Box();
+ let widget = new Preferences(this.getSettings());
+ frame.append(widget.main);
+ if (frame.show_all)
+ frame.show_all();
+ return frame;
+ }
+}
+
+class Preferences{
+ constructor(settings) {
+ this.settings = settings
+ this.main = new Gtk.Grid({
+ margin_top: 10,
+ margin_bottom: 10,
+ margin_start: 10,
+ margin_end: 10,
+ row_spacing: 12,
+ column_spacing: 18,
+ column_homogeneous: false,
+ row_homogeneous: false
+ });
+
+ this.paddingHorizontal = new Gtk.SpinButton({
+ adjustment: new Gtk.Adjustment({
+ lower: 0,
+ upper: 1000,
+ step_increment: 1
+ })
+ });
+ this.paddingVertical = new Gtk.SpinButton({
+ adjustment: new Gtk.Adjustment({
+ lower: 0,
+ upper: 1000,
+ step_increment: 1
+ })
+ });
+ this.animationTime = new Gtk.SpinButton({
+ adjustment: new Gtk.Adjustment({
+ lower: 100,
+ upper: 5000,
+ step_increment: 100
+ })
+ });
+ this.anchorHorizontal = new Gtk.ComboBox({
+ model: this._create_options([ _('Left'), _('Right'), _('Center') ])
+ });
+ this.anchorVertical = new Gtk.ComboBox({
+ model: this._create_options([ _('Top'), _('Bottom'), _('Center') ])
+ });
+ this.animationDirection = new Gtk.ComboBox({
+ model: this._create_options([ _('Slide from Left'), _('Slide from Right'), _('Slide from Top'), _('Slide from Bottom')])
+ });
+ this.alwaysMinimize = new Gtk.ComboBox({
+ model: this._create_options([ _('false'), _('true')])
+ });
+ this.testButton = new Gtk.Button({ label: _("Test") });
+ this.testButton.connect('clicked', () => {
+ const notification = new GLib.Variant('(susssasa{sv}i)', [
+ 'Notification Banner Reloaded',
+ 0,
+ 'dialog-information-symbolic',
+ 'Notification Banner Reloaded',
+ 'Test',
+ [],
+ {},
+ -1,
+ ]);
+ Gio.DBus.session.call(
+ 'org.freedesktop.Notifications',
+ '/org/freedesktop/Notifications',
+ 'org.freedesktop.Notifications',
+ 'Notify',
+ notification,
+ null,
+ Gio.DBusCallFlags.NONE,
+ -1,
+ null,
+ null);
+ });
+
+ let rendererText = new Gtk.CellRendererText();
+ for (const widget of [this.anchorHorizontal, this.anchorVertical, this.animationDirection, this.alwaysMinimize]) {
+ widget.pack_start(rendererText, false);
+ widget.add_attribute(rendererText, "text", 0);
+ }
+
+ let anchorHorizontalLabel = new Gtk.Label({
+ label: _("Horizontal Position"),
+ hexpand: true,
+ halign: Gtk.Align.START
+ });
+ let anchorVerticalLabel = new Gtk.Label({
+ label: _("Vertical Position"),
+ hexpand: true,
+ halign: Gtk.Align.START
+ });
+ let paddingHorizontalLabel = new Gtk.Label({
+ label: _("Horizontal Padding"),
+ hexpand: true,
+ halign: Gtk.Align.START
+ });
+ let paddingVerticalLabel = new Gtk.Label({
+ label: _("Vertical Padding"),
+ hexpand: true,
+ halign: Gtk.Align.START
+ });
+ let animationDirectionLabel = new Gtk.Label({
+ label: _("Animation Direction"),
+ hexpand: true,
+ halign: Gtk.Align.START
+ });
+ let animationTimeLabel = new Gtk.Label({
+ label: _("Animation Time (ms)"),
+ hexpand: true,
+ halign: Gtk.Align.START
+ });
+ let alwaysMinimizeLabel = new Gtk.Label({
+ label: _("Always Minimize"),
+ hexpand: true,
+ halign: Gtk.Align.START
+ });
+
+ const testButtonLabel = new Gtk.Label({
+ label: _("Send Test Notification"),
+ hexpand: true,
+ halign: Gtk.Align.START
+ });
+
+ const addRow = ((main) => {
+ let row = 0;
+ return (label, input) => {
+ let inputWidget = input;
+
+ if (input instanceof Gtk.Switch) {
+ inputWidget = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL,});
+ addBox(inputWidget, input);
+ }
+
+ if (label) {
+ main.attach(label, 0, row, 1, 1);
+ main.attach(inputWidget, 1, row, 1, 1);
+ }
+ else {
+ main.attach(inputWidget, 0, row, 2, 1);
+ }
+
+ row++;
+ };
+ })(this.main);
+
+ addRow(anchorHorizontalLabel, this.anchorHorizontal);
+ addRow(anchorVerticalLabel, this.anchorVertical);
+ addRow(paddingHorizontalLabel, this.paddingHorizontal);
+ addRow(paddingVerticalLabel, this.paddingVertical);
+ addRow(animationDirectionLabel, this.animationDirection);
+ addRow(animationTimeLabel, this.animationTime);
+ addRow(alwaysMinimizeLabel, this.alwaysMinimize);
+ addRow(testButtonLabel, this.testButton);
+
+ settings.bind(Utils.PrefFields.ANCHOR_HORIZONTAL, this.anchorHorizontal, 'active', Gio.SettingsBindFlags.DEFAULT);
+ settings.bind(Utils.PrefFields.ANCHOR_VERTICAL, this.anchorVertical, 'active', Gio.SettingsBindFlags.DEFAULT);
+ settings.bind(Utils.PrefFields.ANIMATION_DIRECTION, this.animationDirection, 'active', Gio.SettingsBindFlags.DEFAULT);
+ settings.bind(Utils.PrefFields.ALWAYS_MINIMIZE, this.alwaysMinimize, 'active', Gio.SettingsBindFlags.DEFAULT);
+
+ this._bindSpinWithThrottle(this.paddingHorizontal, Utils.PrefFields.PADDING_HORIZONTAL);
+ this._bindSpinWithThrottle(this.paddingVertical, Utils.PrefFields.PADDING_VERTICAL);
+ this._bindSpinWithThrottle(this.animationTime, Utils.PrefFields.ANIMATION_TIME);
+ }
+
+ _bindSpinWithThrottle(spinButton, key) {
+ /*- Load value from settings -*/
+ spinButton.value = this.settings.get_int(key);
+ let timeoutId = 0;
+ const updateSettings = () => {
+ const newVal = Math.round(spinButton.value);
+ if (this.settings.get_int(key) !== newVal) {
+ this.settings.set_int(key, newVal);
+ }
+ timeoutId = 0;
+ return GLib.SOURCE_REMOVE;
+ };
+
+ spinButton.connect('value-changed', () => {
+ if (timeoutId) {
+ GLib.source_remove(timeoutId);
+ }
+ timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, updateSettings);
+ });
+
+ /*- If the user update elsewhere, we update spin value -*/
+ this.settings.connect(`changed::${key}`, () => {
+ const val = this.settings.get_int(key);
+ if (Math.round(spinButton.value) !== val) {
+ spinButton.value = val;
+ }
+ });
+ }
+
+ _create_options(opts) {
+ let options = opts.map(function (v) { return { name: v }});
+ let liststore = new Gtk.ListStore();
+ liststore.set_column_types([GObject.TYPE_STRING])
+ for (let i = 0; i < options.length; i++ ) {
+ let option = options[i];
+ let iter = liststore.append();
+ liststore.set (iter, [0], [option.name]);
+ }
+ return liststore;
+ }
+};
diff --git a/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/schemas/gschemas.compiled b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/schemas/gschemas.compiled
new file mode 100644
index 0000000..53f6b1d
Binary files /dev/null and b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/schemas/gschemas.compiled differ
diff --git a/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/schemas/org.gnome.shell.extensions.notification-banner-reloaded.gschema.xml b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/schemas/org.gnome.shell.extensions.notification-banner-reloaded.gschema.xml
new file mode 100644
index 0000000..48097e1
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/schemas/org.gnome.shell.extensions.notification-banner-reloaded.gschema.xml
@@ -0,0 +1,53 @@
+
+
+
+
+ 200
+ Duration of the show/hide animation
+ Duration of the show/hide animation, in milliseconds
+
+
+
+
+ 0
+ Vertical position of the banner
+
+
+
+
+
+ 2
+ Horizontal position of the banner
+
+
+
+
+
+ 0
+ Vertical padding of the banner
+
+
+
+
+
+ 0
+ Horizontal padding of the banner
+
+
+
+
+
+ 2
+ Animation direction
+
+
+
+
+ 0
+ Always minimized notifications by default (ignores urgency of notification)
+
+
+
+
+
diff --git a/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/utils.js b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/utils.js
new file mode 100644
index 0000000..3771286
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/notification-banner-reloaded@marcinjakubowski.github.com/utils.js
@@ -0,0 +1,9 @@
+export const PrefFields = {
+ ANCHOR_VERTICAL : 'anchor-vertical',
+ ANCHOR_HORIZONTAL : 'anchor-horizontal',
+ PADDING_VERTICAL : 'padding-vertical',
+ PADDING_HORIZONTAL : 'padding-horizontal',
+ ANIMATION_TIME : 'animation-time',
+ ANIMATION_DIRECTION : 'animation-direction',
+ ALWAYS_MINIMIZE : 'always-minimized',
+};
diff --git a/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/extension.js b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/extension.js
new file mode 100644
index 0000000..9f58988
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/extension.js
@@ -0,0 +1,1704 @@
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+import Gio from 'gi://Gio';
+import Clutter from 'gi://Clutter';
+import Shell from 'gi://Shell';
+import GLib from 'gi://GLib';
+import { Extension, gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
+import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
+import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+
+const AppPinner = GObject.registerClass(
+ class AppPinner extends PanelMenu.Button {
+
+ // 1. Costruttore e inizializzazione
+ _init(settings) {
+ super._init(0.0, _('App Pinner'));
+ this._settings = settings;
+ this._destroyed = false;
+ this._pendingApps = new Set();
+ this._settingsHandler = [];
+ this._longPressTimeoutId = null;
+
+ this._windowTracker = Shell.WindowTracker.get_default();
+ this._windowTracker.connect('tracked-windows-changed', this._updateRunningIndicators.bind(this));
+
+ this._settingsHandler.push(
+ this._settings.connect('changed::show-in-panel', () => this._updateVisibility()),
+ this._settings.connect('changed::sort-alphabetically', () => this._refreshUI()),
+ this._settings.connect('changed::show-pin-icon', () => this._updatePinIconVisibility())
+ );
+
+ this._appSystem = Shell.AppSystem.get_default();
+ this._runningTracker = new Set();
+ this._appStateChangedId = this._appSystem.connect('app-state-changed',
+ () => this._updateRunningIndicators());
+
+ // Contenitore principale
+ this._mainContainer = new St.BoxLayout({
+ style_class: 'panel-button',
+ vertical: false,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ reactive: true,
+ track_hover: true
+ });
+ this.add_child(this._mainContainer);
+
+ // Icona del menu
+ this._menuIcon = new St.Icon({
+ icon_name: 'view-pin-symbolic',
+ style_class: 'system-status-icon app-pinner-menu-icon',
+ icon_size: 15
+ });
+ this._mainContainer.add_child(this._menuIcon);
+
+ // Contenitore icone pinnate
+ this._pinnedIconsBox = new St.BoxLayout({
+ style_class: 'app-pinner-icons',
+ vertical: false,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ x_expand: true,
+ y_expand: true
+ });
+ this._mainContainer.add_child(this._pinnedIconsBox);
+
+ this._updateIconsSpacing();
+
+ // Connessioni impostazioni
+ this._settingsHandler.push(
+ this._settings.connect('changed::icon-size', () => this._refreshUI()),
+ this._settings.connect('changed::spacing', () => {
+ this._updateIconsSpacing();
+ this._pinnedIconsBox.queue_relayout();
+ }),
+ this._settings.connect('changed::enable-labels', () => this._refreshUI()),
+ this._settings.connect('changed::pinned-apps', () => this._refreshUI()),
+ this._settings.connect('changed::indicator-color', () => {
+ this._updateIndicatorColor();
+ this._updateRunningIndicators();
+ })
+ );
+
+
+ this.menu.actor.connect('button-press-event', (actor, event) => {
+ const target = event.get_source();
+
+ if (!this.menu.actor.contains(target)) {
+ this.menu.close();
+ }
+ });
+
+ this._buildMenu();
+ this._refreshUI();
+
+ this._logindProxy = new Gio.DBusProxy({
+ g_connection: Gio.DBus.system,
+ g_interface_name: 'org.freedesktop.login1.Manager',
+ g_object_path: '/org/freedesktop/login1',
+ g_name: 'org.freedesktop.login1'
+ });
+
+ this._logindProxy.init_async(GLib.PRIORITY_DEFAULT, null, (proxy, res) => {
+ try {
+ this._logindProxy.init_finish(res);
+ this._logindId = this._logindProxy.connectSignal('PrepareForSleep',
+ this._handleSleepSignal.bind(this));
+ } catch (e) {
+ console.error('Errore inizializzazione proxy login1:', e.message);
+ }
+ });
+
+ this._appStateChangedId = this._appSystem.connect('app-state-changed', () => {
+ this._addTimeout(100, () => {
+ this._updateRunningIndicators();
+ return GLib.SOURCE_REMOVE;
+ });
+ });
+
+ this._settingsHandler.push(
+ this._settings.connect('changed::custom-links', () => this._refreshUI())
+ );
+
+ this._timeoutIds = new Set();
+
+ }
+
+ _updateVisibility() {
+ const showInPanel = this._settings.get_boolean('show-in-panel');
+ this._pinnedIconsBox.visible = showInPanel;
+ this._updatePinIconVisibility();
+ this._pinnedIconsBox.queue_relayout();
+ this.queue_relayout();
+ }
+
+ _updatePinIconVisibility() {
+ const showPinIcon = this._settings.get_boolean('show-pin-icon');
+ const hasPinnedApps = this._settings.get_strv('pinned-apps').length > 0;
+ const noPinnedApps = !hasPinnedApps;
+
+ const iconSize = noPinnedApps ? 18 : 18;
+ this._menuIcon.icon_size = iconSize;
+
+ this._menuIcon.opacity = (noPinnedApps || showPinIcon) ? 255 : 0;
+ this._menuIcon.reactive = (noPinnedApps || showPinIcon);
+ this._menuIcon.set_width(showPinIcon ? -1 : iconSize);
+
+ this.queue_relayout();
+ }
+
+ _addTimeout(interval, callback) {
+ const sourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
+ const result = callback();
+ this._timeoutIds.delete(sourceId);
+ return result;
+ });
+ this._timeoutIds.add(sourceId);
+ return sourceId;
+ }
+
+ // 2. Metodi lifecycle e gestione stato
+ destroy() {
+ if (this._destroyed) return;
+ this._destroyed = true;
+
+ if (this._logindId && this._logindProxy) {
+ this._logindProxy.disconnect(this._logindId);
+ this._logindProxy = null;
+ }
+
+ this._settingsHandler?.forEach(h => this._settings.disconnect(h));
+ this._settingsHandler = null;
+
+ this._pinnedIconsBox?.destroy();
+ this._searchInput?.destroy();
+
+ if (this._appStateChangedId) {
+ this._appSystem.disconnect(this._appStateChangedId);
+ this._appStateChangedId = null;
+ }
+
+ if (this._timeoutIds) {
+ this._timeoutIds.forEach(id => GLib.Source.remove(id));
+ this._timeoutIds.clear();
+ }
+
+ if (this._longPressTimeoutId !== null) {
+ GLib.Source.remove(this._longPressTimeoutId);
+ this._timeoutIds.delete(this._longPressTimeoutId);
+ this._longPressTimeoutId = null;
+ }
+
+ super.destroy();
+ }
+
+ // 3. Costruzione UI principale
+ _buildMenu() {
+ this.menu.removeAll();
+
+ // Barra di ricerca
+ const searchEntry = new PopupMenu.PopupBaseMenuItem();
+ const searchBox = new St.BoxLayout({
+ vertical: false,
+ style_class: 'search-box',
+ x_expand: true,
+ x_align: Clutter.ActorAlign.FILL
+ });
+
+ this._searchInput = new St.Entry({
+ hint_text: _('Search applications...'),
+ can_focus: true,
+ x_expand: true
+ });
+
+ searchBox.add_child(this._searchInput);
+ searchEntry.actor.add_child(searchBox);
+ this.menu.addMenuItem(searchEntry);
+
+ // Sezione risultati
+ this._resultsSection = new PopupMenu.PopupMenuSection();
+ this.menu.addMenuItem(this._resultsSection);
+
+ // Sezione pinnati
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+ this._pinnedSection = new PopupMenu.PopupMenuSection();
+ this.menu.addMenuItem(this._pinnedSection);
+
+ this._searchInput.clutter_text.connect('text-changed', () => this._updateSearch());
+
+ // Sezione aggiungi link
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+ const linkSection = new PopupMenu.PopupMenuSection();
+ this.menu.addMenuItem(linkSection);
+
+ const linkBox = new St.BoxLayout({
+ vertical: false,
+ style_class: 'app-pinner-link-box',
+ x_expand: true,
+ margin_top: 6,
+ margin_bottom: 6
+ });
+ this._linkInput = new St.Entry({
+ hint_text: _('Enter URL...'),
+ can_focus: true,
+ style_class: 'app-pinner-link-input'
+ });
+
+ const addLinkBtn = new St.Button({
+ style_class: 'app-pinner-add-link-btn',
+ label: _('Add'),
+ can_focus: true,
+ x_align: Clutter.ActorAlign.END
+ });
+
+ addLinkBtn.connect('clicked', () => {
+ const url = this._linkInput.get_text().trim();
+ if (this._isValidUrl(url)) {
+ this._pinLink(url);
+ this._linkInput.set_text('');
+ this.menu.close();
+ }
+ });
+
+ linkBox.add_child(this._linkInput);
+ linkBox.add_child(addLinkBtn);
+ linkSection.actor.add_child(linkBox);
+
+ this._mainContainer.reactive = true;
+ this._mainContainer.connect('button-press-event', (actor, event) => {
+ if (event.get_button() === Clutter.BUTTON_PRIMARY) {
+ this.menu.toggle();
+ return Clutter.EVENT_STOP;
+ }
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ this.menu.actor.connect('button-press-event', (actor, event) => {
+ const target = event.get_source();
+ if (!this.menu.actor.contains(target)) {
+ this.menu.close();
+ }
+ });
+ }
+
+ _addPinnedIcon(appId) {
+
+ if (appId.startsWith('link://')) {
+ const url = appId.replace('link://', '');
+ return this._addLinkIcon(url);
+ }
+
+
+ const app = Gio.DesktopAppInfo.new(appId + '.desktop') ||
+ Gio.DesktopAppInfo.new(appId);
+
+ if (!app) {
+ const altPaths = [
+ GLib.build_filenamev([GLib.get_home_dir(), '.local', 'share', 'applications', `${appId}.desktop`]),
+ `/var/lib/flatpak/exports/share/applications/${appId}.desktop`,
+ `${GLib.get_home_dir()}/.local/share/flatpak/exports/share/applications/${appId}.desktop`
+ ];
+
+ for (const path of altPaths) {
+ if (GLib.file_test(path, GLib.FileTest.EXISTS)) {
+ const altApp = Gio.DesktopAppInfo.new_from_filename(path);
+ if (altApp) {
+ app = altApp;
+ break;
+ }
+ }
+ }
+
+ if (!app) {
+ console.error(`[CRITICAL] App not found in any location: ${appId}`);
+ return;
+ }
+ }
+
+ const showLabels = this._settings.get_boolean('enable-labels');
+
+ const iconBox = new St.BoxLayout({
+ vertical: true,
+ style_class: 'app-pinner-icon-box',
+ y_expand: true
+ });
+
+ const iconContainer = new St.Bin({
+ style_class: 'app-pinner-icon-container',
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+
+ const iconButton = new St.Button({
+ child: new St.Icon({
+ gicon: app.get_icon(),
+ icon_size: this._settings.get_int('icon-size')
+ }),
+ style_class: 'app-pinner-icon',
+ reactive: true,
+ hover: true,
+ track_hover: true
+ });
+
+ const runningIndicator = new St.Widget({
+ style_class: 'app-pinner-running-indicator',
+ visible: false,
+ reactive: false,
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.START,
+ x_expand: false,
+ y_expand: false,
+ translation_x: +9,
+ translation_y: -9
+ });
+
+ this._updateIndicatorColor(runningIndicator);
+
+ this._settings.connect('changed::indicator-color',
+ () => this._updateIndicatorColor(runningIndicator));
+
+
+ iconContainer.add_child(iconButton);
+ iconContainer.add_child(runningIndicator);
+
+ iconBox.runningIndicator = runningIndicator;
+
+ let pressStartTime = 0;
+ this._longPressTimeoutId = null;
+ let isLongPress = false;
+
+ iconButton.connect('button-press-event', (actor, event) => {
+
+ if (this._longPressTimeoutId !== null) {
+ GLib.Source.remove(this._longPressTimeoutId);
+ this._timeoutIds.delete(this._longPressTimeoutId);
+ this._longPressTimeoutId = null;
+ }
+
+ pressStartTime = Date.now();
+ isLongPress = false;
+
+ this._longPressTimeoutId = this._addTimeout(500, () => {
+ isLongPress = true;
+ this._animateAndMoveToEnd(appId, iconBox);
+ this._longPressTimeoutId = null;
+ return GLib.SOURCE_REMOVE;
+ });
+
+ actor.ease({
+ scale_x: 0.8,
+ scale_y: 0.8,
+ duration: 200
+ });
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ iconButton.connect('button-release-event', (actor, event) => {
+ if (this._longPressTimeoutId !== null) {
+ GLib.Source.remove(this._longPressTimeoutId);
+ this._timeoutIds.delete(this._longPressTimeoutId);
+ this._longPressTimeoutId = null;
+ }
+
+ actor.ease({
+ scale_x: 1.0,
+ scale_y: 1.0,
+ duration: 200
+ });
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ iconButton.connect('clicked', () => {
+ if (!isLongPress) {
+ this._launchApp(appId);
+ }
+ isLongPress = false;
+ });
+
+ iconBox.add_child(iconContainer);
+
+ if (showLabels) {
+ iconBox.add_child(new St.Label({
+ text: app.get_name(),
+ style_class: 'app-pinner-label'
+ }));
+ }
+
+ this._pinnedIconsBox.add_child(iconBox);
+ iconBox.appId = appId;
+
+ this._updateRunningIndicator(appId, runningIndicator);
+
+ return iconBox;
+ }
+
+ _addLinkIcon(url) {
+ const appId = `link://${url}`;
+ const iconSize = this._settings.get_int('icon-size');
+ const showLabels = this._settings.get_boolean('enable-labels');
+
+ const iconBox = new St.BoxLayout({
+ vertical: true,
+ style_class: 'app-pinner-icon-box'
+ });
+
+ const iconButton = new St.Button({
+ child: new St.Icon({
+ icon_name: 'emblem-web-symbolic',
+ icon_size: iconSize,
+ style_class: 'app-pinner-link-icon'
+ }),
+ style_class: 'app-pinner-icon',
+ reactive: true,
+ hover: true,
+ track_hover: true
+ });
+
+ iconButton.connect('notify::hover', () => {
+ iconButton.style = iconButton.hover
+ ? 'background: rgba(255,255,255,0.1); border-radius: 6px;'
+ : '';
+ });
+
+ let pressStartTime = 0;
+ this._longPressTimeoutId = null;
+ let isLongPress = false;
+
+ iconButton.connect('button-press-event', (actor, event) => {
+
+ pressStartTime = Date.now();
+ isLongPress = false;
+
+ this._longPressTimeoutId = this._addTimeout(500, () => {
+ isLongPress = true;
+ this._animateAndMoveToEnd(appId, iconBox);
+ return GLib.SOURCE_REMOVE;
+ });
+
+ actor.ease({
+ scale_x: 0.8,
+ scale_y: 0.8,
+ duration: 200
+ });
+
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ iconButton.connect('button-release-event', (actor, event) => {
+ if (this._longPressTimeoutId !== null) {
+ GLib.Source.remove(this._longPressTimeoutId);
+ this._timeoutIds.delete(this._longPressTimeoutId);
+ this._longPressTimeoutId = null;
+ }
+
+ actor.ease({
+ scale_x: 1.0,
+ scale_y: 1.0,
+ duration: 200
+ });
+
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ iconButton.connect('clicked', () => {
+ if (!isLongPress) {
+ Gio.AppInfo.launch_default_for_uri(url, null);
+ }
+ isLongPress = false;
+ });
+
+ const container = new St.Bin();
+ container.add_child(iconButton);
+ iconBox.add_child(container);
+
+ if (showLabels) {
+ const label = new St.Label({
+ text: this._shortenUrl(url),
+ style_class: 'app-pinner-label',
+ x_align: Clutter.ActorAlign.CENTER
+ });
+ iconBox.add_child(label);
+ }
+
+ iconBox.appId = appId;
+ this._pinnedIconsBox.add_child(iconBox);
+
+ return iconBox;
+ }
+
+ _checkVisibility() {
+ if (this._destroyed) return;
+ this._updateVisibility();
+ this._addTimeout(2000, () => {
+ this._checkVisibility();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ // 4. Gestione interazioni utente
+ _launchApp(appId) {
+
+ if (appId.startsWith('link://')) {
+ const url = appId.replace('link://', '');
+ Gio.AppInfo.launch_default_for_uri(url, null);
+ return;
+ }
+ try {
+ const appSystem = Shell.AppSystem.get_default();
+ let app = appSystem.lookup_app(`${appId}.desktop`) || appSystem.lookup_app(appId);
+
+ if (!app) {
+ const desktopApp = Gio.DesktopAppInfo.new(`${appId}.desktop`);
+ if (desktopApp) {
+ app = new Shell.App({ desktop_app_info: desktopApp });
+ }
+ if (!app) {
+ console.error(`[ERROR] Application ${appId} not found`);
+ return;
+ }
+ }
+
+ this._pendingApps.add(appId);
+
+ app.activate();
+
+ this._forceImmediateUpdate(appId);
+
+ this._addTimeout(1500, () => {
+ this._pendingApps.delete(appId);
+ this._updateRunningIndicators();
+ this._addTimeout(500, () => {
+ this._updateRunningIndicators();
+ return GLib.SOURCE_REMOVE;
+ });
+ return GLib.SOURCE_REMOVE;
+ });
+
+ } catch (e) {
+ this._pendingApps.delete(appId);
+ }
+
+ this._pendingApps.add(appId);
+ this._updateRunningIndicators();
+ }
+
+ _handleSleepSignal(sender, signalName, params) {
+ const [isSleeping] = params.deepUnpack();
+ if (!isSleeping) {
+ this._addTimeout(1000, () => {
+ this._updateVisibility();
+ this._forceFullRefresh();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ }
+
+ // 5. Gestione stato applicazioni
+ _updateRunningIndicators() {
+ const children = this._pinnedIconsBox.get_children();
+
+ children.forEach(iconBox => {
+ const appId = iconBox.appId;
+ if (!appId || appId.startsWith('link://')) return;
+ this._updateRunningIndicator(appId, iconBox.runningIndicator);
+ });
+ }
+
+ _updateRunningIndicator(appId, indicator) {
+ let isRunning = false;
+
+ if (this._pendingApps.has(appId)) {
+ isRunning = true;
+ } else {
+ const app = this._findAppById(appId);
+ if (app) {
+ const state = app.get_state();
+ const hasWindows = app.get_windows().length > 0;
+ const isInAppSystem = this._appSystem.get_running().some(a => a.get_id() === app.get_id());
+
+ isRunning = state === Shell.AppState.RUNNING || hasWindows || isInAppSystem;
+
+ }
+ }
+
+ if (indicator.visible !== isRunning) {
+ indicator.visible = isRunning;
+
+ if (isRunning) {
+ indicator.ease({
+ scale_x: 1.2,
+ scale_y: 1.2,
+ opacity: 200,
+ duration: 200,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ indicator.ease({
+ scale_x: 1.0,
+ scale_y: 1.0,
+ opacity: 255,
+ duration: 300
+ });
+ }
+ });
+ } else {
+ indicator.set_scale(1.0, 1.0);
+ indicator.opacity = 255;
+ }
+ }
+
+ if (!isRunning && this._pendingApps.has(appId)) {
+ this._pendingApps.delete(appId);
+ }
+ }
+
+ // 6. Gestione impostazioni e aggiornamenti UI
+ _refreshUI() {
+ if (this._destroyed) return;
+
+ const schema = this._settings.settings_schema;
+ const key = schema.get_key('icon-size');
+ const range = key.get_range();
+ const [minSize, maxIconSize] = range.deep_unpack();
+
+ let pinnedApps = this._settings.get_strv('pinned-apps');
+
+ if (this._settings.get_boolean('sort-alphabetically') && pinnedApps.length > 0) {
+ const sortedApps = [...pinnedApps].sort((a, b) => {
+ const nameA = this._getAppOrLinkName(a).toLowerCase();
+ const nameB = this._getAppOrLinkName(b).toLowerCase();
+ return nameA.localeCompare(nameB);
+ });
+
+ if (!arraysEqual(pinnedApps, sortedApps)) {
+ const handler = this._settings.connect('changed::pinned-apps', () => { });
+ this._settings.set_strv('pinned-apps', sortedApps);
+ this._settings.disconnect(handler);
+ return;
+ }
+ }
+
+ const spacing = this._settings.get_int('spacing');
+ const totalItems = pinnedApps.length;
+
+ const containerWidth = (maxIconSize * totalItems) + (spacing * Math.max(0, totalItems - 1));
+
+ this._mainContainer.set_style(`
+ min-width: ${containerWidth}px;
+ padding: 0 ${spacing}px;
+ `);
+
+ this._pinnedIconsBox.destroy_all_children();
+
+ pinnedApps.forEach(appId => {
+ const app = Gio.DesktopAppInfo.new(`${appId}.desktop`) || Gio.DesktopAppInfo.new(appId);
+ if (app) {
+ this._addPinnedIcon(appId);
+ } else if (appId.startsWith('link://')) {
+ this._addPinnedIcon(appId);
+ }
+ });
+
+ this._pinnedSection.box.destroy_all_children();
+ pinnedApps.forEach(appId => this._addMenuPinnedItem(appId));
+
+ this._updatePinIconVisibility();
+ }
+
+ _updateIconsSpacing() {
+ const spacing = Math.min(20, Math.max(0, this._settings.get_int('spacing')));
+ this._pinnedIconsBox.set_style(`spacing: ${spacing}px;`);
+ }
+
+ // 7. Gestione elementi pinnati
+ _pinApp(app) {
+
+ const current = this._settings.get_strv('pinned-apps');
+ if (current.length >= 10) {
+ this._showMaxItemsError();
+ return;
+ }
+
+ const rawAppId = app.get_id();
+ const appId = rawAppId.replace(/\.desktop$/i, '');
+
+ if (!current.includes(appId)) {
+ const updated = [...current, appId];
+ this._settings.set_strv('pinned-apps', updated);
+ }
+ }
+
+ _unpinApp(appId) {
+ if (appId.startsWith('link://')) {
+ const url = appId.replace('link://', '');
+ const links = this._settings.get_strv('custom-links')
+ .filter(l => l !== url);
+ this._settings.set_strv('custom-links', links);
+ }
+
+ const startupApps = this._settings.get_strv('startup-apps')
+ .filter(id => id !== appId);
+ this._settings.set_strv('startup-apps', startupApps);
+
+ const pinnedApps = this._settings.get_strv('pinned-apps')
+ .filter(id => id !== appId);
+ this._settings.set_strv('pinned-apps', pinnedApps);
+ }
+
+ _isPinned(appId) {
+ const cleanAppId = this._sanitizeAppId(appId);
+ return this._settings.get_strv('pinned-apps').includes(cleanAppId);
+ }
+
+ // 8. Funzioni helper e utilità
+ _isValidUrl(url) {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return url.startsWith('http') || url.startsWith('ftp');
+ }
+ }
+
+ _sortApps(a, b, query) {
+ const aName = a.get_name().toLowerCase();
+ const bName = b.get_name().toLowerCase();
+
+ if (aName === query) return -1;
+ if (bName === query) return 1;
+
+ const aStart = aName.startsWith(query);
+ const bStart = bName.startsWith(query);
+ if (aStart !== bStart) return aStart ? -1 : 1;
+
+ return aName.length - bName.length;
+ }
+
+ _handleWakeupEvent() {
+ this._addTimeout(2000, () => {
+ this._updateRunningIndicators();
+ this._refreshUI();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _pinLink(url) {
+
+ const pinnedApps = this._settings.get_strv('pinned-apps');
+ if (pinnedApps.length >= 10) {
+ this._showMaxItemsError();
+ return;
+ }
+
+ if (!url) return;
+ if (!url.includes('://')) url = `http://${url}`;
+
+ const links = this._settings.get_strv('custom-links');
+
+ if (!links.includes(url)) {
+ const newLinks = [...links, url];
+ const newPinnedApps = [...pinnedApps, `link://${url}`];
+
+ this._settings.set_strv('custom-links', newLinks);
+ this._settings.set_strv('pinned-apps', newPinnedApps);
+ this._refreshUI();
+ }
+ }
+
+ async _updateSearch() {
+ this._resultsSection.box.destroy_all_children();
+ const query = this._searchInput.get_text().trim().toLowerCase();
+ if (!query) return;
+
+ const iconSize = this._settings.get_int('icon-size');
+ const appSys = Shell.AppSystem.get_default();
+
+ const results = appSys.get_installed()
+ .filter(app => {
+ const appId = this._sanitizeAppId(app.get_id());
+ return !this._isPinned(appId) &&
+ this._matchQuery(query, app.get_name().toLowerCase());
+ })
+ .sort((a, b) => this._sortApps(a, b, query))
+ .slice(0, 10);
+
+ if (results.length === 0) {
+ this._resultsSection.box.add_child(
+ new PopupMenu.PopupMenuItem(_('No results found'))
+ );
+ return;
+ }
+
+ results.forEach(app => {
+ const item = new PopupMenu.PopupMenuItem(app.get_name());
+ const icon = new St.Icon({
+ gicon: app.get_icon(),
+ icon_size: iconSize
+ });
+ item.insert_child_at_index(icon, 0);
+
+ if (this._settings.get_strv('pinned-apps').length >= 10) {
+ item.setSensitive(false);
+ item.label.text = _("Max items reached - unpin something first");
+ }
+
+ item.connect('activate', () => {
+ this._pinApp(app);
+
+ this._searchInput.set_text('');
+ this._updateSearch();
+
+ this.menu.close();
+
+ });
+
+ this._resultsSection.box.add_child(item);
+ });
+
+ this.menu.actor.show_all();
+ this.menu.actor.queue_redraw();
+
+ }
+
+ _sanitizeAppId(appId) {
+ const sanitized = appId
+ .replace(/\.desktop$/i, '')
+ .replace(/^application:\/\//i, '');
+ return sanitized;
+ }
+
+ _matchQuery(query, appName) {
+ const keywords = appName.split(/[\s-]/);
+ return appName.includes(query) ||
+ keywords.some(k => k.startsWith(query)) ||
+ this._fuzzyMatch(query, appName);
+ }
+
+ _fuzzyMatch(pattern, str) {
+ let patternIndex = 0;
+ for (const char of str.toLowerCase()) {
+ if (char === pattern[patternIndex]) {
+ if (++patternIndex === pattern.length) return true;
+ }
+ }
+ return false;
+ }
+
+ _getIndicatorForApp(appId) {
+ const iconBox = this._pinnedIconsBox.get_children().find(b => b.appId === appId);
+ return iconBox ? iconBox.runningIndicator : null;
+ }
+
+ _forceImmediateUpdate(appId) {
+ const indicator = this._getIndicatorForApp(appId);
+ if (indicator) {
+ indicator.visible = true;
+ indicator.opacity = 0;
+ indicator.set_scale(0.5, 0.5);
+ indicator.ease({
+ opacity: 255,
+ scale_x: 1,
+ scale_y: 1,
+ duration: 300,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD
+ });
+ }
+ this._updateRunningIndicators();
+ }
+
+ _shortenUrl(url) {
+ try {
+ const parsed = new URL(url);
+ return parsed.hostname.replace(/^www\./i, '');
+ } catch {
+ return url.substring(0, 15) + '...';
+ }
+ }
+
+ _forceFullRefresh() {
+ this._runningTracker = new Set(
+ this._appSystem.get_running().map(app => this._sanitizeAppId(app.get_id()))
+ );
+
+ this._pinnedIconsBox.get_children().forEach(iconBox => {
+ this._updateRunningIndicator(iconBox.appId, iconBox.runningIndicator);
+ });
+
+ this._addTimeout(1000, () => {
+ this._appSystem = Shell.AppSystem.get_default();
+ this._updateRunningIndicators();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _findAppById(appId) {
+ let app = this._appSystem.lookup_app(appId) ||
+ this._appSystem.lookup_app(`${appId}.desktop`);
+
+ if (!app) {
+ const gioApp = Gio.DesktopAppInfo.new(`${appId}.desktop`) ||
+ Gio.DesktopAppInfo.new(appId);
+
+ if (gioApp) {
+ app = this._appSystem.lookup_app(gioApp.get_id());
+ }
+ }
+
+ return app;
+ }
+
+ _refreshUIWithEffect() {
+ this._pinnedIconsBox.get_children().forEach((child, index) => {
+ child.ease({
+ opacity: 0,
+ scale_x: 0.5,
+ scale_y: 0.5,
+ duration: 300,
+ delay: index * 30,
+ onComplete: () => child.destroy()
+ });
+ });
+
+ this._addTimeout(300, () => {
+ this._settings.get_strv('pinned-apps').forEach(appId =>
+ this._addPinnedIcon(appId)
+ );
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _animateAndMoveToEnd(appId, iconBox) {
+ const currentApps = this._settings.get_strv('pinned-apps');
+ const currentIndex = currentApps.indexOf(appId);
+
+ if (currentIndex === currentApps.length - 1) {
+ iconBox.ease({
+ scale_x: 1.2,
+ scale_y: 1.2,
+ duration: 200,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ iconBox.ease({
+ scale_x: 1.0,
+ scale_y: 1.0,
+ duration: 300,
+ mode: Clutter.AnimationMode.EASE_OUT_BOUNCE
+ });
+ }
+ });
+ return;
+ }
+
+ iconBox.ease({
+ scale_x: 1.2,
+ scale_y: 1.2,
+ opacity: 0,
+ duration: 300,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ const updatedApps = this._settings.get_strv('pinned-apps');
+ const newIndex = updatedApps.indexOf(appId);
+
+ if (newIndex === updatedApps.length - 1) return;
+
+ const newOrder = [
+ ...updatedApps.slice(0, newIndex),
+ ...updatedApps.slice(newIndex + 1),
+ updatedApps[newIndex]
+ ];
+
+ this._settings.set_strv('pinned-apps', newOrder);
+ }
+ });
+ }
+
+ _updateIndicatorColor(indicator) {
+ const color = this._settings.get_string('indicator-color');
+ indicator.set_style(`background-color: ${color};`);
+ }
+
+ _getUrlName(url) {
+ try {
+ if (!url.match(/^[a-zA-Z]+:\/\//)) {
+ url = 'http://' + url;
+ }
+
+ const parsed = new URL(url);
+ let hostname = parsed.hostname;
+
+ hostname = hostname.replace(/^www\./i, '');
+
+ const specialCases = {
+ 'youtube.com': 'YouTube',
+ 'google.com': 'Google',
+ 'github.com': 'GitHub',
+ };
+
+ return specialCases[hostname.toLowerCase()] ||
+ hostname.split('.')[0].charAt(0).toUpperCase() +
+ hostname.split('.')[0].slice(1);
+ } catch (e) {
+ return url.replace(/^https?:\/\//, '')
+ .replace(/^www\./i, '')
+ .split('/')[0];
+ }
+ }
+
+ _getAppOrLinkName(appId) {
+ if (appId.startsWith('link://')) {
+ return this._getUrlName(appId.replace('link://', ''));
+ } else {
+ const app = Gio.DesktopAppInfo.new(`${appId}.desktop`) || Gio.DesktopAppInfo.new(appId);
+ return app ? app.get_name() : appId;
+ }
+ }
+
+
+ _addMenuPinnedItem(appId) {
+ if (appId.startsWith('link://')) {
+ const url = appId.replace('link://', '');
+ const item = new PopupMenu.PopupMenuItem(url);
+
+ const icon = new St.Icon({
+ icon_name: 'emblem-web-symbolic',
+ icon_size: this._settings.get_int('icon-size'),
+ style_class: 'app-pinner-link-icon'
+ });
+ item.insert_child_at_index(icon, 0);
+
+ const spacer = new St.BoxLayout({ x_expand: true });
+ item.actor.add_child(spacer);
+
+ const removeBtn = new St.Button({
+ child: new St.Label({ text: '×' }),
+ style_class: 'app-pinner-remove-btn'
+ });
+
+ item.connect('activate', () => {
+ Gio.AppInfo.launch_default_for_uri(url, null);
+ this.menu.close();
+ });
+
+ removeBtn.connect('clicked', () => {
+ this._unpinApp(`link://${url}`);
+ });
+
+ item.add_child(removeBtn);
+ this._pinnedSection.box.add_child(item);
+ } else {
+ const app = Gio.DesktopAppInfo.new(`${appId}.desktop`);
+ if (!app) {
+ console.error(`Applicazione non trovata nel menu: ${appId}`);
+ return;
+ }
+
+ const item = new PopupMenu.PopupMenuItem(app.get_name());
+ const icon = new St.Icon({
+ gicon: app.get_icon(),
+ icon_size: this._settings.get_int('icon-size')
+ });
+ item.insert_child_at_index(icon, 0);
+
+ const spacer = new St.BoxLayout({ x_expand: true });
+ item.actor.add_child(spacer);
+
+ const removeBtn = new St.Button({
+ child: new St.Label({
+ text: '×',
+ style_class: 'app-pinner-remove-label'
+ }),
+ style_class: 'app-pinner-remove-btn'
+ });
+
+ removeBtn.connect('button-press-event', (actor, event) => {
+ this._unpinApp(appId);
+ this.menu.close();
+ this.menu.open();
+ return Clutter.EVENT_STOP;
+ });
+
+ item.connect('activate', () => {
+ this._launchApp(appId);
+ this.menu.close();
+ });
+
+ item.add_child(removeBtn);
+ this._pinnedSection.box.add_child(item);
+ }
+ }
+ });
+
+function arraysEqual(a, b) {
+ return a.length === b.length && a.every((v, i) => v === b[i]);
+}
+
+export default class AppPinnerExtension extends Extension {
+
+ constructor(metadata) {
+ super(metadata);
+ this._timeoutIds = new Set();
+ this._settingsHandler = [];
+ }
+
+ _addTimeout(interval, callback) {
+ const sourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
+ const result = callback();
+ this._timeoutIds.delete(sourceId);
+ return result;
+ });
+ this._timeoutIds.add(sourceId);
+ return sourceId;
+ }
+
+ _dbusImpl = null;
+
+ // 1. Lifecycle dell'estensione
+ enable() {
+
+ this._settingsHandler = [];
+
+ this._settings = this.getSettings();
+
+ this._validateSettings();
+
+ this._settingsHandler.push(
+ this._settings.connect('changed::show-in-panel', () => {
+ if (this._indicator) {
+ this._indicator._updateVisibility();
+ }
+ }),
+ this._settings.connect('changed::show-pin-icon', () => {
+ if (this._indicator) {
+ this._indicator._updatePinIconVisibility();
+ }
+ })
+ );
+
+ this._sessionConnection = Gio.DBus.session;
+ this._sessionWatcher = this._sessionConnection.watch_name(
+ 'org.gnome.Shell',
+ Gio.BusNameWatcherFlags.NONE,
+ () => { },
+ () => this.disable()
+ );
+
+ const dbusInterface = `
+
+
+
+
+
+
+ `;
+
+ if (!this._dbusImpl) {
+ try {
+ this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(dbusInterface, this);
+ this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Extensions/AppPinner');
+ } catch (e) {
+ console.error('Errore registrazione D-Bus:', e.message);
+ }
+ }
+
+ if (!this._settings.settings_schema.get_key('show-in-panel')) {
+ this._settings.set_boolean('show-in-panel', true);
+ }
+
+ this._positionHandler = this._settings.connect(
+ 'changed::position-in-panel',
+ () => this._safeRecreateIndicator()
+ );
+
+ this._keybindings = [];
+ for (let i = 1; i <= 10; i++) {
+ const shortcut = this._settings.get_string(`shortcut-${i}`);
+ if (shortcut) {
+ this._addKeybinding(i);
+ }
+ }
+
+ this._shortcutHandlers = [];
+ for (let i = 1; i <= 10; i++) {
+ const handlerId = this._settings.connect(`changed::shortcut-${i}`, () => this._updateKeybinding(i));
+ this._shortcutHandlers.push(handlerId);
+ }
+
+ this._safeRecreateIndicator();
+
+ this._settings.connect('changed::startup-apps', () => this._syncAutostart());
+ this._syncAutostart();
+
+ this._startupChangedId = this._settings.connect(
+ 'changed::startup-apps',
+ () => {
+ this._addTimeout(500, () => {
+ console.log("[DEBUG] Startup apps changed!");
+ this._syncAutostart();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ );
+
+ this._settings.connect('changed::pinned-apps', () => {
+ this._cleanOrphanedStartupApps();
+ this._syncAutostart();
+ });
+
+ this._settings.connect('changed::startup-apps', () => this._syncAutostart());
+
+ this._cleanOrphanedStartupApps();
+ this._syncAutostart();
+
+ this._settings.connect('changed::pinned-apps', () => {
+ this._cleanOrphanedStartupApps();
+ this._syncAutostart();
+ });
+
+ this._addTimeout(2000, () => {
+ this._syncAutostart();
+ return GLib.SOURCE_REMOVE;
+ });
+
+ this._indicator._checkVisibility();
+
+ }
+
+ disable() {
+
+ if (this._sessionWatcherId) {
+ this._sessionConnection.unwatch_name(this._sessionWatcherId);
+ this._sessionWatcherId = null;
+ }
+
+ if (this._dbusImpl) {
+ try {
+ this._dbusImpl.unexport();
+ } catch (e) {
+ console.error('DBus unexport error:', e);
+ }
+ this._dbusImpl = null;
+ }
+
+
+ for (let i = 1; i <= 10; i++) {
+ this._removeKeybinding(i);
+ }
+
+ const mediaKeysSettings = new Gio.Settings({
+ schema_id: 'org.gnome.settings-daemon.plugins.media-keys'
+ });
+
+ const currentPaths = mediaKeysSettings.get_strv('custom-keybindings');
+ const newPaths = currentPaths.filter(p => !p.includes('app-pinner-'));
+ mediaKeysSettings.set_strv('custom-keybindings', newPaths);
+
+ if (this._positionHandler) {
+ this._settings.disconnect(this._positionHandler);
+ this._positionHandler = null;
+ }
+
+ this._keybindings.forEach(key => Main.wm.removeKeybinding(key));
+ this._keybindings = [];
+
+ if (this._indicator) {
+ const parent = this._indicator.get_parent();
+ if (parent) {
+ parent.remove_child(this._indicator);
+ }
+ this._indicator.destroy();
+ this._indicator = null;
+ }
+
+ if (this._timeoutIds) {
+ this._timeoutIds.forEach(id => GLib.Source.remove(id));
+ this._timeoutIds.clear();
+ }
+
+ this._settings.disconnect(this._shortcutHandler);
+
+ const autostartDir = GLib.build_filenamev([GLib.get_user_config_dir(), 'autostart']);
+ const dir = Gio.File.new_for_path(autostartDir);
+
+ const enumerator = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
+ let fileInfo;
+ while ((fileInfo = enumerator.next_file(null)) !== null) {
+ const name = fileInfo.get_name();
+ if (name.startsWith('app-pinner-')) {
+ GLib.unlink(`${autostartDir}/${name}`);
+ }
+ }
+
+ if (this._startupChangedId) {
+ this._settings.disconnect(this._startupChangedId);
+ }
+
+ try {
+ const enumerator = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
+ let fileInfo;
+ while ((fileInfo = enumerator.next_file(null)) !== null) {
+ const name = fileInfo.get_name();
+ if (name.startsWith('app-pinner-')) {
+ GLib.unlink(`${autostartDir}/${name}`);
+ }
+ }
+ } catch (e) {
+ console.error('Errore pulizia autostart:', e);
+ }
+
+ if (this._settingsHandler) {
+ this._settingsHandler.forEach(handler => {
+ if (handler) {
+ this._settings.disconnect(handler);
+ }
+ });
+ this._settingsHandler = null;
+ }
+
+ if (this._indicator && this._indicator._checkVisibility) {
+ GLib.Source.remove(this._indicator._checkVisibilityTimeout);
+ }
+
+ this._settings = null
+ }
+
+ _updateKeybinding(position) {
+ const shortcut = this._settings.get_string(`shortcut-${position}`);
+ if (shortcut) {
+ this._addKeybinding(position);
+ } else {
+ this._removeKeybinding(position);
+ }
+ }
+
+ // 2. Gestione D-Bus e keybindings
+ _addKeybinding(position) {
+ const key = `shortcut-${position}`;
+ const shortcut = this._settings.get_string(key);
+
+ if (!shortcut) return;
+
+ const customPath = `/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/app-pinner-${position}/`;
+
+ const customSettings = new Gio.Settings({
+ schema_id: 'org.gnome.settings-daemon.plugins.media-keys.custom-keybinding',
+ path: customPath
+ });
+
+ customSettings.set_string('name', `App Pinner Position ${position}`);
+ customSettings.set_string('command', `dbus-send --session --type=method_call --dest=org.gnome.Shell /org/gnome/Shell/Extensions/AppPinner org.gnome.Shell.Extensions.AppPinner.LaunchPosition uint32:${position}`);
+ customSettings.set_string('binding', shortcut);
+
+ const mediaKeysSettings = new Gio.Settings({
+ schema_id: 'org.gnome.settings-daemon.plugins.media-keys'
+ });
+
+ const currentPaths = mediaKeysSettings.get_strv('custom-keybindings');
+ if (!currentPaths.includes(customPath)) {
+ mediaKeysSettings.set_strv('custom-keybindings', [...currentPaths, customPath]);
+ }
+ }
+
+ LaunchPosition(position) {
+ this._launchAppByPosition(position);
+ }
+
+ _launchAppByPosition(position) {
+
+ const apps = this._settings.get_strv('pinned-apps');
+
+ if (apps.length === 0) return;
+
+ const index = position - 1;
+ if (index < 0 || index >= apps.length) return;
+
+ const appId = apps[index];
+
+ if (appId.startsWith('link://')) {
+ if (!this._indicator) return;
+
+ this._indicator._launchApp(appId);
+ return;
+ }
+
+ const appSys = Shell.AppSystem.get_default();
+ let app = appSys.lookup_app(`${appId}.desktop`) || appSys.lookup_app(appId);
+
+ if (!app) {
+ const gioApp = Gio.DesktopAppInfo.new(`${appId}.desktop`) || Gio.DesktopAppInfo.new(appId);
+
+ if (!gioApp) {
+ console.error(`[ERROR] App ${appId} non trovata in nessun formato`);
+ return;
+ }
+ } else {
+ }
+
+ this._indicator._launchApp(appId);
+ }
+
+ // 3. Gestione autostart
+ _syncAutostart() {
+ const autostartDir = GLib.build_filenamev([GLib.get_user_config_dir(), 'autostart']);
+ const startupApps = this._settings.get_strv('startup-apps');
+
+ if (!GLib.file_test(autostartDir, GLib.FileTest.IS_DIR)) {
+ GLib.mkdir_with_parents(autostartDir, 0o755);
+ }
+
+ const validFiles = new Map();
+
+ startupApps.forEach(entry => {
+ if (entry.startsWith('link://')) {
+ const url = entry.replace('link://', '');
+ const sanitized = this._sanitizeForFilename(url);
+ const fileName = `app-pinner-link-${sanitized}.desktop`;
+ validFiles.set(fileName, true);
+
+ const content = [
+ '[Desktop Entry]',
+ 'Type=Application',
+ `Name=Link ${url}`,
+ `Exec=xdg-open "${url}"`,
+ 'Icon=emblem-web-symbolic',
+ 'X-GNOME-Autostart-enabled=true',
+ 'X-GNOME-Autostart-Delay=2',
+ 'NoDisplay=false',
+ ''
+ ].join('\n');
+
+ try {
+ GLib.file_set_contents(`${autostartDir}/${fileName}`, content);
+ GLib.chmod(`${autostartDir}/${fileName}`, 0o644);
+ } catch (e) {
+ console.error(`Errore scrittura link ${url}:`, e.message);
+ }
+ } else {
+ const appInfo = this._getAppInfo(entry);
+ if (!appInfo) {
+ console.error(`[ERROR] App non trovata: ${entry}`);
+ return;
+ }
+
+ const desktopId = appInfo.get_id() || `${entry}.desktop`;
+ const sanitizedId = this._sanitizeForFilename(desktopId);
+ const fileName = `app-pinner-${sanitizedId}.desktop`;
+ validFiles.set(fileName, true);
+
+ const execCommand = appInfo.get_string('Exec');
+ if (!execCommand) {
+ console.error(`[ERROR] Comando non trovato per ${entry}`);
+ return;
+ }
+
+ const desktopContent = [
+ '[Desktop Entry]',
+ 'Type=Application',
+ `Name=${appInfo.get_name()} (App Pinner)`,
+ `Exec=${execCommand}`,
+ 'X-GNOME-Autostart-enabled=true',
+ 'X-GNOME-Autostart-Delay=2',
+ 'Icon=' + (appInfo.get_icon() || ''),
+ 'Comment=Avviato automaticamente da App Pinner',
+ 'NoDisplay=false',
+ ''
+ ].join('\n');
+
+ try {
+ GLib.file_set_contents(`${autostartDir}/${fileName}`, desktopContent);
+ GLib.chmod(`${autostartDir}/${fileName}`, 0o644);
+ } catch (e) {
+ console.error(`Errore scrittura app ${entry}:`, e.message);
+ }
+ }
+ });
+
+ const dir = Gio.File.new_for_path(autostartDir);
+ try {
+ const enumerator = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
+ let fileInfo;
+ while ((fileInfo = enumerator.next_file(null))) {
+ const fileName = fileInfo.get_name();
+ if (fileName.startsWith('app-pinner-') && !validFiles.has(fileName)) {
+ GLib.unlink(`${autostartDir}/${fileName}`);
+ }
+ }
+ } catch (e) {
+ console.error('Errore durante la pulizia dei file:', e.message);
+ }
+ }
+
+ // 4. Funzioni helper e validazione
+ _validateSettings() {
+ const currentPos = this._settings.get_string('position-in-panel');
+ if (!['left', 'right'].includes(currentPos)) {
+ this._settings.set_string('position-in-panel', 'right');
+ }
+ }
+
+ _getAppInfo(appId) {
+ console.log(`[DEBUG] Getting app info for: ${appId}`);
+
+ const appSys = Shell.AppSystem.get_default();
+ const allApps = appSys.get_installed();
+ const foundApp = allApps.find(app =>
+ app.get_id().toLowerCase() === appId.toLowerCase()
+ );
+
+ if (foundApp) {
+ return Gio.DesktopAppInfo.new(foundApp.get_id());
+ }
+
+ const flatpakPaths = [
+ '/var/lib/flatpak/exports/share/applications/',
+ `${GLib.get_home_dir()}/.local/share/flatpak/exports/share/applications/`
+ ];
+
+ for (const path of flatpakPaths) {
+ const fullPath = `${path}${appId}.desktop`;
+ if (GLib.file_test(fullPath, GLib.FileTest.EXISTS)) {
+ return Gio.DesktopAppInfo.new_from_filename(fullPath);
+ }
+ }
+
+ const dataDirs = GLib.get_system_data_dirs();
+ for (const dataDir of dataDirs) {
+ const appPath = `${dataDir}/applications/${appId}.desktop`;
+ if (GLib.file_test(appPath, GLib.FileTest.EXISTS)) {
+ return Gio.DesktopAppInfo.new_from_filename(appPath);
+ }
+ }
+
+ return null;
+ }
+
+ _sanitizeForFilename(appId) {
+ return appId
+ .replace(/\.desktop$/i, '')
+ .replace(/[^a-zA-Z0-9-]/g, '_')
+ .replace(/_+/g, '_')
+ .substring(0, 50);
+ }
+
+ _cleanOrphanedStartupApps() {
+ const pinnedApps = this._settings.get_strv('pinned-apps');
+ const customLinks = this._settings.get_strv('custom-links');
+ const startupApps = this._settings.get_strv('startup-apps');
+
+ const validEntries = new Set([
+ ...pinnedApps,
+ ...customLinks.map(url => `link://${url}`)
+ ]);
+
+ const cleaned = startupApps.filter(appId =>
+ validEntries.has(appId) ||
+ customLinks.includes(appId.replace('link://', ''))
+ );
+
+ if (!arraysEqual(cleaned, startupApps)) {
+ this._settings.set_strv('startup-apps', cleaned);
+ }
+ }
+
+ _removeKeybinding(position) {
+ const customPath = `/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/app-pinner-${position}/`;
+
+ const mediaKeysSettings = new Gio.Settings({
+ schema_id: 'org.gnome.settings-daemon.plugins.media-keys'
+ });
+
+ const currentPaths = mediaKeysSettings.get_strv('custom-keybindings');
+ if (currentPaths.includes(customPath)) {
+ const newPaths = currentPaths.filter(p => p !== customPath);
+ mediaKeysSettings.set_strv('custom-keybindings', newPaths);
+
+ const customSettings = new Gio.Settings({
+ schema_id: 'org.gnome.settings-daemon.plugins.media-keys.custom-keybinding',
+ path: customPath
+ });
+ customSettings.reset('name');
+ customSettings.reset('command');
+ customSettings.reset('binding');
+ }
+ }
+
+ _showMaxItemsError() {
+ Main.notifyError(
+ _("Maximum items reached"),
+ _("You can pin up to 10 items maximum")
+ );
+ }
+
+ _validateAccelerator(accelerator) {
+ if (!accelerator) return false;
+
+ try {
+ const [key, mods] = Clutter.accelerator_parse(accelerator);
+ return key !== 0;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ _safeRecreateIndicator() {
+ const iconSize = this._settings.get_int('icon-size');
+ const spacing = this._settings.get_int('spacing');
+ const labels = this._settings.get_boolean('enable-labels');
+
+ this._recreateIndicator();
+
+ this._settings.set_int('icon-size', iconSize);
+ this._settings.set_int('spacing', spacing);
+ this._settings.set_boolean('enable-labels', labels);
+ }
+
+ _recreateIndicator() {
+ if (this._indicator) {
+ this._indicator.destroy();
+ this._indicator = null;
+ }
+
+ this._indicator = new AppPinner(this._settings);
+ const position = this._settings.get_string('position-in-panel');
+
+ if (this._indicator.get_parent()) {
+ this._indicator.get_parent().remove_child(this._indicator);
+ }
+
+ switch (position) {
+ case 'left':
+ Main.panel._leftBox.insert_child_at_index(this._indicator, 0);
+ break;
+
+ case 'center':
+ if (Main.panel._centerBox) {
+ Main.panel._centerBox.add_child(this._indicator);
+ } else {
+ Main.panel._centerBox = new St.BoxLayout();
+ Main.panel._centerBox.x_align = Clutter.ActorAlign.CENTER;
+ Main.panel.insert_child_at_index(Main.panel._centerBox, 1);
+ Main.panel._centerBox.add_child(this._indicator);
+ }
+ break;
+
+ case 'right':
+ const dateMenu = Main.panel.statusArea?.dateMenu;
+ let targetIndex = 0;
+ if (dateMenu?.actor) {
+ const children = Main.panel._rightBox.get_children();
+ const dateMenuIndex = children.indexOf(dateMenu.actor);
+ targetIndex = dateMenuIndex !== -1 ? dateMenuIndex + 1 : 0;
+ }
+ Main.panel._rightBox.insert_child_at_index(this._indicator, targetIndex);
+ break;
+ }
+
+ this._indicator.set_style_class_name(
+ `app-pinner-position-${position}`
+ );
+
+ Main.panel.queue_relayout();
+ Main.panel.menuManager.addMenu(this._indicator.menu);
+ }
+}
\ No newline at end of file
diff --git a/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/metadata.json b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/metadata.json
new file mode 100644
index 0000000..9655af5
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/metadata.json
@@ -0,0 +1,18 @@
+{
+ "_generated": "Generated by SweetTooth, do not edit",
+ "creator": "comitanigiacomo",
+ "description": "Supercharge your GNOME workflow with this sleek top-bar dock:\n\n ~ 1-Click Access - Launch apps/websites instantly\n ~ Full Customization - Resize icons, adjust spacing, toggle labels\n ~ Live Activity Indicators - Color-coded dots for running apps\n ~ Smart Search - Find+pin apps directly from the menu\n ~ 10 Keyboard Shortcuts - Instant launches with custom keybinds\n ~ Web Links Support - Pin frequently visited URLs\n ~ Auto-Start Essentials - Launch critical apps on login\n ~ Flexible Positioning - Left/Center/Right panel placement\n ~ Press-to-Reorder - Intuitive visual organization\n ~ Smart Limiter - Focus on what matters (max 10 items)",
+ "name": "quick-launch",
+ "settings-schema": "org.gnome.shell.extensions.app-pinner",
+ "shell-version": [
+ "45",
+ "46",
+ "47",
+ "48",
+ "49"
+ ],
+ "stylesheet": "stylesheet.css",
+ "url": "https://github.com/comitanigiacomo/quicklaunch",
+ "uuid": "quicklaunch@comitanigiacomo.github.com",
+ "version": 15
+}
\ No newline at end of file
diff --git a/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/prefs.js b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/prefs.js
new file mode 100644
index 0000000..9ba3f1b
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/prefs.js
@@ -0,0 +1,672 @@
+import Gtk from 'gi://Gtk';
+import Adw from 'gi://Adw';
+import Gio from 'gi://Gio';
+import Gdk from 'gi://Gdk';
+import GLib from 'gi://GLib';
+import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+export default class AppPinnerExtensionPreferences extends ExtensionPreferences {
+ fillPreferencesWindow(window) {
+ const settings = this.getSettings('org.gnome.shell.extensions.app-pinner');
+
+ window.set_search_enabled(true);
+
+ // Pagina Aspetto
+ const appearancePage = new Adw.PreferencesPage({
+ title: 'Appearance',
+ icon_name: 'applications-graphics-symbolic'
+ });
+ this._buildAppearancePage(appearancePage, settings);
+ window.add(appearancePage);
+
+ // Pagina Scorciatoie
+ const shortcutsPage = new Adw.PreferencesPage({
+ title: 'Shortcuts',
+ icon_name: 'preferences-desktop-keyboard-symbolic'
+ });
+ this._buildShortcutsPage(shortcutsPage, settings);
+ window.add(shortcutsPage);
+
+ // Pagina Avvio Automatico
+ const startupPage = new Adw.PreferencesPage({
+ title: 'Startup Apps',
+ icon_name: 'system-run-symbolic'
+ });
+ this._buildStartupPage(startupPage, settings);
+ window.add(startupPage);
+
+ // Pagina About
+ const aboutPage = new Adw.PreferencesPage({
+ title: 'About',
+ icon_name: 'dialog-information-symbolic'
+ });
+ this._buildAboutPage(aboutPage);
+ window.add(aboutPage);
+ }
+
+ _buildAppearancePage(page, settings) {
+ const group = new Adw.PreferencesGroup({
+ title: 'Panel Customization',
+ description: 'Adjust how pinned apps appear in your top panel'
+ });
+
+ const iconSizeRow = new Adw.SpinRow({
+ title: 'Icon Size',
+ subtitle: 'Recommended between 16-24 pixels for optimal panel integration',
+ adjustment: new Gtk.Adjustment({
+ value: settings.get_int('icon-size'),
+ lower: 16,
+ upper: 24,
+ step_increment: 1
+ })
+ });
+ settings.bind('icon-size', iconSizeRow, 'value', Gio.SettingsBindFlags.DEFAULT);
+ group.add(iconSizeRow);
+
+ const positionRow = new Adw.ComboRow({
+ title: 'Panel Position',
+ subtitle: 'Choose where to show the pinned apps in your top panel',
+ model: new Gtk.StringList({ strings: ['left', 'center', 'right'] })
+ });
+
+ positionRow.connect('notify::selected', () => {
+ const selectedString = positionRow.model.get_string(positionRow.selected);
+ settings.set_string('position-in-panel', selectedString);
+ });
+
+ const currentPos = settings.get_string('position-in-panel');
+ positionRow.selected = ['left', 'center', 'right'].indexOf(currentPos);
+
+ group.add(positionRow);
+
+ const spacingRow = new Adw.SpinRow({
+ title: 'Icon Spacing',
+ subtitle: 'Space between app icons in pixels',
+ adjustment: new Gtk.Adjustment({
+ value: settings.get_int('spacing'),
+ lower: 0,
+ upper: 20,
+ step_increment: 1
+ })
+ });
+ settings.bind('spacing', spacingRow, 'value', Gio.SettingsBindFlags.DEFAULT);
+ group.add(spacingRow);
+
+ const colorRow = new Adw.ActionRow({
+ title: 'Indicator Color',
+ subtitle: 'Choose color for running apps indicator'
+ });
+
+ const colorButton = new Gtk.ColorButton({
+ valign: Gtk.Align.CENTER,
+ use_alpha: true
+ });
+
+ const initialColor = new Gdk.RGBA();
+ initialColor.parse(settings.get_string('indicator-color'));
+ colorButton.set_rgba(initialColor);
+
+ colorButton.connect('color-set', (btn) => {
+ const color = btn.get_rgba().to_string();
+ settings.set_string('indicator-color', color);
+ });
+
+ colorRow.add_suffix(colorButton);
+ colorRow.set_activatable_widget(colorButton);
+ group.add(colorRow);
+
+ const sortToggle = new Adw.SwitchRow({
+ title: 'Sort Alphabetically',
+ subtitle: 'Automatically sort apps by name',
+ active: settings.get_boolean('sort-alphabetically')
+ });
+ settings.bind('sort-alphabetically', sortToggle, 'active', Gio.SettingsBindFlags.DEFAULT);
+ group.add(sortToggle);
+
+ const visibilityGroup = new Adw.PreferencesGroup({
+ title: 'Visibility Options',
+ description: 'Control what elements are displayed in the panel'
+ });
+
+ const panelToggle = new Adw.SwitchRow({
+ title: 'Show in Top Panel',
+ subtitle: 'Display pinned apps directly in the panel',
+ active: settings.get_boolean('show-in-panel')
+ });
+ settings.bind('show-in-panel', panelToggle, 'active', Gio.SettingsBindFlags.DEFAULT);
+ visibilityGroup.add(panelToggle);
+
+ const pinIconToggle = new Adw.SwitchRow({
+ title: 'Show Pin Icon',
+ subtitle: 'Display the pin icon when apps are pinned',
+ active: settings.get_boolean('show-pin-icon')
+ });
+ settings.bind('show-pin-icon', pinIconToggle, 'active', Gio.SettingsBindFlags.DEFAULT);
+ visibilityGroup.add(pinIconToggle);
+
+ page.add(visibilityGroup);
+
+ page.add(group);
+ }
+
+ _buildShortcutsPage(page, settings) {
+ const group = new Adw.PreferencesGroup({
+ title: 'Keyboard Shortcuts',
+ description: 'Assign custom keyboard shortcuts to launch your pinned apps directly.\nShortcuts are position-based and will follow your current app order.'
+ });
+
+ const listBox = new Gtk.ListBox({
+ selection_mode: Gtk.SelectionMode.NONE,
+ css_classes: ['shortcut-list']
+ });
+
+ const updateList = () => {
+ let child = listBox.get_first_child();
+ while (child) {
+ const next = child.get_next_sibling();
+ listBox.remove(child);
+ child = next;
+ }
+
+ const pinnedApps = settings.get_strv('pinned-apps');
+ if (pinnedApps.length === 0) {
+ const emptyRow = new Adw.ActionRow({
+ title: 'No Pinned Apps',
+ subtitle: 'Pin apps from the main menu to enable shortcuts',
+ css_classes: ['dim-label']
+ });
+ listBox.append(emptyRow);
+ return;
+ }
+
+ pinnedApps.forEach((appId, index) => {
+ const position = index + 1;
+ const appName = this._getAppName(appId) || 'Unknown Application';
+
+ const row = new Adw.ActionRow({
+ title: `Position ${position}`,
+ subtitle: appName,
+ css_classes: ['shortcut-row']
+ });
+
+ const entry = new Gtk.Entry({
+ text: this._acceleratorToLabel(settings.get_string(`shortcut-${position}`)),
+ editable: false,
+ width_chars: 20,
+ css_classes: ['shortcut-entry'],
+ placeholder_text: 'Click to set shortcut',
+ tooltip_text: `Press any key combination for ${appName}`
+ });
+
+ entry.connect('notify::has-focus', (widget) => {
+ if (!widget.hasFocus) {
+ widget.set_selection(0, 0);
+ widget.set_position(-1);
+ }
+ });
+
+ const controller = new Gtk.EventControllerKey();
+ controller.connect('key-pressed', (_, keyval, _keycode, state) => {
+ if (keyval === Gdk.KEY_Escape) {
+ entry.grab_focus_away();
+ return Gdk.EVENT_STOP;
+ }
+
+ const isModifier = [
+ Gdk.KEY_Control_L, Gdk.KEY_Control_R,
+ Gdk.KEY_Shift_L, Gdk.KEY_Shift_R,
+ Gdk.KEY_Alt_L, Gdk.KEY_Alt_R,
+ Gdk.KEY_Super_L, Gdk.KEY_Super_R
+ ].includes(keyval);
+
+ if (isModifier) return Gdk.EVENT_STOP;
+
+ const modifiers = state & Gtk.accelerator_get_default_mod_mask();
+ const accelerator = Gtk.accelerator_name(keyval, modifiers);
+
+ entry.text = this._acceleratorToLabel(accelerator);
+ settings.set_string(`shortcut-${position}`, accelerator);
+ return Gdk.EVENT_STOP;
+ });
+
+ entry.add_controller(controller);
+
+ const clearButton = new Gtk.Button({
+ icon_name: 'edit-clear-symbolic',
+ tooltip_text: 'Clear shortcut'
+ });
+
+ clearButton.connect('clicked', () => {
+ entry.text = 'Disabled';
+ settings.set_string(`shortcut-${position}`, '');
+ });
+
+ row.add_suffix(entry);
+ row.add_suffix(clearButton);
+ listBox.append(row);
+ });
+ };
+
+ updateList();
+
+ settings.connect('changed::pinned-apps', () => {
+ updateList();
+ });
+
+ const scrolledWindow = new Gtk.ScrolledWindow({
+ height_request: 300,
+ child: listBox
+ });
+
+ group.add(scrolledWindow);
+ page.add(group);
+ }
+
+ _buildStartupPage(page, settings) {
+ const group = new Adw.PreferencesGroup({
+ title: 'Launch at Startup',
+ description: 'Select applications to launch automatically when you log in'
+ });
+
+ const listStore = new Gtk.StringList();
+ const updateList = () => {
+ const pinned = settings.get_strv('pinned-apps');
+ listStore.splice(0, listStore.get_n_items(), pinned);
+ };
+
+ updateList();
+ settings.connect('changed::pinned-apps', updateList);
+
+ const factory = new Gtk.SignalListItemFactory();
+ factory.connect('setup', (_, listItem) => {
+ const row = new Adw.ActionRow();
+ const icon = new Gtk.Image({ icon_size: Gtk.IconSize.LARGE, margin_end: 12 });
+ row.add_prefix(icon);
+
+ const toggle = new Gtk.Switch({
+ valign: Gtk.Align.CENTER,
+ halign: Gtk.Align.END
+ });
+ row.add_suffix(toggle);
+
+ row._icon = icon;
+ row._toggle = toggle;
+ listItem.set_child(row);
+ });
+
+ factory.connect('bind', (_, listItem) => {
+ const appId = listItem.get_item().string;
+ const row = listItem.get_child();
+ const toggle = row._toggle;
+
+ const startupApps = settings.get_strv('startup-apps');
+ toggle.set_active(startupApps.includes(appId));
+
+ toggle.connect('notify::active', (sw) => {
+ const newStartup = sw.active
+ ? [...settings.get_strv('startup-apps'), appId]
+ : settings.get_strv('startup-apps').filter(id => id !== appId);
+ settings.set_strv('startup-apps', newStartup);
+ });
+
+ const appInfo = this._getAppInfo(appId);
+ row.set_title(appInfo?.get_name() || appId);
+ row._icon.set_from_gicon(appInfo?.get_icon());
+ });
+
+ const listView = new Gtk.ListView({
+ model: new Gtk.SingleSelection({ model: listStore }),
+ factory: factory
+ });
+
+ const scrolledWindow = new Gtk.ScrolledWindow({
+ height_request: 300,
+ child: listView
+ });
+ group.add(scrolledWindow);
+ page.add(group);
+ }
+
+ _buildAboutPage(page) {
+ const group = new Adw.PreferencesGroup({
+ title: this.metadata.name,
+ description: 'A modern application pinner and quick launcher for GNOME Shell'
+ });
+
+ const versionRow = new Adw.ActionRow({
+ title: 'Version'
+ });
+ const versionLabel = new Gtk.Label({
+ label: `v${this.metadata.version.toString()}`,
+ css_classes: ['dim-label'],
+ xalign: 0
+ });
+ versionRow.add_suffix(versionLabel);
+ group.add(versionRow);
+
+ const authorRow = new Adw.ActionRow({
+ title: 'Developer'
+ });
+
+ const githubButton = new Gtk.Button({
+ label: this.metadata.creator,
+ css_classes: ['flat', 'suggested-action'],
+ tooltip_text: 'Open GitHub profile',
+ margin_top: 6,
+ margin_bottom: 6
+ });
+
+ githubButton.connect('clicked', () => {
+ Gio.AppInfo.launch_default_for_uri('https://github.com/comitanigiacomo', null);
+ });
+
+ authorRow.add_suffix(githubButton);
+ authorRow.activatable_widget = githubButton;
+ group.add(authorRow);
+
+ const repoRow = new Adw.ActionRow({
+ title: 'Repository'
+ });
+
+ const repoButton = new Gtk.Button({
+ label: 'Source Code',
+ css_classes: ['flat', 'suggested-action'],
+ tooltip_text: 'Open GitHub repository',
+ margin_top: 6,
+ margin_bottom: 6
+ });
+
+ repoButton.connect('clicked', () => {
+ if (this.metadata.url) {
+ Gio.AppInfo.launch_default_for_uri(this.metadata.url, null);
+ }
+ });
+
+ repoRow.add_suffix(repoButton);
+ repoRow.activatable_widget = repoButton;
+ group.add(repoRow);
+
+ const licenseRow = new Adw.ActionRow({
+ title: 'License'
+ });
+
+ const licenseButton = new Gtk.Button({
+ label: 'GPLv3',
+ css_classes: ['flat', 'suggested-action'],
+ tooltip_text: 'View license details',
+ margin_top: 6,
+ margin_bottom: 6
+ });
+
+ licenseButton.connect('clicked', () => {
+ Gio.AppInfo.launch_default_for_uri('https://github.com/comitanigiacomo/quicklaunch?tab=GPL-3.0-1-ov-file', null);
+ });
+
+ licenseRow.add_suffix(licenseButton);
+ licenseRow.activatable_widget = licenseButton;
+ group.add(licenseRow);
+
+ const descriptionLabel = new Gtk.Label({
+ label: `
+
+ ● 🚀 One-Click Access: Launch apps/links directly from panel\n
+ ● ⌨️ Custom Shortcuts: Configurable keyboard combinations\n
+ ● 🔴 Live Indicators: Real-time running app status\n
+ ● 🌐 Web Links: Open URLs in default browser\n
+ ● ⚡ Auto-Start: Launch critical apps at login\n
+ ● 🔧 Smart Reordering: long-press to organize\n
+ ● 🔍 Instant Search: Find/pin apps from dropdown\n\n
+ Optimized for power users - Seamless GNOME integration
+ `
+ .replace(/ {8}/g, '')
+ .trim(),
+ wrap: true,
+ xalign: 0.5,
+ halign: Gtk.Align.CENTER,
+ hexpand: true
+ });
+
+ const descGroup = new Adw.PreferencesGroup();
+ descGroup.add(descriptionLabel);
+
+ page.add(group);
+ page.add(descGroup);
+ }
+
+ _acceleratorToLabel(accel) {
+ if (!accel) return 'Disabled';
+
+ try {
+ const [success, keyval, mods] = Gtk.accelerator_parse(accel);
+ return success ? Gtk.accelerator_get_label(keyval, mods) : 'Invalid';
+ } catch (e) {
+ return 'Error';
+ }
+ }
+
+ _labelToAccelerator(label) {
+ let [success, keyval, mods] = Gtk.accelerator_parse(label);
+
+ if (!success || keyval === 0) {
+ const custom = this._parseCustomShortcut(label);
+ [success, keyval, mods] = Gtk.accelerator_parse(custom);
+ }
+
+ if (success && keyval !== 0) {
+ return Gtk.accelerator_name(keyval, mods);
+ }
+ return '';
+ }
+
+ _configureShortcut(window, settings, key, entry) {
+
+ const dialog = new Gtk.Dialog({
+ title: 'Set Shortcut',
+ transient_for: window,
+ modal: true,
+ use_header_bar: true
+ });
+
+ let accelerator = settings.get_string(key);
+ const label = new Gtk.Label({
+ label: 'Press desired key combination...\nCurrent: ' + this._acceleratorToLabel(accelerator),
+ margin: 12
+ });
+
+ const controller = new Gtk.EventControllerKey();
+ controller.connect('key-pressed', (_, keyval, _keycode, state) => {
+ try {
+ const mask = state & Gtk.accelerator_get_default_mod_mask();
+
+ const validMods = mask & (
+ Gdk.ModifierType.SHIFT_MASK |
+ Gdk.ModifierType.CONTROL_MASK |
+ Gdk.ModifierType.ALT_MASK |
+ Gdk.ModifierType.SUPER_MASK
+ );
+
+ if (keyval === Gdk.KEY_Shift_L || keyval === Gdk.KEY_Shift_R ||
+ keyval === Gdk.KEY_Control_L || keyval === Gdk.KEY_Control_R ||
+ keyval === Gdk.KEY_Alt_L || keyval === Gdk.KEY_Alt_R ||
+ keyval === Gdk.KEY_Super_L || keyval === Gdk.KEY_Super_R) {
+ return Gdk.EVENT_STOP;
+ }
+
+ accelerator = Gtk.accelerator_name(keyval, validMods);
+ label.label = 'New shortcut: ' + this._acceleratorToLabel(accelerator);
+
+ GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ dialog.queue_draw();
+ return GLib.SOURCE_REMOVE;
+ });
+
+ } catch (e) {
+ console.error('Error processing key:', e);
+ }
+ return Gdk.EVENT_STOP;
+ });
+
+ dialog.add_controller(controller);
+
+ const resetButton = new Gtk.Button({
+ label: 'Reset',
+ margin_end: 12
+ });
+ resetButton.connect('clicked', () => {
+ accelerator = '';
+ label.label = 'Shortcut cleared!';
+ });
+
+ dialog.get_content_area().append(label);
+ dialog.get_header_bar().pack_end(resetButton);
+
+ dialog.add_button('Cancel', Gtk.ResponseType.CANCEL);
+ dialog.add_button('Save', Gtk.ResponseType.OK);
+
+ dialog.connect('response', (_, response) => {
+ if (response === Gtk.ResponseType.OK) {
+ try {
+ const [success] = Gtk.accelerator_parse(accelerator);
+ if (success) {
+ settings.set_string(key, accelerator);
+ entry.text = this._acceleratorToLabel(accelerator);
+ } else {
+ this._showErrorDialog('Invalid shortcut combination');
+ }
+ } catch (e) {
+ this._showErrorDialog(`Error saving shortcut: ${e.message}`);
+ }
+ }
+ dialog.destroy();
+ });
+
+ dialog.present();
+ }
+
+ _showErrorDialog(message) {
+ const dialog = new Gtk.MessageDialog({
+ transient_for: this._window,
+ modal: true,
+ message_type: Gtk.MessageType.ERROR,
+ buttons: Gtk.ButtonsType.OK,
+ text: message
+ });
+ dialog.connect('response', () => dialog.destroy());
+ dialog.present();
+ }
+
+ _parseCustomShortcut(text) {
+ try {
+ const normalized = text
+ .replace(//g, '+')
+ .toLowerCase();
+
+ const parts = normalized.split('+');
+ let mods = 0;
+ let keyval = 0;
+
+ parts.forEach(part => {
+ const mod = Gtk.accelerator_parse_modifier(part.trim());
+ if (mod) {
+ mods |= mod;
+ } else {
+ keyval = Gtk.accelerator_parse_key(part.trim(), 0);
+ }
+ });
+
+ return Gtk.accelerator_name(keyval, mods);
+ } catch (e) {
+ return '';
+ }
+ }
+
+ _validateAccelerator(accel) {
+ try {
+ const [success, keyval] = Gtk.accelerator_parse(accel);
+ return success && keyval !== 0;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ _getAppInfo(appId) {
+ const variants = [
+ appId,
+ `${appId}.desktop`,
+ `${appId.replace(/-/g, '')}.desktop`,
+ appId.toLowerCase(),
+ `${appId.toLowerCase()}.desktop`
+ ];
+
+ const searchPaths = [
+ GLib.get_user_data_dir() + '/applications',
+ '/usr/share/applications',
+ '/usr/local/share/applications'
+ ];
+
+ for (const variant of variants) {
+ const appInfo = Gio.DesktopAppInfo.new(variant);
+ if (appInfo) return appInfo;
+
+ for (const path of searchPaths) {
+ const fullPath = `${path}/${variant}`;
+ if (GLib.file_test(fullPath, GLib.FileTest.EXISTS)) {
+ return Gio.DesktopAppInfo.new_from_filename(fullPath);
+ }
+ }
+ }
+ return null;
+ }
+
+ _getAppDisplayInfo(appId) {
+ let appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`);
+
+ if (!appInfo) {
+ const cleanId = appId.replace(/^application:\/\//, '');
+ appInfo = Gio.DesktopAppInfo.new(`${cleanId}.desktop`);
+ }
+
+ if (!appInfo) {
+ const shellApp = Shell.AppSystem.get_default().lookup_app(appId);
+ if (shellApp) {
+ return {
+ name: shellApp.get_name(),
+ icon: shellApp.get_icon()
+ };
+ }
+ }
+
+ if (!appInfo) {
+ const variations = [
+ appId.toLowerCase(),
+ appId.replace(/-/g, ''),
+ `${appId}.desktop`,
+ `${appId.toLowerCase()}.desktop`
+ ];
+
+ for (const variant of variations) {
+ appInfo = Gio.DesktopAppInfo.new(variant);
+ if (appInfo) break;
+ }
+ }
+
+ return {
+ name: appInfo ? appInfo.get_name() : appId,
+ icon: appInfo ? appInfo.get_icon() : null
+ };
+ }
+
+ _getAppName(appId) {
+ const appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`);
+ return appInfo ? appInfo.get_name() : appId;
+ }
+
+ _sanitizeAppId(appId) {
+ return appId
+ .replace(/^application:\/\//, '')
+ .replace(/\.desktop$/i, '')
+ .replace(/\s+/g, '-')
+ .toLowerCase();
+ }
+}
\ No newline at end of file
diff --git a/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/schemas/gschemas.compiled b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/schemas/gschemas.compiled
new file mode 100644
index 0000000..b876440
Binary files /dev/null and b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/schemas/gschemas.compiled differ
diff --git a/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/schemas/org.gnome.shell.extensions.quicklaunch.gschema.xml b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/schemas/org.gnome.shell.extensions.quicklaunch.gschema.xml
new file mode 100644
index 0000000..0d536af
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/schemas/org.gnome.shell.extensions.quicklaunch.gschema.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+ []
+ Custom keybindings paths
+
+
+
+ true
+ Show pin icon in panel
+ Whether to show the pin icon in the panel when there are pinned items
+
+
+
+
+ []
+ Pinned Applications
+
+
+
+
+ 24
+
+ Icon size in pixels
+
+
+
+ true
+ Show apps in panel
+ Whether to display pinned apps in the top panel
+
+
+
+ false
+ Always show menu icon
+ Keep the menu icon visible even when panel apps are hidden
+
+
+
+ 'right'
+
+
+
+
+
+ Panel position
+ Posizione nella barra superiore
+
+
+
+ 6
+
+ Spacing between icons
+
+
+
+ false
+ Show application labels
+
+
+
+
+ true
+ Enable launch animation
+
+
+
+
+ 10
+
+ Maximum apps to display
+
+
+
+ ['org.gnome.Calculator.desktop']
+ Default suggested apps
+
+
+
+
+ ''
+ Shortcut for position 1
+
+
+ ''
+ Shortcut for position 2
+
+
+ ''
+ Shortcut for position 3
+
+
+ ''
+ Shortcut for position 4
+
+
+ ''
+ Shortcut for position 5
+
+
+ ''
+ Shortcut for position 6
+
+
+ ''
+ Shortcut for position 7
+
+
+ ''
+ Shortcut for position 8
+
+
+ ''
+ Shortcut for position 9
+
+
+ ''
+ Shortcut for position 10
+
+
+ []
+ Startup Applications
+ List of application IDs to launch at system startup.
+
+
+ '#00ff00'
+ Indicator color
+ Color for running application indicator
+
+
+ []
+ Custom pinned links
+
+
+ false
+ Sort apps alphabetically
+ If enabled, pinned apps will be sorted by name.
+
+
+
\ No newline at end of file
diff --git a/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/stylesheet.css b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/stylesheet.css
new file mode 100644
index 0000000..460356b
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/quicklaunch@comitanigiacomo.github.com/stylesheet.css
@@ -0,0 +1,131 @@
+.app-pinner-icons {
+ height: 100%;
+ padding: 0 2px;
+}
+
+.app-pinner-icon-box {
+ height: 100%;
+ padding: 2px 0;
+ margin: auto 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.app-pinner-icon {
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+ padding: 0 !important;
+ margin: auto;
+ border-radius: 6px;
+ transition: all 0.2s ease-in-out;
+}
+
+.app-pinner-link-icon {
+ margin: auto;
+ -st-icon-style: symbolic;
+}
+
+.app-pinner-running-indicator {
+ background-color: #ff0000;
+ border-radius: 4px;
+ width: 8px;
+ height: 8px;
+ position: absolute;
+ top: 3px;
+ right: 3px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
+ z-index: 1000;
+}
+
+.app-pinner-label {
+ max-width: 100px;
+ text-overflow: ellipsis;
+ font-size: 0.8em;
+ margin-top: 2px;
+}
+
+.app-pinner-remove-btn {
+ font-size: 18px;
+ background-color: transparent;
+ border-radius: 100px;
+ min-width: 18px;
+ min-height: 18px;
+ padding: 2px;
+ display: flex;
+ align-items: right;
+ justify-content: right;
+ transition: background-color 0.2s ease-in-out;
+}
+
+.app-pinner-remove-btn:hover {
+ background-color: rgba(255, 0, 0, 0.8);
+}
+
+.app-pinner-remove-label {
+ font-size: 16px;
+ font-weight: bold;
+ color: white;
+ text-align: center;
+}
+
+.search-box {
+ width: 200px;
+ padding: 8px 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.app-pinner-link-input {
+ min-width: 200px;
+ padding: 6px 12px;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.3);
+ color: #ffffff;
+ border: 1px solid rgba(255,255,255,0.2);
+ font-size: 13px;
+}
+
+.app-pinner-link-input:focus {
+ border-color: #3584e4;
+ box-shadow: inset 0 0 0 1px #3584e4;
+}
+
+.app-pinner-add-link-btn {
+ background-color: rgba(78, 166, 78, 0.8);
+ border-radius: 8px;
+ min-width: 32px;
+ min-height: 32px;
+ padding: 6px;
+ margin-left: 6px;
+ transition: all 0.2s ease-in-out;
+}
+
+.app-pinner-add-link-btn:hover {
+ background-color: rgba(98, 196, 98, 0.9);
+ transform: scale(1.05);
+}
+
+.app-pinner-add-link-btn:active {
+ background-color: rgba(68, 146, 68, 0.9);
+}
+
+.app-pinner-icon-container {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+}
+
+.app-pinner-link-box {
+ margin: 8px;
+ padding: 6px;
+ background-color: rgba(255,255,255,0.05);
+ border-radius: 8px;
+ border: 1px solid rgba(255,255,255,0.1);
+}
\ No newline at end of file
diff --git a/gnome/.local/share/gnome-shell/extensions/screen-rotate@shyzus.github.io/locale/ru/LC_MESSAGES/gnome-shell-extension-screen-rotate.mo b/gnome/.local/share/gnome-shell/extensions/screen-rotate@shyzus.github.io/locale/ru/LC_MESSAGES/gnome-shell-extension-screen-rotate.mo
new file mode 100644
index 0000000..6df9677
Binary files /dev/null and b/gnome/.local/share/gnome-shell/extensions/screen-rotate@shyzus.github.io/locale/ru/LC_MESSAGES/gnome-shell-extension-screen-rotate.mo differ
diff --git a/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/extension.js b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/extension.js
new file mode 100644
index 0000000..517f2c6
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/extension.js
@@ -0,0 +1,262 @@
+import GObject from "gi://GObject";
+import GLib from "gi://GLib";
+import Soup from "gi://Soup";
+
+import * as Main from "resource:///org/gnome/shell/ui/main.js";
+import * as QuickSettings from "resource:///org/gnome/shell/ui/quickSettings.js";
+import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
+
+function clamp01(x) {
+ return Math.max(0, Math.min(1, x));
+}
+
+function jsonRpc(method, params = {}) {
+ return JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
+}
+
+async function postJson(session, url, bodyStr) {
+ const msg = Soup.Message.new("POST", url);
+ msg.request_headers.append("Content-Type", "application/json");
+
+ // Soup 3: request body as GLib.Bytes
+ msg.set_request_body_from_bytes(
+ "application/json",
+ new GLib.Bytes(bodyStr),
+ );
+
+ const bytes = await session.send_and_read_async(
+ msg,
+ GLib.PRIORITY_DEFAULT,
+ null,
+ );
+ const text = new TextDecoder().decode(bytes.get_data());
+ return JSON.parse(text);
+}
+
+function connectedClients(group) {
+ return (group?.clients ?? []).filter((c) => c?.connected);
+}
+
+function connectedClientIds(group) {
+ return connectedClients(group)
+ .map((c) => c.id)
+ .filter(Boolean);
+}
+
+function avgConnectedVolumePercent(group) {
+ const percents = connectedClients(group)
+ .map((c) => c?.config?.volume?.percent)
+ .filter((p) => typeof p === "number");
+
+ if (!percents.length) return null;
+
+ return percents.reduce((a, b) => a + b, 0) / percents.length;
+}
+
+function groupLabel(group) {
+ const base =
+ group?.name && group.name.trim()
+ ? group.name.trim()
+ : (group?.stream_id ?? "Group");
+
+ const connected = connectedClients(group).length;
+ const total = group?.clients?.length ?? 0;
+
+ return `${base} (${connected}/${total})`;
+}
+
+const SnapcastSlider = GObject.registerClass(
+ class SnapcastSlider extends QuickSettings.QuickSlider {
+ constructor(extension) {
+ super({
+ title: "Snapcast",
+ iconName: "audio-volume-high-symbolic",
+ });
+
+ this._ext = extension;
+ this._settings = extension.getSettings();
+ this._session = new Soup.Session();
+
+ this._pollId = 0;
+ this._debounceId = 0;
+ this._sliderChangedId = 0;
+ this._settingsChangedId = 0;
+
+ this._isUpdatingFromServer = false;
+ this._groupClientIds = [];
+
+ // Load settings
+ this._reloadSettings();
+
+ // React to settings changes (server-url, group-id, poll-seconds)
+ this._settingsChangedId = this._settings.connect("changed", () => {
+ this._reloadSettings();
+ this._restartPolling();
+ this._pollOnce().catch(logError);
+ });
+
+ // Slider change -> debounce -> set group volume
+ this._sliderChangedId = this.slider.connect("notify::value", () => {
+ if (this._isUpdatingFromServer) return;
+ this._debouncedSetVolume();
+ });
+
+ // Start polling + initial sync
+ this._restartPolling();
+ this._pollOnce().catch(logError);
+ }
+
+ _reloadSettings() {
+ // You’ll define these keys in your gschema:
+ // - server-url (string)
+ // - group-id (string)
+ // - poll-seconds (uint)
+ this._url = this._settings.get_string("server-url");
+ this._groupId = this._settings.get_string("group-id");
+ this._pollSeconds = this._settings.get_uint("poll-seconds");
+
+ // Sensible guards
+ if (!this._pollSeconds || this._pollSeconds < 2)
+ this._pollSeconds = 5;
+ }
+
+ _restartPolling() {
+ if (this._pollId) {
+ GLib.source_remove(this._pollId);
+ this._pollId = 0;
+ }
+
+ this._pollId = GLib.timeout_add_seconds(
+ GLib.PRIORITY_DEFAULT,
+ this._pollSeconds,
+ () => {
+ this._pollOnce().catch(logError);
+ return GLib.SOURCE_CONTINUE;
+ },
+ );
+ }
+
+ _debouncedSetVolume() {
+ if (this._debounceId) {
+ GLib.source_remove(this._debounceId);
+ this._debounceId = 0;
+ }
+
+ // Wait a beat so dragging doesn’t fire 100 requests/sec
+ this._debounceId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ 150,
+ () => {
+ this._debounceId = 0;
+ this._setGroupVolumeFromSlider().catch(logError);
+ return GLib.SOURCE_REMOVE;
+ },
+ );
+ }
+
+ async _pollOnce() {
+ // If not configured yet
+ if (!this._url) {
+ this.title = "Snapcast — set server URL";
+ this._groupClientIds = [];
+ return;
+ }
+ if (!this._groupId) {
+ this.title = "Snapcast — select a group";
+ this._groupClientIds = [];
+ return;
+ }
+
+ // Pull all status in one request
+ const resp = await postJson(
+ this._session,
+ this._url,
+ jsonRpc("Server.GetStatus", {}),
+ );
+ const groups = resp?.result?.server?.groups ?? [];
+ const group = groups.find((g) => g.id === this._groupId);
+
+ if (!group) {
+ this.title = "Snapcast — group not found";
+ this._groupClientIds = [];
+ return;
+ }
+
+ // Only connected clients
+ this._groupClientIds = connectedClientIds(group);
+
+ // Update title with something human-readable
+ this.title = `Snapcast — ${groupLabel(group)}`;
+
+ // Slider position = average of connected clients’ volumes
+ const avg = avgConnectedVolumePercent(group);
+ if (typeof avg === "number") {
+ const v = clamp01(avg / 100);
+
+ this._isUpdatingFromServer = true;
+ try {
+ if (Math.abs(this.slider.value - v) > 0.01)
+ this.slider.value = v;
+ } finally {
+ this._isUpdatingFromServer = false;
+ }
+ }
+ }
+
+ async _setGroupVolumeFromSlider() {
+ if (!this._groupClientIds.length) return;
+
+ const percent = Math.round(clamp01(this.slider.value) * 100);
+
+ // Fan out: set volume for each connected client in the group
+ await Promise.allSettled(
+ this._groupClientIds.map((id) => {
+ const body = jsonRpc("Client.SetVolume", {
+ id,
+ volume: { percent },
+ });
+ return postJson(this._session, this._url, body);
+ }),
+ );
+ }
+
+ destroy() {
+ if (this._pollId) GLib.source_remove(this._pollId);
+ if (this._debounceId) GLib.source_remove(this._debounceId);
+ if (this._sliderChangedId)
+ this.slider.disconnect(this._sliderChangedId);
+ if (this._settingsChangedId)
+ this._settings.disconnect(this._settingsChangedId);
+
+ super.destroy();
+ }
+ },
+);
+
+const SnapcastIndicator = GObject.registerClass(
+ class SnapcastIndicator extends QuickSettings.SystemIndicator {
+ constructor(extension) {
+ super();
+ this.quickSettingsItems.push(new SnapcastSlider(extension));
+ }
+
+ destroy() {
+ this.quickSettingsItems.forEach((item) => item.destroy());
+ super.destroy();
+ }
+ },
+);
+
+export default class SnapshellExtension extends Extension {
+ enable() {
+ this._indicator = new SnapcastIndicator(this);
+ Main.panel.statusArea.quickSettings.addExternalIndicator(
+ this._indicator,
+ );
+ }
+
+ disable() {
+ this._indicator?.destroy();
+ this._indicator = null;
+ }
+}
diff --git a/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/metadata.json b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/metadata.json
new file mode 100644
index 0000000..b4b7295
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/metadata.json
@@ -0,0 +1,7 @@
+{
+ "uuid": "snapshell@unbl.ink",
+ "name": "Snapshell",
+ "description": "Quick Settings volume slider for a Snapcast group (connected clients only).",
+ "shell-version": ["45", "46", "47", "48", "49", "50", "51"],
+ "version": 1
+}
diff --git a/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/prefs.js b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/prefs.js
new file mode 100644
index 0000000..f676390
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/prefs.js
@@ -0,0 +1,200 @@
+import Adw from "gi://Adw";
+import Gio from "gi://Gio";
+import GLib from "gi://GLib";
+import Gtk from "gi://Gtk";
+import Soup from "gi://Soup";
+
+import { ExtensionPreferences } from "resource:///org/gnome/shell/extensions/prefs.js";
+
+function jsonRpc(method, params = {}) {
+ return JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
+}
+
+async function postJson(session, url, bodyStr) {
+ const msg = Soup.Message.new("POST", url);
+ msg.request_headers.append("Content-Type", "application/json");
+ msg.set_request_body_from_bytes(
+ "application/json",
+ new GLib.Bytes(bodyStr),
+ );
+
+ const bytes = await session.send_and_read_async(
+ msg,
+ GLib.PRIORITY_DEFAULT,
+ null,
+ );
+ const text = new TextDecoder().decode(bytes.get_data());
+ return JSON.parse(text);
+}
+
+function connectedCount(group) {
+ return (group?.clients ?? []).filter((c) => c?.connected).length;
+}
+
+function groupLabel(group) {
+ const base =
+ group?.name && group.name.trim()
+ ? group.name.trim()
+ : (group?.stream_id ?? "Group");
+
+ const connected = connectedCount(group);
+ const total = group?.clients?.length ?? 0;
+
+ return `${base} (${connected}/${total})`;
+}
+
+export default class SnapshellPreferences extends ExtensionPreferences {
+ fillPreferencesWindow(window) {
+ const settings = this.getSettings(
+ "org.gnome.shell.extensions.snapshell",
+ );
+ const session = new Soup.Session();
+
+ // --- UI models ---
+ const groupNames = new Gtk.StringList();
+ let groupIds = []; // parallel array: index -> group id
+
+ // --- Page ---
+ const page = new Adw.PreferencesPage({
+ title: "Snapshell",
+ icon_name: "audio-volume-high-symbolic",
+ });
+ window.add(page);
+
+ // --- Group: Connection ---
+ const connectionGroup = new Adw.PreferencesGroup({
+ title: "Connection",
+ description: "Configure Snapserver JSON-RPC endpoint.",
+ });
+ page.add(connectionGroup);
+
+ const urlRow = new Adw.EntryRow({
+ title: "Server URL",
+ text: settings.get_string("server-url"),
+ });
+ urlRow.set_tooltip_text("Example: https://snap.unbl.ink/jsonrpc");
+ connectionGroup.add(urlRow);
+
+ urlRow.connect("notify::text", () => {
+ settings.set_string("server-url", urlRow.text.trim());
+ });
+
+ // --- Group: Target Group ---
+ const targetGroup = new Adw.PreferencesGroup({
+ title: "Target group",
+ description:
+ "Choose which Snapcast group the Quick Settings slider controls.",
+ });
+ page.add(targetGroup);
+
+ const groupRow = new Adw.ComboRow({
+ title: "Snapcast group",
+ model: groupNames,
+ });
+ targetGroup.add(groupRow);
+
+ const refreshRow = new Adw.ActionRow({
+ title: "Refresh groups",
+ subtitle: "Fetch groups from Server.GetStatus",
+ });
+ const refreshBtn = new Gtk.Button({ label: "Refresh" });
+ refreshRow.add_suffix(refreshBtn);
+ refreshRow.activatable_widget = refreshBtn;
+ targetGroup.add(refreshRow);
+
+ // --- Group: Behavior ---
+ const behaviorGroup = new Adw.PreferencesGroup({
+ title: "Behavior",
+ });
+ page.add(behaviorGroup);
+
+ const pollRow = new Adw.SpinRow({
+ title: "Poll interval (seconds)",
+ adjustment: new Gtk.Adjustment({
+ lower: 2,
+ upper: 60,
+ step_increment: 1,
+ page_increment: 5,
+ value: settings.get_uint("poll-seconds") || 5,
+ }),
+ });
+ behaviorGroup.add(pollRow);
+
+ pollRow.connect("notify::value", () => {
+ settings.set_uint("poll-seconds", pollRow.value);
+ });
+
+ // --- Helpers to load + sync groups ---
+ const setStatus = (text) => {
+ refreshRow.subtitle = text;
+ };
+
+ const clearGroups = () => {
+ // Remove all items from StringList
+ while (groupNames.get_n_items() > 0) groupNames.remove(0);
+ groupIds = [];
+ };
+
+ const syncComboSelectionFromSettings = () => {
+ const selectedId = settings.get_string("group-id");
+ const idx = groupIds.indexOf(selectedId);
+ groupRow.selected = idx >= 0 ? idx : Gtk.INVALID_LIST_POSITION;
+ };
+
+ const fetchGroups = async () => {
+ const url = settings.get_string("server-url").trim();
+ if (!url) {
+ clearGroups();
+ setStatus("Set Server URL first.");
+ return;
+ }
+
+ setStatus("Fetching…");
+ try {
+ const resp = await postJson(
+ session,
+ url,
+ jsonRpc("Server.GetStatus", {}),
+ );
+ const groups = resp?.result?.server?.groups ?? [];
+
+ clearGroups();
+
+ // Populate list
+ for (const g of groups) {
+ groupNames.append(groupLabel(g));
+ groupIds.push(g.id);
+ }
+
+ setStatus(`Loaded ${groups.length} group(s).`);
+ syncComboSelectionFromSettings();
+ } catch (e) {
+ clearGroups();
+ setStatus(`Failed: ${e?.message ?? e}`);
+ }
+ };
+
+ // When user picks a row, store its group id
+ groupRow.connect("notify::selected", () => {
+ const idx = groupRow.selected;
+ if (idx === Gtk.INVALID_LIST_POSITION) return;
+ const id = groupIds[idx];
+ if (id) settings.set_string("group-id", id);
+ });
+
+ // Refresh button
+ refreshBtn.connect("clicked", () => {
+ fetchGroups();
+ });
+
+ // If URL changes, offer fresh fetch (don’t auto-spam)
+ urlRow.connect("notify::text", () => {
+ setStatus("URL changed. Click Refresh.");
+ clearGroups();
+ syncComboSelectionFromSettings();
+ });
+
+ // Initial load
+ fetchGroups();
+ }
+}
diff --git a/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/schemas/gschemas.compiled b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/schemas/gschemas.compiled
new file mode 100644
index 0000000..bfcdbc6
Binary files /dev/null and b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/schemas/gschemas.compiled differ
diff --git a/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/schemas/org.gnome.shell.extensions.snapshell.gschema.xml b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/schemas/org.gnome.shell.extensions.snapshell.gschema.xml
new file mode 100644
index 0000000..e4a9f9f
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/snapshell@unbl.ink/schemas/org.gnome.shell.extensions.snapshell.gschema.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ 'https://snap.unbl.ink/jsonrpc'
+ Snapcast JSON-RPC URL
+
+ HTTP(S) endpoint for Snapserver JSON-RPC.
+
+
+
+
+ 'a16be8ca-e36f-5498-08cd-3879f2025775'
+ Snapcast group id
+
+ UUID of the Snapcast group to control by default.
+
+
+
+
+ 5
+ Polling interval (seconds)
+
+ How often to refresh Server.GetStatus to update slider position.
+
+
+
+
+
diff --git a/gnome/.local/share/gnome-shell/extensions/vpn-toggler@rheddes.nl b/gnome/.local/share/gnome-shell/extensions/vpn-toggler@rheddes.nl
new file mode 120000
index 0000000..7ab241b
--- /dev/null
+++ b/gnome/.local/share/gnome-shell/extensions/vpn-toggler@rheddes.nl
@@ -0,0 +1 @@
+/home/powellc/src/github.com/Rheddes/vpn-toggler
\ No newline at end of file