Compare commits

..
2 Commits
Author SHA1 Message Date
Patrick McDavidandClaude Opus 5.5 038b0c03da ft-floatd: a launch for a missing app no longer kills the control socket
Gio.DesktopAppInfo.new() returns NULL for a desktop file that doesn't
exist, and PyGObject raises TypeError ("constructor returned NULL")
rather than returning None, so launch()'s `if info is None` never ran.
The exception escaped the control socket's GLib callback, GLib dropped
the watch, and ft-floatd stopped answering everything: the float key,
dock, Launch as Standalone, and profiles, until the desktop restarted.

Found on the Frame (2026-10-02): a profile saved with RustDesk's
Flatpak open records its window's app id, com.carriez.flutter_hbb,
which has no desktop file (the Flatpak's is com.rustdesk.RustDesk).
`ft-layout use` on that profile asked ft-floatd to launch it, and
ft-floatd went silent. With this, that launch replies "error no app
com.carriez.flutter_hbb" and the profile's other apps open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:12:35 -06:00
DeeJanuzandClaude Opus 5.5 072a294941 README: Frametop doesn't work on the SteamOS beta yet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:12:52 -06:00
8 changed files with 7 additions and 168 deletions

No files matched your search

+2
View File
@@ -17,6 +17,8 @@ Join the [Frametop Discord](https://discord.gg/W3X9f7z3Bc) for questions, ideas,
## Install on the headset
> **Frametop doesn't work on the SteamOS beta right now.** On the beta (SteamOS 0.4.3), gaze mode can't read the eye tracker, and the desktop has started without its taskbar ([#15](https://github.com/DeeJanuz/frametop/issues/15)). Use the stable SteamOS release until this note is gone.
You need a Steam Frame with an internet connection, a keyboard (Bluetooth, or the on-screen one), and about 3 GB of free space.
1. In the launcher, choose Launch a program → Desktop.
-32
View File
@@ -1,32 +0,0 @@
# Controller desktop click stability
Trigger presses reach KDE immediately, but controller motion within 8 logical
pixels of the press stays at that position until release. Releasing without a motion outside this
zone delivers the click at the original position, even if the hand moved during
release. Moving outside the zone begins a normal drag immediately; returning to
the zone does not turn it back into a click. There is no hold-duration timer.
This filters overlay pointer content events on desktop monitors only. Native mouse
input, SteamVR UI, separate screen grab bars and floating-app title-bar carrying
are unaffected. Multi-button gestures keep their existing behavior. A motion
onto another desktop monitor starts a drag; cross-monitor motion is not stabilized.
CLI (runtime preferences, reset to 8 on desktop restart):
```sh
input/ft-clickctl status
input/ft-clickctl threshold 8
input/ft-clickctl threshold 0 # disable without a restart
```
Thresholds are 0–64 logical pixels, normalized to each panel's KDE scale.
Status reports held state, suppressed motions, stabilized clicks and drags.
Changing the threshold while a controller button is held is refused.
This is a separate contribution from desktop mouse/controller ownership. Its
hardware validation must check small controls, intentional text selection,
long presses, cross-monitor dragging and simultaneous mouse use. The existing
renderer laser remains tracked; this change stabilizes desktop input rather
than smoothing the visual laser. Default threshold is a starting point to test.
Run `scripts/test-controller-click.sh` for the isolated gesture-state tests.
+4 -1
View File
@@ -745,7 +745,10 @@ class Daemon:
Returns (reply, pid or None)."""
if app:
app = app.removesuffix(".desktop")
info = Gio.DesktopAppInfo.new(app + ".desktop")
try:
info = Gio.DesktopAppInfo.new(app + ".desktop")
except TypeError: # PyGObject raises for the NULL a missing desktop file returns
info = None
if info is None:
return f"error no app {app}", None
pids = []
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env python3
"""Controller desktop click stability over local IPC; no SteamVR client."""
import argparse
import json
import math
import socket
def request(command):
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as client:
client.bind(''); client.settimeout(2)
client.sendto(command.encode(), '\0ft_screens')
response=client.recv(8192).decode()
if response.startswith('error'): raise RuntimeError(response)
return response
def main():
parser=argparse.ArgumentParser(description=__doc__)
sub=parser.add_subparsers(dest='command', required=True)
sub.add_parser('status')
sub.add_parser('threshold').add_argument('pixels', type=float)
args=parser.parse_args()
try:
if args.command == 'threshold':
if not math.isfinite(args.pixels) or not 0 <= args.pixels <= 64:
raise ValueError('Threshold must be 0–64 logical pixels; 0 disables stabilization')
if request(f'controller-click {args.pixels:g}') != 'ok': raise RuntimeError('Threshold rejected')
print(json.dumps(json.loads(request('controller-click?')), indent=2))
except (OSError, ValueError, RuntimeError) as error: parser.exit(1, str(error)+'\n')
if __name__ == '__main__': main()
+1 -20
View File
@@ -58,7 +58,6 @@
#include <wlr/util/log.h>
#include "vr.h"
#include "controller-click.h"
#define MAX_SCREENS 24 // screens and spare outputs
@@ -105,7 +104,6 @@ struct server {
struct wl_list buffers; // tracked_buffer
struct wl_event_source *tick;
struct screen *pointer_focus;
struct ft_controller_click controller_click;
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).
@@ -294,11 +292,6 @@ static void handle_vr_event(const struct ft_event *e, void *data) {
return;
}
if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return;
struct ft_event filtered = *e;
if (e->screen < s->n_config &&
!ft_controller_click_filter(&s->controller_click, &filtered, s->scale[e->screen])) return;
e = &filtered;
if (e->screen < 0 || e->screen >= MAX_SCREENS || !s->screens[e->screen]) return;
struct screen *sc = s->screens[e->screen];
struct wlr_surface *surface = sc->toplevel->base->surface;
const uint32_t t = now_ms();
@@ -521,18 +514,7 @@ static int control_readable(int fd, uint32_t mask, void *data) {
unsigned code;
int value, index, w, h;
double scale;
char tail;
if (strcmp(buf, "controller-click?") == 0) {
snprintf(reply, sizeof reply,
"{\"supported\":true,\"threshold\":%.3f,\"held\":%s,\"dragging\":%s,\"suppressedMotions\":%lu,\"clicks\":%lu,\"drags\":%lu}",
s->controller_click.threshold, s->controller_click.held ? "true" : "false",
s->controller_click.dragging ? "true" : "false", s->controller_click.suppressed,
s->controller_click.clicks, s->controller_click.drags);
} else if (sscanf(buf, "controller-click %lf %c", &scale, &tail) == 1) {
if (!isfinite(scale) || scale < 0 || scale > 64 || s->controller_click.buttons)
snprintf(reply, sizeof reply, "error threshold or held controller button");
else { s->controller_click.threshold = scale; snprintf(reply, sizeof reply, "ok"); }
} else if (sscanf(buf, "size %d %d %d", &index, &w, &h) == 3) {
if (sscanf(buf, "size %d %d %d", &index, &w, &h) == 3) {
// A new resolution for a screen, live: KWin resizes the screen to match. (KWin makes
// it this size times its scale; ft-floatd sends spares' sizes divided by theirs.)
const int min_w = index - 1 < s->n_config ? 320 : 64, min_h = index - 1 < s->n_config ? 200 : 64;
@@ -659,7 +641,6 @@ static bool setup_dmabuf(struct server *s) {
int main(int argc, char **argv) {
struct server s = {0};
s.controller_click.threshold = 8;
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
s.kb_screen = -1;
const char *socket_name = "ft-screens-0", *control_name = "ft_screens";
-45
View File
@@ -1,45 +0,0 @@
#ifndef FT_CONTROLLER_CLICK_H
#define FT_CONTROLLER_CLICK_H
#include <math.h>
#include <linux/input-event-codes.h>
#include "vr.h"
struct ft_controller_click {
bool held, dragging;
int screen;
uint32_t buttons;
double x, y, threshold, radius;
unsigned long suppressed, clicks, drags;
};
// Hold the desktop position at press time until movement exceeds a logical-pixel
// radius. No timer, delayed button-down, or change to native mouse input.
static inline bool ft_controller_click_filter(struct ft_controller_click *c,
struct ft_event *e, double scale) {
if (e->type == FT_BUTTON && e->button >= BTN_LEFT && e->button < BTN_LEFT+8) {
uint32_t bit = 1u << (e->button-BTN_LEFT);
if (e->pressed) c->buttons |= bit; else c->buttons &= ~bit;
}
if (e->type == FT_BUTTON && e->pressed) {
if (e->button == BTN_LEFT && !c->held && c->threshold > 0) {
c->held = true; c->dragging = false; c->screen = e->screen;
c->x = e->x; c->y = e->y; c->radius = c->threshold * scale;
} else if (e->button != BTN_LEFT) {
c->held = false; // Multi-button gestures retain their usual semantics.
}
} else if (e->type == FT_MOTION && c->held && !c->dragging) {
if (e->screen == c->screen && hypot(e->x-c->x, e->y-c->y) <= c->radius) {
++c->suppressed;
return false;
}
c->dragging = true; ++c->drags;
} else if (e->type == FT_BUTTON && e->button == BTN_LEFT && !e->pressed && c->held) {
if (!c->dragging) {
e->screen = c->screen; e->x = c->x; e->y = c->y;
++c->clicks;
}
c->held = false; c->dragging = false;
} else if (e->type == FT_LEAVE && c->held) {
return false; // Preserve the implicit grab through tiny edge excursions.
}
return true;
}
#endif
-37
View File
@@ -1,37 +0,0 @@
#include <assert.h>
#include "../controller-click.h"
static struct ft_event event(enum ft_event_type t, bool down, double x, double y) {
return (struct ft_event){.type=t,.screen=0,.button=BTN_LEFT,.pressed=down,.x=x,.y=y};
}
int main(void) {
struct ft_controller_click c={.threshold=8};
struct ft_event e=event(FT_BUTTON,true,100,100);
assert(ft_controller_click_filter(&c,&e,2));
e=event(FT_MOTION,false,112,108);
assert(!ft_controller_click_filter(&c,&e,2));
assert(c.held && !c.dragging && c.suppressed==1);
e=event(FT_BUTTON,false,116,111);
assert(ft_controller_click_filter(&c,&e,2));
assert(e.x==100 && e.y==100 && c.clicks==1 && !c.held);
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_MOTION,false,109,100);assert(ft_controller_click_filter(&c,&e,1));
assert(c.dragging && c.drags==1);
e=event(FT_MOTION,false,101,100);assert(ft_controller_click_filter(&c,&e,1));
e=event(FT_BUTTON,false,103,100);assert(ft_controller_click_filter(&c,&e,1));
assert(e.x==103 && c.clicks==1 && !c.held && !c.dragging);
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_LEAVE,false,0,0);assert(!ft_controller_click_filter(&c,&e,1));
e=event(FT_BUTTON,false,800,900);e.screen=1;ft_controller_click_filter(&c,&e,1);
assert(e.screen==0 && e.x==100 && c.clicks==2);
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_MOTION,false,101,100);e.screen=1;
assert(ft_controller_click_filter(&c,&e,1) && c.dragging);
e=event(FT_BUTTON,false,101,100);ft_controller_click_filter(&c,&e,1);
c.threshold=0;
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_MOTION,false,101,100);assert(ft_controller_click_filter(&c,&e,1) && !c.held);
c.threshold=8;
e=event(FT_BUTTON,true,100,100);ft_controller_click_filter(&c,&e,1);
e=event(FT_BUTTON,true,100,100);e.button=BTN_RIGHT;ft_controller_click_filter(&c,&e,1);
assert(!c.held);
}
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
"$root/scripts/frame.sh" -C screens 'mkdir -p build; gcc -std=c11 -Wall -Wextra -Werror tests/controller-click-test.c -lm -o build/controller-click-test && build/controller-click-test'