mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-04 04:42:05 -04:00
* 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>
268 lines
6.1 KiB
Go
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
|
|
}
|