rofi, clipmenu, feh, i3status-rust and the laptop's model module wrote files into i3's config.d, naming no dependency on the window manager. They now contribute their lines; i3 places them under a line naming each module, and config.d is the operator's alone. The catalogue-wide test composes the contributions as the controller does and checks the whole with i3 -C.
193 lines
6.3 KiB
Go
193 lines
6.3 KiB
Go
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
|