mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 15:32:50 -05:00
displays: add configurator (Beta)
- Position, resolution, refresh, orientation, VRR - niri, Hyprland, MangoWC - Rely on wlr-output for reading data, compositors to write output configurations - Re-organize display setting group
This commit is contained in:
@@ -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,85 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
110
quickshell/Services/HyprlandService.qml
Normal file
110
quickshell/Services/HyprlandService.qml
Normal file
@@ -0,0 +1,110 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user