mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
e9bc0169f6
port 1.5
1785 lines
60 KiB
QML
1785 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 setNightModeAutomationMode(mode) {
|
|
SessionData.setNightModeAutoMode(mode);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|