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:
DeeJanuzandClaude Opus 5.5 committed 2026-09-27 21:52:21 -06:00
1 parent eee4aba554
commit 8f051db083
9 files changed
+139 -26

No files matched your search

+1
View File
@@ -65,6 +65,7 @@ This is an early release, tested on one Steam Frame (SteamOS 0.3.0 build 2026092
- The first install downloads 1–2 GB for the build container and compiles everything on the headset, which takes several minutes.
- During a VR game you can't show the screens with a controller button, because the game owns the buttons. Open the SteamVR dashboard, press Meta+Shift+H, or use a mapped mouse button instead.
- Flatscreen games aren't detected as games. If your controllers end up working the screens instead of the game, set Controllers on the screens to "Only with the SteamVR dashboard open" (Frametop Display Settings, Visibility & wrist tab).
- Typing follows your last click. A controller click on a panel other than the screens (the dashboard, a Steam app) doesn't move typing there; click it with the mouse, or click a screen to bring typing back.
- The screens don't draw a mouse cursor of their own. The 3D mouse's dot or SteamVR's laser shows where you're pointing.
- Remote desktop over VNC (`./desktops.sh remote on`) needs Tailscale on the Frame.
+3 -1
View File
@@ -100,7 +100,9 @@ SteamVR opens every input device only when it starts. When a Bluetooth mouse sle
Keyboards aren't grabbed by default, because a grabbed keyboard's keys went into a virtual keyboard nothing typed from; the relay forwards them to ft-screens instead.
Volume keys must never reach gamescope. With the openvr backend, gamescope sends volume up and down to Steam by moving keyboard focus to Steam for the key and then back to the previously focused surface. When nothing had focus, the one it moves back to is null, and wlroots aborts on a null focus surface (`wlr_seat_keyboard_notify_enter: Assertion 'surface' failed`), which ends the whole VR session. Keyboard focus is often empty while you work in VR, so one press of the headset's volume button could take everything down. SteamVR reads the headset's buttons itself and passes keys on to gamescope, so the relay has to stop volume keys before SteamVR sees them. Grabbing `gpio-keys` would also take the headset's click button, so the relay remaps the volume entries in each device's keymap (`EVIOCSKEYCODE`) and handles the stand-in codes itself. That fix covers every device at once, including keyboards that aren't grabbed.
An ungrabbed keyboard reaches both sides at once. In VR, gamescope reads every input device itself (the SteamOS build's `InputStealer`, libinput with udev hotplug, so new devices too) and types into its focused app, and ft-screens types the same keys into the desktop. So Space in the desktop also paused Spotify on the dashboard. Typing now follows the last click. ft-screens sees clicks on its own screens, from the mouse or a controller. A click anywhere else is only visible for the mouse: overlay apps get SteamVR's `OverlayFocusChanged` (which panel the laser is on) but no controller button events, so the pointer helper reports the panel under the dot on each left press. ft-screens tells the relay where typing goes every second, from an unbound socket so the relay's replies can't loop back into its control socket, and the relay grabs pass-through keyboards while it's the desktop. A grab waits until the keyboard has no key down, so no key stays held on either side, and the relay lets go if ft-screens stops reporting. A program that reads every keyboard for a hotkey (a dictation tool, say) loses a grabbed keyboard. Repeating the keys on another input device doesn't work: gamescope reads that device too, whether it's the relay's virtual keyboard or one created later, and every Space, typed or dictated, paused Spotify again. So with `SHARE_KEYS=1` the relay sends a grabbed keyboard's keys to `@frametop_keys` as datagrams (`key <code> <value> <device name>`). It's off by default, because the relay can't tell who is listening: abstract sockets have no permissions, and any local process that binds the name first gets every key typed into the desktop, passwords included. A listener should accept only its own user (`SO_PASSCRED`) and skip any keyboard of its own that the relay grabs too.
Volume keys must never reach gamescope. With the openvr backend, gamescope sends volume up and down to Steam by moving keyboard focus to Steam for the key and then back to the previously focused surface. When nothing had focus, the one it moves back to is null, and wlroots aborts on a null focus surface (`wlr_seat_keyboard_notify_enter: Assertion 'surface' failed`), which ends the whole VR session. Keyboard focus is often empty while you work in VR, so one press of the headset's volume button could take everything down. gamescope reads the headset's buttons and every keyboard itself (`InputStealer`), as do SteamVR's processes, so the relay has to stop volume keys at the device. Grabbing `gpio-keys` would also take the headset's click button, so the relay remaps the volume entries in each device's keymap (`EVIOCSKEYCODE`) and handles the stand-in codes itself. That fix covers every device at once, including keyboards that aren't grabbed.
## The desktop session
+3 -3
View File
@@ -51,7 +51,7 @@ In the last three modes the hotkey shows the screens anyway. Two more settings o
- During VR games, the Always mode hides the screens unless the dashboard is open (the default), or leaves them up.
- Controllers on the screens. Visible screens can keep SteamVR's laser mouse on, so controllers work them with the dashboard closed, but that also takes the controllers away from a game. By default this is off while a VR game runs, and the 3D mouse or the dashboard works the screens. The other choices are always on, or only with the dashboard open, which also suits flatscreen games since they aren't scene apps.
Input from the lasers reaches KWin through ft-screens' own seat. Keys come from the input relay, from any keyboard it doesn't grab and any key a pointer device passes through, and go to the screen you clicked last, except while the SteamVR dashboard is open.
Input from the lasers reaches KWin through ft-screens' own seat. Keys come from the input relay, from pass-through keyboards and any key a pointer device passes through. Typing follows your last click: after a click on a screen it goes to the desktop, even with the SteamVR dashboard open, and after a mouse click on any other panel (the dashboard, Steam, an app like Spotify) it goes there instead. While it goes to the desktop, the relay grabs pass-through keyboards so gamescope, which reads every keyboard itself, doesn't type them into the Steam app too. A program that watches every keyboard for a hotkey loses a grabbed one; with `SHARE_KEYS=1` in `~/.config/frametop.conf`, their keys also go to `@frametop_keys` for it. That's off by default, since any local process that binds the name first would get everything typed into the desktop. Hidden screens don't take typing.
ft-screens listens for datagrams on the abstract socket `@ft_screens` and replies to the sender:
@@ -69,7 +69,7 @@ SteamVR opens input devices only when it starts. A Bluetooth mouse that sleeps a
It runs as the user service `frametop-input-relay.service`, ordered before `steamvr.service`.
The relay also owns the volume keys, on every device that has them, the headset's buttons included. It changes the volume itself (`wpctl`, 5% a step, repeating while held), and SteamVR never sees a volume key: on devices with a keymap (the headset's `gpio-keys`, USB and Bluetooth keyboards) it remaps just the volume entries to unused codes (`KEY_MACRO29`, `KEY_MACRO30`), so the headset's click button and the other keys still reach SteamVR, and it grabs `pmic_resin`, which has only volume down. The keymaps go back when the relay stops. With `--no-grab` it leaves the volume keys alone.
The relay also owns the volume keys, on every device that has them, the headset's buttons included. It changes the volume itself (`wpctl`, 5% a step, repeating while held), and nothing else sees a volume key, gamescope and SteamVR included: on devices with a keymap (the headset's `gpio-keys`, USB and Bluetooth keyboards) it remaps just the volume entries to unused codes (`KEY_MACRO29`, `KEY_MACRO30`), so the headset's click button and the other keys still work, and it grabs `pmic_resin`, which has only volume down. The keymaps go back when the relay stops. With `--no-grab` it leaves the volume keys alone.
```
desktops.sh relay install # enable it (starts with the next reboot or SteamVR start)
@@ -104,7 +104,7 @@ The pointer settings are in `~/.config/frametop.conf`: `POINTER_SENSITIVITY`, `P
A Kirigami app with a Python backend, in the Plasma menu under Settings. It runs in the `dev` container and talks to the relay over its control socket, `@frametop_relay`. It has four pages:
- Devices lists every USB and Bluetooth mouse and keyboard, with a light that flashes when the device is used. Each device gets a role: 3D pointer (grabbed, drives the pointer; the default for anything with a mouse), Pass through (not grabbed; the default for keyboards, where a Meta tap toggles the dashboard if `META_DASHBOARD=1` is in `~/.config/frametop.conf`), or Ignore. A device is identified by its Bluetooth address, or its USB ids and name, so all of its input nodes share one role. Forget drops everything saved for a device.
- Devices lists every USB and Bluetooth mouse and keyboard, with a light that flashes when the device is used. Each device gets a role: 3D pointer (grabbed, drives the pointer; the default for anything with a mouse), Pass through (grabbed only while typing goes to the desktop; the default for keyboards, where a Meta tap toggles the dashboard if `META_DASHBOARD=1` is in `~/.config/frametop.conf`), or Ignore. A device is identified by its Bluetooth address, or its USB ids and name, so all of its input nodes share one role. Forget drops everything saved for a device.
- Buttons maps a pointer device's buttons. Choose Capture a button, press the button or key, then pick an action: a click, back, scroll, toggle dashboard, recenter, pointer on or off, faster or slower, pass the key through, or nothing. Devices with saved mappings are listed even while they're asleep.
- Pointer has sliders for the pointer settings, which apply immediately, and a Recenter button.
- Bluetooth lists paired devices and has Apply Bluetooth fixes, which runs `/etc/steamframe/bt-fixups.sh` through `pkexec`. Pair new devices in Steam.
+79 -12
View File
@@ -13,8 +13,9 @@ so all its event nodes share one role (the Swiftpoint Z3 has a mouse node and a
keyboard node for its extra buttons). Roles, from ~/.config/frametop-input.json
(written by the Frametop Input Settings app):
pointer grabbed; drives the universal 3D mouse (default for devices with a mouse node)
passthrough not grabbed, only observed, e.g. for the Meta dashboard shortcut (default for keyboards;
the shortcut is off unless META_DASHBOARD=1 is in ~/.config/frametop.conf)
passthrough keys go to the desktop; grabbed only while typing goes there, otherwise only observed,
e.g. for the Meta dashboard shortcut (default for keyboards; the shortcut is off
unless META_DASHBOARD=1 is in ~/.config/frametop.conf)
ignore not grabbed, only observed for identification in the settings app
Buttons and keys of pointer devices go through a per-device map to actions
(left, right, middle, back, scroll_up, scroll_down, dashboard, recenter,
@@ -23,8 +24,16 @@ their saved layout, screens_toggle = hide or show the desktop screens, key = pas
through as a key, none).
Keys also go to ft-screens (@ft_screens, the Frametop desktop's compositor), which
types them into the desktop screen that has focus (not while the SteamVR dashboard is
open): from keyboards that aren't grabbed, and keys a pointer device passes through.
types them into the desktop screen that has focus: from pass-through keyboards, and
keys a pointer device passes through. Typing goes to the panel clicked last, and
ft-screens says which ("keyboard desktop|steam" on the control socket, every second).
While it's the desktop, pass-through keyboards are grabbed, so gamescope, which reads
every keyboard itself, doesn't type them into its focused app too. Without word from
ft-screens for 3 seconds they're released. With SHARE_KEYS=1 in ~/.config/frametop.conf,
a grabbed keyboard's keys also go out as "key <code> <value> <device name>" datagrams on
@frametop_keys, for programs that watch every keyboard for a hotkey and lose it to the grab.
It's off by default: any local process that binds that name first gets every key typed
into the desktop.
Volume keys, from every device that has them (the headset's own buttons included),
are handled here: wpctl steps the default output. Nothing else may see a volume key,
@@ -107,6 +116,7 @@ UI_SET_EVBIT = _iow("U", 100, 4)
UI_SET_KEYBIT = _iow("U", 101, 4)
UI_SET_RELBIT = _iow("U", 102, 4)
EVIOCGRAB = _iow("E", 0x90, 4)
EVIOCGKEY = _ior("E", 0x18, (KEY_MAX + 8) // 8)
EVIOCGID = _ior("E", 0x02, 8)
KEYMAP_ENTRY = struct.Struct("BBHI32s") # struct input_keymap_entry: flags, len, index, keycode, scancode
INPUT_KEYMAP_BY_INDEX = 1
@@ -132,6 +142,7 @@ RULES_PATH = os.path.expanduser("~/.config/frametop-input.json")
ACTIONS = ("left", "right", "middle", "back", "scroll_up", "scroll_down", "dashboard", "recenter",
"pointer_toggle", "sens_up", "sens_down", "layout_reset", "screens_toggle", "key", "none")
SCREENS = "\0ft_screens"
KEYS = "\0frametop_keys" # keys of keyboards grabbed for the desktop, for other readers
FT_LAYOUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "layout", "ft-layout")
DEFAULT_BUTTONS = {BTN_LEFT: "left", BTN_RIGHT: "right", BTN_MIDDLE: "middle",
BTN_SIDE: "back", BTN_EXTRA: "back"}
@@ -531,12 +542,16 @@ def main():
control.setblocking(False)
watchers = {} # address -> watch end time
state = {"pointer": None, "rules": {}, "meta_dashboard": False}
# desktop_until: typing goes to the Frametop desktop until then (ft-screens says so
# every second); typing_applied: the grabs match that as of the last apply_roles().
state = {"pointer": None, "rules": {}, "meta_dashboard": False, "share_keys": False,
"desktop_until": 0.0, "typing_applied": None}
def load_config():
conf = read_config()
state["rules"] = read_rules()
state["meta_dashboard"] = conf.get("META_DASHBOARD", "0") == "1"
state["share_keys"] = conf.get("SHARE_KEYS", "0") == "1"
if conf.get("POINTER", "0") == "1":
p = state["pointer"] or Pointer(0.03, 30)
p.sensitivity = float(conf.get("POINTER_SENSITIVITY", "0.03"))
@@ -567,6 +582,19 @@ def main():
next_scan = 0.0
volume = Volume()
keys_sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_NONBLOCK)
def share_key(node, code, value):
"""With SHARE_KEYS=1, a key from a keyboard grabbed for the desktop, for programs
that watch every keyboard for a hotkey and lose it to the grab. It can't go on
another input device: gamescope reads every keyboard itself and would type it
into its focused app."""
if not state["share_keys"]:
return
try:
keys_sock.sendto(f"key {code} {value} {node.name}".encode(), KEYS)
except OSError:
pass # nobody listening
def restore_keymaps():
"""Give remapped devices their volume keys back, so they work without the relay."""
@@ -581,7 +609,7 @@ def main():
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) # so atexit runs on systemctl stop
def take_volume(node):
"""Keep the node's volume keys from SteamVR (and so from gamescope).
"""Keep the node's volume keys from gamescope and SteamVR, which read every device.
Returns False for a non-candidate node there's nothing to do with.
"""
@@ -619,18 +647,39 @@ def main():
def release_held(node):
for code in node.held:
if node.role == "passthrough":
share_key(node, code, 0)
else:
(mouse if code >= BTN_MISC else keyboard).emit(EV_KEY, code, 0)
node.held.clear()
mouse.sync()
keyboard.sync()
def keys_down(node):
buf = bytearray((KEY_MAX + 8) // 8)
try:
fcntl.ioctl(node.fd, EVIOCGKEY, buf)
except OSError:
return False
return any(buf)
def apply_roles():
"""Grab pointer devices, and pass-through keyboards while typing goes to the desktop.
A keyboard with a key down keeps its grab state until it's released, or the key
would stay held on one side. Returns True if one is still waiting.
"""
desktop = time.monotonic() < state["desktop_until"]
waiting = False
for node in nodes.values():
if not node.candidate:
continue # volume keys only, taken over when found
role = role_of(node)
want_grab = can_grab and role == "pointer"
if want_grab != node.grabbed:
want_grab = can_grab and (role == "pointer"
or (role == "passthrough" and node.is_keyboard and desktop))
if want_grab != node.grabbed and role == "passthrough" and keys_down(node):
waiting = True
elif want_grab != node.grabbed:
try:
fcntl.ioctl(node.fd, EVIOCGRAB, 1 if want_grab else 0)
node.grabbed = want_grab
@@ -641,6 +690,10 @@ def main():
if role != node.role:
log(f"{node.name} ({node.path}, {node.id}): {role}{', grabbed' if node.grabbed else ''}")
node.role = role
if not waiting and desktop != state["typing_applied"]:
state["typing_applied"] = desktop
log(f"typing goes to {'the desktop (keyboards grabbed)' if desktop else 'Steam'}")
return waiting
def drop(node, reason):
release_held(node)
@@ -663,10 +716,15 @@ def main():
data, addr = control.recvfrom(4096)
except BlockingIOError:
return
if not addr:
continue # unbound sender, nowhere to reply
words = data.decode(errors="replace").split()
cmd = words[0] if words else ""
if cmd == "keyboard":
# From ft-screens (unbound, no reply): where typing goes, repeated every second.
desktop = len(words) > 1 and words[1] == "desktop"
state["desktop_until"] = now + 3.0 if desktop else 0.0
continue
if not addr:
continue # unbound sender, nowhere to reply
if cmd == "devices":
reply(addr, {"t": "devices", "pointer_mode": state["pointer"] is not None,
"actions": ACTIONS,
@@ -698,6 +756,7 @@ def main():
else:
reply(addr, msg)
waiting = False # a keyboard's grab waits for its keys to come up
while True:
now = time.monotonic()
pointer = state["pointer"]
@@ -736,6 +795,8 @@ def main():
if pointer:
pointer.tick(now)
volume.tick(now)
if (now < state["desktop_until"]) != state["typing_applied"] or waiting:
waiting = apply_roles()
for fd in ready:
if fd is control:
handle_control(now)
@@ -766,10 +827,16 @@ def main():
if node.role == "volume":
continue
if node.role != "pointer":
# Observed only. With META_DASHBOARD=1, a Meta tap on any keyboard
# toggles the dashboard.
# Observed only, unless typing goes to the desktop. With META_DASHBOARD=1,
# a Meta tap on any keyboard toggles the dashboard.
if node.role == "passthrough" and etype == EV_KEY:
to_screens(code, value)
if node.grabbed and code < BTN_MISC and value in (0, 1):
share_key(node, code, value)
if value:
node.held.add(code)
else:
node.held.discard(code)
if (pointer and state["meta_dashboard"] and node.role == "passthrough"
and etype == EV_KEY):
if code in (KEY_LEFTMETA, KEY_RIGHTMETA):
+3
View File
@@ -633,6 +633,9 @@ int main() {
continue;
}
if (std::strncmp(buf, "btn trigger 1", 13) == 0) {
// ft-screens sends the keyboard to the panel clicked last; it sees clicks on
// its own screens, but only we know when one lands on another panel.
SendTo(out, "ft_screens", "click " + (lastHit.empty() ? std::string("-") : lastHit));
leftHeld = true;
dragDistance = lastDistance;
tiltYaw = tiltPitch = 0; // a new drag starts untilted
+46 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+1
View File
@@ -19,5 +19,6 @@ POINTER_ORIGIN_MARGIN=0.15 # the laser starts at least this far (m) in front o
POINTER_SCENE_RADIUS=0.5 # texture-less dashboard overlays (dock, window controls): hit radius (m) around their origin
POINTER_EDGE_REACH=0.3 # just off a panel, the cursor stays on its plane this far (m), to reach its resize edges and window controls
META_DASHBOARD=0 # 1 = a Meta tap on a pass-through keyboard toggles the SteamVR dashboard (pointer mode only)
SHARE_KEYS=0 # 1 = keys of keyboards grabbed for the desktop also go to @frametop_keys, for hotkey tools (any local process can listen)
LAYOUT_GRAB_OFFSET=0.075 # arranging screens: where the floating window's grab bar is, in metres below the screen
LAYOUT_SLIDE_SPEED=0.5 # arranging screens: how fast the invisible controller slides a grabbed screen (m/s)