1 package xinput
2
3 import (
4 "fmt"
5 "os/exec"
6 "regexp"
7 "slices"
8 "strings"
9
10 "edge-infra.dev/pkg/sds/lib/xorg"
11 )
12
13 var (
14 xinputPath = "/usr/bin/xinput"
15
16 list = "list"
17 nameOnly = "--name-only"
18 idOnly = "--id-only"
19
20 absoluteModePattern = ".*Mode: absolute.*"
21 )
22
23 type InputDeviceInfo struct {
24 ID xorg.InputDeviceID
25 Name string
26 Absolute bool
27 }
28
29 type InputDeviceInfos []InputDeviceInfo
30
31 func (inputDeviceInfos InputDeviceInfos) AbsoluteDevices() []InputDeviceInfo {
32 return slices.DeleteFunc(inputDeviceInfos, func(inputDeviceInfo InputDeviceInfo) bool {
33 return !inputDeviceInfo.Absolute
34 })
35 }
36
37 func (inputDeviceInfos InputDeviceInfos) RelativeDevices() []InputDeviceInfo {
38 return slices.DeleteFunc(inputDeviceInfos, func(inputDeviceInfo InputDeviceInfo) bool {
39 return inputDeviceInfo.Absolute
40 })
41 }
42
43 type Xinput interface {
44 GetInputDeviceInfos() (InputDeviceInfos, error)
45 }
46
47 type xinput struct{}
48
49 func NewXinput() Xinput {
50 return &xinput{}
51 }
52
53 func (*xinput) GetInputDeviceInfos() (InputDeviceInfos, error) {
54 ids, names, err := listInputDevices()
55 if err != nil {
56 return nil, err
57 }
58
59 inputDevices := []InputDeviceInfo{}
60 for idx, id := range ids {
61 absolute, err := isAbsoluteDevice(id)
62 if err != nil {
63 return nil, err
64 }
65 inputDevices = append(inputDevices, InputDeviceInfo{
66 ID: id,
67 Name: names[idx],
68 Absolute: absolute,
69 })
70 }
71
72 return inputDevices, nil
73 }
74
75 func listInputDevices() (ids []xorg.InputDeviceID, names []string, err error) {
76 ids, err = listInputDeviceIDs()
77 if err != nil {
78 return nil, nil, err
79 }
80
81 names, err = listInputDeviceNames()
82 if err != nil {
83 return nil, nil, err
84 }
85
86 if len(ids) != len(names) {
87 return nil, nil, fmt.Errorf(
88 "input device IDs and names do not match in length (%d and %d)",
89 len(ids), len(names),
90 )
91 }
92
93 return ids, names, err
94 }
95
96 func listInputDeviceIDs() ([]xorg.InputDeviceID, error) {
97 idsCmd := exec.Command(xinputPath, list, idOnly)
98 output, err := idsCmd.CombinedOutput()
99 if err != nil {
100 return nil, fmt.Errorf("failed to list input device ids: %s: %w", output, err)
101 }
102
103 ids := []xorg.InputDeviceID{}
104 for _, id := range entriesFromOutput(output) {
105 ids = append(ids, xorg.InputDeviceID(id))
106 }
107
108 return ids, nil
109 }
110
111 func listInputDeviceNames() ([]string, error) {
112 namesCmd := exec.Command(xinputPath, list, nameOnly)
113 output, err := namesCmd.CombinedOutput()
114 if err != nil {
115 return nil, fmt.Errorf("failed to list input device names: %s: %w", output, err)
116 }
117 return entriesFromOutput(output), nil
118 }
119
120
121 func entriesFromOutput(output []byte) []string {
122 lines := strings.Split(string(output), "\n")
123 entries := []string{}
124 for _, line := range lines {
125 if line != "" {
126 entries = append(entries, line)
127 }
128 }
129 return entries
130 }
131
132 func isAbsoluteDevice(id xorg.InputDeviceID) (bool, error) {
133 cmd := exec.Command(xinputPath, list, id.String())
134 output, err := cmd.CombinedOutput()
135 if err != nil {
136 return false, fmt.Errorf("failed to get capabilities for device %s: %s: %w", id, output, err)
137 }
138
139 re, err := regexp.Compile(absoluteModePattern)
140 if err != nil {
141 return false, err
142 }
143
144 lines := strings.Split(string(output), "\n")
145 for _, line := range lines {
146 if match := re.MatchString(line); match {
147 return true, nil
148 }
149 }
150
151 return false, nil
152 }
153
View as plain text