Skip to content

Commit e31d666

Browse files
committed
feat(selector): add multi-device selector
1 parent 8fd8055 commit e31d666

3 files changed

Lines changed: 481 additions & 0 deletions

File tree

src/evdev/selector.lua

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---@diagnostic disable: missing-fields, inject-field
2+
3+
local evdev = require "evdev"
4+
5+
local validate = evdev._util.validate
6+
local tbl_find = evdev._util.tbl_find
7+
local is_device = evdev.device.is_device
8+
local poll_devices = evdev._core.poll_devices
9+
10+
---@type evdev.Selector
11+
local Selector = {}
12+
Selector.__index = Selector
13+
14+
local function validate_device(lbl, dev)
15+
if not is_device(dev) then
16+
error(lbl .. ": (evdev.Device expected)", 3)
17+
end
18+
return dev
19+
end
20+
21+
---@param devs evdev.Device[]
22+
local function validate_handles(devs)
23+
for i, dev in ipairs(devs) do
24+
local core = type(dev) == "table" and rawget(dev, "_core")
25+
if not core then
26+
return nil, "devices[" .. i .. "]: device is closed"
27+
end
28+
end
29+
return true
30+
end
31+
32+
function Selector:add(dev)
33+
validate_device("device", dev)
34+
if not tbl_find(self._devices, dev) then
35+
self._devices[#self._devices + 1] = dev
36+
self._handles[#self._handles + 1] = rawget(dev, "_core")
37+
end
38+
return self
39+
end
40+
41+
function Selector:remove(dev)
42+
validate_device("device", dev)
43+
local _, i = tbl_find(self._devices, dev)
44+
if i then
45+
table.remove(self._devices, i)
46+
table.remove(self._handles, i)
47+
end
48+
return self
49+
end
50+
51+
function Selector:clear()
52+
self._devices = {}
53+
self._handles = {}
54+
return self
55+
end
56+
57+
function Selector:poll()
58+
local devs = self._devices
59+
local ok, err = validate_handles(devs)
60+
if not ok then
61+
return nil, err
62+
end
63+
64+
local ready_indexes, poll_err = poll_devices(self._handles)
65+
if not ready_indexes then
66+
return nil, poll_err
67+
end
68+
69+
local ready = {}
70+
for i, v in ipairs(ready_indexes) do
71+
ready[i] = devs[v]
72+
end
73+
return ready
74+
end
75+
76+
function Selector:events()
77+
local ready = {}
78+
local i = 1
79+
80+
return function()
81+
while true do
82+
while i <= #ready do
83+
local dev = ready[i]
84+
local event, err = dev:read()
85+
if event then
86+
return dev, event
87+
end
88+
if err then
89+
error(err, 2)
90+
end
91+
i = i + 1
92+
end
93+
94+
local next, err = self:poll()
95+
if not next then
96+
error(err, 2)
97+
end
98+
ready = next
99+
i = 1
100+
end
101+
end
102+
end
103+
104+
---@type evdev.selector
105+
local M = {}
106+
107+
function M.new(devs)
108+
validate("devices", devs, "table", true)
109+
devs = devs or {}
110+
local handles = {}
111+
112+
for i, dev in ipairs(devs) do
113+
validate_device("devices[" .. i .. "]", dev)
114+
handles[i] = rawget(dev, "_core")
115+
end
116+
117+
return setmetatable({ _devices = devs, _handles = handles }, Selector)
118+
end
119+
120+
return setmetatable(M, {
121+
__call = function(_, devs)
122+
return M.new(devs)
123+
end,
124+
})

tests/selector.test.lua

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
local evdev = require "evdev"
2+
3+
local Selector = evdev.selector.new
4+
local Device = evdev.device.open
5+
6+
describe("evdev.selector", function()
7+
describe("new()", function()
8+
it("creates selectors and manages devices", function()
9+
local first = assert(Device("/dev/null"))
10+
local second = assert(Device("/dev/null"))
11+
local sel = Selector({ first })
12+
13+
assert.Table(sel)
14+
15+
assert(first:close())
16+
assert(second:close())
17+
end)
18+
19+
it("errors on invalid device lists", function()
20+
assert.Error(function()
21+
Selector(false) ---@diagnostic disable-line
22+
end, "devices: (table expected, got boolean)")
23+
24+
assert.Error(function()
25+
Selector({ {} }) ---@diagnostic disable-line
26+
end, "devices[1]: (evdev.Device expected)")
27+
end)
28+
end)
29+
30+
describe("add()", function()
31+
it("errors on non-device values", function()
32+
local sel = Selector()
33+
34+
assert.Error(function()
35+
sel:add(false) ---@diagnostic disable-line
36+
end, "device: (evdev.Device expected)")
37+
38+
assert.Error(function()
39+
sel:add({}) ---@diagnostic disable-line
40+
end, "device: (evdev.Device expected)")
41+
end)
42+
43+
it("ignores devices that are already registered", function()
44+
local dev = assert(Device("/dev/null"))
45+
local sel = Selector()
46+
47+
sel:add(dev):add(dev)
48+
49+
local ready = assert(sel:poll())
50+
assert.Same({ dev }, ready)
51+
52+
dev:close()
53+
end)
54+
end)
55+
56+
describe("remove()", function()
57+
it("errors on non-device values", function()
58+
local sel = Selector()
59+
60+
assert.Error(function()
61+
sel:remove(false) ---@diagnostic disable-line
62+
end, "device: (evdev.Device expected)")
63+
64+
assert.Error(function()
65+
sel:remove({}) ---@diagnostic disable-line
66+
end, "device: (evdev.Device expected)")
67+
end)
68+
end)
69+
70+
describe("clear()", function()
71+
it("removes all registered devices", function()
72+
local first = assert(Device("/dev/null"))
73+
local second = assert(Device("/dev/null"))
74+
local sel = Selector({ first, second })
75+
sel:clear()
76+
first:close()
77+
second:close()
78+
end)
79+
end)
80+
81+
describe("poll()", function()
82+
it("returns registered devices that are ready", function()
83+
local first = assert(Device("/dev/null"))
84+
local second = assert(Device("/dev/null"))
85+
local sel = evdev.selector({ first, second })
86+
local ready, err = sel:poll()
87+
88+
assert.Nil(err)
89+
assert.Same({ first, second }, ready)
90+
91+
first:close()
92+
second:close()
93+
end)
94+
95+
it("returns an error when no devices are registered", function()
96+
local ready, err = Selector():poll()
97+
assert.Nil(ready)
98+
assert.Equal("devices must not be empty", err)
99+
end)
100+
101+
it("returns an error when a registered device is closed", function()
102+
local dev = assert(Device("/dev/null"))
103+
local sel = evdev.selector({ dev })
104+
105+
dev:close()
106+
107+
local ready, err = sel:poll()
108+
assert.Nil(ready)
109+
assert.Equal("devices[1]: device is closed", err)
110+
end)
111+
end)
112+
113+
describe("events()", function()
114+
it("yields events from registered devices", function()
115+
local first = assert(Device("/dev/null"))
116+
local second = assert(Device("/dev/null"))
117+
local sel = Selector({ first, second })
118+
local polled = false
119+
local second_sent = false
120+
121+
function sel:poll()
122+
if not polled then
123+
polled = true
124+
return { second, first }
125+
end
126+
return { first }
127+
end
128+
129+
function first:read()
130+
if polled then
131+
polled = false
132+
return {
133+
device = self,
134+
type = evdev.ecodes.EV_KEY,
135+
code = evdev.ecodes.KEY_F24,
136+
value = 1,
137+
}
138+
end
139+
end
140+
141+
function second:read()
142+
if second_sent then
143+
return nil
144+
end
145+
second_sent = true
146+
return {
147+
device = self,
148+
type = evdev.ecodes.EV_KEY,
149+
code = evdev.ecodes.KEY_F23,
150+
value = 0,
151+
}
152+
end
153+
154+
local events = sel:events()
155+
local dev1, event1 = events()
156+
local dev2, event2 = events()
157+
158+
first:close()
159+
second:close()
160+
161+
assert.Equal(second, dev1)
162+
assert.Equal(evdev.ecodes.KEY_F23, event1.code)
163+
assert.Equal(first, dev2)
164+
assert.Equal(evdev.ecodes.KEY_F24, event2.code)
165+
end)
166+
167+
it("skips ready devices that return no event", function()
168+
local first = assert(Device("/dev/null"))
169+
local second = assert(Device("/dev/null"))
170+
local sel = Selector({ first, second })
171+
172+
function sel:poll()
173+
return { first, second }
174+
end
175+
176+
function first:read() end
177+
178+
function second:read()
179+
return {
180+
device = self,
181+
type = evdev.ecodes.EV_KEY,
182+
code = evdev.ecodes.KEY_F24,
183+
value = 1,
184+
}
185+
end
186+
187+
local dev, event = sel:events()()
188+
189+
first:close()
190+
second:close()
191+
192+
assert.Equal(second, dev)
193+
assert.Equal(evdev.ecodes.KEY_F24, event.code)
194+
end)
195+
196+
it("raises poll errors", function()
197+
local dev = assert(Device("/dev/null"))
198+
local sel = Selector({ dev })
199+
200+
function sel:poll()
201+
return nil, "poll failed"
202+
end
203+
204+
assert.Error(function()
205+
sel:events()()
206+
end, "poll failed")
207+
208+
dev:close()
209+
end)
210+
211+
it("raises read errors", function()
212+
local dev = assert(Device("/dev/null"))
213+
local sel = Selector({ dev })
214+
215+
function sel:poll()
216+
return { dev }
217+
end
218+
219+
function dev:read()
220+
return nil, "read failed"
221+
end
222+
223+
assert.Error(function()
224+
sel:events()()
225+
end, "read failed")
226+
227+
dev:close()
228+
end)
229+
end)
230+
end)

0 commit comments

Comments
 (0)