mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 15:11:31 +00:00
Classify configured devices by keyboard and pointer capability so automatic keyboard capture leaves relative pointer devices with the display server. Run descriptor, grab, physical-state, and LED operations on one worker. Marshal normalized events through each display server's event loop and serialize ownership changes with per-lane barriers. Fall back whole lanes to native input when an evdev cohort cannot be owned, and retry missing or removed devices without disabling the rest. Handle report framing, SYN_DROPPED, high-resolution wheels, queue overflow, held-state cleanup, and guest-driven keyboard LEDs. Restore host LEDs and kernel grabs during teardown. Re-evaluate capture when the active input transport changes.
489 lines
10 KiB
C
489 lines
10 KiB
C
/**
|
|
* Looking Glass
|
|
* Copyright © 2017-2026 The Looking Glass Authors
|
|
* https://looking-glass.io
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#include "test.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdatomic.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "common/array.h"
|
|
|
|
#include "../src/keybind.c"
|
|
|
|
#define MAX_BIND 64U
|
|
#define MAX_KEY 64U
|
|
#define MAX_OP 128U
|
|
|
|
struct Bind
|
|
{
|
|
struct KeybindHandle handle;
|
|
bool active;
|
|
};
|
|
|
|
struct KeyEvent
|
|
{
|
|
int key;
|
|
bool down;
|
|
};
|
|
|
|
struct Trace
|
|
{
|
|
struct Bind bind[MAX_BIND];
|
|
struct KeyEvent key[MAX_KEY];
|
|
int op[MAX_OP];
|
|
unsigned int bindN;
|
|
unsigned int tryN;
|
|
unsigned int activeN;
|
|
unsigned int collisionN;
|
|
unsigned int releaseN;
|
|
unsigned int keyN;
|
|
unsigned int opN;
|
|
unsigned int posN;
|
|
unsigned int cursorN;
|
|
unsigned int realignN;
|
|
unsigned int alertN;
|
|
unsigned int msgN;
|
|
unsigned int videoN;
|
|
unsigned int overlayN;
|
|
bool isLinux;
|
|
bool fullscreen;
|
|
bool cursorInView;
|
|
LGMsg msg;
|
|
char alert[64];
|
|
};
|
|
|
|
static struct Trace t;
|
|
static struct LG_DisplayServerOps ds;
|
|
|
|
struct AppState g_state;
|
|
struct CursorState g_cursor;
|
|
struct AppParams g_params;
|
|
_Atomic(enum RunState) p_appState;
|
|
|
|
enum RunState app_getState(void)
|
|
{
|
|
return atomic_load(&p_appState);
|
|
}
|
|
|
|
void app_setState(enum RunState state)
|
|
{
|
|
atomic_store(&p_appState, state);
|
|
}
|
|
|
|
static const int fKeys[] =
|
|
{
|
|
KEY_F1 , KEY_F2 , KEY_F3 , KEY_F4,
|
|
KEY_F5 , KEY_F6 , KEY_F7 , KEY_F8,
|
|
KEY_F9 , KEY_F10, KEY_F11, KEY_F12,
|
|
};
|
|
|
|
static void realign(void)
|
|
{
|
|
++t.realignN;
|
|
}
|
|
|
|
static void reset(void)
|
|
{
|
|
memset(&t, 0, sizeof(t));
|
|
memset(&g_state, 0, sizeof(g_state));
|
|
memset(&g_cursor, 0, sizeof(g_cursor));
|
|
memset(&g_params, 0, sizeof(g_params));
|
|
memset(&ds, 0, sizeof(ds));
|
|
ds.realignPointer = realign;
|
|
g_state.ds = &ds;
|
|
atomic_store(&p_appState, APP_STATE_RUNNING);
|
|
}
|
|
|
|
static struct Bind * find(int sc)
|
|
{
|
|
for (unsigned int i = 0; i < t.bindN; ++i)
|
|
if (t.bind[i].active && t.bind[i].handle.sc == sc)
|
|
return &t.bind[i];
|
|
return NULL;
|
|
}
|
|
|
|
static void call(int sc)
|
|
{
|
|
struct Bind * bind = find(sc);
|
|
CHECK(bind);
|
|
CHECK(bind->handle.callback);
|
|
bind->handle.callback(sc, bind->handle.opaque);
|
|
}
|
|
|
|
KeybindHandle app_registerKeybind(int sc, KeybindFn callback, void * opaque,
|
|
const char * description)
|
|
{
|
|
CHECK(sc > KEY_RESERVED && sc < KEY_MAX);
|
|
CHECK(callback);
|
|
CHECK(description);
|
|
++t.tryN;
|
|
|
|
if (find(sc))
|
|
{
|
|
++t.collisionN;
|
|
return NULL;
|
|
}
|
|
|
|
CHECK(t.bindN < MAX_BIND);
|
|
CHECK(t.opN < MAX_OP);
|
|
struct Bind * bind = &t.bind[t.bindN++];
|
|
bind->active = true;
|
|
bind->handle.sc = sc;
|
|
bind->handle.callback = callback;
|
|
bind->handle.description = description;
|
|
bind->handle.opaque = opaque;
|
|
t.op[t.opN++] = sc;
|
|
++t.activeN;
|
|
return &bind->handle;
|
|
}
|
|
|
|
void app_releaseKeybind(KeybindHandle * handle)
|
|
{
|
|
CHECK(handle);
|
|
if (!*handle)
|
|
return;
|
|
|
|
struct Bind * bind = NULL;
|
|
for (unsigned int i = 0; i < t.bindN; ++i)
|
|
if (&t.bind[i].handle == *handle)
|
|
{
|
|
bind = &t.bind[i];
|
|
break;
|
|
}
|
|
|
|
CHECK(bind);
|
|
CHECK(bind->active);
|
|
CHECK(t.opN < MAX_OP);
|
|
bind->active = false;
|
|
t.op[t.opN++] = -bind->handle.sc;
|
|
--t.activeN;
|
|
++t.releaseN;
|
|
*handle = NULL;
|
|
}
|
|
|
|
bool app_guestIsLinux(void)
|
|
{
|
|
return t.isLinux;
|
|
}
|
|
|
|
void app_setFullscreen(bool fullscreen)
|
|
{
|
|
t.fullscreen = fullscreen;
|
|
}
|
|
|
|
bool app_getFullscreen(void)
|
|
{
|
|
return t.fullscreen;
|
|
}
|
|
|
|
void app_stopVideo(bool stop)
|
|
{
|
|
++t.videoN;
|
|
g_state.stopVideo = stop;
|
|
}
|
|
|
|
void app_setOverlay(bool enable)
|
|
{
|
|
++t.overlayN;
|
|
g_state.overlayInput = enable;
|
|
}
|
|
|
|
void app_alert(LG_MsgAlert type, const char * format, ...)
|
|
{
|
|
CHECK(type == LG_ALERT_INFO);
|
|
va_list args;
|
|
va_start(args, format);
|
|
vsnprintf(t.alert, sizeof(t.alert), format, args);
|
|
va_end(args);
|
|
++t.alertN;
|
|
}
|
|
|
|
void core_updatePositionInfo(void)
|
|
{
|
|
++t.posN;
|
|
}
|
|
|
|
void core_updateKeyboardGrab(void)
|
|
{
|
|
}
|
|
|
|
void core_setCursorInView(bool enable)
|
|
{
|
|
++t.cursorN;
|
|
t.cursorInView = enable;
|
|
}
|
|
|
|
static bool key(bool down, int sc)
|
|
{
|
|
CHECK(t.keyN < MAX_KEY);
|
|
t.key[t.keyN].key = sc;
|
|
t.key[t.keyN++].down = down;
|
|
return true;
|
|
}
|
|
|
|
bool lgInput_keyDown(int sc)
|
|
{
|
|
return key(true, sc);
|
|
}
|
|
|
|
bool lgInput_keyUp(int sc)
|
|
{
|
|
return key(false, sc);
|
|
}
|
|
|
|
void lgMessage_post(const LGMsg * msg)
|
|
{
|
|
CHECK(msg);
|
|
t.msg = *msg;
|
|
++t.msgN;
|
|
}
|
|
|
|
static void nop(int sc, void * opaque)
|
|
{
|
|
}
|
|
|
|
static void checkBind(unsigned int index, int sc, const char * description)
|
|
{
|
|
CHECK(index < t.bindN);
|
|
CHECK(t.bind[index].active);
|
|
CHECK(t.bind[index].handle.sc == sc);
|
|
CHECK(strcmp(t.bind[index].handle.description, description) == 0);
|
|
}
|
|
|
|
static void checkKey(unsigned int index, bool down, int sc)
|
|
{
|
|
CHECK(index < t.keyN);
|
|
CHECK(t.key[index].down == down);
|
|
CHECK(t.key[index].key == sc);
|
|
}
|
|
|
|
static void testCommon(void)
|
|
{
|
|
reset();
|
|
keybind_commonRegister();
|
|
|
|
CHECK(t.tryN == 6);
|
|
CHECK(t.activeN == 6);
|
|
CHECK(t.collisionN == 0);
|
|
checkBind(0, KEY_F, "Full screen toggle");
|
|
checkBind(1, KEY_V, "Video stream toggle");
|
|
checkBind(2, KEY_R, "Rotate the output clockwise by 90° increments");
|
|
checkBind(3, KEY_Q, "Quit");
|
|
checkBind(4, KEY_O, "Toggle overlay");
|
|
checkBind(5, KEY_EQUAL,
|
|
"Set guest resolution to match window size");
|
|
|
|
keybind_commonRegister();
|
|
CHECK(t.tryN == 12);
|
|
CHECK(t.activeN == 6);
|
|
CHECK(t.collisionN == 6);
|
|
}
|
|
|
|
static void testOS(void)
|
|
{
|
|
reset();
|
|
CHECK(app_registerKeybind(KEY_F1, nop, NULL, "external"));
|
|
t.isLinux = true;
|
|
keybind_inputRegister();
|
|
|
|
CHECK(t.tryN == 21);
|
|
CHECK(t.activeN == 20);
|
|
CHECK(t.collisionN == 1);
|
|
CHECK(find(KEY_I));
|
|
CHECK(find(KEY_LEFTMETA));
|
|
CHECK(find(KEY_RIGHTMETA));
|
|
for (unsigned int i = 0; i < ARRAY_LENGTH(fKeys); ++i)
|
|
CHECK(find(fKeys[i]));
|
|
|
|
struct Bind * fixed = find(KEY_I);
|
|
struct Bind * oldF2 = find(KEY_F2);
|
|
const unsigned int op = t.opN;
|
|
keybind_inputRegister();
|
|
CHECK(t.activeN == 20);
|
|
CHECK(t.releaseN == 11);
|
|
CHECK(t.collisionN == 2);
|
|
CHECK(find(KEY_I) == fixed);
|
|
CHECK(find(KEY_F2) != oldF2);
|
|
CHECK(!oldF2->active);
|
|
CHECK(t.opN == op + 22);
|
|
for (unsigned int i = 0; i < 11; ++i)
|
|
{
|
|
CHECK(t.op[op + i] < 0);
|
|
CHECK(t.op[op + 11 + i] > 0);
|
|
}
|
|
|
|
t.isLinux = false;
|
|
keybind_inputRegister();
|
|
CHECK(t.activeN == 9);
|
|
CHECK(t.releaseN == 22);
|
|
CHECK(find(KEY_I) == fixed);
|
|
CHECK(strcmp(find(KEY_F1)->handle.description, "external") == 0);
|
|
for (unsigned int i = 1; i < ARRAY_LENGTH(fKeys); ++i)
|
|
CHECK(!find(fKeys[i]));
|
|
|
|
t.isLinux = true;
|
|
keybind_inputRegister();
|
|
CHECK(t.activeN == 20);
|
|
CHECK(t.collisionN == 3);
|
|
}
|
|
|
|
static void testOrder(void)
|
|
{
|
|
reset();
|
|
t.isLinux = true;
|
|
keybind_inputRegister();
|
|
call(KEY_F5);
|
|
|
|
CHECK(t.keyN == 6);
|
|
checkKey(0, true , KEY_LEFTCTRL);
|
|
checkKey(1, true , KEY_LEFTALT );
|
|
checkKey(2, true , KEY_F5 );
|
|
checkKey(3, false, KEY_F5 );
|
|
checkKey(4, false, KEY_LEFTALT );
|
|
checkKey(5, false, KEY_LEFTCTRL);
|
|
}
|
|
|
|
static void testRepeat(void)
|
|
{
|
|
reset();
|
|
keybind_inputRegister();
|
|
call(KEY_LEFTMETA);
|
|
call(KEY_LEFTMETA);
|
|
call(KEY_UP);
|
|
call(KEY_UP);
|
|
|
|
CHECK(t.keyN == 8);
|
|
checkKey(0, true , KEY_LEFTMETA);
|
|
checkKey(1, false, KEY_LEFTMETA);
|
|
checkKey(2, true , KEY_LEFTMETA);
|
|
checkKey(3, false, KEY_LEFTMETA);
|
|
checkKey(4, true , KEY_VOLUMEUP);
|
|
checkKey(5, false, KEY_VOLUMEUP);
|
|
checkKey(6, true , KEY_VOLUMEUP);
|
|
checkKey(7, false, KEY_VOLUMEUP);
|
|
}
|
|
|
|
static void testActions(void)
|
|
{
|
|
reset();
|
|
keybind_commonRegister();
|
|
keybind_inputRegister();
|
|
|
|
call(KEY_F);
|
|
CHECK(t.fullscreen);
|
|
call(KEY_F);
|
|
CHECK(!t.fullscreen);
|
|
|
|
call(KEY_V);
|
|
CHECK(g_state.stopVideo);
|
|
call(KEY_V);
|
|
CHECK(!g_state.stopVideo);
|
|
CHECK(t.videoN == 2);
|
|
|
|
g_params.winRotate = LG_ROTATE_MAX - 1;
|
|
call(KEY_R);
|
|
CHECK(g_params.winRotate == 0);
|
|
call(KEY_R);
|
|
CHECK(g_params.winRotate == 1);
|
|
CHECK(t.posN == 2);
|
|
|
|
call(KEY_O);
|
|
CHECK(g_state.overlayInput);
|
|
call(KEY_O);
|
|
CHECK(!g_state.overlayInput);
|
|
CHECK(t.overlayN == 2);
|
|
|
|
g_state.ignoreInput = false;
|
|
call(KEY_I);
|
|
CHECK(g_state.ignoreInput);
|
|
CHECK(t.cursorN == 1);
|
|
CHECK(!t.cursorInView);
|
|
CHECK(strcmp(t.alert, "Input Disabled") == 0);
|
|
call(KEY_I);
|
|
CHECK(!g_state.ignoreInput);
|
|
CHECK(t.realignN == 1);
|
|
CHECK(strcmp(t.alert, "Input Enabled") == 0);
|
|
|
|
g_cursor.sens = 8;
|
|
call(KEY_INSERT);
|
|
call(KEY_INSERT);
|
|
CHECK(g_cursor.sens == 9);
|
|
CHECK(strcmp(t.alert, "Sensitivity: +9") == 0);
|
|
g_cursor.sens = -8;
|
|
call(KEY_DELETE);
|
|
call(KEY_DELETE);
|
|
CHECK(g_cursor.sens == -9);
|
|
CHECK(strcmp(t.alert, "Sensitivity: -9") == 0);
|
|
|
|
g_state.windowW = 1920;
|
|
g_state.windowH = 1080;
|
|
call(KEY_EQUAL);
|
|
CHECK(t.msgN == 0);
|
|
CHECK(strcmp(t.alert,
|
|
"The guest doesn't support this feature") == 0);
|
|
g_state.transportFeatures = LG_TRANSPORT_FEATURE_WINDOW_SIZE;
|
|
call(KEY_EQUAL);
|
|
CHECK(t.msgN == 1);
|
|
CHECK(t.msg.type == LG_MSG_WINDOWSIZE);
|
|
CHECK(t.msg.windowSize.width == 1920);
|
|
CHECK(t.msg.windowSize.height == 1080);
|
|
|
|
call(KEY_Q);
|
|
CHECK(app_getState() == APP_STATE_SHUTDOWN);
|
|
}
|
|
|
|
struct Test
|
|
{
|
|
const char * name;
|
|
void (*run)(void);
|
|
};
|
|
|
|
static const struct Test tests[] =
|
|
{
|
|
{ "common" , testCommon },
|
|
{ "os" , testOS },
|
|
{ "order" , testOrder },
|
|
{ "repeat" , testRepeat },
|
|
{ "actions", testActions },
|
|
};
|
|
|
|
int main(int argc, char ** argv)
|
|
{
|
|
if (argc != 2)
|
|
{
|
|
fprintf(stderr, "usage: %s <case>\n", argv[0]);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
for (unsigned int i = 0; i < ARRAY_LENGTH(tests); ++i)
|
|
if (strcmp(argv[1], tests[i].name) == 0)
|
|
{
|
|
tests[i].run();
|
|
return 0;
|
|
}
|
|
|
|
fprintf(stderr, "unknown test: %s\n", argv[1]);
|
|
return EXIT_FAILURE;
|
|
}
|