package main import ( "context" "fmt" "math" "os" "path" "sort" "strconv" "strings" ) // MinPanelPercent is the floor a brightness change never goes below: a panel at zero is a black // screen that looks like a dead machine, and the keys cannot be seen to bring it back. const MinPanelPercent = 1 // Panel is the internal display's backlight. type Panel struct { Device string `json:"device"` Percent float64 `json:"percent"` Raw int64 `json:"raw"` Max int64 `json:"max"` Others []string `json:"other_backlights,omitempty"` } // panelDevice chooses the backlight that drives the internal panel. // // **This model registers two.** In hybrid mode the integrated GPU drives the panel (amdgpu_bl1, // beneath the eDP connector) and the discrete GPU's driver registers one of its own (nvidia_0) that // moves nothing. The predecessor's scripts named amdgpu_bl1 literally, which is right until the GPU // mode puts the panel on the other GPU. The one that sits under an eDP connector is the panel's; failing // that, the kernel's own preference: firmware, then platform, then raw. func (m *Machine) panelDevice() (string, []string, error) { all := m.glob("/sys/class/backlight/*") if len(all) == 0 { return "", nil, fmt.Errorf("this machine has no backlight in /sys/class/backlight") } names := make([]string, 0, len(all)) for _, d := range all { names = append(names, path.Base(d)) } sort.Strings(names) rank := func(name string) int { dir := "/sys/class/backlight/" + name if target, err := os.Readlink(m.path(dir)); err == nil && strings.Contains(target, "-eDP-") { return 0 } switch m.read(dir + "/type") { case "firmware": return 1 case "platform": return 2 } return 3 } best := names[0] for _, n := range names[1:] { if rank(n) < rank(best) { best = n } } var others []string for _, n := range names { if n != best { others = append(others, n) } } return best, others, nil } // PanelBrightness reads the panel. func (m *Machine) PanelBrightness() (Panel, error) { dev, others, err := m.panelDevice() if err != nil { return Panel{}, err } dir := "/sys/class/backlight/" + dev raw, ok1 := m.readInt(dir + "/brightness") max, ok2 := m.readInt(dir + "/max_brightness") if !ok1 || !ok2 || max <= 0 { return Panel{}, fmt.Errorf("%s does not say its brightness", dir) } return Panel{Device: dev, Raw: raw, Max: max, Percent: round1(float64(raw) / float64(max) * 100), Others: others}, nil } // PanelTarget turns a request — "40", "40%", "+5", "-10" — into the percentage to set, clamped to // [MinPanelPercent, 100]. func PanelTarget(current float64, request string) (float64, error) { r := strings.TrimSuffix(strings.TrimSpace(request), "%") if r == "" { return 0, fmt.Errorf("panel needs a percentage (40) or a step (+5, -5)") } n, err := strconv.ParseFloat(r, 64) if err != nil || math.IsNaN(n) || math.IsInf(n, 0) { return 0, fmt.Errorf("panel %q is not a percentage or a step", request) } target := n if strings.HasPrefix(r, "+") || strings.HasPrefix(r, "-") { target = current + n } return math.Max(MinPanelPercent, math.Min(100, target)), nil } // SetPanel sets the panel to a percentage. func (m *Machine) SetPanel(ctx context.Context, request string) (Panel, error) { p, err := m.PanelBrightness() if err != nil { return p, err } target, err := PanelTarget(p.Percent, request) if err != nil { return p, err } raw := int64(math.Round(target / 100 * float64(p.Max))) if raw < 1 { raw = 1 } if err := m.write(ctx, "/sys/class/backlight/"+p.Device+"/brightness", strconv.FormatInt(raw, 10)); err != nil { return p, err } return m.PanelBrightness() } // Keyboard is the keyboard's backlight. type Keyboard struct { Device string `json:"device"` Level string `json:"level"` Value int64 `json:"value"` Max int64 `json:"max"` } // KeyboardBrightness reads the keyboard backlight from the kernel. func (m *Machine) KeyboardBrightness() (Keyboard, error) { found := m.glob("/sys/class/leds/*kbd_backlight*") if len(found) == 0 { return Keyboard{}, fmt.Errorf("this machine has no keyboard backlight in /sys/class/leds") } dir := found[0] v, ok1 := m.readInt(dir + "/brightness") max, ok2 := m.readInt(dir + "/max_brightness") if !ok1 || !ok2 { return Keyboard{}, fmt.Errorf("%s does not say its brightness", dir) } k := Keyboard{Device: path.Base(dir), Value: v, Max: max} if max == int64(len(KeyboardLevels)-1) && v >= 0 && v <= max { k.Level = KeyboardLevels[v] } return k, nil } // KeyboardTarget turns a request — off/low/med/high, 0-3, "+", "-" — into asusd's level name. func KeyboardTarget(current int64, request string) (string, error) { r := strings.ToLower(strings.TrimSpace(request)) switch r { case "medium": r = "med" case "+", "up": r = strconv.FormatInt(min64(current+1, int64(len(KeyboardLevels)-1)), 10) case "-", "down": r = strconv.FormatInt(max64(current-1, 0), 10) } for _, l := range KeyboardLevels { if r == l { return l, nil } } if n, err := strconv.Atoi(r); err == nil && n >= 0 && n < len(KeyboardLevels) { return KeyboardLevels[n], nil } return "", fmt.Errorf("keyboard %q is not one of off, low, med, high, 0-3, + or -", request) } // SetKeyboard has asusd set the keyboard backlight, so its own record of the level stays true. func (m *Machine) SetKeyboard(ctx context.Context, request string) (Keyboard, error) { k, err := m.KeyboardBrightness() if err != nil { return k, err } level, err := KeyboardTarget(k.Value, request) if err != nil { return k, err } if _, err := m.Run(ctx, "asusctl", "leds", "set", level); err != nil { return k, vendor("asusctl", err) } return m.KeyboardBrightness() } func min64(a, b int64) int64 { if a < b { return a } return b } func max64(a, b int64) int64 { if a > b { return a } return b }