1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 11:38:30 -04:00
Files
DankMaterialShell/quickshell/Services/DisplayService.qml
T
2026-07-26 23:22:22 -04:00

1789 lines
60 KiB
QML

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"
}
}