Commit Graph
28 Commits
Author SHA1 Message Date
DeeJanuzandClaude Opus 5.5 2c472879ed Spotwatch: Meta+Space search for apps and remote displays
A Spotlight-like search pill (docs/spotwatch.md). Meta+Space (the input
relay's new spotwatch action, bindable to mouse and controller buttons
too) opens a faux-glass pill straight ahead of the head. Typing finds
apps and remote displays ({host} - {display}). Enter brings a running
app's most recent window to the front with the lazy susan, launches an
app that isn't running as a floating window, or connects and spins to a
remote display. Shift+Enter or a right click opens the item's menu: the
app's desktop actions, Open new window, Open docked on this screen, and
its windows; a display's Go to and Disconnect.

- spotwatch/ft-spotwatch (Python, in the Plasma session's autostart):
  the index, ranking (spotsearch.py), keys via libxkbcommon, usage.
- spotwatch/ft-spotpanel.cpp (SteamVR overlay, built by
  screens/build.sh): the pill, drawn on the CPU; --render for a look
  without a headset.
- ft-screens: "spin to N", "front N", and "spotwatch on|off", which
  routes every key to the search while it's open (a lease, renewed
  every second) and dismisses it on a click elsewhere or a pause.
- ft-floatd: "spotlist", "activate ID", and launch action=/screen=;
  the KWin script tracks when each window was last active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:08:59 -06:00
DeeJanuzandClaude Opus 5.5 b3275e7843 Voice: frame-voice becomes Frametop Voice, installed with Frametop
voice/ft-voiced (was frame_voice.py) runs in Frametop's container as
frametop-voice.service. It keeps the mic, the wake word and live text,
and hands the keyboard side to the input relay:
- push-to-talk keys are key combinations (dictate, dictate_hold); the
  relay sends "toggle|start|stop" to @frametop_voice, taken only from
  this user
- text goes to the relay as "type TEXT" (ASCII, in pieces of 2000
  bytes), so it lands where typing goes; the relay holds it back while
  a modifier is held
- controller buttons are read from vrserver's web socket with
  input/vrws.py, in a thread, so the websockets package goes
No own uinput keyboard, fd store, evdev, or controller service any more.
whisper-server runs only while the wake word is on, started and stopped
by ft-voiced. Voice commands and hooks run on the host through
distrobox-host-exec (env passed through env(1)); their FRAME_VOICE_*
variables keep frame-voice's names.

Settings: ~/.config/frametop-voice.json (voice_settings.py, standard
library), wake word off by default, "advanced" for the old config.toml
tunables. voice/run.sh install carries frame-voice's settings over once
(its keys become key combinations) and turns the standalone units off.

Install: install.sh step 8/11 asks (yes by default; --no-voice through
get.sh, install-release.sh and a FrameDrop checkbox). voice/build.sh
builds whisper-server (whisper.cpp d09f61a, Vulkan, static) and
build/venv; voice/models.sh fetches the 322 MB of models into
~/.local/share/frametop/voice by fixed URL and sha256. The image gets
glslc, spirv-headers-devel and pipewire-utils, uv.lock a voice group (sherpa-onnx with its
core pinned: uv dropped it), CI smoke whisper-server and the venv.
uninstall.sh asks about the models; update-check and report.sh know the
service (report.sh leaves out the lines with what was said).

Tests: voice/test (settings, carrying over, typing through a stand-in
relay, push-to-talk, controller buttons from a stand-in web socket) in
just test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:18:14 -06:00
DeeJanuzandClaude Opus 5.5 e21a2beae0 Release CI: an experimental release's install line says --experimental
get.sh --release asks for a channel and offers stable first, and the
Frametop organization has no stable release yet, so the notes' line
fetched releases/latest and got a 404 unless you picked 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:04:59 -06:00
DeeJanuzandClaude Opus 5.5 8a88fbea05 Release CI: the FrameDrop manifest names this repo's release; get.sh --release takes FRAMETOP_REPO
So a fork's test release (Frametop/frametop, where Depot's runners are set up) points at its
own zip, and get.sh --release can install from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:44:43 -06:00
DeeJanuzandClaude Opus 5.5 9411148596 Releases are one file: Frametop.zip on a GitHub release, built on Depot, no registry
User decision (2026-10-07): people download one file from GitHub, installed through FrameDrop
or unpacked on the headset, holding everything, with no container registry.

Frametop.zip (framedrop/build.sh --image, about 1.1 GB) holds the image as an OCI archive
(podman save), frametop-release.json (pack/release-info.py: version, commit, the archive's
sha256, the image's ID, and the SteamOS table), the install window, and
pack/install-release.sh. That script checks this SteamOS build (against main's steamos.json
when it can fetch it, else the release's copy), checks the archive's sha256, loads it and
checks the ID, copies the release's tree out and runs its install.sh, then keeps the release
before and removes older ones. pack/release-box.sh checks the image's ID before making the
container. The install window runs install-release.sh when the zip has one (get.sh in a test
zip), and reaches systemd through the user's real bus when it's opened from a VR desktop.

get.sh --release now downloads the zip from GitHub (newest stable, --experimental,
--version V, or --zip FILE|URL), resumable and per release, and runs its installer; a
damaged download (exit status 3) is fetched again next time. The registry release list and
its writer are gone.

.github/workflows/release.yml replaces image.yml: on a v* tag or by hand only, on
depot-ubuntu-24.04-arm-4, it builds the image with podman, runs the test gate in it, checks
what a release installs, builds the zip, and makes a draft release (prerelease for a tag
with a "-") with the zip, its FrameDrop manifest, frametop-release.json, and SHA256SUMS.
pack/release-notes.md is the draft's install text.

Tested on the Frame: the release tests (24 checks), a real zip from the local image installed
with get.sh --release --zip --clone-only (sha256, load, ID, copy), a damaged archive (exit 3),
and release-box.sh refusing an image with another ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:34:57 -06:00
DeeJanuzandClaude Opus 5.5 525c77e60c Tests in the image get the host's datagram queue length (512, not 10)
A container's own network namespace starts with net.unix.max_dgram_qlen
at the kernel's 10; systemd sets 512 on the host. The input relay sends
without blocking and drops what a full queue refuses, so in the image
keys-test.py saw a starting relay's release burst lose its last five
mouse-button releases. ft runs and CI's test step now set 512 and keep
their own namespace, so tests can't collide with a live desktop's
sockets. pack/design.md adds the queue length to what the runtime on the
Frame must keep (the host namespace has it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:06:09 -06:00
DeeJanuzandClaude Opus 5.5 64d3a9eb9c CI smoke: test this run's image, through docker and a registry in the job
The runner has podman as well as docker, so ft picked podman, which
pulled the last image published to GHCR: the wrapper smoke tested that
image, not the one the run built (on the fork, ghcr.io/0x1f6/frametop:
pack-frametop-image). On a branch nothing is in GHCR, so ft update failed.
FT_ENGINE now picks the engine, the job sets it to docker, and ft update
pulls this run's image from a registry started inside the job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:58:34 -06:00
DeeJanuzandClaude Opus 5.5 b15dce6a0a CI: a lowercase image name, so the workflow runs under DeeJanuz/frametop
github.repository keeps the owner's capitals, and image names must be
lowercase: under DeeJanuz every run failed at the first docker run. The
workflow_dispatch choice of a self-hosted runner goes too (that runner
and its setup script exist only on the fork), and scripts/** now
triggers a build, since the build scripts the image runs live there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:51:35 -06:00
0x1f6 18e436d7b4 CI: publish (ghcr push + ft artifact) only from main and version tags
Feature branches are validated, not published — the registry holds only
states a user would actually run. Branch validation still covers the
full build/test/smoke chain against the local image.
2026-10-06 02:30:12 +02:00
0x1f6 bb88599085 CI: a newer push to the same ref cancels the superseded run 2026-10-06 02:28:54 +02:00
0x1f6 d6f6f0fe7f CI: upload-artifact v4.6.2 → v7.0.1 (Node 24 native, no deprecation warning) 2026-10-06 02:14:09 +02:00
0x1f6 491202acfc CI: one job, five steps — the image is built once and never re-pulled
Separate jobs meant every stage re-pulled the just-pushed image from
GHCR (~1.5 GB each). One runner keeps it in the local Docker cache:
build (no push) → test → smoke → push → artifact. The registry only
ever receives a green image, and the artifact's image-digest.txt names
a digest that exists because the push preceded it.
2026-10-06 02:13:26 +02:00
0x1f6 b234abc245 CI: smoke stops rerunning the suites; just test delegates to the strict recipes
The smoke job ran 'just test' beside the new test job — the same suites
twice, once lenient, once strict. Smoke now checks only the artifact
(binaries, venv) and the wrapper paths; 'just test' is a thin alias for
test-python test-c test-bash, so there is exactly one definition of
what passing means.
2026-10-06 02:08:24 +02:00
0x1f6 f8f33c5663 CI: publish the ft wrapper as a checksummed artifact
An artifact job (needs build+test+smoke) extracts /src/frametop/ft from
the image the run built, verifies it byte-for-byte against the checkout,
and uploads it with its sha256 and the image's RepoDigest. The artifact,
the image, and the commit are thereby provably one thing — and install.sh
can later verify the wrapper it installs against ft.sha256.
2026-10-06 02:06:10 +02:00
0x1f6 b33c5df41f CI smoke: capture the published reference before unsetting FT_IMAGE
The unset landed before the sandbox published-file was written, so
update pulled the empty reference (podman: 'repository name must have
at least one component', exit 125). The wrapper was never at fault.
2026-10-06 02:00:39 +02:00
0x1f6 06288ff20c ft: defer the wrapper-from-image refresh and the baked update default
The hard wrapper/image pairing broke the smoke job under Docker (exit
125, 'repository name must have at least one component' right after the
digest pin). Deferred until the smoke job can say exactly where it
fails; ft update stays the simple, locally verified pull + digest pin,
and the published reference must be configured explicitly again (no
silent fallback). The smoke job drops the pair cmp accordingly.
2026-10-06 01:54:20 +02:00
0x1f6 7854d6ab9d CI: a strict test gate (test-python, test-c, test-bash) beside the smoke job
just test-python runs every Python suite strictly (hands and gaze
included — all pass off-device); test-c compiles the controller-click
unit test with -Werror in the image (sides_test stays with the hands
ncnn build, documented); test-bash syntax-gates every shipped shell
script. The new test job runs inside the image the build pushed, next
to smoke: smoke asks if the artifact is sound, test asks if the code
is right. controller-click-test.c #undefs NDEBUG so a release build
still tests. hands/** joins the CI trigger paths.
2026-10-06 01:48:34 +02:00
0x1f6 434b4aadc9 CI smoke: unset FT_IMAGE in the installed-mode section — the override shadowed the pin, so the digest and :latest checks tested nothing 2026-10-06 01:43:46 +02:00
0x1f6 ed6caf6ff0 ft update: refresh the installed wrapper from the image; CI verifies the pair
ft update now also copies /src/frametop/ft out of the pulled (digest-
pinned) image over the installed wrapper, backing the old copy up as
ft.previous — wrapper and image are one artifact, built from the same
commit by CI, and they cannot drift apart on a device. The update
source falls back to the channel baked into the wrapper
(FT_DEFAULT_PUBLISHED) instead of failing when install.sh has not
written a config yet.

CI smoke: after the sandbox update, the refreshed wrapper must be
byte-identical to the checkout's ft — the pair test.
2026-10-06 01:40:21 +02:00
0x1f6 5fa557afbb CI: the smoke job exercises the ft wrapper; wrapper changes trigger CI
The wrapper is the counterpart of the image — every device run goes
through it. The smoke job now runs it against the image the build
pushed: repo-mode program run, installed-mode update with digest-pin
verification, and the :latest refusal. ft joins the CI trigger paths.
2026-10-06 01:37:42 +02:00
0x1f6 e984418b25 CI: merge the doc exclusion into paths (paths-ignore cannot combine with paths) 2026-10-06 01:28:29 +02:00
0x1f6 77f7cc82be CI: doc-only changes no longer build the image 2026-10-06 01:25:12 +02:00
0x1f6 2ce81c553a actions: bump the docker actions to their node-24 majors (checkout v7, setup-buildx v4, login v4, metadata v6, build-push v7) — silences the runner's node-20 deprecation 2026-10-06 01:11:04 +02:00
0x1f6 332633c919 smoke: pull the tag the build pushed and log in first (fresh GHCR packages are private) 2026-10-06 01:11:04 +02:00
0x1f6 d2a9e2bbc9 CI: point buildx at pack/Containerfile (it defaults to ./Dockerfile) 2026-10-06 01:11:04 +02:00
0x1f6 318a4a1682 runs-on: the env context is not allowed there — inline the runner choice 2026-10-06 01:11:04 +02:00
0x1f6 60095b02bf workflow_dispatch with a local-runner option
The cloud arm64 runner stays the default on push. For on-demand testing while
it sits in the queue, workflow_dispatch takes a 'local' choice: both jobs then
run on a self-hosted runner labeled ft-arm64 — an ephemeral actions-runner
container in OrbStack (native arm64, same architecture the Frame runs), set up
by a local script that stays out of the repo. The runner fetches a single-use
registration token per start; the PAT stays in the macOS Keychain and only
ever scopes this one repository. The workflow triggers on push and
workflow_dispatch only, never pull_request — that is what keeps a self-hosted
runner safe on a public repo.
2026-10-06 01:11:04 +02:00
0x1f6 ea47422872 Frametop as an OCI image: pack/Containerfile, the ft wrapper, just, CI
The container becomes an artifact instead of a recipe: the base toolbox image
is pinned by digest, Python dependencies come from the committed uv.lock
(uv-managed CPython, clean venv at /opt/frametop/venv, no system-site-packages),
and OpenVR's header and libopenvr_api come from the same pinned public tag the
Frame-side build scripts pin. The native binaries compile inside the image the
same way the build.sh scripts compile them; hand tracking stays deferred as in
install.sh.

- ft: one wrapper for everything that runs (build, shell, run, test, update);
  Frame mounts (ipc=host, /run/user, /opt/steamvr) are designed behind
  FT_FRAME=1 until validated on the device (pack/README.md)
- justfile: runner for the in-image actions (test, lint as report, check)
- CI: native arm64 runner builds and pushes ghcr.io/0x1f6/frametop and runs
  the suites inside the built image as a smoke job; actions pinned to SHAs

Validated locally (docker, aarch64): full build, all six binaries, and
./ft test green (unittest suites, check scripts, 11 pytest tests).
2026-10-06 01:11:04 +02:00