aboutsummaryrefslogtreecommitdiff
path: root/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name
diff options
context:
space:
mode:
authorRaul Benencia <raul@thousandeyes.com>2026-08-12 13:32:16 -0700
committerRaul Benencia <raul@thousandeyes.com>2026-08-12 13:32:16 -0700
commit1b856a6b31b8f8369256612cef6cc242486534a2 (patch)
treea3ebedc60b1f9ebc2c2c76175c601b3542644f2a /.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name
parent2543fba39dbd61d8af14b7d5e3ef3d1e08ba1012 (diff)
gnome: consolidate workspace-router extension
Diffstat (limited to '.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name')
-rw-r--r--.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/extension.js375
-rw-r--r--.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/metadata.json10
2 files changed, 0 insertions, 385 deletions
diff --git a/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/extension.js b/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/extension.js
deleted file mode 100644
index 8a80b8b..0000000
--- a/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/extension.js
+++ /dev/null
@@ -1,375 +0,0 @@
-import Gio from 'gi://Gio';
-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 BUS_NAME = 'name.rbenencia.WorkspaceRouter';
-const OBJECT_PATH = '/name/rbenencia/WorkspaceRouter';
-const INTERFACE_NAME = 'name.rbenencia.WorkspaceRouter';
-
-const DBUS_XML = `
-<node>
- <interface name="${INTERFACE_NAME}">
- <method name="RouteWindows">
- <arg type="s" name="result" direction="out"/>
- </method>
- <method name="ListWindows">
- <arg type="s" name="result" direction="out"/>
- </method>
- <method name="ActivateWindow">
- <arg type="u" name="windowId" direction="in"/>
- <arg type="s" name="result" direction="out"/>
- </method>
- </interface>
-</node>`;
-
-const ROUTABLE_WINDOW_TYPES = new Set([
- Meta.WindowType.NORMAL,
- Meta.WindowType.DIALOG,
- Meta.WindowType.MODAL_DIALOG,
-]);
-
-const WINDOW_RULES = [
- {
- name: 'misc',
- workspace: 4,
- matchAny: [
- {field: 'title', pattern: /(cisco secure client|secure client|anyconnect|vpnui|keepass)/i},
- {field: 'appId', pattern: /(cisco|secureclient|anyconnect|vpnui|keepass)/i},
- {field: 'wmClass', pattern: /(cisco|secureclient|anyconnect|vpnui|keepass)/i},
- {field: 'wmClassInstance', pattern: /(cisco|secureclient|anyconnect|vpnui|keepass)/i},
- ],
- },
- {
- name: 'main-frame',
- workspace: 0,
- matchAll: [
- {field: 'appId', pattern: /emacs/i},
- {field: 'title', pattern: /^main$/i},
- ],
- },
- {
- name: 'browsers',
- workspace: 1,
- 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,
- matchAll: [
- {field: 'appId', pattern: /emacs/i},
- {field: 'title', pattern: /^communications$/i},
- ],
- },
- {
- name: 'communications-apps',
- workspace: 2,
- matchAny: [
- {field: 'title', pattern: /(webex|slack|communications|notmuch|outlook|elfeed|thunderbird)/i},
- {field: 'appId', pattern: /(webex|slack|outlook|thunderbird)/i},
- {field: 'wmClass', pattern: /(webex|slack|outlook|thunderbird)/i},
- {field: 'wmClassInstance', pattern: /(webex|slack|outlook|thunderbird)/i},
- ],
- },
- {
- name: 'terminals-frame',
- workspace: 3,
- matchAll: [
- {field: 'appId', pattern: /emacs/i},
- {field: 'title', pattern: /^terminals$/i},
- ],
- },
- {
- name: 'terminals-apps',
- workspace: 3,
- matchAny: [
- {field: 'title', pattern: /(alacritty|kitty|terminal)/i},
- {field: 'appId', pattern: /(alacritty|kitty|console|terminal)/i},
- {field: 'wmClass', pattern: /(alacritty|kitty|terminal)/i},
- {field: 'wmClassInstance', pattern: /(alacritty|kitty|terminal)/i},
- ],
- },
- {
- name: 'teleport',
- workspace: 5,
- matchAny: [
- {field: 'title', pattern: /teleport/i},
- {field: 'appId', pattern: /teleport/i},
- {field: 'wmClass', pattern: /teleport/i},
- {field: 'wmClassInstance', pattern: /teleport/i},
- ],
- },
- {
- name: 'media',
- workspace: 8,
- 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() ?? '',
- monitor: window.get_monitor(),
- workspace: window.get_workspace()?.index() ?? null,
- };
-}
-
-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 windowInfoForResult(window, info, rule) {
- return {
- id: window.get_stable_sequence(),
- title: info.title,
- appId: info.appId,
- appName: info.appName,
- wmClass: info.wmClass,
- wmClassInstance: info.wmClassInstance,
- role: info.role,
- monitor: info.monitor,
- workspace: info.workspace,
- route: rule?.name ?? null,
- targetWorkspace: rule?.workspace ?? null,
- finalMonitor: window.get_monitor(),
- finalWorkspace: window.get_workspace()?.index() ?? null,
- };
-}
-
-function isSkipTaskbar(window) {
- if (typeof window.is_skip_taskbar === 'function')
- return window.is_skip_taskbar();
-
- if (typeof window.skip_taskbar === 'function')
- return window.skip_taskbar();
-
- return Boolean(window.skip_taskbar);
-}
-
-export default class WorkspaceRouterCliExtension extends Extension {
- enable() {
- this._windowTracker = Shell.WindowTracker.get_default();
- this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(DBUS_XML, this);
- this._dbusImpl.export(Gio.DBus.session, OBJECT_PATH);
- this._ownName = Gio.DBus.session.own_name(
- BUS_NAME,
- Gio.BusNameOwnerFlags.REPLACE,
- () => {},
- () => log(`Workspace Router CLI lost D-Bus name ${BUS_NAME}`));
- }
-
- disable() {
- if (this._ownName) {
- Gio.DBus.session.unown_name(this._ownName);
- this._ownName = null;
- }
-
- if (this._dbusImpl) {
- this._dbusImpl.unexport();
- this._dbusImpl.run_dispose();
- this._dbusImpl = null;
- }
-
- this._windowTracker = null;
- }
-
- RouteWindowsAsync(_params, invocation) {
- try {
- invocation.return_value(GLib.Variant.new('(s)', [this._routeWindows()]));
- } catch (e) {
- logError(e, 'Workspace Router CLI failed to route windows');
- invocation.return_dbus_error(`${INTERFACE_NAME}.Error`, e.message);
- }
- }
-
- ListWindowsAsync(_params, invocation) {
- try {
- invocation.return_value(GLib.Variant.new('(s)', [this._listWindows()]));
- } catch (e) {
- logError(e, 'Workspace Router CLI failed to list windows');
- invocation.return_dbus_error(`${INTERFACE_NAME}.Error`, e.message);
- }
- }
-
- ActivateWindowAsync(params, invocation) {
- try {
- const [windowId] = params.deepUnpack();
- invocation.return_value(GLib.Variant.new('(s)', [this._activateWindow(windowId)]));
- } catch (e) {
- logError(e, 'Workspace Router CLI failed to activate window');
- invocation.return_dbus_error(`${INTERFACE_NAME}.Error`, e.message);
- }
- }
-
- _routeWindows() {
- const result = {
- primaryMonitor: Main.layoutManager.primaryIndex,
- movedToPrimary: 0,
- routed: 0,
- skipped: 0,
- windows: [],
- };
-
- for (const window of this._collectWindows()) {
- const routeResult = this._routeWindow(window);
-
- if (!routeResult) {
- result.skipped++;
- continue;
- }
-
- if (routeResult.movedToPrimary)
- result.movedToPrimary++;
-
- if (routeResult.routed)
- result.routed++;
- else
- result.skipped++;
-
- result.windows.push(routeResult.window);
- }
-
- return JSON.stringify(result);
- }
-
- _listWindows() {
- const windows = this._collectWindows().map(window => {
- const info = getWindowInfo(window, this._windowTracker);
- const rule = WINDOW_RULES.find(candidate => ruleMatches(info, candidate));
-
- return windowInfoForResult(window, info, rule);
- });
-
- return JSON.stringify(windows);
- }
-
- _activateWindow(windowId) {
- const window = this._collectWindows().find(candidate => candidate.get_stable_sequence() === windowId);
-
- if (!window)
- throw new Error(`Window not found: ${windowId}`);
-
- Main.activateWindow(window);
- return JSON.stringify({activated: true, id: windowId});
- }
-
- _collectWindows() {
- const windows = [];
- const seen = new Set();
-
- for (const actor of global.get_window_actors()) {
- const window = actor.meta_window;
-
- if (!window || seen.has(window) || !this._isRoutableWindow(window))
- continue;
-
- seen.add(window);
- windows.push(window);
- }
-
- return windows;
- }
-
- _routeWindow(window) {
- if (!this._isRoutableWindow(window))
- return null;
-
- const info = getWindowInfo(window, this._windowTracker);
- const rule = WINDOW_RULES.find(candidate => ruleMatches(info, candidate));
- const movedToPrimary = this._moveWindowToMonitor(window, Main.layoutManager.primaryIndex);
-
- if (rule)
- this._moveWindowToWorkspace(window, rule.workspace);
-
- return {
- movedToPrimary,
- routed: Boolean(rule),
- window: windowInfoForResult(window, info, rule),
- };
- }
-
- _moveWindowToMonitor(window, monitorIndex) {
- if (monitorIndex === null || monitorIndex === undefined)
- return false;
-
- if (monitorIndex === window.get_monitor())
- return false;
-
- const wasFullscreen = window.is_fullscreen();
- const maximized = window.get_maximized();
-
- if (wasFullscreen)
- window.unmake_fullscreen();
-
- if (maximized)
- window.unmaximize(maximized);
-
- window.move_to_monitor(monitorIndex);
-
- if (maximized)
- window.maximize(maximized);
-
- if (wasFullscreen)
- window.make_fullscreen();
-
- return true;
- }
-
- _moveWindowToWorkspace(window, workspaceIndex) {
- this._ensureWorkspace(workspaceIndex);
-
- const workspace = window.get_workspace();
- if (!workspace || workspace.index() !== workspaceIndex)
- window.change_workspace_by_index(workspaceIndex, 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 (isSkipTaskbar(window) || window.is_override_redirect())
- return false;
-
- if (window.is_on_all_workspaces())
- return false;
-
- return ROUTABLE_WINDOW_TYPES.has(window.get_window_type());
- }
-}
diff --git a/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/metadata.json b/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/metadata.json
deleted file mode 100644
index c1b99b5..0000000
--- a/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/metadata.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "uuid": "workspace-router-cli@rbenencia.name",
- "extension-id": "workspace-router-cli",
- "name": "Workspace Router CLI",
- "description": "Expose Raul's GNOME window routing workflow over D-Bus.",
- "shell-version": ["46", "47", "48", "49"],
- "url": "https://rbenencia.name",
- "session-modes": ["user"],
- "version": 1
-}
nihil fit ex nihilo