Skip to content

Commit 36b9663

Browse files
committed
feat(uinput): add repeat rate support
1 parent 469e828 commit 36b9663

4 files changed

Lines changed: 262 additions & 12 deletions

File tree

src/evdev/uinput.c

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,16 @@ int evdev_create_uinput(lua_State *L) {
284284
uinput->created = 1;
285285
uinput->path = NULL;
286286
luaL_setmetatable(L, EVDEV_UINPUT_MT);
287+
288+
result = evdev_cache_uinput_path(L, uinput);
289+
if (result != 0) {
290+
(void)ioctl(fd, UI_DEV_DESTROY);
291+
evdev_close_fd(&fd);
292+
uinput->fd = -1;
293+
uinput->created = 0;
294+
return result;
295+
}
296+
287297
return 1;
288298
}
289299

@@ -336,6 +346,104 @@ static int evdev_uinput_sync(lua_State *L) {
336346
return evdev_emit_raw(L, uinput->fd, EV_SYN, SYN_REPORT, 0);
337347
}
338348

349+
static int evdev_push_repeat_unsupported(lua_State *L, const char *action,
350+
const char *path) {
351+
lua_pushnil(L);
352+
lua_pushfstring(L, "%s %s: device does not support repeat settings", action,
353+
path != NULL ? path : "<unknown>");
354+
return 2;
355+
}
356+
357+
static int evdev_uinput_open_event_node(lua_State *L, evdev_uinput_t *uinput,
358+
int flags) {
359+
int err_result;
360+
int fd;
361+
362+
err_result = evdev_cache_uinput_path(L, uinput);
363+
if (err_result != 0) {
364+
return -1;
365+
}
366+
367+
fd = evdev_open_cloexec(uinput->path, flags | O_NONBLOCK);
368+
if (fd < 0) {
369+
(void)evdev_push_errno(L, "open", uinput->path);
370+
return -1;
371+
}
372+
373+
return fd;
374+
}
375+
376+
static int evdev_uinput_set_repeat(lua_State *L) {
377+
evdev_uinput_t *uinput = evdev_check_uinput(L, 1);
378+
unsigned int repeat[2];
379+
lua_Integer delay = luaL_checkinteger(L, 2);
380+
lua_Integer period = luaL_checkinteger(L, 3);
381+
int err_result;
382+
int fd;
383+
384+
luaL_argcheck(L, delay >= 0, 2, "delay must be non-negative");
385+
luaL_argcheck(L, period >= 0, 3, "period must be non-negative");
386+
387+
err_result = evdev_check_open_uinput(L, uinput);
388+
if (err_result != 0) {
389+
return err_result;
390+
}
391+
392+
fd = evdev_uinput_open_event_node(L, uinput, O_RDWR);
393+
if (fd < 0) {
394+
return 2;
395+
}
396+
397+
repeat[0] = (unsigned int)delay;
398+
repeat[1] = (unsigned int)period;
399+
400+
if (ioctl(fd, EVIOCSREP, repeat) < 0) {
401+
int saved_errno = errno;
402+
evdev_close_fd(&fd);
403+
errno = saved_errno;
404+
if (errno == ENOSYS || errno == ENOTTY || errno == EINVAL) {
405+
return evdev_push_repeat_unsupported(L, "set repeat", uinput->path);
406+
}
407+
return evdev_push_errno(L, "set repeat", uinput->path);
408+
}
409+
410+
evdev_close_fd(&fd);
411+
lua_pushboolean(L, 1);
412+
return 1;
413+
}
414+
415+
static int evdev_uinput_get_repeat(lua_State *L) {
416+
evdev_uinput_t *uinput = evdev_check_uinput(L, 1);
417+
unsigned int repeat[2] = {0, 0};
418+
int err_result;
419+
int fd;
420+
421+
err_result = evdev_check_open_uinput(L, uinput);
422+
if (err_result != 0) {
423+
return err_result;
424+
}
425+
426+
fd = evdev_uinput_open_event_node(L, uinput, O_RDONLY);
427+
if (fd < 0) {
428+
return 2;
429+
}
430+
431+
if (ioctl(fd, EVIOCGREP, repeat) < 0) {
432+
int saved_errno = errno;
433+
evdev_close_fd(&fd);
434+
errno = saved_errno;
435+
if (errno == ENOSYS || errno == ENOTTY || errno == EINVAL) {
436+
return evdev_push_repeat_unsupported(L, "get repeat", uinput->path);
437+
}
438+
return evdev_push_errno(L, "get repeat", uinput->path);
439+
}
440+
441+
evdev_close_fd(&fd);
442+
lua_pushinteger(L, repeat[0]);
443+
lua_pushinteger(L, repeat[1]);
444+
return 2;
445+
}
446+
339447
static int evdev_uinput_close(lua_State *L) {
340448
evdev_uinput_t *uinput = evdev_check_uinput(L, 1);
341449
int destroy_errno = 0;
@@ -451,10 +559,16 @@ static int evdev_uinput_tostring(lua_State *L) {
451559
}
452560

453561
const luaL_Reg evdev_uinput_methods[] = {
454-
{"emit", evdev_uinput_emit}, {"sync", evdev_uinput_sync},
455-
{"close", evdev_uinput_close}, {"is_open", evdev_uinput_is_open},
456-
{"path", evdev_uinput_get_path}, {"info", evdev_uinput_info},
457-
{"fd", evdev_uinput_fd}, {NULL, NULL},
562+
{"emit", evdev_uinput_emit},
563+
{"sync", evdev_uinput_sync},
564+
{"set_repeat", evdev_uinput_set_repeat},
565+
{"get_repeat", evdev_uinput_get_repeat},
566+
{"close", evdev_uinput_close},
567+
{"is_open", evdev_uinput_is_open},
568+
{"path", evdev_uinput_get_path},
569+
{"info", evdev_uinput_info},
570+
{"fd", evdev_uinput_fd},
571+
{NULL, NULL},
458572
};
459573

460574
const luaL_Reg evdev_uinput_meta[] = {

src/evdev/uinput.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,20 @@ function UInput:emit(type, code, value)
176176
return call_uinput(self, "emit", type, code, value)
177177
end
178178

179+
function UInput:set_repeat(delay, period)
180+
validate("delay", delay, "number")
181+
validate("period", period, "number")
182+
return call_uinput(self, "set_repeat", delay, period)
183+
end
184+
185+
function UInput:get_repeat()
186+
local delay, period = call_uinput(self, "get_repeat")
187+
if not delay then
188+
return nil, nil, period
189+
end
190+
return delay, period
191+
end
192+
179193
function UInput:is_open()
180194
local core = rawget(self, "_core")
181195
return core ~= nil and core:is_open()

tests/uinput.test.lua

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local UInput = evdev.uinput.create
77
local normalize = evdev.uinput._normalize ---@diagnostic disable-line: undefined-field
88
local ecodes = evdev.ecodes
99
local sleep = system.sleep
10+
local fmt = string.format
1011

1112
describe("evdev.uinput()", function()
1213
describe("validations", function()
@@ -182,24 +183,31 @@ describe("evdev.uinput()", function()
182183
end)
183184

184185
describe("UInput object", function()
186+
local ui
187+
188+
before_each(function()
189+
if not (ui and ui:is_open()) then
190+
ui = assert(UInput({ name = "Lua Virtual Device" }))
191+
sleep(0.1)
192+
end
193+
end)
194+
195+
teardown(function()
196+
ui:close()
197+
end)
198+
185199
it("loads metadata fields", function()
186-
local ui = assert(UInput({ name = "cached mock device" }))
187-
sleep(0.1)
188200
assert.Match("^/dev/input/event%d+$", ui.path)
189-
assert.Equal("cached mock device", ui.name)
190-
assert.True(ui:close())
201+
assert.Equal("Lua Virtual Device", ui.name)
191202
end)
192203

193204
it("reports open state and closes cleanly", function()
194-
local ui = assert(UInput({ name = "mock open state" }))
195205
assert.True(ui:is_open())
196206
assert.True(ui:close())
197207
assert.False(ui:is_open())
198-
assert.True(ui:close())
199208
end)
200209

201210
it("returns a closed-device error for emit after close", function()
202-
local ui = assert(UInput({ name = "closed emit test" }))
203211
assert.True(ui:close())
204212

205213
local ok, err = ui:emit(ecodes.EV_KEY, ecodes.KEY_A, 1)
@@ -208,14 +216,76 @@ describe("evdev.uinput()", function()
208216
end)
209217

210218
it("returns a closed-device error for sync after close", function()
211-
local ui = assert(UInput({ name = "closed sync test" }))
212219
assert.True(ui:close())
213220

214221
local ok, err = ui:sync()
215222
assert.Nil(ok)
216223
assert.Equal("uinput device is closed", err)
217224
end)
218225

226+
it("returns repeat settings for repeat-capable devices", function()
227+
local delay, period, err = ui:get_repeat()
228+
assert.Number(delay)
229+
assert.Number(period)
230+
assert.Nil(err)
231+
end)
232+
233+
it("updates repeat settings for repeat-capable devices", function()
234+
local delay, period, err = ui:get_repeat()
235+
assert.Number(delay)
236+
assert.Number(period)
237+
assert.Nil(err)
238+
assert.True(ui:set_repeat(delay, period))
239+
end)
240+
241+
it("returns an unsupported get_repeat error for non-repeat devices", function()
242+
local ui = assert(UInput({
243+
name = "uinput unsupported get repeat test",
244+
keys = { ecodes.BTN_LEFT },
245+
rels = { ecodes.REL_X },
246+
}))
247+
248+
sleep(0.1)
249+
250+
local delay, period, err = ui:get_repeat()
251+
assert.Nil(delay)
252+
assert.Nil(period)
253+
assert.Equal(fmt("get repeat %s: device does not support repeat settings", ui.path), err)
254+
assert.True(ui:close())
255+
end)
256+
257+
it("returns an unsupported set_repeat error for non-repeat devices", function()
258+
local ui = assert(UInput({
259+
name = "uinput unsupported set repeat test",
260+
keys = { ecodes.BTN_RIGHT },
261+
rels = { ecodes.REL_Y },
262+
}))
263+
sleep(0.1)
264+
local ok, err = ui:set_repeat(300, 40)
265+
assert.Nil(ok)
266+
assert.Equal(fmt("set repeat %s: device does not support repeat settings", ui.path), err)
267+
assert.True(ui:close())
268+
end)
269+
270+
it("returns a closed-device error for get_repeat after close", function()
271+
local ui = assert(UInput({ name = "closed get repeat test" }))
272+
assert.True(ui:close())
273+
274+
local delay, period, err = ui:get_repeat()
275+
assert.Nil(delay)
276+
assert.Nil(period)
277+
assert.Equal("uinput device is closed", err)
278+
end)
279+
280+
it("returns a closed-device error for set_repeat after close", function()
281+
local ui = assert(UInput({ name = "closed set repeat test" }))
282+
assert.True(ui:close())
283+
284+
local ok, err = ui:set_repeat(300, 40)
285+
assert.Nil(ok)
286+
assert.Equal("uinput device is closed", err)
287+
end)
288+
219289
it("validates emit argument types before using the handle", function()
220290
local ui = assert(UInput({ name = "emit validation test" }))
221291

@@ -233,5 +303,19 @@ describe("evdev.uinput()", function()
233303

234304
ui:close()
235305
end)
306+
307+
it("validates set_repeat argument types before using the handle", function()
308+
local ui = assert(UInput({ name = "set repeat validation test" }))
309+
310+
assert.Error(function()
311+
ui:set_repeat("fast", 40)
312+
end, "delay: (number expected, got string)")
313+
314+
assert.Error(function()
315+
ui:set_repeat(300, "slow")
316+
end, "period: (number expected, got string)")
317+
318+
ui:close()
319+
end)
236320
end)
237321
end)

types/uinput.d.lua

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ function M.create(spec) end
5050
---@field info fun(self: evdev.coreUInput): (info:evdev.deviceInfo?, err:string?)
5151
---@field is_open fun(self: evdev.coreUInput): boolean
5252
---@field sync fun(self: evdev.coreUInput): (ok:true?, err:string?)
53+
---@field set_repeat fun(self: evdev.coreUInput, delay:integer, period:integer): (ok:true?, err:string?)
54+
---@field get_repeat fun(self: evdev.coreUInput): (delay:integer?, period_or_err:(integer|string)?)
5355

5456
---
5557
---Open virtual input device handle.
@@ -135,4 +137,40 @@ function UInput:emit(type, code, value) end
135137
---@return string? err Error message on failure.
136138
function UInput:sync() end
137139

140+
---
141+
---Set the keyboard repeat rate on the virtual device.
142+
---
143+
---```lua
144+
---local UInput = evdev.uinput.create
145+
---local ui = assert(UInput())
146+
---os.execute("sleep 0.5")
147+
---
148+
----- Set repeat delay to 500ms, repeat period to 50ms
149+
---ui:set_repeat(500, 50)
150+
---```
151+
---
152+
---@param delay integer Delay in milliseconds before key repeat starts.
153+
---@param period integer Period in milliseconds between repeated key events.
154+
---@return true? ok `true` when the repeat rate is set successfully.
155+
---@return string? err Error message on failure.
156+
function UInput:set_repeat(delay, period) end
157+
158+
---
159+
---Get the current keyboard repeat rate from the virtual device.
160+
---
161+
---```lua
162+
---local UInput = evdev.uinput.create
163+
---local ui = assert(UInput())
164+
---os.execute("sleep 0.5")
165+
---
166+
---local delay, period, err = ui:get_repeat()
167+
---assert(delay, err)
168+
---print(delay, period)
169+
---```
170+
---
171+
---@return integer? delay Repeat delay in milliseconds.
172+
---@return integer? period Repeat period in milliseconds.
173+
---@return string? err Error message on failure.
174+
function UInput:get_repeat() end
175+
138176
return M

0 commit comments

Comments
 (0)