1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-12 00:32:17 -04:00

displays: add configurator for niri, Hyprland, and MangoWC

- Configure position, VRR, orientation, resolution, refresh rate
- Split Display section into Configuration, Gamma, and Widgets
- MangoWC omits VRR because it doesnt have per-display VRR
- HDR configuration not present for Hyprland
This commit is contained in:
bbedward
2025-12-15 16:36:14 -05:00
parent bafe1c5fee
commit 2745116ac5
15 changed files with 2727 additions and 648 deletions

View File

@@ -1,13 +1,19 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string mangoDmsDir: configDir + "/mango/dms"
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
property bool dwlAvailable: false
property var outputs: ({})
property var tagCount: 9
@@ -263,4 +269,80 @@ Singleton {
return Array.from(visibleTags).sort((a, b) => a - b);
}
function generateOutputsConfig(outputsData) {
if (!outputsData || Object.keys(outputsData).length === 0)
return;
let lines = ["# Auto-generated by DMS - do not edit manually", "# VRR is global: set adaptive_sync=1 in config.conf", ""];
for (const outputName in outputsData) {
const output = outputsData[outputName];
if (!output)
continue;
let width = 1920;
let height = 1080;
let refreshRate = 60;
if (output.modes && output.current_mode !== undefined) {
const mode = output.modes[output.current_mode];
if (mode) {
width = mode.width || 1920;
height = mode.height || 1080;
refreshRate = Math.round((mode.refresh_rate || 60000) / 1000);
}
}
const x = output.logical?.x ?? 0;
const y = output.logical?.y ?? 0;
const scale = output.logical?.scale ?? 1.0;
const transform = transformToMango(output.logical?.transform ?? "Normal");
const rule = [outputName, "0.55", "1", "tile", transform, scale, x, y, width, height, refreshRate].join(",");
lines.push("monitorrule=" + rule);
}
lines.push("");
const content = lines.join("\n");
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("DwlService: Failed to write outputs config:", output);
return;
}
console.info("DwlService: Generated outputs config at", outputsPath);
if (CompositorService.isDwl)
reloadConfig();
});
}
function reloadConfig() {
Proc.runCommand("mango-reload", ["mmsg", "-d", "reload_config"], (output, exitCode) => {
if (exitCode !== 0)
console.warn("DwlService: mmsg reload_config failed:", output);
});
}
function transformToMango(transform) {
switch (transform) {
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;
}
}
}

View File

@@ -0,0 +1,125 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import qs.Common
Singleton {
id: root
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string hyprDmsDir: configDir + "/hypr/dms"
readonly property string outputsPath: hyprDmsDir + "/outputs.conf"
function getOutputIdentifier(output, outputName) {
if (SettingsData.displayNameMode === "model" && output.make && output.model) {
return "desc:" + output.make + " " + output.model;
}
return outputName;
}
function generateOutputsConfig(outputsData) {
if (!outputsData || Object.keys(outputsData).length === 0)
return;
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
for (const outputName in outputsData) {
const output = outputsData[outputName];
if (!output)
continue;
let resolution = "preferred";
if (output.modes && output.current_mode !== undefined) {
const mode = output.modes[output.current_mode];
if (mode)
resolution = mode.width + "x" + mode.height + "@" + (mode.refresh_rate / 1000).toFixed(3);
}
const x = output.logical?.x ?? 0;
const y = output.logical?.y ?? 0;
const position = x + "x" + y;
const scale = output.logical?.scale ?? 1.0;
const identifier = getOutputIdentifier(output, outputName);
let monitorLine = "monitor = " + identifier + ", " + resolution + ", " + position + ", " + scale;
const transform = transformToHyprland(output.logical?.transform ?? "Normal");
if (transform !== 0)
monitorLine += ", transform, " + transform;
if (output.vrr_supported && output.vrr_enabled)
monitorLine += ", vrr, 1";
lines.push(monitorLine);
}
lines.push("");
const content = lines.join("\n");
Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("HyprlandService: Failed to write outputs config:", output);
return;
}
console.info("HyprlandService: Generated outputs config at", outputsPath);
if (CompositorService.isHyprland)
reloadConfig();
});
}
function reloadConfig() {
Proc.runCommand("hyprctl-reload", ["hyprctl", "reload"], (output, exitCode) => {
if (exitCode !== 0)
console.warn("HyprlandService: hyprctl reload failed:", output);
});
}
function transformToHyprland(transform) {
switch (transform) {
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;
}
}
function hyprlandToTransform(value) {
switch (value) {
case 0:
return "Normal";
case 1:
return "90";
case 2:
return "180";
case 3:
return "270";
case 4:
return "Flipped";
case 5:
return "Flipped90";
case 6:
return "Flipped180";
case 7:
return "Flipped270";
default:
return "Normal";
}
}
}

View File

@@ -1,5 +1,5 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtCore
import QtQuick
@@ -23,6 +23,8 @@ Singleton {
property var windows: []
property var displayScales: ({})
property var _realOutputs: ({})
property bool inOverview: false
property int currentKeyboardLayoutIndex: 0
@@ -214,12 +216,12 @@ Singleton {
const ws = workspaces[w.workspace_id];
if (!ws) {
return {
window: w,
outputX: 999999,
outputY: 999999,
wsIdx: 999999,
col: 999999,
row: 999999
"window": w,
"outputX": 999999,
"outputY": 999999,
"wsIdx": 999999,
"col": 999999,
"row": 999999
};
}
@@ -232,12 +234,12 @@ Singleton {
const row = (pos && pos.length >= 2) ? pos[1] : 999999;
return {
window: w,
outputX: outputX,
outputY: outputY,
wsIdx: ws.idx,
col: col,
row: row
"window": w,
"outputX": outputX,
"outputY": outputY,
"wsIdx": ws.idx,
"col": col,
"row": row
};
});
@@ -598,7 +600,7 @@ Singleton {
const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR");
const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path];
Quickshell.execDetached({
command: command
"command": command
});
pendingScreenshotPath = "";
}
@@ -993,35 +995,35 @@ Singleton {
const gaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const dmsWarning = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`;
`;
const configContent = dmsWarning + `layout {
gaps ${gaps}
gaps ${gaps}
border {
border {
width 2
}
}
focus-ring {
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius ${cornerRadius}
clip-to-geometry true
tiled-state true
draw-border-with-background false
}`;
}
}
window-rule {
geometry-corner-radius ${cornerRadius}
clip-to-geometry true
tiled-state true
draw-border-with-background false
}`;
const alttabContent = dmsWarning + `recent-windows {
highlight {
highlight {
corner-radius ${cornerRadius}
}
}`;
}
}`;
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const niriDmsDir = configDir + "/niri/dms";
@@ -1054,6 +1056,141 @@ window-rule {
writeBlurruleProcess.running = true;
}
function updateOutputPosition(outputName, x, y) {
if (!outputs || !outputs[outputName])
return;
const updatedOutputs = {};
for (const name in outputs) {
const output = outputs[name];
if (name === outputName && output.logical) {
updatedOutputs[name] = JSON.parse(JSON.stringify(output));
updatedOutputs[name].logical.x = x;
updatedOutputs[name].logical.y = y;
} else {
updatedOutputs[name] = output;
}
}
outputs = updatedOutputs;
}
function applyOutputConfig(outputName, config, callback) {
if (!CompositorService.isNiri || !outputName) {
if (callback)
callback(false, "Invalid config");
return;
}
const commands = [];
if (config.position !== undefined) {
commands.push(`niri msg output "${outputName}" position ${config.position.x} ${config.position.y}`);
}
if (config.mode !== undefined) {
commands.push(`niri msg output "${outputName}" mode ${config.mode}`);
}
if (config.vrr !== undefined) {
commands.push(`niri msg output "${outputName}" vrr ${config.vrr ? "on" : "off"}`);
}
if (config.scale !== undefined) {
commands.push(`niri msg output "${outputName}" scale ${config.scale}`);
}
if (config.transform !== undefined) {
commands.push(`niri msg output "${outputName}" transform "${config.transform}"`);
}
if (commands.length === 0) {
if (callback)
callback(true, "No changes");
return;
}
const fullCommand = commands.join(" && ");
Proc.runCommand("niri-output-config", ["sh", "-c", fullCommand], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("NiriService: Failed to apply output config:", output);
if (callback)
callback(false, output);
return;
}
console.info("NiriService: Applied output config for", outputName);
fetchOutputs();
if (callback)
callback(true, "Success");
});
}
function getOutputIdentifier(output, outputName) {
if (SettingsData.displayNameMode === "model" && output.make && output.model) {
const serial = output.serial || "Unknown";
return output.make + " " + output.model + " " + serial;
}
return outputName;
}
function generateOutputsConfig(outputsData) {
const data = outputsData || outputs;
if (!data || Object.keys(data).length === 0)
return;
let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`;
for (const outputName in data) {
const output = data[outputName];
const identifier = getOutputIdentifier(output, outputName);
kdlContent += `output "${identifier}" {\n`;
if (output.current_mode !== undefined && output.modes && output.modes[output.current_mode]) {
const mode = output.modes[output.current_mode];
kdlContent += ` mode "${mode.width}x${mode.height}@${(mode.refresh_rate / 1000).toFixed(3)}"\n`;
}
if (output.logical) {
if (output.logical.scale && output.logical.scale !== 1.0) {
kdlContent += ` scale ${output.logical.scale}\n`;
}
if (output.logical.transform && output.logical.transform !== "Normal") {
const transformMap = {
"Normal": "normal",
"90": "90",
"180": "180",
"270": "270",
"Flipped": "flipped",
"Flipped90": "flipped-90",
"Flipped180": "flipped-180",
"Flipped270": "flipped-270"
};
kdlContent += ` transform "${transformMap[output.logical.transform] || "normal"}"\n`;
}
if (output.logical.x !== undefined && output.logical.y !== undefined) {
kdlContent += ` position x=${output.logical.x} y=${output.logical.y}\n`;
}
}
if (output.vrr_enabled) {
kdlContent += ` variable-refresh-rate\n`;
}
kdlContent += `}\n\n`;
}
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const niriDmsDir = configDir + "/niri/dms";
const outputsPath = niriDmsDir + "/outputs.kdl";
Proc.runCommand("niri-write-outputs", ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${kdlContent}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("NiriService: Failed to write outputs config:", output);
return;
}
console.info("NiriService: Generated outputs config at", outputsPath);
});
}
IpcHandler {
function screenshot(): string {
if (!CompositorService.isNiri) {