1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-13 17:22:08 -04:00

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).
This commit is contained in:
Evgeny Zemtsov
2026-03-11 18:28:14 +01:00
committed by GitHub
parent 25dce2961b
commit bddc2f6295
2 changed files with 190 additions and 116 deletions

View File

@@ -909,6 +909,9 @@ Singleton {
case "dwl": case "dwl":
DwlService.generateOutputsConfig(outputsData); DwlService.generateOutputsConfig(outputsData);
break; break;
default:
WlrOutputService.applyOutputsConfig(outputsData, outputs);
break;
} }
} }

View File

@@ -18,258 +18,329 @@ Singleton {
target: DMSService target: DMSService
function onCapabilitiesReceived() { function onCapabilitiesReceived() {
checkCapabilities() checkCapabilities();
} }
function onConnectionStateChanged() { function onConnectionStateChanged() {
if (DMSService.isConnected) { if (DMSService.isConnected) {
checkCapabilities() checkCapabilities();
return return;
} }
wlrOutputAvailable = false wlrOutputAvailable = false;
} }
function onWlrOutputStateUpdate(data) { function onWlrOutputStateUpdate(data) {
if (!wlrOutputAvailable) { if (!wlrOutputAvailable) {
return return;
} }
handleStateUpdate(data) handleStateUpdate(data);
} }
} }
Component.onCompleted: { Component.onCompleted: {
if (!DMSService.dmsAvailable) { if (!DMSService.dmsAvailable) {
return return;
} }
checkCapabilities() checkCapabilities();
} }
function checkCapabilities() { function checkCapabilities() {
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) { if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
wlrOutputAvailable = false wlrOutputAvailable = false;
return return;
} }
const hasWlrOutput = DMSService.capabilities.includes("wlroutput") const hasWlrOutput = DMSService.capabilities.includes("wlroutput");
if (hasWlrOutput && !wlrOutputAvailable) { if (hasWlrOutput && !wlrOutputAvailable) {
wlrOutputAvailable = true wlrOutputAvailable = true;
console.info("WlrOutputService: wlr-output-management capability detected") console.info("WlrOutputService: wlr-output-management capability detected");
requestState() requestState();
return return;
} }
if (!hasWlrOutput) { if (!hasWlrOutput) {
wlrOutputAvailable = false wlrOutputAvailable = false;
} }
} }
function requestState() { function requestState() {
if (!DMSService.isConnected || !wlrOutputAvailable) { if (!DMSService.isConnected || !wlrOutputAvailable) {
return return;
} }
DMSService.sendRequest("wlroutput.getState", null, response => { DMSService.sendRequest("wlroutput.getState", null, response => {
if (!response.result) { if (!response.result) {
return return;
} }
handleStateUpdate(response.result) handleStateUpdate(response.result);
}) });
} }
function handleStateUpdate(state) { function handleStateUpdate(state) {
outputs = state.outputs || [] outputs = state.outputs || [];
serial = state.serial || 0 serial = state.serial || 0;
if (outputs.length === 0) { if (outputs.length === 0) {
console.warn("WlrOutputService: Received empty outputs list") console.warn("WlrOutputService: Received empty outputs list");
} else { } else {
console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial) console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial);
outputs.forEach((output, index) => { outputs.forEach((output, index) => {
console.log("WlrOutputService: Output", index, "-", output.name, 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");
"enabled:", output.enabled, });
"mode:", output.currentMode ?
output.currentMode.width + "x" + output.currentMode.height + "@" +
(output.currentMode.refresh / 1000) + "Hz" : "none")
})
} }
stateChanged() stateChanged();
} }
function getOutput(name) { function getOutput(name) {
for (const output of outputs) { for (const output of outputs) {
if (output.name === name) { if (output.name === name) {
return output return output;
} }
} }
return null return null;
} }
function getEnabledOutputs() { function getEnabledOutputs() {
return outputs.filter(output => output.enabled) return outputs.filter(output => output.enabled);
} }
function applyConfiguration(heads, callback) { function applyConfiguration(heads, callback) {
if (!DMSService.isConnected || !wlrOutputAvailable) { if (!DMSService.isConnected || !wlrOutputAvailable) {
if (callback) { if (callback) {
callback(false, "Not connected") callback(false, "Not connected");
} }
return return;
} }
console.log("WlrOutputService: Applying configuration for", heads.length, "outputs") console.log("WlrOutputService: Applying configuration for", heads.length, "outputs");
heads.forEach((head, index) => { heads.forEach((head, index) => {
console.log("WlrOutputService: Head", index, "- name:", head.name, 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);
"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", { DMSService.sendRequest("wlroutput.applyConfiguration", {
"heads": heads "heads": heads
}, response => { }, response => {
const success = !response.error const success = !response.error;
const message = response.error || response.result?.message || "" const message = response.error || response.result?.message || "";
if (response.error) { if (response.error) {
console.warn("WlrOutputService: applyConfiguration error:", response.error) console.warn("WlrOutputService: applyConfiguration error:", response.error);
} else { } else {
console.log("WlrOutputService: Configuration applied successfully") console.log("WlrOutputService: Configuration applied successfully");
} }
configurationApplied(success, message) configurationApplied(success, message);
if (callback) { if (callback) {
callback(success, message) callback(success, message);
} }
}) });
} }
function testConfiguration(heads, callback) { function testConfiguration(heads, callback) {
if (!DMSService.isConnected || !wlrOutputAvailable) { if (!DMSService.isConnected || !wlrOutputAvailable) {
if (callback) { if (callback) {
callback(false, "Not connected") callback(false, "Not connected");
} }
return return;
} }
console.log("WlrOutputService: Testing configuration for", heads.length, "outputs") console.log("WlrOutputService: Testing configuration for", heads.length, "outputs");
DMSService.sendRequest("wlroutput.testConfiguration", { DMSService.sendRequest("wlroutput.testConfiguration", {
"heads": heads "heads": heads
}, response => { }, response => {
const success = !response.error const success = !response.error;
const message = response.error || response.result?.message || "" const message = response.error || response.result?.message || "";
if (response.error) { if (response.error) {
console.warn("WlrOutputService: testConfiguration error:", response.error) console.warn("WlrOutputService: testConfiguration error:", response.error);
} else { } else {
console.log("WlrOutputService: Configuration test passed") console.log("WlrOutputService: Configuration test passed");
} }
if (callback) { if (callback) {
callback(success, message) callback(success, message);
} }
}) });
} }
function setOutputEnabled(outputName, enabled, callback) { function setOutputEnabled(outputName, enabled, callback) {
const output = getOutput(outputName) const output = getOutput(outputName);
if (!output) { if (!output) {
console.warn("WlrOutputService: Output not found:", outputName) console.warn("WlrOutputService: Output not found:", outputName);
if (callback) { if (callback) {
callback(false, "Output not found") callback(false, "Output not found");
} }
return return;
} }
const heads = [{ const heads = [
"name": outputName, {
"enabled": enabled "name": outputName,
}] "enabled": enabled
}
];
if (enabled && output.currentMode) { if (enabled && output.currentMode) {
heads[0].modeId = output.currentMode.id heads[0].modeId = output.currentMode.id;
} }
applyConfiguration(heads, callback) applyConfiguration(heads, callback);
} }
function setOutputMode(outputName, modeId, callback) { function setOutputMode(outputName, modeId, callback) {
const heads = [{ const heads = [
"name": outputName, {
"enabled": true, "name": outputName,
"modeId": modeId "enabled": true,
}] "modeId": modeId
}
];
applyConfiguration(heads, callback) applyConfiguration(heads, callback);
} }
function setOutputCustomMode(outputName, width, height, refresh, callback) { function setOutputCustomMode(outputName, width, height, refresh, callback) {
const heads = [{ const heads = [
"name": outputName, {
"enabled": true, "name": outputName,
"customMode": { "enabled": true,
"width": width, "customMode": {
"height": height, "width": width,
"refresh": refresh "height": height,
"refresh": refresh
}
} }
}] ];
applyConfiguration(heads, callback) applyConfiguration(heads, callback);
} }
function setOutputPosition(outputName, x, y, callback) { function setOutputPosition(outputName, x, y, callback) {
const heads = [{ const heads = [
"name": outputName, {
"enabled": true, "name": outputName,
"position": { "enabled": true,
"x": x, "position": {
"y": y "x": x,
"y": y
}
} }
}] ];
applyConfiguration(heads, callback) applyConfiguration(heads, callback);
} }
function setOutputScale(outputName, scale, callback) { function setOutputScale(outputName, scale, callback) {
const heads = [{ const heads = [
"name": outputName, {
"enabled": true, "name": outputName,
"scale": scale "enabled": true,
}] "scale": scale
}
];
applyConfiguration(heads, callback) applyConfiguration(heads, callback);
} }
function setOutputTransform(outputName, transform, callback) { function setOutputTransform(outputName, transform, callback) {
const heads = [{ const heads = [
"name": outputName, {
"enabled": true, "name": outputName,
"transform": transform "enabled": true,
}] "transform": transform
}
];
applyConfiguration(heads, callback) applyConfiguration(heads, callback);
} }
function setOutputAdaptiveSync(outputName, state, callback) { function setOutputAdaptiveSync(outputName, state, callback) {
const heads = [{ const heads = [
"name": outputName, {
"enabled": true, "name": outputName,
"adaptiveSync": state "enabled": true,
}] "adaptiveSync": state
}
];
applyConfiguration(heads, callback) applyConfiguration(heads, callback);
} }
function configureOutput(config, callback) { function configureOutput(config, callback) {
const heads = [config] const heads = [config];
applyConfiguration(heads, callback) applyConfiguration(heads, callback);
} }
function configureMultipleOutputs(configs, callback) { function configureMultipleOutputs(configs, callback) {
applyConfiguration(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;
}
} }
} }