package main import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" ) // Key is one custom key on the laptop: what it is, what runs, and where that is defined. type Key struct { Key string `json:"key"` Physical string `json:"physical,omitempty"` When string `json:"when,omitempty"` Runs string `json:"runs"` From string `json:"from"` } // KeysReport is what zephyrus_keys answers. type KeysReport struct { Keys []Key `json:"keys"` Warnings []string `json:"warnings"` NotRead []string `json:"not_read"` } // TriggerDir is triggerhappy's directory of trigger files; the module owns one file in it. const TriggerDir = "/etc/triggerhappy/triggers.d" // I3Config is the window manager's configuration, relative to the account's home. This module's lines // are its contribution to node-display-session (novox/hq ADR 0212), placed there by the i3 module // under a line that names this module. const I3Config = ".config/i3/config" // ModuleName is the line the controller writes before this module's contributed lines. const ModuleName = "asus-zephyrus-g14" // ownSection is this module's contributed lines in a placed file: from the line naming it to the next // line naming another module, a section header, or the file's end. func ownSection(text string) []string { var out []string in := false for _, line := range strings.Split(text, "\n") { t := strings.TrimSpace(line) if t == "# "+ModuleName { in = true continue } if !in { continue } if strings.HasPrefix(t, "####") || (strings.HasPrefix(t, "# ") && isModuleLine(strings.TrimPrefix(t, "# "))) { break } out = append(out, line) } return out } // isModuleLine is a comment that is one catalogue module's name: lower case, digits and dashes. func isModuleLine(s string) bool { if s == "" { return false } for _, r := range s { if !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-') { return false } } return true } // physicalKeys names the key behind an evdev code, as far as it is known on this model. The media // codes are the M4 key and Fn+F4/F5 together; which code is which key was not recorded. var physicalKeys = map[string]string{ "KEY_PROG1": "a media key (M4, Fn+F4 or Fn+F5)", "KEY_PROG3": "a media key (M4, Fn+F4 or Fn+F5)", "KEY_PROG4": "a media key (M4, Fn+F4 or Fn+F5)", "KEY_BRIGHTNESSDOWN": "Fn+F7", "KEY_BRIGHTNESSUP": "Fn+F8", "KEY_F21": "Fn+F10 (touchpad)", "$mod+Shift+s": "Fn+F6 (screenshot; the firmware sends Super+Shift+S)", "$mod+p": "Fn+F9 (display; the firmware sends Super+P)", } // firmwareKeys are handled below any configuration file. var firmwareKeys = []Key{ {Key: "Fn+F2 / Fn+F3", Physical: "keyboard backlight", Runs: "the firmware and asusd; zephyrus-kbd-notify shows the level", From: "firmware"}, } var ( triggerLine = regexp.MustCompile(`^(\S+)\s+([0-9]+)\s+(.+)$`) i3Bind = regexp.MustCompile(`^bindsym\s+((?:--\S+\s+)*)(\S+)\s+(.+)$`) i3Exec = regexp.MustCompile(`^exec(?:_always)?\s+(?:--no-startup-id\s+)?(.+)$`) ) var triggerWhen = map[string]string{"0": "released", "1": "pressed", "2": "held (repeat)"} // Keys lists the laptop's custom keys: every triggerhappy trigger, the module's i3 lines and the keys // the firmware handles itself. func (m *Machine) Keys() KeysReport { r := KeysReport{Keys: []Key{}, Warnings: []string{}, NotRead: []string{}} seen := map[string][]string{} files := m.glob(TriggerDir + "/*.conf") sort.Strings(files) if len(files) == 0 { r.Warnings = append(r.Warnings, "no trigger file in "+TriggerDir+": the vendor keys do nothing") } for _, f := range files { for _, line := range strings.Split(m.read(f), "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } g := triggerLine.FindStringSubmatch(line) if g == nil { continue } r.Keys = append(r.Keys, Key{Key: g[1], Physical: physicalKeys[g[1]], When: triggerWhen[g[2]], Runs: g[3], From: f}) id := g[1] + " " + g[2] seen[id] = append(seen[id], f) } } for id, fs := range seen { if len(fs) > 1 { r.Warnings = append(r.Warnings, fmt.Sprintf("%s is bound in %d places (%s): it fires every one", id, len(fs), strings.Join(fs, ", "))) } } home := os.Getenv("MESH_OPERATOR_HOME") if home == "" { r.NotRead = append(r.NotRead, "the module's i3 lines (MESH_OPERATOR_HOME is not set)") } if home != "" { lines := ownSection(m.read(filepath.Join(home, I3Config))) if len(lines) == 0 { r.Warnings = append(r.Warnings, "~/"+I3Config+" holds no lines of this module's: is the i3 module assigned and pushed?") } from := "~/" + I3Config + " (" + ModuleName + "'s contribution)" for _, line := range lines { line = strings.TrimSpace(line) if g := i3Bind.FindStringSubmatch(line); g != nil { when := "pressed" if strings.Contains(g[1], "--release") { when = "released" } r.Keys = append(r.Keys, Key{Key: g[2], Physical: physicalKeys[g[2]], When: when, Runs: strings.TrimPrefix(strings.TrimPrefix(g[3], "exec "), "--no-startup-id "), From: from}) } else if g := i3Exec.FindStringSubmatch(line); g != nil { r.Keys = append(r.Keys, Key{Key: "(session start)", When: "at login", Runs: g[1], From: from}) } } } r.Keys = append(r.Keys, firmwareKeys...) sort.Strings(r.Warnings) return r } // asUserReferences finds the predecessor's as-user wrapper still named in a trigger, a udev rule or // the module's i3 lines. func (m *Machine) asUserReferences() []string { var at []string for _, pattern := range []string{TriggerDir + "/*.conf", "/etc/udev/rules.d/*.rules", "/etc/systemd/system-sleep/*"} { for _, f := range m.glob(pattern) { if strings.Contains(m.read(f), "as-user") { at = append(at, f) } } } sort.Strings(at) return at } // triggerhappyUID is the real user id the running thd has, or -1 when none runs. func (m *Machine) triggerhappyUID() int { for _, dir := range m.glob("/proc/[0-9]*") { if strings.TrimSpace(m.read(dir+"/comm")) != "thd" { continue } for _, line := range strings.Split(m.read(dir+"/status"), "\n") { if f := strings.Fields(line); len(f) >= 2 && f[0] == "Uid:" { var uid int if _, err := fmt.Sscanf(f[1], "%d", &uid); err == nil { return uid } } } } return -1 } // operatorUID is the account the module's code runs as: the runtime runs it as the operator's. var operatorUID = os.Getuid