150 Commits
Author SHA1 Message Date
Patrick McDavidandClaude Opus 5.5 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>
2026-10-02 21:12:49 -06:00
DeeJanuzandClaude Opus 5.5 072a294941 README: Frametop doesn't work on the SteamOS beta yet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:12:52 -06:00
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>
v0.2.0
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
0x1f6andClaude Opus 5.5 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 f2bdbd97a0)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:34:22 -06:00
0x1f6andClaude Opus 5.5 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 ca7e58069b)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:34:12 -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