add noctalia and fuzzel
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
pragma Singleton
|
||||
import QtQml
|
||||
import QtQuick
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Commons
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var primaryDevice: _laptopBattery || _bluetoothBattery || null // Primary battery device (prioritizes laptop over Bluetooth)
|
||||
readonly property real batteryPercentage: getPercentage(primaryDevice)
|
||||
readonly property bool batteryCharging: isCharging(primaryDevice)
|
||||
readonly property bool batteryPluggedIn: isPluggedIn(primaryDevice)
|
||||
readonly property bool batteryReady: isDeviceReady(primaryDevice)
|
||||
readonly property bool batteryPresent: isDevicePresent(primaryDevice)
|
||||
readonly property real warningThreshold: Settings.data.systemMonitor.batteryWarningThreshold
|
||||
readonly property real criticalThreshold: Settings.data.systemMonitor.batteryCriticalThreshold
|
||||
readonly property string batteryIcon: getIcon(batteryPercentage, batteryCharging, batteryPluggedIn, batteryReady)
|
||||
|
||||
readonly property var laptopBatteries: UPower.devices.values.filter(d => d.isLaptopBattery).sort((x, y) => {
|
||||
// Force DisplayDevice to the top
|
||||
if (x.nativePath.includes("DisplayDevice"))
|
||||
return -1;
|
||||
if (y.nativePath.includes("DisplayDevice"))
|
||||
return 1;
|
||||
|
||||
// Standard string comparison works for BAT0 vs BAT1
|
||||
return x.nativePath.localeCompare(y.nativePath, undefined, {
|
||||
numeric: true
|
||||
});
|
||||
})
|
||||
|
||||
readonly property var bluetoothBatteries: {
|
||||
var list = [];
|
||||
var btArray = BluetoothService.devices?.values || [];
|
||||
for (var i = 0; i < btArray.length; i++) {
|
||||
var btd = btArray[i];
|
||||
if (btd && btd.connected && btd.batteryAvailable) {
|
||||
list.push(btd);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
readonly property var _laptopBattery: UPower.displayDevice.isPresent ? UPower.displayDevice : (laptopBatteries.length > 0 ? laptopBatteries[0] : null)
|
||||
readonly property var _bluetoothBattery: bluetoothBatteries.length > 0 ? bluetoothBatteries[0] : null
|
||||
|
||||
property var deviceModel: {
|
||||
var model = [
|
||||
{
|
||||
"key": "__default__",
|
||||
"name": I18n.tr("bar.battery.device-default")
|
||||
}
|
||||
];
|
||||
const devices = UPower.devices?.values || [];
|
||||
for (let d of devices) {
|
||||
if (!d || d.type === UPowerDeviceType.LinePower) {
|
||||
continue;
|
||||
}
|
||||
model.push({
|
||||
key: d.nativePath || "",
|
||||
name: d.model || d.nativePath || I18n.tr("common.unknown")
|
||||
});
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
property var _hasNotified: ({})
|
||||
|
||||
function findDevice(nativePath) {
|
||||
if (!nativePath || nativePath === "__default__" || nativePath === "DisplayDevice") {
|
||||
return _laptopBattery;
|
||||
}
|
||||
|
||||
if (!UPower.devices) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const devices = UPower.devices?.values || [];
|
||||
for (let d of devices) {
|
||||
if (d && d.nativePath === nativePath) {
|
||||
if (d.type === UPowerDeviceType.LinePower) {
|
||||
continue;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isDevicePresent(device) {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle Bluetooth devices (identified by having batteryAvailable property)
|
||||
if (device.batteryAvailable !== undefined) {
|
||||
return device.connected === true;
|
||||
}
|
||||
|
||||
// Handle UPower devices
|
||||
if (device.type !== undefined) {
|
||||
if (device.type === UPowerDeviceType.Battery && device.isPresent !== undefined) {
|
||||
return device.isPresent === true;
|
||||
}
|
||||
// Fallback for non-battery UPower devices or if isPresent is missing
|
||||
return device.ready && device.percentage !== undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDeviceReady(device) {
|
||||
if (!isDevicePresent(device)) {
|
||||
return false;
|
||||
}
|
||||
if (device.batteryAvailable !== undefined) {
|
||||
return device.battery !== undefined;
|
||||
}
|
||||
return device.ready && device.percentage !== undefined;
|
||||
}
|
||||
|
||||
function getPercentage(device) {
|
||||
if (!device) {
|
||||
return -1;
|
||||
}
|
||||
if (device.batteryAvailable !== undefined) {
|
||||
return Math.round((device.battery || 0) * 100);
|
||||
}
|
||||
return Math.round((device.percentage || 0) * 100);
|
||||
}
|
||||
|
||||
function isCharging(device) {
|
||||
if (!device || isBluetoothDevice(device)) {
|
||||
// Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?!
|
||||
return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely.
|
||||
}
|
||||
if (device.state !== undefined) {
|
||||
return device.state === UPowerDeviceState.Charging;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPluggedIn(device) {
|
||||
if (!device || isBluetoothDevice(device)) {
|
||||
// Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?!
|
||||
return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely.
|
||||
}
|
||||
if (device.state !== undefined) {
|
||||
return device.state === UPowerDeviceState.FullyCharged || device.state === UPowerDeviceState.PendingCharge;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isCriticalBattery(device) {
|
||||
return (!isCharging(device) && !isPluggedIn(device)) && getPercentage(device) <= criticalThreshold;
|
||||
}
|
||||
|
||||
function isLowBattery(device) {
|
||||
return (!isCharging(device) && !isPluggedIn(device)) && getPercentage(device) <= warningThreshold && getPercentage(device) > criticalThreshold;
|
||||
}
|
||||
|
||||
function isBluetoothDevice(device) {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
// Check for Quickshell Bluetooth device property
|
||||
if (device.batteryAvailable !== undefined) {
|
||||
return true;
|
||||
}
|
||||
// Check for UPower device path indicating it's a Bluetooth device
|
||||
if (device.nativePath && (device.nativePath.includes("bluez") || device.nativePath.includes("bluetooth"))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getDeviceName(device) {
|
||||
if (!isDeviceReady(device)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!isBluetoothDevice(device) && device.isLaptopBattery) {
|
||||
// If there is more than one battery explicitly name them
|
||||
// Logger.e("BatteryDebug", "Available Battery count: " + laptopBatteries.length); // can be useful for debugging
|
||||
if (laptopBatteries.length > 1 && device.nativePath) {
|
||||
if (device.nativePath === "DisplayDevice") {
|
||||
return I18n.tr("battery.all-batteries");
|
||||
}
|
||||
var match = device.nativePath.match(/(\d+)$/);
|
||||
if (match) {
|
||||
// In case of 2 batteries: bat0 => bat1 bat1 => bat2
|
||||
return I18n.tr("common.battery") + " " + (parseInt(match[1]) + 1); // Append numbers
|
||||
}
|
||||
}
|
||||
// Return Battery if there is only one
|
||||
return I18n.tr("common.battery");
|
||||
}
|
||||
|
||||
if (isBluetoothDevice(device) && device.name) {
|
||||
return device.name;
|
||||
}
|
||||
|
||||
if (device.model) {
|
||||
return device.model;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function getIcon(percent, charging, pluggedIn, isReady) {
|
||||
if (!isReady) {
|
||||
return "battery-exclamation";
|
||||
}
|
||||
if (charging) {
|
||||
return "battery-charging";
|
||||
}
|
||||
if (pluggedIn) {
|
||||
return "battery-charging-2";
|
||||
}
|
||||
|
||||
const icons = [
|
||||
{
|
||||
threshold: 86,
|
||||
icon: "battery-4"
|
||||
},
|
||||
{
|
||||
threshold: 56,
|
||||
icon: "battery-3"
|
||||
},
|
||||
{
|
||||
threshold: 31,
|
||||
icon: "battery-2"
|
||||
},
|
||||
{
|
||||
threshold: 11,
|
||||
icon: "battery-1"
|
||||
},
|
||||
{
|
||||
threshold: 0,
|
||||
icon: "battery"
|
||||
}
|
||||
];
|
||||
|
||||
const match = icons.find(tier => percent >= tier.threshold);
|
||||
return match ? match.icon : "battery-off"; // New fallback icon clearly represent if nothing is true here.
|
||||
}
|
||||
|
||||
function getRateText(device) {
|
||||
if (!device || device.changeRate === undefined) {
|
||||
return "";
|
||||
}
|
||||
const rate = Math.abs(device.changeRate);
|
||||
if (device.timeToFull > 0) {
|
||||
return I18n.tr("battery.charging-rate", {
|
||||
"rate": rate.toFixed(2)
|
||||
});
|
||||
} else if (device.timeToEmpty > 0) {
|
||||
return I18n.tr("battery.discharging-rate", {
|
||||
"rate": rate.toFixed(2)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeRemainingText(device) {
|
||||
if (!isDeviceReady(device)) {
|
||||
return I18n.tr("battery.no-battery-detected");
|
||||
}
|
||||
if (isPluggedIn(device)) {
|
||||
return I18n.tr("battery.plugged-in");
|
||||
} else if (device.timeToFull > 0) {
|
||||
return I18n.tr("battery.time-until-full", {
|
||||
"time": Time.formatVagueHumanReadableDuration(device.timeToFull)
|
||||
});
|
||||
} else if (device.timeToEmpty > 0) {
|
||||
return I18n.tr("battery.time-left", {
|
||||
"time": Time.formatVagueHumanReadableDuration(device.timeToEmpty)
|
||||
});
|
||||
}
|
||||
return I18n.tr("common.idle");
|
||||
}
|
||||
|
||||
function checkDevice(device) {
|
||||
if (!device || !isDeviceReady(device)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const percentage = getPercentage(device);
|
||||
const charging = isCharging(device);
|
||||
const pluggedIn = isPluggedIn(device);
|
||||
const level = isLowBattery(device) ? "low" : (isCriticalBattery(device) ? "critical" : "");
|
||||
var deviceKey = device.nativePath;
|
||||
|
||||
if (!_hasNotified[deviceKey]) {
|
||||
_hasNotified[deviceKey] = {
|
||||
low: false,
|
||||
critical: false
|
||||
};
|
||||
}
|
||||
|
||||
if (charging || pluggedIn) {
|
||||
_hasNotified[deviceKey].low = false;
|
||||
_hasNotified[deviceKey].critical = false;
|
||||
}
|
||||
|
||||
if (percentage > warningThreshold) {
|
||||
_hasNotified[deviceKey].low = false;
|
||||
_hasNotified[deviceKey].critical = false;
|
||||
} else if (percentage > criticalThreshold) {
|
||||
_hasNotified[deviceKey].critical = false;
|
||||
}
|
||||
|
||||
if (level) {
|
||||
if (!_hasNotified[deviceKey][level]) {
|
||||
notify(device, level);
|
||||
_hasNotified[deviceKey][level] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notify(device, level) {
|
||||
if (!Settings.data.notifications.enableBatteryToast) {
|
||||
return;
|
||||
}
|
||||
var name = getDeviceName(device);
|
||||
var titleKey = level === "critical" ? "toast.battery.critical" : "toast.battery.low";
|
||||
var descKey = level === "critical" ? "toast.battery.critical-desc" : "toast.battery.low-desc";
|
||||
|
||||
var title = I18n.tr(titleKey);
|
||||
var desc = I18n.tr(descKey, {
|
||||
"percent": getPercentage(device)
|
||||
});
|
||||
var icon = level === "critical" ? "battery-exclamation" : "battery-charging-2";
|
||||
|
||||
if (device == _bluetoothBattery && name) {
|
||||
title = title + " " + name;
|
||||
}
|
||||
|
||||
// Only 'showNotice' supports custom icons
|
||||
ToastService.showNotice(title, desc, icon, 6000);
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: deviceModel
|
||||
delegate: Connections {
|
||||
required property var modelData
|
||||
property var device: findDevice(modelData.key)
|
||||
target: device
|
||||
|
||||
function onPercentageChanged() {
|
||||
if (device.isLaptopBattery && modelData.key !== "__default__") {
|
||||
return;
|
||||
}
|
||||
checkDevice(device);
|
||||
}
|
||||
function onStateChanged() {
|
||||
if (device.isLaptopBattery && modelData.key !== "__default__") {
|
||||
return;
|
||||
}
|
||||
checkDevice(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<var> ddcMonitors: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
property bool appleDisplayPresent: false
|
||||
property list<var> availableBacklightDevices: []
|
||||
|
||||
function getMonitorForScreen(screen: ShellScreen): var {
|
||||
return monitors.find(m => m.modelData === screen);
|
||||
}
|
||||
|
||||
// Signal emitted when a specific monitor's brightness changes, includes monitor context
|
||||
signal monitorBrightnessChanged(var monitor, real newBrightness)
|
||||
|
||||
function getAvailableMethods(): list<string> {
|
||||
var methods = [];
|
||||
if (Settings.data.brightness.enableDdcSupport && monitors.some(m => m.isDdc))
|
||||
methods.push("ddcutil");
|
||||
if (monitors.some(m => !m.isDdc))
|
||||
methods.push("internal");
|
||||
if (appleDisplayPresent)
|
||||
methods.push("apple");
|
||||
return methods;
|
||||
}
|
||||
|
||||
// Global helpers for IPC and shortcuts
|
||||
function increaseBrightness(): void {
|
||||
monitors.forEach(m => m.increaseBrightness());
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
monitors.forEach(m => m.decreaseBrightness());
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
monitors.forEach(m => m.setBrightnessDebounced(value));
|
||||
}
|
||||
|
||||
function getDetectedDisplays(): list<var> {
|
||||
return detectedDisplays;
|
||||
}
|
||||
|
||||
function normalizeBacklightDevicePath(devicePath): string {
|
||||
if (devicePath === undefined || devicePath === null)
|
||||
return "";
|
||||
|
||||
var normalized = String(devicePath).trim();
|
||||
if (normalized === "")
|
||||
return "";
|
||||
|
||||
if (normalized.startsWith("/sys/class/backlight/"))
|
||||
return normalized;
|
||||
|
||||
if (normalized.indexOf("/") === -1)
|
||||
return "/sys/class/backlight/" + normalized;
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getBacklightDeviceName(devicePath): string {
|
||||
var normalized = normalizeBacklightDevicePath(devicePath);
|
||||
if (normalized === "")
|
||||
return "";
|
||||
|
||||
var parts = normalized.split("/");
|
||||
while (parts.length > 0 && parts[parts.length - 1] === "") {
|
||||
parts.pop();
|
||||
}
|
||||
return parts.length > 0 ? parts[parts.length - 1] : "";
|
||||
}
|
||||
|
||||
function getMappedBacklightDevice(outputName): string {
|
||||
var normalizedOutput = String(outputName || "").trim();
|
||||
if (normalizedOutput === "")
|
||||
return "";
|
||||
|
||||
var mappings = Settings.data.brightness.backlightDeviceMappings || [];
|
||||
for (var i = 0; i < mappings.length; i++) {
|
||||
var mapping = mappings[i];
|
||||
if (!mapping || typeof mapping !== "object")
|
||||
continue;
|
||||
|
||||
if (String(mapping.output || "").trim() === normalizedOutput)
|
||||
return normalizeBacklightDevicePath(mapping.device || "");
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function setMappedBacklightDevice(outputName, devicePath): void {
|
||||
var normalizedOutput = String(outputName || "").trim();
|
||||
if (normalizedOutput === "")
|
||||
return;
|
||||
|
||||
var normalizedDevicePath = normalizeBacklightDevicePath(devicePath);
|
||||
var mappings = Settings.data.brightness.backlightDeviceMappings || [];
|
||||
var nextMappings = [];
|
||||
var replaced = false;
|
||||
|
||||
for (var i = 0; i < mappings.length; i++) {
|
||||
var mapping = mappings[i];
|
||||
if (!mapping || typeof mapping !== "object")
|
||||
continue;
|
||||
|
||||
var mappingOutput = String(mapping.output || "").trim();
|
||||
var mappingDevice = normalizeBacklightDevicePath(mapping.device || "");
|
||||
if (mappingOutput === "" || mappingDevice === "")
|
||||
continue;
|
||||
|
||||
if (mappingOutput === normalizedOutput) {
|
||||
if (!replaced && normalizedDevicePath !== "") {
|
||||
nextMappings.push({
|
||||
"output": normalizedOutput,
|
||||
"device": normalizedDevicePath
|
||||
});
|
||||
}
|
||||
replaced = true;
|
||||
} else {
|
||||
nextMappings.push({
|
||||
"output": mappingOutput,
|
||||
"device": mappingDevice
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!replaced && normalizedDevicePath !== "") {
|
||||
nextMappings.push({
|
||||
"output": normalizedOutput,
|
||||
"device": normalizedDevicePath
|
||||
});
|
||||
}
|
||||
|
||||
Settings.data.brightness.backlightDeviceMappings = nextMappings;
|
||||
}
|
||||
|
||||
function scanBacklightDevices(): void {
|
||||
if (!scanBacklightProc.running)
|
||||
scanBacklightProc.running = true;
|
||||
}
|
||||
|
||||
reloadableId: "brightness"
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("Brightness", "Service started");
|
||||
scanBacklightDevices();
|
||||
if (Settings.data.brightness.enableDdcSupport) {
|
||||
ddcProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = [];
|
||||
scanBacklightDevices();
|
||||
if (Settings.data.brightness.enableDdcSupport) {
|
||||
ddcProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.brightness
|
||||
function onEnableDdcSupportChanged() {
|
||||
if (Settings.data.brightness.enableDdcSupport) {
|
||||
// Re-detect DDC monitors when enabled
|
||||
ddcMonitors = [];
|
||||
ddcProc.running = true;
|
||||
} else {
|
||||
// Clear DDC monitors when disabled
|
||||
ddcMonitors = [];
|
||||
}
|
||||
}
|
||||
function onBacklightDeviceMappingsChanged() {
|
||||
scanBacklightDevices();
|
||||
for (var i = 0; i < monitors.length; i++) {
|
||||
var m = monitors[i];
|
||||
if (m && !m.isDdc && !m.isAppleDisplay)
|
||||
m.initBrightness();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: variants
|
||||
model: Quickshell.screens
|
||||
Monitor {}
|
||||
}
|
||||
|
||||
// Check for Apple Display support
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c", "which asdbctl >/dev/null 2>&1 && asdbctl get || echo ''"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.appleDisplayPresent = text.trim().length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Detect available internal backlight devices
|
||||
Process {
|
||||
id: scanBacklightProc
|
||||
command: ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$dev\"; fi; done"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var data = text.trim();
|
||||
if (data === "") {
|
||||
root.availableBacklightDevices = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = data.split("\n");
|
||||
var found = [];
|
||||
var seen = ({});
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var path = root.normalizeBacklightDevicePath(lines[i]);
|
||||
if (path === "" || seen[path])
|
||||
continue;
|
||||
seen[path] = true;
|
||||
found.push(path);
|
||||
}
|
||||
|
||||
root.availableBacklightDevices = found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect DDC monitors
|
||||
Process {
|
||||
id: ddcProc
|
||||
property list<var> ddcMonitors: []
|
||||
command: ["ddcutil", "detect", "--enable-dynamic-sleep", "--sleep-multiplier=0.5"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var displays = text.trim().split("\n\n");
|
||||
ddcProc.ddcMonitors = displays.map(d => {
|
||||
var ddcModelMatch = d.match(/(This monitor does not support DDC\/CI|Invalid display)/);
|
||||
var modelMatch = d.match(/Model:\s*(.*)/);
|
||||
var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/);
|
||||
var connectorMatch = d.match(/DRM[_ ]connector:\s*card\d+-(.+)/);
|
||||
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false;
|
||||
var model = modelMatch ? modelMatch[1] : "Unknown";
|
||||
var bus = busMatch ? busMatch[1] : "Unknown";
|
||||
var connector = connectorMatch ? connectorMatch[1].trim() : "";
|
||||
Logger.i("Brightness", "Detected DDC Monitor:", model, "connector:", connector, "bus:", bus, "is DDC:", !ddcModel);
|
||||
return {
|
||||
"model": model,
|
||||
"busNum": bus,
|
||||
"connector": connector,
|
||||
"isDdc": !ddcModel
|
||||
};
|
||||
});
|
||||
root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Monitor: QtObject {
|
||||
id: monitor
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property bool isDdc: Settings.data.brightness.enableDdcSupport && root.ddcMonitors.some(m => m.connector === modelData.name)
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
|
||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||
readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal")
|
||||
|
||||
// Check if brightness control is available for this monitor
|
||||
readonly property bool brightnessControlAvailable: {
|
||||
if (isAppleDisplay)
|
||||
return true;
|
||||
if (isDdc)
|
||||
return true;
|
||||
// For internal displays, check if we have a brightness path
|
||||
return brightnessPath !== "";
|
||||
}
|
||||
|
||||
property real brightness
|
||||
property real lastBrightness: 0
|
||||
property real queuedBrightness: NaN
|
||||
property bool commandRunning: false
|
||||
|
||||
// For internal displays - store the backlight device path
|
||||
property string backlightDevice: ""
|
||||
property string brightnessPath: ""
|
||||
property string maxBrightnessPath: ""
|
||||
property int maxBrightness: 100
|
||||
property bool ignoreNextChange: false
|
||||
property bool initInProgress: false
|
||||
|
||||
// Signal for brightness changes
|
||||
signal brightnessUpdated(real newBrightness)
|
||||
|
||||
// Execute a system command to get the current brightness value directly
|
||||
readonly property Process refreshProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var dataText = text.trim();
|
||||
if (dataText === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
var newBrightness = NaN;
|
||||
|
||||
if (monitor.isAppleDisplay) {
|
||||
// Apple display format: single integer (0-101)
|
||||
var val = parseInt(dataText);
|
||||
if (!isNaN(val)) {
|
||||
newBrightness = val / 101;
|
||||
}
|
||||
} else if (monitor.isDdc) {
|
||||
// DDC format: "VCP 10 C 100 100" (space-separated)
|
||||
var parts = dataText.split(" ");
|
||||
if (parts.length >= 4) {
|
||||
var current = parseInt(parts[3]);
|
||||
var max = parseInt(parts[4]);
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.maxBrightness = max;
|
||||
newBrightness = current / max;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Internal display format: two lines (current\nmax)
|
||||
var lines = dataText.split("\n");
|
||||
if (lines.length >= 2) {
|
||||
var current = parseInt(lines[0].trim());
|
||||
var max = parseInt(lines[1].trim());
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
newBrightness = current / max;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update if we got a valid brightness value
|
||||
if (!isNaN(newBrightness) && (Math.abs(newBrightness - monitor.brightness) > 0.001 || monitor.brightness === 0)) {
|
||||
monitor.brightness = newBrightness;
|
||||
monitor.brightnessUpdated(monitor.brightness);
|
||||
root.monitorBrightnessChanged(monitor, monitor.brightness);
|
||||
Logger.d("Brightness", "Refreshed brightness from system:", monitor.modelData.name, monitor.brightness);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Process setBrightnessProc: Process {
|
||||
stdout: StdioCollector {}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
monitor.commandRunning = false;
|
||||
// If there's a queued brightness change, process it now
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
Qt.callLater(() => {
|
||||
monitor.setBrightness(monitor.queuedBrightness);
|
||||
monitor.queuedBrightness = NaN;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to actively refresh the brightness from system
|
||||
function refreshBrightnessFromSystem() {
|
||||
if (!monitor.isDdc && !monitor.isAppleDisplay) {
|
||||
// For internal displays, query the system directly
|
||||
refreshProc.command = ["sh", "-c", "cat " + monitor.brightnessPath + " && " + "cat " + monitor.maxBrightnessPath];
|
||||
refreshProc.running = true;
|
||||
} else if (monitor.isDdc && monitor.busNum !== "") {
|
||||
// For DDC displays, get the current value
|
||||
refreshProc.command = ["ddcutil", "-b", monitor.busNum, "--enable-dynamic-sleep", "--sleep-multiplier=0.05", "getvcp", "10", "--brief"];
|
||||
refreshProc.running = true;
|
||||
} else if (monitor.isAppleDisplay) {
|
||||
// For Apple displays, get the current value
|
||||
refreshProc.command = ["asdbctl", "get"];
|
||||
refreshProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
// FileView to watch for external brightness changes (internal displays only)
|
||||
readonly property FileView brightnessWatcher: FileView {
|
||||
id: brightnessWatcher
|
||||
// Only set path for internal displays with a valid brightness path
|
||||
path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : ""
|
||||
watchChanges: path !== ""
|
||||
onFileChanged: {
|
||||
// When a file change is detected, actively refresh from system
|
||||
// to ensure we get the most up-to-date value
|
||||
Qt.callLater(() => {
|
||||
monitor.refreshBrightnessFromSystem();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize brightness
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var dataText = text.trim();
|
||||
if (dataText === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
//Logger.i("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
|
||||
if (monitor.isAppleDisplay) {
|
||||
var val = parseInt(dataText);
|
||||
if (!isNaN(val)) {
|
||||
monitor.brightness = val / 101;
|
||||
Logger.d("Brightness", "Apple display brightness:", monitor.brightness);
|
||||
}
|
||||
} else if (monitor.isDdc) {
|
||||
var parts = dataText.split(" ");
|
||||
if (parts.length >= 4) {
|
||||
var current = parseInt(parts[3]);
|
||||
var max = parseInt(parts[4]);
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.maxBrightness = max;
|
||||
monitor.brightness = current / max;
|
||||
Logger.d("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Internal backlight - parse the response which includes device path
|
||||
var lines = dataText.split("\n");
|
||||
if (lines.length >= 3) {
|
||||
monitor.backlightDevice = lines[0];
|
||||
monitor.brightnessPath = monitor.backlightDevice + "/brightness";
|
||||
monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness";
|
||||
|
||||
var current = parseInt(lines[1]);
|
||||
var max = parseInt(lines[2]);
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.maxBrightness = max;
|
||||
monitor.brightness = current / max;
|
||||
Logger.d("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness);
|
||||
Logger.d("Brightness", "Using backlight device:", monitor.backlightDevice);
|
||||
}
|
||||
} else {
|
||||
monitor.backlightDevice = "";
|
||||
monitor.brightnessPath = "";
|
||||
monitor.maxBrightnessPath = "";
|
||||
}
|
||||
}
|
||||
|
||||
monitor.initInProgress = false;
|
||||
}
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
monitor.initInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real stepSize: Settings.data.brightness.brightnessStep / 100.0
|
||||
readonly property real minBrightnessValue: (Settings.data.brightness.enforceMinimum ? 0.01 : 0.0)
|
||||
|
||||
// Timer for debouncing rapid changes
|
||||
readonly property Timer timer: Timer {
|
||||
interval: monitor.isDdc ? 250 : 33
|
||||
onTriggered: {
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
monitor.setBrightness(monitor.queuedBrightness);
|
||||
monitor.queuedBrightness = NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightnessDebounced(value: real): void {
|
||||
monitor.queuedBrightness = value;
|
||||
timer.start();
|
||||
}
|
||||
|
||||
function increaseBrightness(): void {
|
||||
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness;
|
||||
// Enforce minimum brightness if enabled
|
||||
if (Settings.data.brightness.enforceMinimum && value < minBrightnessValue) {
|
||||
setBrightnessDebounced(Math.max(stepSize, minBrightnessValue));
|
||||
} else {
|
||||
// Normal brightness increase
|
||||
setBrightnessDebounced(value + stepSize);
|
||||
}
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness;
|
||||
setBrightnessDebounced(value - stepSize);
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(minBrightnessValue, Math.min(1, value));
|
||||
var rounded = Math.round(value * 100);
|
||||
|
||||
// Always update internal value and trigger UI feedback immediately
|
||||
monitor.brightness = value;
|
||||
monitor.brightnessUpdated(value);
|
||||
root.monitorBrightnessChanged(monitor, monitor.brightness);
|
||||
|
||||
if (timer.running) {
|
||||
monitor.queuedBrightness = value;
|
||||
return;
|
||||
}
|
||||
|
||||
// If a command is already running, queue this value
|
||||
if (monitor.commandRunning) {
|
||||
monitor.queuedBrightness = value;
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the brightness change command
|
||||
if (isAppleDisplay) {
|
||||
monitor.commandRunning = true;
|
||||
monitor.ignoreNextChange = true;
|
||||
setBrightnessProc.command = ["asdbctl", "set", rounded];
|
||||
setBrightnessProc.running = true;
|
||||
} else if (isDdc && busNum !== "") {
|
||||
monitor.commandRunning = true;
|
||||
monitor.ignoreNextChange = true;
|
||||
var ddcValue = Math.round(value * monitor.maxBrightness);
|
||||
var ddcBus = busNum;
|
||||
Qt.callLater(() => {
|
||||
setBrightnessProc.command = ["ddcutil", "-b", ddcBus, "--noverify", "--async", "--enable-dynamic-sleep", "--sleep-multiplier=0.05", "setvcp", "10", ddcValue];
|
||||
setBrightnessProc.running = true;
|
||||
});
|
||||
} else if (!isDdc) {
|
||||
monitor.commandRunning = true;
|
||||
monitor.ignoreNextChange = true;
|
||||
var backlightDeviceName = root.getBacklightDeviceName(monitor.backlightDevice);
|
||||
if (backlightDeviceName !== "") {
|
||||
setBrightnessProc.command = ["brightnessctl", "-d", backlightDeviceName, "s", rounded + "%"];
|
||||
} else {
|
||||
setBrightnessProc.command = ["brightnessctl", "s", rounded + "%"];
|
||||
}
|
||||
setBrightnessProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
function initBrightness(): void {
|
||||
monitor.initInProgress = true;
|
||||
if (isAppleDisplay) {
|
||||
initProc.command = ["asdbctl", "get"];
|
||||
initProc.running = true;
|
||||
} else if (isDdc && busNum !== "") {
|
||||
initProc.command = ["ddcutil", "-b", busNum, "--enable-dynamic-sleep", "--sleep-multiplier=0.05", "getvcp", "10", "--brief"];
|
||||
initProc.running = true;
|
||||
} else if (!isDdc) {
|
||||
// Internal backlight: first try explicit output mapping, then fall back to first available.
|
||||
var preferredDevicePath = root.getMappedBacklightDevice(modelData.name);
|
||||
var probeScript = ["preferred=\"$1\"", "if [ -n \"$preferred\" ] && [ ! -d \"$preferred\" ]; then preferred=\"/sys/class/backlight/$preferred\"; fi", "selected=\"\"",
|
||||
"if [ -n \"$preferred\" ] && [ -f \"$preferred/brightness\" ] && [ -f \"$preferred/max_brightness\" ]; then selected=\"$preferred\"; else for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then selected=\"$dev\"; break; fi; done; fi",
|
||||
"if [ -n \"$selected\" ]; then echo \"$selected\"; cat \"$selected/brightness\"; cat \"$selected/max_brightness\"; fi"].join("; ");
|
||||
initProc.command = ["sh", "-c", probeScript, "sh", preferredDevicePath];
|
||||
initProc.running = true;
|
||||
} else {
|
||||
monitor.initInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
onBusNumChanged: initBrightness()
|
||||
onIsDdcChanged: initBrightness()
|
||||
Component.onCompleted: initBrightness()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user