import GLib from 'gi://GLib'; import Gio from 'gi://Gio'; import Service from "resource:///com/github/Aylur/ags/service.js"; const SIS = GLib.getenv('SWAYSOCK'); export const PAYLOAD_TYPE = { MESSAGE_RUN_COMMAND: 0, MESSAGE_GET_WORKSPACES: 1, MESSAGE_SUBSCRIBE: 2, MESSAGE_GET_OUTPUTS: 3, MESSAGE_GET_TREE: 4, MESSAGE_GET_MARKS: 5, MESSAGE_GET_BAR_CONFIG: 6, MESSAGE_GET_VERSION: 7, MESSAGE_GET_BINDING_NODES: 8, MESSAGE_GET_CONFIG: 9, MESSAGE_SEND_TICK: 10, MESSAGE_SYNC: 11, MESSAGE_GET_BINDING_STATE: 12, MESSAGE_GET_INPUTS: 100, MESSAGE_GET_SEATS: 101, EVENT_WORKSPACE: 0x80000000, EVENT_MODE: 0x80000002, EVENT_WINDOW: 0x80000003, EVENT_BARCONFIG_UPDATE: 0x80000004, EVENT_BINDING: 0x80000005, EVENT_SHUTDOWN: 0x80000006, EVENT_TICK: 0x80000007, EVENT_BAR_STATE_UPDATE: 0x80000014, EVENT_INPUT: 0x80000015, } const Client_Event = { change: undefined, container: undefined, } const Workspace_Event = { change: undefined, current: undefined, old: undefined, } const Geometry = { x: undefined, y: undefined, width: undefined, height: undefined, } //NOTE: not all properties are listed here export const Node = { id: undefined, name: undefined, type: undefined, border: undefined, current_border_width: undefined, layout: undefined, orientation: undefined, percent: undefined, rect: undefined, window_rect: undefined, deco_rect: undefined, geometry: undefined, urgent: undefined, sticky: undefined, marks: undefined, focused: undefined, active: undefined, focus: undefined, nodes: undefined, floating_nodes: undefined, representation: undefined, fullscreen_mode: undefined, app_id: undefined, pid: undefined, visible: undefined, shell: undefined, output: undefined, inhibit_idle: undefined, idle_inhibitors: { application: undefined, user: undefined, }, window: undefined, window_properties: { title: undefined, class: undefined, instance: undefined, window_role: undefined, window_type: undefined, transient_for: undefined, } } export class SwayActiveClient extends Service { static { Service.register(this, {}, { 'id': ['int'], 'name': ['string'], 'class': ['string'], }); } _id = 0; _name = ''; _class = ''; get id() { return this._id; } get name() { return this._name; } get class() { return this._class; } updateProperty(prop, value) { if (!['id', 'name', 'class'].includes(prop)) return; super.updateProperty(prop, value); this.emit('changed'); } } export class SwayActiveID extends Service { static { Service.register(this, {}, { 'id': ['int'], 'name': ['string'], }); } _id = 0; _name = ''; get id() { return this._id; } get name() { return this._name; } update(id, name) { super.updateProperty('id', id); super.updateProperty('name', name); this.emit('changed'); } } export class SwayActives extends Service { static { Service.register(this, {}, { 'client': ['jsobject'], 'monitor': ['jsobject'], 'workspace': ['jsobject'], }); } _client = new SwayActiveClient; _monitor = new SwayActiveID; _workspace = new SwayActiveID; constructor() { super(); (['client', 'workspace', 'monitor']).forEach(obj => { this[`_${obj}`].connect('changed', () => { this.notify(obj); this.emit('changed'); }); }); } get client() { return this._client; } get monitor() { return this._monitor; } get workspace() { return this._workspace; } } export class Sway extends Service { static { Service.register(this, {}, { 'active': ['jsobject'], 'monitors': ['jsobject'], 'workspaces': ['jsobject'], 'clients': ['jsobject'], }); } _decoder = new TextDecoder(); _encoder = new TextEncoder(); _socket; _active; _monitors; _workspaces; _clients; get active() { return this._active; } get monitors() { return Array.from(this._monitors.values()); } get workspaces() { return Array.from(this._workspaces.values()); } get clients() { return Array.from(this._clients.values()); } getMonitor(id) { return this._monitors.get(id); } getWorkspace(name) { return this._workspaces.get(name); } getClient(id) { return this._clients.get(id); } msg(payload) { this._send(PAYLOAD_TYPE.MESSAGE_RUN_COMMAND, payload); } constructor() { if (!SIS) console.error('Sway is not running'); super(); this._active = new SwayActives(); this._monitors = new Map(); this._workspaces = new Map(); this._clients = new Map(); this._socket = new Gio.SocketClient().connect(new Gio.UnixSocketAddress({ path: `${SIS}`, }), null); this._watchSocket(this._socket.get_input_stream()); this._send(PAYLOAD_TYPE.MESSAGE_GET_TREE, ''); this._send(PAYLOAD_TYPE.MESSAGE_SUBSCRIBE, JSON.stringify(['window', 'workspace'])); this._active.connect('changed', () => this.emit('changed')); ['monitor', 'workspace', 'client'].forEach(active => this._active.connect(`notify::${active}`, () => this.notify('active'))); } _send(payloadType, payload) { const pb = this._encoder.encode(payload); const type = new Uint32Array([payloadType]); const pl = new Uint32Array([pb.length]); const magic_string = this._encoder.encode('i3-ipc'); const data = new Uint8Array([ ...magic_string, ...(new Uint8Array(pl.buffer)), ...(new Uint8Array(type.buffer)), ...pb]); this._socket.get_output_stream().write(data, null); } _watchSocket(stream) { stream.read_bytes_async(14, GLib.PRIORITY_DEFAULT, null, (_, resultHeader) => { const data = stream.read_bytes_finish(resultHeader).get_data(); if (!data) return; const payloadLength = new Uint32Array(data.slice(6, 10).buffer)[0]; const payloadType = new Uint32Array(data.slice(10, 14).buffer)[0]; stream.read_bytes_async( payloadLength, GLib.PRIORITY_DEFAULT, null, (_, resultPayload) => { const data = stream.read_bytes_finish(resultPayload).get_data(); if (!data) return; this._onEvent(payloadType, JSON.parse(this._decoder.decode(data))); this._watchSocket(stream); }); }); } async _onEvent(event_type, event) { if (!event) return; try { switch (event_type) { case PAYLOAD_TYPE.EVENT_WORKSPACE: this._handleWorkspaceEvent(event); break; case PAYLOAD_TYPE.EVENT_WINDOW: this._handleWindowEvent(event); break; case PAYLOAD_TYPE.MESSAGE_GET_TREE: this._handleTreeMessage(event); break; default: break; } } catch (error) { logError(error); } this.emit('changed'); } _handleWorkspaceEvent(workspaceEvent) { const workspace = workspaceEvent.current; switch (workspaceEvent.change) { case 'init': this._workspaces.set(workspace.name, workspace); break; case 'empty': this._workspaces.delete(workspace.name); break; case 'focus': this._active.workspace.update(workspace.id, workspace.name); this._active.monitor.update(1, workspace.output); this._workspaces.set(workspace.name, workspace); this._workspaces.set(workspaceEvent.old.name, workspaceEvent.old); break; case 'rename': if (this._active.workspace.id === workspace.id) this._active.workspace.updateProperty('name', workspace.name); this._workspaces.set(workspace.name, workspace); break; case 'reload': break; case 'move': case 'urgent': default: this._workspaces.set(workspace.name, workspace); } this.notify('workspaces'); } _handleWindowEvent(clientEvent) { const client = clientEvent.container; const id = client.id; switch (clientEvent.change) { case 'new': case 'close': case 'floating': case 'move': // Refresh tree since client events don't contain the relevant information // to be able to modify `workspace.nodes` or `workspace.floating_nodes`. // There has to be a better way than this though :/ this._send(PAYLOAD_TYPE.MESSAGE_GET_TREE, ''); break; case 'focus': if (this._active.client.id === id) return; // eslint-disable-next-line no-case-declarations const current_active = this._clients.get(this._active.client.id); if (current_active) current_active.focused = false; this._active.client.updateProperty('id', id); this._active.client.updateProperty('name', client.name); this._active.client.updateProperty('class', client.shell === 'xwayland' ? client.window_properties?.class || '' : client.app_id, ); break; case 'title': if (client.focused) this._active.client.updateProperty('name', client.name); this._clients.set(id, client); this.notify('clients'); break; case 'fullscreen_mode': case 'urgent': case 'mark': default: this._clients.set(id, client); this.notify('clients'); } } _handleTreeMessage(node) { switch (node.type) { case 'root': this._workspaces.clear(); this._clients.clear(); this._monitors.clear(); node.nodes.map(n => this._handleTreeMessage(n)); break; case 'output': this._monitors.set(node.id, node); if (node.active) this._active.monitor.update(node.id, node.name); node.nodes.map(n => this._handleTreeMessage(n)); this.notify('monitors'); break; case 'workspace': this._workspaces.set(node.name, node); // I think I'm missing something. There has to be a better way. // eslint-disable-next-line no-case-declarations const hasFocusedChild = (n) => n.nodes.some(c => c.focused || hasFocusedChild(c)); if (node.focused || hasFocusedChild(node)) this._active.workspace.update(node.id, node.name); node.nodes.map(n => this._handleTreeMessage(n)); this.notify('workspaces'); break; case 'con': case 'floating_con': this._clients.set(node.id, node); if (node.focused) { this._active.client.updateProperty('id', node.id); this._active.client.updateProperty('name', node.name); this._active.client.updateProperty('class', node.shell === 'xwayland' ? node.window_properties?.class || '' : node.app_id, ); } node.nodes.map(n => this._handleTreeMessage(n)); this.notify('clients'); break; } } } export const sway = new Sway; export default sway;