From d97fe8be9daba1ff43fb09ee7ee051439a60daf1 Mon Sep 17 00:00:00 2001 From: purian23 Date: Fri, 30 Oct 2026 22:51:16 -0400 Subject: [PATCH] hyprland: write desc: identifiers when auto-applying monitor profiles - monitors.json keys hyprland outputs by bare "Make Model", so hl.monitor ignored every rule we wrote on replug or login. Resolved by the live desc: string at write time instead Fixes #4044 Port 2.0 (cherry picked from commit d76475d4e96ab5e0a6c2a1106fa6b91a227869cc) --- quickshell/Common/OutputModel.js | 7 +++++++ quickshell/Services/HyprlandService.qml | 8 ++++---- quickshell/tests/hyprland-lua.test.mjs | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/quickshell/Common/OutputModel.js b/quickshell/Common/OutputModel.js index 81bde4b49..f80eb11e5 100644 --- a/quickshell/Common/OutputModel.js +++ b/quickshell/Common/OutputModel.js @@ -46,6 +46,13 @@ function hyprlandIdentifier(output, name, displayNameMode) { return ("desc:" + [output.make, output.model, output.serial].filter(p => p).join(" ")).replace(/,/g, ""); } +// monitors.json keys Hyprland profiles by bare "Make Model", which hl.monitor ignores; only a connector or desc: matches +function hyprlandProfileOutputIdentifier(output, key, isConnector) { + if (isConnector || key.startsWith("desc:") || !output?.make || !output?.model) + return key; + return hyprlandIdentifier(output, key, "model"); +} + function modelIdentifier(output) { return output.make + " " + output.model; } diff --git a/quickshell/Services/HyprlandService.qml b/quickshell/Services/HyprlandService.qml index a4cb9df5a..e0b274e05 100644 --- a/quickshell/Services/HyprlandService.qml +++ b/quickshell/Services/HyprlandService.qml @@ -165,7 +165,7 @@ Singleton { function getOutputIdentifier(output, outputName) { if (output.explicitIdentifier) - return outputName; + return OutputModel.hyprlandProfileOutputIdentifier(output, outputName, !!liveMonitor(outputName)); return OutputModel.hyprlandIdentifier(output, outputName, SettingsData.displayNameMode); } @@ -332,7 +332,7 @@ Singleton { if (!(output.make && output.model) && liveIdentifiers[identifier.trim()]) continue; - const outputSettings = settings[identifier] || {}; + const outputSettings = settings[identifier] || settings[outputName] || {}; if (outputSettings.disabled) { lines.push(`hl.monitor({ output = ${luaQuoted(identifier)}, disabled = true })`); @@ -346,7 +346,7 @@ Singleton { resolution = mode.width + "x" + mode.height + "@" + (mode.refresh_rate / 1000).toFixed(3); } - const geometry = OutputModel.hyprlandLuaGeometry(output, saved[identifier], liveMonitor(outputName)); + const geometry = OutputModel.hyprlandLuaGeometry(output, saved[identifier] ?? saved[outputName], liveMonitor(outputName)); const scale = geometry.scale === "auto" ? luaQuoted("auto") : geometry.scale; const parts = [`output = ${luaQuoted(identifier)}`, `mode = ${luaQuoted(resolution)}`, `position = ${luaQuoted(geometry.position)}`, `scale = ${scale}`]; @@ -354,7 +354,7 @@ Singleton { if (transform !== 0) parts.push(`transform = ${transform}`); - const vrrMode = OutputModel.hyprlandVrrMode(outputSettings, saved[identifier]); + const vrrMode = OutputModel.hyprlandVrrMode(outputSettings, saved[identifier] ?? saved[outputName]); if (vrrMode !== undefined) parts.push(`vrr = ${vrrMode}`); diff --git a/quickshell/tests/hyprland-lua.test.mjs b/quickshell/tests/hyprland-lua.test.mjs index e785d491f..6c93e09d9 100644 --- a/quickshell/tests/hyprland-lua.test.mjs +++ b/quickshell/tests/hyprland-lua.test.mjs @@ -85,6 +85,20 @@ test("outputs: auto position and scale survive a rewrite until the written value assert.ok(disconnected.includes('position = "auto-right", scale = "auto"'), disconnected); }); +test("outputs: a monitors.json profile key is written as desc: so Hyprland matches it, settings still resolve by the key, connector and desc: keys pass through", () => { + live["eDP-1"] = { x: 0, y: 0, scale: 1.25 }; + const profile = (key, make, model, serial) => ({ ...connected(key, 1920, 0, 1.5), make, model, serial, explicitIdentifier: true, configured_mode: "3840x2160@60.000" }); + const outputsData = { + "Dell Inc. DELL U2720Q": profile("Dell Inc. DELL U2720Q", "Dell Inc.", "DELL U2720Q", "ABC123"), + "desc:LG Electronics LG ULTRAGEAR": profile("desc:LG Electronics LG ULTRAGEAR", "LG Electronics", "LG ULTRAGEAR", "0x0001"), + "eDP-1": { ...connected("eDP-1", 0, 0, 1.25), make: "BOE", model: "0x0A1B", explicitIdentifier: true } + }; + const content = scope.buildOutputsLua(outputsData, { "Dell Inc. DELL U2720Q": { bitdepth: 10 } }, saved); + const names = content.split("\n").map(line => line.match(/output = "([^"]*)"/)?.[1]).filter(name => name); + assert.deepEqual(names, ["desc:Dell Inc. DELL U2720Q ABC123", "desc:LG Electronics LG ULTRAGEAR", "eDP-1"]); + assert.ok(monitorLine(content, "desc:Dell Inc. DELL U2720Q ABC123").includes("bitdepth = 10"), content); +}); + test("outputs: vrr is written only for a chosen mode, so untouched monitors inherit misc:vrr", () => { const vrrFile = OutputModel.parseHyprlandOutputs([ 'hl.monitor({ output = "eDP-1", mode = "2400x1600@120.000", position = "0x0", scale = 1.25, vrr = 0 })',