Files
DankMaterialShell/quickshell/Services/OutputCycleState.js
T
Connor Welsh ba2b4a4d5d outputs: only re-enable displays the cycle itself turned off (#3377)
The cycle fallback fired from a never-cycled state. With one display
enabled and another marked off in settings, a DPMS standby that drops
the DisplayPort link looked like an unplug, and DMS sent an On to the
disabled display over niri IPC. It stayed on until the next config
reload and the workspaces round tripped through it.

Track each output's enabled state the first time a cycle touches it,
drop the entry once it is back at that state, and only rescue outputs
that were on before the cycle disabled them. Outputs that were already
off are never enabled automatically.
2026-09-09 13:02:21 -04:00

141 lines
4.6 KiB
JavaScript

.pragma library
function emptyState() {
return {
ring: [],
selected: "",
pending: "",
original: {}
};
}
function requestCycle(state, outputs, currentOutput) {
if (state.pending)
return result(state, "busy", []);
const ring = outputNames(outputs);
if (ring.length < 2)
return result(state, "no-op", []);
const current = findOutput(outputs, currentOutput) ? currentOutput : (firstEnabled(outputs) || ring[0]);
const target = ring[(ring.indexOf(current) + 1) % ring.length];
const targetOutput = findOutput(outputs, target);
if (targetOutput.enabled) {
const intents = disableOthers(outputs, target);
return result(withState(state, { selected: target, original: recordOriginal(state.original, outputs, intents) }), "accepted", intents);
}
const intents = [{ id: target, enabled: true }];
return result(withState(state, { pending: target, original: recordOriginal(state.original, outputs, intents) }), "accepted", intents);
}
function handleOutputsChanged(state, outputs) {
const ring = outputNames(outputs);
if (ring.length === 0)
return result(state, "", []);
let nextState = withState(state, { original: pruneRestored(state.original, outputs) });
let intents = [];
if (state.pending) {
const pendingOutput = findOutput(outputs, state.pending);
if (!pendingOutput) {
nextState = withState(nextState, { pending: "" });
} else if (pendingOutput.enabled) {
intents = disableOthers(outputs, pendingOutput.id);
nextState = withState(nextState, {
pending: "",
selected: pendingOutput.id,
original: recordOriginal(nextState.original, outputs, intents)
});
}
}
if (!nextState.pending && nextState.selected && !findOutput(outputs, nextState.selected) && !firstEnabled(outputs)) {
const fallback = previousConnectedOutput(state.ring, nextState.selected, outputs, nextState.original);
if (fallback) {
nextState = withState(nextState, { selected: fallback.id });
intents = intents.concat([{ id: fallback.id, enabled: true }]);
}
}
nextState = withState(nextState, { ring: ring });
const enabled = firstEnabled(outputs);
if (enabled && enabledOutputCount(outputs) === 1)
nextState = withState(nextState, { selected: enabled });
return result(nextState, "", intents);
}
function clearPending(state) {
return withState(state, { pending: "" });
}
function outputNames(outputs) {
return outputs.map(output => output.id).sort((a, b) => a.localeCompare(b));
}
function findOutput(outputs, id) {
return outputs.find(output => output.id === id);
}
function firstEnabled(outputs) {
return outputNames(outputs).find(id => findOutput(outputs, id).enabled) || "";
}
function enabledOutputCount(outputs) {
return outputs.filter(output => output.enabled).length;
}
function disableOthers(outputs, selected) {
return outputs
.filter(output => output.enabled && output.id !== selected)
.map(output => ({ id: output.id, enabled: false }));
}
function previousConnectedOutput(ring, selected, outputs, original) {
const selectedIndex = ring.indexOf(selected);
if (selectedIndex < 0)
return null;
for (let offset = 1; offset < ring.length; offset++) {
const candidate = findOutput(outputs, ring[(selectedIndex - offset + ring.length) % ring.length]);
if (candidate && original[candidate.id] === true)
return candidate;
}
return null;
}
function recordOriginal(original, outputs, intents) {
const next = Object.assign({}, original);
for (const intent of intents) {
if (next[intent.id] === undefined)
next[intent.id] = findOutput(outputs, intent.id).enabled;
}
return next;
}
function pruneRestored(original, outputs) {
const next = {};
for (const id in original) {
const output = findOutput(outputs, id);
if (!output || output.enabled !== original[id])
next[id] = original[id];
}
return next;
}
function withState(state, changes) {
return {
ring: changes.ring !== undefined ? changes.ring : state.ring,
selected: changes.selected !== undefined ? changes.selected : state.selected,
pending: changes.pending !== undefined ? changes.pending : state.pending,
original: changes.original !== undefined ? changes.original : (state.original || {})
};
}
function result(state, status, intents) {
return {
state: state,
status: status,
intents: intents
};
}