mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 09:00:11 +02:00
9f2ee5ad68d7a7d9470790f8686ed4f845f5e0ef
154
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f2ee5ad68 |
A one-command uninstaller that keeps the headset usable
uninstall.sh replaces the README's thirteen uninstall commands. Run from a Frametop desktop, those commands stopped the input relay second, which took the keyboard and mouse away before the rest could be typed. And deleting ~/frametop first left Launch a program -> Desktop pointing at a missing script. The script works in two steps. Step 1 stops Frametop from starting: the launcher entry, the user services (disabled, not stopped), the SteamVR driver, the menu entries, and the eye grabber and Bluetooth fixes under /etc (one sudo). Everything running keeps running until the headset restarts, and the script offers to restart it. Step 2 runs once none of Frametop's programs run. It deletes the code, and asks before deleting the settings, recordings, and the dev container. It doesn't use the rest of the repo, so the Pages one-liner works even when ~/frametop is gone. --dry-run shows each command without running it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ca9492be00 |
Pages: link the Frametop Discord at the top and bottom of the hand recorder page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
41b5b68a41 |
Pages: hand recorder install page
hand-recorder.md, served at deejanuz.github.io/frametop/hand-recorder.html: the Konsole commands to install, update and uninstall the Hand Recorder, who can take part, and what it needs. Frametop comes first for now, since the recorder runs in its desktop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9d7c8a0b91 |
Experimental (#29)
* Input relay: typing with the pointer helper down no longer ends the relay Typing on a pass-through keyboard tells the helper "typing". With the helper not running (SteamVR off), that send raised ConnectionRefusedError and the relay exited, dropping every grab until systemd restarted it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ft-steam: open Steam's menu through Steam's own UI steam/ft-steam menu opens the SteamVR dashboard on Steam's menu, or closes the dashboard if it's up, without pointer mode: it asks Steam's UI over its debugging port to show its dashboard overlay (ShowVROverlay, what Steam calls itself) and focus the Steam frame's left menu (MenuStore.OpenMainMenu). ft-steam check says whether those calls still exist, and update-check.py runs it, since a Steam client update can rename them. The CDP client moves from display-settings/steam_settings.py to steam/steamui.py, so both use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shortcuts: Steam menu, commands, and modifier taps Key combinations (and mouse and controller buttons) get two new actions: - steam_menu: Open Steam menu / close dashboard (steam/ft-steam menu). - command:CMD: run CMD with sh -c, as the relay's service, with layout/, float/ and steam/ on its PATH. Input Settings offers it for key combinations as Run a command... Both work without pointer mode. A modifier on its own is now a key combination too: a tap, pressed and released with no other key, mouse button, or scroll in between. A bound tap sends the desktop F24 before the release, so Plasma's launcher stays shut. The defaults gain a Meta tap for the Steam menu; this replaces META_DASHBOARD, which only worked in pointer mode. input/test/keys-test.py runs the relay against fake devices with every outgoing socket renamed, so it's safe next to the live relay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Relay: share a key combination's Meta release with frame-voice A Meta+key combination (Meta+J for gaze_left, say) hides Meta's release from the desktop, and the relay skipped share_key for it too. frame-voice saw Meta go down on @frametop_keys and never come up, so it held all dictated text back, waiting for that release. Keys of grabbed keyboards are now shared as pressed, before key_binding() decides what the desktop gets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hands: ft-cutouts, the hand cutouts without pinches and grips hands/ft-cutouts on|off|status starts ft-camd and ft-hands as transient user units with ft-hands' new --no-gestures: hands are published for ft-screens' cutouts, but no pinch or grip is detected, so nothing clicks or drags and a closing hand doesn't raise the tracking rate. It needs a build and ft-camd's capabilities, not hands/run.sh install. Its units conflict with ft-handsctl's, so each stops the other, and they stop with SteamVR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ft-cutouts status: only the current run's tracker lines Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Gaze: say why gaze mode can't work yet, and open the calibration whenever it's missing Turning gaze mode on without a calibration opened Calibrate only on the off-to-on change, and only if it could open right then. With the headset off, the eye tracker silent, or the panel not built, or with gaze mode already on when the gaze service started, nothing opened and nothing said why: the pointer just stayed a mouse. - The gaze service now checks every second: gaze mode on, no calibration for the tracker in use, eyes seen -> the full calibration opens. One that closes unfinished opens again only after the headset comes off and on, gaze mode off and on, or Calibrate, so it doesn't loop. A start that fails retries every 10 s. - Its status says why gaze mode can't work yet (checks.problem): not calibrated and opening, open, closed unfinished, or can't open and why. - Input Settings shows that under the Gaze pointer switch, along with the gaze service not installed or not running and our tracker missing its frame grabber. - ft-gazectl on notes a missing calibration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Gaze: install our eye tracker, prefer it, and say why a calibration dot wasn't taken A user on a fresh install got "Calibration failed: only 0 of 21 dots" with no reason. The installer never installed our tracker, so gaze used SteamVR's, and the only way SteamVR's tracker rejects a dot is losing an eye for most of the look. The panel just showed a red ring. - install.sh: step 9/10 installs our tracker (gaze/tracker/install.sh) after gaze mode, yes by default; it needs sudo, so --yes runs it only when sudo won't prompt. If it fails, gaze keeps SteamVR's tracker. Configs that still say GAZE_TRACKER=steam (the old template) are asked whether to switch. - GAZE_TRACKER=auto, the new default: ours when it's installed (the frame grabber, its unit, and ft-eyes' Python), else SteamVR's. ft-gazed rechecks every second, so installing it switches over. Input Settings lists Own tracker first as recommended, and says how to install it when it's missing (checking the host's /etc through /run/host from the dev container). - The calibration panel has a note line, orange over the instructions: why a dot wasn't taken (an eye lost, a blink, the eyes disagreeing for SteamVR's tracker, from steady_samples' new drop counts; ft-eyes' reply for ours), what a click is still waiting for after 1.5 s, and a failed calibration's most common reason, which the Gaze page shows too. steady_samples keeps the same samples as before (checked on 2037 windows of recordings); a lost eye is named before a blink, since its openness reads 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * README: link the Frametop Discord Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Wait for a new container to finish setting up before entering it container-up.sh starts the dev container in a scope of its own, so distrobox enter finds it running and skips its wait for distrobox-init. On a fresh install, init was still setting up passwordless sudo when dev-container.sh ran sudo dnf install, and sudo asked for a password with no terminal to read it from. container-up.sh now waits for container_setup_done itself, and the container's sudo calls use -n, so a password prompt fails at once with a clear message. Fixes #9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Prevent small desktop overlay pointer movements from starting a drag * Clear reported drag state on controller release * Click stability: only a hand controller's press starts it The 3D mouse drives SteamVR's laser through the ft_pointer virtual controller, so its events reach the screens the same way a controller's do. The filter held every press, which turned the mouse's short drags (selecting a character or two, nudging a slider) into clicks. Mark button events from hand controllers and start the filter only on those. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Input relay: retry a new device until udev gives it to the input group A new /dev/input node is root:root 0600 until udev applies GROUP=input. The scan probed each new node once and marked it seen even when the open failed, so a node caught in that gap was never opened. Behind a KVM, a switch brings back a hub of devices at once: on the Frame, four nodes failed with EACCES in one switch, the keyboard was never grabbed, and its keys went to gamescope instead of the desktop screens. A node that isn't readable yet now waits for the next scan. * Screens: take a screen's overlays from one copy in the catcher While a button pressed on a screen is held, UpdateCatcher checks every tick whether the laser is still on one of the screen's overlays. It built that list from s.All().begin() and s.All().end(), but All() returns a std::array by value: iterators into two different temporaries, which is undefined behaviour. A clang build of ft-screens got a garbage length, threw std::length_error, and aborted on the first click, taking KWin and the desktop with it. * Gaze: leave SteamVR's gaze action alone during VR games From curiousjtuber's PR #13: with the gaze service running, SteamVR restarted its eye tracker every 10 to 13 s of Beat Saber, as if the headset came off, and each restart took input focus from the game. The PR stopped every read in a game. Only the action path reaches SteamVR (UpdateActionState on the gaze set at overlay-global priority, then GetEyeTrackingDataRelativeToNow); the mmap and our tracker are read-only files. So only the action is skipped while a scene app runs, and gaze keeps moving the pointer over the dashboard in a game. The action source is only used with --source action. Co-Authored-By: CuriousJ <curious.j.tuber@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hands: --record-hz, and the hand recorder's design (hands/rec/DESIGN.md) ft-hands --record-hz N records at most N frame sets a second, for the hand recorder (10). DESIGN.md lays out the recorder: the headset panel, the session runner and its script, the files, review and export, consent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hands: ft-handpanel, the hand recorder's headset panel A head-locked SteamVR overlay for the hand recorder (hands/rec/DESIGN.md): 1.2 m ahead, 12 degrees up, 36 degrees wide, drawn with stb_truetype into three shared DMA-BUFs as ft-gazepanel does. It shows the title, step, wrapped instruction, note, countdown, hand chips, near/far bar and a "Paused" cover, driven over @ft_handpanel. It also places the touch target, a 2 cm dot in its own overlay fixed in the room where the head was at the first command for that point, and logs head and controller poses to poses.jsonl at 250 Hz from a thread of its own. Both threads take one lock around OpenVR calls. --no-vr prints each picture's state to stdout (and --dump writes the pictures), for testing without a headset. hands/rec/build.sh builds it in the dev container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hands: the recorder's worn check goes by the panel's backlight, as frame-job does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hands: the hand recorder's session runner and guided script hands/rec/session.py runs a recording session from script.json: it starts ft-camd and a tracking ft-hands as transient units only if they aren't running, records each section as one take (ft-hands --record-only at 10 sets/s, a new sets-N.bin after each pause), drives ft-handpanel, and writes session.json, calibration.json (identifying fields removed), prompts.jsonl and take.json. Feedback comes from the live hands file and, in the controller sections, from the panel's device poll. It also runs from the command line (--dry-run, --speed, --ring, --no-start). hands/rec/script.json: 11 sections, about 9 minutes without the object and controller sections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hands: the hand recorder's window, review and export hands/rec/ft_handrec.py + main.qml (Kirigami, dev container; host launcher hands/rec/ft-handrec): consent (CONSENT.md, asked again when its version changes; profile.json with a random contributor id), the before-you-start checklist with the lighting and free-space checks, the session controls (Space pauses, Esc stops), review with a frame-set viewer that deletes ranges, takes and sessions, export with progress and cancel (warns while the headset is worn), and the upload page (UPLOAD.md, the huggingface-cli command; HF_DATASET is a placeholder). --dry-run runs sessions without processes, for testing. hands/rec/takes.py (standard library): indexes sets.bin and sets-N.bin without reading pixels, reads one set's cameras, keeps deleted ranges in take.json, and exports: deleted sets left out, zstd -10 -T2 at nice 19, manifest.json and SHA256SUMS, nothing left behind on cancel. CONSENT.md and UPLOAD.md are drafts pending a legal review; the window says contributions aren't open yet. The dev container gains zstd. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hands: upload from the hand recorder's window, export checks, a rehearsal hands/rec/validate.py (standard library; Linux and Windows, Python 3.12+) checks an export before upload and when it's received: SHA256SUMS, an allow-list of files, the manifest's schema and keys, the consent version, a uuid4 contributor, no identifying fields in calibration.json or device.json, every sets.bin.zst decompressed to its end as a stream with each FHSET01 header checked against the manifest, jsonl lines, the total size. It decompresses with compression.zstd, zstandard or the zstd program. validate.py DIR [--json]. hands/rec/hub.py uploads an export with huggingface_hub, as a pull request to contributions/<contributor>/<session>: validate first, refuse a repeat of the same export, check the login (whoami) and access (auth_check), upload_folder(create_pr=True) with the manifest summary as the description, then record the PR under "uploads" in session.json. Errors are explained (terms not accepted, not found, 401/403, network). --dry-run makes no network calls. FT_HANDREC_DATASET overrides HF_DATASET (DeeJanuz/frametop-hands); while the texts are drafts a real upload needs FT_HANDREC_ALLOW_UPLOAD=1. The Upload page shows the login with "Check again" and how to run hf auth login in a terminal (the token never enters the window), then Upload with a phase, progress and Cancel (hub.py as a child process), the PR link, and a warning for an export uploaded before. The manual command stays as the fallback. ft-handrec --hub-dry-run. session.py also saves device.json: cv.cad_from_cal and head from /persist/device_config.json, the labeller's shape, nothing identifying; export copies it. Session ids with a -N suffix are accepted everywhere. hands/rec/rehearse.sh runs it all without the headset: ft-ringplay plays 30 s of a capture into a ring, session.py records a short test script with ft-handpanel --no-vr and a tracker, then export, validate and a dry-run upload (--repo ID uploads for real). It runs in one frame-job scope, deletes its data and stops its processes, also on Ctrl+C. hands/rec/tests/test_validate.py covers good and broken exports and hub.py without the network. The dev container gains python3-huggingface-hub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * README: Frametop doesn't work on the SteamOS beta yet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit |
||
|
|
1ebf3692fb |
ft-floatd: a launch for a missing app no longer kills the control socket (#16)
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>
|
||
|
|
072a294941 |
README: Frametop doesn't work on the SteamOS beta yet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7816633353 |
README: link the Frametop Discord
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8b854fd424 |
Uninstall cleans up after itself; a shorter install command
- desktops.sh uninstall also removes Launch as Standalone's app copies and the title bar decoration, and Display Settings' uninstall the profiles' launcher entries; its install writes them back from the saved profiles (new: ft-layout launchers) - the README says which settings an uninstall leaves - the install command is now curl -fsSL https://deejanuz.github.io/ frametop/get.sh | bash (GitHub Pages, from main) - ft-gaze logs "action manifest ...: ok" instead of "error 0" Found in a clean install test on the Frame (2026-10-01): uninstall, then get.sh from the headset into a fresh clone; everything installed and doctor.sh passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.2.0 |
||
|
|
7bcda94267 |
Defer hand tracking; the README leads with what Frametop does now
- install.sh no longer offers hand tracking (it's heavy on the CPU and needs more work); hands/ still builds and installs by hand - the build container drops python3-opencv and python3-numpy, which only hand tracking's tools used: Fedora's OpenCV pulls in over a GB - README: a new opening and feature list, the Use section by topic (screens, mouse, floating windows, profiles, gaze), and new known limitations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c257000a23 |
A one-line installer, and gaze mode in install.sh
- get.sh: curl ... | bash asks for stable (main) or experimental, clones or updates ~/frametop, and runs install.sh; run it again to update or switch - install.sh offers gaze mode (step 8, yes by default) and notes what to do if an SSH connection drops - Input Settings' Gaze page says when the gaze service isn't installed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0280e7441d |
Installers: ask for sudo in the terminal, and cope with SteamVR off
- frame_sudo (scripts/_env.sh) replaces three copies of sudo_run. On the Frame it asks in the terminal even when stdin isn't one, which install.sh's steps never had: saying yes to the Bluetooth fixes or hand tracking stopped the install with "no terminal for sudo". From a PC it asks through ssh -t when .env has no password. - start_with_steamvr: the pointer, power, and gaze services restart when SteamVR runs (a re-install runs the new code) and are left to start with it when it doesn't, instead of failing the install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
12c42cd455 |
Bring the docs up to date with the code
- floating-windows.md and profiles.md describe what's built, with what isn't listed as such; the plans, phases, and branch notes are gone - hands-migration.md is gone: the move is done; its open items are in hands/README.md's Known issues - reference.md: Layout & profiles, every action, key combinations, floating windows, the gaze pointer, and hand tracking as they are - design.md gets the KWin findings from floating-windows.md - README, gaze/README, AGENTS, hazards, gaze-controllers, and the example config catch up with gaze, hands, and the stuck-key fix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0ac2b10cd7 |
Remove leftovers before a release
- gaze/tracker/lab/eyes_track.py: nothing used it - Input Settings: the pointer role backend, whose page was removed - ft-screens: no log line for every floating-window resize - hands: ft-hands --help gives the palm-down default (1: off), the uninstall removes ft-handsctl's link, .frame-job is ignored - Display Settings: "Save as profile…", not "Save current arrangement" - two stale comments (ft-pointer's grabprobe, ft-gaze) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3da17bc838 |
Open a profile's apps even when the screens can't be arranged
With no head pose (the headset off), ft-layout use stopped before ft-floatd opened the apps. The screens now stay where they are, the profile's hidden screens still hide, and the apps open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
20e8d6254a |
Merge gaze-calibration into experimental
The gaze checks and calibration in a headset panel (quick, five, full, headset fit; shared-buffer drawing, click-to-capture), keyboard and mouse gaze clicks (Meta+J/K, the mouse's buttons alike, mouse moves only while a button is held, double right/Meta+K tilts a drag), one 55 degree learning limit with a quick check past it, eye presence for "the headset went on", the gaze probe as a development tool, and the relay releasing keys the desktop has down that no keyboard holds. Conflicts with the profiles: the relay keeps known_action/needs_pointer and the gaze defaults (Meta+J/K and the float key); Input Settings keeps the profile actions everywhere and the gaze actions in key combinations only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2ae10a3b65 |
Input relay: release keys the desktop has down that no keyboard holds
After a gaze calibration, Meta stayed down in KWin: typing opened the overview, and clicks on the desktop did other things. A key can be left down there when its device vanishes with it held (release_held only let go of it on the relay's own devices; the Z3's keyboard dropped out twice right then) or a release goes astray. The relay now remembers which keys it told ft-screens went down, and once a second releases any no device holds (EVIOCGKEY), and the key combinations forget a Meta or modifier no device holds. A relay that starts releases the modifiers on the desktop, for keys an earlier one left down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
da3e3d66a7 |
Gaze panel: shared buffers, no flicker; calibration dots wait for a click
The panel drew each picture with SetOverlayRaw, a new texture upload every time: the full calibration's 1024x768 picture flickered on every change, and in a live test the headset kept showing the last calibration picture after the panel had drawn the fit check (2026-10-01). Now, as screens/keyboard.cpp does, it writes into three linear DMA-BUFs SteamVR imported once (the size of the biggest panel; texture bounds show the part in use) and switches between them. A show makes the overlay visible once its first picture is in. The full calibration's and the five-dot check's dots wait for a left click or Meta+J while you look at the dot, in place of capturing any steady gaze (which can be a look somewhere else); no capture ring, no 8 s skip, and the check closes after 2 minutes without a click. The quick check still captures itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
45b1b06cdb |
Calibration all in the headset panel; the gaze probe is a development tool
Check headset fit now runs in the headset panel too ("fitcheck"): a card per
eye (tracked or lost, the tracker's signal, how much of the last 10 s it was
seen) and the hints, from the probe's fitcheck.py, updated at most twice a
second; left click or Meta+J runs its guided check, right click or Meta+K closes
it. So Quick check, Calibrate, and Check headset fit all happen in one place.
The gaze probe moves to the Gaze page's overflow menu as "Gaze probe
(development)", and its app menu entry says it's a development tool. Texts that
sent users to it ("use Calibrate… with Own tracker") point at Calibrate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
964c250858 |
Gaze mode: a double right click or double Meta+K pans and tilts a drag
A drag begun by right during the left's held-back press (or Meta+K during Meta+J's) now lasts while either button or key is held, so pressing the right one again is free: it tilts, as a right press does during any mouse drag. Meta+K during a keyboard drag (held still into one, or the second Meta+K) tilts while held: the head turns the panel, and the mouse can too; let go and the head drags again from there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1f46616c75 |
Gaze mode: the mouse's buttons work like Meta+J and Meta+K
The right button's press is held back like the left's: hold it, the pointer stops where you look, move onto the target, and the right click comes on the release (held still, it's a real right press). Right during the left's held-back press now starts a drag where the pointer is, as Meta+J then Meta+K does, in place of a right click there; letting go of either drops it. So once you've moved, the left alone only clicks, and the right starts a drag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
949f629338 |
Input Settings: the mouse movement explanation is a tooltip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a2a5a4c98e |
Gaze mode: the mouse only corrects, and left-then-right right-clicks
POINTER_GAZE_MOUSE_MOVE=held, the default (the Gaze page's Mouse movement switch, with why it's on): while the gaze has the pointer, moving the mouse does nothing; it moves the pointer only while a button is held, as a correction. A bumped or drifting mouse can't pull the pointer off what you're looking at, and every mouse move is a correction, so lessons aren't polluted by mouse moves to somewhere else. With the gaze stale for a second, in a game, or with the headset off, the mouse moves the pointer as usual. "free" is the old behaviour. Pressing the right button while the left one's press is held back right-clicks where the pointer is instead (correct with the left, then right-click); both releases are then nothing. Meta+J then Meta+K stays a drag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8998fc1ddd |
Gaze learning limit: 55 degrees, past it a quick check; a solid panel
Drops the quick check's second step (
|
||
|
|
362968198b |
Quick check: put the pointer on the dot, and one limit for learning
After the quick check's capture, in gaze mode, the dot stays where it is in the
room (the panel's new "lock") and the gaze has the pointer again. You put the
pointer on the dot as you'd correct a click, with the mouse or Meta+J and the
head. That click clicks nothing: the helper sends it as "calverify", learned
whatever its size. Over POINTER_GAZE_NUDGE_MAX, the capture runs again, up to
3 times. A right click or Meta+K skips it.
POINTER_GAZE_NUDGE_MAX is now the one limit: 15 degrees by default (was 8),
and ft-gazed's own limits (8 for SteamVR's tracker, 25 for ours) follow it. A
keyboard correction past it isn't learned but opens a quick check ("recheck"),
in place of the 30 degree keyboard limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
cbad7bc470 |
Keyboard corrections are learned up to 30 degrees
A Meta+J hold's correction is always meant, but it went through the mouse's 8 degree nudge limit. Live, our tracker was 12 degrees off and every correction was dropped without a word. Debug output now says when one is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
580004bb00 |
Gaze checks: the headset going on is eyes coming back
Steam's eyetracking.txt writes "HMD on" every minute or so with nobody in the headset, and SteamVR said it was worn for 12 hours straight, so the quick check opened about 40 times an hour at an empty headset. It now opens when SteamVR's tracker sees eyes for 3 s after none for 3 s, and closes when they're gone 2 s. The log reader also drops repeated "HMD on" lines and sub-second offs, so lessons stop counting as from an older wear every minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5f885cbe65 |
Merge volume-swap into experimental
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bfc044c1b0 |
Try every volume key when one swap fails
remap_volume stopped at the first EVIOCSKEYCODE_V2 that failed, so the volume entries after it were never tried. On a keyboard where swapping volume up failed, volume down stayed a real key that gamescope reads, and one press with nothing focused aborts gamescope and the VR session. Restoring had the same hole: a failed entry left the later stand-ins in place after the relay exited. Now every entry is tried and the first failure is raised at the end. take_volume already marks the device remapped on that error, so the relay routes the stand-ins that did swap and restores them on exit. Checked against a fake keymap with a stubbed fcntl.ioctl: with volume up's swap failing, volume down now swaps (it stayed real before), the same for restore, and full swaps, restores and the no-keymap case come out as before. Found by 0x1f6 in PR #8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
46e5c59cc0 |
Merge PR #8 fixes 1 and 2 into experimental
From 0x1f6's robustness PR: survive a malformed control datagram (adapted to wrap experimental's textfield command too) and plan a layout when the rows setting exceeds what the screens fill. The PR's third commit, the volume-key swap rollback, is left out: rolling back a partial remap leaves every real volume key exposed to gamescope instead of some, and on restore it turns already-restored keys back into stand-ins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0f4243c7f2 |
Plan a layout when the rows setting exceeds what the screens fill
ft-layout crashed - and so did arranging the screens - whenever the row
setting in the layout preset is above what the screens fill. plan()
builds a grid with cols = ceil(count / rows) columns; when that leaves
fewer full rows than the setting asked for, the extra rows are empty,
and the per-row height max() over an empty row raised
ValueError: max() arg is an empty sequence
at plan, line 300 (flat) and 312 (curved)
The smallest real case is 4 screens with 3 rows: cols = ceil(4/3) = 2,
the first two rows hold all 4 screens, the third row is empty. Both the
flat wall and the curved preset crashed, so apply, plan and the
auto-arrange at desktop start all failed until the row setting was
lowered. The Rows spinner in Frametop Display Settings (main.qml) allows
any row count up to the screen count, and clamping to the screen count
does not prevent this - 3 rows for 4 screens is within that range and
never fits a full grid - so the crash was reachable from the UI as
shipped.
Reproduced by calling plan() directly with 4 screens and 3 rows: both
kinds raised. Also verified the whole placement matrix (counts 1-10,
rows 1-4) places every screen after the fix.
The fix trims rows to the number of rows the screens actually fill,
rows = ceil(count / cols), after cols is computed. The row count is
used again for stacking (gap times rows - 1, the sum of row heights),
so the stacking matches the trimmed grid: no empty row is ever built,
and a rows setting that can't be honoured degrades to the tightest fit
instead of failing.
(cherry picked from commit
|
||
|
|
9c2fccf0a0 |
Survive a malformed control datagram
One bad datagram on the control socket ended the whole relay. Its
dispatch in handle_control ran unguarded in the main loop, so any
exception propagated out of main() and the process exited. The simplest
trigger is "watch abc": float(words[1]) raises ValueError, and the relay
died with
ValueError: could not convert string to float: 'abc'
at handle_control, via the bare handle_control(now) call in the
select loop
The control socket is an abstract socket bound to @frametop_relay.
Abstract sockets carry no permissions, so every local process can send
to it; nothing authenticates the sender. Dying from a datagram was bad
in three ways:
- Every grab is lost. Physical devices go back to gamescope and SteamVR,
which read them themselves, so the mouse types into Steam and the
desktop at once, and Meta+Shift shortcuts fire on both sides.
- The volume-key takeover is lost. gamescope aborts on a volume key when
no window has keyboard focus (wlr_seat_keyboard_notify_enter asserts on
a null focus surface), which ends the whole VR session - the exact
failure the relay exists to prevent.
- systemd restarts the service, but Type=notify with READY only after
the virtual devices exist makes the restart a visible hiccup, and a
crash loop from repeated bad datagrams would flap SteamVR's input.
Reproduced by running the relay with stubbed uinput devices on a
non-Linux host and sending "watch abc" to the control socket: it exited
on the first datagram after answering "devices" correctly. After the
fix, the same exchange gets a log line ("bad control datagram ...") and
the relay keeps answering.
The fix wraps each datagram's dispatch in try/except inside
handle_control's loop, so one malformed message is logged and skipped
while the rest of the queue is still processed. Parsing of the common
helper commands (vrbtn, vrhello, gazeawake, keyboard) keeps its own
guards; the catch-all is only a backstop for anything the guards miss,
including float() on a non-numeric argument to "watch" and "vrcapture".
Adapted for experimental (from PR #8): the guard also wraps the textfield
command, which experimental added to this dispatch after the PR's base.
(cherry picked from commit
|
||
|
|
d54e907d37 | Merge main into experimental (#6 is already here as 3aea571..4e5131d; history only) | ||
|
|
4c46d69b0c |
Check what Frametop needs from SteamOS after an update
On the Frame, a SteamOS update replaces SteamVR, KWin, and gamescope with the rest of the OS image. scripts/update-check.py, run by doctor.sh and report.sh, checks what Frametop uses from it: the OpenVR interface versions the installed programs were built against, the vrcmd --overlays format, the eye tracker's shared memory layout, host files, services, sockets, and the driver registration. doctor.sh --mark-good records the package versions once things work, and later runs say what changed and what to try by hand. The session no longer stops when mesavars.sh or flatpak.sh is missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3c5bfd8d2f | Merge float-tracker into experimental | ||
|
|
c3785bb46e |
Floating windows: keep KWin's placement memory out
KWin's PlacementTracker keeps each window's geometry, full screen and maximized state per layout of the outputs, and puts windows back when a layout it has seen comes back. A spare output resizes after its window, so resizing a floating window back to an earlier size (or changing its scale, or full screen) made the window and its output flip forever, and floating or docking one window could move others onto or off a spare. The KWin script now keeps where each window belongs, reports nothing while KWin changes the outputs, and on screensChanged puts floating windows back (and the screens' windows when only spares changed), cancelling KWin's requests before the app sees them. A size asked for is held for a second against late answers. With that: a launched app and a profile get their remembered scale back, scale steps keep the size in pixels, ft-floatd waits for the end of an edge resize before resizing the output (KWin cancels the resize on any output change), a window taken over after a restart keeps its app and panel density, and `ft-float float ID` asks the script for the window's current place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
81b64302aa | Merge profiles fixes into experimental | ||
|
|
a699540d95 |
Profiles: fixes from trying them on the live desktop
- KWin 6.2's scripts have no maximize mode: a window counts as maximized when it fills its output's maximize area. - A late event for a window that just closed brought it back into ft-floatd's table, so a profile "found" it open, floated a window that no longer existed, and held a slot. The script doesn't report deleted windows, and ft-floatd ignores events for ids it has seen close. - Docking a window that came from a screen that's hidden now puts it on the first screen that shows instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
079390cde4 | Merge screen-hide and profiles into experimental | ||
|
|
858dbe1562 |
Profiles: named layouts that open apps
A profile is a named layout plus the screens it hides and its apps' windows (docs/profiles.md). ft-layout save captures them (ft-floatd's "windows", after the KWin script reports every window as it is now); use opens them: the screens move, open windows of each app go to their places (on a screen, maximized or not, or floating), and missing apps start, once and then again for each window still missing 3 s after the first. Nothing closes. The desktop starts in FT_PROFILE or default_profile (ft-layout start, from the session script). Each profile gets a launcher entry (Frametop: NAME, in SteamVR's Launch a program list) that switches to it or starts the desktop in it. The relay's profile:NAME action and Input Settings' "Open profile" entries put one on a key, mouse button, or controller button. Display Settings' Layout page becomes Layout & profiles: Save as profile, Open profile, the profile's apps, and Start in profile. Plasma's own session restore is off in the Frametop session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9d91ecbcaa |
Hide screens one at a time
ft-screens: "conceal <screen|all>" hides a screen on its own, whatever the
visibility mode or the hotkey says, until "reveal"; "concealed" lists them.
(Not "hide N": older builds read anything starting with "hide" as the hotkey.)
ft-layout keeps it per screen ("hidden" in the layout), applies it when it
arranges the screens, and has hide/show N|all and hidden. Display Settings
gets a Shown switch per screen on the Visibility tab. ft-floatd floats a new
window that opens on a hidden screen, and puts a stray window on a screen
that shows. Profiles (next) use it to show only some screens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
0348821384 | Merge float-launch into experimental | ||
|
|
38843a4717 |
Launch apps floating, remember where they floated, and Launch as Standalone
ft-float launch APP / run COMMAND: ft-floatd starts the app and floats its first window (matched by process or desktop file name for 30 s) where that app last floated, or in front of you at the primary screen's density. Each app's place (pose relative to the primary screen, size in pixels, scale) is kept in ~/.config/frametop-float.json whenever one of its windows stops floating; the scale isn't applied yet, since rescaling can loop (written up in docs/floating-windows.md, Known problems). Launch as Standalone (float/ft_apps.py): the session writes copies of the apps' desktop files with that action and puts them first in XDG_DATA_DIRS, so it shows in the Application Launcher's and the taskbar's right-click menus in the Frametop desktop only. ft-floatd rewrites them when apps change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c4c10e6d92 |
Gaze checks and calibration in a panel fixed to the headset
ft-gazepanel (gaze/panel) is a SteamVR overlay that stays put in your view and shows dots at known head-relative directions; the gaze service runs it and drives it (gaze/gazecheck.py): - a one-dot quick check 3 s after the headset goes on, when our tracker asks for a click (reseat), at most every 2 minutes, and from Quick check on the Gaze page; five dots follow if the next 3 lessons are still over 2 degrees off - the full calibration (the probe's three rounds, dark to bright) when gaze mode comes on without one, or from Calibrate; quitting it with still no calibration turns gaze mode off Each dot takes the gaze once it has held still for 0.6 s; a left click or Meta+J takes it at once, a right click or Meta+K closes the panel (the pointer helper hides its dot and passes those presses on while "calpanel" lasts). Our tracker gets clicks and its own calibration; SteamVR's gets lessons per eye, or a new calibration.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1aa88641ec | Merge float-titlebar into experimental | ||
|
|
00dfd61396 |
Title bar button: fixes from trying it in the live desktop
- The decoration's "bottom" property clashed with Item's (KWin refused it). - No On All Desktops button with one virtual desktop, like Breeze. - decoration/apply.sh finds the session's D-Bus through plasmashell (KWin's environment isn't readable), and installs each try under a new name: KWin keeps a decoration's QML by name until it restarts. - ft-floatd starts its script with Scripting.start: after a reload, the new script gets the old one's id while the old one is still being deleted, so run() on /Scripting/Script<id> went to the old script and the new one never ran (a second ft-floatd start left floating windows without their script). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6ea70f32bc |
Merge branch 'experimental' into gaze-calibration
# Conflicts: # input-settings/ft_input_settings.py # input-settings/main.qml # input/input-relay.py |
||
|
|
dc3a1b8208 |
Keyboard clicks at the gaze: Meta+J and Meta+K
gaze_left and gaze_right (Meta+J and Meta+K by default) click where you look.
A tap clicks where the dot was at the press and tells the gaze tracker it was
right there. Held, the pointer stays put in your view, so turning your head
carries it onto the target; the release clicks there and the correction is a
lesson (judged by the net correction, not the head's path). Held still for
POINTER_GAZE_HOLD it's a real press that the head drags, and Meta+K while
Meta+J aims presses where the dot is now, to correct and then drag.
A Meta combination now also sends the desktop an F24 press and Meta's release
at once: Meta's release no longer opens Plasma's launcher, and the click isn't
Meta+click (KWin's window move and resize, which swallowed right clicks). The
desktop gets Meta back for the next key if it's still held.
Also fixes
|
||
|
|
f32187824a |
A float button in every window's title bar
Frametop's own window decoration (decoration/), a QML decoration for KWin's Aurorae engine drawn like Breeze, puts a float button left of Close. It's the Keep Below button with its own glyph: the KWin script floats a window when keep-below is set and docks it when it's cleared, and keeps the flag set on every floating window, so the button shows "back to the desktop" there. The session script installs the decoration for the Frametop desktop only; decoration/apply.sh switches a running desktop to it or back to Breeze. Not yet tried in a running KWin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
86c89f90af | Merge float-access into experimental | ||
|
|
1e83f96b0f |
Float key in the input relay, docking everything, and the plan for profiles
The input relay owns the float key now: float_toggle (Meta+Shift+F unless the rules have their own key combinations) floats the window under the pointer, or the active one over the wallpaper, and docks it if it floats. dock_all puts every floating window back. Both are mappable to mouse and controller buttons, and work without pointer mode. The KWin script no longer registers a shortcut, and ft-floatd drops the old one. Key combinations move to Input Settings' Keyboard page. docs/floating-windows.md records the decisions from 2026-09-30 (the title bar button, Launch as Standalone, profiles), and docs/profiles.md plans profiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |