1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

displays: add hyprland HDR options

This commit is contained in:
bbedward
2025-12-16 14:12:51 -05:00
parent 55fe463405
commit d9a1089039
7 changed files with 555 additions and 14 deletions

View File

@@ -25,11 +25,13 @@ Singleton {
property var pendingChanges: ({})
property var pendingNiriChanges: ({})
property var pendingHyprlandChanges: ({})
property var originalNiriSettings: null
property var originalHyprlandSettings: null
property var originalOutputs: null
property string originalDisplayNameMode: ""
property bool formatChanged: originalDisplayNameMode !== "" && originalDisplayNameMode !== SettingsData.displayNameMode
property bool hasPendingChanges: Object.keys(pendingChanges).length > 0 || Object.keys(pendingNiriChanges).length > 0 || formatChanged
property bool hasPendingChanges: Object.keys(pendingChanges).length > 0 || Object.keys(pendingNiriChanges).length > 0 || Object.keys(pendingHyprlandChanges).length > 0 || formatChanged
property bool validatingConfig: false
property string validationError: ""
@@ -85,9 +87,48 @@ Singleton {
const parsed = parseOutputsConfig(content);
const filtered = filterDisconnectedOnly(parsed);
savedOutputs = filtered;
if (CompositorService.isHyprland)
initHyprlandSettingsFromConfig(parsed);
});
}
function initHyprlandSettingsFromConfig(parsedOutputs) {
const current = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings));
let changed = false;
for (const outputName in parsedOutputs) {
const output = parsedOutputs[outputName];
const settings = output.hyprlandSettings;
if (!settings)
continue;
if (current[outputName])
continue;
const hasSettings = settings.colorManagement || settings.bitdepth ||
settings.sdrBrightness !== undefined || settings.sdrSaturation !== undefined;
if (!hasSettings)
continue;
current[outputName] = {};
if (settings.colorManagement)
current[outputName].colorManagement = settings.colorManagement;
if (settings.bitdepth)
current[outputName].bitdepth = settings.bitdepth;
if (settings.sdrBrightness !== undefined)
current[outputName].sdrBrightness = settings.sdrBrightness;
if (settings.sdrSaturation !== undefined)
current[outputName].sdrSaturation = settings.sdrSaturation;
changed = true;
}
if (changed) {
SettingsData.hyprlandOutputSettings = current;
SettingsData.saveSettings();
}
}
function filterDisconnectedOnly(parsedOutputs) {
const result = {};
const liveNames = Object.keys(outputs);
@@ -165,17 +206,46 @@ Singleton {
const result = {};
const lines = content.split("\n");
for (const line of lines) {
const match = line.match(/^\s*monitor\s*=\s*([^,]+),\s*(\d+)x(\d+)@([\d.]+),\s*(-?\d+)x(-?\d+),\s*([\d.]+)(?:,\s*transform,\s*(\d+))?(?:,\s*vrr,\s*(\d+))?/);
const match = line.match(/^\s*monitor\s*=\s*([^,]+),\s*(\d+)x(\d+)@([\d.]+),\s*(-?\d+)x(-?\d+),\s*([\d.]+)/);
if (!match)
continue;
const name = match[1].trim();
const rest = line.substring(line.indexOf(match[7]) + match[7].length);
let transform = 0, vrr = false, bitdepth = undefined, cm = undefined;
let sdrBrightness = undefined, sdrSaturation = undefined;
const transformMatch = rest.match(/,\s*transform,\s*(\d+)/);
if (transformMatch)
transform = parseInt(transformMatch[1]);
const vrrMatch = rest.match(/,\s*vrr,\s*(\d+)/);
if (vrrMatch)
vrr = vrrMatch[1] === "1";
const bitdepthMatch = rest.match(/,\s*bitdepth,\s*(\d+)/);
if (bitdepthMatch)
bitdepth = parseInt(bitdepthMatch[1]);
const cmMatch = rest.match(/,\s*cm,\s*(\w+)/);
if (cmMatch)
cm = cmMatch[1];
const sdrBrightnessMatch = rest.match(/,\s*sdrbrightness,\s*([\d.]+)/);
if (sdrBrightnessMatch)
sdrBrightness = parseFloat(sdrBrightnessMatch[1]);
const sdrSaturationMatch = rest.match(/,\s*sdrsaturation,\s*([\d.]+)/);
if (sdrSaturationMatch)
sdrSaturation = parseFloat(sdrSaturationMatch[1]);
result[name] = {
"name": name,
"logical": {
"x": parseInt(match[5]),
"y": parseInt(match[6]),
"scale": parseFloat(match[7]),
"transform": hyprlandToTransform(parseInt(match[8] || "0"))
"transform": hyprlandToTransform(transform)
},
"modes": [
{
@@ -185,8 +255,14 @@ Singleton {
}
],
"current_mode": 0,
"vrr_enabled": match[9] === "1",
"vrr_supported": true
"vrr_enabled": vrr,
"vrr_supported": true,
"hyprlandSettings": {
"bitdepth": bitdepth,
"colorManagement": cm,
"sdrBrightness": sdrBrightness,
"sdrSaturation": sdrSaturation
}
};
}
return result;
@@ -426,7 +502,7 @@ Singleton {
NiriService.generateOutputsConfig(outputsData);
break;
case "hyprland":
HyprlandService.generateOutputsConfig(outputsData);
HyprlandService.generateOutputsConfig(outputsData, buildMergedHyprlandSettings());
break;
case "dwl":
DwlService.generateOutputsConfig(outputsData);
@@ -582,6 +658,42 @@ Singleton {
originalNiriSettings = JSON.parse(JSON.stringify(SettingsData.niriOutputSettings));
}
function getHyprlandOutputIdentifier(output, outputName) {
if (SettingsData.displayNameMode === "model" && output?.make && output?.model)
return "desc:" + output.make + " " + output.model;
return outputName;
}
function getHyprlandSetting(output, outputName, key, defaultValue) {
if (!CompositorService.isHyprland)
return defaultValue;
const identifier = getHyprlandOutputIdentifier(output, outputName);
const pending = pendingHyprlandChanges[identifier];
if (pending && (key in pending)) {
const val = pending[key];
return (val !== null && val !== undefined) ? val : defaultValue;
}
return SettingsData.getHyprlandOutputSetting(identifier, key, defaultValue);
}
function setHyprlandSetting(output, outputName, key, value) {
if (!CompositorService.isHyprland)
return;
initOriginalHyprlandSettings();
const identifier = getHyprlandOutputIdentifier(output, outputName);
const newPending = JSON.parse(JSON.stringify(pendingHyprlandChanges));
if (!newPending[identifier])
newPending[identifier] = {};
newPending[identifier][key] = value;
pendingHyprlandChanges = newPending;
}
function initOriginalHyprlandSettings() {
if (originalHyprlandSettings)
return;
originalHyprlandSettings = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings));
}
function initOriginalOutputs() {
if (!originalOutputs)
originalOutputs = JSON.parse(JSON.stringify(outputs));
@@ -667,8 +779,10 @@ Singleton {
function clearPendingChanges() {
pendingChanges = {};
pendingNiriChanges = {};
pendingHyprlandChanges = {};
originalOutputs = null;
originalNiriSettings = null;
originalHyprlandSettings = null;
originalDisplayNameMode = "";
}
@@ -719,6 +833,22 @@ Singleton {
changeDescriptions.push(outputId + ": " + I18n.tr("Layout") + " → " + I18n.tr("Modified"));
}
for (const outputId in pendingHyprlandChanges) {
const changes = pendingHyprlandChanges[outputId];
if (changes.bitdepth !== undefined)
changeDescriptions.push(outputId + ": " + I18n.tr("Bit Depth") + " → " + changes.bitdepth);
if (changes.colorManagement !== undefined)
changeDescriptions.push(outputId + ": " + I18n.tr("Color Management") + " → " + changes.colorManagement);
if (changes.sdrBrightness !== undefined)
changeDescriptions.push(outputId + ": " + I18n.tr("SDR Brightness") + " → " + changes.sdrBrightness);
if (changes.sdrSaturation !== undefined)
changeDescriptions.push(outputId + ": " + I18n.tr("SDR Saturation") + " → " + changes.sdrSaturation);
if (changes.supportsHdr !== undefined)
changeDescriptions.push(outputId + ": " + I18n.tr("Force HDR") + " → " + (changes.supportsHdr ? I18n.tr("Yes") : I18n.tr("No")));
if (changes.supportsWideColor !== undefined)
changeDescriptions.push(outputId + ": " + I18n.tr("Force Wide Color") + " → " + (changes.supportsWideColor ? I18n.tr("Yes") : I18n.tr("No")));
}
if (CompositorService.isNiri) {
validateAndApplyNiriConfig(changeDescriptions);
return;
@@ -729,6 +859,9 @@ Singleton {
if (formatChanged)
SettingsData.saveSettings();
if (CompositorService.isHyprland)
commitHyprlandSettingsChanges();
const mergedOutputs = buildOutputsWithPendingChanges();
backendWriteOutputsConfig(mergedOutputs);
}
@@ -788,6 +921,34 @@ Singleton {
}
}
function buildMergedHyprlandSettings() {
const merged = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings));
for (const outputId in pendingHyprlandChanges) {
if (!merged[outputId])
merged[outputId] = {};
for (const key in pendingHyprlandChanges[outputId]) {
const val = pendingHyprlandChanges[outputId][key];
if (val === null || val === undefined)
delete merged[outputId][key];
else
merged[outputId][key] = val;
}
}
return merged;
}
function commitHyprlandSettingsChanges() {
for (const outputId in pendingHyprlandChanges) {
for (const key in pendingHyprlandChanges[outputId]) {
const val = pendingHyprlandChanges[outputId][key];
if (val === null || val === undefined)
SettingsData.removeHyprlandOutputSetting(outputId, key);
else
SettingsData.setHyprlandOutputSetting(outputId, key, val);
}
}
}
function generateNiriOutputsKdl(outputsData, niriSettings) {
let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`;
for (const outputName in outputsData) {
@@ -893,6 +1054,7 @@ Singleton {
function revertChanges() {
const hadFormatChange = originalDisplayNameMode !== "";
const hadNiriChanges = originalNiriSettings !== null;
const hadHyprlandChanges = originalHyprlandSettings !== null;
if (hadFormatChange) {
SettingsData.displayNameMode = originalDisplayNameMode;
@@ -904,7 +1066,15 @@ Singleton {
SettingsData.saveSettings();
}
if (!originalOutputs && !hadNiriChanges) {
if (hadHyprlandChanges) {
SettingsData.hyprlandOutputSettings = JSON.parse(JSON.stringify(originalHyprlandSettings));
SettingsData.saveSettings();
}
pendingHyprlandChanges = {};
pendingNiriChanges = {};
if (!originalOutputs && !hadNiriChanges && !hadHyprlandChanges) {
if (hadFormatChange)
backendWriteOutputsConfig(buildOutputsWithPendingChanges());
clearPendingChanges();

View File

@@ -0,0 +1,275 @@
import QtQuick
import qs.Common
import qs.Widgets
Column {
id: root
property string outputName: ""
property var outputData: null
property bool expanded: false
width: parent.width
spacing: 0
Rectangle {
width: parent.width
height: headerRow.implicitHeight + Theme.spacingS * 2
color: headerMouse.containsMouse ? Theme.withAlpha(Theme.primary, 0.1) : "transparent"
radius: Theme.cornerRadius / 2
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.expanded ? "expand_more" : "chevron_right"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Compositor Settings")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: headerMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
}
}
Column {
id: settingsColumn
width: parent.width
spacing: Theme.spacingS
visible: root.expanded
topPadding: Theme.spacingS
property int currentBitdepth: {
DisplayConfigState.pendingHyprlandChanges;
return DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "bitdepth", 8);
}
property bool is10Bit: currentBitdepth === 10
property string currentCm: {
DisplayConfigState.pendingHyprlandChanges;
return DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto");
}
property bool isHdrMode: currentCm === "hdr" || currentCm === "hdredid"
DankToggle {
width: parent.width
text: I18n.tr("10-bit Color")
description: I18n.tr("Enable 10-bit color depth for wider color gamut and HDR support")
checked: settingsColumn.is10Bit
onToggled: checked => {
if (checked) {
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "bitdepth", 10);
} else {
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "bitdepth", null);
if (settingsColumn.isHdrMode)
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto");
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: settingsColumn.is10Bit
Rectangle {
width: parent.width
height: 1
color: Theme.withAlpha(Theme.outline, 0.15)
}
DankDropdown {
width: parent.width
text: I18n.tr("Color Gamut")
addHorizontalPadding: true
currentValue: {
DisplayConfigState.pendingHyprlandChanges;
const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "colorManagement", "auto");
return cmLabelMap[val] || I18n.tr("Auto (Wide)");
}
options: [I18n.tr("Auto (Wide)"), I18n.tr("Wide (BT2020)"), "DCI-P3", "Apple P3", "Adobe RGB", "EDID", "HDR", I18n.tr("HDR (EDID)")]
property var cmValueMap: ({
[I18n.tr("Auto (Wide)")]: "auto",
[I18n.tr("Wide (BT2020)")]: "wide",
"DCI-P3": "dcip3",
"Apple P3": "dp3",
"Adobe RGB": "adobe",
"EDID": "edid",
"HDR": "hdr",
[I18n.tr("HDR (EDID)")]: "hdredid"
})
property var cmLabelMap: ({
"auto": I18n.tr("Auto (Wide)"),
"wide": I18n.tr("Wide (BT2020)"),
"dcip3": "DCI-P3",
"dp3": "Apple P3",
"adobe": "Adobe RGB",
"edid": "EDID",
"hdr": "HDR",
"hdredid": I18n.tr("HDR (EDID)")
})
onValueChanged: value => {
const cmValue = cmValueMap[value] || "auto";
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "colorManagement", cmValue);
}
}
Rectangle {
width: parent.width - Theme.spacingM * 2
anchors.horizontalCenter: parent.horizontalCenter
height: warningColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius / 2
color: Theme.withAlpha(Theme.warning, 0.15)
border.color: Theme.withAlpha(Theme.warning, 0.3)
border.width: 1
visible: settingsColumn.isHdrMode
Column {
id: warningColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
DankIcon {
name: "warning"
size: Theme.iconSize - 4
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Experimental Feature")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("HDR mode is experimental. Verify your monitor supports HDR before enabling.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: settingsColumn.isHdrMode
Rectangle {
width: parent.width
height: 1
color: Theme.withAlpha(Theme.outline, 0.15)
}
StyledText {
text: I18n.tr("HDR Tone Mapping")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
leftPadding: Theme.spacingM
}
Row {
width: parent.width - Theme.spacingM * 2
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("SDR Brightness")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: parent.width
height: 40
placeholderText: "1.0 - 2.0"
text: {
DisplayConfigState.pendingHyprlandChanges;
const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", null);
return val !== null ? val.toString() : "";
}
onEditingFinished: {
const trimmed = text.trim();
if (!trimmed) {
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", null);
return;
}
const val = parseFloat(trimmed);
if (isNaN(val) || val < 0.1 || val > 5.0)
return;
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrBrightness", parseFloat(val.toFixed(2)));
}
}
}
Column {
width: (parent.width - Theme.spacingM) / 2
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("SDR Saturation")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
width: parent.width
height: 40
placeholderText: "0.5 - 1.5"
text: {
DisplayConfigState.pendingHyprlandChanges;
const val = DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", null);
return val !== null ? val.toString() : "";
}
onEditingFinished: {
const trimmed = text.trim();
if (!trimmed) {
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", null);
return;
}
const val = parseFloat(trimmed);
if (isNaN(val) || val < 0.0 || val > 3.0)
return;
DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "sdrSaturation", parseFloat(val.toFixed(2)));
}
}
}
}
}
}
}
}

View File

@@ -263,7 +263,7 @@ StyledRect {
case "niri":
return "NiriOutputSettings.qml";
case "hyprland":
return "";
return "HyprlandOutputSettings.qml";
default:
return "";
}