summaryrefslogtreecommitdiffstats
path: root/src/detection/brightness
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/detection/brightness
Add the files
Diffstat (limited to 'src/detection/brightness')
-rw-r--r--src/detection/brightness/brightness.h26
-rw-r--r--src/detection/brightness/brightness_apple.c244
-rw-r--r--src/detection/brightness/brightness_bsd.c147
-rw-r--r--src/detection/brightness/brightness_haiku.cpp46
-rw-r--r--src/detection/brightness/brightness_linux.c176
-rw-r--r--src/detection/brightness/brightness_nbsd.c26
-rw-r--r--src/detection/brightness/brightness_nosupport.c5
-rw-r--r--src/detection/brightness/brightness_obsd.c44
-rw-r--r--src/detection/brightness/brightness_windows.c223
9 files changed, 937 insertions, 0 deletions
diff --git a/src/detection/brightness/brightness.h b/src/detection/brightness/brightness.h
new file mode 100644
index 0000000..abb9478
--- /dev/null
+++ b/src/detection/brightness/brightness.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "fastfetch.h"
+#include "modules/brightness/option.h"
+
+typedef struct FFBrightnessResult {
+ FFstrbuf name;
+ double min, max, current;
+ bool builtin;
+} FFBrightnessResult;
+
+enum {
+ FF_DDC_EDID_ADDR = 0x50u,
+ FF_DDC_CI_ADDR = 0x37u,
+ FF_DDC_CI_WRITE_ADDR = FF_DDC_CI_ADDR << 1,
+ FF_DDC_CI_READ_ADDR = FF_DDC_CI_WRITE_ADDR | 1,
+ FF_DDC_CI_VCP_COMMAND = 0x51u,
+ FF_DDC_CI_GET_VCP = 0x01u,
+ FF_DDC_CI_SET_VCP = 0x03u,
+ FF_DDC_CI_COMMAND_PACKET = 0x80u,
+ FF_DDC_CI_LUMINANCE_OPCODE = 0x10u,
+};
+#define FF_BRIGHTNESS_DDCCI_SLEEP_SKIP ((uint32_t) -1)
+#define FF_DDC_CI_MAKE_HEADER(len) (FF_DDC_CI_COMMAND_PACKET | ((len) & 0x7F))
+
+const char* ffDetectBrightness(FFBrightnessOptions* options, FFlist* result); // list of FFBrightnessResult
diff --git a/src/detection/brightness/brightness_apple.c b/src/detection/brightness/brightness_apple.c
new file mode 100644
index 0000000..5dd12b2
--- /dev/null
+++ b/src/detection/brightness/brightness_apple.c
@@ -0,0 +1,244 @@
+#include "brightness.h"
+#include "detection/displayserver/displayserver.h"
+#include "common/apple/cf_helpers.h"
+#include "common/edidHelper.h"
+#include "common/time.h"
+
+#include <CoreGraphics/CoreGraphics.h>
+
+// DDC/CI
+#ifdef __aarch64__
+typedef CFTypeRef IOAVServiceRef;
+extern IOAVServiceRef IOAVServiceCreate(CFAllocatorRef allocator) FF_A_WEAK_IMPORT;
+extern IOAVServiceRef IOAVServiceCreateWithService(CFAllocatorRef allocator, io_service_t service) FF_A_WEAK_IMPORT;
+extern IOReturn IOAVServiceCopyEDID(IOAVServiceRef service, CFDataRef* x2) FF_A_WEAK_IMPORT;
+extern IOReturn IOAVServiceReadI2C(IOAVServiceRef service, uint32_t chipAddress, uint32_t offset, void* outputBuffer, uint32_t outputBufferSize) FF_A_WEAK_IMPORT;
+extern IOReturn IOAVServiceWriteI2C(IOAVServiceRef service, uint32_t chipAddress, uint32_t dataAddress, void* inputBuffer, uint32_t inputBufferSize) FF_A_WEAK_IMPORT;
+#else
+ // DDC/CI (Intel)
+ #include <IOKit/IOKitLib.h>
+ #include <IOKit/graphics/IOGraphicsLib.h>
+ #include <IOKit/i2c/IOI2CInterface.h>
+extern void CGSServiceForDisplayNumber(CGDirectDisplayID display, io_service_t* service) FF_A_WEAK_IMPORT;
+#endif
+
+// ACPI
+extern int DisplayServicesGetBrightness(CGDirectDisplayID display, float* brightness) FF_A_WEAK_IMPORT;
+
+// Works for internal display
+static const char* detectWithDisplayServices(const FFDisplayServerResult* displayServer, FFlist* result) {
+ if (DisplayServicesGetBrightness == NULL) {
+ return "DisplayServices function DisplayServicesGetBrightness is not available";
+ }
+
+ FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) {
+ if (display->type == FF_DISPLAY_TYPE_BUILTIN || display->type == FF_DISPLAY_TYPE_UNKNOWN) {
+ float value;
+ if (DisplayServicesGetBrightness((CGDirectDisplayID) display->id, &value) == kCGErrorSuccess) {
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ brightness->current = value;
+ brightness->max = 1;
+ brightness->min = 0;
+ ffStrbufInitCopy(&brightness->name, &display->name);
+ brightness->builtin = true;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+#ifdef __aarch64__
+// https://github.com/waydabber/m1ddc
+// Works for Apple Silicon and USB-C adapter connection ( but not HTMI )
+static const char* detectWithDdcci(FF_A_UNUSED const FFDisplayServerResult* displayServer, FFBrightnessOptions* options, FFlist* result) {
+ if (!IOAVServiceCreate || !IOAVServiceReadI2C || !IOAVServiceWriteI2C) {
+ return "IOAVService is not available";
+ }
+
+ FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL;
+ if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("DCPAVServiceProxy"), &iterator) != kIOReturnSuccess) {
+ return "IOServiceGetMatchingServices() failed";
+ }
+
+ io_registry_entry_t registryEntry;
+ while ((registryEntry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
+ FF_CFTYPE_AUTO_RELEASE IOAVServiceRef service = NULL;
+ {
+ FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryAv = registryEntry;
+
+ FF_CFTYPE_AUTO_RELEASE CFBooleanRef IOAVServiceUserInterfaceSupported = IORegistryEntryCreateCFProperty(entryAv, CFSTR("IOAVServiceUserInterfaceSupported"), kCFAllocatorDefault, kNilOptions);
+ if (IOAVServiceUserInterfaceSupported && !CFBooleanGetValue(IOAVServiceUserInterfaceSupported)) {
+ // IOAVServiceCreateWithService won't work
+ continue;
+ }
+
+ FF_CFTYPE_AUTO_RELEASE CFStringRef location = IORegistryEntryCreateCFProperty(entryAv, CFSTR("Location"), kCFAllocatorDefault, kNilOptions);
+ if (location && CFStringCompare(location, CFSTR("Embedded"), 0) == 0) {
+ // Builtin display should be handled by DisplayServices
+ continue;
+ }
+
+ service = IOAVServiceCreateWithService(kCFAllocatorDefault, (io_service_t) registryEntry);
+ if (!service) {
+ continue;
+ }
+ }
+
+ {
+ uint8_t i2cIn[4] = { FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE };
+ i2cIn[3] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2];
+
+ for (uint32_t i = 0; i < 2; ++i) {
+ IOAVServiceWriteI2C(service, FF_DDC_CI_ADDR, FF_DDC_CI_VCP_COMMAND, i2cIn, ARRAY_SIZE(i2cIn));
+ ffTimeSleep(options->ddcciSleep);
+ }
+ }
+
+ uint8_t i2cOut[12] = {};
+ if (IOAVServiceReadI2C(service, FF_DDC_CI_ADDR, FF_DDC_CI_VCP_COMMAND, i2cOut, ARRAY_SIZE(i2cOut)) == KERN_SUCCESS) {
+ if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) {
+ continue;
+ }
+
+ uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9];
+ uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7];
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ brightness->max = max;
+ brightness->min = 0;
+ brightness->current = current;
+ ffStrbufInit(&brightness->name);
+ brightness->builtin = false;
+
+ uint8_t edid[128] = {};
+ if (IOAVServiceReadI2C(service, 0x50, 0x00, edid, ARRAY_SIZE(edid)) == KERN_SUCCESS) {
+ ffEdidGetName(edid, &brightness->name);
+ }
+ }
+ }
+
+ return NULL;
+}
+#else
+static IOOptionBits getSupportedTransactionType(void) {
+ FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL;
+
+ if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceNameMatching("IOFramebufferI2CInterface"), &iterator) != KERN_SUCCESS) {
+ return kIOI2CNoTransactionType;
+ }
+
+ io_registry_entry_t registryEntry;
+ while ((registryEntry = IOIteratorNext(iterator)) != MACH_PORT_NULL) {
+ FF_IOOBJECT_AUTO_RELEASE io_service_t io_service = registryEntry;
+ FF_CFTYPE_AUTO_RELEASE CFNumberRef IOI2CTransactionTypes = IORegistryEntryCreateCFProperty(io_service, CFSTR(kIOI2CTransactionTypesKey), kCFAllocatorDefault, kNilOptions);
+
+ if (IOI2CTransactionTypes) {
+ int64_t types = 0;
+ ffCfNumGetInt64(IOI2CTransactionTypes, &types);
+
+ if (types) {
+ if ((1 << kIOI2CDDCciReplyTransactionType) & (uint64_t) types) {
+ return kIOI2CDDCciReplyTransactionType;
+ }
+ if ((1 << kIOI2CSimpleTransactionType) & (uint64_t) types) {
+ return kIOI2CSimpleTransactionType;
+ }
+ }
+ }
+ break;
+ }
+
+ return kIOI2CNoTransactionType;
+}
+
+static const char* detectWithDdcci(const FFDisplayServerResult* displayServer, FFBrightnessOptions* options, FFlist* result) {
+ if (!CGSServiceForDisplayNumber) {
+ return "CGSServiceForDisplayNumber is not available";
+ }
+ IOOptionBits transactionType = getSupportedTransactionType();
+ if (transactionType == kIOI2CNoTransactionType) {
+ return "No supported IOI2C transaction type found";
+ }
+
+ FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) {
+ if (display->type == FF_DISPLAY_TYPE_EXTERNAL) {
+ FF_IOOBJECT_AUTO_RELEASE io_service_t framebuffer = IO_OBJECT_NULL;
+ CGSServiceForDisplayNumber((CGDirectDisplayID) display->id, &framebuffer);
+ if (framebuffer == IO_OBJECT_NULL) {
+ continue;
+ }
+
+ IOItemCount count;
+ if (IOFBGetI2CInterfaceCount(framebuffer, &count) != KERN_SUCCESS || count == 0) {
+ continue;
+ }
+
+ for (IOItemCount bus = 0; bus < count; ++bus) {
+ FF_IOOBJECT_AUTO_RELEASE io_service_t interface = IO_OBJECT_NULL;
+ if (IOFBCopyI2CInterfaceForBus(framebuffer, bus, &interface) != KERN_SUCCESS) {
+ continue;
+ }
+
+ uint8_t i2cOut[12] = {};
+ IOI2CConnectRef connect = NULL;
+ if (IOI2CInterfaceOpen(interface, kNilOptions, &connect) != KERN_SUCCESS) {
+ continue;
+ }
+
+ uint8_t i2cIn[] = { FF_DDC_CI_VCP_COMMAND, FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE, 0 };
+ i2cIn[4] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2] ^ i2cIn[3];
+
+ IOI2CRequest request = {
+ .commFlags = kNilOptions,
+ .sendAddress = FF_DDC_CI_WRITE_ADDR,
+ .sendTransactionType = kIOI2CSimpleTransactionType,
+ .sendBuffer = (vm_address_t) i2cIn,
+ .sendBytes = ARRAY_SIZE(i2cIn),
+ .minReplyDelay = options->ddcciSleep * 1000ULL,
+ .replyAddress = FF_DDC_CI_READ_ADDR,
+ .replySubAddress = FF_DDC_CI_VCP_COMMAND,
+ .replyTransactionType = transactionType,
+ .replyBytes = ARRAY_SIZE(i2cOut),
+ .replyBuffer = (vm_address_t) i2cOut,
+ };
+ IOReturn ret = IOI2CSendRequest(connect, kNilOptions, &request);
+ IOI2CInterfaceClose(connect, kNilOptions);
+
+ if (ret != KERN_SUCCESS || request.result != kIOReturnSuccess || request.replyBytes < 10) {
+ continue;
+ }
+ if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) {
+ continue;
+ }
+
+ uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9];
+ uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7];
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ brightness->max = max;
+ brightness->min = 0;
+ brightness->current = current;
+ ffStrbufInitCopy(&brightness->name, &display->name);
+ brightness->builtin = false;
+
+ break;
+ }
+ }
+ }
+
+ return NULL;
+}
+#endif
+
+const char* ffDetectBrightness(FFBrightnessOptions* options, FFlist* result) {
+ const FFDisplayServerResult* displayServer = ffConnectDisplayServer();
+
+ detectWithDisplayServices(displayServer, result);
+
+ if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && displayServer->displays.length > result->length) {
+ detectWithDdcci(displayServer, options, result);
+ }
+
+ return NULL;
+}
diff --git a/src/detection/brightness/brightness_bsd.c b/src/detection/brightness/brightness_bsd.c
new file mode 100644
index 0000000..5ff411c
--- /dev/null
+++ b/src/detection/brightness/brightness_bsd.c
@@ -0,0 +1,147 @@
+#include "brightness.h"
+#include "common/arrutil.h"
+#include "common/io.h"
+#include "common/kmod.h"
+#include "common/debug.h"
+#include "common/time.h"
+
+#include <sys/ioctl.h>
+#include <sys/fcntl.h>
+#include <unistd.h>
+
+#if __has_include(<dev/iicbus/iic.h>)
+ #include <dev/iicbus/iic.h>
+
+const char* detectWithDdcci(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ // FIXME: doesn't work for me
+ for (char i = '0'; i <= '9'; ++i) {
+ char path[] = "/dev/iic0";
+ path[ARRAY_SIZE(path) - 2] = i;
+
+ FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC);
+ if (fd < 0) {
+ int err = errno;
+ if (err == ENOENT) {
+ FF_DEBUG("No more I2C devices");
+ break;
+ } else if (i == '0' && !ffKmodLoaded("iicbus")) {
+ FF_DEBUG("iicbus module is not loaded");
+ return "No I2C devices found and iicbus module is not loaded";
+ }
+ FF_DEBUG("open(/dev/iic%c) failed: %s", i, strerror(errno));
+ continue;
+ }
+
+ uint8_t i2cIn[] = { FF_DDC_CI_VCP_COMMAND, FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE, 0 };
+ i2cIn[4] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2] ^ i2cIn[3];
+
+ int ret = ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){ .msgs = &(struct iic_msg){ .slave = FF_DDC_CI_WRITE_ADDR, .flags = IIC_M_WR, .len = ARRAY_SIZE(i2cIn), .buf = i2cIn }, .nmsgs = 1 });
+ if (ret < 0) {
+ FF_DEBUG("First ioctl(/dev/iic%c, I2CRDWR) failed: %s", i, strerror(errno));
+ continue;
+ }
+
+ ffTimeSleep(options->ddcciSleep);
+
+ uint8_t i2cOut[12] = {};
+ ret = ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){ .msgs = &(struct iic_msg){ .slave = FF_DDC_CI_READ_ADDR, // LSB will be overridden by kernel to set read bit
+ .flags = IIC_M_RD,
+ .len = ARRAY_SIZE(i2cOut),
+ .buf = i2cOut },
+ .nmsgs = 1 });
+ if (ret < 0) {
+ FF_DEBUG("Second ioctl(/dev/iic%c, I2CRDWR) failed: %s", i, strerror(errno));
+ continue;
+ }
+ if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) {
+ FF_DEBUG("i2c out validation failed: i2cOut[2] = 0x%02x, i2cOut[3] = 0x%02x", i2cOut[2], i2cOut[3]);
+ continue;
+ }
+
+ uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9];
+ uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7];
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ brightness->max = max;
+ brightness->min = 0;
+ brightness->current = current;
+ ffStrbufInitS(&brightness->name, path + strlen("/dev/"));
+ brightness->builtin = false;
+ }
+
+ return NULL;
+}
+
+#else
+
+const char* detectWithDdcci(FF_A_UNUSED FFBrightnessOptions* options, FF_A_UNUSED FFlist* result) {
+ FF_DEBUG("DDC/CI support is not available on this system");
+ return "DDC/CI is supported only on FreeBSD";
+}
+
+#endif
+
+#if __has_include(<sys/backlight.h>)
+ #include <sys/backlight.h>
+
+const char* detectWithBacklight(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ // https://man.freebsd.org/cgi/man.cgi?query=backlight&sektion=9
+ char path[] = "/dev/backlight/backlight0";
+
+ for (char i = '0'; i <= '9'; ++i) {
+ path[ARRAY_SIZE(path) - 2] = i;
+
+ FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ int err = errno;
+ if (err == ENOENT) {
+ FF_DEBUG("No more backlight devices");
+ break;
+ } else if (i == '0' && !ffKmodLoaded("backlight")) {
+ FF_DEBUG("backlight module is not loaded");
+ return "No backlight devices found and backlight module is not loaded";
+ }
+ FF_DEBUG("open(/dev/backlight/backlight%c) failed: %s", i, strerror(errno));
+ continue;
+ }
+
+ struct backlight_props status;
+ if (ioctl(fd, BACKLIGHTGETSTATUS, &status) < 0) {
+ continue;
+ }
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ ffStrbufInit(&brightness->name);
+
+ brightness->max = BACKLIGHTMAXLEVELS;
+ brightness->min = 0;
+ brightness->current = status.brightness;
+ brightness->builtin = true;
+
+ struct backlight_info info;
+ if (ioctl(fd, BACKLIGHTGETINFO, &info) == 0) {
+ ffStrbufAppendS(&brightness->name, info.name);
+ } else {
+ ffStrbufAppendS(&brightness->name, path + strlen("/dev/backlight/"));
+ }
+ }
+ return NULL;
+}
+
+#else
+
+const char* detectWithBacklight(FF_A_UNUSED FFBrightnessOptions* options, FF_A_UNUSED FFlist* result) {
+ FF_DEBUG("Backlight support is not available on this system");
+ return "Backlight is supported only on FreeBSD 13 and newer";
+}
+
+#endif
+
+const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ detectWithBacklight(options, result);
+
+ if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && result->length == 0) {
+ detectWithDdcci(options, result);
+ }
+ return NULL;
+}
diff --git a/src/detection/brightness/brightness_haiku.cpp b/src/detection/brightness/brightness_haiku.cpp
new file mode 100644
index 0000000..86fbe6d
--- /dev/null
+++ b/src/detection/brightness/brightness_haiku.cpp
@@ -0,0 +1,46 @@
+extern "C" {
+#include "brightness.h"
+#include "common/strutil.h"
+}
+
+#include <Application.h>
+#include <Screen.h>
+
+const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ // We need a valid be_app to query the app_server here.
+ BApplication app("application/x-vnd.fastfetch-cli-fastfetch");
+ BScreen screen{}; // default screen is the main one
+
+ do {
+ if (!screen.IsValid()) {
+ continue;
+ }
+
+ float value = 1.0f;
+ status_t status = screen.GetBrightness(&value);
+ if (status != B_OK) {
+ continue;
+ }
+
+ monitor_info monitor;
+ // WARNING: This is experimental new Haiku API
+ status_t err = screen.GetMonitorInfo(&monitor);
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+
+ if (err == B_OK) {
+ ffStrbufInitF(&brightness->name, "%s %s (%d)", monitor.vendor, monitor.name, screen.ID().id);
+ ffStrbufTrimRightSpace(&brightness->name);
+ } else {
+ ffStrbufInitF(&brightness->name, "Screen %d", screen.ID().id);
+ }
+
+ brightness->max = 1.0f;
+ brightness->min = 0.0f;
+ brightness->current = value;
+ brightness->builtin = true;
+
+ } while (screen.SetToNext() == B_OK);
+
+ return NULL;
+}
diff --git a/src/detection/brightness/brightness_linux.c b/src/detection/brightness/brightness_linux.c
new file mode 100644
index 0000000..e7125f8
--- /dev/null
+++ b/src/detection/brightness/brightness_linux.c
@@ -0,0 +1,176 @@
+#include "brightness.h"
+#include "common/io.h"
+#include "common/edidHelper.h"
+#include "common/strutil.h"
+
+#include <dirent.h>
+#include <limits.h>
+
+static const char* detectWithBacklight(FFlist* result) {
+ // https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-backlight
+ const char* backlightDirPath = "/sys/class/backlight/";
+
+ DIR* dirp = opendir(backlightDirPath);
+ if (dirp == NULL) {
+ return "Failed to open `/sys/class/backlight/`";
+ }
+
+ FF_STRBUF_AUTO_DESTROY backlightDir = ffStrbufCreateA(64);
+ ffStrbufAppendS(&backlightDir, backlightDirPath);
+
+ uint32_t backlightDirLength = backlightDir.length;
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+
+ struct dirent* entry;
+ while ((entry = readdir(dirp)) != NULL) {
+ if (entry->d_name[0] == '.') {
+ continue;
+ }
+
+ ffStrbufAppendS(&backlightDir, entry->d_name);
+ ffStrbufAppendS(&backlightDir, "/brightness");
+ if (ffReadFileBuffer(backlightDir.chars, &buffer)) {
+ double actualBrightness = ffStrbufToDouble(&buffer, 0);
+ ffStrbufSubstrBefore(&backlightDir, backlightDirLength);
+ ffStrbufAppendS(&backlightDir, entry->d_name);
+ ffStrbufAppendS(&backlightDir, "/max_brightness");
+ if (ffReadFileBuffer(backlightDir.chars, &buffer)) {
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ ffStrbufSubstrBeforeLastC(&backlightDir, '/');
+ ffStrbufAppendS(&backlightDir, "/device");
+ ffStrbufInitA(&brightness->name, PATH_MAX);
+ if (realpath(backlightDir.chars, brightness->name.chars)) {
+ ffStrbufRecalculateLength(&brightness->name);
+ // if we managed to get edid, use it
+ ffStrbufAppendS(&brightness->name, "/edid");
+ uint8_t edidData[128];
+ if (ffReadFileData(brightness->name.chars, ARRAY_SIZE(edidData), edidData) == ARRAY_SIZE(edidData)) {
+ ffStrbufClear(&brightness->name);
+ ffEdidGetName(edidData, &brightness->name);
+ } else {
+ ffStrbufSubstrBeforeLastC(&brightness->name, '/'); // remove "/edid"
+ ffStrbufSubstrAfterLastC(&brightness->name, '/'); // try getting DRM connector name
+ if (ffCharIsDigit(brightness->name.chars[0])) {
+ // PCI address or some unknown path, give up
+ ffStrbufSetS(&brightness->name, entry->d_name);
+ } else {
+ if (ffStrbufStartsWithS(&brightness->name, "card") && ffCharIsDigit(brightness->name.chars[4])) {
+ ffStrbufSubstrAfterFirstC(&brightness->name, '-');
+ }
+ }
+ }
+ } else {
+ ffStrbufInitS(&brightness->name, entry->d_name);
+ }
+ brightness->max = ffStrbufToDouble(&buffer, 0);
+ brightness->min = 0;
+ brightness->current = actualBrightness;
+ brightness->builtin = true;
+ }
+ }
+ ffStrbufSubstrBefore(&backlightDir, backlightDirLength);
+ }
+
+ closedir(dirp);
+
+ return NULL;
+}
+
+#ifdef FF_HAVE_DDCUTIL
+ #include "detection/displayserver/displayserver.h"
+ #include "common/library.h"
+ #include "common/mallocHelper.h"
+
+ #include <ddcutil_macros.h>
+ #include <ddcutil_c_api.h>
+
+ // Try to be compatible with ddcutil 2.0
+ #if DDCUTIL_VMAJOR >= 2
+double ddca_set_default_sleep_multiplier(double multiplier); // ddcutil 1.4
+ #else
+DDCA_Status ddca_init(const char* libopts, int syslog_level, int opts);
+ #endif
+
+static const char* detectWithDdcci(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ FF_LIBRARY_LOAD_MESSAGE(libddcutil, "libddcutil" FF_LIBRARY_EXTENSION, 5);
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_get_display_info_list2)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_open_display2)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_get_any_vcp_value_using_explicit_type)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_free_any_vcp_value)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_close_display)
+
+ #ifndef FF_DISABLE_DLOPEN
+ FF_LIBRARY_LOAD_SYMBOL_LAZY(libddcutil, ddca_init)
+ if (ffddca_init) {
+ FF_SUPPRESS_IO();
+ // Ref: https://github.com/rockowitz/ddcutil/issues/344
+ if (ffddca_init(NULL, -1 /*DDCA_SYSLOG_NOT_SET*/, 1 /*DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE*/) < 0) {
+ return "ddca_init() failed";
+ }
+ } else {
+ FF_LIBRARY_LOAD_SYMBOL_LAZY(libddcutil, ddca_set_default_sleep_multiplier);
+ if (ffddca_set_default_sleep_multiplier) {
+ ffddca_set_default_sleep_multiplier(options->ddcciSleep / 40.0);
+ }
+
+ libddcutil = NULL; // Don't dlclose libddcutil. See https://github.com/rockowitz/ddcutil/issues/330
+ }
+ #else
+ #if DDCUTIL_VMAJOR >= 2
+ if (ddca_init(NULL, -1 /*DDCA_SYSLOG_NOT_SET*/, 1 /*DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE*/) < 0) {
+ return "ddca_init() failed";
+ }
+ #else
+ ddca_set_default_sleep_multiplier(options->ddcciSleep / 40.0);
+ #endif
+ #endif
+
+ FF_AUTO_FREE DDCA_Display_Info_List* infoList = NULL;
+ if (ffddca_get_display_info_list2(false, &infoList) < 0) {
+ return "ddca_get_display_info_list2(false, &infoList) failed";
+ }
+
+ if (infoList->ct == 0) {
+ return "No DDC/CI compatible displays found";
+ }
+
+ for (int index = 0; index < infoList->ct; ++index) {
+ const DDCA_Display_Info* display = &infoList->info[index];
+
+ DDCA_Display_Handle handle;
+ if (ffddca_open_display2(display->dref, false, &handle) >= 0) {
+ DDCA_Any_Vcp_Value* vcpValue = NULL;
+ if (ffddca_get_any_vcp_value_using_explicit_type(handle, 0x10 /*brightness*/, DDCA_NON_TABLE_VCP_VALUE, &vcpValue) >= 0) {
+ assert(vcpValue->value_type == DDCA_NON_TABLE_VCP_VALUE);
+ int current = VALREC_CUR_VAL(vcpValue), max = VALREC_MAX_VAL(vcpValue);
+ ffddca_free_any_vcp_value(vcpValue);
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ brightness->max = max;
+ brightness->min = 0;
+ brightness->current = current;
+ ffStrbufInitS(&brightness->name, display->model_name);
+ }
+ ffddca_close_display(handle);
+ }
+ }
+
+ return NULL;
+}
+#endif
+
+const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ detectWithBacklight(result);
+
+#ifdef FF_HAVE_DDCUTIL
+ if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) {
+ const FFDisplayServerResult* displayServer = ffConnectDisplayServer();
+ if (result->length < displayServer->displays.length) {
+ detectWithDdcci(options, result);
+ }
+ }
+#endif
+
+ return NULL;
+}
diff --git a/src/detection/brightness/brightness_nbsd.c b/src/detection/brightness/brightness_nbsd.c
new file mode 100644
index 0000000..afd6656
--- /dev/null
+++ b/src/detection/brightness/brightness_nbsd.c
@@ -0,0 +1,26 @@
+#include "brightness.h"
+
+#include "common/sysctl.h"
+
+const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ // https://man.netbsd.org/NetBSD-10.1/acpiout.4#DESCRIPTION
+ char key[] = "hw.acpi.acpiout0.brightness";
+ char* pn = key + strlen("hw.acpi.acpiout");
+
+ for (uint32_t i = 0; i <= 9; ++i) {
+ *pn = (char) ('0' + i);
+ int value = ffSysctlGetInt(key, -1);
+ if (value == -1) {
+ continue;
+ }
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ ffStrbufInitF(&brightness->name, "acpiout%d", i);
+
+ brightness->max = 100;
+ brightness->min = 0;
+ brightness->current = value;
+ brightness->builtin = true;
+ }
+ return NULL;
+}
diff --git a/src/detection/brightness/brightness_nosupport.c b/src/detection/brightness/brightness_nosupport.c
new file mode 100644
index 0000000..6e9284e
--- /dev/null
+++ b/src/detection/brightness/brightness_nosupport.c
@@ -0,0 +1,5 @@
+#include "brightness.h"
+
+const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FF_A_UNUSED FFlist* result) {
+ return "Not supported on this platform";
+}
diff --git a/src/detection/brightness/brightness_obsd.c b/src/detection/brightness/brightness_obsd.c
new file mode 100644
index 0000000..102a8c2
--- /dev/null
+++ b/src/detection/brightness/brightness_obsd.c
@@ -0,0 +1,44 @@
+#include "brightness.h"
+#include "common/io.h"
+
+#include <dev/wscons/wsconsio.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ char path[] = "/dev/ttyCX";
+ for (char i = '0'; i <= '9'; ++i) {
+ path[strlen("/dev/ttyC")] = i;
+
+ FF_AUTO_CLOSE_FD int devfd = open(path, O_RDONLY | O_CLOEXEC);
+
+ if (devfd < 0) {
+ if (errno == EACCES && i == '0') {
+ return "Permission denied when opening tty device";
+ }
+ if (errno == ENOENT) {
+ break;
+ }
+ continue;
+ }
+
+ struct wsdisplay_param param = {
+ .param = WSDISPLAYIO_PARAM_BRIGHTNESS,
+ };
+
+ if (ioctl(devfd, WSDISPLAYIO_GETPARAM, &param) < 0) {
+ continue;
+ }
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ ffStrbufInitF(&brightness->name, "ttyC%c", i);
+
+ brightness->max = param.max;
+ brightness->min = param.min;
+ brightness->current = param.curval;
+ brightness->builtin = true;
+ }
+
+ return NULL;
+}
diff --git a/src/detection/brightness/brightness_windows.c b/src/detection/brightness/brightness_windows.c
new file mode 100644
index 0000000..70b7f01
--- /dev/null
+++ b/src/detection/brightness/brightness_windows.c
@@ -0,0 +1,223 @@
+#include "brightness.h"
+#include "detection/displayserver/displayserver.h"
+#include "common/debug.h"
+#include "common/library.h"
+#include "common/mallocHelper.h"
+#include "common/windows/wmi.h"
+#include "common/windows/unicode.h"
+
+#include <inttypes.h>
+#include <winternl.h>
+#include <windows.h>
+
+NTSYSAPI NTSTATUS WINAPI GetPhysicalMonitors(
+ _In_ UNICODE_STRING* pstrDeviceName,
+ _In_ DWORD dwPhysicalMonitorArraySize,
+ _Out_ DWORD* pdwNumPhysicalMonitorHandlesInArray,
+ _Out_ HANDLE* phPhysicalMonitorArray);
+
+typedef enum _MC_VCP_CODE_TYPE {
+ MC_MOMENTARY,
+ MC_SET_PARAMETER
+} MC_VCP_CODE_TYPE,
+ *LPMC_VCP_CODE_TYPE;
+
+NTSYSAPI NTSTATUS WINAPI DDCCIGetVCPFeature(
+ _In_ HANDLE hMonitor,
+ _In_ DWORD dwVCPCode,
+ _Out_opt_ LPMC_VCP_CODE_TYPE pvct,
+ _Out_ DWORD* pdwCurrentValue,
+ _Out_opt_ DWORD* pdwMaximumValue);
+
+NTSYSAPI NTSTATUS WINAPI DestroyPhysicalMonitorInternal(
+ _In_ HANDLE hMonitor);
+
+NTSTATUS WINAPI GetPhysicalMonitorDescription(
+ _In_ HANDLE hMonitor,
+ _In_ DWORD dwPhysicalMonitorDescriptionSizeInChars,
+ _Out_ LPWSTR szPhysicalMonitorDescription);
+
+static const char* detectWithWmi(FFlist* result) {
+ FF_DEBUG("WMI: start detection");
+
+ // https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/km/wmicore.mof#L21200
+ const GUID WmiMonitorBrightnessGuid = {
+ 0xd43412ac, 0x67f9, 0x4fbb, { 0xa0, 0x81, 0x17, 0x52, 0xa2, 0xc3, 0x3e, 0x84 }
+ };
+
+ FF_AUTO_CLOSE_WMI_BLOCK HANDLE hBlock = NULL;
+
+ ULONG status = WmiOpenBlock(&WmiMonitorBrightnessGuid, WMIGUID_QUERY, &hBlock);
+ if (status != 0) {
+ FF_DEBUG("WMI: WmiOpenBlock failed: %s", ffDebugWin32Error(status));
+ return "WmiOpenBlock() failed";
+ }
+
+ ULONG bufferSize = 0;
+ status = WmiQueryAllDataW(hBlock, &bufferSize, NULL);
+ if (status != ERROR_SUCCESS && status != ERROR_INSUFFICIENT_BUFFER) {
+ FF_DEBUG("WMI: first WmiQueryAllDataW failed, bufferSize=%lu: %s", bufferSize, ffDebugWin32Error(status));
+ return "WmiQueryAllDataW() failed";
+ }
+
+ FF_DEBUG("WMI: initial query bufferSize=%lu", bufferSize);
+
+ if (bufferSize == 0) {
+ FF_DEBUG("WMI: WmiQueryAllDataW returned empty buffer");
+ return "WmiQueryAllDataW() returned no data";
+ }
+
+ FF_AUTO_FREE PWNODE_ALL_DATA pAllData = (PWNODE_ALL_DATA) malloc(bufferSize);
+
+ status = WmiQueryAllDataW(hBlock, &bufferSize, pAllData);
+ if (status != ERROR_SUCCESS) {
+ FF_DEBUG("WMI: second WmiQueryAllDataW failed, bufferSize=%lu: %s", bufferSize, ffDebugWin32Error(status));
+ return "WmiQueryAllDataW() failed";
+ }
+
+ if (bufferSize < sizeof(WNODE_ALL_DATA)) {
+ FF_DEBUG("WMI: insufficient buffer for WNODE_ALL_DATA, bufferSize=%lu", bufferSize);
+ return "WmiQueryAllDataW() returned insufficient data for WNODE_ALL_DATA";
+ }
+
+ FF_DEBUG("WMI: instanceCount=%lu, flags=0x%lX", pAllData->InstanceCount, pAllData->WnodeHeader.Flags);
+
+ PULONG pNameOffsets = (PULONG) ((PUCHAR) pAllData + pAllData->OffsetInstanceNameOffsets);
+
+ for (ULONG i = 0; i < pAllData->InstanceCount; i++) {
+ ULONG dataOffset = 0;
+ ULONG dataLength = 0;
+
+ if (pAllData->WnodeHeader.Flags & WNODE_FLAG_FIXED_INSTANCE_SIZE) {
+ dataLength = pAllData->FixedInstanceSize;
+ dataOffset = pAllData->DataBlockOffset + i * dataLength;
+ } else {
+ dataOffset = pAllData->OffsetInstanceDataAndLength[i].OffsetInstanceData;
+ dataLength = pAllData->OffsetInstanceDataAndLength[i].LengthInstanceData;
+ }
+
+ if (dataLength == 0 || dataOffset >= bufferSize || dataLength > bufferSize - dataOffset) {
+ FF_DEBUG("WMI: skip invalid instance %lu (dataOffset=%lu, dataLength=%lu, bufferSize=%lu)", i, dataOffset, dataLength, bufferSize);
+ continue;
+ }
+
+ USHORT nameCharsCount = *(PUSHORT) ((PUCHAR) pAllData + pNameOffsets[i]) / sizeof(WCHAR);
+ PCWSTR pNameChars = (PCWSTR) ((PUCHAR) pAllData + (pNameOffsets[i] + sizeof(USHORT)));
+
+ PUCHAR pDataBlock = (PUCHAR) pAllData + dataOffset;
+ UCHAR currentBrightness = pDataBlock[0];
+
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ brightness->max = 100;
+ brightness->min = 0;
+ brightness->current = currentBrightness;
+ brightness->builtin = true;
+ ffStrbufInitNWS(&brightness->name, nameCharsCount, pNameChars);
+ ffStrbufSubstrAfterFirstC(&brightness->name, '\\');
+ ffStrbufSubstrBeforeFirstC(&brightness->name, '\\');
+
+ FF_DEBUG("WMI: detected builtin display '%s', current=%u", brightness->name.chars, (unsigned) currentBrightness);
+ }
+
+ FF_DEBUG("WMI: finished detection, total results=%u", result->length);
+
+ return NULL;
+}
+
+static const char* detectWithDdcci(const FFDisplayServerResult* displayServer, FFlist* result) {
+ FF_DEBUG("DDC/CI: start detection, displayCount=%u", displayServer->displays.length);
+
+ void* gdi32 = ffLibraryGetModule(L"gdi32.dll");
+ if (!gdi32) {
+ FF_DEBUG("DDC/CI: failed to load gdi32.dll");
+ return "ffLibraryGetModule(gdi32.dll) failed";
+ }
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(gdi32, GetPhysicalMonitors)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(gdi32, DDCCIGetVCPFeature)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(gdi32, DestroyPhysicalMonitorInternal)
+
+ FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) {
+ if (display->type == FF_DISPLAY_TYPE_BUILTIN) {
+ FF_DEBUG("DDC/CI: skip builtin display id=%" PRIu64, display->id);
+ continue;
+ }
+
+ MONITORINFOEXW mi;
+ mi.cbSize = sizeof(mi);
+ if (!GetMonitorInfoW((HMONITOR) (uintptr_t) display->id, (LPMONITORINFO) &mi)) {
+ FF_DEBUG("DDC/CI: GetMonitorInfoW failed for display id=%" PRIu64 ": %s", display->id, ffDebugWin32Error(GetLastError()));
+ continue;
+ }
+
+ UNICODE_STRING deviceName = {
+ .Length = (USHORT) (wcslen(mi.szDevice) * sizeof(wchar_t)),
+ .MaximumLength = 0,
+ .Buffer = mi.szDevice,
+ };
+ HANDLE physicalMonitor;
+ DWORD monitorCount = 0;
+ NTSTATUS monitorStatus = ffGetPhysicalMonitors(&deviceName, 1, &monitorCount, &physicalMonitor);
+ if (NT_SUCCESS(monitorStatus) && monitorCount >= 1) {
+ DWORD curr = 0, max = 0;
+ if (NT_SUCCESS(ffDDCCIGetVCPFeature(physicalMonitor, FF_DDC_CI_LUMINANCE_OPCODE, NULL, &curr, &max))) {
+ FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
+ if (display->name.length > 0) {
+ ffStrbufInitCopy(&brightness->name, &display->name);
+ } else {
+ FF_LIBRARY_LOAD_SYMBOL_LAZY(gdi32, GetPhysicalMonitorDescription)
+ if (ffGetPhysicalMonitorDescription) {
+ wchar_t description[128 /*MUST be PHYSICAL_MONITOR_DESCRIPTION_SIZE*/];
+ if (NT_SUCCESS(ffGetPhysicalMonitorDescription(physicalMonitor, ARRAY_SIZE(description), description))) {
+ ffStrbufInitWS(&brightness->name, description);
+ }
+ }
+ if (brightness->name.length == 0) {
+ ffStrbufSetNWS(&brightness->name, deviceName.Length / 2, deviceName.Buffer);
+ }
+ }
+ brightness->max = max;
+ brightness->min = 0;
+ brightness->current = curr;
+ brightness->builtin = false;
+
+ FF_DEBUG("DDC/CI: detected external display '%s', current=%u, max=%u", brightness->name.chars, (unsigned) curr, (unsigned) max);
+ } else {
+ FF_DEBUG("DDC/CI: DDCCIGetVCPFeature failed for monitor '%ls': %s", deviceName.Buffer, ffDebugWin32Error(GetLastError()));
+ }
+
+ ffDestroyPhysicalMonitorInternal(physicalMonitor);
+ } else {
+ FF_DEBUG("DDC/CI: GetPhysicalMonitors failed for '%ls', status=0x%08X, monitorCount=%lu, error=%s", deviceName.Buffer, (unsigned) monitorStatus, monitorCount, ffDebugWin32Error(GetLastError()));
+ }
+ }
+
+ FF_DEBUG("DDC/CI: finished detection, total results=%u", result->length);
+ return NULL;
+}
+
+static bool hasBuiltinDisplay(const FFDisplayServerResult* displayServer) {
+ FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) {
+ if (display->type == FF_DISPLAY_TYPE_BUILTIN || display->type == FF_DISPLAY_TYPE_UNKNOWN) {
+ return true;
+ }
+ }
+ return false;
+}
+
+const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) {
+ const FFDisplayServerResult* displayServer = ffConnectDisplayServer();
+ FF_DEBUG("start, displayCount=%u", displayServer->displays.length);
+
+ if (hasBuiltinDisplay(displayServer)) {
+ FF_DEBUG("builtin display detected, trying WMI");
+ detectWithWmi(result);
+ }
+
+ if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && result->length < displayServer->displays.length) {
+ FF_DEBUG("resultCount=%u < displayCount=%u, trying DDC/CI", result->length, displayServer->displays.length);
+ detectWithDdcci(displayServer, result);
+ }
+
+ FF_DEBUG("finished, resultCount=%u", result->length);
+ return NULL;
+}