1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 12:52:06 -04:00
Files
DankMaterialShell/quickshell/Services/WlrOutputService.qml
Evgeny Zemtsov bddc2f6295 display: support generic wlr-output-management-unstable-v1 (#1840)
The display config UI only applied changes for compositors with a
config-file backend (niri, hyprland, dwl).  For any other compositor
that supports wlr-output-management-unstable-v1 the "Apply Changes"
button was silently a no-op.

Add WlrOutputService.applyOutputsConfig() as a high-level apply that
mirrors the generateOutputsConfig() pattern of the existing services
but applies directly via the protocol instead of writing a config file.
Route the default case in backendWriteOutputsConfig() to it.

This enables using dms-shell as a wayland compositor for emacs wayland
manager (ewm).
2026-03-11 13:28:14 -04:00

347 lines
9.8 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
property bool wlrOutputAvailable: false
property var outputs: []
property int serial: 0
signal stateChanged
signal configurationApplied(bool success, string message)
Connections {
target: DMSService
function onCapabilitiesReceived() {
checkCapabilities();
}
function onConnectionStateChanged() {
if (DMSService.isConnected) {
checkCapabilities();
return;
}
wlrOutputAvailable = false;
}
function onWlrOutputStateUpdate(data) {
if (!wlrOutputAvailable) {
return;
}
handleStateUpdate(data);
}
}
Component.onCompleted: {
if (!DMSService.dmsAvailable) {
return;
}
checkCapabilities();
}
function checkCapabilities() {
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
wlrOutputAvailable = false;
return;
}
const hasWlrOutput = DMSService.capabilities.includes("wlroutput");
if (hasWlrOutput && !wlrOutputAvailable) {
wlrOutputAvailable = true;
console.info("WlrOutputService: wlr-output-management capability detected");
requestState();
return;
}
if (!hasWlrOutput) {
wlrOutputAvailable = false;
}
}
function requestState() {
if (!DMSService.isConnected || !wlrOutputAvailable) {
return;
}
DMSService.sendRequest("wlroutput.getState", null, response => {
if (!response.result) {
return;
}
handleStateUpdate(response.result);
});
}
function handleStateUpdate(state) {
outputs = state.outputs || [];
serial = state.serial || 0;
if (outputs.length === 0) {
console.warn("WlrOutputService: Received empty outputs list");
} else {
console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial);
outputs.forEach((output, index) => {
console.log("WlrOutputService: Output", index, "-", output.name, "enabled:", output.enabled, "mode:", output.currentMode ? output.currentMode.width + "x" + output.currentMode.height + "@" + (output.currentMode.refresh / 1000) + "Hz" : "none");
});
}
stateChanged();
}
function getOutput(name) {
for (const output of outputs) {
if (output.name === name) {
return output;
}
}
return null;
}
function getEnabledOutputs() {
return outputs.filter(output => output.enabled);
}
function applyConfiguration(heads, callback) {
if (!DMSService.isConnected || !wlrOutputAvailable) {
if (callback) {
callback(false, "Not connected");
}
return;
}
console.log("WlrOutputService: Applying configuration for", heads.length, "outputs");
heads.forEach((head, index) => {
console.log("WlrOutputService: Head", index, "- name:", head.name, "enabled:", head.enabled, "modeId:", head.modeId, "customMode:", JSON.stringify(head.customMode), "position:", JSON.stringify(head.position), "scale:", head.scale, "transform:", head.transform, "adaptiveSync:", head.adaptiveSync);
});
DMSService.sendRequest("wlroutput.applyConfiguration", {
"heads": heads
}, response => {
const success = !response.error;
const message = response.error || response.result?.message || "";
if (response.error) {
console.warn("WlrOutputService: applyConfiguration error:", response.error);
} else {
console.log("WlrOutputService: Configuration applied successfully");
}
configurationApplied(success, message);
if (callback) {
callback(success, message);
}
});
}
function testConfiguration(heads, callback) {
if (!DMSService.isConnected || !wlrOutputAvailable) {
if (callback) {
callback(false, "Not connected");
}
return;
}
console.log("WlrOutputService: Testing configuration for", heads.length, "outputs");
DMSService.sendRequest("wlroutput.testConfiguration", {
"heads": heads
}, response => {
const success = !response.error;
const message = response.error || response.result?.message || "";
if (response.error) {
console.warn("WlrOutputService: testConfiguration error:", response.error);
} else {
console.log("WlrOutputService: Configuration test passed");
}
if (callback) {
callback(success, message);
}
});
}
function setOutputEnabled(outputName, enabled, callback) {
const output = getOutput(outputName);
if (!output) {
console.warn("WlrOutputService: Output not found:", outputName);
if (callback) {
callback(false, "Output not found");
}
return;
}
const heads = [
{
"name": outputName,
"enabled": enabled
}
];
if (enabled && output.currentMode) {
heads[0].modeId = output.currentMode.id;
}
applyConfiguration(heads, callback);
}
function setOutputMode(outputName, modeId, callback) {
const heads = [
{
"name": outputName,
"enabled": true,
"modeId": modeId
}
];
applyConfiguration(heads, callback);
}
function setOutputCustomMode(outputName, width, height, refresh, callback) {
const heads = [
{
"name": outputName,
"enabled": true,
"customMode": {
"width": width,
"height": height,
"refresh": refresh
}
}
];
applyConfiguration(heads, callback);
}
function setOutputPosition(outputName, x, y, callback) {
const heads = [
{
"name": outputName,
"enabled": true,
"position": {
"x": x,
"y": y
}
}
];
applyConfiguration(heads, callback);
}
function setOutputScale(outputName, scale, callback) {
const heads = [
{
"name": outputName,
"enabled": true,
"scale": scale
}
];
applyConfiguration(heads, callback);
}
function setOutputTransform(outputName, transform, callback) {
const heads = [
{
"name": outputName,
"enabled": true,
"transform": transform
}
];
applyConfiguration(heads, callback);
}
function setOutputAdaptiveSync(outputName, state, callback) {
const heads = [
{
"name": outputName,
"enabled": true,
"adaptiveSync": state
}
];
applyConfiguration(heads, callback);
}
function configureOutput(config, callback) {
const heads = [config];
applyConfiguration(heads, callback);
}
function configureMultipleOutputs(configs, callback) {
applyConfiguration(configs, callback);
}
// High-level apply matching the generateOutputsConfig() pattern used by
// NiriService, HyprlandService and DwlService. Instead of writing a
// config file, the changes are applied directly via the
// wlr-output-management protocol.
function applyOutputsConfig(outputsData, connectedOutputs) {
if (!wlrOutputAvailable)
return;
const heads = [];
for (const name in outputsData) {
if (!connectedOutputs[name])
continue;
const output = outputsData[name];
const mode = (output.modes && output.current_mode >= 0) ? output.modes[output.current_mode] : null;
const enabled = !!mode;
const head = {
"name": name,
"enabled": enabled
};
if (enabled) {
if (mode.id !== undefined)
head.modeId = mode.id;
else
head.customMode = {
"width": mode.width,
"height": mode.height,
"refresh": mode.refresh_rate
};
if (output.logical) {
head.position = {
"x": output.logical.x ?? 0,
"y": output.logical.y ?? 0
};
head.scale = output.logical.scale ?? 1.0;
head.transform = transformFromName(output.logical.transform);
}
}
heads.push(head);
}
if (heads.length > 0)
applyConfiguration(heads);
}
function transformFromName(name) {
switch (name) {
case "Normal":
return 0;
case "90":
return 1;
case "180":
return 2;
case "270":
return 3;
case "Flipped":
return 4;
case "Flipped90":
return 5;
case "Flipped180":
return 6;
case "Flipped270":
return 7;
default:
return 0;
}
}
}