mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
264 lines
6.0 KiB
Go
264 lines
6.0 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) 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, 0644); 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
|
|
}
|