400 lines
12 KiB
JavaScript

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;