Commit Graph
100 Commits
Author SHA1 Message Date
DeeJanuzandClaude Opus 5.5 7816633353 README: link the Frametop Discord
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:18:09 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 15:42:37 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 15:20:39 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 15:04:49 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 15:04:49 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 14:47:53 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 14:47:53 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 14:47:53 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 11:55:04 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 11:49:51 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 11:27:30 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 11:14:33 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 11:03:54 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 11:00:23 -06:00
DeeJanuzandClaude Opus 5.5 949f629338 Input Settings: the mouse movement explanation is a tooltip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:57:22 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 10:56:33 -06:00
DeeJanuzandClaude Opus 5.5 8998fc1ddd Gaze learning limit: 55 degrees, past it a quick check; a solid panel
Drops the quick check's second step (3629681): the user didn't want it, and it
re-entered itself every tick, re-placing and redrawing the panel, which
flickered in the headset.

POINTER_GAZE_NUDGE_MAX is the one learning limit, now 55 degrees by default:
half of the 109 the Frame shows across. The helper measures the correction
itself (raw gaze to click), not the mouse's path; past the limit it isn't
learned and the helper sends "recheck" for the quick check, for the mouse and
keyboard alike. ft-gazed's own limits (8 and 25) follow the setting.

Test, our tracker's 409 clicks since its Sep 29 calibration: a one-dot check
set from any one of them puts the next 2 minutes' clicks within 15 degrees (99%
within 4.2) and the next 10 minutes' within 25, so it gets well under 55.

The panel's dot is still and its capture ring fills in quarters, so it's drawn
again only a few times per dot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:48:57 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 10:41:52 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 10:30:49 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 10:21:21 -06:00
DeeJanuzandClaude Opus 5.5 5f885cbe65 Merge volume-swap into experimental
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:38:32 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 09:38:32 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 09:34:31 -06:00
DeeJanuz d54e907d37 Merge main into experimental (#6 is already here as 3aea571..4e5131d; history only) 2026-10-01 09:32:35 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 08:51:09 -06:00
DeeJanuz 3c5bfd8d2f Merge float-tracker into experimental 2026-10-01 08:50:38 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-10-01 08:50:38 -06:00
DeeJanuz 81b64302aa Merge profiles fixes into experimental 2026-09-30 22:37:50 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-09-30 22:37:50 -06:00
DeeJanuz 079390cde4 Merge screen-hide and profiles into experimental 2026-09-30 22:32:18 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-09-30 22:32:10 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-09-30 22:25:35 -06:00
DeeJanuz 0348821384 Merge float-launch into experimental 2026-09-30 22:20:36 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-09-30 22:20:36 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-09-30 22:12:26 -06:00
DeeJanuz 1aa88641ec Merge float-titlebar into experimental 2026-09-30 22:11:29 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-09-30 22:11:29 -06:00
DeeJanuz 6ea70f32bc Merge branch 'experimental' into gaze-calibration
# Conflicts:
#	input-settings/ft_input_settings.py
#	input-settings/main.qml
#	input/input-relay.py
2026-09-30 22:07:04 -06:00
DeeJanuzandClaude Opus 5.5 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 e16b758, which had taken the gaze actions off the key combination
list along with the controllers'; and gaze_quickcal (the gaze service's
one-dot check) joins the actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:04:14 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-09-30 22:03:41 -06:00
DeeJanuz 86c89f90af Merge float-access into experimental 2026-09-30 21:55:47 -06:00
DeeJanuzandClaude Opus 5.5 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>
2026-09-30 21:55:47 -06:00
DeeJanuzandClaude Opus 5.5 e16b7588e2 Gaze mode is a mouse feature: drop its controller parts
Steam reads the Frame controllers itself, outside SteamVR's bindings, and every
press and release it sees takes SteamVR out of laser mode, so controller clicks
at the gaze can't be done cleanly (docs/gaze-controllers.md, from the gaze-first
branch's tests).

- Gaze precision and Gaze drag take a mouse button or a key combination only;
  the controller source, its aim steering, and POINTER_PRECISION_GAIN,
  POINTER_PRECISION_DEADZONE and POINTER_GAZE_DRAG_GAIN are gone.
- The gaze actions (including Gaze pointer on/off) can't be mapped to controller
  buttons: the relay ignores them there and doesn't ask the helper for those
  buttons, and Input Settings no longer offers them.
- Last used wins in gaze mode too: picking up a controller hands it the laser,
  as docs/design.md already said.
- The gaze dot shows all the time (POINTER_GAZE_DOT=always, the default;
  moving brings back the old behaviour), with a switch on the Gaze page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:11:24 -06:00
DeeJanuzandClaude Opus 5.5 d5c2ec65d7 Take the user, host, and home from the machine, not this Frame
The local RDP login between krdp and the VNC bridge uses the account's
own name instead of "steamos", its certificate the hostname instead of
"steam-frame", and the ft_pointer driver installs under the user's home
instead of /home/steamos. The remote-access address already came from
the tailnet (tailscale0 and tailscaled's local API).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:57:33 -06:00
DeeJanuzandClaude Opus 5.5 1f19732aaf Add Frametop Remote Access, a settings app for the VNC view
A GTK 4 / libadwaita app (remote/ft-remote-settings, on the host's own
Python like the gaze probe): remote access on and off (REMOTE, applied at
once when the desktop allows it), the tailnet name and address to connect
to, and the VNC password shown, copied, or replaced. The password stays
random and made on the Frame, in ~/.config/frametop-remote; none is in
the code.

session/remote-ctl.sh starts, stops, and reports remote access; the
session uses it and leaves a remote-capable marker, since KWin allows the
capture only in a desktop that started with REMOTE=1. The password
between krdp and the VNC bridge (local only, but on krdp's command line)
is now new at every start. The installer adds the app to the menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:55:49 -06:00
DeeJanuz 9820e7996f Serve only the desktop's primary screen over VNC
krdp streams every screen, so the VNC screen is now the primary's size and the
FreeRDP window is shifted so the primary fills it (ft-layout remote-view gives
the offset). It resizes and reconnects when the layout changes. With remote
access on, KWin's D-Bus screenshot interface is open too, for scripts that look
at the screens without the headset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6f5a23c80f2ecd26e6a96c86b102be36d518ee25)
2026-09-30 15:50:46 -06:00
DeeJanuzandClaude Opus 5.5 97f853130d Merge eye-tracking into experimental
Our own eye tracker joins the gaze folder: ft-eyegrab (the root frame
grabber, frametop-eyegrab.service) and ft-eyes, run by the gaze service
when GAZE_TRACKER=own, with its lab tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:45:38 -06:00
DeeJanuzandClaude Opus 5.5 55d92b411b Input Settings: drop the pointer role choice
The hand role the pointer takes isn't something to choose by hand. The
helper still reads POINTER_ROLE (right by default) for experiments.

Tried and reverted (not committed): in gaze mode, taking the stylus role
by reconnecting. SteamVR gave the device no role at all (hint 5, role
none) and kept the dashboard laser on the held controllers, so the mouse
couldn't click either. The dashboard laser needs a hand role, and a held
Frame controller takes its hand's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:35:08 -06:00
DeeJanuzandClaude Opus 5.5 d666fa031f Gaze first: precision and drag buttons, key combinations, pointer role
Two new actions for mouse buttons, controller buttons, and key
combinations: gaze_precision (hold: the pointer stops where you look and
the button's device steers it, a controller by where it points at
POINTER_PRECISION_GAIN, the mouse by its moves; release: click there) and
gaze_drag (the same with a real press at once, dragging until the
release). The relay sends "precision|gazedrag <source> 1|0" to the
helper, which runs them through the same holds as pinches and grips.

- Key combinations on any keyboard ("key_bindings" in the rules, e.g.
  Ctrl+Alt+G for gaze on/off): the last key isn't typed.
- POINTER_GAZE_MOUSE: in gaze mode the left button is a precision button
  (the default, as before) or clicks right away (direct).
- In gaze mode a moving controller no longer takes the pointer away.
- POINTER_ROLE (right, left, stylus): the driver takes a "role" command,
  so the pointer can stay off the hand holding the precision controller,
  which would otherwise take the role back. Needs the rebuilt driver.
- Input Settings: the actions on the Controllers and Buttons pages; on the
  Gaze page the mouse choice, the role, the precision sliders, and key
  combinations (captured from any keyboard).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:14:57 -06:00
DeeJanuzandClaude Opus 5.5 c2739cb0df Hands: park hand tracking behind ft-handsctl
Hand tracking no longer starts with SteamVR: hands/run.sh install leaves
the units disabled and links hands/ft-handsctl into ~/.local/bin, which
turns it on and off (on | off | status | log | cutouts on|off | gestures).
With it on, hands show through the screens; pinches and grips move the
pointer only with POINTER_HANDS=1, now off by default. ft-camd's service
runs the mono cameras only: while the headset is worn, the colour module
writes just a half-size image into the top-left quarter of its buffers,
which ft-camd can't use yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:08:54 -06:00
DeeJanuzandClaude Opus 5.5 5ca4160a7e Hands: fixes from the second headset test (pinches)
- The palm-down filter is off by default: the user's deliberate pinches,
  hand raised, read 0.90-0.99, like typing.
- Typing is told apart by the keyboard instead: the input relay sends the
  pointer helper "typing" on key presses, and it takes no pinch within
  POINTER_PINCH_TYPING (1 s) of one.
- Grips are still held to hands raised (POINTER_GRIP_BELOW, 0.35 m below
  the eyes); pinches aren't, since the user's own sat 0.35-0.45 m below,
  elbow resting.
- A grip doesn't begin with the thumb on the index tip: that's a pinch
  with the other fingers curled, which was taken for a grip.
- A pinch's point is the index and middle knuckles: the tips' midpoint
  moved 1-2 cm as the pinch opened, dragging every release off its press.
- ft-hands --gesture-log prints what the detectors measure, 10 times a
  second.

Pinches still aren't reliable enough to use; hand tracking is parked for
now in favour of the controllers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:07:58 -06:00
DeeJanuzandClaude Opus 5.5 fbd188f5f9 ft-pointer: without gaze mode, a pinch is a real press
Pressed when the pinch closes, released when it opens, its hand dragging
the pointer in between (at the pinch gain, past the dead zone), like the
mouse's button. That's what the gaze probe's Click practice needs: it
freezes its own gaze dot at the press and drags it by the pointer's
movement until the release. With gaze mode on, the pinch still holds the
press back and clicks on the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:42:59 -06:00
DeeJanuzandClaude Opus 5.5 6d1b04e032 ft-pointer: pinch to click, grip to drag
The pointer helper reads ft-hands' gestures. A pinch holds the pointer
where the gaze put it and clicks on the release; held, the hand nudges
the pointer at half its angle past a 1.5 degree dead zone, which also
teaches the gaze tracker. A grip presses where the pointer is, drags with
the hand, and releases when the hand opens, unless it began more than
30 cm below the eyes (hands on a desk). Hand movement is taken in the
room with the head pose at capture time. Hand use keeps the pointer from
the relay's idle release, as gaze mode does. POINTER_HANDS and friends in
frametop.conf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:31:17 -06:00
DeeJanuzandClaude Opus 5.5 50c14545ed Hands: pick the cameras by the light, and detect grips
ft-hands tracks with the mono IR cameras in dim light and with every
camera (or the colour pair, HANDS_BRIGHT) in bright light, going by the
colour frames' mean brightness with hysteresis and a 2 s hold
(HANDS_CAMERAS=auto, the default; mono, color and all fix it). Colour
frames are placed on the mono cameras' clock by their dequeue time, and a
view in a camera a step lacks waits for that camera's next frame.

A grip (a closed hand) is a second gesture next to the pinch, in version 2
of the gestures file: every fingertip curled toward the wrist, beginning
only on a hand seen open within a second and held up in front. On the
2026-09-30 lit recording that leaves 6 false grips of 14, all with the
hands on the desk; pinch counts are unchanged. ft-handreplay logs grips
and finger curl, watch_gestures.py shows them, and tools/cut_sets.py
copies a few sets out of a recording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:31:17 -06:00
DeeJanuzandClaude Opus 5.5 40a2f39b2a ft-camd: keep colour trouble away from the mono cameras, and idle colour
A colour camera probes at most 4 buffers a frame (each probe syncs a
~9 MB buffer's cache, which made the mono cameras miss frames), and one
that goes stale twice in a row is paused (10 s, doubling to 160 s) and
learned again instead of ft-camd exiting. The colour cameras run at 2 fps
until a reader asks for more in frametop-hands/color-fps, which saves
most of their decoding while only their brightness is needed. Each mono
camera's latest near-black frame's mean goes in the ring (dark_mean), a
measure of the room's IR light. The service now starts with --with-color;
HANDS_CAMERAS=mono leaves the colour cameras out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:31:16 -06:00
DeeJanuzandClaude Opus 5.5 8bc6739dca Merge hands-migration into experimental
Hand tracking (ft-camd, ft-hands, and their tools) joins the desktop. The
hands file and ring move to /run/user/UID/frametop-hands/, which ft-screens'
hand cutouts now read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:02:01 -06:00
DeeJanuzandClaude Opus 5.5 e2abaa06b6 Hands: fixes from the first headset test
- The runtime files move to /run/user/UID/frametop-hands/: the desktop
  session deletes /run/user/UID/frametop at every start.
- The cutout copy shader runs at highp: mediump (16-bit on Adreno)
  stepped 1.7 texels across a 3440-pixel screen.
- One hand no longer pinches both sides after its left/right call flips
  mid-pinch, and --pinch-palm-down (0.6) holds back pinches with the palm
  facing down (typing on a lap keyboard).
- ft-camd judges a colour frame fresh by its luma rows only, and logs
  per-buffer changes at stale colour frames with FT_CAMD_DEBUG=1.
- hands/run.sh caps skips the setcap when ft-camd already has them.
- The replay tool dumps poses (--poses), and its pinch events carry the
  hand id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:00:54 -06:00
DeeJanuzandClaude Opus 5.5 4493b789fd Merge floating-windows into experimental
Floating windows join the keyboard and the hand cutouts. Floating panels
don't get cutouts yet: their panel and popups show crops of the client
buffer (texture bounds), which the side-by-side cutout buffer doesn't
match. KWin gets both the spare outputs and our input method, and the
pointer helper's frametop. prefix already covers the float panels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:53:28 -06:00
DeeJanuzandClaude Opus 5.5 5714978e65 Add a keyboard for text fields on the desktop
Fixes #7: SteamVR's keyboard never came up for the desktop's apps, and
opening it for our panels doesn't work well on the Frame (it's Steam's own
panel, mounted in the dashboard's scene, it follows the laser between
panels, and it takes the controllers over to SteamVR's laser).

- KWin starts input/ft-textinput as the desktop's input method. It tells
  the input relay when a text field gains or loses focus, and the relay
  asks ft-screens to open or close the keyboard. The session drops the
  QT_IM_MODULE=xim and GTK_IM_MODULE=xim that the gamescope session sets,
  or Qt and GTK apps never report text fields.
- The keyboard is ft-screens' own panel (screens/keyboard.cpp): a US laptop
  layout, typed with a controller's laser or the 3D mouse. It opens 0.7 m
  in front of you, below your eyes and facing you. It has a grab bar to
  move it, a Close key, latching Shift, Ctrl and Alt, and repeat on a held
  key. It's drawn into shared DMA-BUFs, so it doesn't flicker. Its keys
  reach the focused screen as key presses, so every app takes them.
- It steps aside while the Steam menu or Steam's own keyboard is up and
  comes back after. A layout reset closes it, and it doesn't open without
  a head pose.
- Frametop Input Settings has a Keyboard page: open it for every text
  field, only while no keyboard is connected (the default), only from a
  mapped button (the new Open/close keyboard action, for mice and
  controllers), or never. A switch keeps it open until you close it.
- The pointer helper treats every frametop.* overlay as a real panel. The
  keyboard's shared texture reports 0x0 like SteamVR's scene-graph
  controls, and the helper had given it their wide catch radius.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:41:47 -06:00
DeeJanuzandClaude Opus 5.5 c8fc6351bb Floating windows: fix the scale crash, the click offset, and placement
- KWin's nested backend makes an output the size it's configured to times its
  scale, so after Meta+scroll every size ft-floatd sent was multiplied again,
  and an odd result disconnected KWin (buffer not divisible by its scale).
  ft-floatd now asks for sizes in the output's scaled terms (kwin_size), and
  asks again after each scale change.
- SteamVR reports mouse positions on a panel with texture bounds in the whole
  texture, not the crop, so clicks on a floating window landed up to ~200 px
  off. The mouse scale is now the buffer's size, as on a screen.
- A floated window starts 30 cm in front of its screen (was 5 cm), so it's
  easy to point at apart from the screen behind it.
- No 1 s wait before a spare turns on (a disabled output never commits), and
  the login splash on the spares isn't taken for floating windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:57:36 -06:00
DeeJanuzandClaude Opus 5.5 ed75614f62 Bring our own eye tracker into the gaze folder, run by the gaze service
frame-eyes, a separate project until now, becomes gaze/tracker:
- ft-eyegrab (was fe-bufprobe) copies the eye-camera frames, read-only, out of
  SteamVR's eyetracking process. It runs as the system service
  frametop-eyegrab.service, which gaze/tracker/install.sh installs to
  /etc/frametop with sudo. It keeps only CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, and
  CAP_CHOWN, and copies frames only while /dev/shm/frametop-eyes-want is fresh,
  holding none of the tracker's buffers otherwise.
- ft-eyes (was fe-trackd) runs under ft-gazed in the dev container, with
  build/venv's pinned numpy and OpenCV: while Eye tracker is Own tracker, or on
  the probe's lease ("eyes SECONDS"). No sudo password or fe-live script at
  run time any more.
- lab/ holds the research tools (ft-eyes-score, -e2e, -record, -replay,
  -session) and findings.md. Recordings live outside the repo, in
  ~/.local/share/frametop/eyes/captures; .gitignore catches stray frame dumps.

Its socket is now @ft_eyes, its output /dev/shm/frametop-eyes-gaze, and its state
~/.local/state/frametop/gaze/eyes. On practice1 -> practice2 the whole live path
(ft-eyes-e2e) gives 1.30 deg median and 3.18 for the worst tenth, as before the
move (1.30, 3.21).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:00:07 -06:00
DeeJanuzandClaude Opus 5.5 369736f0ca Read the hands file through the shared header, at its Frametop path
ft-screens' hand cutouts read /run/user/UID/frametop/hands through
hands/include/fh_hands.h instead of their own copy of its offsets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:15:07 -06:00
DeeJanuzandClaude Opus 5.5 499035216c Make hand tracking a Frametop component
- Programs: ft-camd (the camera broker), ft-hands (the tracker), and
  ft-handreplay and ft-ringplay for recordings, built by hands/build.sh
  into hands/build/ with one Makefile. The first build fetches ncnn at
  frame-hands' pinned tag and builds it with the same options.
- ft-camd gets its privileges from file capabilities (CAP_SYS_PTRACE,
  CAP_PERFMON, CAP_DAC_READ_SEARCH) that hands/run.sh install sets with
  sudo, and drops them once set up. It still works under sudo. It runs
  on the host, linked statically, as frametop-camd.service. ft-hands
  runs in the dev container as frametop-hands.service. Both start and
  stop with SteamVR.
- Files move to /run/user/UID/frametop/ (cam-ring, hands, gestures),
  not $XDG_RUNTIME_DIR, which a terminal in the Frametop desktop has
  its own of. SIGUSR1 recordings go to ~/.local/share/frametop/hands.
- The calibration is read through /run/host in the container.
- Settings: HANDS_SWAP_SIDES and HANDS_CPUS in frametop.conf.
- install.sh offers hand tracking as an optional last step.
- The container gets jsoncpp-devel, glibc-static, and NumPy and OpenCV
  for the Python tools.
- tools/ring.py reads the ring, and models/NOTICE credits the
  Apache-2.0 models.

Checked: ft-handreplay gives identical summaries and byte-identical
depth dumps to frame-hands' fh-replay on both 2026-09-29 recordings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:15:07 -06:00
DeeJanuzandClaude Opus 5.5 5565a25444 Let the gaze pointer use our own eye tracker, and weight the eyes
Frametop Input Settings' Gaze page gets two settings, saved in frametop.conf and
read again by ft-gazed when the file changes: Eye tracker (GAZE_TRACKER: SteamVR's
or our own, frame-eyes' fe-trackd) and Eye bias (GAZE_EYE: auto, left, right).

ft-gazed now combines the eyes, each calibrated on its own: SteamVR's set 2 eyes
with the probe's Left eye and Right eye calibrations, or our tracker's eyes as
they come. Without per-eye calibrations, or with --source, it keeps the older
one-source path. A pointer nudge finds its look from the raw gaze the helper
echoes back; with our tracker it goes to fe-trackd as a click.

The bias leans instead of choosing (gazecal.EyeWeights): on 306 live clicks the
eyes' errors partly cancelled, both together 0.65 deg off against 0.96 and 1.11
for either alone. Left or Right counts that eye twice; auto weights each eye by
its RMS miss at its last 20 nudges, since the calibration's fit picked the wrong
eye on SteamVR's test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:11:54 -06:00
DeeJanuz 3440ec8b90 Merge branch 'main' into experimental 2026-09-30 09:11:24 -06:00
DeeJanuzandClaude Opus 5.5 3e3d31c728 Lay out hands/ like Frametop's other components
trackd/ becomes track/, and the calibration helper the Python tools
import moves from the prototype's folder into tools/. The model
development tools (nettest and the scripts that compare it with the
Python models or cut int8 calibration crops) stay in frame-hands with
the prototype they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:01:10 -06:00
DeeJanuzandClaude Opus 5.5 1a76d1560b Bring in frame-hands' hand tracking under hands/
The history of frame-hands (~/Desktop/Projects/frame-hands on the
Frame), filtered to what moves: the camera broker (camd/), the tracker
and its offline tools (trackd/), the shared file layouts (include/), the
ncnn models, the analysis tools, and the calibration and model helpers
they import from the Python prototype. The reverse-engineering notes,
probes, camprobe, and the rest of the prototype stay in frame-hands.
Unchanged here: the renames to ft- names and Frametop paths follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:59:32 -06:00
DeeJanuzandClaude Opus 5.5 9f6ce5cc59 Plan the move of hand tracking into Frametop
frame-hands (the headset cameras' hand tracking, a separate project so
far) becomes a native component under hands/. docs/hands-migration.md
has what it is, where each part goes, names, build, how ft-camd gets
its privileges (file capabilities), interfaces, open items, and steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:59:27 -06:00
DeeJanuzandClaude Opus 5.5 2031b9ca8f Add colour cameras, pinch gestures, and depth measures
- fh-camd --with-color publishes the Arcturus colour pair (luma, half
  size, 30 fps) to the ring, flagged FH_CAM_COLOR. fh-tracker records
  them, and fh-replay --cams mono|color|all tracks with them, using the
  module's EEPROM calibration (load_color_calibration).
  tools/check_color.py checks which node is left and how the crop maps.
- Pinch detection per hand (trackd/pinch.h), published to
  $XDG_RUNTIME_DIR/frame-hands/gestures (include/fh_gestures.h). It has
  begin and end counters, times, the pinch point, and the begin point
  for drags. tools/watch_gestures.py shows it live, and fh-replay
  reports it.
- fh-replay --depth and tools/depth_report.py measure the depth without
  ground truth: noise along the line of sight against across it, the
  one-camera guess, and a simulated camera loss.
- fh-tracker --swap-sides, --ring, --keep-presence, --record-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:58:30 -06:00
DeeJanuzandClaude Opus 5.5 fc15a8a8a6 Record what's built of floating windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:32:03 -06:00
DeeJanuzandClaude Opus 5.5 691b66cd87 Stop ft-screens without an abort
wlroots asserts that nothing still listens to its xdg-shell and decoration
globals when the display goes, so every desktop stop ended with ft-screens
aborting and leaving a core dump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:31:54 -06:00
DeeJanuzandClaude Opus 5.5 6122b7eb15 Floating windows: full screen, per-window scale, and drags across panels
Full screen fills the window's own panel (the margin drops to zero).
Meta+scroll over a floating window changes its scale at the same size in
pixels. Spare outputs are sized to a multiple of KWin's buffer scale, and
screens to even sizes: an odd buffer at a fractional scale is a protocol
error that disconnected KWin. The 3D mouse's drag lock now crosses onto
other Frametop panels unless the pressed one is being carried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:31:23 -06:00
DeeJanuzandClaude Opus 5.5 c2e5e861c8 Show floating windows as panels of their own
ft-screens makes a panel for each spare output (frametop.float.N), hidden
until ft-floatd floats a window on it. The panel shows only the window's
rectangle of the buffer at the density of the screen it came from, its
popups and dialogs get small panels over it, pressing its title bar
carries it while KWin's pointer stays put, the corner tab resizes the
window in pixels, and two more buttons close it and put it back on the
desktop. The session adds FLOAT_SLOTS spare outputs to KWin and starts
ft-floatd; ft-layout arranges only the screens' outputs, and the pointer
helper treats the new panels like screens. Not yet tried in the headset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:26:20 -06:00
DeeJanuzandClaude Opus 5.5 87a402c68d Add ft-floatd and the frametop-float KWin script
ft-floatd loads the script into the desktop's KWin, which reports windows
over D-Bus and takes commands through a long poll. Floating a window (the
window menu's Float in VR, Meta+Shift+F, or ft-float) turns on a spare
output sized to the window plus a margin, moves the window onto it, and
tells ft-screens the panel's crop, density, and place; docking puts it
back and turns the spare off. Tested on the headless test desktop; the
panel side in ft-screens comes next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:20:26 -06:00
DeeJanuzandClaude Opus 5.5 fcc8d96246 Record the headless phase 0 results
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:14:22 -06:00
DeeJanuzandClaude Opus 5.5 4859412126 Put the first click after crossing onto a screen where the pointer is
KWin's nested backend ignores the position in wl_pointer.enter, and
wlroots drops a motion to the position it entered at, so KWin kept its old
pointer until the next move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:14:09 -06:00
DeeJanuzandClaude Opus 5.5 42a755b9f4 Add a headless test mode to ft-screens and a throwaway test desktop
ft-screens --no-vr runs without SteamVR and leaves the input relay alone;
--control names its control socket; "toplevels" lists KWin's windows with
their titles and sizes; "input" feeds pointer events as if from a panel.
screens/test/headless.sh starts it with a bare nested KWin next to the
running desktop, and loads KWin scripts, runs apps, and takes screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:14:09 -06:00
DeeJanuzandClaude Opus 5.5 76f3cdd2dc Ask for one look at a centre dot after the headset was off
With the Own tracker, the probe polls its status each second. After the headset was off
(or the tracker restarted), it shows one dot at the centre until you look at it and press
(S skips): that click resets both eyes' shifts, so the first real clicks aren't 10-17
degrees off. The screen-to-direction geometry the calibration used is shared with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:08:07 -06:00
DeeJanuzandClaude Opus 5.5 0a88a6b7c9 Release a button held on a screen even when the laser lets go between panels
While a button is held on a screen, the laser leaving it no longer takes
KWin's pointer, and an invisible catcher overlay sits on the laser whenever
it's off every panel, so the release reaches KWin at the pointer's last
spot. The pointer helper also reports the mouse's left release as a
backstop. Not yet tested in the headset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:05:54 -06:00
DeeJanuz f17a5a66ab Merge remote-tracking branch 'frame/layouts-headpin' into floating-windows
# Conflicts:
#	README.md
#	display-settings/ft_display_settings.py
#	display-settings/main.qml
#	docs/reference.md
2026-09-29 23:02:12 -06:00
DeeJanuz d5dbe23cae Merge remote-tracking branch 'frame/pointer-ignore' into floating-windows 2026-09-29 23:01:46 -06:00
DeeJanuzandClaude Opus 5.5 389878b9fd Settle the floating-windows design with the user
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:01:46 -06:00
DeeJanuzandClaude Opus 5.5 0af087c776 Plan floating windows: any desktop app in a VR panel of its own
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:00:33 -06:00
DeeJanuzandClaude Opus 5.5 3022d7de9d Run the tracker on CPUs 5-7 by default
probes/core_ab.py with the headset on (3 rounds, the same replayed frames in
every block): on 5-7 a step took 8.4 ms against 13.2 ms on 2-4, where XRService's
head tracking also runs, and latency fell from 14.1 to 9.6 ms. The compositor's
late frames and CPU/GPU time per frame didn't change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:59:19 -06:00
DeeJanuzandClaude Opus 5.5 c37bee27c9 Keep the Own tracker's calibration in reach, and let it learn after a re-seat
The calibration dots for the Own tracker go out to the Calibration ring angle each way on
an oval, not to the window's corners, which were too far to look at while facing the
centre. The Own tracker learns from drags up to 25 degrees (after taking the headset off
and on, its first clicks were 11-17 degrees off, and the 6-degree limit blocked them). The
probe starts on the Own tracker when it's running, and the calibration header names the
tracker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:51:37 -06:00
DeeJanuzandClaude Opus 5.5 355f30f335 Add a live replay harness for the CPU-placement test
fh-ringplay plays a recording into a frame ring in real time, and fh-tracker
--ring reads it (cameras without a device node are mapped by name). With the
same frames in every run, probes/core_ab.py compares the tracker on CPUs 2-4,
on 5-7, and not running, measuring the tracker's step time and latency, the
compositor's late frames and CPU/GPU time, XRService's timing warnings, CPU
temperature and clocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:42:24 -06:00
DeeJanuzandClaude Opus 5.5 067a03ce38 Hand tracking for Frametop's hand cutouts
fh-camd (camd/) borrows XRService's camera buffers and publishes the tracking
cameras' frames to a shared ring. fh-tracker (trackd/) finds hands in them with
MediaPipe's palm and landmark models on ncnn, triangulates them in 3D, and
publishes them for ft-screens. fh-replay replays recordings offline. tracker/ is
the earlier Python version; tools/ and probes/ hold the checks and experiments.

As of this commit: crop contrast defaults to CLAHE for the palm search and plain
crops for the landmarks, --swap-sides works around fh-camd naming the side
cameras backwards after some XRService restarts (tools/check_sides.py detects
it), and --record-only, --with-dark, --cpus and --keep-presence support the
bright-light and CPU-placement tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:36:48 -06:00
DeeJanuzandClaude Opus 5.5 b76a8c50e0 Spread the Own tracker's calibration dots over the practice area
Its fit goes wrong past its dots, and the ring (limited by the window's height) never
reached the sides, where frame-eyes' worst practice clicks were. With the Own tracker
the calibration now uses the centre, corners, and side middles of the practice area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:04:06 -06:00
DeeJanuzandClaude Opus 5.5 30a9d15072 Add our own eye tracker as a gaze source in ft-gaze and the probe
ft-gaze reads frame-eyes' /dev/shm/frame-eyes-gaze and reports it as the source "own",
with each eye's gaze, where each lands on the screen, and its slip. The probe gets a
SteamVR / Own tracker toggle. With Own tracker on, it hides SteamVR's gaze and draws a
red dot per eye, its calibration fits fe-trackd's own calibration, and practice clicks
teach fe-trackd instead of the probe's correction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:38:17 -06:00
DeeJanuzandClaude Opus 5.5 a31d42b25c Move hand cutouts ahead to where the hands will be
The tracked hands arrive 30-60 ms after the cameras saw them and reach the
displays later still, so holes trailed moving hands. Track each hand's palm
velocity in the room and move its capsules ahead to about when the frame is
on the displays, every tick, so the holes also move smoothly between tracker
updates. Slow hands aren't moved (their velocity is noise). The control
socket gets cutouts predict on|off and cutouts lead <ms>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:18:40 -06:00
DeeJanuz d888b6c3f1 Merge branch 'pointer-ignore' into experimental 2026-09-29 14:13:36 -06:00
DeeJanuzandClaude Opus 5.5 9c04207bb9 Let the pointer pass through panels you pick, like a performance overlay
A head-locked performance overlay kept catching the 3D mouse. It has no
input method, so SteamVR's laser passes through it, but the helper hit
tests every visible overlay with ComputeOverlayIntersection, and the dot
stuck to it whenever it crossed that corner of the view.

POINTER_IGNORE in frametop.conf now lists overlay keys the helper leaves
out of the collision, comma-separated shell patterns, so "vendor.app*"
covers a whole app, including panels it opens later. The laser starts
just before the cursor point, so an ignored panel nearer to you doesn't
catch it either.

Frametop Input Settings has a new Ignored panels page. It asks the
helper for SteamVR's overlays ("overlays", answered from the list's
thread once vrcmd has run again, even while the pointer is off), groups
them by app, and has a checkbox per panel and one for the whole app.
Frametop's own screens aren't offered, since ignoring one would leave
nothing to click the app on with the mouse. Entries for apps that
aren't open are listed so they can be removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:13:34 -06:00
DeeJanuzandClaude Opus 5.5 094a7b27f0 Don't cut out hand parts right in front of the eyes
A capsule end near the eyes' plane projects far across a screen with a huge
radius, so one bad hand estimate there tore a hole through the screens for a
moment. Clip capsules 12 cm in front of the eye, as the tracker now does too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:36:45 -06:00
DeeJanuzandClaude Opus 5.5 b7cbd9fe16 Cut tracked hands out of screens so you see them through it (work in progress)
Where frame-hands tracks a hand between an eye and a screen, that eye sees the
room through the screen. handcut.cpp draws the screen's buffer side by side
(one half per eye) with the hands cut out, only while a hand is in front of it.
The cutouts command turns it on or off. ft-handtest tries it on a test panel.

Not yet tested in the headset.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:28:27 -06:00
DeeJanuz ed9542d3b8 Merge branch 'layouts-headpin' into experimental
# Conflicts:
#	README.md
#	display-settings/ft_display_settings.py
#	display-settings/main.qml
#	docs/reference.md
2026-09-29 10:24:30 -06:00
DeeJanuz 0c40b2c6de Merge remote-tracking branch 'origin/main' into experimental 2026-09-29 10:24:04 -06:00
DeeJanuzandClaude Opus 5.5 89522e1894 Let Flatpak apps save and upload files in the desktop
The file picker hands a sandboxed app the host path of the document
portal, which the desktop's private runtime directory moves to
$runtime/doc. Inside the sandbox that path is an empty private folder,
so Brave finished downloads into it and they were lost when the session
cleaned up. Link it to /run/flatpak/doc in each installed app's
runtime folder before Plasma starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:13:46 -06:00
DeeJanuzandClaude Opus 5.5 65cedb673c Warn in Input Settings when SteamVR hasn't loaded the pointer driver
When SteamVR crashes, safe mode can block the newest add-on, and then it
skips the ft_pointer driver at every start. The cursor still moves (the
pointer helper draws it), but clicks, scrolling and mapped actions go
through the driver's virtual controller, so none of them do anything,
and nothing said why (#4).

Input Settings now shows a warning at the top of every page with the
fix: Manage Add-Ons in SteamVR's settings, unblock ft_pointer, restart
SteamVR. It reads steamvr.vrsettings (~/.config/openvr/config on the
Frame) for blocked_by_safe_mode, a disabled driver, or SteamVR's own
safe mode. When none of those is set but SteamVR is running (the helper
answers) and @ft_pointer doesn't, it says SteamVR runs without the
driver: unblocked but not restarted yet, or not installed. The driver
ignores the "ping" it sends. It checks at startup and every 30 minutes,
since the driver only changes when SteamVR restarts.

Fixes #4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:04:17 -06:00
DeeJanuzandClaude Opus 5.5 51e7e0fa85 Warn in Input Settings when SteamVR hasn't loaded the pointer driver
When SteamVR crashes, safe mode can block the newest add-on, and then it
skips the ft_pointer driver at every start. The cursor still moves (the
pointer helper draws it), but clicks, scrolling and mapped actions go
through the driver's virtual controller, so none of them do anything,
and nothing said why (#4).

Input Settings now shows a warning at the top of every page with the
fix: Manage Add-Ons in SteamVR's settings, unblock ft_pointer, restart
SteamVR. It reads steamvr.vrsettings (~/.config/openvr/config on the
Frame) for blocked_by_safe_mode, a disabled driver, or SteamVR's own
safe mode. When none of those is set but SteamVR is running (the helper
answers) and @ft_pointer doesn't, it says SteamVR runs without the
driver: unblocked but not restarted yet, or not installed. The driver
ignores the "ping" it sends. It checks at startup and every 30 minutes,
since the driver only changes when SteamVR restarts.

Fixes #4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:04:17 -06:00
DeeJanuzandClaude Opus 5.5 b99fb97f32 Turn the displays off while the headset isn't used, and keep it awake on a charger
A display mount that covers the proximity sensor makes the headset seem
worn, so SteamVR never turned its displays off and they stayed on all
night. The new power service, ft-powerd (frametop-power.service), goes by
use instead: after DISPLAY_OFF_MIN minutes in which the headset and
controllers didn't move and no input device was used, it turns the
backlight off, and the next movement or input turns it back on.

The new Power tab in Frametop Display Settings sets that time and has a
Stay awake while plugged in switch. The switch sets Steam's own "When
Plugged In and Idle -> Sleep after" to Never through Steam's UI, so the
Frame stays reachable remotely while the power button still works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:34:34 -06:00