package brightness import ( "os" "path/filepath" "strconv" "strings" "sync" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/pilebones/go-udev/netlink" ) 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) { conn := &netlink.UEventConn{} if err := conn.Connect(netlink.UdevEvent); err != nil { log.Errorf("Failed to connect to udev netlink: %v", err) return } defer conn.Close() 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 } 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 case err := <-errs: log.Errorf("Udev monitor error: %v", err) return 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) }