From 5839a5de30f5ca23f6cfce3b58f68b4ae0005d57 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 11 Feb 2026 09:31:35 -0500 Subject: [PATCH] displays: add full screen only for hyprland and convert vrr to dropdown fixes #1649 fixes #1548 --- .../DisplayConfig/DisplayConfigState.qml | 66 +++++++++++++++++-- .../Settings/DisplayConfig/OutputCard.qml | 51 ++++++++++++-- quickshell/Services/HyprlandService.qml | 6 +- quickshell/Services/NiriService.qml | 9 ++- 4 files changed, 115 insertions(+), 17 deletions(-) diff --git a/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml b/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml index 8ea1671e..f1606f88 100644 --- a/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml +++ b/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml @@ -391,8 +391,12 @@ Singleton { const filtered = filterDisconnectedOnly(parsed); savedOutputs = filtered; - if (CompositorService.isHyprland) + if (CompositorService.isHyprland) { initHyprlandSettingsFromConfig(parsed); + syncHyprlandVrrFromConfig(parsed); + } + if (CompositorService.isNiri) + syncNiriVrrFromConfig(parsed); }); } @@ -431,6 +435,44 @@ Singleton { } } + function syncHyprlandVrrFromConfig(parsedOutputs) { + const current = JSON.parse(JSON.stringify(SettingsData.hyprlandOutputSettings)); + let changed = false; + for (const outputName in parsedOutputs) { + const settings = parsedOutputs[outputName]?.hyprlandSettings; + const fromConfig = settings?.vrrFullscreenOnly ?? false; + const stored = current[outputName]?.vrrFullscreenOnly ?? false; + if (fromConfig === stored) + continue; + if (!current[outputName]) + current[outputName] = {}; + if (fromConfig) + current[outputName].vrrFullscreenOnly = true; + else + delete current[outputName].vrrFullscreenOnly; + changed = true; + } + if (changed) { + SettingsData.hyprlandOutputSettings = current; + SettingsData.saveSettings(); + } + } + + function syncNiriVrrFromConfig(parsedOutputs) { + let changed = false; + for (const outputName in parsedOutputs) { + const output = parsedOutputs[outputName]; + const current = SettingsData.getNiriOutputSetting(outputName, "vrrOnDemand", false); + const fromConfig = output.vrr_on_demand ?? false; + if (current === fromConfig) + continue; + SettingsData.setNiriOutputSetting(outputName, "vrrOnDemand", fromConfig || undefined); + changed = true; + } + if (changed) + SettingsData.saveSettings(); + } + function filterDisconnectedOnly(parsedOutputs) { const result = {}; const liveNames = Object.keys(outputs); @@ -479,7 +521,8 @@ Singleton { const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/); const scaleMatch = body.match(/scale\s+([\d.]+)/); const transformMatch = body.match(/transform\s+"([^"]+)"/); - const vrrMatch = body.match(/variable-refresh-rate(?:\s+on)?/); + const vrrMatch = body.match(/variable-refresh-rate/); + const vrrOnDemandMatch = body.match(/variable-refresh-rate\s+on-demand=true/); result[name] = { "name": name, @@ -498,6 +541,7 @@ Singleton { ] : [], "current_mode": 0, "vrr_enabled": !!vrrMatch, + "vrr_on_demand": !!vrrOnDemandMatch, "vrr_supported": true }; } @@ -535,7 +579,7 @@ Singleton { 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 transform = 0, vrrMode = 0, bitdepth = undefined, cm = undefined; let sdrBrightness = undefined, sdrSaturation = undefined; const transformMatch = rest.match(/,\s*transform,\s*(\d+)/); @@ -544,7 +588,7 @@ Singleton { const vrrMatch = rest.match(/,\s*vrr,\s*(\d+)/); if (vrrMatch) - vrr = vrrMatch[1] === "1"; + vrrMode = parseInt(vrrMatch[1]); const bitdepthMatch = rest.match(/,\s*bitdepth,\s*(\d+)/); if (bitdepthMatch) @@ -583,13 +627,14 @@ Singleton { } ], "current_mode": 0, - "vrr_enabled": vrr, + "vrr_enabled": vrrMode >= 1, "vrr_supported": true, "hyprlandSettings": { "bitdepth": bitdepth, "colorManagement": cm, "sdrBrightness": sdrBrightness, - "sdrSaturation": sdrSaturation + "sdrSaturation": sdrSaturation, + "vrrFullscreenOnly": vrrMode === 2 ? true : undefined }, "mirror": mirror }; @@ -1205,6 +1250,8 @@ Singleton { 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 (changes.vrrFullscreenOnly !== undefined) + changeDescriptions.push(outputId + ": " + I18n.tr("VRR Fullscreen Only") + " → " + (changes.vrrFullscreenOnly ? I18n.tr("Enabled") : I18n.tr("Disabled"))); } if (CompositorService.isNiri) { @@ -1309,7 +1356,12 @@ Singleton { function generateNiriOutputsKdl(outputsData, niriSettings) { let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`; - for (const outputName in outputsData) { + const sortedNames = Object.keys(outputsData).sort((a, b) => { + const la = outputsData[a].logical || {}; + const lb = outputsData[b].logical || {}; + return (la.x ?? 0) - (lb.x ?? 0) || (la.y ?? 0) - (lb.y ?? 0); + }); + for (const outputName of sortedNames) { const output = outputsData[outputName]; const identifier = getNiriOutputIdentifier(output, outputName); const settings = niriSettings[identifier] || {}; diff --git a/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml b/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml index 20a02d1d..91cfeac9 100644 --- a/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml +++ b/quickshell/Modules/Settings/DisplayConfig/OutputCard.qml @@ -251,7 +251,7 @@ StyledRect { DankToggle { width: parent.width text: I18n.tr("Variable Refresh Rate") - visible: root.isConnected && !CompositorService.isDwl && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) + visible: root.isConnected && !CompositorService.isDwl && !CompositorService.isHyprland && !CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) checked: { const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr"); if (pendingVrr !== undefined) @@ -261,13 +261,52 @@ StyledRect { onToggled: checked => DisplayConfigState.setPendingChange(root.outputName, "vrr", checked) } - DankToggle { + DankDropdown { width: parent.width - text: I18n.tr("VRR On-Demand") - description: I18n.tr("VRR activates only when applications request it") + text: I18n.tr("Variable Refresh Rate") + addHorizontalPadding: true + visible: root.isConnected && CompositorService.isHyprland && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) + options: [I18n.tr("Off"), I18n.tr("On"), I18n.tr("Fullscreen Only")] + currentValue: { + DisplayConfigState.pendingHyprlandChanges; + if (DisplayConfigState.getHyprlandSetting(root.outputData, root.outputName, "vrrFullscreenOnly", false)) + return I18n.tr("Fullscreen Only"); + const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr"); + const vrrEnabled = pendingVrr !== undefined ? pendingVrr : (DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false); + if (vrrEnabled) + return I18n.tr("On"); + return I18n.tr("Off"); + } + onValueChanged: value => { + const off = I18n.tr("Off"); + const fullscreen = I18n.tr("Fullscreen Only"); + DisplayConfigState.setPendingChange(root.outputName, "vrr", value !== off); + DisplayConfigState.setHyprlandSetting(root.outputData, root.outputName, "vrrFullscreenOnly", value === fullscreen || null); + } + } + + DankDropdown { + width: parent.width + text: I18n.tr("Variable Refresh Rate") + addHorizontalPadding: true visible: root.isConnected && CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) - checked: DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "vrrOnDemand", false) - onToggled: checked => DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "vrrOnDemand", checked) + options: [I18n.tr("Off"), I18n.tr("On"), I18n.tr("On-Demand")] + currentValue: { + DisplayConfigState.pendingNiriChanges; + if (DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "vrrOnDemand", false)) + return I18n.tr("On-Demand"); + const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr"); + const vrrEnabled = pendingVrr !== undefined ? pendingVrr : (DisplayConfigState.outputs[root.outputName]?.vrr_enabled ?? false); + if (!vrrEnabled) + return I18n.tr("Off"); + return I18n.tr("On"); + } + onValueChanged: value => { + const off = I18n.tr("Off"); + const onDemand = I18n.tr("On-Demand"); + DisplayConfigState.setPendingChange(root.outputName, "vrr", value !== off); + DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "vrrOnDemand", value === onDemand || null); + } } Rectangle { diff --git a/quickshell/Services/HyprlandService.qml b/quickshell/Services/HyprlandService.qml index 5ccd0ba7..a3d076de 100644 --- a/quickshell/Services/HyprlandService.qml +++ b/quickshell/Services/HyprlandService.qml @@ -98,8 +98,10 @@ Singleton { if (transform !== 0) monitorLine += ", transform, " + transform; - if (output.vrr_supported) - monitorLine += ", vrr, " + (output.vrr_enabled ? "1" : "0"); + if (output.vrr_supported) { + const vrrMode = outputSettings.vrrFullscreenOnly ? 2 : (output.vrr_enabled ? 1 : 0); + monitorLine += ", vrr, " + vrrMode; + } if (output.mirror && output.mirror.length > 0) monitorLine += ", mirror, " + output.mirror; diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index 3b1e84eb..8a90f123 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -1359,7 +1359,12 @@ Singleton { return; let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`; - for (const outputName in data) { + const sortedNames = Object.keys(data).sort((a, b) => { + const la = data[a].logical || {}; + const lb = data[b].logical || {}; + return (la.x ?? 0) - (lb.x ?? 0) || (la.y ?? 0) - (lb.y ?? 0); + }); + for (const outputName of sortedNames) { const output = data[outputName]; const identifier = getOutputIdentifier(output, outputName); const niriSettings = SettingsData.getNiriOutputSettings(identifier); @@ -1397,7 +1402,7 @@ Singleton { } } - if (output.vrr_enabled) { + if (output.vrr_enabled || niriSettings.vrrOnDemand) { const vrrOnDemand = niriSettings.vrrOnDemand ?? false; kdlContent += vrrOnDemand ? ` variable-refresh-rate on-demand=true\n` : ` variable-refresh-rate\n`; }