diff --git a/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml b/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml index 7e59185f..bd976700 100644 --- a/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml +++ b/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml @@ -445,7 +445,7 @@ Singleton { return result; } - // Extract niri settings map from neutral config entry for generateNiriOutputsKdl + // Extract niri settings map from a neutral config entry. function getNiriSettingsFromConfig(configEntry) { const result = {}; for (const outputId in (configEntry.outputs || {})) { @@ -473,6 +473,28 @@ Singleton { return result; } + function backendSettingsFromConfig(configEntry) { + switch (CompositorService.compositor) { + case "niri": + return getNiriSettingsFromConfig(configEntry); + case "hyprland": + return getHyprlandSettingsFromConfig(configEntry); + default: + return null; + } + } + + function backendMergedSettings() { + switch (CompositorService.compositor) { + case "niri": + return buildMergedNiriSettings(); + case "hyprland": + return buildMergedHyprlandSettings(); + default: + return null; + } + } + function ensureEnabledOutput(configEntry) { const outputKeys = Object.keys(configEntry.outputs || {}); if (outputKeys.length === 0) @@ -518,51 +540,12 @@ Singleton { WlrOutputService.requestState(); }; - switch (CompositorService.compositor) { - case "niri": - { - const paths = getConfigPaths(); - if (!paths) { - onWriteFailed(); - return; - } - const configContent = generateNiriOutputsKdl(outputsData, getNiriSettingsFromConfig(configEntry)); - Proc.runCommand("apply-config-write", ["sh", "-c", `mkdir -p "$(dirname "${paths.outputsFile}")" && cat > "${paths.outputsFile}" << 'EOF'\n${configContent}EOF`], (output, exitCode) => { - if (exitCode !== 0) { - onWriteFailed(); - return; - } - onWriteSuccess(); - }); - break; - } - case "hyprland": - HyprlandService.generateOutputsConfig(outputsData, getHyprlandSettingsFromConfig(configEntry), success => { - if (success) - onWriteSuccess(); - else - onWriteFailed(); - }); - break; - case "mango": - MangoService.generateOutputsConfig(outputsData, success => { - if (success) - onWriteSuccess(); - else - onWriteFailed(); - }); - break; - case "dwl": - DwlService.generateOutputsConfig(outputsData, success => { - if (success) - onWriteSuccess(); - else - onWriteFailed(); - }); - break; - default: - onWriteFailed(); - } + backendWriteOutputsConfig(outputsData, backendSettingsFromConfig(configEntry), success => { + if (success) + onWriteSuccess(); + else + onWriteFailed(); + }); } // ── Profile management ───────────────────────────────────────────────── @@ -867,8 +850,7 @@ Singleton { Component.onCompleted: { outputs = buildOutputsMap(); reloadSavedOutputs(); - if (CompositorService.isHyprland) - checkIncludeStatus(); + checkIncludeStatus(); } function reloadSavedOutputs() { @@ -1466,12 +1448,12 @@ Singleton { } const liveOutputs = buildOutputsMap(); - if (CompositorService.isHyprland && Object.keys(liveOutputs).length > 0) { + if (Object.keys(liveOutputs).length > 0) { outputs = liveOutputs; - HyprlandService.generateOutputsConfig(liveOutputs, SettingsData.hyprlandOutputSettings, success => { + backendWriteOutputsConfig(liveOutputs, backendMergedSettings(), success => { fixingInclude = false; if (!success) - ToastService.showError(I18n.tr("Display setup failed"), I18n.tr("Failed to write Hyprland outputs config."), "", "display-config"); + ToastService.showError(I18n.tr("Display setup failed"), I18n.tr("Failed to write outputs config."), "", "display-config"); checkIncludeStatus(); WlrOutputService.requestState(); }); @@ -1570,31 +1552,153 @@ Singleton { WlrOutputService.requestState(); } - function backendWriteOutputsConfig(outputsData) { + function backendWriteOutputsConfig(outputsData, settingsOrCallback, maybeCallback) { + const settings = typeof settingsOrCallback === "function" ? null : settingsOrCallback; + const callback = typeof settingsOrCallback === "function" ? settingsOrCallback : maybeCallback; + const hasExplicitSettings = settings !== null && settings !== undefined; + + function finish(success) { + if (callback) + callback(success); + } + switch (CompositorService.compositor) { case "niri": - NiriService.generateOutputsConfig(outputsData); - break; - case "hyprland": - if (readOnly) { - showHyprlandReadOnlyWarning(); - return false; + { + const niriSettings = hasExplicitSettings ? settings : buildMergedNiriSettings(); + NiriService.generateOutputsConfig(outputsData, niriSettings, success => { + if (!success) { + finish(false); + return; + } + reloadAndApplyNiriLiveOutputsConfig(outputsData, niriSettings, finish); + }); + break; + } + case "hyprland": + { + if (readOnly) { + showHyprlandReadOnlyWarning(); + finish(false); + return false; + } + const hyprlandSettings = hasExplicitSettings ? settings : buildMergedHyprlandSettings(); + HyprlandService.generateOutputsConfig(outputsData, hyprlandSettings, finish); + break; } - HyprlandService.generateOutputsConfig(outputsData, buildMergedHyprlandSettings()); - break; case "mango": - MangoService.generateOutputsConfig(outputsData); + MangoService.generateOutputsConfig(outputsData, finish); break; case "dwl": - DwlService.generateOutputsConfig(outputsData); + DwlService.generateOutputsConfig(outputsData, finish); break; default: WlrOutputService.applyOutputsConfig(outputsData, outputs); + finish(true); break; } return true; } + function niriTransformArg(transform) { + switch (transform) { + case "90": + return "90"; + case "180": + return "180"; + case "270": + return "270"; + case "Flipped": + return "flipped"; + case "Flipped90": + return "flipped-90"; + case "Flipped180": + return "flipped-180"; + case "Flipped270": + return "flipped-270"; + default: + return "normal"; + } + } + + function getLiveNiriOutputName(outputName, outputData) { + if (outputs[outputName]) + return outputName; + const targetId = getNiriOutputIdentifier(outputData, outputName); + for (const liveName in outputs) { + if (getNiriOutputIdentifier(outputs[liveName], liveName) === targetId) + return liveName; + } + return ""; + } + + function applyNiriLiveOutputsConfig(outputsData, niriSettings, callback) { + const names = Object.keys(outputsData || {}); + let pending = 0; + let failed = false; + + function done(success) { + if (callback) + callback(success); + } + + for (const outputName of names) { + const output = outputsData[outputName]; + if (!output) + continue; + const liveName = getLiveNiriOutputName(outputName, output); + if (!liveName) + continue; + + const identifier = getNiriOutputIdentifier(output, outputName); + const settings = niriSettings?.[outputName] || niriSettings?.[identifier] || {}; + const config = {}; + + if (settings.disabled === true) + config.disabled = true; + else if (settings.disabled === false) + config.disabled = false; + + if (!config.disabled) { + if (output.current_mode !== undefined && output.modes && output.modes[output.current_mode]) { + const mode = output.modes[output.current_mode]; + config.mode = mode.width + "x" + mode.height + "@" + (mode.refresh_rate / 1000).toFixed(3); + } + if (output.logical) { + config.scale = output.logical.scale ?? 1.0; + config.position = { + "x": output.logical.x ?? 0, + "y": output.logical.y ?? 0 + }; + config.transform = niriTransformArg(output.logical.transform); + } + if (settings.vrrOnDemand !== undefined) + config.vrrOnDemand = settings.vrrOnDemand; + else if (output.vrr_enabled !== undefined) + config.vrr = output.vrr_enabled; + } + + pending++; + NiriService.applyOutputConfig(liveName, config, success => { + failed = failed || !success; + pending--; + if (pending === 0) { + WlrOutputService.requestState(); + done(!failed); + } + }); + } + + if (pending === 0) + done(true); + } + + function reloadAndApplyNiriLiveOutputsConfig(outputsData, niriSettings, callback) { + Proc.runCommand("niri-reload-output-config", ["niri", "msg", "action", "load-config-file"], () => { + applyNiriLiveOutputsConfig(outputsData, niriSettings, callback); + }); + } + function normalizeOutputPositions(outputsData) { const names = Object.keys(outputsData); if (names.length === 0) @@ -1982,7 +2086,7 @@ Singleton { const mergedOutputs = buildOutputsWithPendingChanges(); const mergedNiriSettings = buildMergedNiriSettings(); - const configContent = generateNiriOutputsKdl(mergedOutputs, mergedNiriSettings); + const configContent = NiriService.buildOutputsConfig(mergedOutputs, mergedNiriSettings); const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); const tempFile = configDir + "/niri/dms/.outputs-validate-tmp.kdl"; @@ -2006,7 +2110,7 @@ Singleton { if (formatChanged) SettingsData.saveSettings(); commitNiriSettingsChanges(); - backendWriteOutputsConfig(mergedOutputs); + backendWriteOutputsConfig(mergedOutputs, mergedNiriSettings); }); }); } @@ -2083,108 +2187,6 @@ Singleton { } } - function generateNiriOutputsKdl(outputsData, niriSettings) { - let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`; - 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] || {}; - kdlContent += `output "${identifier}" {\n`; - if (settings.disabled) { - kdlContent += ` off\n}\n\n`; - continue; - } - 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) { - kdlContent += ` scale ${output.logical.scale ?? 1.0}\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 (settings.vrrOnDemand) { - kdlContent += ` variable-refresh-rate on-demand=true\n`; - } else if (output.vrr_enabled) { - kdlContent += ` variable-refresh-rate\n`; - } - if (settings.focusAtStartup) - kdlContent += ` focus-at-startup\n`; - if (settings.backdropColor) - kdlContent += ` backdrop-color "${settings.backdropColor}"\n`; - kdlContent += generateHotCornersBlock(settings); - kdlContent += generateLayoutBlock(settings); - kdlContent += `}\n\n`; - } - return kdlContent; - } - - function generateHotCornersBlock(settings) { - if (!settings.hotCorners) - return ""; - const hc = settings.hotCorners; - if (hc.off) - return ` hot-corners {\n off\n }\n`; - const corners = hc.corners || []; - if (corners.length === 0) - return ""; - let block = ` hot-corners {\n`; - for (const corner of corners) - block += ` ${corner}\n`; - block += ` }\n`; - return block; - } - - function generateLayoutBlock(settings) { - if (!settings.layout) - return ""; - const layout = settings.layout; - const hasSettings = layout.gaps !== undefined || layout.defaultColumnWidth || layout.presetColumnWidths || layout.alwaysCenterSingleColumn !== undefined; - if (!hasSettings) - return ""; - let block = ` layout {\n`; - if (layout.gaps !== undefined) - block += ` gaps ${layout.gaps}\n`; - if (layout.defaultColumnWidth?.type === "proportion") { - const val = layout.defaultColumnWidth.value; - const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString(); - block += ` default-column-width { proportion ${formatted}; }\n`; - } - if (layout.presetColumnWidths && layout.presetColumnWidths.length > 0) { - block += ` preset-column-widths {\n`; - for (const preset of layout.presetColumnWidths) { - if (preset.type === "proportion") { - const val = preset.value; - const formatted = Number.isInteger(val) ? val.toFixed(1) : val.toString(); - block += ` proportion ${formatted}\n`; - } - } - block += ` }\n`; - } - if (layout.alwaysCenterSingleColumn !== undefined) - block += layout.alwaysCenterSingleColumn ? ` always-center-single-column\n` : ` always-center-single-column false\n`; - block += ` }\n`; - return block; - } - function confirmChanges(profileId) { const outputConfigs = buildCurrentOutputConfigs(); lastAppliedEntry = { diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index c717ec86..7e3ef34a 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -1248,15 +1248,36 @@ Singleton { const commands = []; + if (config.disabled !== undefined) { + commands.push(`niri msg output "${outputName}" ${config.disabled ? "off" : "on"}`); + if (config.disabled) { + const fullDisableCommand = "{ " + commands.join(" && ") + "; } 2>&1"; + Proc.runCommand("niri-output-config", ["sh", "-c", fullDisableCommand], (output, exitCode) => { + if (exitCode !== 0) { + log.warn("Failed to apply output config:", outputName, "exit:", exitCode, output); + if (callback) + callback(false, output); + return; + } + fetchOutputs(); + if (callback) + callback(true, "Success"); + }); + return; + } + } + if (config.position !== undefined) { - commands.push(`niri msg output "${outputName}" position ${config.position.x} ${config.position.y}`); + commands.push(`niri msg output "${outputName}" position set ${config.position.x} ${config.position.y}`); } if (config.mode !== undefined) { commands.push(`niri msg output "${outputName}" mode ${config.mode}`); } - if (config.vrr !== undefined) { + if (config.vrrOnDemand !== undefined) { + commands.push(`niri msg output "${outputName}" vrr --on-demand ${config.vrrOnDemand ? "on" : "off"}`); + } else if (config.vrr !== undefined) { commands.push(`niri msg output "${outputName}" vrr ${config.vrr ? "on" : "off"}`); } @@ -1274,10 +1295,10 @@ Singleton { return; } - const fullCommand = commands.join(" && "); + const fullCommand = "{ " + commands.join(" && ") + "; } 2>&1"; Proc.runCommand("niri-output-config", ["sh", "-c", fullCommand], (output, exitCode) => { if (exitCode !== 0) { - log.warn("Failed to apply output config:", output); + log.warn("Failed to apply output config:", outputName, "exit:", exitCode, output); if (callback) callback(false, output); return; @@ -1297,10 +1318,32 @@ Singleton { return outputName; } - function generateOutputsConfig(outputsData) { + function outputSettingsFor(output, outputName, niriSettings) { + const identifier = getOutputIdentifier(output, outputName); + if (niriSettings) + return niriSettings[identifier] || niriSettings[outputName] || {}; + return SettingsData.getNiriOutputSettings(identifier); + } + + function transformToNiri(transform) { + const transformMap = { + "Normal": "normal", + "90": "90", + "180": "180", + "270": "270", + "Flipped": "flipped", + "Flipped90": "flipped-90", + "Flipped180": "flipped-180", + "Flipped270": "flipped-270" + }; + return transformMap[transform] || "normal"; + } + + function buildOutputsConfig(outputsData, niriSettings) { const data = outputsData || outputs; if (!data || Object.keys(data).length === 0) - return; + return ""; + let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`; const sortedNames = Object.keys(data).sort((a, b) => { @@ -1311,11 +1354,11 @@ Singleton { for (const outputName of sortedNames) { const output = data[outputName]; const identifier = getOutputIdentifier(output, outputName); - const niriSettings = SettingsData.getNiriOutputSettings(identifier); + const outputSettings = outputSettingsFor(output, outputName, niriSettings); kdlContent += `output "${identifier}" {\n`; - if (niriSettings.disabled) { + if (outputSettings.disabled) { kdlContent += ` off\n`; kdlContent += `}\n\n`; continue; @@ -1330,17 +1373,7 @@ Singleton { kdlContent += ` scale ${output.logical.scale || 1.0}\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`; + kdlContent += ` transform "${transformToNiri(output.logical.transform)}"\n`; } if (output.logical.x !== undefined && output.logical.y !== undefined) { @@ -1348,25 +1381,38 @@ Singleton { } } - if (output.vrr_enabled || niriSettings.vrrOnDemand) { - const vrrOnDemand = niriSettings.vrrOnDemand ?? false; + if (output.vrr_enabled || outputSettings.vrrOnDemand) { + const vrrOnDemand = outputSettings.vrrOnDemand ?? false; kdlContent += vrrOnDemand ? ` variable-refresh-rate on-demand=true\n` : ` variable-refresh-rate\n`; } - if (niriSettings.focusAtStartup) { + if (outputSettings.focusAtStartup) { kdlContent += ` focus-at-startup\n`; } - if (niriSettings.backdropColor) { - kdlContent += ` backdrop-color "${niriSettings.backdropColor}"\n`; + if (outputSettings.backdropColor) { + kdlContent += ` backdrop-color "${outputSettings.backdropColor}"\n`; } - kdlContent += generateHotCornersBlock(niriSettings); - kdlContent += generateLayoutBlock(niriSettings); + kdlContent += generateHotCornersBlock(outputSettings); + kdlContent += generateLayoutBlock(outputSettings); kdlContent += `}\n\n`; } + return kdlContent; + } + + function generateOutputsConfig(outputsData, settingsOrCallback, maybeCallback) { + const niriSettings = typeof settingsOrCallback === "function" ? null : settingsOrCallback; + const callback = typeof settingsOrCallback === "function" ? settingsOrCallback : maybeCallback; + const kdlContent = buildOutputsConfig(outputsData, niriSettings); + if (!kdlContent) { + if (callback) + callback(false); + return; + } + const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); const niriDmsDir = configDir + "/niri/dms"; const outputsPath = niriDmsDir + "/outputs.kdl"; @@ -1374,9 +1420,13 @@ Singleton { Proc.runCommand("niri-write-outputs", ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${kdlContent}EOF`], (output, exitCode) => { if (exitCode !== 0) { log.warn("Failed to write outputs config:", output); + if (callback) + callback(false, output); return; } log.info("Generated outputs config at", outputsPath); + if (callback) + callback(true, ""); }); }