The laptop model's hardware module and a memory-pressure module for any machine (hq research 027/03, 026/05, to-be 42 phase 3). The predecessor's polling auto-profile and mem-guard user scripts become each module's own Go code launched by the node runtime (ADR 0198): a profile switcher woken by the kernel's power-supply uevents, and a guard that warns on RAM, swap or PSI before systemd-oomd acts, on the desktop over the account's bus and always as an event. supergfxctl and triggerhappy are kept as found (research 027 Q1).
203 lines
5.7 KiB
Go
203 lines
5.7 KiB
Go
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
|
|
}
|