Desktop screens and remote displays get a round pin button left of the
reset button. Pressed, the screen rides on the headset where it is, as
"pin N head" does, and the button turns blue; pressed again, it stays in
the room where it is then. A layout reset ends it, since ft-layout places
every screen again and pins only what the profile saved pinned.
Hands press it like the other round buttons (Control::Pin).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A switch on Display Settings' Power tab (or desktops.sh autostart on) sets
AUTOSTART=1. frametop-autostart.service, wanted by steamvr.service and
installed by desktops.sh install, runs session/autostart.sh at each SteamVR
start: it waits for vrserver and vrcompositor, then runs desktops.sh start.
The Frame starts SteamVR at boot, so one switch covers both. Off by default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fingertip on Full Keyboard's panel went through ft-screens' SteamVR
panel path, so the pointer helper's laser did the click and showed up.
Now ft-screens sends those pokes to the input relay as "kbpoke SIDE VERB
[x y z]", the relay passes them to Full Keyboard as "kb poke ...", and
Full Keyboard presses keys with its own pointers, as Frametop's screens
take a hand. Other SteamVR panels keep the laser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like Half-Life: Alyx's keypads: each Frame controller's tip (its render
model's "tip" component, the nose of the body) hovers keys within 6 cm in
front of Full Keyboard and presses one when it reaches the surface from
the front, with the laser's haptic clicks. The key comes up 12 mm back
out, off the keyboard, 12 cm through it, or when tracking is lost; a tip
coming through from behind or the side never presses. Off while the
keyboard moves and for a hand gripping it.
On by default: General -> Touch typing (touch_typing). The keyboard opens
where it did. Fingertips already poke it through ft-screens, as any
SteamVR overlay, so they needed no change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Firefox drew its own title bar in the Frametop desktop, so its windows
had no KWin decoration and no float button. The session now sets
MOZ_GTK_TITLEBAR_DECORATION=none: Firefox's automatic title bar setting
then picks the system one, KWin decorates the window, and nothing is
written to the profile, so Firefox keeps its own title bar outside the
desktop. FIREFOX_TITLEBAR=firefox in frametop.conf turns this off.
Checked with Flathub Firefox 158 through Marionette and a KWin script:
no Firefox buttons, and the frame is 30 px taller than the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The search for apps and remote displays is now frunner everywhere: the
frunner/ folder, ft-frunner, the @ft_frunner socket, ft-screens'
"frunner on|off", the relay's frunner action, docs/frunner.md, the tour
and Input Settings labels. ft-spotpanel and spotsearch.py keep their names.
Spotwatch shipped in releases since v0.3.0-exp.8, so old state still works:
- rules files that bind "spotwatch" mean frunner (the relay and Input
Settings read it as frunner; Input Settings saves the new name)
- use counts are read from ~/.config/frametop-spotwatch.json until the
first save to frametop-frunner.json
- the desktop's session removes the old frametop-spotwatch.desktop autostart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
keyboard/build.sh builds Full Keyboard in the dev container (FULL_KEYBOARD_TESTS=1 with
its tests), loading SteamVR's libopenvr_api first like Frametop's other programs.
frametop-keyboard.service runs it with --frametop alongside SteamVR; keyboard/run.sh
installs it, and install.sh does that in its desktop step. The image builds it and keeps
only what it runs from. uninstall.sh covers its process and settings.
The README credits TaiKeid's Full Keyboard (MIT) and lists keyboard/; keyboard/FRAMETOP.md
records the import, every Frametop change, how it works with Frametop, and how to update
from upstream. docs/reference.md and docs/design.md describe it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From a tester's notes:
- Other languages: a language setting (English, detect, or 18 others) and a
whisper model (small.en, small, large-v3-turbo). Moonshine types English
live; other languages go to whisper-server, which voice/whisper-run starts
with the chosen model. A missing model is downloaded from the Voice page
(voice/models.sh model NAME, pinned commit, sha256). Text keeps its accents
and scripts (to_text) and is chunked by UTF-8 bytes.
- Single words: min_seconds 2.0 -> 0.3; a final window the live labels missed
is decoded anyway; when Moonshine hears nothing in a short window it's
decoded again cut to its speech, then after 0.2 s of silence; a lone word it
loops on is typed once. Replays of yes/way/hello/thank you: all type now.
- Tones: the start tone waits for the mic's first audio (or 0.6 s), tones play
one at a time, the mic stays open until the last ends (speaker and mic share
the echo-cancel filter), and each tone has silence before and after it.
- The Voice page: "Keys do" (tap to start and stop, or hold to talk),
language and model; "cancel <source> quiet" for a hold that was a shortcut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Firefox and Chromium draw their own title bar and take its buttons from
GTK's decoration layout. In the desktop they're Wayland clients and read
it from the Settings portal, where SteamOS has nothing to fill it in
(no kde-gtk-config, no xdg-desktop-portal-gtk), so they fell back to
GNOME's 'appmenu:close' and showed Close alone. From the dashboard they
run on X11 under gamescope and got GTK's own min/max/close default.
session/ft-gtksettings is a Settings portal backend that serves
org.gnome.desktop.wm.preferences button-layout from kwinrc's button
order (as kde-gtk-config maps it), follows changes, and writes the key
to the desktop's own GSettings for non-Flatpak GTK apps. The session
gives xdg-desktop-portal its own backend folder with it and a copy of
KDE's portals.conf asking kde first, then frametop, for Settings, and
puts the helper's D-Bus service file on XDG_DATA_DIRS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tour/ft-tour walks through Frametop a page at a time: screens and the 3D mouse,
the key combinations the relay has now and macros, gaze mode with Meta+J/K,
hand tracking with and without gaze, spotwatch, floating windows (with the
dashboard apps choice), pinning, profiles, pausing for VR games, and more.
Pages follow what's installed, with buttons that try things or open their
settings, and practice targets on the gaze and hands pages.
The session's autostart runs ft-tour --first-run, which opens it only while
~/.local/state/frametop/tour.json is missing (or from an older TOUR_VERSION).
Closing, skipping, or finishing writes it; --reset, or the last page's box,
shows it again. Menu entry via tour/install.sh (install.sh step 7, removed by
uninstall.sh), and a Frametop Tour entry in Input Settings' drawer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New setting DASHBOARD_APPS (Display Settings, Visibility & pins): with it
set to frametop, an app picked in the dashboard's Launch a program list
opens floating in the Frametop desktop instead of as a SteamVR window.
float/ft-dashroute runs in the desktop session and wraps Steam's
SteamClient.Apps.LaunchNonSteamApp over its DevTools port
(float/dash-route.js). ft-floatd's new dashlaunch command finds the app's
desktop file by its command and launches it floating, or says it stays
SteamVR's: setting off, Frametop paused, every floating window in use, or
Steam, SteamVR, a desktop or a layout entry. Then Steam's own launch runs.
The wrapper only diverts while the router's heartbeat is fresh, so with the
desktop closed every launch is Steam's own. update-check checks the call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default gesture needs both thumbsticks, so with only one controller
connected Frametop couldn't be paused or resumed from the controller. A
gesture with a button on each controller now falls back to the connected
controller's button alone, clicked twice, until the other one is back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A per-host switch, Send my microphone, off by default ("mic" in the
host's layout entry, ft-layout remote host NAME mic=on|off). The stream
that plays the host's sound records PipeWire's default source and sends
it as mono Opus on the encrypted control stream, through a small
moonlight-common-c patch (LiSendMicrophoneOpusData, packet 0x3003, the
layout of Vibepollo PR #169). Packets go reliable on a channel of their
own: ENet's throttle dropped a quarter of unreliable ones on Wi-Fi.
Frametop's Vibepollo build on Windows plays it into Steam's virtual
microphone, so apps on the PC record "Microphone (Steam Streaming
Microphone)". Other hosts ignore it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fusion (GAZE_FUSION=on, off by default): SteamVR's two-eye mean,
corrected by an affine fitted on our calibration's dots (fusion.json,
saved by Calibrate with Own tracker or compare.py --fit-fusion), mixed
half and half with ours while ours sees both eyes, alone while ours
has one or none. On the 2026-10-17 bench runs (25 presses, fitted on
the other presses): ours 2.01 (p90 6.47), SteamVR affine 1.78 (4.26),
fusion 1.51 (3.10).
Size guard (EYES_SIZE_GUARD=both by default): a pupil find under 0.75
of its eye's 10 s usual size, relative to the other eye's, counts as
not found, so the right camera's dark-spot misfinds drop while light
shrinking both pupils passes. Finds log marks refused rows.
compare.py: SteamVR scored as its eyes' mean, affine and fusion
techniques (live, -loo, -xrun, with clicks), guard replays,
--fit-fusion. benchrec copies fusion.json; the bench's quick column
shows SteamVR's eyes' mean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every head-locked gaze calibration panel (quick check, five dots, the full calibration, the fit
check) now takes a dot from an index pinch with hand tracking (its begin, with or without
POINTER_HANDS, not in a VR game, while typing or at a controller) and from Enter on a keyboard,
as well as a left click and Meta+J.
- pointer helper: CalPinch reads ft-hands' pinch begins while the panel is up; other hand
gestures wait meanwhile. "calpanel 1 NAME" lets another program's panel claim the answers
(on @NAME); the helper tells the relay "calpanel 1|0".
- input relay: while the panel is up, Enter (no modifier) goes to the helper as "calkey 1",
and its press, repeats and release aren't typed.
- gaze bench: claims the panel as ft_gazebench.PID while a run or its summary shows, so a click,
Enter or pinch marks (by "helper") and a right click ends; the pointer hides meanwhile.
- panel texts and docs name the new inputs; tests for each part.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A keyboard with a touchpad has a keyboard node and a mouse node at one
Bluetooth address, so it got one role: 3D pointer, which took its keys
too (no key combinations, and Frametop's keyboard thought none was
connected). The new "combo" role splits the device by node: mouse nodes
are pointer, keyboard nodes passthrough (node_role, now module level and
tested in input/test/roles-test.py).
It's never a default, because the Swiftpoint Z3's keyboard node is its
extra buttons and nothing on the nodes tells the two apart. Input
Settings offers it for devices with both kinds; the Buttons and Keyboard
pages count such devices, and button capture skips their pass-through
keys (watch events now carry the node's role).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 2026-10-16 the pause gesture resumed Frametop and paused it again 2 s
later. An update then restarted the desktop and the relay: the relay came
back paused from its state file, but the new ft-screens never heard
"pause on", so the desktop showed while the 3D mouse stayed off.
- The relay passes the pid of each ft-screens "keyboard" report (sent
every second) to GamePause.screens_seen, which tells a new ft-screens
"pause on" while paused.
- The gesture is ignored for GESTURE_COOLDOWN (3 s) after it goes off,
in place of the 1 s triple-press guard; dropped ones are logged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user, 2026-10-15: right clicks were hard and the dot kept going away.
The laser's "ready" gate now opens with the thumb near the index or the
middle tip, so a middle pinch (right click) or a three-finger pinch has a
laser to aim with. A middle pinch's aim lock follows the thumb to middle
distance.
A three-finger pinch (thumb on the index and middle tips, the gaze
scroll's POINTER_GAZE_SCROLL_GAP) on the ray scrolls as the hand moves and
never clicks, whichever finger ft-hands calls it, at the begin or joining
before the press went out. A middle tap's first POINTER_RAY_TAP scrolls
only past 1.5 cm, so a right click that wobbles still clicks (two of the
day's turned into scrolls).
Grab that's claim skipped a thumb on the middle tip, so a middle pinch
from a hand held out (the ray's pose) looked like the fist closing and
was left to ft-screens (13 of the day's middle pinches). It now needs the
thumb off both tips; the user's flag keeps it 3.5 cm or more from the
middle tip as the fist closes, and still fires twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Off by default until it's been tried in the headset. The fit check keeps
a good fit as yours (fit-baseline.json), as the eye recorder's fitment
does. With GAZE_FIT_WATCH=1:
- The quick check that opens when the headset goes on first holds its
dot while the fit coach reads the fit against the saved one. Moved
past 5 degrees, the panel widens and says which way to move it, live,
until it's back; a click or 30 s takes the dot as it sits.
- With our tracker in use and gaze mode on, ft-eyes' glint slip (status
now has jump_px) past 5 degrees for 2 s shows a small strip low in
view ("show note" in ft-gazepanel), for 8 s or until the slip is gone,
at most once every 2 minutes.
gaze/test/fit-watch-test.py tests both offline. The eye recorder's
redo test waited on a state the step before left, and missed the redo
about 1 run in 8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install.sh offers hand tracking (step 9/12, yes by default; --no-hands skips it,
and --yes installs it only when sudo runs without a prompt). Installed, it runs
with the Frametop desktop (HANDS_MODE=frametop), unless the settings already say
always. get.sh, pack/install-release.sh and the FrameDrop window pass --no-hands,
and the window has a hand tracking box that needs the password like the eye
tracker's.
The release image builds ft-camd and ft-hands (with ncnn at the tag
hands/Makefile pins), and hands/run.sh install doesn't build in a release, so
it only sets ft-camd's file capabilities and installs the units. The release
workflow's smoke test checks both binaries. Docs drop "deferred" and describe
the gestures and the Hands page.
Reverts 797e5c7. With pointer/dashpatch, the treadmill moves SteamVR's
windows by their grab bars, so the default goes back to the role that
leaves both hands to the controllers. docs/design.md explains the cause and
the patch; reference.md lists the patch; update-check.py checks that the
dashboard still has the function the patch wraps and that its debugger is
reachable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As a treadmill (the default since 564bb4b), the 3D mouse's laser clicks and
resizes SteamVR's windows but can't move them by their grab bars: the
compositor's window grab follows only /user/hand/left or /user/hand/right,
and logs nothing. With the right hand's role the same laser moves them
(headset test 2026-10-13, SteamVR 2.18.2). The driver's default role is 2
again and POINTER_ROLE defaults to right; treadmill stays a setting.
update-check warns when the device is at /user/treadmill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Remote displays section in the README with the three host kits; host/windows/README.md
and host/linux/README.md beside the Mac's; reference.md names frametop-2.0.0-2 for the
resolution switch; remote-displays.md is the design notes now. No machine names in hands/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spotwatch's "spin to N" still turned the whole ring after the merge, the panel in the
front slot with it, while next and prev bring one panel to the slot. With
SCREEN_SPIN_BRING on (the default) it now brings panel N there too; the ring turn stays
for SCREEN_SPIN_BRING=0 and for degrees. Docs: reference.md's spin, spotwatch.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- hands/rec/demo: ft-handdemo, sheets.py and gifs.py recorded the demo clips in the
headset, dropped on 2026-10-12 for generated skeleton clips. camgifs.py keeps its
gifsicle lookup; the README covers what's left.
- hands/.gitignore: rec/demos-recorded/, the recorded clips show the recorder's room.
- spotwatch/test/render.py -> spotwatch/tools/render.py: it needs an output path, and
just test-python ran every spotwatch/test/*.py with no arguments.
- voice/README.md: the advanced example's hook is the repo's hooks/on-command, not a
path in one person's home.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frametop Streams lists the Mac's displays (its Sunshine instances) and turns
each one, or all of them, on or off. ft-sunshine off|on|status does the same
in Terminal. Off disables the instance's LaunchAgent, so it stays off through
logins until on. The setup compiles the app from Frametop Streams.applescript
(AppleScriptObjC, no compiler) and starts it at login; --undo removes it.
README.md is the Mac setup guide, also the notes of the kit's release.
Fifth live test (2026-10-10 22:55): grab that brought the desktop in, it was
carried and thrown 3 m out, then five gestures at it or the remote displays
logged "no panel there". The open palm is held out below the line of sight,
so the line from the eyes through it runs 12-15 deg under where the head
points (the user's flag 150115). Since the fourth live test the line had to
meet a panel exactly, and a thrown 1.35 m tall desktop 3 m out is only
12.7 deg each way, so the line passed just under it. One of the flag's two
fires misses such a panel that way.
Pick now takes the panel the line meets (unchanged, nearest wins), else the
one whose outline it passes nearest within HANDS_GRAB_THAT_MARGIN_DEG
(6 deg seen from the eyes; 0 = the old rule). The gesture itself is
unchanged, so it stays as hard to trigger, and held, pinned, pressed and
near panels are still never taken. "no panel there" now logs where the line
went relative to the head and the nearest panel it may take, with its angle.
Test: TestGrabThatAfterThrow runs the user's sequence with synthetic hands:
grab that brings the desktop in, a bar carry is thrown out and up at
1.3 m/s, then grab that at it again (missed by 0.8 deg with the old rule,
taken now), a remote display after that, precision cases, and the flag's
two fires (2 of 2 now, 1 with the old rule).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The example config gets HANDS_FIST_REACH, HANDS_JOIN_REACH, HANDS_BUTTON_REACH,
HANDS_PINCH_BUTTONS, HANDS_EDGE_GLOW, HANDS_GRAB_THAT_FROM, HANDS_GRAB_THAT_MARGIN and
HANDS_GRAB_THAT_FACING; hands/README.md and docs/reference.md describe them, the white
halo, the corner, edge and button glows, and that near panels go away by push, flick or
throw now, not by grab that. `handinput state` counts joins, pinches that waited to be
fists, and buttons pinched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hands/README.md (near input, the palm menu), docs/reference.md and session/frametop.conf.example:
HANDS_PUSH, HANDS_PUSH_SPEED, HANDS_STRETCH_TURN, HANDS_TRACK_GATE, HANDS_TARGET_GLOW,
HANDS_SEND_AWAY_WIDTH; fists ft-hands calls pinches, the pinch slack, holds through the helper's
word, the near-the-eyes carry, the stricter palm menu, with the replays' numbers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
session/frametop.conf.example: HANDS_POKE_TRAVEL=0.017, the card
pinch's fingers, and the new HANDS_HOLD_LOST_MS, HANDS_FLICK,
HANDS_FLICK_SPEED, HANDS_FLICK_OPENING, HANDS_THROW, HANDS_THROW_SPEED,
HANDS_THROW_DISTANCE and POINTER_GAZE_CONTROL_HOLD. scripts/conf-migrate.sh
moves the example's exact HANDS_POKE_TRAVEL=0.025 line (hands-touch,
never released) to 0.017. hands/README.md's near input section,
docs/reference.md's .resize, settings and handinput state, and the
gesture tables at the top of handinput.h and vr.cpp say what each
gesture does now and what wins. The README notes that the hand glows
are drawn into the cutout buffer, so they don't show with the cutouts
off (the default since the drawn hands).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
session/frametop.conf.example: HANDS_NEAR_REACH=0.075, the new
HANDS_POKE_TRAVEL, HANDS_GRAB_BAND, HANDS_PINCH_CLICK, HANDS_CARD_PINCH and
HANDS_FIST_GRAB. scripts/conf-migrate.sh moves the example's exact 0.30
line (hands-reach, never released) to 0.075. hands/README.md's near input
section, docs/reference.md's settings and handinput state, and the
gesture table at the top of handinput.h and vr.cpp say what each gesture
does near a panel and what wins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- ft-pointer.cpp, the gaze pinch comment: typing's "the rules for all
gestures hold (..., typing, ...)" kept, gazechord's chord and
POINTER_GAZE_PINCH paragraphs added after it, and a line that a hand
typing holds back makes no chord.
- ft-pointer.cpp, the hands settings: gazechord's gazePinchOn, nearOn,
handsTrack kept; typingHold dropped for typing's typing::Params.
- ft-pointer.cpp, mayPoint: both conditions (handsOn, and not a side
typing holds back).
- ft-pointer.cpp, beginGazePinch: gazechord's busy line (a chord isn't
busy), without the old sinceKey/typingHold lines. The chord's pinch
goes through typing.h's Why in the gesture loop before startGesture,
like any pinch, so a hand typing holds back can't make one.
- docs/reference.md settings line: typing's and reach's keys, plus
POINTER_GAZE_PINCH.
- hands/README.md near input: typing's hold-back sentence and
gazechord's HANDS_NEAR=on.
- session/frametop.conf.example: reach's HANDS_GRAB_ROTATE* and
HANDS_GRAB_THAT* lines, then gazechord's HANDS_NEAR line.
Reconciled: gazehand's Situation loses sinceKey and typingHold, and
Refused its "ignored (typing)": the helper had stopped setting them on
hands-typing (typing.h holds hands back first), so that guard could
never fire. gazehand-test's key checks now drive typing.h: keys hold a
hand back (2 s now, was 1), POINTER_PINCH_TYPING=0 is no guard, and a
chord's pinch by a hand on the keys is held back while one lifted off
them goes ahead. The chord's own pinch counts as a gesture held for
typing's "busy" (free if out of the keyboard zone when keys come), as
the left's hold does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: docs/reference.md's hands settings line (typing's POINTER_TYPING_*
keys next to reach's HANDS_NEAR_REACH, HANDS_GRAB_ROTATE* and
HANDS_GRAB_THAT*): both kept.
Typing and grab that: a hand typing holds back (the helper's "typing" word)
doesn't arm or fire "grab that" either, so it never claims a fist as near;
vr.cpp's UpdateGrabThat leaves it out, as Poke and Grab already do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hold an open hand out to a screen, remote display or floating window,
palm to it, then pull the arm back making a fist: the panel comes over
250 ms to 45 cm ahead of the eyes, at the fist's height, facing you
(user, 2026-10-10). Screens wider than HANDS_GRAB_THAT_WIDTH (0.6 m)
come at that width. The same gesture at a panel that came, still where
it came to, sends it back; the reset button sends floating windows back
(the layout reset places screens and remote displays itself).
screens/grabthat.{h,cpp}: the detector (pure logic): open fingers, palm
ahead and the wrist 28 cm out for 0.2 s arm it; the fingers closing with
the palm still to the panel, then on one frame a fist, the wrist 8 cm
back within 0.5 s and the palm turned over fire it. The panel is the one
the eye-through-palm line meets, else the one the head points at. From
the closing (thumb clear of the index: not a pinch) to 0.4 s after the
fire the hand counts as near, so the helper doesn't take the fist as a
gaze grip. Hands frames carry the head pose at capture now
(Hands::captureHead).
Tests replay the user's flag (fires twice, right hand, at -11.35 and
-5.89 s; expected -11.4 and -5.9), and the other 12 flags and 15 minutes
of recordings fire nothing (1 of 143 pinches, 3 of 12 grips begin on a
claimed hand); synthetic waves, pushes, fists without the pull, slow
pulls, a hand turned over first, held-out pinches, a drop to the lap and
typing don't fire. handinput-test --grabthat FLAG_DIR... scans flags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without Accessibility, Sunshine on macOS still moves the pointer (it
warps it) but clicks, scrolling and typing are dropped. Sunshine asks
only once, so a lost grant, or one given to a Homebrew Sunshine, never
gets asked for again.
The setup now reports Screen Recording and Accessibility from what
Sunshine noted at its last start, at the end of a run and with --check.
If Accessibility is still missing after the first instance restarts, it
offers to clear Sunshine's TCC records and restart it so macOS asks
again. Documented in remote-displays.md and reference.md.
While one hand carries a panel, the hand's turn since the grab turns it
too, 1:1 about yaw, pitch and roll (user, 2026-10-10: line displays up).
The turn comes from the wrist and the index and little knuckles, which
hold still while the fingers pinch, smoothed with fh_smooth's Direction
filter, relative to facing the head across the floor from the hand, and
is applied on top of the panel facing the head: carrying a panel round
you still only keeps it facing you. Nothing turns until the hand has
turned HANDS_GRAB_ROTATE_DEADZONE (5 deg), then it turns from there
without a jump; within 2.5 deg of level it snaps level. Let go, it keeps
the turn, and the next grab starts from it. Corner and tab grabs only
resize; a stretch is unchanged and the hand left holding starts afresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>