pragma Singleton pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io import qs.Common import qs.Services Singleton { id: root readonly property var log: Log.scoped("DisplayService") property bool brightnessAvailable: devices.length > 0 property var devices: [] property var deviceBrightness: ({}) property var deviceBrightnessUserSet: ({}) property var deviceMaxCache: ({}) property var userControlledDevices: ({}) property var pendingOsdDevices: ({}) property int brightnessVersion: 0 property string currentDevice: "" property string lastIpcDevice: "" property int brightnessLevel: { brightnessVersion; const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice); if (!deviceToUse) { return 50; } return getDeviceBrightness(deviceToUse); } property int maxBrightness: 100 property bool brightnessInitialized: false property bool suppressOsd: true readonly property int batteryRefreshRateTarget: 60000 readonly property int batteryRefreshRateTolerance: 1000 property var _lastAppliedTargets: ({}) Timer { id: cascadeGuard interval: 3000 repeat: false onTriggered: root._lastAppliedTargets = ({}) } property string _pendingReason: "" Timer { id: syncDebounce interval: 300 repeat: false onTriggered: root._runSync() } Timer { id: startupRefreshRateSync interval: 500 repeat: false running: true onTriggered: root.requestSync("startup") } signal brightnessChanged(bool showOsd) signal deviceSwitched Connections { target: BatteryService function onIsPluggedInChanged() { root.requestSync("power-change"); } } Connections { target: SettingsData function onLowerDisplayRefreshRateOnBatteryChanged() { root.requestSync("setting-change"); } function onActiveDisplayProfileChanged() { root.requestSync("profile-change"); } function onActiveDisplayProfileModesChanged() { root.requestSync("profile-change"); } } Connections { target: NiriService function onOutputsChanged() { root.requestSync("output-change"); } } Connections { target: WlrOutputService function onStateChanged() { root.requestSync("output-change"); } } property bool nightModeActive: nightModeEnabled property bool nightModeEnabled: false property bool automationAvailable: false property bool gammaControlAvailable: false property int resumeRecoveryAttempt: 0 property var gammaState: ({}) property int gammaCurrentTemp: gammaState?.currentTemp ?? 0 property string gammaNextTransition: gammaState?.nextTransition ?? "" property string gammaSunriseTime: gammaState?.sunriseTime ?? "" property string gammaSunsetTime: gammaState?.sunsetTime ?? "" property string gammaDawnTime: gammaState?.dawnTime ?? "" property string gammaNightTime: gammaState?.nightTime ?? "" property bool gammaIsDay: gammaState?.isDay ?? true property real gammaSunPosition: gammaState?.sunPosition ?? 0 property int gammaLowTemp: gammaState?.config?.LowTemp ?? 0 property int gammaHighTemp: gammaState?.config?.HighTemp ?? 0 function syncRefreshRates(isPluggedIn, reason) { if (!SettingsData.lowerDisplayRefreshRateOnBattery) { if (reason === "setting-change") applyConfiguredTargets("disabled", reason); return; } if (!isPluggedIn) { applyBatteryTargets(reason); return; } applyConfiguredTargets("AC", reason); } function requestSync(reason) { _pendingReason = reason || _pendingReason; syncDebounce.restart(); } function _runSync() { syncRefreshRates(BatteryService.isPluggedIn, _pendingReason || "sync"); _pendingReason = ""; } function outputIdentifiers(outputName, output) { const identifiers = [outputName]; const make = output?.make || ""; const model = output?.model || ""; if (make && model) { const serial = output.serial || output.serialNumber || "Unknown"; const modelId = make + " " + model; const serialId = modelId + " " + serial; const descId = ("desc:" + [make, model, output.serial || output.serialNumber].filter(p => p).join(" ")).replace(/,/g, ""); if (!identifiers.includes(serialId)) identifiers.push(serialId); if (!identifiers.includes(modelId)) identifiers.push(modelId); if (descId !== "desc:" && !identifiers.includes(descId)) identifiers.push(descId); } return identifiers; } function previousRefreshModes() { return SettingsData.displayPreviousRefreshModes?.[CompositorService.compositor] || {}; } function findPreviousRefreshMode(outputName, output) { const modes = previousRefreshModes(); for (const identifier of outputIdentifiers(outputName, output)) { const entry = modes[identifier]; const mode = typeof entry === "string" ? entry : entry?.mode; if (mode) return mode; } return ""; } function storePreviousRefreshMode(outputName, output, mode) { const modeString = formatModeString(mode); if (!modeString) return; const modes = JSON.parse(JSON.stringify(previousRefreshModes())); for (const identifier of outputIdentifiers(outputName, output)) { modes[identifier] = { "mode": modeString }; } SettingsData.setDisplayPreviousRefreshModes(CompositorService.compositor, modes); } function clearPreviousRefreshMode(outputName, output) { const modes = JSON.parse(JSON.stringify(previousRefreshModes())); let changed = false; for (const identifier of outputIdentifiers(outputName, output)) { if (modes[identifier] === undefined) continue; delete modes[identifier]; changed = true; } if (changed) SettingsData.setDisplayPreviousRefreshModes(CompositorService.compositor, modes); } function applyConfiguredTargets(context, reason) { if (CompositorService.isNiri) { const outputs = NiriService.outputs || {}; const applied = []; for (const name in outputs) { const currentMode = getNiriCurrentMode(outputs[name]); const target = computeTargetMode(name, outputs[name], "niri"); if (!target || !target.value) continue; if (isModeAlreadyCurrent(currentMode, target.mode)) { if (target.source === "previous") clearPreviousRefreshMode(name, outputs[name]); continue; } if (root._lastAppliedTargets[name] === target.value) continue; root._lastAppliedTargets[name] = target.value; cascadeGuard.restart(); NiriService.applyOutputConfig(name, { "mode": target.value }, success => { if (success && target.source === "previous") clearPreviousRefreshMode(name, outputs[name]); }); applied.push(name + " " + (getModeRefresh(target.mode) / 1000).toFixed(0) + "Hz (" + target.source + ")"); } if (applied.length > 0) log.info("Updated display refresh rate: ", applied.join(", "), " (", context, ")"); return; } if (!WlrOutputService.wlrOutputAvailable) return; const outputs = WlrOutputService.outputs || []; const modeOverrides = ({}); const restoredOutputs = []; const applied = []; for (const output of outputs) { const target = computeTargetMode(output.name, output, "wlr"); if (!target || !target.value) continue; if (isModeAlreadyCurrent(output.currentMode, target.mode)) { if (target.source === "previous") clearPreviousRefreshMode(output.name, output); continue; } if (root._lastAppliedTargets[output.name] === target.value) continue; root._lastAppliedTargets[output.name] = target.value; cascadeGuard.restart(); modeOverrides[output.name] = target; if (target.source === "previous") restoredOutputs.push(output); applied.push(output.name + " " + (getModeRefresh(target.mode) / 1000).toFixed(0) + "Hz (" + target.source + ")"); } if (applied.length > 0) { log.info("Updated display refresh rate: ", applied.join(", "), " (", context, ")"); applyWlrModeOverrides(modeOverrides, success => { if (!success) return; for (const output of restoredOutputs) clearPreviousRefreshMode(output.name, output); }); } } function applyBatteryTargets(reason) { if (CompositorService.isNiri) { const outputs = NiriService.outputs || {}; const applied = []; for (const name in outputs) { const output = outputs[name]; const currentMode = getNiriCurrentMode(output); if (!currentMode) continue; const currentRefresh = getModeRefresh(currentMode); if (currentRefresh <= batteryRefreshRateTarget + batteryRefreshRateTolerance) continue; const target = findBatteryRefreshMode(output, currentMode, "niri"); if (!target) continue; const targetValue = formatNiriMode(target); storePreviousRefreshMode(name, output, currentMode); if (root._lastAppliedTargets[name] === targetValue) continue; root._lastAppliedTargets[name] = targetValue; cascadeGuard.restart(); NiriService.applyOutputConfig(name, { "mode": targetValue }); applied.push(name + " " + (currentRefresh / 1000).toFixed(0) + "\u2192" + (getModeRefresh(target) / 1000).toFixed(0) + "Hz"); } if (applied.length > 0) log.info("Updated display refresh rate: ", applied.join(", "), " (battery)"); return; } if (!WlrOutputService.wlrOutputAvailable) return; const outputs = WlrOutputService.outputs || []; const modeOverrides = ({}); const applied = []; for (const output of outputs) { const currentMode = output.currentMode; if (!currentMode) continue; const currentRefresh = getModeRefresh(currentMode); if (currentRefresh <= batteryRefreshRateTarget + batteryRefreshRateTolerance) continue; const target = findBatteryRefreshMode(output, currentMode, "wlr"); if (!target) continue; storePreviousRefreshMode(output.name, output, currentMode); if (root._lastAppliedTargets[output.name] === target.id) continue; root._lastAppliedTargets[output.name] = target.id; cascadeGuard.restart(); modeOverrides[output.name] = { "value": target.id, "mode": target, "source": "battery" }; applied.push(output.name + " " + (currentRefresh / 1000).toFixed(0) + "\u2192" + (getModeRefresh(target) / 1000).toFixed(0) + "Hz"); } if (applied.length > 0) { log.info("Updated display refresh rate: ", applied.join(", "), " (battery)"); applyWlrModeOverrides(modeOverrides); } } function buildWlrHeads(modeOverrides) { const outputs = WlrOutputService.outputs || []; const heads = []; for (const output of outputs) { const enabled = output.enabled !== false; const head = { "name": output.name, "enabled": enabled }; if (enabled) { const modeId = modeOverrides[output.name] !== undefined ? modeOverrides[output.name].value : output.currentMode?.id; if (modeId !== undefined) head.modeId = modeId; head.position = { "x": output.x ?? 0, "y": output.y ?? 0 }; head.scale = output.scale ?? 1.0; head.transform = output.transform ?? 0; if (output.adaptiveSyncSupported) head.adaptiveSync = output.adaptiveSync ?? 0; } heads.push(head); } return heads; } function applyWlrModeOverrides(modeOverrides, callback) { if (CompositorService.isHyprland) { HyprlandService.generateOutputsConfig(buildWlrOutputsData(modeOverrides), SettingsData.hyprlandOutputSettings, callback); return; } if (CompositorService.isMango) { MangoService.generateOutputsConfig(buildWlrOutputsData(modeOverrides), callback); return; } WlrOutputService.applyConfiguration(buildWlrHeads(modeOverrides), callback); } function buildWlrOutputsData(modeOverrides) { const outputs = WlrOutputService.outputs || []; const data = ({}); for (const output of outputs) { const target = modeOverrides[output.name]?.mode || output.currentMode; const normalizedModes = (output.modes || []).map(mode => ({ "id": mode.id, "width": getModeWidth(mode), "height": getModeHeight(mode), "refresh_rate": getModeRefresh(mode), "preferred": mode.preferred === true || mode.is_preferred === true })); const currentMode = target ? normalizedModes.findIndex(mode => getModeWidth(mode) === getModeWidth(target) && getModeHeight(mode) === getModeHeight(target) && Math.abs(getModeRefresh(mode) - getModeRefresh(target)) <= batteryRefreshRateTolerance) : -1; data[output.name] = { "name": output.name, "enabled": output.enabled !== false, "make": output.make || "", "model": output.model || "", "serial": output.serial || output.serialNumber || "", "modes": normalizedModes, "current_mode": currentMode, "configured_mode": target ? formatModeString(target) : "", "vrr_supported": output.adaptiveSyncSupported ?? output.vrr_supported ?? false, "vrr_enabled": isOutputVrrEnabled(output), "logical": { "x": output.x ?? 0, "y": output.y ?? 0, "width": getModeWidth(target || output.currentMode) || 1920, "height": getModeHeight(target || output.currentMode) || 1080, "scale": output.scale ?? 1.0, "transform": mapWlrTransform(output.transform) } }; } return data; } function mapWlrTransform(transform) { switch (transform) { 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 findBatteryRefreshMode(output, currentMode, backend) { if (!output || !currentMode || (backend === "wlr" && !output.enabled)) return null; if (isOutputVrrEnabled(output)) return null; const modes = output.modes || []; const sameResolutionModes = modes.filter(m => getModeWidth(m) === getModeWidth(currentMode) && getModeHeight(m) === getModeHeight(currentMode)); const uniqueRefreshRates = []; for (const mode of sameResolutionModes) { const refresh = getModeRefresh(mode); if (!uniqueRefreshRates.some(r => Math.abs(r - refresh) <= batteryRefreshRateTolerance)) uniqueRefreshRates.push(refresh); } if (uniqueRefreshRates.length <= 1) return null; let bestMode = null; let bestDiff = Infinity; for (const mode of sameResolutionModes) { const diff = Math.abs(getModeRefresh(mode) - batteryRefreshRateTarget); if (diff < bestDiff) { bestMode = mode; bestDiff = diff; } } if (!bestMode || bestDiff > batteryRefreshRateTolerance) return null; return bestMode; } function computeTargetMode(outputName, output, backend) { const profileMode = findActiveProfileMode(outputName, output); if (profileMode) { const mode = findModeByString(output?.modes || [], profileMode); if (mode) { return { "value": formatRestoreModeValue(mode, backend), "mode": mode, "source": "profile" }; } } const previousMode = findPreviousRefreshMode(outputName, output); if (previousMode) { const mode = findModeByString(output?.modes || [], previousMode); if (mode) { return { "value": formatRestoreModeValue(mode, backend), "mode": mode, "source": "previous" }; } } return { "value": null, "mode": null, "source": "none" }; } function findActiveProfileMode(outputName, output) { const compositor = CompositorService.compositor; const profileModes = SettingsData.activeDisplayProfileModes?.[compositor] || {}; if (Object.keys(profileModes).length === 0) return ""; const identifiers = [outputName]; if (output?.make && output?.model) { const modelId = output.make + " " + output.model; const serial = output.serial || output.serialNumber || "Unknown"; const serialId = modelId + " " + serial; if (!identifiers.includes(serialId)) identifiers.push(serialId); if (!identifiers.includes(modelId)) identifiers.push(modelId); } for (const identifier of identifiers) { const mode = profileModes[identifier]?.mode; if (mode) return mode; } return ""; } function findModeByString(modes, modeString) { for (const mode of modes) { if (formatModeString(mode) === modeString) return mode; } const parsed = parseModeString(modeString); if (!parsed) return null; for (const mode of modes) { if (getModeWidth(mode) === parsed.width && getModeHeight(mode) === parsed.height && Math.abs(getModeRefresh(mode) - parsed.refresh) <= batteryRefreshRateTolerance) return mode; } return null; } function parseModeString(modeString) { const match = (modeString || "").match(/^(\d+)x(\d+)@([\d.]+)$/); if (!match) return null; return { "width": parseInt(match[1]), "height": parseInt(match[2]), "refresh": Math.round(parseFloat(match[3]) * 1000) }; } function isModeAlreadyCurrent(currentMode, targetMode) { if (!currentMode || !targetMode) return true; return getModeWidth(currentMode) === getModeWidth(targetMode) && getModeHeight(currentMode) === getModeHeight(targetMode) && Math.abs(getModeRefresh(currentMode) - getModeRefresh(targetMode)) <= batteryRefreshRateTolerance; } function formatRestoreModeValue(mode, backend) { if (!mode) return null; if (backend === "wlr") return mode.id ?? null; return formatNiriMode(mode); } function isOutputVrrEnabled(output) { return output.vrr_enabled === true || output.adaptiveSync === 1; } function getNiriCurrentMode(output) { if (!output || !output.modes || output.current_mode === undefined) return null; return output.modes[output.current_mode] || null; } function getModeWidth(mode) { return mode?.width ?? 0; } function getModeHeight(mode) { return mode?.height ?? 0; } function getModeRefresh(mode) { return mode?.refresh_rate ?? mode?.refresh ?? 0; } function formatModeString(mode) { if (!mode) return ""; return getModeWidth(mode) + "x" + getModeHeight(mode) + "@" + (getModeRefresh(mode) / 1000).toFixed(3); } function formatNiriMode(mode) { return mode.width + "x" + mode.height + "@" + (getModeRefresh(mode) / 1000).toFixed(3); } function markDeviceUserControlled(deviceId) { const newControlled = Object.assign({}, userControlledDevices); newControlled[deviceId] = Date.now(); userControlledDevices = newControlled; } function isDeviceUserControlled(deviceId) { const controlTime = userControlledDevices[deviceId]; if (!controlTime) { return false; } return (Date.now() - controlTime) < 1000; } function clearDeviceUserControlled(deviceId) { const newControlled = Object.assign({}, userControlledDevices); delete newControlled[deviceId]; userControlledDevices = newControlled; } function markDevicePendingOsd(deviceId) { const newPending = Object.assign({}, pendingOsdDevices); newPending[deviceId] = true; pendingOsdDevices = newPending; } function clearDevicePendingOsd(deviceId) { const newPending = Object.assign({}, pendingOsdDevices); delete newPending[deviceId]; pendingOsdDevices = newPending; } function updateSingleDevice(device) { if (device.class === "leds") { return; } const isUserControlled = isDeviceUserControlled(device.id); if (isUserControlled) { return; } const deviceIndex = devices.findIndex(d => d.id === device.id); if (deviceIndex !== -1) { const newDevices = [...devices]; const cachedMax = deviceMaxCache[device.id]; let displayMax = cachedMax || (device.class === "ddc" ? device.max : 100); if (displayMax > 0 && !cachedMax) { const newCache = Object.assign({}, deviceMaxCache); newCache[device.id] = displayMax; deviceMaxCache = newCache; } newDevices[deviceIndex] = { "id": device.id, "name": device.id, "class": device.class, "current": device.current, "percentage": device.currentPercent, "max": device.max, "backend": device.backend, "displayMax": displayMax }; devices = newDevices; } const isExponential = SessionData.getBrightnessExponential(device.id); const userSetValue = deviceBrightnessUserSet[device.id]; let displayValue = device.currentPercent; if (isExponential) { if (userSetValue !== undefined) { const exponent = SessionData.getBrightnessExponent(device.id); const expectedHardware = Math.round(Math.pow(userSetValue / 100.0, exponent) * 100.0); if (Math.abs(device.currentPercent - expectedHardware) > 2) { const newUserSet = Object.assign({}, deviceBrightnessUserSet); delete newUserSet[device.id]; deviceBrightnessUserSet = newUserSet; SessionData.clearBrightnessUserSetValue(device.id); displayValue = linearToExponential(device.currentPercent, device.id); } else { displayValue = userSetValue; } } else { displayValue = linearToExponential(device.currentPercent, device.id); } } const oldValue = deviceBrightness[device.id]; const newBrightness = Object.assign({}, deviceBrightness); newBrightness[device.id] = displayValue; deviceBrightness = newBrightness; brightnessVersion++; const isPendingOsd = pendingOsdDevices[device.id] === true; if (isPendingOsd) { clearDevicePendingOsd(device.id); if (!suppressOsd) { brightnessChanged(true); } return; } if (!brightnessInitialized || oldValue === displayValue) { return; } if (suppressOsd) { return; } brightnessChanged(true); } function updateFromBrightnessState(state) { if (!state || !state.devices) { return; } const newMaxCache = Object.assign({}, deviceMaxCache); devices = state.devices.map(d => { const cachedMax = deviceMaxCache[d.id]; let displayMax = cachedMax || (d.class === "ddc" ? d.max : 100); if (displayMax > 0 && !cachedMax) { newMaxCache[d.id] = displayMax; } return { "id": d.id, "name": d.id, "class": d.class, "current": d.current, "percentage": d.currentPercent, "max": d.max, "backend": d.backend, "displayMax": displayMax }; }); deviceMaxCache = newMaxCache; const newBrightness = {}; let anyDeviceBrightnessChanged = false; for (const device of state.devices) { const isExponential = SessionData.getBrightnessExponential(device.id); const userSetValue = deviceBrightnessUserSet[device.id]; const oldValue = deviceBrightness[device.id]; if (isExponential) { if (userSetValue !== undefined) { newBrightness[device.id] = userSetValue; } else { newBrightness[device.id] = linearToExponential(device.currentPercent, device.id); } } else { newBrightness[device.id] = device.currentPercent; } const newValue = newBrightness[device.id]; if (oldValue !== undefined && oldValue !== newValue) { anyDeviceBrightnessChanged = true; } } deviceBrightness = newBrightness; brightnessVersion++; brightnessAvailable = devices.length > 0; if (devices.length > 0 && !currentDevice) { const lastDevice = SessionData.lastBrightnessDevice || ""; const deviceExists = devices.some(d => d.id === lastDevice); if (deviceExists) { setCurrentDevice(lastDevice, false); } else { const backlight = internalPanelActive() ? devices.find(d => d.class === "backlight") : null; const nonKbdDevice = devices.find(d => !d.id.includes("kbd")); const defaultDevice = backlight || nonKbdDevice || devices[0]; setCurrentDevice(defaultDevice.id, false); } } const shouldShowOsd = brightnessInitialized && anyDeviceBrightnessChanged && !suppressOsd; if (!brightnessInitialized) { brightnessInitialized = true; } if (shouldShowOsd) { brightnessChanged(true); } } function setBrightness(percentage, device, suppressOsd) { const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice()); if (!actualDevice) { log.warn("No device selected for brightness change"); return; } if (actualDevice !== lastIpcDevice) { lastIpcDevice = actualDevice; } const deviceInfo = getCurrentDeviceInfoByName(actualDevice); const isExponential = SessionData.getBrightnessExponential(actualDevice); let minValue = 0; let maxValue = 100; switch (true) { case isExponential: minValue = 1; maxValue = 100; break; default: minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0; maxValue = deviceInfo?.displayMax || 100; break; } if (maxValue <= 0) { log.warn("Invalid max value for device", actualDevice, "- skipping brightness change"); return; } const clampedValue = Math.max(minValue, Math.min(maxValue, percentage)); if (!DMSService.isConnected) { log.warn("Not connected to DMS"); return; } const isLedDevice = deviceInfo?.class === "leds"; if (suppressOsd) { markDeviceUserControlled(actualDevice); } else if (!isLedDevice) { markDevicePendingOsd(actualDevice); } const newBrightness = Object.assign({}, deviceBrightness); newBrightness[actualDevice] = clampedValue; deviceBrightness = newBrightness; brightnessVersion++; if (isLedDevice && !suppressOsd) { brightnessChanged(true); } if (isExponential) { const newUserSet = Object.assign({}, deviceBrightnessUserSet); newUserSet[actualDevice] = clampedValue; deviceBrightnessUserSet = newUserSet; SessionData.setBrightnessUserSetValue(actualDevice, clampedValue); } const params = { "device": actualDevice, "percent": clampedValue }; if (isExponential) { params.exponential = true; params.exponent = SessionData.getBrightnessExponent(actualDevice); } DMSService.sendRequest("brightness.setBrightness", params, response => { if (response.error) { log.error("Failed to set brightness:", response.error); ToastService.showError(I18n.tr("Failed to set brightness"), response.error, "", "brightness"); } else { ToastService.dismissCategory("brightness"); } }); } function setCurrentDevice(deviceName, saveToSession = false) { if (currentDevice === deviceName) { return; } currentDevice = deviceName; lastIpcDevice = deviceName; if (saveToSession) { SessionData.setLastBrightnessDevice(deviceName); } deviceSwitched(); } function getDeviceBrightness(deviceName) { if (!deviceName) { return 50; } if (deviceName in deviceBrightness) { return deviceBrightness[deviceName]; } return 50; } function linearToExponential(linearPercent, deviceName) { const exponent = SessionData.getBrightnessExponent(deviceName); const hardwarePercent = linearPercent / 100.0; const normalizedPercent = Math.pow(hardwarePercent, 1.0 / exponent); return Math.round(normalizedPercent * 100.0); } function internalPanelActive() { return Quickshell.screens.some(s => /^(eDP|LVDS|DSI)/i.test(s.name)); } function getDefaultDevice() { if (internalPanelActive()) { for (const device of devices) { if (device.class === "backlight") { return device.id; } } } const nonKbdDevice = devices.find(d => d.class !== "backlight" && !d.id.includes("kbd")); if (nonKbdDevice) return nonKbdDevice.id; return devices.length > 0 ? devices[0].id : ""; } function getPinnedDeviceForFocusedScreen() { const focusedScreen = CompositorService.getFocusedScreen(); if (!focusedScreen) return ""; const pins = CacheData.brightnessDevicePins || {}; const screenKey = SettingsData.getScreenDisplayName(focusedScreen); if (!screenKey) return ""; const pinnedDevice = pins[screenKey]; if (!pinnedDevice) return ""; const deviceExists = devices.some(d => d.id === pinnedDevice); if (!deviceExists) return ""; return pinnedDevice; } function getPreferredDevice() { const pinned = getPinnedDeviceForFocusedScreen(); if (pinned) return pinned; return getDefaultDevice(); } function getCurrentDeviceInfo() { const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice); if (!deviceToUse) { return null; } for (const device of devices) { if (device.id === deviceToUse) { return device; } } return null; } function isCurrentDeviceReady() { const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice); return deviceToUse !== ""; } function getCurrentDeviceInfoByName(deviceName) { if (!deviceName) { return null; } for (const device of devices) { if (device.id === deviceName) { return device; } } return null; } function getDeviceMax(deviceName) { const deviceInfo = getCurrentDeviceInfoByName(deviceName); if (!deviceInfo) { return 100; } return deviceInfo.displayMax || 100; } // Night Mode Functions - Simplified function enableNightMode() { if (!gammaControlAvailable) { ToastService.showWarning(I18n.tr("Night mode failed: DMS gamma control not available")); return; } nightModeEnabled = true; SessionData.setNightModeEnabled(true); DMSService.sendRequest("wayland.gamma.setEnabled", { "enabled": true }, response => { if (response.error) { log.error("Failed to enable gamma control:", response.error); ToastService.showError(I18n.tr("Failed to enable night mode"), response.error, "", "night-mode"); nightModeEnabled = false; SessionData.setNightModeEnabled(false); return; } ToastService.dismissCategory("night-mode"); if (SessionData.nightModeAutoEnabled) { startAutomation(); } else { applyNightModeDirectly(); } }); } function disableNightMode() { nightModeEnabled = false; SessionData.setNightModeEnabled(false); if (!gammaControlAvailable) { return; } DMSService.sendRequest("wayland.gamma.setEnabled", { "enabled": false }, response => { if (response.error) { log.error("Failed to disable gamma control:", response.error); ToastService.showError(I18n.tr("Failed to disable night mode"), response.error, "", "night-mode"); } else { ToastService.dismissCategory("night-mode"); } }); } function toggleNightMode() { if (nightModeEnabled) { disableNightMode(); } else { enableNightMode(); } } function applyNightModeDirectly() { const temperature = SessionData.nightModeTemperature || 4000; DMSService.sendRequest("wayland.gamma.setManualTimes", { "sunrise": null, "sunset": null }, response => { if (response.error) { log.error("Failed to clear manual times:", response.error); return; } DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": false }, response => { if (response.error) { log.error("Failed to disable IP location:", response.error); return; } DMSService.sendRequest("wayland.gamma.setTemperature", { "low": temperature, "high": temperature }, response => { if (response.error) { log.error("Failed to set temperature:", response.error); ToastService.showError(I18n.tr("Failed to set night mode temperature"), response.error, "", "night-mode"); } else { ToastService.dismissCategory("night-mode"); } }); }); }); } function startAutomation() { if (!automationAvailable) { return; } const mode = SessionData.nightModeAutoMode || "time"; switch (mode) { case "time": startTimeBasedMode(); break; case "location": startLocationBasedMode(); break; } } function startTimeBasedMode() { const temperature = SessionData.nightModeTemperature || 4000; const highTemp = SessionData.nightModeHighTemperature || 6500; const sunriseHour = SessionData.nightModeEndHour; const sunriseMinute = SessionData.nightModeEndMinute; const sunsetHour = SessionData.nightModeStartHour; const sunsetMinute = SessionData.nightModeStartMinute; const sunrise = `${String(sunriseHour).padStart(2, '0')}:${String(sunriseMinute).padStart(2, '0')}`; const sunset = `${String(sunsetHour).padStart(2, '0')}:${String(sunsetMinute).padStart(2, '0')}`; DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": false }, response => { if (response.error) { log.error("Failed to disable IP location:", response.error); return; } DMSService.sendRequest("wayland.gamma.setTemperature", { "low": temperature, "high": highTemp }, response => { if (response.error) { log.error("Failed to set temperature:", response.error); ToastService.showError(I18n.tr("Failed to set night mode temperature"), response.error, "", "night-mode"); return; } DMSService.sendRequest("wayland.gamma.setManualTimes", { "sunrise": sunrise, "sunset": sunset }, response => { if (response.error) { log.error("Failed to set manual times:", response.error); ToastService.showError(I18n.tr("Failed to set night mode schedule"), response.error, "", "night-mode"); } else { ToastService.dismissCategory("night-mode"); } }); }); }); } function startLocationBasedMode() { const temperature = SessionData.nightModeTemperature || 4000; const highTemp = SessionData.nightModeHighTemperature || 6500; DMSService.sendRequest("wayland.gamma.setManualTimes", { "sunrise": null, "sunset": null }, response => { if (response.error) { log.error("Failed to clear manual times:", response.error); return; } DMSService.sendRequest("wayland.gamma.setTemperature", { "low": temperature, "high": highTemp }, response => { if (response.error) { log.error("Failed to set temperature:", response.error); ToastService.showError(I18n.tr("Failed to set night mode temperature"), response.error, "", "night-mode"); return; } if (SessionData.nightModeUseIPLocation) { DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": true }, response => { if (response.error) { log.error("Failed to enable IP location:", response.error); ToastService.showError(I18n.tr("Failed to enable IP location"), response.error, "", "night-mode"); } else { ToastService.dismissCategory("night-mode"); } }); } else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": false }, response => { if (response.error) { log.error("Failed to disable IP location:", response.error); return; } DMSService.sendRequest("wayland.gamma.setLocation", { "latitude": SessionData.latitude, "longitude": SessionData.longitude }, response => { if (response.error) { log.error("Failed to set location:", response.error); ToastService.showError(I18n.tr("Failed to set night mode location"), response.error, "", "night-mode"); } else { ToastService.dismissCategory("night-mode"); } }); }); } else { log.warn("Location mode selected but no coordinates set and IP location disabled"); } }); }); } function evaluateNightMode() { if (!nightModeEnabled) { return; } if (SessionData.nightModeAutoEnabled) { restartTimer.nextAction = "automation"; restartTimer.start(); } else { restartTimer.nextAction = "direct"; restartTimer.start(); } } function runResumeRecoveryPass() { checkGammaControlAvailability(); rescanDevices(); if (nightModeEnabled) { evaluateNightMode(); } } function checkGammaControlAvailability() { if (!DMSService.isConnected) { return; } if (DMSService.apiVersion < 6) { gammaControlAvailable = false; automationAvailable = false; return; } if (!DMSService.capabilities.includes("gamma")) { gammaControlAvailable = false; automationAvailable = false; return; } DMSService.sendRequest("wayland.gamma.getState", null, response => { if (response.error) { gammaControlAvailable = false; automationAvailable = false; log.error("Gamma control not available:", response.error); } else { gammaControlAvailable = true; automationAvailable = true; if (nightModeEnabled) { DMSService.sendRequest("wayland.gamma.setEnabled", { "enabled": true }, enableResponse => { if (enableResponse.error) { log.error("Failed to enable gamma control on startup:", enableResponse.error); return; } evaluateNightMode(); }); } } }); } Timer { id: restartTimer property string nextAction: "" interval: 250 repeat: false onTriggered: { if (nextAction === "automation") { startAutomation(); } else if (nextAction === "direct") { applyNightModeDirectly(); } nextAction = ""; } } Timer { id: resumeRecoveryTimer interval: 400 repeat: false onTriggered: { runResumeRecoveryPass(); resumeRecoveryAttempt++; switch (resumeRecoveryAttempt) { case 1: interval = 1400; restart(); return; case 2: interval = 2600; restart(); return; } resumeRecoveryAttempt = 0; interval = 400; } } function rescanDevices() { if (!DMSService.isConnected) { return; } DMSService.sendRequest("brightness.rescan", null, response => { if (response.error) { log.error("Failed to rescan brightness devices:", response.error); } }); } function updateDeviceBrightnessDisplay(deviceName) { brightnessVersion++; brightnessChanged(); } Connections { target: SessionData function onBrightnessDisplayHintChanged(deviceName) { root.updateDeviceBrightnessDisplay(deviceName); } } Timer { id: osdSuppressTimer interval: 2000 running: true onTriggered: suppressOsd = false } Component.onCompleted: { nightModeEnabled = SessionData.nightModeEnabled; deviceBrightnessUserSet = Object.assign({}, SessionData.brightnessUserSetValues); if (DMSService.isConnected) { checkGammaControlAvailability(); } } Timer { id: screenChangeRescanTimer property int rescanAttempt: 0 interval: 3000 repeat: false onTriggered: { rescanDevices(); rescanAttempt++; if (rescanAttempt < 3) { interval = rescanAttempt === 1 ? 5000 : 8000; restart(); return; } rescanAttempt = 0; interval = 3000; osdSuppressTimer.restart(); } } Connections { target: Quickshell function onScreensChanged() { suppressOsd = true; screenChangeRescanTimer.rescanAttempt = 0; screenChangeRescanTimer.interval = 3000; screenChangeRescanTimer.restart(); } } Connections { target: DMSService function onConnectionStateChanged() { if (DMSService.isConnected) { checkGammaControlAvailability(); } else { brightnessAvailable = false; gammaControlAvailable = false; automationAvailable = false; } } function onCapabilitiesReceived() { checkGammaControlAvailability(); } function onBrightnessStateUpdate(data) { updateFromBrightnessState(data); } function onBrightnessDeviceUpdate(device) { updateSingleDevice(device); } function onGammaStateUpdate(data) { root.gammaState = data; } } Connections { target: SessionService function onSessionResumed() { suppressOsd = true; osdSuppressTimer.restart(); resumeRecoveryAttempt = 0; resumeRecoveryTimer.interval = 400; resumeRecoveryTimer.restart(); } } // Session Data Connections Connections { target: SessionData function onNightModeEnabledChanged() { nightModeEnabled = SessionData.nightModeEnabled; evaluateNightMode(); } function onNightModeAutoEnabledChanged() { evaluateNightMode(); } function onNightModeAutoModeChanged() { evaluateNightMode(); } function onNightModeStartHourChanged() { evaluateNightMode(); } function onNightModeStartMinuteChanged() { evaluateNightMode(); } function onNightModeEndHourChanged() { evaluateNightMode(); } function onNightModeEndMinuteChanged() { evaluateNightMode(); } function onNightModeTemperatureChanged() { evaluateNightMode(); } function onNightModeHighTemperatureChanged() { evaluateNightMode(); } function onLatitudeChanged() { evaluateNightMode(); } function onLongitudeChanged() { evaluateNightMode(); } function onNightModeUseIPLocationChanged() { evaluateNightMode(); } } // IPC Handler for external control IpcHandler { function set(percentage: string, device: string): string { if (!root.brightnessAvailable) return "Brightness control not available"; const value = parseInt(percentage); if (isNaN(value)) return "Invalid brightness value: " + percentage; const actualDevice = device || root.getPreferredDevice(); if (actualDevice && !root.devices.some(d => d.id === actualDevice)) return "Device not found: " + actualDevice; const deviceInfo = actualDevice ? root.getCurrentDeviceInfoByName(actualDevice) : null; const minValue = (deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc")) ? 1 : 0; const clampedValue = Math.max(minValue, Math.min(100, value)); root.lastIpcDevice = actualDevice; if (actualDevice && actualDevice !== root.currentDevice) root.setCurrentDevice(actualDevice, false); root.setBrightness(clampedValue, actualDevice); return actualDevice ? "Brightness set to " + clampedValue + "% on " + actualDevice : "Brightness set to " + clampedValue + "%"; } function increment(step: string, device: string): string { if (!root.brightnessAvailable) return "Brightness control not available"; const actualDevice = device || root.getPreferredDevice(); if (actualDevice && !root.devices.some(d => d.id === actualDevice)) return "Device not found: " + actualDevice; const stepValue = parseInt(step || "5"); root.lastIpcDevice = actualDevice; if (actualDevice && actualDevice !== root.currentDevice) root.setCurrentDevice(actualDevice, false); const isExponential = SessionData.getBrightnessExponential(actualDevice); const currentBrightness = root.getDeviceBrightness(actualDevice); const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice); const maxValue = isExponential ? 100 : (deviceInfo?.displayMax || 100); const newBrightness = Math.min(maxValue, currentBrightness + stepValue); root.setBrightness(newBrightness, actualDevice); return "Brightness increased by " + stepValue + "%" + (device ? " on " + actualDevice : ""); } function decrement(step: string, device: string): string { if (!root.brightnessAvailable) return "Brightness control not available"; const actualDevice = device || root.getPreferredDevice(); if (actualDevice && !root.devices.some(d => d.id === actualDevice)) return "Device not found: " + actualDevice; const stepValue = parseInt(step || "5"); root.lastIpcDevice = actualDevice; if (actualDevice && actualDevice !== root.currentDevice) root.setCurrentDevice(actualDevice, false); const isExponential = SessionData.getBrightnessExponential(actualDevice); const currentBrightness = root.getDeviceBrightness(actualDevice); const deviceInfo = root.getCurrentDeviceInfoByName(actualDevice); let minValue = 0; switch (true) { case isExponential: minValue = 1; break; case deviceInfo && (deviceInfo.class === "backlight" || deviceInfo.class === "ddc"): minValue = 1; break; default: minValue = 0; break; } const newBrightness = Math.max(minValue, currentBrightness - stepValue); root.setBrightness(newBrightness, actualDevice); return "Brightness decreased by " + stepValue + "%" + (device ? " on " + actualDevice : ""); } function status(): string { if (!root.brightnessAvailable) { return "Brightness control not available"; } return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%"; } function list(): string { if (!root.brightnessAvailable) { return "No brightness devices available"; } let result = "Available devices:\n"; for (const device of root.devices) { const isExp = SessionData.getBrightnessExponential(device.id); result += device.id + " (" + device.class + ")" + (isExp ? " [exponential]" : "") + "\n"; } return result; } function enableExponential(device: string): string { const targetDevice = device || root.currentDevice; if (!targetDevice) { return "No device specified"; } if (!root.devices.some(d => d.id === targetDevice)) { return "Device not found: " + targetDevice; } SessionData.setBrightnessExponential(targetDevice, true); return "Exponential mode enabled for " + targetDevice; } function disableExponential(device: string): string { const targetDevice = device || root.currentDevice; if (!targetDevice) { return "No device specified"; } if (!root.devices.some(d => d.id === targetDevice)) { return "Device not found: " + targetDevice; } SessionData.setBrightnessExponential(targetDevice, false); return "Exponential mode disabled for " + targetDevice; } function toggleExponential(device: string): string { const targetDevice = device || root.currentDevice; if (!targetDevice) { return "No device specified"; } if (!root.devices.some(d => d.id === targetDevice)) { return "Device not found: " + targetDevice; } const currentState = SessionData.getBrightnessExponential(targetDevice); SessionData.setBrightnessExponential(targetDevice, !currentState); return "Exponential mode " + (!currentState ? "enabled" : "disabled") + " for " + targetDevice; } target: "brightness" } IpcHandler { function toggle(): string { root.toggleNightMode(); return root.nightModeEnabled ? "Night mode enabled" : "Night mode disabled"; } function enable(): string { root.enableNightMode(); return "Night mode enabled"; } function disable(): string { root.disableNightMode(); return "Night mode disabled"; } function status(): string { if (!root.gammaControlAvailable) return "Night mode: unavailable (no gamma control)"; const parts = ["Night mode: " + (root.nightModeEnabled ? "enabled" : "disabled")]; if (root.gammaCurrentTemp > 0) parts.push("Current temperature: " + root.gammaCurrentTemp + "K"); parts.push("Target night temperature: " + SessionData.nightModeTemperature + "K"); if (SessionData.nightModeAutoEnabled) { parts.push("Target day temperature: " + SessionData.nightModeHighTemperature + "K"); parts.push("Automation: " + SessionData.nightModeAutoMode); parts.push("Period: " + (root.gammaIsDay ? "day" : "night")); if (root.gammaNextTransition) parts.push("Next transition: " + root.gammaNextTransition); if (root.gammaSunriseTime) parts.push("Sunrise: " + root.gammaSunriseTime); if (root.gammaSunsetTime) parts.push("Sunset: " + root.gammaSunsetTime); } return parts.join("\n"); } function getCurrentTemp(): string { if (!root.gammaControlAvailable) return "Gamma control not available"; if (root.gammaCurrentTemp <= 0) return "No current temperature reported"; return root.gammaCurrentTemp.toString(); } function getTargetTemp(): string { return SessionData.nightModeTemperature.toString(); } function getDayTemp(): string { return SessionData.nightModeHighTemperature.toString(); } function setTargetTemp(value: string): string { if (!value) return "Usage: night setTargetTemp <2500-6000>"; const temp = parseInt(value); if (isNaN(temp)) return "Invalid temperature: " + value; if (temp < 2500 || temp > 6000) return "Temperature must be between 2500K and 6000K"; const rounded = Math.round(temp / 500) * 500; SessionData.setNightModeTemperature(rounded); if (root.nightModeEnabled) { switch (true) { case SessionData.nightModeAutoEnabled: root.startAutomation(); break; default: root.applyNightModeDirectly(); break; } } if (rounded !== temp) return "Night temperature set to " + rounded + "K (rounded from " + temp + "K)"; return "Night temperature set to " + rounded + "K"; } function setDayTemp(value: string): string { if (!value) return "Usage: night setDayTemp <2500-6500>"; const temp = parseInt(value); if (isNaN(temp)) return "Invalid temperature: " + value; if (temp < 2500 || temp > 6500) return "Temperature must be between 2500K and 6500K"; const rounded = Math.round(temp / 500) * 500; SessionData.setNightModeHighTemperature(rounded); if (root.nightModeEnabled && SessionData.nightModeAutoEnabled) root.startAutomation(); if (rounded !== temp) return "Day temperature set to " + rounded + "K (rounded from " + temp + "K)"; return "Day temperature set to " + rounded + "K"; } function getSchedule(): string { if (!SessionData.nightModeAutoEnabled) return "Automation disabled"; const parts = ["Mode: " + SessionData.nightModeAutoMode]; parts.push("Period: " + (root.gammaIsDay ? "day" : "night")); if (root.gammaDawnTime) parts.push("Dawn: " + root.gammaDawnTime); if (root.gammaSunriseTime) parts.push("Sunrise: " + root.gammaSunriseTime); if (root.gammaSunsetTime) parts.push("Sunset: " + root.gammaSunsetTime); if (root.gammaNightTime) parts.push("Night: " + root.gammaNightTime); if (root.gammaNextTransition) parts.push("Next transition: " + root.gammaNextTransition); if (root.gammaSunPosition > 0) parts.push("Sun position: " + root.gammaSunPosition.toFixed(2) + "°"); return parts.join("\n"); } target: "night" } }