summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml1837
1 files changed, 0 insertions, 1837 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml
deleted file mode 100644
index b6e253b..0000000
--- a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Settings/DisplayConfig/DisplayConfigState.qml
+++ /dev/null
@@ -1,1837 +0,0 @@
-pragma Singleton
-pragma ComponentBehavior: Bound
-
-import QtCore
-import QtQuick
-import Quickshell
-import qs.Common
-import qs.Services
-
-Singleton {
- id: root
-
- readonly property bool hasOutputBackend: WlrOutputService.wlrOutputAvailable
- readonly property var wlrOutputs: WlrOutputService.outputs
- property var outputs: ({})
- property var savedOutputs: ({})
- property var allOutputs: buildAllOutputsMap()
-
- property var includeStatus: ({
- "exists": false,
- "included": false
- })
- property bool checkingInclude: false
- property bool fixingInclude: false
-
- 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 || Object.keys(pendingHyprlandChanges).length > 0 || formatChanged
-
- property bool validatingConfig: false
- property string validationError: ""
-
- property var currentOutputSet: []
- property string matchedProfile: ""
- property bool profilesLoading: false
- property var validatedProfiles: ({})
- property bool manualActivation: false
-
- signal changesApplied(var changeDescriptions)
- signal changesConfirmed
- signal changesReverted
- signal profileActivated(string profileId, string profileName)
- signal profileSaved(string profileId, string profileName)
- signal profileDeleted(string profileId)
- signal profileError(string message)
-
- function buildCurrentOutputSet() {
- const connected = [];
- for (const name in outputs) {
- const output = outputs[name];
- connected.push(getOutputIdentifier(output, name));
- }
- return connected.sort();
- }
-
- function getOutputIdentifier(output, outputName) {
- if (SettingsData.displayNameMode === "model" && output?.make && output?.model) {
- if (CompositorService.isNiri) {
- const serial = output.serial || "Unknown";
- return output.make + " " + output.model + " " + serial;
- }
- return output.make + " " + output.model;
- }
- return outputName;
- }
-
- function validateProfiles() {
- const compositor = CompositorService.compositor;
- const profiles = SettingsData.getDisplayProfiles(compositor);
- const profilesDir = getProfilesDir();
- const ext = getProfileExtension();
-
- if (!profilesDir) {
- validatedProfiles = {};
- return;
- }
-
- const profileIds = Object.keys(profiles);
- if (profileIds.length === 0) {
- validatedProfiles = {};
- return;
- }
-
- const fileChecks = profileIds.map(id => profilesDir + "/" + id + ext).join(" ");
- Proc.runCommand("validate-profiles", ["sh", "-c", `for f in ${fileChecks}; do [ -f "$f" ] && echo "$f"; done`], (output, exitCode) => {
- const existingFiles = new Set(output.trim().split("\n").filter(f => f));
- const validated = {};
- for (const profileId of profileIds) {
- const profileFile = profilesDir + "/" + profileId + ext;
- if (existingFiles.has(profileFile))
- validated[profileId] = profiles[profileId];
- else
- SettingsData.removeDisplayProfile(compositor, profileId);
- }
- validatedProfiles = validated;
- matchedProfile = findMatchingProfile();
- });
- }
-
- function findMatchingProfile() {
- const profiles = validatedProfiles;
-
- console.log("[Profile Match] Current outputs:", JSON.stringify(currentOutputSet));
-
- let bestMatch = "";
- let bestScore = -1;
- let bestUpdatedAt = 0;
-
- for (const profileId in profiles) {
- const profile = profiles[profileId];
- const profileSet = new Set(profile.outputSet);
-
- console.log("[Profile Match] Checking", profile.name, "outputSet:", JSON.stringify(profile.outputSet));
-
- let allCurrentPresent = true;
- for (const output of currentOutputSet) {
- if (!profileSet.has(output)) {
- console.log("[Profile Match] - Missing output:", output);
- allCurrentPresent = false;
- break;
- }
- }
- if (!allCurrentPresent) {
- console.log("[Profile Match] - SKIP: not all current outputs present");
- continue;
- }
-
- const disconnectedCount = profile.outputSet.length - currentOutputSet.length;
- const score = currentOutputSet.length * 100 - disconnectedCount;
- const updatedAt = profile.updatedAt || profile.createdAt || 0;
- console.log("[Profile Match] - MATCH score:", score, "(disconnected:", disconnectedCount, "updatedAt:", updatedAt + ")");
-
- if (score > bestScore || (score === bestScore && updatedAt > bestUpdatedAt)) {
- bestScore = score;
- bestMatch = profileId;
- bestUpdatedAt = updatedAt;
- }
- }
- console.log("[Profile Match] Best match:", bestMatch, "score:", bestScore);
- return bestMatch;
- }
-
- function getProfilesDir() {
- const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
- switch (CompositorService.compositor) {
- case "niri":
- return configDir + "/niri/dms/profiles";
- case "hyprland":
- return configDir + "/hypr/dms/profiles";
- case "dwl":
- return configDir + "/mango/dms/profiles";
- default:
- return "";
- }
- }
-
- function getProfileExtension() {
- return CompositorService.compositor === "niri" ? ".kdl" : ".conf";
- }
-
- function createProfile(name) {
- const compositor = CompositorService.compositor;
- const profileId = "profile_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6);
- const outputSet = buildCurrentOutputSet();
- const now = Date.now();
-
- const profileData = {
- "id": profileId,
- "name": name,
- "outputSet": outputSet,
- "createdAt": now,
- "updatedAt": now
- };
-
- const profilesDir = getProfilesDir();
- const profileFile = profilesDir + "/" + profileId + getProfileExtension();
- const paths = getConfigPaths();
- if (!paths) {
- profileError(I18n.tr("Compositor not supported"));
- return;
- }
-
- profilesLoading = true;
- Proc.runCommand("create-profile-dir", ["mkdir", "-p", profilesDir], (output, exitCode) => {
- if (exitCode !== 0) {
- profilesLoading = false;
- profileError(I18n.tr("Failed to create profiles directory"));
- return;
- }
- Proc.runCommand("copy-profile", ["cp", "-L", paths.outputsFile, profileFile], (output2, exitCode2) => {
- if (exitCode2 !== 0) {
- profilesLoading = false;
- profileError(I18n.tr("Failed to save profile file"));
- return;
- }
- SettingsData.setDisplayProfile(compositor, profileId, profileData);
- SettingsData.setActiveDisplayProfile(compositor, profileId);
- const updated = JSON.parse(JSON.stringify(validatedProfiles));
- updated[profileId] = profileData;
- validatedProfiles = updated;
- Proc.runCommand("link-new-profile", ["ln", "-sf", profileFile, paths.outputsFile], () => {
- profilesLoading = false;
- currentOutputSet = outputSet;
- matchedProfile = profileId;
- profileSaved(profileId, name);
- });
- });
- });
- }
-
- function renameProfile(profileId, newName) {
- const compositor = CompositorService.compositor;
- const profiles = SettingsData.getDisplayProfiles(compositor);
- const profile = profiles[profileId];
- if (!profile) {
- profileError(I18n.tr("Profile not found"));
- return;
- }
- profile.name = newName;
- profile.updatedAt = Date.now();
- SettingsData.setDisplayProfile(compositor, profileId, profile);
- }
-
- function deleteProfile(profileId) {
- const compositor = CompositorService.compositor;
- const profilesDir = getProfilesDir();
- const profileFile = profilesDir + "/" + profileId + getProfileExtension();
- const isActive = SettingsData.getActiveDisplayProfile(compositor) === profileId;
-
- profilesLoading = true;
- Proc.runCommand("delete-profile", ["rm", "-f", profileFile], (output, exitCode) => {
- profilesLoading = false;
- SettingsData.removeDisplayProfile(compositor, profileId);
- if (isActive) {
- SettingsData.setActiveDisplayProfile(compositor, "");
- backendWriteOutputsConfig(allOutputs);
- }
- const updated = JSON.parse(JSON.stringify(validatedProfiles));
- delete updated[profileId];
- validatedProfiles = updated;
- matchedProfile = findMatchingProfile();
- profileDeleted(profileId);
- });
- }
-
- function activateProfile(profileId) {
- const compositor = CompositorService.compositor;
- const profiles = SettingsData.getDisplayProfiles(compositor);
- const profile = profiles[profileId];
- if (!profile) {
- profileError(I18n.tr("Profile not found"));
- return;
- }
-
- const profilesDir = getProfilesDir();
- const profileFile = profilesDir + "/" + profileId + getProfileExtension();
- const paths = getConfigPaths();
- if (!paths)
- return;
-
- manualActivation = true;
- profilesLoading = true;
- Proc.runCommand("activate-profile", ["ln", "-sf", profileFile, paths.outputsFile], (output, exitCode) => {
- if (exitCode !== 0) {
- profilesLoading = false;
- manualActivation = false;
- profileError(I18n.tr("Failed to activate profile - file not found"));
- return;
- }
- SettingsData.setActiveDisplayProfile(compositor, profileId);
-
- const reloadCmd = CompositorService.isNiri ? ["niri", "msg", "action", "reload-config-or-panic"] : CompositorService.isHyprland ? ["hyprctl", "reload"] : [];
- if (reloadCmd.length > 0) {
- Proc.runCommand("reload-compositor", reloadCmd, (output2, exitCode2) => {
- profilesLoading = false;
- WlrOutputService.requestState();
- profileActivated(profileId, profile.name);
- manualActivationTimer.restart();
- });
- } else {
- profilesLoading = false;
- profileActivated(profileId, profile.name);
- manualActivationTimer.restart();
- }
- });
- }
-
- Timer {
- id: manualActivationTimer
- interval: 2000
- onTriggered: root.manualActivation = false
- }
-
- Timer {
- id: autoSelectDebounceTimer
- interval: 800
- onTriggered: root.doAutoSelectProfile()
- }
-
- // ! TODO - auto profile switching is buggy on niri and other compositors, might need a longer debounce before updating output configuration idk
- function autoSelectProfile() {
- return; // disabled
- autoSelectDebounceTimer.restart();
- }
-
- function doAutoSelectProfile() {
- return; // disabled
- if (!SettingsData.displayProfileAutoSelect || manualActivation)
- return;
- currentOutputSet = buildCurrentOutputSet();
- const matched = findMatchingProfile();
- matchedProfile = matched;
- if (matched && matched !== SettingsData.getActiveDisplayProfile(CompositorService.compositor))
- activateProfile(matched);
- }
-
- function deleteDisconnectedOutput(outputName) {
- if (outputs[outputName]?.connected)
- return;
-
- const updated = JSON.parse(JSON.stringify(savedOutputs));
- delete updated[outputName];
- savedOutputs = updated;
-
- const mergedOutputs = {};
- for (const name in outputs)
- mergedOutputs[name] = outputs[name];
- for (const name in updated)
- mergedOutputs[name] = updated[name];
-
- backendWriteOutputsConfig(mergedOutputs);
- }
-
- function buildAllOutputsMap() {
- const result = {};
- for (const name in savedOutputs) {
- result[name] = Object.assign({}, savedOutputs[name], {
- "connected": false
- });
- }
- for (const name in outputs) {
- result[name] = Object.assign({}, outputs[name], {
- "connected": true
- });
- }
- return result;
- }
-
- onOutputsChanged: {
- allOutputs = buildAllOutputsMap();
- currentOutputSet = buildCurrentOutputSet();
- matchedProfile = findMatchingProfile();
- // ! TODO - auto profile switching disabled for now
- // if (SettingsData.displayProfileAutoSelect)
- // Qt.callLater(autoSelectProfile);
- }
- onSavedOutputsChanged: allOutputs = buildAllOutputsMap()
-
- Connections {
- target: WlrOutputService
- function onStateChanged() {
- root.outputs = root.buildOutputsMap();
- root.reloadSavedOutputs();
- }
- }
-
- Component.onCompleted: {
- outputs = buildOutputsMap();
- reloadSavedOutputs();
- checkIncludeStatus();
- currentOutputSet = buildCurrentOutputSet();
- validateProfiles();
- }
-
- function reloadSavedOutputs() {
- const paths = getConfigPaths();
- if (!paths) {
- savedOutputs = {};
- return;
- }
-
- Proc.runCommand("load-saved-outputs", ["cat", paths.outputsFile], (content, exitCode) => {
- if (exitCode !== 0 || !content.trim()) {
- savedOutputs = {};
- return;
- }
- const parsed = parseOutputsConfig(content);
- const filtered = filterDisconnectedOnly(parsed);
- savedOutputs = filtered;
-
- if (CompositorService.isHyprland) {
- initHyprlandSettingsFromConfig(parsed);
- syncHyprlandVrrFromConfig(parsed);
- }
- if (CompositorService.isNiri)
- syncNiriVrrFromConfig(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 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);
- const liveByIdentifier = {};
- for (const name of liveNames) {
- const o = outputs[name];
- if (o?.make && o?.model) {
- const serial = o.serial || "Unknown";
- const id = (o.make + " " + o.model + " " + serial).trim();
- liveByIdentifier[id] = true;
- liveByIdentifier[o.make + " " + o.model] = true;
- }
- liveByIdentifier[name] = true;
- }
-
- for (const savedName in parsedOutputs) {
- const trimmed = savedName.trim();
- if (!liveByIdentifier[trimmed])
- result[savedName] = parsedOutputs[savedName];
- }
- return result;
- }
-
- function parseOutputsConfig(content) {
- switch (CompositorService.compositor) {
- case "niri":
- return parseNiriOutputs(content);
- case "hyprland":
- return parseHyprlandOutputs(content);
- case "dwl":
- return parseMangoOutputs(content);
- default:
- return {};
- }
- }
-
- function parseNiriOutputs(content) {
- const result = {};
- const outputRegex = /output\s+"([^"]+)"\s*\{([^}]*)\}/g;
- let match;
- while ((match = outputRegex.exec(content)) !== null) {
- const name = match[1];
- const body = match[2];
-
- const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/);
- 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/);
- const vrrOnDemandMatch = body.match(/variable-refresh-rate\s+on-demand=true/);
-
- result[name] = {
- "name": name,
- "logical": {
- "x": posMatch ? parseInt(posMatch[1]) : 0,
- "y": posMatch ? parseInt(posMatch[2]) : 0,
- "scale": scaleMatch ? parseFloat(scaleMatch[1]) : 1.0,
- "transform": transformMatch ? transformMatch[1] : "Normal"
- },
- "modes": modeMatch ? [
- {
- "width": parseInt(modeMatch[1]),
- "height": parseInt(modeMatch[2]),
- "refresh_rate": Math.round(parseFloat(modeMatch[3]) * 1000)
- }
- ] : [],
- "current_mode": 0,
- "vrr_enabled": !!vrrMatch,
- "vrr_on_demand": !!vrrOnDemandMatch,
- "vrr_supported": true
- };
- }
- return result;
- }
-
- function parseHyprlandOutputs(content) {
- const result = {};
- const lines = content.split("\n");
- for (const line of lines) {
- const disableMatch = line.match(/^\s*monitor\s*=\s*([^,]+),\s*disable\s*$/);
- if (disableMatch) {
- const name = disableMatch[1].trim();
- result[name] = {
- "name": name,
- "logical": {
- "x": 0,
- "y": 0,
- "scale": 1.0,
- "transform": "Normal"
- },
- "modes": [],
- "current_mode": -1,
- "vrr_enabled": false,
- "vrr_supported": false,
- "hyprlandSettings": {
- "disabled": true
- }
- };
- continue;
- }
- 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, vrrMode = 0, 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)
- vrrMode = parseInt(vrrMatch[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]);
-
- let mirror = "";
- const mirrorMatch = rest.match(/,\s*mirror,\s*([^,\s]+)/);
- if (mirrorMatch)
- mirror = mirrorMatch[1];
-
- result[name] = {
- "name": name,
- "logical": {
- "x": parseInt(match[5]),
- "y": parseInt(match[6]),
- "scale": parseFloat(match[7]),
- "transform": hyprlandToTransform(transform)
- },
- "modes": [
- {
- "width": parseInt(match[2]),
- "height": parseInt(match[3]),
- "refresh_rate": Math.round(parseFloat(match[4]) * 1000)
- }
- ],
- "current_mode": 0,
- "vrr_enabled": vrrMode >= 1,
- "vrr_supported": true,
- "hyprlandSettings": {
- "bitdepth": bitdepth,
- "colorManagement": cm,
- "sdrBrightness": sdrBrightness,
- "sdrSaturation": sdrSaturation,
- "vrrFullscreenOnly": vrrMode === 2 ? true : undefined
- },
- "mirror": mirror
- };
- }
- return result;
- }
-
- 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";
- }
- }
-
- function parseMangoOutputs(content) {
- const result = {};
- const lines = content.split("\n");
- for (const line of lines) {
- const trimmed = line.trim();
- if (!trimmed.startsWith("monitorrule="))
- continue;
-
- const params = {};
- for (const pair of trimmed.substring("monitorrule=".length).split(",")) {
- const colonIdx = pair.indexOf(":");
- if (colonIdx < 0)
- continue;
- params[pair.substring(0, colonIdx).trim()] = pair.substring(colonIdx + 1).trim();
- }
-
- const name = params.name;
- if (!name)
- continue;
-
- result[name] = {
- "name": name,
- "logical": {
- "x": parseInt(params.x || "0"),
- "y": parseInt(params.y || "0"),
- "scale": parseFloat(params.scale || "1"),
- "transform": mangoToTransform(parseInt(params.rr || "0"))
- },
- "modes": [
- {
- "width": parseInt(params.width || "1920"),
- "height": parseInt(params.height || "1080"),
- "refresh_rate": parseFloat(params.refresh || "60") * 1000
- }
- ],
- "current_mode": 0,
- "vrr_enabled": parseInt(params.vrr || "0") === 1,
- "vrr_supported": true
- };
- }
- return result;
- }
-
- function mangoToTransform(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";
- }
- }
-
- function getConfigPaths() {
- const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
- switch (CompositorService.compositor) {
- case "niri":
- return {
- "configFile": configDir + "/niri/config.kdl",
- "outputsFile": configDir + "/niri/dms/outputs.kdl",
- "grepPattern": 'include.*"dms/outputs.kdl"',
- "includeLine": 'include "dms/outputs.kdl"'
- };
- case "hyprland":
- return {
- "configFile": configDir + "/hypr/hyprland.conf",
- "outputsFile": configDir + "/hypr/dms/outputs.conf",
- "grepPattern": 'source.*dms/outputs.conf',
- "includeLine": "source = ./dms/outputs.conf"
- };
- case "dwl":
- return {
- "configFile": configDir + "/mango/config.conf",
- "outputsFile": configDir + "/mango/dms/outputs.conf",
- "grepPattern": 'source.*dms/outputs.conf',
- "includeLine": "source=./dms/outputs.conf"
- };
- default:
- return null;
- }
- }
-
- function checkIncludeStatus() {
- const compositor = CompositorService.compositor;
- if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl") {
- includeStatus = {
- "exists": false,
- "included": false
- };
- return;
- }
-
- const filename = (compositor === "niri") ? "outputs.kdl" : "outputs.conf";
- const compositorArg = (compositor === "dwl") ? "mangowc" : compositor;
-
- checkingInclude = true;
- Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
- checkingInclude = false;
- if (exitCode !== 0) {
- includeStatus = {
- "exists": false,
- "included": false
- };
- return;
- }
- try {
- includeStatus = JSON.parse(output.trim());
- } catch (e) {
- includeStatus = {
- "exists": false,
- "included": false
- };
- }
- });
- }
-
- function fixOutputsInclude() {
- const paths = getConfigPaths();
- if (!paths)
- return;
-
- fixingInclude = true;
- const outputsDir = paths.outputsFile.substring(0, paths.outputsFile.lastIndexOf("/"));
- const unixTime = Math.floor(Date.now() / 1000);
- const backupFile = paths.configFile + ".backup" + unixTime;
-
- Proc.runCommand("fix-outputs-include", ["sh", "-c", `cp "${paths.configFile}" "${backupFile}" 2>/dev/null; ` + `mkdir -p "${outputsDir}" && ` + `touch "${paths.outputsFile}" && ` + `if ! grep -v '^[[:space:]]*\\(//\\|#\\)' "${paths.configFile}" 2>/dev/null | grep -q '${paths.grepPattern}'; then ` + `echo '' >> "${paths.configFile}" && ` + `echo '${paths.includeLine}' >> "${paths.configFile}"; fi`], (output, exitCode) => {
- fixingInclude = false;
- if (exitCode !== 0)
- return;
- checkIncludeStatus();
- WlrOutputService.requestState();
- });
- }
-
- function buildOutputsMap() {
- const map = {};
- for (const output of wlrOutputs) {
- const normalizedModes = (output.modes || []).map(m => ({
- "id": m.id,
- "width": m.width,
- "height": m.height,
- "refresh_rate": m.refresh,
- "preferred": m.preferred ?? false
- }));
- map[output.name] = {
- "name": output.name,
- "make": output.make || "",
- "model": output.model || "",
- "serial": output.serialNumber || "",
- "modes": normalizedModes,
- "current_mode": normalizedModes.findIndex(m => m.id === output.currentMode?.id),
- "vrr_supported": output.adaptiveSyncSupported ?? false,
- "vrr_enabled": output.adaptiveSync === 1,
- "logical": {
- "x": output.x ?? 0,
- "y": output.y ?? 0,
- "width": output.currentMode?.width ?? 1920,
- "height": output.currentMode?.height ?? 1080,
- "scale": output.scale ?? 1.0,
- "transform": mapWlrTransform(output.transform)
- }
- };
- }
- return map;
- }
-
- function mapWlrTransform(wlrTransform) {
- switch (wlrTransform) {
- 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";
- }
- }
-
- function mapTransformToWlr(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 backendFetchOutputs() {
- WlrOutputService.requestState();
- }
-
- function backendWriteOutputsConfig(outputsData) {
- switch (CompositorService.compositor) {
- case "niri":
- NiriService.generateOutputsConfig(outputsData);
- break;
- case "hyprland":
- HyprlandService.generateOutputsConfig(outputsData, buildMergedHyprlandSettings());
- break;
- case "dwl":
- DwlService.generateOutputsConfig(outputsData);
- break;
- default:
- WlrOutputService.applyOutputsConfig(outputsData, outputs);
- break;
- }
- }
-
- function normalizeOutputPositions(outputsData) {
- const names = Object.keys(outputsData);
- if (names.length === 0)
- return outputsData;
-
- let minX = Infinity;
- let minY = Infinity;
-
- for (const name of names) {
- const output = outputsData[name];
- if (!output.logical)
- continue;
- minX = Math.min(minX, output.logical.x);
- minY = Math.min(minY, output.logical.y);
- }
-
- if (minX === Infinity || (minX === 0 && minY === 0))
- return outputsData;
-
- const normalized = JSON.parse(JSON.stringify(outputsData));
- for (const name of names) {
- if (!normalized[name].logical)
- continue;
- normalized[name].logical.x -= minX;
- normalized[name].logical.y -= minY;
- }
-
- return normalized;
- }
-
- function buildOutputsWithPendingChanges() {
- const result = {};
-
- for (const outputName in savedOutputs) {
- if (!outputs[outputName])
- result[outputName] = JSON.parse(JSON.stringify(savedOutputs[outputName]));
- }
-
- for (const outputName in outputs) {
- result[outputName] = JSON.parse(JSON.stringify(outputs[outputName]));
- }
-
- for (const outputName in pendingChanges) {
- if (!result[outputName])
- continue;
- const changes = pendingChanges[outputName];
- if (changes.position && result[outputName].logical) {
- result[outputName].logical.x = changes.position.x;
- result[outputName].logical.y = changes.position.y;
- }
- if (changes.mode !== undefined && result[outputName].modes) {
- for (var i = 0; i < result[outputName].modes.length; i++) {
- if (formatMode(result[outputName].modes[i]) === changes.mode) {
- result[outputName].current_mode = i;
- break;
- }
- }
- }
- if (changes.scale !== undefined && result[outputName].logical)
- result[outputName].logical.scale = changes.scale;
- if (changes.transform !== undefined && result[outputName].logical)
- result[outputName].logical.transform = changes.transform;
- if (changes.vrr !== undefined)
- result[outputName].vrr_enabled = changes.vrr;
- if (changes.mirror !== undefined)
- result[outputName].mirror = changes.mirror;
- }
- return normalizeOutputPositions(result);
- }
-
- function backendUpdateOutputPosition(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 backendUpdateOutputScale(outputName, scale) {
- 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.scale = scale;
- } else {
- updatedOutputs[name] = output;
- }
- }
- outputs = updatedOutputs;
- }
-
- function getOutputDisplayName(output, outputName) {
- if (SettingsData.displayNameMode === "model" && output?.make && output?.model) {
- if (CompositorService.isNiri) {
- const serial = output.serial || "Unknown";
- return output.make + " " + output.model + " " + serial;
- }
- return output.make + " " + output.model;
- }
- return outputName;
- }
-
- function getNiriOutputIdentifier(output, outputName) {
- if (SettingsData.displayNameMode === "model" && output?.make && output?.model) {
- const serial = output.serial || "Unknown";
- return output.make + " " + output.model + " " + serial;
- }
- return outputName;
- }
-
- function getNiriSetting(output, outputName, key, defaultValue) {
- if (!CompositorService.isNiri)
- return defaultValue;
- const identifier = getNiriOutputIdentifier(output, outputName);
- const pending = pendingNiriChanges[identifier];
- if (pending && pending[key] !== undefined)
- return pending[key];
- return SettingsData.getNiriOutputSetting(identifier, key, defaultValue);
- }
-
- function setNiriSetting(output, outputName, key, value) {
- if (!CompositorService.isNiri)
- return;
- initOriginalNiriSettings();
- const identifier = getNiriOutputIdentifier(output, outputName);
- const newPending = JSON.parse(JSON.stringify(pendingNiriChanges));
- if (!newPending[identifier])
- newPending[identifier] = {};
- newPending[identifier][key] = value;
- pendingNiriChanges = newPending;
- }
-
- function initOriginalNiriSettings() {
- if (originalNiriSettings)
- return;
- 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 + " " + (output?.serial || "Unknown");
- 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));
- }
-
- function setPendingChange(outputName, key, value) {
- initOriginalOutputs();
- const newPending = JSON.parse(JSON.stringify(pendingChanges));
- if (!newPending[outputName])
- newPending[outputName] = {};
- newPending[outputName][key] = value;
- pendingChanges = newPending;
-
- if (key === "scale") {
- recalculateAdjacentPositions(outputName, value);
- backendUpdateOutputScale(outputName, value);
- }
- }
-
- function recalculateAdjacentPositions(changedOutput, newScale) {
- const output = outputs[changedOutput];
- if (!output?.logical)
- return;
- const oldPhys = getPhysicalSize(output);
- const oldLogicalW = Math.round(oldPhys.w / (output.logical.scale || 1.0));
- const newLogicalW = Math.round(oldPhys.w / newScale);
-
- const changedX = getPendingValue(changedOutput, "position")?.x ?? output.logical.x;
- const changedY = getPendingValue(changedOutput, "position")?.y ?? output.logical.y;
-
- for (const name in outputs) {
- if (name === changedOutput)
- continue;
- const other = outputs[name];
- if (!other?.logical)
- continue;
- const otherX = getPendingValue(name, "position")?.x ?? other.logical.x;
- const otherY = getPendingValue(name, "position")?.y ?? other.logical.y;
- const otherSize = getLogicalSize(other);
- const otherRight = otherX + otherSize.w;
-
- if (Math.abs(changedX - otherRight) < 5) {
- const newX = otherRight;
- const newPending = JSON.parse(JSON.stringify(pendingChanges));
- if (!newPending[changedOutput])
- newPending[changedOutput] = {};
- newPending[changedOutput].position = {
- "x": newX,
- "y": changedY
- };
- pendingChanges = newPending;
- backendUpdateOutputPosition(changedOutput, newX, changedY);
- return;
- }
-
- const changedRight = changedX + oldLogicalW;
- if (Math.abs(otherX - changedRight) < 5) {
- const newOtherX = changedX + newLogicalW;
- const newPending = JSON.parse(JSON.stringify(pendingChanges));
- if (!newPending[name])
- newPending[name] = {};
- newPending[name].position = {
- "x": newOtherX,
- "y": otherY
- };
- pendingChanges = newPending;
- backendUpdateOutputPosition(name, newOtherX, otherY);
- }
- }
- }
-
- function getPendingValue(outputName, key) {
- if (!pendingChanges[outputName])
- return undefined;
- return pendingChanges[outputName][key];
- }
-
- function getEffectiveValue(outputName, key, originalValue) {
- const pending = getPendingValue(outputName, key);
- return pending !== undefined ? pending : originalValue;
- }
-
- function clearPendingChanges() {
- pendingChanges = {};
- pendingNiriChanges = {};
- pendingHyprlandChanges = {};
- originalOutputs = null;
- originalNiriSettings = null;
- originalHyprlandSettings = null;
- originalDisplayNameMode = "";
- }
-
- function discardChanges() {
- if (originalDisplayNameMode !== "") {
- SettingsData.displayNameMode = originalDisplayNameMode;
- SettingsData.saveSettings();
- }
- backendFetchOutputs();
- clearPendingChanges();
- }
-
- function applyChanges() {
- if (!hasPendingChanges)
- return;
- const changeDescriptions = [];
-
- if (formatChanged) {
- const formatLabel = SettingsData.displayNameMode === "model" ? I18n.tr("Model") : I18n.tr("Name");
- changeDescriptions.push(I18n.tr("Config Format") + " → " + formatLabel);
- }
-
- for (const outputName in pendingChanges) {
- const changes = pendingChanges[outputName];
- if (changes.position)
- changeDescriptions.push(outputName + ": " + I18n.tr("Position") + " → " + changes.position.x + ", " + changes.position.y);
- if (changes.mode)
- changeDescriptions.push(outputName + ": " + I18n.tr("Mode") + " → " + changes.mode);
- if (changes.scale !== undefined)
- changeDescriptions.push(outputName + ": " + I18n.tr("Scale") + " → " + changes.scale);
- if (changes.transform)
- changeDescriptions.push(outputName + ": " + I18n.tr("Transform") + " → " + getTransformLabel(changes.transform));
- if (changes.vrr !== undefined)
- changeDescriptions.push(outputName + ": " + I18n.tr("VRR") + " → " + (changes.vrr ? I18n.tr("Enabled") : I18n.tr("Disabled")));
- }
-
- for (const outputId in pendingNiriChanges) {
- const changes = pendingNiriChanges[outputId];
- if (changes.disabled !== undefined)
- changeDescriptions.push(outputId + ": " + I18n.tr("Disabled") + " → " + (changes.disabled ? I18n.tr("Yes") : I18n.tr("No")));
- if (changes.vrrOnDemand !== undefined)
- changeDescriptions.push(outputId + ": " + I18n.tr("VRR On-Demand") + " → " + (changes.vrrOnDemand ? I18n.tr("Enabled") : I18n.tr("Disabled")));
- if (changes.focusAtStartup !== undefined)
- changeDescriptions.push(outputId + ": " + I18n.tr("Focus at Startup") + " → " + (changes.focusAtStartup ? I18n.tr("Yes") : I18n.tr("No")));
- if (changes.hotCorners !== undefined)
- changeDescriptions.push(outputId + ": " + I18n.tr("Hot Corners") + " → " + I18n.tr("Modified"));
- if (changes.layout !== undefined)
- changeDescriptions.push(outputId + ": " + I18n.tr("Layout") + " → " + I18n.tr("Modified"));
- }
-
- for (const outputId in pendingHyprlandChanges) {
- const changes = pendingHyprlandChanges[outputId];
- if (changes.disabled !== undefined)
- changeDescriptions.push(outputId + ": " + I18n.tr("Disabled") + " → " + (changes.disabled ? I18n.tr("Yes") : I18n.tr("No")));
- 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 (changes.vrrFullscreenOnly !== undefined)
- changeDescriptions.push(outputId + ": " + I18n.tr("VRR Fullscreen Only") + " → " + (changes.vrrFullscreenOnly ? I18n.tr("Enabled") : I18n.tr("Disabled")));
- }
-
- if (CompositorService.isNiri) {
- validateAndApplyNiriConfig(changeDescriptions);
- return;
- }
-
- changesApplied(changeDescriptions);
-
- if (formatChanged)
- SettingsData.saveSettings();
-
- if (CompositorService.isHyprland)
- commitHyprlandSettingsChanges();
-
- const mergedOutputs = buildOutputsWithPendingChanges();
- backendWriteOutputsConfig(mergedOutputs);
- }
-
- function validateAndApplyNiriConfig(changeDescriptions) {
- validatingConfig = true;
- validationError = "";
-
- const mergedOutputs = buildOutputsWithPendingChanges();
- const mergedNiriSettings = buildMergedNiriSettings();
- const configContent = generateNiriOutputsKdl(mergedOutputs, mergedNiriSettings);
-
- const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
- const tempFile = configDir + "/niri/dms/.outputs-validate-tmp.kdl";
-
- Proc.runCommand("niri-validate-write-tmp", ["sh", "-c", `mkdir -p "$(dirname "${tempFile}")" && cat > "${tempFile}" << 'EOF'\n${configContent}EOF`], (output, writeExitCode) => {
- if (writeExitCode !== 0) {
- validatingConfig = false;
- validationError = I18n.tr("Failed to write temp file for validation");
- ToastService.showError(I18n.tr("Config validation failed"), validationError, "", "display-config");
- return;
- }
- Proc.runCommand("niri-validate-config", ["sh", "-c", `niri validate -c "${tempFile}" 2>&1`], (validateOutput, validateExitCode) => {
- validatingConfig = false;
- Proc.runCommand("niri-validate-cleanup", ["rm", "-f", tempFile], () => {});
- if (validateExitCode !== 0) {
- validationError = validateOutput.trim() || I18n.tr("Invalid configuration");
- ToastService.showError(I18n.tr("Config validation failed"), validationError, "", "display-config");
- return;
- }
- changesApplied(changeDescriptions);
- if (formatChanged)
- SettingsData.saveSettings();
- commitNiriSettingsChanges();
- backendWriteOutputsConfig(mergedOutputs);
- });
- });
- }
-
- function buildMergedNiriSettings() {
- const merged = JSON.parse(JSON.stringify(SettingsData.niriOutputSettings));
- for (const outputId in pendingNiriChanges) {
- if (!merged[outputId])
- merged[outputId] = {};
- for (const key in pendingNiriChanges[outputId]) {
- merged[outputId][key] = pendingNiriChanges[outputId][key];
- }
- }
- return merged;
- }
-
- function commitNiriSettingsChanges() {
- for (const outputId in pendingNiriChanges) {
- for (const key in pendingNiriChanges[outputId]) {
- SettingsData.setNiriOutputSetting(outputId, key, pendingNiriChanges[outputId][key]);
- }
- }
- }
-
- 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`;
- 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() {
- clearPendingChanges();
- changesConfirmed();
- }
-
- function revertChanges() {
- const hadFormatChange = originalDisplayNameMode !== "";
- const hadNiriChanges = originalNiriSettings !== null;
- const hadHyprlandChanges = originalHyprlandSettings !== null;
-
- if (hadFormatChange) {
- SettingsData.displayNameMode = originalDisplayNameMode;
- SettingsData.saveSettings();
- }
-
- if (hadNiriChanges) {
- SettingsData.niriOutputSettings = JSON.parse(JSON.stringify(originalNiriSettings));
- SettingsData.saveSettings();
- }
-
- if (hadHyprlandChanges) {
- SettingsData.hyprlandOutputSettings = JSON.parse(JSON.stringify(originalHyprlandSettings));
- SettingsData.saveSettings();
- }
-
- pendingHyprlandChanges = {};
- pendingNiriChanges = {};
-
- if (!originalOutputs && !hadNiriChanges && !hadHyprlandChanges) {
- if (hadFormatChange)
- backendWriteOutputsConfig(buildOutputsWithPendingChanges());
- clearPendingChanges();
- changesReverted();
- return;
- }
-
- const original = originalOutputs ? JSON.parse(JSON.stringify(originalOutputs)) : buildOutputsWithPendingChanges();
- for (const name in savedOutputs) {
- if (!original[name])
- original[name] = JSON.parse(JSON.stringify(savedOutputs[name]));
- }
- backendWriteOutputsConfig(original);
- clearPendingChanges();
- if (originalOutputs)
- outputs = original;
- changesReverted();
- }
-
- function getOutputBounds() {
- if (!allOutputs || Object.keys(allOutputs).length === 0)
- return {
- "minX": 0,
- "minY": 0,
- "maxX": 1920,
- "maxY": 1080,
- "width": 1920,
- "height": 1080
- };
-
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
-
- for (const name in allOutputs) {
- const output = allOutputs[name];
- if (!output.logical)
- continue;
- const x = output.logical.x;
- const y = output.logical.y;
- const size = getLogicalSize(output);
- minX = Math.min(minX, x);
- minY = Math.min(minY, y);
- maxX = Math.max(maxX, x + size.w);
- maxY = Math.max(maxY, y + size.h);
- }
-
- if (minX === Infinity)
- return {
- "minX": 0,
- "minY": 0,
- "maxX": 1920,
- "maxY": 1080,
- "width": 1920,
- "height": 1080
- };
- return {
- "minX": minX,
- "minY": minY,
- "maxX": maxX,
- "maxY": maxY,
- "width": maxX - minX,
- "height": maxY - minY
- };
- }
-
- function isRotated(transform) {
- switch (transform) {
- case "90":
- case "270":
- case "Flipped90":
- case "Flipped270":
- return true;
- default:
- return false;
- }
- }
-
- function getPhysicalSize(output) {
- if (!output)
- return {
- "w": 1920,
- "h": 1080
- };
-
- let w = 1920, h = 1080;
- if (output.modes && output.current_mode !== undefined) {
- const mode = output.modes[output.current_mode];
- if (mode) {
- w = mode.width || 1920;
- h = mode.height || 1080;
- }
- } else if (output.logical) {
- const scale = output.logical.scale || 1.0;
- w = Math.round((output.logical.width || 1920) * scale);
- h = Math.round((output.logical.height || 1080) * scale);
- }
-
- if (output.logical && isRotated(output.logical.transform))
- return {
- "w": h,
- "h": w
- };
- return {
- "w": w,
- "h": h
- };
- }
-
- function getLogicalSize(output) {
- if (!output)
- return {
- "w": 1920,
- "h": 1080
- };
-
- const phys = getPhysicalSize(output);
- const scale = output.logical?.scale || 1.0;
-
- return {
- "w": Math.round(phys.w / scale),
- "h": Math.round(phys.h / scale)
- };
- }
-
- function checkOverlap(testName, testX, testY, testW, testH) {
- for (const name in outputs) {
- if (name === testName)
- continue;
- const output = outputs[name];
- if (!output.logical)
- continue;
- const x = output.logical.x;
- const y = output.logical.y;
- const size = getLogicalSize(output);
- if (!(testX + testW <= x || testX >= x + size.w || testY + testH <= y || testY >= y + size.h))
- return true;
- }
- return false;
- }
-
- function snapToEdges(testName, posX, posY, testW, testH) {
- const snapThreshold = 200;
- let snappedX = posX;
- let snappedY = posY;
- let bestXDist = snapThreshold;
- let bestYDist = snapThreshold;
-
- for (const name in outputs) {
- if (name === testName)
- continue;
- const output = outputs[name];
- if (!output.logical)
- continue;
- const x = output.logical.x;
- const y = output.logical.y;
- const size = getLogicalSize(output);
-
- const rightEdge = x + size.w;
- const bottomEdge = y + size.h;
- const testRight = posX + testW;
- const testBottom = posY + testH;
-
- const xSnaps = [
- {
- "val": rightEdge,
- "dist": Math.abs(posX - rightEdge)
- },
- {
- "val": x - testW,
- "dist": Math.abs(testRight - x)
- },
- {
- "val": x,
- "dist": Math.abs(posX - x)
- },
- {
- "val": rightEdge - testW,
- "dist": Math.abs(testRight - rightEdge)
- }
- ];
-
- const ySnaps = [
- {
- "val": bottomEdge,
- "dist": Math.abs(posY - bottomEdge)
- },
- {
- "val": y - testH,
- "dist": Math.abs(testBottom - y)
- },
- {
- "val": y,
- "dist": Math.abs(posY - y)
- },
- {
- "val": bottomEdge - testH,
- "dist": Math.abs(testBottom - bottomEdge)
- }
- ];
-
- for (const snap of xSnaps) {
- if (snap.dist < bestXDist) {
- bestXDist = snap.dist;
- snappedX = snap.val;
- }
- }
-
- for (const snap of ySnaps) {
- if (snap.dist < bestYDist) {
- bestYDist = snap.dist;
- snappedY = snap.val;
- }
- }
- }
-
- if (checkOverlap(testName, snappedX, snappedY, testW, testH)) {
- if (!checkOverlap(testName, snappedX, posY, testW, testH))
- return Qt.point(snappedX, posY);
- if (!checkOverlap(testName, posX, snappedY, testW, testH))
- return Qt.point(posX, snappedY);
- return Qt.point(posX, posY);
- }
- return Qt.point(snappedX, snappedY);
- }
-
- function findBestSnapPosition(testName, posX, posY, testW, testH) {
- const outputNames = Object.keys(outputs).filter(n => n !== testName);
-
- if (outputNames.length === 0)
- return Qt.point(posX, posY);
-
- let bestPos = null;
- let bestDist = Infinity;
-
- for (const name of outputNames) {
- const output = outputs[name];
- if (!output.logical)
- continue;
- const x = output.logical.x;
- const y = output.logical.y;
- const size = getLogicalSize(output);
-
- const candidates = [
- {
- "px": x + size.w,
- "py": y
- },
- {
- "px": x - testW,
- "py": y
- },
- {
- "px": x,
- "py": y + size.h
- },
- {
- "px": x,
- "py": y - testH
- },
- {
- "px": x + size.w,
- "py": y + size.h - testH
- },
- {
- "px": x - testW,
- "py": y + size.h - testH
- },
- {
- "px": x + size.w - testW,
- "py": y + size.h
- },
- {
- "px": x + size.w - testW,
- "py": y - testH
- }
- ];
-
- for (const c of candidates) {
- if (checkOverlap(testName, c.px, c.py, testW, testH))
- continue;
- const dist = Math.hypot(c.px - posX, c.py - posY);
- if (dist < bestDist) {
- bestDist = dist;
- bestPos = Qt.point(c.px, c.py);
- }
- }
- }
-
- return bestPos || Qt.point(posX, posY);
- }
-
- function formatMode(mode) {
- if (!mode)
- return "";
- return mode.width + "x" + mode.height + "@" + (mode.refresh_rate / 1000).toFixed(3);
- }
-
- function getTransformLabel(transform) {
- switch (transform) {
- case "Normal":
- return I18n.tr("Normal");
- case "90":
- return I18n.tr("90°");
- case "180":
- return I18n.tr("180°");
- case "270":
- return I18n.tr("270°");
- case "Flipped":
- return I18n.tr("Flipped");
- case "Flipped90":
- return I18n.tr("Flipped 90°");
- case "Flipped180":
- return I18n.tr("Flipped 180°");
- case "Flipped270":
- return I18n.tr("Flipped 270°");
- default:
- return I18n.tr("Normal");
- }
- }
-
- function getTransformValue(label) {
- if (label === I18n.tr("Normal"))
- return "Normal";
- if (label === I18n.tr("90°"))
- return "90";
- if (label === I18n.tr("180°"))
- return "180";
- if (label === I18n.tr("270°"))
- return "270";
- if (label === I18n.tr("Flipped"))
- return "Flipped";
- if (label === I18n.tr("Flipped 90°"))
- return "Flipped90";
- if (label === I18n.tr("Flipped 180°"))
- return "Flipped180";
- if (label === I18n.tr("Flipped 270°"))
- return "Flipped270";
- return "Normal";
- }
-
- function setOriginalDisplayNameMode(mode) {
- if (originalDisplayNameMode === "")
- originalDisplayNameMode = mode;
- }
-}