mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
Send typing to the panel clicked last
An ungrabbed keyboard reached both sides at once: gamescope, which in VR reads every input device itself (the SteamOS build's InputStealer), typed into its focused app, and ft-screens typed into the desktop. Space in the desktop paused Spotify on the dashboard, including every space frame-voice dictated. And while the SteamVR dashboard was open, the desktop got no keys at all. Typing now follows the last click. ft-screens sees clicks on its own screens; the pointer helper reports the panel under the dot on each mouse press, so a click on any other panel sends typing to Steam. While typing goes to the desktop and the screens are showing, ft-screens tells the relay every second, and the relay grabs pass-through keyboards. A grab waits until no key is down, and the relay lets go if ft-screens stops reporting. Controller clicks on other panels aren't visible to overlay apps, so they don't move typing (noted in the README). A program that reads every keyboard for a hotkey loses a grabbed one. Repeating the keys on another input device doesn't work, since gamescope reads that too, so with SHARE_KEYS=1 the relay sends them to @frametop_keys as datagrams with the device name. It's off by default: any local process that binds that name first would get every key typed into the desktop. The docs now say gamescope reads keyboards and the headset's buttons itself; they said SteamVR passed keys on to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eee4aba554
commit
8f051db083
9 files changed
+140
-27
No files matched your search
+46
-7
@@ -91,6 +91,13 @@ struct server {
|
||||
struct wl_event_source *tick;
|
||||
struct screen *pointer_focus;
|
||||
pid_t child;
|
||||
// Where typing goes: the screens after a click on one, Steam after a click on another
|
||||
// panel. The input relay grabs the keyboards while it's the screens (see keys_update).
|
||||
bool keys_clicked; // the last click was on a screen
|
||||
bool keys_desktop; // ...and the screens are showing: typing goes to the desktop
|
||||
int relay_fd; // unbound, so the relay can't reply into our control socket
|
||||
uint32_t relay_sent; // when the relay last heard from us (ms)
|
||||
unsigned ticks;
|
||||
};
|
||||
|
||||
static uint32_t now_ms(void) {
|
||||
@@ -248,7 +255,10 @@ static void handle_vr_event(const struct ft_event *e, void *data) {
|
||||
wlr_seat_pointer_notify_button(s->seat, t, e->button,
|
||||
e->pressed ? WL_POINTER_BUTTON_STATE_PRESSED
|
||||
: WL_POINTER_BUTTON_STATE_RELEASED);
|
||||
if (e->pressed) wlr_seat_keyboard_notify_enter(s->seat, surface, NULL, 0, NULL);
|
||||
if (e->pressed) {
|
||||
wlr_seat_keyboard_notify_enter(s->seat, surface, NULL, 0, NULL);
|
||||
s->keys_clicked = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FT_SCROLL:
|
||||
@@ -274,10 +284,30 @@ static void handle_vr_event(const struct ft_event *e, void *data) {
|
||||
wlr_seat_pointer_notify_frame(s->seat);
|
||||
}
|
||||
|
||||
// Tell the input relay where typing goes, on a change and every second: while it's the
|
||||
// desktop, the relay grabs pass-through keyboards so SteamVR (and the Steam app with
|
||||
// gamescope's focus) doesn't get the keys too. Without word from us for a few seconds,
|
||||
// the relay gives the keyboards back, so a closed desktop doesn't keep them.
|
||||
static void keys_update(struct server *s) {
|
||||
const bool desktop = s->keys_clicked && ft_vr_screens_shown();
|
||||
const uint32_t t = now_ms();
|
||||
if (desktop == s->keys_desktop && t - s->relay_sent < 1000) return;
|
||||
if (desktop != s->keys_desktop) wlr_log(WLR_INFO, "typing goes to %s", desktop ? "the desktop" : "Steam");
|
||||
s->keys_desktop = desktop;
|
||||
s->relay_sent = t;
|
||||
struct sockaddr_un addr = {.sun_family = AF_UNIX};
|
||||
const char name[] = "frametop_relay";
|
||||
memcpy(addr.sun_path + 1, name, sizeof name - 1);
|
||||
const char *msg = desktop ? "keyboard desktop" : "keyboard steam";
|
||||
sendto(s->relay_fd, msg, strlen(msg), MSG_DONTWAIT, (struct sockaddr *)&addr,
|
||||
offsetof(struct sockaddr_un, sun_path) + 1 + sizeof name - 1);
|
||||
}
|
||||
|
||||
// Every ~11 ms (90 Hz): SteamVR events, and frame callbacks for screens that committed.
|
||||
static int tick(void *data) {
|
||||
struct server *s = data;
|
||||
ft_vr_poll(handle_vr_event, s);
|
||||
if (++s->ticks % 9 == 0) keys_update(s);
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
for (int i = 0; i < MAX_SCREENS; ++i) {
|
||||
@@ -310,15 +340,15 @@ static bool key_held(const struct wlr_keyboard *kb, uint32_t code) {
|
||||
}
|
||||
|
||||
// Keys from the input relay (physical keyboards): "key <evdev code> <1 press|0 release>".
|
||||
// They go to the screen KWin has keyboard focus on (the last one clicked), but not while
|
||||
// the SteamVR dashboard is open: typing belongs to Steam then. The release of a key the
|
||||
// desktop got the press for always goes through, or the key stays held there (a modifier
|
||||
// held as the dashboard opens would otherwise modify every key typed after it).
|
||||
// They go to the screen KWin has keyboard focus on (the last one clicked), while typing
|
||||
// goes to the desktop (keys_update). The release of a key the desktop got the press for
|
||||
// always goes through, or the key stays held there (a modifier held as typing moves to
|
||||
// Steam would otherwise modify every key typed after it).
|
||||
static void handle_key(struct server *s, uint32_t code, int value, char *reply, int size) {
|
||||
if (value == 2) return (void)snprintf(reply, size, "ok repeat ignored"); // KWin repeats itself
|
||||
if (value || !key_held(&s->keyboard, code)) {
|
||||
if (!s->seat->keyboard_state.focused_surface) return (void)snprintf(reply, size, "ok no focus");
|
||||
if (ft_vr_dashboard_visible()) return (void)snprintf(reply, size, "ok dashboard open");
|
||||
if (!s->keys_desktop) return (void)snprintf(reply, size, "ok typing goes to Steam");
|
||||
}
|
||||
struct wlr_keyboard_key_event ev = {
|
||||
.time_msec = now_ms(), .keycode = code, .update_state = true,
|
||||
@@ -330,7 +360,9 @@ static void handle_key(struct server *s, uint32_t code, int value, char *reply,
|
||||
}
|
||||
|
||||
// Control socket: abstract datagram @ft_screens. Here: "size <screen> <w> <h>" (a new
|
||||
// resolution, live) and "key <code> <value>"; the rest is in vr.cpp (ft_vr_command).
|
||||
// resolution, live), "key <code> <value>", and "click <overlay key>" (from the pointer
|
||||
// helper: a mouse click landed on that panel, "-" for none); the rest is in vr.cpp
|
||||
// (ft_vr_command).
|
||||
static int control_readable(int fd, uint32_t mask, void *data) {
|
||||
struct server *s = data;
|
||||
char buf[512], reply[2048];
|
||||
@@ -355,6 +387,12 @@ static int control_readable(int fd, uint32_t mask, void *data) {
|
||||
handle_key(s, code, value, reply, sizeof reply);
|
||||
len = sizeof from;
|
||||
continue; // no reply: keys are fire-and-forget
|
||||
} else if (strncmp(buf, "click ", 6) == 0) {
|
||||
// A click on another panel takes typing to Steam. Our own panels (the screens,
|
||||
// their controls) leave it: a click on a screen arrives as a panel event.
|
||||
if (strcmp(buf + 6, "-") != 0 && strncmp(buf + 6, "frametop.", 9) != 0) s->keys_clicked = false;
|
||||
len = sizeof from;
|
||||
continue;
|
||||
} else {
|
||||
ft_vr_command(buf, reply, sizeof reply);
|
||||
}
|
||||
@@ -478,6 +516,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
const int control = open_control_socket();
|
||||
if (control < 0) return 1;
|
||||
s.relay_fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
wl_event_loop_add_fd(s.loop, control, WL_EVENT_READABLE, control_readable, &s);
|
||||
|
||||
s.tick = wl_event_loop_add_timer(s.loop, tick, &s);
|
||||
|
||||
+1
-1
@@ -1032,7 +1032,7 @@ int ft_vr_modifiers(uint32_t format, uint64_t *out, int max) {
|
||||
return int(n < uint32_t(max) ? n : uint32_t(max));
|
||||
}
|
||||
|
||||
bool ft_vr_dashboard_visible(void) { return vr::VROverlay()->IsDashboardVisible(); }
|
||||
bool ft_vr_screens_shown(void) { return ModeVisible(); }
|
||||
|
||||
void ft_vr_screen_create(int index, double metres, int count) {
|
||||
Screen &s = g_screens[index];
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ bool ft_vr_init(void);
|
||||
void ft_vr_shutdown(void);
|
||||
// Modifiers SteamVR can import for a DRM format. Returns the count (at most max).
|
||||
int ft_vr_modifiers(uint32_t format, uint64_t *out, int max);
|
||||
// The SteamVR dashboard is open (typing belongs to it then, not to the screens).
|
||||
bool ft_vr_dashboard_visible(void);
|
||||
// The screens are showing (by the visibility mode; not counting a wrist-pinned screen).
|
||||
bool ft_vr_screens_shown(void);
|
||||
// A panel for screen `index`, width in metres, placed in a row in front of the head.
|
||||
void ft_vr_screen_create(int index, double metres, int count);
|
||||
void ft_vr_screen_destroy(int index);
|
||||
|
||||
Reference in new issue
Block a user