package brightness import ( "fmt" "math" "os" "path/filepath" "strconv" "strings" "github.com/AvengeMedia/DankMaterialShell/backend/internal/log" ) func NewSysfsBackend() (*SysfsBackend, error) { b := &SysfsBackend{ basePath: "/sys/class", classes: []string{"backlight", "leds"}, deviceCache: make(map[string]*sysfsDevice), } if err := b.scanDevices(); err != nil { return nil, err } return b, nil } func (b *SysfsBackend) scanDevices() error { b.deviceCacheMutex.Lock() defer b.deviceCacheMutex.Unlock() 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[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) { b.deviceCacheMutex.RLock() defer b.deviceCacheMutex.RUnlock() devices := make([]Device, 0, len(b.deviceCache)) for _, dev := range b.deviceCache { if shouldSuppressDevice(dev.name) { continue } parts := strings.SplitN(dev.id, ":", 2) if len(parts) != 2 { continue } 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) continue } current, err := strconv.Atoi(strings.TrimSpace(string(brightnessData))) if err != nil { log.Debugf("failed to parse brightness for %s: %v", dev.id, err) continue } 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 devices, nil } func (b *SysfsBackend) GetDevice(id string) (*sysfsDevice, error) { b.deviceCacheMutex.RLock() defer b.deviceCacheMutex.RUnlock() dev, ok := b.deviceCache[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 }