aboutsummaryrefslogtreecommitdiff
path: root/.local/share
diff options
context:
space:
mode:
Diffstat (limited to '.local/share')
-rw-r--r--.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js274
-rw-r--r--.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/metadata.json10
2 files changed, 284 insertions, 0 deletions
diff --git a/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js
new file mode 100644
index 0000000..a61ef47
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js
@@ -0,0 +1,274 @@
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+
+import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+
+const EVALUATE_DELAY_MS = 150;
+const ROUTABLE_WINDOW_TYPES = new Set([
+ Meta.WindowType.NORMAL,
+ Meta.WindowType.DIALOG,
+ Meta.WindowType.MODAL_DIALOG,
+]);
+const WINDOW_RULES = [
+ {
+ name: 'main-frame',
+ workspace: 0,
+ monitor: 'primary',
+ matchAll: [
+ {field: 'appId', pattern: /emacs/i},
+ {field: 'title', pattern: /^main$/i},
+ ],
+ },
+ {
+ name: 'browsers',
+ workspace: 1,
+ monitor: 'primary',
+ matchAny: [
+ {field: 'appId', pattern: /(firefox|chromium|chrome)/i},
+ {field: 'wmClass', pattern: /(firefox|chromium|chrome)/i},
+ {field: 'wmClassInstance', pattern: /(firefox|chromium|chrome)/i},
+ ],
+ },
+ {
+ name: 'communications-frame',
+ workspace: 2,
+ monitor: 'primary',
+ matchAll: [
+ {field: 'appId', pattern: /emacs/i},
+ {field: 'title', pattern: /^communications$/i},
+ ],
+ },
+ {
+ name: 'communications-apps',
+ workspace: 2,
+ monitor: 'primary',
+ matchAny: [
+ {field: 'title', pattern: /(ebex|lack|communications|notmuch|outlook|elfeed)/i},
+ {field: 'appId', pattern: /(slack|outlook)/i},
+ {field: 'wmClass', pattern: /(slack|outlook)/i},
+ {field: 'wmClassInstance', pattern: /(slack|outlook)/i},
+ ],
+ },
+ {
+ name: 'terminals-frame',
+ workspace: 3,
+ monitor: 'primary',
+ matchAll: [
+ {field: 'appId', pattern: /emacs/i},
+ {field: 'title', pattern: /^terminals$/i},
+ ],
+ },
+ {
+ name: 'terminals-apps',
+ workspace: 3,
+ monitor: 'primary',
+ matchAny: [
+ {field: 'title', pattern: /(alacritty|kitty|terminal|teleport)/i},
+ {field: 'appId', pattern: /(alacritty|kitty|console|terminal|teleport)/i},
+ {field: 'wmClass', pattern: /(alacritty|kitty|terminal|teleport)/i},
+ {field: 'wmClassInstance', pattern: /(alacritty|kitty|terminal|teleport)/i},
+ ],
+ },
+ {
+ name: 'misc',
+ workspace: 4,
+ monitor: 'primary',
+ matchAny: [
+ {field: 'title', pattern: /(isco|eepa)/i},
+ ],
+ },
+ {
+ name: 'media',
+ workspace: 8,
+ monitor: 'primary',
+ matchAny: [
+ {field: 'title', pattern: /(youtube|spotify)/i},
+ {field: 'appId', pattern: /spotify/i},
+ {field: 'wmClass', pattern: /spotify/i},
+ {field: 'wmClassInstance', pattern: /spotify/i},
+ ],
+ },
+];
+
+function getWindowInfo(window, windowTracker) {
+ const app = windowTracker.get_window_app(window);
+
+ return {
+ title: window.get_title() ?? '',
+ wmClass: window.get_wm_class() ?? '',
+ wmClassInstance: window.get_wm_class_instance() ?? '',
+ appId: app?.get_id() ?? '',
+ appName: app?.get_name() ?? '',
+ role: window.get_role() ?? '',
+ };
+}
+
+function matchCondition(info, condition) {
+ const value = info[condition.field] ?? '';
+ return condition.pattern.test(value);
+}
+
+function ruleMatches(info, rule) {
+ const matchAll = rule.matchAll ?? [];
+ const matchAny = rule.matchAny ?? [];
+
+ if (!matchAll.every(condition => matchCondition(info, condition)))
+ return false;
+
+ if (matchAny.length === 0)
+ return true;
+
+ return matchAny.some(condition => matchCondition(info, condition));
+}
+
+function resolveMonitor(monitor) {
+ if (monitor === 'primary')
+ return Main.layoutManager.primaryIndex;
+
+ if (Number.isInteger(monitor))
+ return monitor;
+
+ return null;
+}
+
+export default class WorkspaceRouterExtension extends Extension {
+ enable() {
+ this._windowTracker = Shell.WindowTracker.get_default();
+ this._trackedWindows = new Set();
+ this._routedWindows = new Set();
+ this._pendingEvaluations = new Map();
+
+ global.display.connectObject(
+ 'window-created', (_display, window) => this._trackWindow(window),
+ this);
+
+ this._windowTracker.connectObject(
+ 'tracked-windows-changed', () => this._queueUnmatchedWindows(),
+ this);
+
+ for (const actor of global.get_window_actors())
+ this._trackWindow(actor.meta_window);
+ }
+
+ disable() {
+ global.display.disconnectObject(this);
+ this._windowTracker?.disconnectObject(this);
+
+ for (const [window, sourceId] of this._pendingEvaluations) {
+ GLib.source_remove(sourceId);
+ window.disconnectObject(this);
+ }
+
+ for (const window of this._trackedWindows)
+ window.disconnectObject(this);
+
+ this._pendingEvaluations.clear();
+ this._trackedWindows.clear();
+ this._routedWindows.clear();
+ this._windowTracker = null;
+ }
+
+ _trackWindow(window) {
+ if (!window || this._trackedWindows.has(window))
+ return;
+
+ if (!this._isRoutableWindow(window))
+ return;
+
+ this._trackedWindows.add(window);
+
+ window.connectObject(
+ 'notify::title', () => this._queueWindow(window),
+ 'notify::wm-class', () => this._queueWindow(window),
+ 'unmanaged', () => this._cleanupWindow(window),
+ this);
+
+ this._queueWindow(window);
+ }
+
+ _queueUnmatchedWindows() {
+ for (const window of this._trackedWindows)
+ this._queueWindow(window);
+ }
+
+ _queueWindow(window) {
+ if (!this._trackedWindows.has(window) || this._routedWindows.has(window))
+ return;
+
+ this._clearPendingEvaluation(window);
+
+ const sourceId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ EVALUATE_DELAY_MS,
+ () => {
+ this._pendingEvaluations.delete(window);
+ this._routeWindow(window);
+ return GLib.SOURCE_REMOVE;
+ });
+
+ this._pendingEvaluations.set(window, sourceId);
+ }
+
+ _routeWindow(window) {
+ if (!this._trackedWindows.has(window) || this._routedWindows.has(window))
+ return;
+
+ if (!this._isRoutableWindow(window))
+ return;
+
+ const info = getWindowInfo(window, this._windowTracker);
+ const rule = WINDOW_RULES.find(candidate => ruleMatches(info, candidate));
+
+ if (!rule)
+ return;
+
+ this._moveWindow(window, rule);
+ this._routedWindows.add(window);
+ }
+
+ _moveWindow(window, rule) {
+ this._ensureWorkspace(rule.workspace);
+
+ const monitorIndex = resolveMonitor(rule.monitor);
+ if (monitorIndex !== null && monitorIndex !== window.get_monitor())
+ window.move_to_monitor(monitorIndex);
+
+ const workspace = window.get_workspace();
+ if (!workspace || workspace.index() !== rule.workspace)
+ window.change_workspace_by_index(rule.workspace, false);
+ }
+
+ _ensureWorkspace(index) {
+ const workspaceManager = global.workspace_manager;
+
+ while (workspaceManager.n_workspaces <= index)
+ workspaceManager.append_new_workspace(false, global.get_current_time());
+ }
+
+ _isRoutableWindow(window) {
+ if (window.skip_taskbar || window.is_override_redirect())
+ return false;
+
+ if (window.is_on_all_workspaces())
+ return false;
+
+ return ROUTABLE_WINDOW_TYPES.has(window.get_window_type());
+ }
+
+ _clearPendingEvaluation(window) {
+ const sourceId = this._pendingEvaluations.get(window);
+ if (sourceId) {
+ GLib.source_remove(sourceId);
+ this._pendingEvaluations.delete(window);
+ }
+ }
+
+ _cleanupWindow(window) {
+ this._clearPendingEvaluation(window);
+ this._trackedWindows.delete(window);
+ this._routedWindows.delete(window);
+ window.disconnectObject(this);
+ }
+}
diff --git a/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/metadata.json b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/metadata.json
new file mode 100644
index 0000000..6fad314
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/metadata.json
@@ -0,0 +1,10 @@
+{
+ "uuid": "workspace-router@rbenencia.name",
+ "extension-id": "workspace-router",
+ "name": "Workspace Router",
+ "description": "Route newly created windows to fixed workspaces based on title, app ID, or WM class.",
+ "shell-version": ["49"],
+ "url": "https://rbenencia.name",
+ "session-modes": ["user"],
+ "version": 1
+}
nihil fit ex nihilo