diff options
| author | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
|---|---|---|
| committer | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
| commit | a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5 (patch) | |
| tree | 1a8769217f84bfe9d6216fafaadaf8cdd876d457 /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness | |
| parent | 34b9c34f982b2596cfa9e368a2d7f74e9a17477c (diff) | |
| download | RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.tar.gz RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.zip | |
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness')
15 files changed, 0 insertions, 2993 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc.go deleted file mode 100644 index 00cf2d9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc.go +++ /dev/null @@ -1,510 +0,0 @@ -package brightness - -import ( - "encoding/binary" - "fmt" - "math" - "os" - "strings" - "syscall" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "golang.org/x/sys/unix" -) - -const ( - I2C_SLAVE = 0x0703 - DDCCI_ADDR = 0x37 - DDCCI_VCP_GET = 0x01 - DDCCI_VCP_SET = 0x03 - VCP_BRIGHTNESS = 0x10 - DDC_SOURCE_ADDR = 0x51 -) - -func NewDDCBackend() (*DDCBackend, error) { - b := &DDCBackend{ - scanInterval: 30 * time.Second, - debounceTimers: make(map[string]*time.Timer), - debouncePending: make(map[string]ddcPendingSet), - } - - if err := b.scanI2CDevices(); err != nil { - return nil, err - } - - return b, nil -} - -func (b *DDCBackend) scanI2CDevices() error { - return b.scanI2CDevicesInternal(false) -} - -func (b *DDCBackend) ForceRescan() error { - return b.scanI2CDevicesInternal(true) -} - -func (b *DDCBackend) scanI2CDevicesInternal(force bool) error { - b.scanMutex.Lock() - defer b.scanMutex.Unlock() - - if !force && time.Since(b.lastScan) < b.scanInterval { - return nil - } - - activeBuses := make(map[int]bool) - - for i := 0; i < 32; i++ { - busPath := fmt.Sprintf("/dev/i2c-%d", i) - if _, err := os.Stat(busPath); os.IsNotExist(err) { - continue - } - - if isIgnorableI2CBus(i) { - log.Debugf("Skipping ignorable i2c-%d", i) - continue - } - - activeBuses[i] = true - id := fmt.Sprintf("ddc:i2c-%d", i) - - dev, err := b.probeDDCDevice(i) - if err != nil || dev == nil { - continue - } - - dev.id = id - b.devices.Store(id, dev) - log.Debugf("found DDC device on i2c-%d", i) - } - - b.devices.Range(func(id string, dev *ddcDevice) bool { - if !activeBuses[dev.bus] { - b.devices.Delete(id) - log.Debugf("removed DDC device %s (bus no longer exists)", id) - } - return true - }) - - b.lastScan = time.Now() - - return nil -} - -func (b *DDCBackend) probeDDCDevice(bus int) (*ddcDevice, error) { - busPath := fmt.Sprintf("/dev/i2c-%d", bus) - - fd, err := syscall.Open(busPath, syscall.O_RDWR, 0) - if err != nil { - return nil, err - } - defer syscall.Close(fd) - - if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(DDCCI_ADDR)); errno != 0 { - return nil, errno - } - - dummy := make([]byte, 32) - syscall.Read(fd, dummy) //nolint:errcheck - - writebuf := []byte{0x00} - n, err := syscall.Write(fd, writebuf) - if err == nil && n == len(writebuf) { - name := b.getDDCName(bus) - dev := &ddcDevice{ - bus: bus, - addr: DDCCI_ADDR, - name: name, - } - b.readInitialBrightness(fd, dev) - return dev, nil - } - - readbuf := make([]byte, 4) - n, err = syscall.Read(fd, readbuf) - if err != nil || n == 0 { - return nil, fmt.Errorf("x37 unresponsive") - } - - name := b.getDDCName(bus) - - dev := &ddcDevice{ - bus: bus, - addr: DDCCI_ADDR, - name: name, - } - b.readInitialBrightness(fd, dev) - return dev, nil -} - -func (b *DDCBackend) getDDCName(bus int) string { - sysfsPath := fmt.Sprintf("/sys/class/i2c-adapter/i2c-%d/name", bus) - data, err := os.ReadFile(sysfsPath) - if err != nil { - return fmt.Sprintf("I2C-%d", bus) - } - - name := strings.TrimSpace(string(data)) - if name == "" { - name = fmt.Sprintf("I2C-%d", bus) - } - - return name -} - -func (b *DDCBackend) readInitialBrightness(fd int, dev *ddcDevice) { - cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS) - if err != nil { - log.Debugf("failed to read initial brightness for %s: %v", dev.name, err) - return - } - - dev.max = cap.max - dev.lastBrightness = cap.current - log.Debugf("initialized %s with brightness %d/%d", dev.name, cap.current, cap.max) -} - -func (b *DDCBackend) GetDevices() ([]Device, error) { - if err := b.scanI2CDevices(); err != nil { - log.Debugf("DDC scan error: %v", err) - } - - devices := make([]Device, 0) - - b.devices.Range(func(id string, dev *ddcDevice) bool { - devices = append(devices, Device{ - Class: ClassDDC, - ID: id, - Name: dev.name, - Current: dev.lastBrightness, - Max: dev.max, - CurrentPercent: dev.lastBrightness, - Backend: "ddc", - }) - return true - }) - - return devices, nil -} - -func (b *DDCBackend) SetBrightness(id string, value int, exponential bool, callback func()) error { - return b.SetBrightnessWithExponent(id, value, exponential, 1.2, callback) -} - -func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error { - _, ok := b.devices.Load(id) - - if !ok { - if err := b.scanI2CDevicesInternal(true); err != nil { - log.Debugf("rescan failed for %s: %v", id, err) - } - _, ok = b.devices.Load(id) - } - - if !ok { - return fmt.Errorf("device not found: %s", id) - } - - if value < 0 { - return fmt.Errorf("value out of range: %d", value) - } - - b.debounceMutex.Lock() - b.debouncePending[id] = ddcPendingSet{ - percent: value, - callback: callback, - } - - if timer, exists := b.debounceTimers[id]; exists { - timer.Reset(200 * time.Millisecond) - } else { - b.debounceWg.Add(1) - b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() { - defer b.debounceWg.Done() - b.debounceMutex.Lock() - pending, exists := b.debouncePending[id] - if exists { - delete(b.debouncePending, id) - } - b.debounceMutex.Unlock() - - if !exists { - return - } - - err := b.setBrightnessImmediateWithExponent(id, pending.percent) - if err != nil { - log.Debugf("Failed to set brightness for %s: %v", id, err) - } - - if pending.callback != nil { - pending.callback() - } - }) - } - b.debounceMutex.Unlock() - - return nil -} - -func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error { - dev, ok := b.devices.Load(id) - - if !ok { - if err := b.scanI2CDevicesInternal(true); err != nil { - log.Debugf("rescan failed for %s: %v", id, err) - } - dev, ok = b.devices.Load(id) - } - - if !ok { - return fmt.Errorf("device not found: %s", id) - } - - busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus) - - if _, err := os.Stat(busPath); os.IsNotExist(err) { - b.devices.Delete(id) - log.Debugf("removed stale DDC device %s (bus no longer exists)", id) - return fmt.Errorf("device disconnected: %s", id) - } - - fd, err := syscall.Open(busPath, syscall.O_RDWR, 0) - if err != nil { - b.devices.Delete(id) - log.Debugf("removed DDC device %s (open failed: %v)", id, err) - return fmt.Errorf("open i2c device: %w", err) - } - defer syscall.Close(fd) - - if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(dev.addr)); errno != 0 { - return fmt.Errorf("set i2c slave addr: %w", errno) - } - - max := dev.max - if max == 0 { - cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS) - if err != nil { - return fmt.Errorf("get current capability: %w", err) - } - max = cap.max - dev.max = max - b.devices.Store(id, dev) - } - - if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil { - return fmt.Errorf("set vcp feature: %w", err) - } - - log.Debugf("set %s to %d/%d", id, value, max) - - dev.max = max - dev.lastBrightness = value - b.devices.Store(id, dev) - - return nil -} - -func (b *DDCBackend) getVCPFeature(fd int, vcp byte) (*ddcCapability, error) { - for flushTry := 0; flushTry < 3; flushTry++ { - dummy := make([]byte, 32) - n, _ := syscall.Read(fd, dummy) - if n == 0 { - break - } - time.Sleep(20 * time.Millisecond) - } - - data := []byte{ - DDCCI_VCP_GET, - vcp, - } - - payload := []byte{ - DDC_SOURCE_ADDR, - byte(len(data)) | 0x80, - } - payload = append(payload, data...) - payload = append(payload, ddcciChecksum(payload)) - - n, err := syscall.Write(fd, payload) - if err != nil || n != len(payload) { - return nil, fmt.Errorf("write i2c: %w", err) - } - - time.Sleep(50 * time.Millisecond) - - pollFds := []unix.PollFd{ - { - Fd: int32(fd), - Events: unix.POLLIN, - }, - } - - pollTimeout := 200 - pollResult, err := unix.Poll(pollFds, pollTimeout) - if err != nil { - return nil, fmt.Errorf("poll i2c: %w", err) - } - if pollResult == 0 { - return nil, fmt.Errorf("poll timeout after %dms", pollTimeout) - } - if pollFds[0].Revents&unix.POLLIN == 0 { - return nil, fmt.Errorf("poll returned but POLLIN not set") - } - - response := make([]byte, 12) - n, err = syscall.Read(fd, response) - if err != nil || n < 8 { - return nil, fmt.Errorf("read i2c: %w", err) - } - - if response[0] != 0x6E || response[2] != 0x02 { - return nil, fmt.Errorf("invalid ddc response") - } - - resultCode := response[3] - if resultCode != 0x00 { - return nil, fmt.Errorf("vcp feature not supported") - } - - responseVCP := response[4] - if responseVCP != vcp { - return nil, fmt.Errorf("vcp mismatch: wanted 0x%02x, got 0x%02x", vcp, responseVCP) - } - - maxHigh := response[6] - maxLow := response[7] - currentHigh := response[8] - currentLow := response[9] - - max := int(binary.BigEndian.Uint16([]byte{maxHigh, maxLow})) - current := int(binary.BigEndian.Uint16([]byte{currentHigh, currentLow})) - - return &ddcCapability{ - vcp: vcp, - max: max, - current: current, - }, nil -} - -func ddcciChecksum(payload []byte) byte { - sum := byte(0x6E) - for _, b := range payload { - sum ^= b - } - return sum -} - -func (b *DDCBackend) setVCPFeature(fd int, vcp byte, value int) error { - data := []byte{ - DDCCI_VCP_SET, - vcp, - byte(value >> 8), - byte(value & 0xFF), - } - - payload := []byte{ - DDC_SOURCE_ADDR, - byte(len(data)) | 0x80, - } - payload = append(payload, data...) - payload = append(payload, ddcciChecksum(payload)) - - if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(DDCCI_ADDR)); errno != 0 { - return fmt.Errorf("set i2c slave for write: %w", errno) - } - - n, err := syscall.Write(fd, payload) - if err != nil || n != len(payload) { - return fmt.Errorf("write i2c: wrote %d/%d: %w", n, len(payload), err) - } - - time.Sleep(50 * time.Millisecond) - - return nil -} - -func (b *DDCBackend) percentToValue(percent int, max int, exponential bool) int { - const minValue = 1 - - if percent == 0 { - return minValue - } - - usableRange := max - minValue - var value int - - if exponential { - const exponent = 2.0 - normalizedPercent := float64(percent) / 100.0 - hardwarePercent := math.Pow(normalizedPercent, 1.0/exponent) - value = minValue + int(math.Round(hardwarePercent*float64(usableRange))) - } else { - value = minValue + ((percent - 1) * usableRange / 99) - } - - if value < minValue { - value = minValue - } - if value > max { - value = max - } - - return value -} - -func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int { - const minValue = 1 - - if max == 0 { - return 0 - } - - if value <= minValue { - return 1 - } - - usableRange := max - minValue - if usableRange == 0 { - return 100 - } - - var percent int - - if exponential { - const exponent = 2.0 - linearPercent := 1 + ((value - minValue) * 99 / usableRange) - normalizedLinear := float64(linearPercent) / 100.0 - expPercent := math.Pow(normalizedLinear, exponent) - percent = int(math.Round(expPercent * 100.0)) - } else { - percent = 1 + ((value - minValue) * 99 / usableRange) - } - - if percent > 100 { - percent = 100 - } - if percent < 1 { - percent = 1 - } - - return percent -} - -func (b *DDCBackend) WaitPending() { - done := make(chan struct{}) - go func() { - b.debounceWg.Wait() - close(done) - }() - - select { - case <-done: - case <-time.After(5 * time.Second): - log.Debug("WaitPending timed out waiting for DDC writes") - } -} - -func (b *DDCBackend) Close() { -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter.go deleted file mode 100644 index 12fc535..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter.go +++ /dev/null @@ -1,135 +0,0 @@ -package brightness - -import ( - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -// isIgnorableI2CBus checks if an I2C bus should be skipped during DDC probing. -// Based on ddcutil's sysfs_is_ignorable_i2c_device() (sysfs_base.c:1441) -func isIgnorableI2CBus(busno int) bool { - name := getI2CDeviceSysfsName(busno) - driver := getI2CSysfsDriver(busno) - - if name != "" && isIgnorableI2CDeviceName(name, driver) { - log.Debugf("i2c-%d: ignoring '%s' (driver: %s)", busno, name, driver) - return true - } - - // Only probe display adapters (0x03xxxx) and docking stations (0x0axxxx) - class := getI2CDeviceSysfsClass(busno) - if class != 0 { - classHigh := class & 0xFFFF0000 - ignorable := (classHigh != 0x030000 && classHigh != 0x0A0000) - if ignorable { - log.Debugf("i2c-%d: ignoring class 0x%08x", busno, class) - } - return ignorable - } - - return false -} - -// Based on ddcutil's ignorable_i2c_device_sysfs_name() (sysfs_base.c:1408) -func isIgnorableI2CDeviceName(name, driver string) bool { - ignorablePrefixes := []string{ - "SMBus", - "Synopsys DesignWare", - "soc:i2cdsi", - "smu", - "mac-io", - "u4", - "AMDGPU SMU", // AMD Navi2+ - probing hangs GPU - } - - for _, prefix := range ignorablePrefixes { - if strings.HasPrefix(name, prefix) { - return true - } - } - - // nouveau driver: only nvkm-* buses are valid - if driver == "nouveau" && !strings.HasPrefix(name, "nvkm-") { - return true - } - - return false -} - -// Based on ddcutil's get_i2c_device_sysfs_name() (sysfs_base.c:1175) -func getI2CDeviceSysfsName(busno int) string { - path := fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/name", busno) - data, err := os.ReadFile(path) - if err != nil { - return "" - } - return strings.TrimSpace(string(data)) -} - -// Based on ddcutil's get_i2c_device_sysfs_class() (sysfs_base.c:1380) -func getI2CDeviceSysfsClass(busno int) uint32 { - classPath := fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/device/class", busno) - data, err := os.ReadFile(classPath) - if err != nil { - classPath = fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d/device/device/device/class", busno) - data, err = os.ReadFile(classPath) - if err != nil { - return 0 - } - } - - classStr := strings.TrimSpace(string(data)) - classStr = strings.TrimPrefix(classStr, "0x") - - class, err := strconv.ParseUint(classStr, 16, 32) - if err != nil { - return 0 - } - - return uint32(class) -} - -// Based on ddcutil's get_i2c_sysfs_driver_by_busno() (sysfs_base.c:1284) -func getI2CSysfsDriver(busno int) string { - devicePath := fmt.Sprintf("/sys/bus/i2c/devices/i2c-%d", busno) - adapterPath, err := findI2CAdapter(devicePath) - if err != nil { - return "" - } - - driverLink := filepath.Join(adapterPath, "driver") - target, err := os.Readlink(driverLink) - if err != nil { - return "" - } - - return filepath.Base(target) -} - -func findI2CAdapter(devicePath string) (string, error) { - currentPath := devicePath - - for depth := 0; depth < 10; depth++ { - if _, err := os.Stat(filepath.Join(currentPath, "name")); err == nil { - return currentPath, nil - } - - deviceLink := filepath.Join(currentPath, "device") - target, err := os.Readlink(deviceLink) - if err != nil { - break - } - - if !filepath.IsAbs(target) { - target = filepath.Join(filepath.Dir(currentPath), target) - } - currentPath = filepath.Clean(target) - } - - return "", fmt.Errorf("could not find adapter for %s", devicePath) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter_test.go deleted file mode 100644 index dc3751f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_filter_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package brightness - -import ( - "testing" -) - -func TestIsIgnorableI2CDeviceName(t *testing.T) { - tests := []struct { - name string - deviceName string - driver string - want bool - }{ - { - name: "AMDGPU SMU should be ignored", - deviceName: "AMDGPU SMU", - driver: "amdgpu", - want: true, - }, - { - name: "SMBus should be ignored", - deviceName: "SMBus I801 adapter", - driver: "", - want: true, - }, - { - name: "Synopsys DesignWare should be ignored", - deviceName: "Synopsys DesignWare I2C adapter", - driver: "", - want: true, - }, - { - name: "smu prefix should be ignored (Mac G5)", - deviceName: "smu-i2c-controller", - driver: "", - want: true, - }, - { - name: "Regular NVIDIA DDC should not be ignored", - deviceName: "NVIDIA i2c adapter 1", - driver: "nvidia", - want: false, - }, - { - name: "nouveau nvkm bus should not be ignored", - deviceName: "nvkm-0000:01:00.0-bus-0000", - driver: "nouveau", - want: false, - }, - { - name: "nouveau non-nvkm bus should be ignored", - deviceName: "nouveau-other-bus", - driver: "nouveau", - want: true, - }, - { - name: "Regular AMD display adapter should not be ignored", - deviceName: "AMDGPU DM i2c hw bus 0", - driver: "amdgpu", - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := isIgnorableI2CDeviceName(tt.deviceName, tt.driver) - if got != tt.want { - t.Errorf("isIgnorableI2CDeviceName(%q, %q) = %v, want %v", - tt.deviceName, tt.driver, got, tt.want) - } - }) - } -} - -func TestClassFiltering(t *testing.T) { - tests := []struct { - name string - class uint32 - want bool - }{ - { - name: "Display adapter class should not be ignored", - class: 0x030000, - want: false, - }, - { - name: "Docking station class should not be ignored", - class: 0x0a0000, - want: false, - }, - { - name: "Display adapter with subclass should not be ignored", - class: 0x030001, - want: false, - }, - { - name: "SMBus class should be ignored", - class: 0x0c0500, - want: true, - }, - { - name: "Bridge class should be ignored", - class: 0x060400, - want: true, - }, - { - name: "Generic system peripheral should be ignored", - class: 0x088000, - want: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - classHigh := tt.class & 0xFFFF0000 - ignorable := (classHigh != 0x030000 && classHigh != 0x0A0000) - if ignorable != tt.want { - t.Errorf("class 0x%08x: ignorable = %v, want %v", tt.class, ignorable, tt.want) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_test.go deleted file mode 100644 index 7f85510..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/ddc_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package brightness - -import ( - "testing" -) - -func TestDDCBackend_PercentConversions(t *testing.T) { - tests := []struct { - name string - max int - percent int - wantValue int - }{ - { - name: "0% should map to minValue=1", - max: 100, - percent: 0, - wantValue: 1, - }, - { - name: "1% should be 1", - max: 100, - percent: 1, - wantValue: 1, - }, - { - name: "50% should be ~50", - max: 100, - percent: 50, - wantValue: 50, - }, - { - name: "100% should be max", - max: 100, - percent: 100, - wantValue: 100, - }, - } - - b := &DDCBackend{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := b.percentToValue(tt.percent, tt.max, false) - diff := got - tt.wantValue - if diff < 0 { - diff = -diff - } - if diff > 1 { - t.Errorf("percentToValue() = %v, want %v (±1)", got, tt.wantValue) - } - }) - } -} - -func TestDDCBackend_ValueToPercent(t *testing.T) { - tests := []struct { - name string - max int - value int - wantPercent int - tolerance int - }{ - { - name: "zero value should be 1%", - max: 100, - value: 0, - wantPercent: 1, - tolerance: 0, - }, - { - name: "min value should be 1%", - max: 100, - value: 1, - wantPercent: 1, - tolerance: 0, - }, - { - name: "mid value should be ~50%", - max: 100, - value: 50, - wantPercent: 50, - tolerance: 2, - }, - { - name: "max value should be 100%", - max: 100, - value: 100, - wantPercent: 100, - tolerance: 0, - }, - } - - b := &DDCBackend{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := b.valueToPercent(tt.value, tt.max, false) - diff := got - tt.wantPercent - if diff < 0 { - diff = -diff - } - if diff > tt.tolerance { - t.Errorf("valueToPercent() = %v, want %v (±%d)", got, tt.wantPercent, tt.tolerance) - } - }) - } -} - -func TestDDCBackend_RoundTrip(t *testing.T) { - b := &DDCBackend{} - - tests := []struct { - name string - max int - percent int - }{ - {"1%", 100, 1}, - {"25%", 100, 25}, - {"50%", 100, 50}, - {"75%", 100, 75}, - {"100%", 100, 100}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - value := b.percentToValue(tt.percent, tt.max, false) - gotPercent := b.valueToPercent(value, tt.max, false) - - if diff := tt.percent - gotPercent; diff < -1 || diff > 1 { - t.Errorf("round trip failed: wanted %d%%, got %d%% (value=%d)", tt.percent, gotPercent, value) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/handlers.go deleted file mode 100644 index c71163e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/handlers.go +++ /dev/null @@ -1,124 +0,0 @@ -package brightness - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, m *Manager) { - switch req.Method { - case "brightness.getState": - handleGetState(conn, req, m) - case "brightness.setBrightness": - handleSetBrightness(conn, req, m) - case "brightness.increment": - handleIncrement(conn, req, m) - case "brightness.decrement": - handleDecrement(conn, req, m) - case "brightness.rescan": - handleRescan(conn, req, m) - case "brightness.subscribe": - handleSubscribe(conn, req, m) - default: - models.RespondError(conn, req.ID, "unknown method: "+req.Method) - } -} - -func handleGetState(conn net.Conn, req models.Request, m *Manager) { - models.Respond(conn, req.ID, m.GetState()) -} - -func handleSetBrightness(conn net.Conn, req models.Request, m *Manager) { - device, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - percent, err := params.Int(req.Params, "percent") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - exponential := params.BoolOpt(req.Params, "exponential", false) - exponent := params.FloatOpt(req.Params, "exponent", 1.2) - - if err := m.SetBrightnessWithExponent(device, percent, exponential, exponent); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, m.GetState()) -} - -func handleIncrement(conn net.Conn, req models.Request, m *Manager) { - device, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - step := params.IntOpt(req.Params, "step", 10) - exponential := params.BoolOpt(req.Params, "exponential", false) - exponent := params.FloatOpt(req.Params, "exponent", 1.2) - - if err := m.IncrementBrightnessWithExponent(device, step, exponential, exponent); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, m.GetState()) -} - -func handleDecrement(conn net.Conn, req models.Request, m *Manager) { - device, err := params.String(req.Params, "device") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - step := params.IntOpt(req.Params, "step", 10) - exponential := params.BoolOpt(req.Params, "exponential", false) - exponent := params.FloatOpt(req.Params, "exponent", 1.2) - - if err := m.IncrementBrightnessWithExponent(device, -step, exponential, exponent); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - models.Respond(conn, req.ID, m.GetState()) -} - -func handleRescan(conn net.Conn, req models.Request, m *Manager) { - m.Rescan() - models.Respond(conn, req.ID, m.GetState()) -} - -func handleSubscribe(conn net.Conn, req models.Request, m *Manager) { - clientID := fmt.Sprintf("brightness-%d", req.ID) - - ch := m.Subscribe(clientID) - defer m.Unsubscribe(clientID) - - initialState := m.GetState() - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &initialState, - }); err != nil { - return - } - - for state := range ch { - if err := json.NewEncoder(conn).Encode(models.Response[State]{ - ID: req.ID, - Result: &state, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind.go deleted file mode 100644 index 8449ab5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind.go +++ /dev/null @@ -1,66 +0,0 @@ -package brightness - -import ( - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/godbus/dbus/v5" -) - -type DBusConn interface { - Object(dest string, path dbus.ObjectPath) dbus.BusObject - Close() error -} - -type LogindBackend struct { - conn DBusConn -} - -func NewLogindBackend() (*LogindBackend, error) { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("connect to system bus: %w", err) - } - - obj := conn.Object("org.freedesktop.login1", "/org/freedesktop/login1/session/auto") - call := obj.Call("org.freedesktop.DBus.Peer.Ping", 0) - if call.Err != nil { - conn.Close() - return nil, fmt.Errorf("logind not available: %w", call.Err) - } - - conn.Close() - - return &LogindBackend{}, nil -} - -func NewLogindBackendWithConn(conn DBusConn) *LogindBackend { - return &LogindBackend{ - conn: conn, - } -} - -func (b *LogindBackend) SetBrightness(subsystem, name string, brightness uint32) error { - if b.conn == nil { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return fmt.Errorf("connect to system bus: %w", err) - } - b.conn = conn - } - - obj := b.conn.Object("org.freedesktop.login1", "/org/freedesktop/login1/session/auto") - call := obj.Call("org.freedesktop.login1.Session.SetBrightness", 0, subsystem, name, brightness) - if call.Err != nil { - return fmt.Errorf("dbus call failed: %w", call.Err) - } - - log.Debugf("logind: set %s/%s to %d", subsystem, name, brightness) - return nil -} - -func (b *LogindBackend) Close() { - if b.conn != nil { - b.conn.Close() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind_test.go deleted file mode 100644 index 3d4c2ee..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/logind_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package brightness - -import ( - "errors" - "testing" - - mocks_brightness "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/brightness" - mock_dbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5" - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/mock" -) - -func TestLogindBackend_SetBrightness_Success(t *testing.T) { - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - backend := NewLogindBackendWithConn(mockConn) - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", dbus.Flags(0), "backlight", "nvidia_0", uint32(75)). - Return(&dbus.Call{Err: nil}). - Once() - - err := backend.SetBrightness("backlight", "nvidia_0", 75) - if err != nil { - t.Errorf("SetBrightness() error = %v, want nil", err) - } -} - -func TestLogindBackend_SetBrightness_DBusError(t *testing.T) { - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - backend := NewLogindBackendWithConn(mockConn) - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - dbusErr := errors.New("permission denied") - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(&dbus.Call{Err: dbusErr}). - Once() - - err := backend.SetBrightness("backlight", "test_device", 50) - if err == nil { - t.Error("SetBrightness() error = nil, want error") - } -} - -func TestLogindBackend_SetBrightness_LEDDevice(t *testing.T) { - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - backend := NewLogindBackendWithConn(mockConn) - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", dbus.Flags(0), "leds", "test_led", uint32(128)). - Return(&dbus.Call{Err: nil}). - Once() - - err := backend.SetBrightness("leds", "test_led", 128) - if err != nil { - t.Errorf("SetBrightness() error = %v, want nil", err) - } -} - -func TestLogindBackend_Close(t *testing.T) { - mockConn := mocks_brightness.NewMockDBusConn(t) - backend := NewLogindBackendWithConn(mockConn) - - mockConn.EXPECT(). - Close(). - Return(nil). - Once() - - backend.Close() -} - -func TestLogindBackend_Close_NilConn(t *testing.T) { - backend := &LogindBackend{conn: nil} - backend.Close() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager.go deleted file mode 100644 index 4cf7dd4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager.go +++ /dev/null @@ -1,389 +0,0 @@ -package brightness - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -func NewManager() (*Manager, error) { - return NewManagerWithOptions(false) -} - -func NewManagerWithOptions(exponential bool) (*Manager, error) { - m := &Manager{ - stopChan: make(chan struct{}), - exponential: exponential, - } - - go m.initLogind() - go m.initSysfs() - go m.initDDC() - - return m, nil -} - -func (m *Manager) initLogind() { - log.Debug("Initializing logind backend...") - logind, err := NewLogindBackend() - if err != nil { - log.Infof("Logind backend not available: %v", err) - log.Info("Will use direct sysfs access for brightness control") - return - } - - m.logindBackend = logind - m.logindReady = true - log.Info("Logind backend initialized - will use for brightness control") -} - -func (m *Manager) initSysfs() { - log.Debug("Initializing sysfs backend...") - sysfs, err := NewSysfsBackend() - if err != nil { - log.Warnf("Failed to initialize sysfs backend: %v", err) - return - } - - devices, err := sysfs.GetDevices() - if err != nil { - log.Warnf("Failed to get initial sysfs devices: %v", err) - m.sysfsBackend = sysfs - m.sysfsReady = true - m.updateState() - m.initUdev() - return - } - - log.Infof("Sysfs backend initialized with %d devices", len(devices)) - for _, d := range devices { - log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent) - } - - m.sysfsBackend = sysfs - m.sysfsReady = true - m.updateState() - m.initUdev() -} - -func (m *Manager) initUdev() { - m.udevMonitor = NewUdevMonitor(m) -} - -func (m *Manager) initDDC() { - ddc, err := NewDDCBackend() - if err != nil { - log.Debugf("Failed to initialize DDC backend: %v", err) - return - } - - m.ddcBackend = ddc - m.ddcReady = true - log.Info("DDC backend initialized") - - m.updateState() -} - -func (m *Manager) Rescan() { - log.Debug("Rescanning brightness devices...") - - if m.ddcReady && m.ddcBackend != nil { - if err := m.ddcBackend.ForceRescan(); err != nil { - log.Debugf("DDC force rescan failed: %v", err) - } - } - - if m.sysfsReady && m.sysfsBackend != nil { - if err := m.sysfsBackend.Rescan(); err != nil { - log.Debugf("Sysfs rescan failed: %v", err) - } - } - - m.updateState() -} - -func sortDevices(devices []Device) { - sort.Slice(devices, func(i, j int) bool { - classOrder := map[DeviceClass]int{ - ClassBacklight: 0, - ClassDDC: 1, - ClassLED: 2, - } - - orderI := classOrder[devices[i].Class] - orderJ := classOrder[devices[j].Class] - - if orderI != orderJ { - return orderI < orderJ - } - - return devices[i].Name < devices[j].Name - }) -} - -func stateChanged(old, new State) bool { - if len(old.Devices) != len(new.Devices) { - return true - } - - oldMap := make(map[string]Device) - for _, d := range old.Devices { - oldMap[d.ID] = d - } - - for _, newDev := range new.Devices { - oldDev, exists := oldMap[newDev.ID] - if !exists { - return true - } - if oldDev.Current != newDev.Current || oldDev.Max != newDev.Max { - return true - } - } - - return false -} - -func (m *Manager) updateState() { - allDevices := make([]Device, 0) - - if m.sysfsReady && m.sysfsBackend != nil { - devices, err := m.sysfsBackend.GetDevices() - if err != nil { - log.Debugf("Failed to get sysfs devices: %v", err) - } - if err == nil { - allDevices = append(allDevices, devices...) - } - } - - if m.ddcReady && m.ddcBackend != nil { - devices, err := m.ddcBackend.GetDevices() - if err != nil { - log.Debugf("Failed to get DDC devices: %v", err) - } - if err == nil { - allDevices = append(allDevices, devices...) - } - } - - sortDevices(allDevices) - - m.stateMutex.Lock() - oldState := m.state - newState := State{Devices: allDevices} - - if !stateChanged(oldState, newState) { - m.stateMutex.Unlock() - return - } - - m.state = newState - m.stateMutex.Unlock() - log.Debugf("State changed, notifying subscribers") - m.NotifySubscribers() -} - -func (m *Manager) SetBrightness(deviceID string, percent int) error { - return m.SetBrightnessWithMode(deviceID, percent, m.exponential) -} - -func (m *Manager) SetBrightnessWithMode(deviceID string, percent int, exponential bool) error { - return m.SetBrightnessWithExponent(deviceID, percent, exponential, 1.2) -} - -func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, exponential bool, exponent float64) error { - if percent < 0 { - return fmt.Errorf("percent out of range: %d", percent) - } - - log.Debugf("SetBrightness: %s to %d%%", deviceID, percent) - - m.stateMutex.Lock() - currentState := m.state - var found bool - var deviceClass DeviceClass - var deviceIndex int - - log.Debugf("Current state has %d devices", len(currentState.Devices)) - - for i, dev := range currentState.Devices { - if dev.ID == deviceID { - found = true - deviceClass = dev.Class - deviceIndex = i - break - } - } - - if !found { - m.stateMutex.Unlock() - log.Debugf("Device not found in state: %s", deviceID) - return fmt.Errorf("device not found: %s", deviceID) - } - - newDevices := make([]Device, len(currentState.Devices)) - copy(newDevices, currentState.Devices) - newDevices[deviceIndex].CurrentPercent = percent - m.state = State{Devices: newDevices} - m.stateMutex.Unlock() - - var err error - if deviceClass == ClassDDC { - log.Debugf("Calling DDC backend for %s", deviceID) - err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() { - m.updateState() - m.debouncedBroadcast(deviceID) - }) - } else if m.logindReady && m.logindBackend != nil { - log.Debugf("Calling logind backend for %s", deviceID) - err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent) - } else { - log.Debugf("Calling sysfs backend for %s", deviceID) - err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent) - } - - if err != nil { - m.updateState() - return fmt.Errorf("failed to set brightness: %w", err) - } - - if deviceClass != ClassDDC { - log.Debugf("Queueing broadcast for %s", deviceID) - m.debouncedBroadcast(deviceID) - } - return nil -} - -func (m *Manager) IncrementBrightness(deviceID string, step int) error { - return m.IncrementBrightnessWithMode(deviceID, step, m.exponential) -} - -func (m *Manager) IncrementBrightnessWithMode(deviceID string, step int, exponential bool) error { - return m.IncrementBrightnessWithExponent(deviceID, step, exponential, 1.2) -} - -func (m *Manager) IncrementBrightnessWithExponent(deviceID string, step int, exponential bool, exponent float64) error { - m.stateMutex.RLock() - currentState := m.state - m.stateMutex.RUnlock() - - var currentPercent int - var found bool - - for _, dev := range currentState.Devices { - if dev.ID == deviceID { - currentPercent = dev.CurrentPercent - found = true - break - } - } - - if !found { - return fmt.Errorf("device not found: %s", deviceID) - } - - newPercent := currentPercent + step - if newPercent > 100 { - newPercent = 100 - } - if newPercent < 0 { - newPercent = 0 - } - - return m.SetBrightnessWithExponent(deviceID, newPercent, exponential, exponent) -} - -func (m *Manager) DecrementBrightness(deviceID string, step int) error { - return m.IncrementBrightness(deviceID, -step) -} - -func (m *Manager) setViaSysfsWithLogindWithExponent(deviceID string, percent int, exponential bool, exponent float64) error { - parts := strings.SplitN(deviceID, ":", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid device id: %s", deviceID) - } - - subsystem := parts[0] - name := parts[1] - - dev, err := m.sysfsBackend.GetDevice(deviceID) - if err != nil { - return err - } - - value := m.sysfsBackend.PercentToValueWithExponent(percent, dev, exponential, exponent) - - if m.logindBackend == nil { - return m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent) - } - - err = m.logindBackend.SetBrightness(subsystem, name, uint32(value)) - if err != nil { - log.Debugf("logind SetBrightness failed, falling back to direct sysfs: %v", err) - return m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent) - } - - log.Debugf("set %s to %d%% (%d/%d) via logind", deviceID, percent, value, dev.maxBrightness) - return nil -} - -func (m *Manager) debouncedBroadcast(deviceID string) { - m.broadcastMutex.Lock() - defer m.broadcastMutex.Unlock() - - m.broadcastPending = true - m.pendingDeviceID = deviceID - - if m.broadcastTimer == nil { - m.broadcastTimer = time.AfterFunc(150*time.Millisecond, func() { - m.broadcastMutex.Lock() - pending := m.broadcastPending - deviceID := m.pendingDeviceID - m.broadcastPending = false - m.pendingDeviceID = "" - m.broadcastMutex.Unlock() - - if !pending || deviceID == "" { - return - } - - m.broadcastDeviceUpdate(deviceID) - }) - } else { - m.broadcastTimer.Reset(150 * time.Millisecond) - } -} - -func (m *Manager) broadcastDeviceUpdate(deviceID string) { - m.stateMutex.RLock() - var targetDevice *Device - for _, dev := range m.state.Devices { - if dev.ID == deviceID { - devCopy := dev - targetDevice = &devCopy - break - } - } - m.stateMutex.RUnlock() - - if targetDevice == nil { - log.Debugf("Device not found for broadcast: %s", deviceID) - return - } - - update := DeviceUpdate{Device: *targetDevice} - - log.Debugf("Broadcasting device update: %s at %d%%", deviceID, targetDevice.CurrentPercent) - - m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool { - select { - case ch <- update: - default: - } - return true - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager_test.go deleted file mode 100644 index a8ae0f8..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/manager_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package brightness - -import ( - "testing" -) - -// Manager tests can be added here as needed -func TestManager_Placeholder(t *testing.T) { - // Placeholder test to keep the test file valid - t.Skip("No tests implemented yet") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs.go deleted file mode 100644 index 1ff38f9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs.go +++ /dev/null @@ -1,267 +0,0 @@ -package brightness - -import ( - "fmt" - "math" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -func NewSysfsBackend() (*SysfsBackend, error) { - b := &SysfsBackend{ - basePath: "/sys/class", - classes: []string{"backlight", "leds"}, - } - - if err := b.scanDevices(); err != nil { - return nil, err - } - - return b, nil -} - -func (b *SysfsBackend) scanDevices() error { - for _, class := range b.classes { - classPath := filepath.Join(b.basePath, class) - entries, err := os.ReadDir(classPath) - if err != nil { - if os.IsNotExist(err) { - continue - } - return fmt.Errorf("read %s: %w", classPath, err) - } - - for _, entry := range entries { - devicePath := filepath.Join(classPath, entry.Name()) - - stat, err := os.Stat(devicePath) - if err != nil || !stat.IsDir() { - continue - } - maxPath := filepath.Join(devicePath, "max_brightness") - - maxData, err := os.ReadFile(maxPath) - if err != nil { - log.Debugf("skip %s/%s: no max_brightness", class, entry.Name()) - continue - } - - maxBrightness, err := strconv.Atoi(strings.TrimSpace(string(maxData))) - if err != nil || maxBrightness <= 0 { - log.Debugf("skip %s/%s: invalid max_brightness", class, entry.Name()) - continue - } - - deviceClass := ClassBacklight - minValue := 1 - if class == "leds" { - deviceClass = ClassLED - minValue = 0 - } - - deviceID := fmt.Sprintf("%s:%s", class, entry.Name()) - b.deviceCache.Store(deviceID, &sysfsDevice{ - class: deviceClass, - id: deviceID, - name: entry.Name(), - maxBrightness: maxBrightness, - minValue: minValue, - }) - - log.Debugf("found %s device: %s (max=%d)", class, entry.Name(), maxBrightness) - } - } - - return nil -} - -func shouldSuppressDevice(name string) bool { - if strings.HasSuffix(name, "::lan") { - return true - } - - keyboardLEDs := []string{ - "::scrolllock", - "::capslock", - "::numlock", - "::kana", - "::compose", - } - - for _, suffix := range keyboardLEDs { - if strings.HasSuffix(name, suffix) { - return true - } - } - - return false -} - -func (b *SysfsBackend) Rescan() error { - return b.scanDevices() -} - -func (b *SysfsBackend) GetDevices() ([]Device, error) { - devices := make([]Device, 0) - - b.deviceCache.Range(func(key string, dev *sysfsDevice) bool { - if shouldSuppressDevice(dev.name) { - return true - } - - parts := strings.SplitN(dev.id, ":", 2) - if len(parts) != 2 { - return true - } - - class := parts[0] - name := parts[1] - - devicePath := filepath.Join(b.basePath, class, name) - brightnessPath := filepath.Join(devicePath, "brightness") - - brightnessData, err := os.ReadFile(brightnessPath) - if err != nil { - log.Debugf("failed to read brightness for %s: %v", dev.id, err) - return true - } - - current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData))) - if err != nil { - log.Debugf("failed to parse brightness for %s: %v", dev.id, err) - return true - } - - percent := b.ValueToPercent(current, dev, false) - - devices = append(devices, Device{ - Class: dev.class, - ID: dev.id, - Name: dev.name, - Current: current, - Max: dev.maxBrightness, - CurrentPercent: percent, - Backend: "sysfs", - }) - return true - }) - - return devices, nil -} - -func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) { - dev, ok := b.deviceCache.Load(id) - if !ok { - return nil, fmt.Errorf("device not found: %s", id) - } - - return dev, nil -} - -func (b *SysfsBackend) SetBrightness(id string, percent int, exponential bool) error { - return b.SetBrightnessWithExponent(id, percent, exponential, 1.2) -} - -func (b *SysfsBackend) SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error { - dev, err := b.GetDevice(id) - if err != nil { - return err - } - - if percent < 0 { - return fmt.Errorf("percent out of range: %d", percent) - } - - value := b.PercentToValueWithExponent(percent, dev, exponential, exponent) - - parts := strings.SplitN(id, ":", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid device id: %s", id) - } - - class := parts[0] - name := parts[1] - - devicePath := filepath.Join(b.basePath, class, name) - brightnessPath := filepath.Join(devicePath, "brightness") - - data := []byte(fmt.Sprintf("%d", value)) - if err := os.WriteFile(brightnessPath, data, 0o644); err != nil { - return fmt.Errorf("write brightness: %w", err) - } - - log.Debugf("set %s to %d%% (%d/%d) via direct sysfs", id, percent, value, dev.maxBrightness) - - return nil -} - -func (b *SysfsBackend) PercentToValue(percent int, dev *sysfsDevice, exponential bool) int { - return b.PercentToValueWithExponent(percent, dev, exponential, 1.2) -} - -func (b *SysfsBackend) PercentToValueWithExponent(percent int, dev *sysfsDevice, exponential bool, exponent float64) int { - if percent == 0 { - return dev.minValue - } - - usableRange := dev.maxBrightness - dev.minValue - var value int - - if exponential { - normalizedPercent := float64(percent-1) / 99.0 - hardwarePercent := math.Pow(normalizedPercent, exponent) - value = dev.minValue + int(math.Round(hardwarePercent*float64(usableRange))) - } else { - value = dev.minValue + ((percent - 1) * usableRange / 99) - } - - if value < dev.minValue { - value = dev.minValue - } - if value > dev.maxBrightness { - value = dev.maxBrightness - } - - return value -} - -func (b *SysfsBackend) ValueToPercent(value int, dev *sysfsDevice, exponential bool) int { - return b.ValueToPercentWithExponent(value, dev, exponential, 1.2) -} - -func (b *SysfsBackend) ValueToPercentWithExponent(value int, dev *sysfsDevice, exponential bool, exponent float64) int { - if value <= dev.minValue { - if dev.minValue == 0 && value == 0 { - return 0 - } - return 1 - } - - usableRange := dev.maxBrightness - dev.minValue - if usableRange == 0 { - return 100 - } - - var percent int - - if exponential { - hardwarePercent := float64(value-dev.minValue) / float64(usableRange) - normalizedPercent := math.Pow(hardwarePercent, 1.0/exponent) - percent = 1 + int(math.Round(normalizedPercent*99.0)) - } else { - percent = 1 + int(math.Round(float64(value-dev.minValue)*99.0/float64(usableRange))) - } - - if percent > 100 { - percent = 100 - } - if percent < 1 { - percent = 1 - } - - return percent -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_logind_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_logind_test.go deleted file mode 100644 index 6ba84b8..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_logind_test.go +++ /dev/null @@ -1,278 +0,0 @@ -package brightness - -import ( - "os" - "path/filepath" - "testing" - - mocks_brightness "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/brightness" - mock_dbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5" - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/mock" -) - -func TestManager_SetBrightness_LogindSuccess(t *testing.T) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil { - t.Fatal(err) - } - - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - mockLogind := NewLogindBackendWithConn(mockConn) - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight"}, - } - - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - logindBackend: mockLogind, - sysfsBackend: sysfs, - logindReady: true, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassBacklight, - ID: "backlight:test_backlight", - Name: "test_backlight", - Current: 50, - Max: 100, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", mock.Anything, "backlight", "test_backlight", uint32(75)). - Return(&dbus.Call{Err: nil}). - Once() - - err := m.SetBrightness("backlight:test_backlight", 75) - if err != nil { - t.Errorf("SetBrightness() with logind error = %v, want nil", err) - } - - data, _ := os.ReadFile(filepath.Join(backlightDir, "brightness")) - if string(data) == "75\n" { - t.Error("Direct sysfs write occurred when logind should have been used") - } -} - -func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil { - t.Fatal(err) - } - - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - mockLogind := NewLogindBackendWithConn(mockConn) - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight"}, - } - - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - logindBackend: mockLogind, - sysfsBackend: sysfs, - logindReady: true, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassBacklight, - ID: "backlight:test_backlight", - Name: "test_backlight", - Current: 50, - Max: 100, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", mock.Anything, "backlight", "test_backlight", mock.Anything). - Return(&dbus.Call{Err: dbus.ErrMsgNoObject}). - Once() - - err := m.SetBrightness("backlight:test_backlight", 75) - if err != nil { - t.Errorf("SetBrightness() with fallback error = %v, want nil", err) - } - - data, _ := os.ReadFile(filepath.Join(backlightDir, "brightness")) - brightness := string(data) - if brightness != "75" { - t.Errorf("Fallback sysfs write did not occur, got brightness = %q, want %q", brightness, "75") - } -} - -func TestManager_SetBrightness_NoLogind(t *testing.T) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil { - t.Fatal(err) - } - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight"}, - } - - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - logindBackend: nil, - sysfsBackend: sysfs, - logindReady: false, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassBacklight, - ID: "backlight:test_backlight", - Name: "test_backlight", - Current: 50, - Max: 100, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - err := m.SetBrightness("backlight:test_backlight", 75) - if err != nil { - t.Errorf("SetBrightness() without logind error = %v, want nil", err) - } - - data, _ := os.ReadFile(filepath.Join(backlightDir, "brightness")) - brightness := string(data) - if brightness != "75" { - t.Errorf("Direct sysfs write = %q, want %q", brightness, "75") - } -} - -func TestManager_SetBrightness_LEDWithLogind(t *testing.T) { - tmpDir := t.TempDir() - - ledsDir := filepath.Join(tmpDir, "leds", "test_led") - if err := os.MkdirAll(ledsDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil { - t.Fatal(err) - } - - mockConn := mocks_brightness.NewMockDBusConn(t) - mockObj := mock_dbus.NewMockBusObject(t) - - mockLogind := NewLogindBackendWithConn(mockConn) - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"leds"}, - } - - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - logindBackend: mockLogind, - sysfsBackend: sysfs, - logindReady: true, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassLED, - ID: "leds:test_led", - Name: "test_led", - Current: 128, - Max: 255, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - mockConn.EXPECT(). - Object("org.freedesktop.login1", dbus.ObjectPath("/org/freedesktop/login1/session/auto")). - Return(mockObj). - Once() - - mockObj.EXPECT(). - Call("org.freedesktop.login1.Session.SetBrightness", mock.Anything, "leds", "test_led", uint32(0)). - Return(&dbus.Call{Err: nil}). - Once() - - err := m.SetBrightness("leds:test_led", 0) - if err != nil { - t.Errorf("SetBrightness() LED with logind error = %v, want nil", err) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_test.go deleted file mode 100644 index 8061513..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/sysfs_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package brightness - -import ( - "os" - "path/filepath" - "testing" -) - -func TestSysfsBackend_PercentConversions(t *testing.T) { - tests := []struct { - name string - device *sysfsDevice - percent int - wantValue int - tolerance int - }{ - { - name: "backlight 0% should be minValue=1", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - percent: 0, - wantValue: 1, - tolerance: 0, - }, - { - name: "backlight 1% should be minValue=1", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - percent: 1, - wantValue: 1, - tolerance: 0, - }, - { - name: "backlight 50% should be ~50", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - percent: 50, - wantValue: 50, - tolerance: 1, - }, - { - name: "backlight 100% should be max", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - percent: 100, - wantValue: 100, - tolerance: 0, - }, - { - name: "led 0% should be 0", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - percent: 0, - wantValue: 0, - tolerance: 0, - }, - { - name: "led 1% should be ~2-3", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - percent: 1, - wantValue: 2, - tolerance: 3, - }, - { - name: "led 50% should be ~127", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - percent: 50, - wantValue: 127, - tolerance: 2, - }, - { - name: "led 100% should be max", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - percent: 100, - wantValue: 255, - tolerance: 0, - }, - } - - b := &SysfsBackend{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := b.PercentToValue(tt.percent, tt.device, false) - diff := got - tt.wantValue - if diff < 0 { - diff = -diff - } - if diff > tt.tolerance { - t.Errorf("percentToValue() = %v, want %v (±%d)", got, tt.wantValue, tt.tolerance) - } - - gotPercent := b.ValueToPercent(got, tt.device, false) - if tt.percent > 1 && gotPercent == 0 { - t.Errorf("valueToPercent() returned 0 for non-zero input (percent=%d, got value=%d)", tt.percent, got) - } - }) - } -} - -func TestSysfsBackend_ValueToPercent(t *testing.T) { - tests := []struct { - name string - device *sysfsDevice - value int - wantPercent int - }{ - { - name: "backlight min value", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - value: 1, - wantPercent: 1, - }, - { - name: "backlight max value", - device: &sysfsDevice{maxBrightness: 100, minValue: 1, class: ClassBacklight}, - value: 100, - wantPercent: 100, - }, - { - name: "led zero", - device: &sysfsDevice{maxBrightness: 255, minValue: 0, class: ClassLED}, - value: 0, - wantPercent: 0, - }, - } - - b := &SysfsBackend{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := b.ValueToPercent(tt.value, tt.device, false) - if got != tt.wantPercent { - t.Errorf("valueToPercent() = %v, want %v", got, tt.wantPercent) - } - }) - } -} - -func TestSysfsBackend_ScanDevices(t *testing.T) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil { - t.Fatal(err) - } - - ledsDir := filepath.Join(tmpDir, "leds", "test_led") - if err := os.MkdirAll(ledsDir, 0o755); err != nil { - t.Fatal(err) - } - - if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil { - t.Fatal(err) - } - - b := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight", "leds"}, - } - - if err := b.scanDevices(); err != nil { - t.Fatalf("scanDevices() error = %v", err) - } - - backlightID := "backlight:test_backlight" - if _, ok := b.deviceCache.Load(backlightID); !ok { - t.Errorf("backlight device not found") - } - - ledID := "leds:test_led" - if _, ok := b.deviceCache.Load(ledID); !ok { - t.Errorf("LED device not found") - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/types.go deleted file mode 100644 index 81e00cf..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/types.go +++ /dev/null @@ -1,184 +0,0 @@ -package brightness - -import ( - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" -) - -type DeviceClass string - -const ( - ClassBacklight DeviceClass = "backlight" - ClassLED DeviceClass = "leds" - ClassDDC DeviceClass = "ddc" -) - -type Device struct { - Class DeviceClass `json:"class"` - ID string `json:"id"` - Name string `json:"name"` - Current int `json:"current"` - Max int `json:"max"` - CurrentPercent int `json:"currentPercent"` - Backend string `json:"backend"` -} - -type State struct { - Devices []Device `json:"devices"` -} - -type DeviceUpdate struct { - Device Device `json:"device"` -} - -type Manager struct { - logindBackend *LogindBackend - sysfsBackend *SysfsBackend - ddcBackend *DDCBackend - udevMonitor *UdevMonitor - - logindReady bool - sysfsReady bool - ddcReady bool - - exponential bool - - stateMutex sync.RWMutex - state State - - subscribers syncmap.Map[string, chan State] - updateSubscribers syncmap.Map[string, chan DeviceUpdate] - - broadcastMutex sync.Mutex - broadcastTimer *time.Timer - broadcastPending bool - pendingDeviceID string - - stopChan chan struct{} -} - -type SysfsBackend struct { - basePath string - classes []string - - deviceCache syncmap.Map[string, *sysfsDevice] -} - -type sysfsDevice struct { - class DeviceClass - id string - name string - maxBrightness int - minValue int -} - -type DDCBackend struct { - devices syncmap.Map[string, *ddcDevice] - - scanMutex sync.Mutex - lastScan time.Time - scanInterval time.Duration - - debounceMutex sync.Mutex - debounceTimers map[string]*time.Timer - debouncePending map[string]ddcPendingSet - debounceWg sync.WaitGroup -} - -type ddcPendingSet struct { - percent int - callback func() -} - -type ddcDevice struct { - bus int - addr int - id string - name string - max int - lastBrightness int -} - -type ddcCapability struct { - vcp byte - max int - current int -} - -func (m *Manager) Subscribe(id string) chan State { - ch := make(chan State, 16) - - m.subscribers.Store(id, ch) - - return ch -} - -func (m *Manager) Unsubscribe(id string) { - - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - - } - -} - -func (m *Manager) SubscribeUpdates(id string) chan DeviceUpdate { - ch := make(chan DeviceUpdate, 16) - m.updateSubscribers.Store(id, ch) - return ch -} - -func (m *Manager) UnsubscribeUpdates(id string) { - if val, ok := m.updateSubscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) NotifySubscribers() { - m.stateMutex.RLock() - state := m.state - m.stateMutex.RUnlock() - - m.subscribers.Range(func(key string, ch chan State) bool { - select { - case ch <- state: - default: - } - return true - }) -} - -func (m *Manager) GetState() State { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - return m.state -} - -func (m *Manager) Close() { - close(m.stopChan) - - m.subscribers.Range(func(key string, ch chan State) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - m.updateSubscribers.Range(func(key string, ch chan DeviceUpdate) bool { - close(ch) - m.updateSubscribers.Delete(key) - return true - }) - - if m.udevMonitor != nil { - m.udevMonitor.Close() - } - - if m.logindBackend != nil { - m.logindBackend.Close() - } - - if m.ddcBackend != nil { - m.ddcBackend.Close() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev.go deleted file mode 100644 index b7f02a8..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev.go +++ /dev/null @@ -1,237 +0,0 @@ -package brightness - -import ( - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/pilebones/go-udev/netlink" -) - -const ( - udevRecvBufSize = 8 * 1024 * 1024 - udevMaxRetries = 5 - udevBaseDelay = 2 * time.Second - udevMaxDelay = 60 * time.Second -) - -type UdevMonitor struct { - stop chan struct{} - rescanMutex sync.Mutex - rescanTimer *time.Timer - rescanPending bool -} - -func NewUdevMonitor(manager *Manager) *UdevMonitor { - m := &UdevMonitor{ - stop: make(chan struct{}), - } - - go m.run(manager) - return m -} - -func (m *UdevMonitor) run(manager *Manager) { - matcher := &netlink.RuleDefinitions{ - Rules: []netlink.RuleDefinition{ - {Env: map[string]string{"SUBSYSTEM": "backlight"}}, - {Env: map[string]string{"SUBSYSTEM": "drm"}}, - {Env: map[string]string{"SUBSYSTEM": "i2c"}}, - }, - } - if err := matcher.Compile(); err != nil { - log.Errorf("Failed to compile udev matcher: %v", err) - return - } - - failures := 0 - for { - if err := m.monitorLoop(manager, matcher); err != nil { - log.Errorf("Udev monitor error: %v", err) - } - - select { - case <-m.stop: - return - default: - } - - failures++ - if failures > udevMaxRetries { - log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries) - return - } - - delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay) - log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries) - - select { - case <-m.stop: - return - case <-time.After(delay): - } - } -} - -func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error { - conn := &netlink.UEventConn{} - if err := conn.Connect(netlink.UdevEvent); err != nil { - return err - } - defer conn.Close() - - if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil { - log.Warnf("Failed to set udev socket receive buffer: %v", err) - } - - events := make(chan netlink.UEvent) - errs := make(chan error) - conn.Monitor(events, errs, matcher) - - log.Info("Udev monitor started for backlight/drm/i2c events") - - for { - select { - case <-m.stop: - return nil - case err := <-errs: - return err - case event := <-events: - m.handleEvent(manager, event) - } - } -} - -func (m *UdevMonitor) handleEvent(manager *Manager, event netlink.UEvent) { - subsystem := event.Env["SUBSYSTEM"] - devpath := event.Env["DEVPATH"] - - if subsystem == "" || devpath == "" { - return - } - - sysname := filepath.Base(devpath) - action := string(event.Action) - - switch subsystem { - case "drm", "i2c": - m.handleDisplayEvent(manager, action, subsystem, sysname) - case "backlight": - m.handleBacklightEvent(manager, action, sysname) - } -} - -func (m *UdevMonitor) handleDisplayEvent(manager *Manager, action, subsystem, sysname string) { - switch action { - case "add", "remove", "change": - log.Debugf("Udev %s event: %s:%s - queueing DDC rescan", action, subsystem, sysname) - m.debouncedRescan(manager) - } -} - -func (m *UdevMonitor) debouncedRescan(manager *Manager) { - m.rescanMutex.Lock() - defer m.rescanMutex.Unlock() - - m.rescanPending = true - - if m.rescanTimer != nil { - m.rescanTimer.Reset(2 * time.Second) - return - } - - m.rescanTimer = time.AfterFunc(2*time.Second, func() { - m.rescanMutex.Lock() - pending := m.rescanPending - m.rescanPending = false - m.rescanMutex.Unlock() - - if !pending { - return - } - - log.Debug("Executing debounced DDC rescan") - manager.Rescan() - }) -} - -func (m *UdevMonitor) handleBacklightEvent(manager *Manager, action, sysname string) { - switch action { - case "change": - m.handleChange(manager, "backlight", sysname) - case "add", "remove": - log.Debugf("Udev %s event: backlight:%s - triggering rescan", action, sysname) - manager.Rescan() - } -} - -func (m *UdevMonitor) handleChange(manager *Manager, subsystem, sysname string) { - deviceID := subsystem + ":" + sysname - - if manager.sysfsBackend == nil { - return - } - - brightnessPath := filepath.Join(manager.sysfsBackend.basePath, subsystem, sysname, "brightness") - data, err := os.ReadFile(brightnessPath) - if err != nil { - log.Debugf("Udev change event for %s but failed to read brightness: %v", deviceID, err) - return - } - - brightness, err := strconv.Atoi(strings.TrimSpace(string(data))) - if err != nil { - log.Debugf("Failed to parse brightness for %s: %v", deviceID, err) - return - } - - manager.handleUdevBrightnessChange(deviceID, brightness) -} - -func (m *UdevMonitor) Close() { - close(m.stop) -} - -func (m *Manager) handleUdevBrightnessChange(deviceID string, rawBrightness int) { - if m.sysfsBackend == nil { - return - } - - dev, err := m.sysfsBackend.GetDevice(deviceID) - if err != nil { - log.Debugf("Udev event for unknown device %s: %v", deviceID, err) - return - } - - percent := m.sysfsBackend.ValueToPercent(rawBrightness, dev, false) - - m.stateMutex.Lock() - var found bool - for i, d := range m.state.Devices { - if d.ID != deviceID { - continue - } - found = true - if d.Current == rawBrightness { - m.stateMutex.Unlock() - return - } - m.state.Devices[i].Current = rawBrightness - m.state.Devices[i].CurrentPercent = percent - break - } - m.stateMutex.Unlock() - - if !found { - log.Debugf("Udev event for device not in state: %s", deviceID) - return - } - - log.Debugf("Udev brightness change: %s -> %d (%d%%)", deviceID, rawBrightness, percent) - m.broadcastDeviceUpdate(deviceID) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev_test.go deleted file mode 100644 index a5c406b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/brightness/udev_test.go +++ /dev/null @@ -1,260 +0,0 @@ -package brightness - -import ( - "os" - "path/filepath" - "testing" - "time" - - "github.com/pilebones/go-udev/netlink" -) - -func setupTestManager(t *testing.T) (*Manager, string) { - tmpDir := t.TempDir() - - backlightDir := filepath.Join(tmpDir, "backlight", "intel_backlight") - if err := os.MkdirAll(backlightDir, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("1000\n"), 0o644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("500\n"), 0o644); err != nil { - t.Fatal(err) - } - - sysfs := &SysfsBackend{ - basePath: tmpDir, - classes: []string{"backlight"}, - } - if err := sysfs.scanDevices(); err != nil { - t.Fatal(err) - } - - m := &Manager{ - sysfsBackend: sysfs, - sysfsReady: true, - stopChan: make(chan struct{}), - } - - m.state = State{ - Devices: []Device{ - { - Class: ClassBacklight, - ID: "backlight:intel_backlight", - Name: "intel_backlight", - Current: 500, - Max: 1000, - CurrentPercent: 50, - Backend: "sysfs", - }, - }, - } - - return m, tmpDir -} - -func TestHandleUdevBrightnessChange_UpdatesState(t *testing.T) { - m, _ := setupTestManager(t) - - m.handleUdevBrightnessChange("backlight:intel_backlight", 750) - - state := m.GetState() - if len(state.Devices) != 1 { - t.Fatalf("expected 1 device, got %d", len(state.Devices)) - } - - dev := state.Devices[0] - if dev.Current != 750 { - t.Errorf("expected Current=750, got %d", dev.Current) - } - if dev.CurrentPercent != 75 { - t.Errorf("expected CurrentPercent=75, got %d", dev.CurrentPercent) - } -} - -func TestHandleUdevBrightnessChange_NoChangeWhenSameValue(t *testing.T) { - m, _ := setupTestManager(t) - - updateCh := m.SubscribeUpdates("test") - defer m.UnsubscribeUpdates("test") - - m.handleUdevBrightnessChange("backlight:intel_backlight", 500) - - select { - case <-updateCh: - t.Error("should not broadcast when brightness unchanged") - case <-time.After(50 * time.Millisecond): - } -} - -func TestHandleUdevBrightnessChange_BroadcastsOnChange(t *testing.T) { - m, _ := setupTestManager(t) - - updateCh := m.SubscribeUpdates("test") - defer m.UnsubscribeUpdates("test") - - m.handleUdevBrightnessChange("backlight:intel_backlight", 750) - - select { - case update := <-updateCh: - if update.Device.Current != 750 { - t.Errorf("broadcast had wrong Current: got %d, want 750", update.Device.Current) - } - case <-time.After(100 * time.Millisecond): - t.Error("expected broadcast on brightness change") - } -} - -func TestHandleUdevBrightnessChange_UnknownDevice(t *testing.T) { - m, _ := setupTestManager(t) - - m.handleUdevBrightnessChange("backlight:unknown_device", 500) - - state := m.GetState() - if len(state.Devices) != 1 { - t.Errorf("state should be unchanged, got %d devices", len(state.Devices)) - } -} - -func TestHandleUdevBrightnessChange_NilSysfsBackend(t *testing.T) { - m := &Manager{ - sysfsBackend: nil, - stopChan: make(chan struct{}), - } - - m.handleUdevBrightnessChange("backlight:test", 500) -} - -func TestHandleUdevBrightnessChange_DeviceNotInState(t *testing.T) { - m, _ := setupTestManager(t) - - m.sysfsBackend.deviceCache.Store("backlight:other_device", &sysfsDevice{ - class: ClassBacklight, - id: "backlight:other_device", - name: "other_device", - maxBrightness: 100, - minValue: 1, - }) - - m.handleUdevBrightnessChange("backlight:other_device", 50) - - state := m.GetState() - for _, d := range state.Devices { - if d.ID == "backlight:other_device" { - t.Error("device should not be added to state via udev change event") - } - } -} - -func TestHandleEvent_ChangeAction(t *testing.T) { - m, tmpDir := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness") - if err := os.WriteFile(brightnessPath, []byte("800\n"), 0o644); err != nil { - t.Fatal(err) - } - - event := netlink.UEvent{ - Action: netlink.CHANGE, - Env: map[string]string{ - "SUBSYSTEM": "backlight", - "DEVPATH": "/devices/pci0000:00/0000:00:02.0/drm/card0/card0-eDP-1/intel_backlight", - }, - } - - um.handleEvent(m, event) - - state := m.GetState() - if state.Devices[0].Current != 800 { - t.Errorf("expected Current=800 after change event, got %d", state.Devices[0].Current) - } -} - -func TestHandleEvent_MissingEnvVars(t *testing.T) { - m, _ := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - event := netlink.UEvent{ - Action: netlink.CHANGE, - Env: map[string]string{}, - } - - um.handleEvent(m, event) - - state := m.GetState() - if state.Devices[0].Current != 500 { - t.Error("state should be unchanged with missing env vars") - } -} - -func TestHandleEvent_MissingSubsystem(t *testing.T) { - m, _ := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - event := netlink.UEvent{ - Action: netlink.CHANGE, - Env: map[string]string{ - "DEVPATH": "/devices/foo/bar", - }, - } - - um.handleEvent(m, event) - - state := m.GetState() - if state.Devices[0].Current != 500 { - t.Error("state should be unchanged with missing SUBSYSTEM") - } -} - -func TestHandleChange_BrightnessFileNotFound(t *testing.T) { - m, _ := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - um.handleChange(m, "backlight", "nonexistent_device") - - state := m.GetState() - if state.Devices[0].Current != 500 { - t.Error("state should be unchanged when brightness file not found") - } -} - -func TestHandleChange_InvalidBrightnessValue(t *testing.T) { - m, tmpDir := setupTestManager(t) - um := &UdevMonitor{stop: make(chan struct{})} - - brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness") - if err := os.WriteFile(brightnessPath, []byte("not_a_number\n"), 0o644); err != nil { - t.Fatal(err) - } - - um.handleChange(m, "backlight", "intel_backlight") - - state := m.GetState() - if state.Devices[0].Current != 500 { - t.Error("state should be unchanged with invalid brightness value") - } -} - -func TestUdevMonitor_Close(t *testing.T) { - um := &UdevMonitor{stop: make(chan struct{})} - - um.Close() - - select { - case <-um.stop: - default: - t.Error("stop channel should be closed") - } -} - -func TestHandleChange_NilSysfsBackend(t *testing.T) { - m := &Manager{ - sysfsBackend: nil, - stopChan: make(chan struct{}), - } - um := &UdevMonitor{stop: make(chan struct{})} - - um.handleChange(m, "backlight", "test_device") -} |