mirror of
https://github.com/end-4/dots-hyprland.git
synced 2026-06-05 14:59:27 -05:00
262 lines
9.1 KiB
QML
262 lines
9.1 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
// From https://github.com/caelestia-dots/shell with modifications.
|
|
// License: GPLv3
|
|
|
|
import qs.modules.common
|
|
import qs.modules.common.functions
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Hyprland
|
|
import QtQuick
|
|
|
|
/**
|
|
* For managing brightness of monitors. Supports both brightnessctl and ddcutil.
|
|
*/
|
|
Singleton {
|
|
id: root
|
|
signal brightnessChanged()
|
|
|
|
property var ddcMonitors: []
|
|
readonly property list<BrightnessMonitor> monitors: Quickshell.screens.map(screen => monitorComp.createObject(root, {
|
|
screen
|
|
}))
|
|
|
|
function getMonitorForScreen(screen: ShellScreen): var {
|
|
return monitors.find(m => m.screen === screen);
|
|
}
|
|
|
|
function increaseBrightness(): void {
|
|
const focusedName = Hyprland.focusedMonitor.name;
|
|
const monitor = monitors.find(m => focusedName === m.screen.name);
|
|
if (monitor)
|
|
monitor.setBrightness(monitor.brightness + 0.05);
|
|
}
|
|
|
|
function decreaseBrightness(): void {
|
|
const focusedName = Hyprland.focusedMonitor.name;
|
|
const monitor = monitors.find(m => focusedName === m.screen.name);
|
|
if (monitor)
|
|
monitor.setBrightness(monitor.brightness - 0.05);
|
|
}
|
|
|
|
reloadableId: "brightness"
|
|
|
|
onMonitorsChanged: {
|
|
ddcMonitors = [];
|
|
ddcProc.running = true;
|
|
}
|
|
|
|
Process {
|
|
id: ddcProc
|
|
|
|
command: ["ddcutil", "detect", "--brief"]
|
|
stdout: SplitParser {
|
|
splitMarker: "\n\n"
|
|
onRead: data => {
|
|
if (data.startsWith("Display ")) {
|
|
const lines = data.split("\n").map(l => l.trim());
|
|
root.ddcMonitors.push({
|
|
model: lines.find(l => l.startsWith("Monitor:")).split(":")[2],
|
|
busNum: lines.find(l => l.startsWith("I2C bus:")).split("/dev/i2c-")[1]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
onExited: root.ddcMonitorsChanged()
|
|
}
|
|
|
|
Process {
|
|
id: setProc
|
|
}
|
|
|
|
component BrightnessMonitor: QtObject {
|
|
id: monitor
|
|
|
|
required property ShellScreen screen
|
|
readonly property bool isDdc: {
|
|
const match = root.ddcMonitors.find(m => m.model === screen.model && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum));
|
|
return !!match;
|
|
}
|
|
readonly property string busNum: {
|
|
const match = root.ddcMonitors.find(m => m.model === screen.model && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum));
|
|
return match?.busNum ?? "";
|
|
}
|
|
property int rawMaxBrightness: 100
|
|
property real brightness
|
|
property real brightnessMultiplier: 1.0
|
|
property real multipliedBrightness: Math.max(0, Math.min(1, brightness * (Config.options.light.antiFlashbang.enable ? brightnessMultiplier : 1)))
|
|
property bool ready: false
|
|
property bool animateChanges: !monitor.isDdc
|
|
|
|
onBrightnessChanged: {
|
|
if (!monitor.ready) return;
|
|
root.brightnessChanged();
|
|
}
|
|
|
|
Behavior on multipliedBrightness {
|
|
enabled: monitor.animateChanges
|
|
NumberAnimation {
|
|
duration: 200
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Appearance.animationCurves.expressiveEffects
|
|
}
|
|
}
|
|
onMultipliedBrightnessChanged: {
|
|
if (monitor.animationEnabled) syncBrightness();
|
|
else setTimer.restart();
|
|
}
|
|
|
|
function initialize() {
|
|
monitor.ready = false;
|
|
initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`];
|
|
initProc.running = true;
|
|
}
|
|
|
|
readonly property Process initProc: Process {
|
|
stdout: SplitParser {
|
|
onRead: data => {
|
|
const [, , , current, max] = data.split(" ");
|
|
monitor.rawMaxBrightness = parseInt(max);
|
|
monitor.brightness = parseInt(current) / monitor.rawMaxBrightness;
|
|
monitor.ready = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We need a delay for DDC monitors because they can be quite slow and might act weird with rapid changes
|
|
property var setTimer: Timer {
|
|
id: setTimer
|
|
interval: monitor.isDdc ? 300 : 0
|
|
onTriggered: {
|
|
syncBrightness();
|
|
}
|
|
}
|
|
|
|
function syncBrightness() {
|
|
const brightnessValue = Math.max(monitor.multipliedBrightness, 0);
|
|
if (isDdc) {
|
|
const rawValueRounded = Math.max(Math.floor(brightnessValue * monitor.rawMaxBrightness), 1);
|
|
setProc.command = ["ddcutil", "-b", busNum, "setvcp", "10", rawValueRounded];
|
|
setProc.startDetached();
|
|
} else {
|
|
const valuePercentNumber = Math.floor(brightnessValue * 100);
|
|
let valuePercent = `${valuePercentNumber}%`;
|
|
if (valuePercentNumber == 0) valuePercent = "1"; // Prevent fully black
|
|
setProc.command = ["brightnessctl", "--class", "backlight", "s", valuePercent, "--quiet"];
|
|
setProc.startDetached();
|
|
}
|
|
}
|
|
|
|
function setBrightness(value: real): void {
|
|
value = Math.max(0, Math.min(1, value));
|
|
monitor.brightness = value;
|
|
}
|
|
|
|
function setBrightnessMultiplier(value: real): void {
|
|
monitor.brightnessMultiplier = value;
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
initialize();
|
|
}
|
|
|
|
onBusNumChanged: {
|
|
initialize();
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: monitorComp
|
|
|
|
BrightnessMonitor {}
|
|
}
|
|
|
|
// Anti-flashbang
|
|
property int workspaceAnimationDelay: 500
|
|
property int contentSwitchDelay: 30
|
|
property string screenshotDir: "/tmp/quickshell/brightness/antiflashbang"
|
|
function brightnessMultiplierForLightness(x: real): real {
|
|
// I hand picked some values and fitted an exponential curve for this
|
|
// 6.600135 + 216.360356 * e^(-0.0811129189x)
|
|
// Division by 100 is to normalize to [0, 1]
|
|
return (6.600135 + 216.360356 * Math.pow(Math.E, -0.0811129189 * x)) / 100.0;
|
|
}
|
|
Variants {
|
|
model: Quickshell.screens
|
|
Scope {
|
|
id: screenScope
|
|
required property var modelData
|
|
property string screenName: modelData.name
|
|
property string screenshotPath: `${root.screenshotDir}/screenshot-${screenName}.png`
|
|
Connections {
|
|
enabled: Config.options.light.antiFlashbang.enable && Appearance.m3colors.darkmode
|
|
target: Hyprland
|
|
function onRawEvent(event) {
|
|
if (["activewindowv2", "windowtitlev2"].includes(event.name)) {
|
|
screenshotTimer.interval = root.contentSwitchDelay;
|
|
screenshotTimer.restart();
|
|
} else if (["workspacev2"].includes(event.name)) {
|
|
screenshotTimer.interval = root.workspaceAnimationDelay;
|
|
screenshotTimer.restart();
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: screenshotTimer
|
|
interval: 700 // This is what I have for a Hyprland ws anim
|
|
onTriggered: {
|
|
screenshotProc.running = false;
|
|
screenshotProc.running = true;
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: screenshotProc
|
|
command: ["bash", "-c",
|
|
`mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}'`
|
|
+ ` && grim -o '${StringUtils.shellSingleQuoteEscape(screenScope.screenName)}' -`
|
|
+ ` | magick png:- -colorspace Gray -format "%[fx:mean*100]" info:`
|
|
]
|
|
stdout: StdioCollector {
|
|
id: lightnessCollector
|
|
onStreamFinished: {
|
|
Quickshell.execDetached(["rm", screenScope.screenshotPath]); // Cleanup
|
|
const lightness = lightnessCollector.text
|
|
const newMultiplier = root.brightnessMultiplierForLightness(parseFloat(lightness))
|
|
Brightness.getMonitorForScreen(screenScope.modelData).setBrightnessMultiplier(newMultiplier)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// External trigger points
|
|
|
|
IpcHandler {
|
|
target: "brightness"
|
|
|
|
function increment() {
|
|
onPressed: root.increaseBrightness()
|
|
}
|
|
|
|
function decrement() {
|
|
onPressed: root.decreaseBrightness()
|
|
}
|
|
}
|
|
|
|
GlobalShortcut {
|
|
name: "brightnessIncrease"
|
|
description: "Increase brightness"
|
|
onPressed: root.increaseBrightness()
|
|
}
|
|
|
|
GlobalShortcut {
|
|
name: "brightnessDecrease"
|
|
description: "Decrease brightness"
|
|
onPressed: root.decreaseBrightness()
|
|
}
|
|
}
|