package main import ( "bufio" "regexp" "strconv" "strings" ) // Device is one input device the X server knows. type Device struct { ID int `json:"id"` Name string `json:"name"` Role string `json:"role"` // master or slave Kind string `json:"kind"` // pointer or keyboard Settings map[string]any `json:"settings,omitempty"` } var deviceLine = regexp.MustCompile(`^[^A-Za-z0-9]*(.+?)\s+id=(\d+)\s+\[(master|slave|floating)\s+(pointer|keyboard)?`) // ParseDevices reads `xinput list`, the tree's drawing characters stripped. func ParseDevices(text string) []Device { var out []Device sc := bufio.NewScanner(strings.NewReader(text)) for sc.Scan() { m := deviceLine.FindStringSubmatch(sc.Text()) if m == nil { continue } id, _ := strconv.Atoi(m[2]) kind := m[4] if kind == "" { kind = "floating" } out = append(out, Device{ID: id, Name: strings.TrimSpace(m[1]), Role: m[3], Kind: kind}) } return out } // Knob is one setting input-set changes, and the libinput property that holds it. type Knob struct { Arg, Property, Kind string // Kind: bool or float } // Knobs are what input-set may change, by libinput's own property names (libinput(4)). var Knobs = []Knob{ {"enabled", "Device Enabled", "bool"}, {"tap", "libinput Tapping Enabled", "bool"}, {"tap_drag", "libinput Tapping Drag Enabled", "bool"}, {"natural_scroll", "libinput Natural Scrolling Enabled", "bool"}, {"disable_while_typing", "libinput Disable While Typing Enabled", "bool"}, {"left_handed", "libinput Left Handed Enabled", "bool"}, {"middle_emulation", "libinput Middle Emulation Enabled", "bool"}, {"accel_speed", "libinput Accel Speed", "float"}, } var propLine = regexp.MustCompile(`^\s+(.+?) \(\d+\):\s*(.*)$`) // ParseProps reads `xinput list-props` into the settings Knobs name, as booleans and numbers. A // device without a property (a keyboard has no tapping) simply lacks the setting. func ParseProps(text string) map[string]any { byProp := map[string]string{} sc := bufio.NewScanner(strings.NewReader(text)) for sc.Scan() { if m := propLine.FindStringSubmatch(sc.Text()); m != nil { byProp[m[1]] = strings.TrimSpace(m[2]) } } out := map[string]any{} for _, k := range Knobs { v, ok := byProp[k.Property] if !ok { continue } first := strings.TrimSpace(strings.Split(v, ",")[0]) switch k.Kind { case "bool": out[k.Arg] = first == "1" case "float": if f, err := strconv.ParseFloat(first, 64); err == nil { out[k.Arg] = f } } } if v, ok := byProp["libinput Accel Profile Enabled"]; ok { profiles := []string{"adaptive", "flat", "custom"} for i, bit := range strings.Split(v, ",") { if strings.TrimSpace(bit) == "1" && i < len(profiles) { out["accel_profile"] = profiles[i] } } } return out } // Keyboard is the X keyboard map as setxkbmap reports it. type Keyboard struct { Rules string `json:"rules,omitempty"` Model string `json:"model,omitempty"` Layout string `json:"layout,omitempty"` Variant string `json:"variant,omitempty"` Options []string `json:"options"` } // ParseKeyboard reads `setxkbmap -query`. func ParseKeyboard(text string) Keyboard { k := Keyboard{Options: []string{}} sc := bufio.NewScanner(strings.NewReader(text)) for sc.Scan() { key, value, ok := strings.Cut(sc.Text(), ":") if !ok { continue } value = strings.TrimSpace(value) switch strings.TrimSpace(key) { case "rules": k.Rules = value case "model": k.Model = value case "layout": k.Layout = value case "variant": k.Variant = value case "options": for _, o := range strings.Split(value, ",") { if o = strings.TrimSpace(o); o != "" { k.Options = append(k.Options, o) } } } } return k } // xkbName is what setxkbmap accepts as a layout, variant, model or option: letters, digits and the // punctuation XKB names use. Anything else is refused before it reaches a command line. var xkbName = regexp.MustCompile(`^[A-Za-z0-9_:+(),.-]+$`)