aboutsummaryrefslogtreecommitdiff
path: root/.local/share/gnome-shell
diff options
context:
space:
mode:
Diffstat (limited to '.local/share/gnome-shell')
-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
-rw-r--r--.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js25
3 files changed, 404 insertions, 6 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
new file mode 100644
index 0000000..8a80b8b
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/extension.js
@@ -0,0 +1,375 @@
+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
new file mode 100644
index 0000000..c1b99b5
--- /dev/null
+++ b/.local/share/gnome-shell/extensions/workspace-router-cli@rbenencia.name/metadata.json
@@ -0,0 +1,10 @@
+{
+ "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
+}
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
index 74f6cdf..a59c491 100644
--- a/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js
+++ b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js
@@ -45,10 +45,10 @@ const WINDOW_RULES = [
workspace: 2,
monitor: 'primary',
matchAny: [
- {field: 'title', pattern: /(ebex|lack|communications|notmuch|outlook|elfeed|thunderbird)/i},
- {field: 'appId', pattern: /(slack|outlook|thunderbird)/i},
- {field: 'wmClass', pattern: /(slack|outlook|thunderbird)/i},
- {field: 'wmClassInstance', pattern: /(slack|outlook|thunderbird)/i},
+ {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},
],
},
{
@@ -87,7 +87,10 @@ const WINDOW_RULES = [
workspace: 4,
monitor: 'primary',
matchAny: [
- {field: 'title', pattern: /(isco|eepa)/i},
+ {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},
],
},
{
@@ -144,6 +147,16 @@ function resolveMonitor(monitor) {
return 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 WorkspaceRouterExtension extends Extension {
enable() {
this._windowTracker = Shell.WindowTracker.get_default();
@@ -259,7 +272,7 @@ export default class WorkspaceRouterExtension extends Extension {
}
_isRoutableWindow(window) {
- if (window.skip_taskbar || window.is_override_redirect())
+ if (isSkipTaskbar(window) || window.is_override_redirect())
return false;
if (window.is_on_all_workspaces())
nihil fit ex nihilo