mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-16 08:14:03 -04:00
- Auto detect the snake_case dialect of Mango master from the binary, write configs and respell existing fragments with: `dms config mango-migrate` - Setup backs up and replaces config.conf, moves binds and rules into dms/ - Start DMS from mango-session.target with systemd or exec-once without - fix cheatsheet run action and bind precedence, window-rule and display save updates - Support urgent active tag, greeter loading /etc/mango, per-screen overviews
495 lines
18 KiB
QML
495 lines
18 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Services
|
|
import "../Common/OutputModel.js" as OutputModel
|
|
|
|
Singleton {
|
|
id: root
|
|
readonly property var log: Log.scoped("RefreshRateService")
|
|
|
|
readonly property int batteryRefreshRateTarget: 60000
|
|
readonly property int batteryRefreshRateTolerance: 1000
|
|
|
|
property int _resumeRecoveryAttempt: 0
|
|
readonly property var _resumeRecoveryIntervals: [1000, 5000, 15000, 30000]
|
|
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: SettingsData.lowerDisplayRefreshRateOnBattery
|
|
onTriggered: root.requestSync("startup")
|
|
}
|
|
|
|
Timer {
|
|
id: resumeRecoveryTimer
|
|
interval: root._resumeRecoveryIntervals[0]
|
|
repeat: false
|
|
onTriggered: {
|
|
root.requestSync("resume-reconcile");
|
|
root._resumeRecoveryAttempt++;
|
|
if (root._resumeRecoveryAttempt < root._resumeRecoveryIntervals.length) {
|
|
interval = root._resumeRecoveryIntervals[root._resumeRecoveryAttempt];
|
|
restart();
|
|
return;
|
|
}
|
|
root._resumeRecoveryAttempt = 0;
|
|
interval = root._resumeRecoveryIntervals[0];
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SessionService
|
|
enabled: SettingsData.lowerDisplayRefreshRateOnBattery
|
|
function onSessionResumed() {
|
|
root._resumeRecoveryAttempt = 0;
|
|
resumeRecoveryTimer.interval = root._resumeRecoveryIntervals[0];
|
|
resumeRecoveryTimer.restart();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: BatteryService
|
|
enabled: SettingsData.lowerDisplayRefreshRateOnBattery
|
|
function onIsPluggedInChanged() {
|
|
root.requestSync("power-change");
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
function onLowerDisplayRefreshRateOnBatteryChanged() {
|
|
resumeRecoveryTimer.stop();
|
|
root.requestSync("setting-change");
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: SessionData
|
|
enabled: SettingsData.lowerDisplayRefreshRateOnBattery
|
|
function onActiveDisplayProfileChanged() {
|
|
root.requestSync("profile-change");
|
|
}
|
|
|
|
function onActiveDisplayProfileModesChanged() {
|
|
root.requestSync("profile-change");
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: NiriService
|
|
enabled: SettingsData.lowerDisplayRefreshRateOnBattery
|
|
function onOutputsChanged() {
|
|
root.requestSync("output-change");
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: WlrOutputService
|
|
enabled: SettingsData.lowerDisplayRefreshRateOnBattery
|
|
function onStateChanged() {
|
|
root.requestSync("output-change");
|
|
}
|
|
}
|
|
|
|
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 identifierHead(output) {
|
|
return {
|
|
"make": output?.make || "",
|
|
"model": output?.model || "",
|
|
"serial": output?.serial || output?.serialNumber || ""
|
|
};
|
|
}
|
|
|
|
function withIdentifiers(identifiers, candidates) {
|
|
for (const id of candidates) {
|
|
if (!identifiers.includes(id))
|
|
identifiers.push(id);
|
|
}
|
|
return identifiers;
|
|
}
|
|
|
|
function outputIdentifiers(outputName, output) {
|
|
const head = identifierHead(output);
|
|
if (!head.make || !head.model)
|
|
return [outputName];
|
|
return withIdentifiers([outputName], [OutputModel.niriIdentifier(head, outputName, "model"), OutputModel.modelIdentifier(head), OutputModel.hyprlandIdentifier(head, outputName, "model")]);
|
|
}
|
|
|
|
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 = OutputModel.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 = OutputModel.niriCurrentMode(outputs[name]);
|
|
const target = computeTargetMode(name, outputs[name], "niri");
|
|
if (!target || !target.value)
|
|
continue;
|
|
if (OutputModel.modeAlreadyCurrent(currentMode, target.mode, batteryRefreshRateTolerance)) {
|
|
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 + " " + (OutputModel.modeRefresh(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 (OutputModel.modeAlreadyCurrent(output.currentMode, target.mode, batteryRefreshRateTolerance)) {
|
|
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 + " " + (OutputModel.modeRefresh(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 = OutputModel.niriCurrentMode(output);
|
|
if (!currentMode)
|
|
continue;
|
|
const currentRefresh = OutputModel.modeRefresh(currentMode);
|
|
if (currentRefresh <= batteryRefreshRateTarget + batteryRefreshRateTolerance)
|
|
continue;
|
|
const target = OutputModel.batteryRefreshMode(output, currentMode, "niri", batteryRefreshRateTarget, batteryRefreshRateTolerance);
|
|
if (!target)
|
|
continue;
|
|
const targetValue = OutputModel.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" + (OutputModel.modeRefresh(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 = OutputModel.modeRefresh(currentMode);
|
|
if (currentRefresh <= batteryRefreshRateTarget + batteryRefreshRateTolerance)
|
|
continue;
|
|
const target = OutputModel.batteryRefreshMode(output, currentMode, "wlr", batteryRefreshRateTarget, batteryRefreshRateTolerance);
|
|
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" + (OutputModel.modeRefresh(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 || CompositorService.isMango) {
|
|
applyModeOverridesWithoutReload(modeOverrides, callback);
|
|
return;
|
|
}
|
|
|
|
WlrOutputService.applyConfiguration(buildWlrHeads(modeOverrides), callback);
|
|
}
|
|
|
|
// apply live and persist without a compositor config reload — full reloads
|
|
// are a suspected trigger for IME input lockups (#3073)
|
|
function applyModeOverridesWithoutReload(modeOverrides, callback) {
|
|
WlrOutputService.applyConfiguration(buildWlrHeads(modeOverrides), success => {
|
|
if (!success) {
|
|
persistOutputsConfig(modeOverrides, callback, false);
|
|
return;
|
|
}
|
|
persistOutputsConfig(modeOverrides, null, true);
|
|
if (callback)
|
|
callback(true);
|
|
});
|
|
}
|
|
|
|
function persistOutputsConfig(modeOverrides, callback, skipReload) {
|
|
if (CompositorService.isHyprland) {
|
|
HyprlandService.generateOutputsConfig(buildWlrOutputsData(modeOverrides), SessionData.hyprlandOutputSettings, callback, skipReload);
|
|
return;
|
|
}
|
|
MangoService.generateOutputsConfig(buildWlrOutputsData(modeOverrides), callback, skipReload);
|
|
}
|
|
|
|
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": OutputModel.modeWidth(mode),
|
|
"height": OutputModel.modeHeight(mode),
|
|
"refresh_rate": OutputModel.modeRefresh(mode),
|
|
"preferred": mode.preferred === true || mode.is_preferred === true
|
|
}));
|
|
const currentMode = target ? normalizedModes.findIndex(mode => OutputModel.modeWidth(mode) === OutputModel.modeWidth(target) && OutputModel.modeHeight(mode) === OutputModel.modeHeight(target) && Math.abs(OutputModel.modeRefresh(mode) - OutputModel.modeRefresh(target)) <= batteryRefreshRateTolerance) : -1;
|
|
|
|
// No enabled flag: a head asleep via sleep_monitor reports enabled=false, and Mango would persist that as disable:1.
|
|
data[output.name] = {
|
|
"name": output.name,
|
|
"make": output.make || "",
|
|
"model": output.model || "",
|
|
"serial": output.serial || output.serialNumber || "",
|
|
"modes": normalizedModes,
|
|
"current_mode": currentMode,
|
|
"configured_mode": target ? OutputModel.formatModeString(target) : "",
|
|
"vrr_supported": output.adaptiveSyncSupported ?? output.vrr_supported ?? false,
|
|
"vrr_enabled": OutputModel.outputVrrEnabled(output),
|
|
"logical": {
|
|
"x": output.x ?? 0,
|
|
"y": output.y ?? 0,
|
|
"width": OutputModel.modeWidth(target || output.currentMode) || 1920,
|
|
"height": OutputModel.modeHeight(target || output.currentMode) || 1080,
|
|
"scale": output.scale ?? 1.0,
|
|
"transform": OutputModel.transformName(output.transform)
|
|
}
|
|
};
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
function computeTargetMode(outputName, output, backend) {
|
|
const profileMode = findActiveProfileMode(outputName, output);
|
|
if (profileMode) {
|
|
const mode = OutputModel.findModeByString(output?.modes || [], profileMode, batteryRefreshRateTolerance);
|
|
if (mode) {
|
|
return {
|
|
"value": OutputModel.restoreModeValue(mode, backend),
|
|
"mode": mode,
|
|
"source": "profile"
|
|
};
|
|
}
|
|
}
|
|
|
|
const previousMode = findPreviousRefreshMode(outputName, output);
|
|
if (previousMode) {
|
|
const mode = OutputModel.findModeByString(output?.modes || [], previousMode, batteryRefreshRateTolerance);
|
|
if (mode) {
|
|
return {
|
|
"value": OutputModel.restoreModeValue(mode, backend),
|
|
"mode": mode,
|
|
"source": "previous"
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
"value": null,
|
|
"mode": null,
|
|
"source": "none"
|
|
};
|
|
}
|
|
|
|
function findActiveProfileMode(outputName, output) {
|
|
const compositor = CompositorService.compositor;
|
|
const profileModes = SessionData.activeDisplayProfileModes?.[compositor] || {};
|
|
if (Object.keys(profileModes).length === 0)
|
|
return "";
|
|
|
|
const head = identifierHead(output);
|
|
const identifiers = head.make && head.model ? withIdentifiers([outputName], [OutputModel.niriIdentifier(head, outputName, "model"), OutputModel.modelIdentifier(head)]) : [outputName];
|
|
|
|
for (const identifier of identifiers) {
|
|
const mode = profileModes[identifier]?.mode;
|
|
if (mode)
|
|
return mode;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
}
|