1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00
Files
DankMaterialShell/core/internal/server/brightness/sysfs.go
Artem 24e3024b57 fix(brightness): refresh sysfs cache on hotplug (#1674)
* fix(brightness): refresh sysfs cache on hotplug

The SysfsBackend used a cache that was never refreshed on display hot plug, causing new backlight devices to not appear in IPC until restart.

This adds Rescan() to SysfsBackend and calls it in Manager.Rescan(), matching the behavior of DDCBackend.

Fixes: hotplugged external monitor brightness control via IPC

* make fmt

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-14 14:00:01 -05:00

268 lines
6.1 KiB
Go

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
}