diff options
Diffstat (limited to '.local/share')
| -rw-r--r-- | .local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js | 274 | ||||
| -rw-r--r-- | .local/share/gnome-shell/extensions/workspace-router@rbenencia.name/metadata.json | 10 |
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 +} |
