Files
BB 94710cf643 feat: support for Aqueous Compositor (#3357)
* updated dank to support aqueous

* further updates

* aqueous support work

* updated to support workspace options better in aqueous

* added blur to dms shell for aqueous

* added keybinding update

* remove unneeded blur

* clean up per claud review

* swapped to ipc

* aqueous: move IPC integration to QML, delete test fixtures (since we
dont have a QML test structure even setup), delete in-repo docs

* fix staticcheck

* aqueous: consolidate more aqueous specific stuff

* add aqueous logo

---------

Co-authored-by: Zoey <[email protected]>
(cherry picked from commit b9365610c8)
2026-09-08 09:23:54 -04:00

971 lines
37 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "../Common/AqueousIpc.js" as Ipc
Singleton {
id: root
readonly property var log: Log.scoped("AqueousService")
property var state: ({})
property string seatName: ""
readonly property bool enabled: CompositorService.isAqueous
readonly property bool available: enabled && requestConnection.ready && eventConnection.ready && state.available === true
property var discoveredCapabilities: ({})
property var pendingProcesses: []
property int lifecycle: 0
readonly property string socketPath: Quickshell.env("AQUEOUS_SOCKET")
readonly property string runtimePath: Quickshell.env("XDG_RUNTIME_DIR")
readonly property bool validSocketPath: Ipc.socketPath(socketPath, runtimePath)
property bool watching: false
property bool recovering: false
property bool reconnecting: false
property int reconnectAttempt: 0
property var commandQueue: []
property var currentCommand: null
readonly property bool commandPending: currentCommand !== null
readonly property int maxBatchBytes: 4 * 1024 * 1024
readonly property int maxModelBytes: 2 * 1024 * 1024
readonly property var capabilities: discoveredCapabilities
readonly property string session: available ? state.batch.session : ""
readonly property string sequence: available ? state.batch.sequence : ""
readonly property var entities: available ? state.batch.upsert : []
readonly property var outputs: entities.filter(e => e.kind === "output")
readonly property var workspaces: entities.filter(e => e.kind === "workspace").map(w => Object.assign({}, w, {
aqueousSession: session
}))
readonly property var windows: entities.filter(e => e.kind === "window")
readonly property var seats: entities.filter(e => e.kind === "seat")
readonly property var sessionState: entities.find(e => e.kind === "session") || ({})
readonly property bool locked: sessionState.locked === true
readonly property var seat: seatName ? seats.find(s => s.id === seatName) : (seats.length === 1 ? seats[0] : null)
readonly property var keyboard: entities.find(e => e.kind === "keyboard" && e.id === seat?.keyboard)
readonly property var keyboardLayouts: keyboard?.layouts || []
readonly property string keyboardLayout: keyboardLayouts[keyboard?.index] || ""
readonly property string focusedOutput: outputs.find(o => o.id === seat?.output)?.name || ""
readonly property bool inOverview: !!sessionState.overview_output
readonly property var focusedWindow: {
const window = windows.find(w => w.id === seat?.window);
return window ? windowFacade(window) : null;
}
readonly property var toplevels: windows.filter(w => taskbarEligible(w)).map(w => windowFacade(w))
function taskbarEligible(window) {
return !window.skip_taskbar && (window.visible || window.minimized || !workspaces.find(ws => ws.id === window.workspace)?.active);
}
function outputId(name) {
return outputs.find(o => o.name === name)?.id || "";
}
function workspacesForOutput(name) {
const id = outputId(name || focusedOutput);
return workspaces.filter(w => w.output === id).sort((a, b) => a.number - b.number);
}
function parseJson(text) {
if (Ipc.byteLength(text) > maxBatchBytes + (text.endsWith("\n") ? 1 : 0))
throw new Error("JSON exceeds 4 MiB");
return JSON.parse(text);
}
function validateEntity(entity) {
const fields = {
output: "id:s name:s bounds:r usable_bounds:r scale:n transform:s active_workspace:? enabled:b powered:b",
workspace: "id:s output:s name:s number:i active:b urgent:b",
window: "id:s backend:s app_id:? class:? title:? workspace:? output:? geometry:r outer_geometry:r layout:s focused:b visible:b floating:b minimized:b maximized:b fullscreen:b skip_taskbar:b skip_switcher:b always_above:b always_below:b snapped:b fixed_position:b can_minimize:b can_maximize:b can_activate:b",
seat: "id:s output:? window:? focus_kind:s keyboard:?",
keyboard: "id:s seat:s layouts:a index:i",
keyboard_device: "id:s name:? seat:s group:? virtual:b",
session: "id:s locked:b default_seat:? overview_output:? overview_window:?"
};
if (!entity || !Object.prototype.hasOwnProperty.call(fields, entity.kind) || typeof entity.id !== "string" || !entity.id)
throw new Error("invalid entity kind or ID");
for (const field of fields[entity.kind].split(" ")) {
const [name, type] = field.split(":");
const value = entity[name];
let valid = false;
switch (type) {
case "s":
valid = typeof value === "string";
break;
case "?":
valid = value === null || typeof value === "string";
break;
case "b":
valid = typeof value === "boolean";
break;
case "i":
valid = Number.isSafeInteger(value);
break;
case "n":
valid = typeof value === "number" && Number.isFinite(value);
break;
case "a":
valid = Array.isArray(value) && value.every(v => typeof v === "string");
break;
case "r":
valid = value && ["x", "y", "width", "height"].every(k => Number.isSafeInteger(value[k]));
break;
}
if (!valid)
throw new Error("invalid " + entity.kind + "." + name);
}
if ((entity.kind === "session" && entity.id !== "session") || (entity.kind === "workspace" && entity.number < 1) || (entity.kind === "output" && entity.scale <= 0) || (entity.kind === "keyboard" && (entity.index < 0 || (entity.layouts.length && entity.index >= entity.layouts.length))) || (entity.kind === "window" && !["xdg", "xwayland"].includes(entity.backend)) || (entity.kind === "seat" && !["window", "shell_surface", "layer_surface", "override_redirect", "lock_surface", "none"].includes(entity.focus_kind)))
throw new Error("invalid " + entity.kind);
}
function reduceBatch(previous, batch) {
if (!batch || batch.schema !== 1 || typeof batch.session !== "string" || !/^[0-9a-f]{32}$/.test(batch.session) || typeof batch.sequence !== "string" || !/^[0-9]+$/.test(batch.sequence) || !Array.isArray(batch.upsert) || !Array.isArray(batch.removed))
throw new Error("invalid shell batch envelope");
if (batch.type !== "snapshot" && batch.type !== "delta")
throw new Error("invalid shell batch type");
if (batch.type === "snapshot" && (batch.base_sequence !== null || batch.removed.length))
throw new Error("invalid snapshot baseline");
if (batch.type === "delta" && (!previous || batch.session !== previous.session || batch.base_sequence !== previous.sequence || batch.sequence === previous.sequence))
throw new Error("shell delta continuity mismatch");
const model = Object.assign({}, batch.type === "delta" ? previous.model : {});
const entityBytes = Object.assign({}, batch.type === "delta" ? previous.entityBytes : {});
let modelBytes = batch.type === "delta" ? previous.modelBytes : 1;
const seen = new Set();
for (const entity of batch.upsert) {
validateEntity(entity);
const key = entity.kind + ":" + entity.id;
if (seen.has(key))
throw new Error("duplicate entity key");
seen.add(key);
const size = Ipc.byteLength(JSON.stringify(key) + ":" + JSON.stringify(entity)) + 1;
modelBytes += size - (entityBytes[key] || 0);
entityBytes[key] = size;
model[key] = entity;
}
for (const key of batch.removed) {
if (typeof key !== "string" || !/^(output|workspace|window|seat|keyboard|keyboard_device|session):.+$/.test(key) || seen.has(key))
throw new Error("invalid or duplicate removal");
seen.add(key);
modelBytes -= entityBytes[key] || 0;
delete entityBytes[key];
delete model[key];
}
if (!model["session:session"])
throw new Error("missing session entity");
const references = {
output: {
active_workspace: "workspace"
},
workspace: {
output: "output"
},
window: {
output: "output",
workspace: "workspace"
},
seat: {
output: "output",
window: "window",
keyboard: "keyboard"
},
keyboard: {
seat: "seat"
},
keyboard_device: {
seat: "seat",
group: "keyboard"
},
session: {
default_seat: "seat",
overview_output: "output",
overview_window: "window"
}
};
for (const entity of Object.values(model)) {
for (const [field, kind] of Object.entries(references[entity.kind])) {
if (entity[field] !== null && !model[kind + ":" + entity[field]])
throw new Error("dangling " + entity.kind + "." + field);
}
}
if (modelBytes > maxModelBytes)
throw new Error("shell model exceeds size bound");
return {
session: batch.session,
sequence: batch.sequence,
model: model,
entityBytes: entityBytes,
modelBytes: modelBytes,
upsert: Object.values(model)
};
}
function acceptBatch(batch) {
if (batch.session !== capabilities.session)
throw new Error("shell session changed after discovery");
const next = reduceBatch(state.available ? state.batch : null, batch);
reconnectAttempt = 0;
state = {
available: true,
batch: next
};
}
function commandPayload(action, fields) {
const request = Object.assign({
session: session,
seat: seat?.id || ""
}, fields || {});
if (!available || request.session !== session)
throw new Error("unavailable: stale session");
if (locked)
throw new Error("locked");
const [kind, operation] = action.split(".");
const capability = kind === "keyboard" ? "keyboard" : kind === "overview" ? "overview" : "commands";
if (!capabilities[capability])
throw new Error("unsupported: " + capability);
for (const key of ["id", "seat", "group", "output", "workspace", "name"]) {
if (request[key] !== undefined && (typeof request[key] !== "string" || Ipc.byteLength(request[key]) > 1024))
throw new Error("invalid: command string");
}
const target = entities.find(e => e.kind === kind && e.id === request.id);
if ((kind === "window" || kind === "workspace") && !target)
throw new Error("not_found");
const selectedSeat = request.seat ? seats.find(s => s.id === request.seat) : (seats.length === 1 ? seats[0] : null);
const output = outputs.find(o => o.id === request.output);
let payload;
switch (action) {
case "window.activate":
case "workspace.activate":
if (!selectedSeat)
throw new Error(request.seat ? "not_found: seat" : "ambiguous_seat");
if (kind === "window" && !target.can_activate)
throw new Error("unavailable: window activation");
payload = {
id: target.id,
seat: selectedSeat.id
};
break;
case "window.close":
payload = {
id: target.id
};
break;
case "window.minimized":
case "window.maximized":
case "window.fullscreen":
if (typeof request.value !== "boolean")
throw new Error("invalid: missing state");
if (operation !== "fullscreen" && !target["can_" + operation.slice(0, -1)])
throw new Error("unavailable: window state");
payload = {
id: target.id,
value: request.value
};
break;
case "window.move":
if (!!request.workspace === !!request.output)
throw new Error("invalid: move destination");
if (request.workspace && !workspaces.some(w => w.id === request.workspace))
throw new Error("not_found: workspace");
if (request.output && !output)
throw new Error("not_found: output");
payload = {
id: target.id
};
payload[request.workspace ? "workspace" : "output"] = request.workspace || output.id;
break;
case "workspace.rename":
if (typeof request.name !== "string" || /[\r\n]/.test(request.name))
throw new Error("invalid: workspace name");
payload = {
id: target.id,
name: request.name
};
break;
case "keyboard.set":
case "keyboard.next":
{
if (!selectedSeat)
throw new Error(request.seat ? "not_found: seat" : "ambiguous_seat");
const group = entities.find(e => e.kind === "keyboard" && e.id === (request.group || selectedSeat.keyboard) && e.seat === selectedSeat.id);
if (!group)
throw new Error("not_found: keyboard group");
payload = {
seat: selectedSeat.id,
group: group.id
};
if (operation === "set") {
if (!Number.isInteger(request.index) || request.index < 0 || request.index >= group.layouts.length)
throw new Error("invalid: keyboard index");
payload.index = request.index;
}
break;
}
case "overview.show":
case "overview.toggle":
case "overview.hide":
payload = {};
if (operation !== "hide") {
if (!output)
throw new Error("not_found: output");
payload.output = output.id;
}
break;
case "session.exit":
payload = {};
break;
default:
throw new Error("unsupported: action");
}
return {
action: action,
fields: payload
};
}
function commandResult(action, result, error) {
if (error)
return error;
const status = action === "window.close" || action === "session.exit" ? "accepted" : "applied";
if (result?.status !== status || (status === "applied" || result.sequence !== undefined) && (typeof result.sequence !== "string" || !/^[0-9]{1,20}$/.test(result.sequence)))
return result?.status || "missing command acknowledgement";
return "";
}
function reportCommand(action, result, error, callback) {
const failure = commandResult(action, result, error);
if (failure) {
log.warn("command failed:", action, failure);
ToastService.showError(I18n.tr("Error"), errorMessage(failure), failure);
}
if (callback)
callback(!failure, failure || result.status);
}
function errorMessage(error) {
const code = String(error).replace(/^Error: /, "").split(":")[0];
switch (code) {
case "busy":
return I18n.tr("Another Aqueous operation is in progress.", "Aqueous request rejected because another operation is running");
case "locked":
case "unavailable":
case "not_found":
return I18n.tr("Unavailable");
case "unsupported":
return I18n.tr("This operation is not supported by the installed Aqueous version.", "Aqueous compositor or configuration helper lacks a required capability");
case "conflict":
case "external_change":
case "stale_session":
return I18n.tr("Configuration changed. Refresh to continue.", "Aqueous configuration or compositor session changed while editing");
case "uncertain":
case "command completion uncertain":
return I18n.tr("The result is unknown. Refresh before trying again.", "An Aqueous operation may have completed without a reply");
default:
return I18n.tr("The Aqueous operation failed.", "Fallback error for an Aqueous compositor or configuration request");
}
}
function command(action, fields, callback) {
try {
commandPayload(action, fields);
if (commandQueue.length >= 32)
throw new Error("busy: command queue full");
} catch (e) {
reportCommand(action, null, String(e), callback);
return;
}
commandQueue = commandQueue.concat([
{
action: action,
fields: Object.assign({
session: session,
seat: seat?.id || ""
}, fields || {}),
callback: callback,
generation: lifecycle,
queuedAt: Date.now()
}
]);
drainCommands();
}
function drainCommands() {
if (currentCommand || recovering)
return;
while (commandQueue.length) {
const next = commandQueue[0];
commandQueue = commandQueue.slice(1);
try {
if (next.generation !== lifecycle || Date.now() - next.queuedAt >= 5000)
throw new Error("unavailable: queued command expired before sending");
const payload = commandPayload(next.action, next.fields);
currentCommand = next;
requestConnection.request("command", payload);
return;
} catch (e) {
currentCommand = null;
reportCommand(next.action, null, String(e), next.callback);
}
}
}
function completeCommand(result, error) {
const current = currentCommand;
currentCommand = null;
if (!current || current.generation !== lifecycle)
return;
if (!error && commandResult(current.action, result, "")) {
reportCommand(current.action, null, "command completion uncertain: invalid acknowledgement", current.callback);
disconnected("invalid command acknowledgement");
return;
}
reportCommand(current.action, result, error, current.callback);
if (error.startsWith("stale_session:")) {
disconnected(error);
return;
}
drainCommands();
}
function failCommands(error) {
const current = currentCommand;
const queued = commandQueue;
currentCommand = null;
commandQueue = [];
if (current)
reportCommand(current.action, null, "command completion uncertain: " + error, current.callback);
for (const command of queued)
reportCommand(command.action, null, "unavailable: command not sent: " + error, command.callback);
}
function runJson(args, input, callback) {
if (!enabled) {
callback(null, "unavailable");
return;
}
if (input !== null && Ipc.byteLength(input) > maxBatchBytes) {
callback(null, "request exceeds 4 MiB");
return;
}
const process = jsonProcessComponent.createObject(root, {
command: args,
input: input,
callback: callback
});
pendingProcesses = pendingProcesses.concat([process]);
process.running = true;
process.deadline.start();
}
function startWatching() {
if (!enabled)
return;
if (!validSocketPath) {
state = {
error: "unavailable: missing or invalid AQUEOUS_SOCKET"
};
return;
}
state = {
error: "unavailable: connecting to Aqueous IPC"
};
watching = true;
}
function handshakesReady() {
if (!requestConnection.ready || !eventConnection.ready)
return;
const first = requestConnection.handshake;
const second = eventConnection.handshake;
if (first.session !== second.session || Object.keys(first.capabilities).some(key => first.capabilities[key] !== second.capabilities[key]) || first.schema !== second.schema) {
disconnected("IPC connection sessions or capabilities disagree");
return;
}
discoveredCapabilities = Object.assign({}, first.capabilities, {
session: first.session,
schema: first.schema,
max_batch_bytes: Math.min(first.max_batch_bytes, second.max_batch_bytes)
});
try {
eventConnection.subscribe();
} catch (e) {
disconnected(String(e));
}
}
function disconnected(error) {
if (recovering || !watching)
return;
recovering = true;
lifecycle++;
state = {
error: error
};
discoveredCapabilities = ({});
reconnecting = true;
requestConnection.clear();
eventConnection.clear();
const delay = Math.min(400 * Math.pow(2, Math.min(reconnectAttempt++, 6)), 15000);
reconnectTimer.interval = delay + Math.floor(Math.random() * delay / 4);
reconnectTimer.restart();
failCommands(error);
recovering = false;
}
function stopWatching() {
recovering = true;
lifecycle++;
watching = false;
reconnectTimer.stop();
reconnecting = false;
reconnectAttempt = 0;
requestConnection.clear();
eventConnection.clear();
state = ({});
discoveredCapabilities = ({});
failCommands("backend changed or shell stopped");
for (const process of pendingProcesses.slice())
process.finish(null, "unavailable: backend changed or shell stopped");
recovering = false;
}
onEnabledChanged: {
stopWatching();
startWatching();
}
Component.onCompleted: startWatching()
Component.onDestruction: stopWatching()
IpcConnection {
id: requestConnection
path: root.socketPath
connected: root.watching && !root.reconnecting
onHelloReceived: root.handshakesReady()
onReply: (result, error) => root.completeCommand(result, error)
onFailed: error => root.disconnected(error)
}
IpcConnection {
id: eventConnection
path: root.socketPath
connected: root.watching && !root.reconnecting
events: true
onHelloReceived: root.handshakesReady()
onBatchReceived: batch => {
try {
root.acceptBatch(batch);
} catch (e) {
root.disconnected(String(e));
}
}
onFailed: error => root.disconnected(error)
}
Timer {
id: reconnectTimer
onTriggered: root.reconnecting = false
}
component IpcConnection: DankSocket {
id: connection
property bool events: false
property var handshake: null
property var pending: null
property string lastId: "0"
property bool subscribed: false
property bool installed: false
property string lastDelivery: ""
property var frameParser: null
readonly property bool ready: linkUp && handshake !== null
signal helloReceived
signal batchReceived(var batch)
signal reply(var result, string error)
signal failed(string error)
function clear() {
const previousParser = frameParser;
frameParser = null;
if (previousParser)
previousParser.destroy();
deadline.stop();
initialDeadline.stop();
pending = null;
handshake = null;
subscribed = false;
installed = false;
lastId = "0";
lastDelivery = "";
}
function fail(error) {
if (connected && !root.recovering)
failed(error);
}
function request(op, params) {
if (!linkUp || pending || (op !== "hello" && !handshake))
throw new Error("unavailable: IPC connection");
const id = Ipc.nextId(lastId);
const message = {
ipc: 1,
id: id,
op: op,
params: params
};
if (op !== "hello")
message.session = handshake.session;
const text = JSON.stringify(message);
if (Ipc.byteLength(text) > (handshake?.max_request_bytes || 65536))
throw new Error("invalid: IPC request exceeds size bound");
lastId = id;
pending = {
id: id,
op: op,
params: params
};
deadline.restart();
send(text);
}
function receive(line) {
try {
const value = Ipc.envelope(line, handshake?.max_frame_bytes || 4259840);
if (value.event !== undefined) {
if (!events || !subscribed || pending || value.event !== "state" || !Ipc.decimal(value.delivery) || value.delivery === lastDelivery || value.id !== undefined || value.ok !== undefined || !Ipc.object(value.batch))
throw new Error("unexpected IPC state event");
if (!installed && value.batch.type !== "snapshot")
throw new Error("missing initial snapshot");
if (value.batch.session !== handshake.session || Ipc.byteLength(JSON.stringify(value.batch)) > handshake.max_batch_bytes)
throw new Error("invalid IPC batch identity or size");
batchReceived(value.batch);
if (!ready || root.recovering)
return;
installed = true;
initialDeadline.stop();
lastDelivery = value.delivery;
request("ack", {
delivery: value.delivery
});
return;
}
if (!pending)
throw new Error("unsolicited IPC response");
const error = Ipc.response(value, pending.id);
const operation = pending;
pending = null;
deadline.stop();
if (operation.op === "command") {
reply(value.result || null, error);
return;
}
if (error)
throw new Error(error);
switch (operation.op) {
case "hello":
handshake = Ipc.hello(value.result);
helloReceived();
break;
case "subscribe":
if (value.result.subscribed !== true)
throw new Error("invalid subscription response");
subscribed = true;
break;
case "ack":
if (value.result.acked !== operation.params.delivery)
throw new Error("invalid ack response");
break;
default:
throw new Error("unexpected IPC operation");
}
} catch (e) {
fail(String(e));
}
}
function subscribe() {
initialDeadline.restart();
request("subscribe", {});
}
onConnectionStateChanged: {
clear();
if (root.recovering)
return;
if (!linkUp) {
fail("IPC stream disconnected");
return;
}
frameParser = parserComponent.createObject(connection);
try {
request("hello", {});
} catch (e) {
fail(String(e));
}
}
parser: frameParser
Component {
id: parserComponent
SplitParser {
id: lineParser
onRead: line => {
if (connection.frameParser === lineParser)
connection.receive(line);
}
}
}
Timer {
id: deadline
interval: 5000
onTriggered: connection.fail("IPC request timed out")
}
Timer {
id: initialDeadline
interval: 8000
onTriggered: connection.fail("IPC initial snapshot timed out")
}
}
function windowFacade(window) {
const windowSession = session;
const output = outputs.find(o => o.id === window.output);
return {
id: window.id,
aqueousWindowId: window.id,
aqueousKey: windowSession + ":window:" + window.id,
aqueousSession: windowSession,
aqueousWorkspaceId: window.workspace,
aqueousOutputId: window.output,
appId: window.app_id || window.class || "",
title: window.title || "",
activated: root.seat?.window === window.id,
get fullscreen() {
return window.fullscreen;
},
set fullscreen(value) {
root.command("window.fullscreen", {
id: window.id,
session: windowSession,
value: value
});
},
get maximized() {
return window.maximized;
},
set maximized(value) {
root.command("window.maximized", {
id: window.id,
session: windowSession,
value: value
});
},
visible: window.visible,
skipSwitcher: window.skip_switcher,
canMinimize: window.can_minimize,
canMaximize: window.can_maximize,
canActivate: window.can_activate,
screens: Quickshell.screens.filter(s => s.name === output?.name),
get minimized() {
return window.minimized;
},
set minimized(value) {
root.command("window.minimized", {
id: window.id,
session: windowSession,
value: value
});
},
activate: function () {
root.command("window.activate", {
id: window.id,
session: windowSession
});
},
close: function () {
root.command("window.close", {
id: window.id,
session: windowSession
});
}
};
}
function activateWorkspace(workspace) {
if (!seat || !workspace)
return;
command("workspace.activate", {
id: workspace.id,
session: workspace.aqueousSession
});
}
function cycleKeyboardLayout() {
if (!seat || !keyboard || !capabilities.keyboard)
return;
command("keyboard.next", {
group: keyboard.id
});
}
function toggleOverview(screenName) {
if (!capabilities.overview)
return;
command("overview.toggle", {
output: outputId(screenName || focusedOutput)
});
}
function quit() {
command("session.exit");
}
function snapshot(path) {
if (!available || path !== socketPath)
return JSON.stringify({
error: "unavailable"
});
return JSON.stringify({
schema: 1,
session: session,
sequence: sequence,
type: "snapshot",
base_sequence: null,
upsert: entities,
removed: []
});
}
IpcHandler {
target: "aqueous"
function snapshot(path: string): string {
return root.snapshot(path);
}
function status(): string {
return JSON.stringify({
available: root.available,
session: root.session,
sequence: root.sequence,
focusedOutput: root.focusedOutput,
outputs: root.outputs.length,
windows: root.windows.length,
workspaces: root.workspaces.length,
keyboardLayout: root.keyboardLayout,
overviewOutput: root.sessionState.overview_output || null,
overviewWindow: root.sessionState.overview_window || null,
error: root.state.error || ""
});
}
function overview(action: string, output: string): string {
if (!["show", "hide", "toggle"].includes(action))
return "INVALID_ACTION";
if (!root.available || root.locked || !root.capabilities.overview)
return "UNAVAILABLE";
root.command("overview." + action, {
output: root.outputId(output || root.focusedOutput)
});
return "OVERVIEW_REQUESTED";
}
function selectSeat(name: string): string {
if (name && !root.seats.some(s => s.id === name))
return "SEAT_NOT_FOUND";
root.seatName = name;
return "SEAT_SELECTED";
}
}
function overlapsDock(screenName, position, thickness, width, height) {
const output = outputs.find(o => o.name === screenName);
if (!output)
return false;
return windows.some(w => {
if (w.output !== output.id || !w.visible || w.minimized)
return false;
const box = w.outer_geometry;
const x = box.x - output.bounds.x;
const y = box.y - output.bounds.y;
switch (position) {
case SettingsData.Position.Top:
return y < thickness && y + box.height > 0;
case SettingsData.Position.Left:
return x < thickness && x + box.width > 0;
case SettingsData.Position.Right:
return x < width && x + box.width > width - thickness;
default:
return y < height && y + box.height > height - thickness;
}
});
}
Component {
id: jsonProcessComponent
Process {
id: jsonProcess
property var input: null
property var callback: null
property bool finished: false
property Timer deadline: Timer {
id: jsonDeadline
interval: 8000
onTriggered: jsonProcess.finish(null, "command completion uncertain: timeout")
}
stdinEnabled: input !== null
function finish(result, error) {
if (finished)
return;
finished = true;
jsonDeadline.stop();
if (running)
signal(9);
root.pendingProcesses = root.pendingProcesses.filter(p => p !== jsonProcess);
const complete = callback;
callback = null;
if (complete)
complete(result, error);
Qt.callLater(() => jsonProcess.destroy());
}
onStarted: {
if (input !== null) {
write(input);
stdinEnabled = false;
}
}
stdout: StdioCollector {
waitForEnd: false
onDataChanged: {
if (data.byteLength > root.maxBatchBytes + 1)
jsonProcess.finish(null, "process output exceeds 4 MiB");
}
}
stderr: SplitParser {
splitMarker: ""
}
onExited: (code, status) => {
if (finished)
return;
let result = null;
let error = "";
try {
result = root.parseJson(stdout.text);
if (code !== 0 || status !== 0)
error = result?.code ? result.code + ": " + (result.message || "") : (result?.status || "command failed (" + code + ")");
} catch (e) {
error = String(e);
}
finish(result, error);
}
onRunningChanged: {
if (!running)
Qt.callLater(() => {
if (!jsonProcess.finished)
jsonProcess.finish(null, "command could not start");
});
}
}
}
}