Compare commits

...
Author SHA1 Message Date
saphidandClaude Opus 5.5 02b9413f78 Merge main into devices: several headsets alongside the app store, comfort, panels and media
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 12:44:30 +10:00
Alex Southwell 1a08258e3d Merge pull request #49 from saphid/theatre-media-device-fixes
Media player: keep playing through headset standby (real-Frame test fixes)
2026-09-29 12:40:35 +10:00
Alex Southwell a84d6b912b Merge pull request #44 from saphid/apk-store-fixes
Android game store: every source in one search, and proper Steam library entries
2026-09-29 12:39:16 +10:00
saphidandClaude Opus 5.5 448720d8d6 Tests: skip the SIGINT launcher case on bash 3.2 (macOS's), which doesn't run the trap during wait
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 12:35:23 +10:00
saphidandClaude Opus 5.5 1cd56740a6 Media player: keep retrying the theatre surround after a still is shown
Review (GPT-6 Astra, P2): the still-image loop stopped calling show() once
the screen took its first frame, so a surround refused during standby was
never retried and stayed missing for PNG and splat playback until restart.
hold() now keeps draining pending uploads after the screen is shown, until
both are up.

Also: stills and the surround wait out standby without counting as dropped
video frames or tripping the five-minute limit (video only); teardown
errors no longer overwrite a finished status; Stop is ignored once the
outcome is decided.

Tests: PNG and splat where the screen is accepted before the surround
recovers (fail on the old loop); fake-clock coverage of the five-minute
limit and its reset; status keeps filename/metadata layout sources and
explicit layouts stay explicit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 12:32:12 +10:00
saphidandClaude Opus 5.5 b685a601f7 Mac view: checking the headset and publishing a tunnel happen under one short lock with retarget
Astra review: a switch landing between the check and the assignment could still
install the old headset's tunnel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 12:31:47 +10:00
Alex Southwell 31de17aab8 Merge pull request #48 from saphid/frame-mcp-verified
Record real-Frame MCP end-to-end results
2026-09-29 12:27:26 +10:00
saphidandClaude Opus 5.5 ffef4d897a Devices: the assistant's keep-awake and panel tools reach the chosen headset; a Mac view tunnel opened during a switch is dropped
Integration review findings: the scripts they run ssh'd to whatever 'frame' means
in ~/.ssh/config. They now take FRAME_ALIAS and FRAME_SSH_OPTS from the server's route.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 12:16:25 +10:00
saphidandGPT-6 Astra 5aea46afd0 Merge latest origin/main VR utilities into apk-store-fixes
Preserve the complete store, artwork, telemetry, input, media and agent route table alongside the newly landed VR utilities and performance HUD.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-29 12:09:04 +10:00
saphidandGPT-6 Astra 8fca0fe0a2 Merge origin/main into apk-store-fixes, preserving store and headset features
Keep the union of server routes, desktop resources and responsive controls. Preserve OpenXR install defaults and telemetry hooks alongside library artwork. Adapt the resource test to single-file entries and avoid a completed-refresh race in the F-Droid test.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-29 12:07:09 +10:00
Alex Southwell 6d3cde64fe Merge pull request #43 from saphid/vr-utilities
feat: add OpenVR performance HUD and optional VR utilities
2026-09-29 12:05:38 +10:00
saphidandClaude Opus 5.5 3f280ba59a Merge main into vr-utilities: the performance HUD alongside comfort, keyboard, panels and media
Also from review: a SteamVR build without the timing exports can't break status
(AttributeError), and the device test class runs when the file is run directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:59:15 +10:00
Alex Southwell 83fa5c6c5b Merge pull request #41 from saphid/panel-workspaces
Add panel switchers and document workspace feasibility
2026-09-29 11:56:15 +10:00
saphidandClaude Opus 5.5 094c12c6d8 Keep media playing through headset standby
Real-Frame testing (2026-09-29) found an unworn headset enters standby
within seconds; SetOverlayRaw then returns RequestFailed (23) and the
movie died. The player now drops frames during standby, keeps audio
and pacing, re-sends stills and the theatre surround after waking, and
only errors after five minutes without an accepted frame.

A Stop arriving while the player is already shutting down is ignored,
so a finished video stays 'ended' instead of 'error: Stopped'. The
status now reports the layout's real source (filename/metadata).

Docs record the end-to-end device matrix (API, web UI, CLI).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:54:05 +10:00
saphidandClaude Opus 5.5 3ef7312654 Merge main into panel-workspaces (testing notes)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:50:17 +10:00
Alex Southwell 70e7f7f5e5 Merge pull request #39 from saphid/family-comfort
Family and comfort: safe timers, casting, alerts and breaks
2026-09-29 11:38:47 +10:00
saphidandClaude Opus 5.5 2bd86207c7 Merge main into panel-workspaces: the panel switcher alongside media, analytics and the Mac view
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:37:18 +10:00
saphidandClaude Opus 5.5 be1ab129c9 Tests: read the page as UTF-8 (Windows' default codec can't read its arrows)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:33:34 +10:00
saphid a38d0cda6e Document shared Frame test procedure and blocked recheck 2026-09-29 11:16:29 +10:00
saphidandClaude Opus 5.5 c46bf68dd0 Record real-Frame MCP end-to-end results, including approved mutations
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:15:07 +10:00
Alex SouthwellandClaude Opus 5.5 e702adbd77 Live view: a Desktop view that stays still, and Control to tap on the Frame (#46)
* Live view: a Desktop view that stays still, and Control to tap on the Frame

The live view gets a second source and a way to use the Frame from it:

- Desktop: the app panel in use in the headset, streamed from its own window
  (x11grab of gamescope's redirected window), so it doesn't move as the
  wearer looks around. A picker shows any other panel, view only.
- Control: on the Desktop view a tap or click lands exactly where you put it;
  drag is a mouse drag, press and hold right-clicks, two fingers scroll, and
  on a computer the mouse, wheel and keyboard work directly. On the headset
  view the view is a trackpad. A text field and key row type from a phone.

Input goes through gamescope's own EIS socket (the way Steam feeds Remote
Play input) with the libei already on the image: ui/frame_touch.py, over
the same long-lived ssh machinery as the keyboard agent, nothing to
install. It reaches the panel that has focus on either X display, which
the KDE Connect route can't. Verified on the Frame and from the iPhone app
in the Simulator.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Control: fixes from review

- Keys held on the Frame are released with buttons when Control stops or
  the view loses focus; keys for the Frame no longer trigger Frame Control's
  own shortcuts.
- Taps only act when the picture on screen is the panel in use; positions,
  presses, keys, text and scrolls name their panel (display and window: ids
  repeat across :0 and :1, told apart by pid), and the Frame drops them if
  focus has moved on. Releases always go.
- While connecting, a tap keeps its position; on an error only releases wait
  and retries back off; trimming a long queue never drops a release.
- Lifting one of two scrolling fingers ends the scroll; a cancelled touch
  isn't a tap; clicks and holds on the bars around the picture do nothing.
- A capture loop from before a Live restart can't stop the new video.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Control: close the targeting gaps from the second review

- The focused panel's display comes from GAMESCOPE_FOCUS_DISPLAY (gamescope
  packs ":1" into the first value), so a window id repeated across :0 and :1
  can't be mistaken; the pid is only the fallback.
- Presses, keys, text and scrolls read focus afresh on the Frame; only moves
  use a reading up to a second old.
- A gesture remembers the panel it started on and does nothing more if that
  stops being the one in use; a press with no panel to aim at isn't sent.
- Opening a screenshot clears the panel Control would act on; switching to
  another app releases held keys and buttons.
- Trimming keeps a click with its position; the error backoff holds for new
  input too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Control: fixes from the SWE-2 Max review

- The Frame side tracks keys as well as buttons and lets go of both when the
  session ends.
- A stale tap tells the page, which re-reads the panels at once.
- A paused input device waits instead of ending the session; only a
  disconnect does. An OS error on one event skips it.
- Presses check focus with two property reads and do the full lookup only
  when it changed.
- Writes to an agent's stdin are serialized, so two devices sending at once
  can't tear a line (the keyboard agent too).
- Connecting gives up with a message after 15 s instead of hanging on
  "Connecting…"; text goes in 100-character pieces so releases don't wait
  behind a long paste; a cancelled mouse gesture releases what's held.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Control: stale clears, pauses reconverge, pastes split per request

- The Frame says it has caught up as soon as an aimed event lands after a
  stale one, so the page stops re-reading the panels.
- After a device pause it lets go of everything it holds (releases that
  arrived while paused were dropped), and waits for the device once per
  batch, not once per event.
- The quick focus check no longer freshens the panel geometry's age.
- Each request carries at most about 100 characters of text.
- Turning Control off while it connects doesn't report an error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* frame_touch: build the socket path on the Frame, so Windows can import it for tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* frame_touch: any event that goes through clears the stale flag

A trackpad move names no panel, so waiting for an aimed event could leave
the page re-reading panels for the rest of the session; a release still
aimed at the old panel doesn't count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:13:21 +10:00
saphidandClaude Opus 5.5 ab1985b6b3 Comfort: a state missing 'started' can't crash status (timestamps of 0 still count)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:12:41 +10:00
saphidandClaude Opus 5.5 1203ec0a9e Merge main into family-comfort; a session state without 'started' can't crash status
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:09:35 +10:00
saphidandClaude Opus 5.5 08fbe730c6 Merge main into devices: switching headset stops the keyboard agent and retargets the Mac view
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:37:45 +10:00
saphidandClaude Opus 5.5 1cf353f419 Tests: give Windows time to refuse a closed loopback port
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:34:11 +10:00
Alex Southwell ef56025ad1 Merge pull request #19 from saphid/frame-input
Keyboard and trackpad for the Frame, through its own KDE Connect
2026-09-29 10:33:35 +10:00
Alex Southwell 697452e5a9 Merge pull request #42 from saphid/theatre-media
Add owned Frame-side theatre and stereo media previews
2026-09-29 10:27:40 +10:00
saphidandClaude Opus 5.5 4bb7a1ee86 Merge main into frame-input: keyboard and trackpad alongside analytics, the Mac view and MCP
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:25:26 +10:00
saphidandClaude Opus 5.5 c0183ca8b2 Tests: the private ControlPath is per headset too
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:22:33 +10:00
saphid ae7f733b90 Merge remote-tracking branch 'origin/main' into theatre-media
# Conflicts:
#	ui/server.py
2026-09-29 10:21:27 +10:00
saphidandClaude Opus 5.5 bf8a478063 Devices with the Mac view and the MCP adapter: the tunnel follows the headset, a private server runs alongside
The Mac view's tunnel is its own ssh, so it now takes the headset's route (and its
pinned identity, which also checks the USB-C address), and closes when the app
switches headset. The MCP adapter's private server (FRAME_PRIVATE_SSH=1) skips the
one-server lock and can't add, remove or switch headsets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:19:48 +10:00
saphidandClaude Opus 5.5 b00db2484d Keep the backslash upload-name test POSIX-only
Windows treats the backslash as a separator, so that name can't occur there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:15:37 +10:00
saphid 215bc357ef Merge remote-tracking branch 'origin/main' into devices 2026-09-29 10:14:51 +10:00
saphidandClaude Opus 5.5 7d6ff7f919 Merge main into devices: MCP, analytics, Mac view alongside several headsets
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:14:50 +10:00
Alex Southwell 363497fdd5 Merge pull request #20 from saphid/vr-apks
Run Quest and other VR APKs on the Frame: VR detection, launcher fix, OpenXR 1.1 compatibility layer
2026-09-29 10:13:11 +10:00
Alex Southwell ff52f50c0c Merge pull request #22 from saphid/mac-in-headset
Mac in the headset: Mac windows as panels, measured and adapting to the network
2026-09-29 10:12:46 +10:00
saphid b0d6039eec Merge remote-tracking branch 'origin/main' into theatre-media
# Conflicts:
#	docs/testing.md
#	ui/server.py
2026-09-29 10:08:45 +10:00
saphidandClaude Opus 5.5 48158d1fe9 Merge main into mac-in-headset (README index)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:05:27 +10:00
saphidandClaude Opus 5.5 86b8ab1d82 Assert the start-failure test reaches systemd-run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:01:17 +10:00
saphidandClaude Opus 5.5 e288946b81 Merge main into vr-apks: VR installs report through install_hooks like every other install
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:58:55 +10:00
Alex Southwell 66b6d46e0e Merge pull request #37 from saphid/flat2vr-mods
docs: record VR mod feasibility and own-manager requirements
2026-09-29 09:57:59 +10:00
saphidandClaude Opus 5.5 d970107716 Merge main into mac-in-headset: the Mac view alongside analytics and Report a problem
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:57:35 +10:00
Alex Southwell 6fbc450eea Merge pull request #17 from saphid/analytics-and-updates
Analytics, self-update and Report a problem
2026-09-29 09:55:12 +10:00
saphidandClaude Opus 5.5 1343900aa1 Devices: placeholder IPv6 in a validation test
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:42:15 +10:00
saphidandClaude Opus 5.5 d2a5db7caf Devices: no real tailnet addresses in tests or screenshots
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:42:04 +10:00
saphidandClaude Opus 5.5 f7573e3565 Merge main into mac-in-headset (MCP agent routes alongside the Mac view); skip the bash syntax test on Windows
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:41:24 +10:00
Alex Southwell 4f6d40625a Merge pull request #36 from saphid/linux-vr-streaming
docs: Linux VR feasibility and first-party streaming options
2026-09-29 09:39:01 +10:00
saphid c6ed6c9ea1 Merge remote-tracking branch 'origin/main' into analytics-and-updates
# Conflicts:
#	docs/frame-control.md
#	ui/server.py
2026-09-29 09:38:44 +10:00
saphidandClaude Opus 5.5 020e3386e1 Make media stop race-free and cover start failures
Stop always calls systemctl and treats exit 5 (unit already collected)
as done, so there is no is-active/stop race. Cleanup never masks the
copy error, the play ssh timeout covers the remote worst case, and
tests cover stop exit codes and systemd-run stderr reporting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:30:03 +10:00
Alex Southwell 6d03317970 Merge pull request #15 from saphid/docs-announcements
Docs: house style for announcing features and fixes
2026-09-29 09:26:22 +10:00
saphidandClaude Opus 5.5 f81f70ed5d Fix media stop, upload cleanup and review findings
- Stop is a no-op when the collected player unit is already gone
  (raw systemctl stop exits 5 on the Frame; verified 2026-09-29).
- Surface systemd-run stderr when the player can't start.
- Keep the copy error if the cleanup ssh also fails; reject upload
  names that the play path can never accept.
- Allow 60 s for play (ffprobe 30 s + systemd-run 15 s remote).
- Docs: four-hour cap is unconditional; no delete action yet; fix a
  garbled timing sentence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:22:22 +10:00
Alex Southwell 976008065f Merge pull request #16 from saphid/keep-awake
Keep the Frame awake during agent work
2026-09-29 09:16:03 +10:00
Alex Southwell 8b5ada1272 Merge pull request #35 from saphid/frame-mcp
Add Frame Control MCP tools and an opt-in assistant panel
2026-09-29 09:04:49 +10:00
saphidandClaude Opus 5.5 48a9914124 Skip reverse-DNS lookup when binding the loopback server
HTTPServer.server_bind calls socket.getfqdn, which stalled past the MCP
backend's 10-second startup window on GitHub's macOS runners.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:54:17 +10:00
saphidandClaude Opus 5.5 a08ba6f65b Docs: screenshots of the Devices tab and the connection pill
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:51:44 +10:00
saphidandClaude Opus 5.5 53c51e1888 Devices: restarting during startup starts afresh; a headset capture keeps its headset through clean-up (review round 33)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:42:19 +10:00
saphidandClaude Opus 5.5 72e4ccf080 App: overlapping restarts and server starts share one (review round 32)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:35:19 +10:00
saphid 222d5eccc9 fix(comfort): harden timer recovery and notification UX after review 2026-09-29 08:31:00 +10:00
saphidandClaude Opus 5.5 a9740ad6f2 Devices: the app waits for its old server before starting the new one; clipboard sends keep their headset (review round 31)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:27:52 +10:00
saphid 002c859572 Set up self-contained MCP startup and inspect Frame computer-use capabilities 2026-09-29 08:18:47 +10:00
saphidandClaude Opus 5.5 cb6f294299 Devices: one Frame Control server per user
Two servers each connected, reconnected and edited the headsets on their own,
and several review findings were ways one could move the other's install to a
different headset. A lock file in the data folder now refuses a second server
with a plain message; FRAME_CONTROL_DATA_DIR still gives a separate one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:17:43 +10:00
saphidandClaude Opus 5.5 2c8e2fb2a8 SteamGridDB: request PNG only for logos and icons
The live API rejects mimes=image/jpeg on /logos and /icons, so every logo and
icon lookup fell back to generated art. Found with a real key: SuperTux now
gets grid, wide, hero and logo; Beat Saber all five.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:08:08 +10:00
saphidandClaude Opus 5.5 b87be6866b Devices: while an install runs, nothing elsewhere moves it to another headset (review round 29)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:08:04 +10:00
saphidandClaude Opus 5.5 747dbcf72f Devices: route to the saved headset before serving; a headset removed elsewhere mid-install reaches nothing (review round 28)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:01:32 +10:00
saphidandClaude Opus 5.5 409e328880 Devices: each headset its own SSH connection, and each server keeps its own headset (review round 27)
ssh's %C hashes only address, user and port, so two headsets reached at one
address shared a ControlMaster and one's commands could run on the other: the
ControlPath now names the headset. Another Frame Control server choosing a
different headset no longer moves this one's commands mid-install.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 07:52:28 +10:00
saphidandClaude Opus 5.5 c5a614d989 Devices: two servers sharing devices.json can't save over each other's changes (review round 26)
Every change now takes a lock file shared across processes and starts from
what's on disk; reads pick up a newer file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 07:42:46 +10:00
saphidandClaude Opus 5.5 0f33b4f094 Devices: saving port 22 overrides a port inherited from a later Host entry (review round 25)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 07:35:07 +10:00
saphidandClaude Opus 5.5 49378b2c80 Devices: fixes from review round 24
- While the connector is taking a queued reconnect off its list, the old
  connection no longer counts as live, so no install starts on it.
- Importing a block without a Port takes the port ssh would really use
  (ssh -F <config> -G), e.g. one a later Host * sets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 01:13:15 +10:00
saphidandClaude Opus 5.5 22863f2f87 Devices: match the host in ssh's login line case-insensitively (review round 23)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 01:01:29 +10:00
saphidandClaude Opus 5.5 b779376f5b Devices: fixes from review round 22
- An upload's answer arriving after a switch opens nothing; the APK
  alternatives dialog installs on the headset the APK was checked for.
- A handshake that goes silent (e.g. a jump host's forward hanging) moves on
  to the next address.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:53:18 +10:00
saphidandClaude Opus 5.5 8bd8d63546 Devices: fixes from review round 21
- Behind a jump host, a forward it couldn't open moves on to the next address;
  only a refused key stops (judged by ssh's words, not the step).
- Add a headset suggests an alias no Host in ~/.ssh/config already uses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:44:18 +10:00
saphidandClaude Opus 5.5 42b51afc5a Devices: fixes from review round 20
- A jump host's own "Authenticated to" line no longer counts as the headset's,
  and a master that logs in but doesn't start lets the next address be tried.
- Devices tab changes refresh through the ordered list load, so a late answer
  can't undo a newer selection.
- A probe's time out starts after the name lookup: macOS can take 5 s to look
  up a .local name (found on the real Frame once its USB link went away).
- An attempt's ending is published from a method, not a return in finally.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:35:29 +10:00
saphidandClaude Opus 5.5 6597a90059 Devices: fixes from review round 19
- A switch the server refuses no longer drops answers the page is waiting for
  (an install's job id): only an actual change of headset does.
- Test now goes through a jump host when the alias uses one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:25:10 +10:00
saphidandClaude Opus 5.5 c3e3f2629c Devices: fixes from review round 18
- A set-up headset whose alias goes through a jump host (ProxyJump or
  ProxyCommand in ~/.ssh/config) is reached through it, address by address,
  still pinned per headset.
- A refused switch puts the header's switcher back on the headset in use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:17:29 +10:00
saphidandClaude Opus 5.5 d18f1655be e2e: the app icon lives under artwork/ now
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:08:44 +10:00
saphidandClaude Opus 5.5 90fd2684ba Devices: fixes from review round 17
- A command that fails after a switch doesn't make the connector drop the new
  headset's connection.
- On first import, the app keeps using the `frame` headset even when Set Up
  Connection put another block above it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:08:36 +10:00
saphidandClaude Opus 5.5 fa05a4b310 Devices: fixes from review round 16
- Terminals, power and a reconnect's probes use the route commands have now
  (a pinned bare-alias destination, a login change still deferred).
- Saving port 22 keeps an explicit Port line where there was one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:00:00 +10:00
saphidandClaude Opus 5.5 f81c98a87b Fix CI: title removal order, Windows fixtures and POSIX-only tests
- Title removal ran the Steam shortcut tidy-up before steamos-delete, which
  finds the Proton prefix through that shortcut, so compatdata was left behind
  (e2e caught it). steamos-delete runs first again; art/collection tidy-up after.
- Test fixtures are byte-exact: never convert line endings (a text-looking
  fixture APK got CRLF on Windows and failed its SHA-256).
- Read index.html/artwork-settings.js as UTF-8 in tests; app-data backup and
  OBB shell tests run only on POSIX (they exercise the Frame-side scripts).
- e2e expects the icon under artwork/ now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:59:44 +10:00
saphidandClaude Opus 5.5 93ebd34f2c Devices: fixes from review round 15
- A bare alias's route is pinned to where ~/.ssh/config sent it when it was
  routed (HostName, Port, User), so editing that file can't move an install.
- Renaming during an install is allowed: only a real user or port change waits.
- The Devices tab follows a network change even while the headset is offline.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:51:12 +10:00
saphidandClaude Opus 5.5 cb05395280 Artwork fetch: release the DNS slot if its thread can't start; stricter GIF control blocks
Final review follow-up. A failed Thread.start() leaked a resolver slot (four
failures disabled artwork lookups). GIF graphic-control blocks must have the
fixed 4-byte payload (otherwise dropped) and an image with no pixel data is
rejected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:48:20 +10:00
saphidandClaude Opus 5.5 34e91988b1 Devices: fixes from review round 14
- Retry now (while connected) and Forget identity wait for running installs.
- Terminal windows get the headset's address by name, so a link-local IPv6
  zone never has to pass through Windows' console.
- Renaming the headset in use shows at once in the header.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:40:10 +10:00
saphidandClaude Opus 5.5 3b36feff52 Keep worker notes and proof logs out of the repository
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:38:54 +10:00
saphidandClaude Opus 5.5 1b5f540a66 Bulk fill-only refresh passes fill_only on to each app and title
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:37:42 +10:00
saphidandClaude Opus 5.5 47266afe85 Artwork fetches: TLS handshake within the deadline; cap stuck name lookups
The handshake runs after the watchdog can reach the TLS socket, with the
remaining time as timeout, and the watchdog shuts the socket with the plain
socket method. At most four lookups that outlived their deadline may run; more
fail at once with a clear error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:37:42 +10:00
saphidandClaude Opus 5.5 b9714fda45 Artwork GIFs: parse the blocks and pass on the first frame only, bounded
The screen and first frame must be at most 4096x4096 and the frame inside the
screen; anything malformed or truncated is rejected. Only a minimal
single-frame GIF (header, screen, colour table, graphic control, first
image) reaches the Frame's Chromium, however many frames the source has.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:37:42 +10:00
saphidandClaude Opus 5.5 a045f5479f Android launcher: drop process-group reaping; orphan recovery stops only the app's own container
Matching the APK path in command lines could hit an unrelated process, and
the pgid file had a registration race. The launcher keeps the flock (not
inherited by Lepton) and, holding it, stops only lepton-steamlaunch-<instance>,
whose name is this app's alone. A Lepton host process may linger briefly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:37:42 +10:00
saphidandClaude Opus 5.5 5874fe33f6 Devices: fixes from review round 13
- Every command to a set-up headset checks its pinned key
  (StrictHostKeyChecking=yes, whatever ~/.ssh/config says); only the
  connector's first handshake may save one.
- A reconnect during an install keeps the whole route it started with, also
  when a bare alias is set up meanwhile.
- Removing the headset FRAME_ALIAS named doesn't bring it back as a bare alias.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:31:22 +10:00
saphid 2f9ddeea76 Merge branch 'art-sources' into apk-store-fixes 2026-09-28 23:23:44 +10:00
saphid 4589221661 Merge branch 'library-fixes' into apk-store-fixes 2026-09-28 23:23:44 +10:00
saphidandClaude Opus 5.5 7af5916378 Docs: backfill fills only pending entries; launcher reaps a killed launch's group
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:23:27 +10:00
saphidandClaude Opus 5.5 1b39fc1718 Library backfill only fills art Frame Control couldn't apply; remove serialised with refresh
- Automatic backfill touches only entries marked art_pending at install (a
  devkit title Steam registered later) and fills only slots Steam has no art
  for: no name, exe, VR flag or icon changes, no clearing. Older installs
  without the flag are left alone and refreshed only when the user asks.
- Android remove takes the install lock that install and refresh hold, so a
  refresh in progress can't recreate a removed app; a refresh after removal
  finds it not installed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:23:20 +10:00
saphidandClaude Opus 5.5 2df0f0e32a Tests: put LocalMode's Windows skip back; artwork settings tests run everywhere
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:23:20 +10:00
saphidandClaude Opus 5.5 d7cb967786 Artwork fetches: the deadline bounds the whole request, trickling servers included
Name resolution runs in a thread within the budget, a watchdog shuts the
socket at the deadline, and the body is read one receive at a time with the
remaining time as timeout. SteamGridDB goes through the same bounded fetch,
without redirects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:23:20 +10:00
saphidandClaude Opus 5.5 f7bc2a8f68 Android launcher: reap a previous launch's Lepton left by a SIGKILLed launcher
The lock isn't inherited by Lepton, so a launcher killed before Lepton made its
container left an untracked Lepton that a new Play could overlap. The launcher
records its child's process group and, once it holds the lock, ends a recorded
group that is still running this app.apk (never an unrelated reused id).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:23:20 +10:00
saphidandClaude Opus 5.5 98ef6944c9 Devices: fixes from review round 12
- A reconnect while an install runs keeps the login it started with; a new
  one from ~/.ssh/config applies after.
- Frame > Open SSH goes through the server, so it uses the same headset and
  address as the app and refuses when there's none.
- A bare frame alias in use when a headset is set up stays selectable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:21:19 +10:00
saphidandClaude Opus 5.5 2e9bc8624b Devices: fixes from review round 11
- A headset set up while a bare alias is in use doesn't take over by itself;
  a login change from ~/.ssh/config waits for running installs.
- Saving a headset writes only the login fields that changed, and only if the
  block still holds the old ones.
- SSH, SFTP, power and remote desktop open with the same headset and address
  as every other command, and refuse when there's no address.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:12:41 +10:00
saphidandClaude Opus 5.5 b5caee5b86 Library art: real gameplay instead of GitHub social cards; accept GIF sources
GitHub's opengraph preview is repo text, stats and an identicon; as a Steam hero
it looked broken. GitHub entries no longer default to it (the store draws its
fallback, Steam gets generated art). Source GIFs (common gameplay captures) are
accepted; the Frame's Chromium draws the first frame. Open Saber Plus uses its
gameplay GIF as banner. Verified on the Frame: hero/wide now show gameplay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:12:24 +10:00
saphid 7990553620 Merge branch 'library-fixes' into apk-store-fixes 2026-09-28 23:09:14 +10:00
saphidandClaude Opus 5.5 6c1ece1b62 Docs: library artwork limits, backfill for titles, and Steam's missing devkit_gameid (checked on the Frame)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:08:40 +10:00
saphidandClaude Opus 5.5 fde2f9620a Library artwork backfill: devkit titles in refresh-art; missing art offered and re-applied
- 'Refresh artwork' (settings) and the API's refresh-art --all cover devkit
  titles as well as Android apps; frame_titles.py gains refresh-art ID|--all.
- Apps and titles without complete Steam artwork are flagged (art_missing):
  the app shows 'Add artwork', and the CLIs' list prints the refresh command.
- When the app lists them and Steam answers, Frame Control re-applies their
  art in the background (at most every five minutes), e.g. for a title Steam
  registered after an install made while it wasn't running.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:06:48 +10:00
saphidandClaude Opus 5.5 f0db805d4b Steam library: safe removal and title matching, artwork within Steam's limit
- Remove: collections and artwork clearing are best effort in Steam's JS, and
  the host carries on to delete the files when Steam isn't running (Android
  apps and devkit titles).
- Devkit titles: the shortcut is found by devkit id, the saved id, or an
  executable/start folder inside the title's folder; never by display name.
  Steam's overviews don't carry devkit_gameid (checked on the Frame
  2026-09-28), so 'list' now reads exe/start dir from app details.
- Photo-based grid/wide/hero slots render as JPEG (a noise-heavy 3840x1240
  hero was over 12 MiB as PNG on the Frame); the logo stays transparent PNG.
  Rendering gets 75 s and retries once with generated art. Each slot is
  cleared before it is set, since Steam keeps .png and .jpg side by side.
- Devkit titles keep their own VR flag (vr=None skips SetShortcutIsVR) and
  their Sideloaded collection.
- A failed title install's cleanup can't replace the original error.
- refresh_art for devkit titles (frame_titles.refresh_art).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:06:25 +10:00
saphidandClaude Opus 5.5 baefa105a9 Artwork sources: every optional source falls back, within limits
- Any failure of a source image or SteamGridDB (HTTPException, odd JSON) now
  becomes a warning and generated art, never an aborted install; refresh-art
  --all reports each app and carries on.
- URL artwork goes through apk_sources._images: public addresses only, at most
  three redirects, and one overall deadline for all of an install's fetches.
- PNGs are checked from their header only (any depth or interlace; Steam's
  Chromium decodes them), JPEGs may have trailing padding, and 4K screenshots
  are within limits. The slow pure-Python decoder is gone.
- SteamGridDB title matching keeps letters of every script and never matches
  on an empty name. One warning per source slot, not per candidate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:06:15 +10:00
saphidandClaude Opus 5.5 70897cfd1d Android launcher: stop an orphaned container instead of refusing Play; keep the lock out of Lepton's tree
Once flock is held no launcher owns a running container (a SIGKILLed launcher
left it), so it is stopped and the launch continues. fd 9 is closed for the
Lepton child so it can't keep the lock held.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:06:04 +10:00
saphidandClaude Opus 5.5 060801c674 Artwork settings: send the UI key through api(), so the panel and refresh work
Every /api call needs X-Frame-UI; the panel's own fetch() got 403s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:06:04 +10:00
saphidandClaude Opus 5.5 c69ea6266f Devices: fixes from review round 10
- Removing or moving the active headset's address waits for running installs,
  like switching.
- A volume change still waiting to be sent goes to the headset whose slider
  it was, and a switch cancels it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:03:27 +10:00
saphidandClaude Opus 5.5 829897087a App data: test overlapping restore cleanups; skip the flock test off POSIX
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:56:14 +10:00
saphidandClaude Opus 5.5 a97e8a6183 Store: close second-round races in F-Droid loads, APK reuse and pruning
- The locked publication step also refuses a v1 index once v2 was accepted, so
  an overlapping v1 fallback can't replace a v2 cache at an equal timestamp.
- A cached APK is touched before hashing; if it vanishes, it's downloaded again.
- Only the app prunes (at start and after store downloads), since claims are
  in-process; the CLIs never prune.
- The CLI joins background refreshes on error exits too.
- The Windows lock loop retries only contention errors.
The concurrent-publication test now uses real flock contention.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:56:14 +10:00
saphidandClaude Opus 5.5 43e19d17f6 Devices: fixes from review round 9
- A title waiting its turn to be read stays with the headset it was dropped
  on, and is dropped if the app switches meanwhile.
- Probes still finishing from an earlier attempt can't overwrite the rows of
  a newer one.
- The FRAME_ALIAS the server started with stays on the list after switching
  away, so it can be picked again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:55:14 +10:00
saphid 08c306c3a6 docs: record CI results and incomplete independent review 2026-09-28 22:51:03 +10:00
saphid 4d4e45f622 Pin fake Frame data directory modes 2026-09-28 22:46:00 +10:00
saphidandClaude Opus 5.5 175c39a2b0 Devices: fixes from review round 8
- A batch of dropped files stays with the headset it was dropped on, and
  stops if the app switches.
- Removing or moving the address in use reroutes at once; a headset with no
  addresses reaches nothing rather than whatever ~/.ssh/config says.
- A late answer to an older device-list request is ignored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:45:34 +10:00
saphidandClaude Opus 5.5 8315c7c3aa Merge vr-library (Steam library art, Play/Stop, refresh-art) into the store
Resolve CLI usage and POST table conflicts. Store installs now hand the
source's own image URLs (icon, banner, screenshots) to frame_android.install
as Steam artwork; before, they passed UI proxy paths (or nothing), so every
store install fell back to generated art.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:45:31 +10:00
saphid 6ecd0cea39 Match fake Frame user data ownership to the headset 2026-09-28 22:44:14 +10:00
saphid 6a63a5a597 docs: record VR device evidence and paused comfort controls 2026-09-28 22:41:54 +10:00
saphid 3fb541dce7 feat: add OpenVR performance HUD and optional VR utilities 2026-09-28 22:41:45 +10:00
saphid c335cd5130 Fix media test portability and e2e discovery 2026-09-28 22:37:28 +10:00
saphidandClaude Opus 5.5 51ef5d8283 Devices: fixes from review round 7
- Removing every headset leaves none in use (commands fail at once) instead of
  falling back to the `frame` alias.
- ssh goes to the IP that answered for IPv6 too, with a link-local address's
  interface (verified: frame.local over fe80::…%en9 on the real Frame).
- Find results only show in the panel of the headset they were for.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:35:43 +10:00
saphid 91c5853627 Refresh panel switcher screenshot from final device check 2026-09-28 22:34:53 +10:00
saphid 0d448ab510 Add owned OpenVR media playback and stereo previews 2026-09-28 22:33:50 +10:00
saphid 72352c5914 Add companion and headset panel switchers with feasibility evidence 2026-09-28 22:33:23 +10:00
saphid d3fa282377 docs(comfort): include verified desktop and iPhone notification evidence 2026-09-28 22:32:54 +10:00
saphidandClaude Opus 5.5 6c41a341e5 App data: serialise restores of a package with a lock beside its data
Two clients restoring the same package could each swap directories and then
delete the other's pre-restore copy. The swap and retention cleanup now run
under flock on .<package>.restore.lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:30:08 +10:00
saphid 0622afcae9 feat(ui): add family controls, casting and native notifications 2026-09-28 22:29:58 +10:00
saphid 522c46ed6f feat(comfort): run safe session timers and alerts on the Frame 2026-09-28 22:29:58 +10:00
saphidandClaude Opus 5.5 a7261b1601 Store: pruning rechecks each APK before deleting and spares ones being installed
Deletion re-stats under a lock shared with touch() (F-Droid cache reuse) and
claim()/release() (held by the store around install), so a reused or
installing APK is never removed from an out-of-date scan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:29:39 +10:00
saphid b5cf8253e6 Bind approval UI to current request and verify panel cleanup 2026-09-28 22:29:18 +10:00
saphidandClaude Opus 5.5 4fd8bb79af Store: publish a search's completion and hand over its queue atomically
A query arriving between the queue handover and the completion event could be
queued with nobody to start it, leaving the source 'loading' forever.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:28:45 +10:00
saphidandClaude Opus 5.5 5ac109c381 F-Droid: inter-process lock for index publication; CLI waits for refreshes
The final timestamp recheck, cache write and state update now run under a file
lock (flock, or msvcrt on Windows), so the CLI and the app can't publish
indexes out of order. The CLI joins background refreshes before exiting so an
expired index doesn't stay expired.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:28:28 +10:00
saphid beccfec307 docs: record Frame mod feasibility and manager requirements 2026-09-28 22:25:05 +10:00
saphidandClaude Opus 5.5 ba33d2ff40 Devices: fixes from review round 6
- The headset a change is meant for is checked and the work counted in one
  step, so a switch can't slip in between (uploads too).
- A sideloaded title read on one headset can't be installed on another; open
  confirmations close on a switch.
- The only headset can't be removed while its ssh alias stays behind.
- Answers about the previous headset are dropped without touching panels; the
  catalogue's Installed tags are rebuilt for the new headset.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:24:46 +10:00
saphid a6137351d9 docs: record Linux VR client blockers and streaming options 2026-09-28 22:23:19 +10:00
saphid 6a8e3fadbf Open assistant on Frame and document verified agent workflows 2026-09-28 22:21:22 +10:00
saphid 643cb65c79 Add key-free MCP tools, human approvals and opt-in assistant 2026-09-28 22:21:22 +10:00
saphidandGPT-6 Astra f695f398de Render complete Steam artwork for every sideload and fix VR shortcut identity
Add optional SteamGridDB settings, source-first fallbacks rendered with Steam canvas, backfill commands and shared APK/native library details. Verify live artwork and Open Saber Steam Play/Stop; document SuperTux's clipboard crash.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 22:20:27 +10:00
saphidandClaude Opus 5.5 5000fa4147 App data: skip symlinks into the backup manifest; keep one pre-restore copy
Backups no longer abort on a symlink: it is left out and listed (path and
target) in manifest.json, now written last. A hard link is stored as a copy of
its file. Restore removes older pre-restore copies of the same package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:14:25 +10:00
saphidandClaude Opus 5.5 41ac28248a Devices: fixes from review round 5
- A switch publishes the new headset at once, so the page clears the old one's
  panels and the lists behind them (games, store, Android apps, screenshots).
- The page names the headset its changes are for (X-Frame-Device); the server
  refuses one meant for a headset it has switched away from (409).
- A rejected address edit changes nothing.
- Test now goes to the IPv4 address that answered, like the connection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:14:25 +10:00
saphidandClaude Opus 5.5 9b0fedf602 Store: OBB game data becomes a follow-up 'Add game data' step
install_obb needs the app's running instance, which doesn't exist straight after
install, so the store no longer calls it there. The install result says the app
needs its game data; after opening the app once, 'Add game data' copies the
downloaded OBB files (a background job).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:13:42 +10:00
saphidandClaude Opus 5.5 a9d78679ec Store: add repositories in a background job and show the TOFU fingerprint
Adding a repository downloads and verifies its whole index, so it now runs as a
job (runJob in the UI) and reports 'Trusted on first use: <fingerprint>' when
no pin was given. fdroidrepos:// links pass the server check, as documented.
Jobs report SourceError messages without a 'SourceError:' prefix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:13:04 +10:00
saphidandClaude Opus 5.5 ddcf3b2ad2 Store: prune download caches (2 GB LRU for APKs, orphaned .part/temp, old listings)
Runs after each APK download and at server start. APKs used in the last hour
are kept; an F-Droid cache hit refreshes the APK's mtime.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:12:02 +10:00
saphidandClaude Opus 5.5 029c92bccb F-Droid: serve an expired index as stale while refreshing in the background
Searches no longer wait for (or fail on) a refresh of an expired index; the
store notes which sources show saved listings. A failed refresh keeps the old
index and is retried after 10 minutes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:11:04 +10:00
saphidandClaude Opus 5.5 7c439fdf28 Store: per-host backoff after 403/429 honouring Retry-After
A host that answers 403/429 is left alone until its Retry-After (or GitHub's
rate-limit reset; default 10 minutes). Meanwhile cached data is served, or the
source reports 'limited' with its own name, e.g. 'GitHub is limiting requests;
try again in 10 minutes'. Covers _web reads/downloads and F-Droid fetches.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:09:55 +10:00
saphidandClaude Opus 5.5 c68afa6c5d F-Droid: refuse index rollbacks, v1 downgrades after v2, and SHA-1 entry.jar
Each repository's newest accepted index timestamp is stored and older indexes
are refused. index-v1.jar is only a fallback while no v2 index has been
accepted. entry.jar must use SHA-2; the recorded IzzyOnDroid entry.jar is
SHA-256 and still verifies. Tests sign JARs with a throwaway key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:08:26 +10:00
saphidandClaude Opus 5.5 328b7ed211 F-Droid: one load lock per repository; downloads never hold the settings lock
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:06:51 +10:00
saphidandClaude Opus 5.5 3149a6e269 Store: newest query runs after a source's current search; no source calls under the search lock
A search for a different query while a source is busy now queues (newest wins)
instead of being dropped, and warm() uses the browse limit so the first browse
reuses it. set_enabled calls the source before taking search._lock. A
SourceLimited error reports the source as 'limited'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:06:20 +10:00
saphidandClaude Opus 5.5 ea73145fbc Store: unknown VR counts as flat; browse keeps unknown-fit VR first
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:05:50 +10:00
saphidandClaude Opus 5.5 69f790b83a Store: real-source fixes found by running search against live repos
- F-Droid: percent-encode repo file names (a '#' in one screenshot name broke
  the whole main repo); a bad image name drops that image, not the app.
- Search: sources still fetching report 'loading' (UI says so and refreshes
  quietly); indexes warm up at server start; page-only SideQuest is not
  searched and appears as a 'Browse SideQuest' link instead of an error.
- Browse (empty query) ranks VR, artwork and recent updates first; the F-Droid
  archive is off by default (old versions only).
- Throttled sources fall back to their last cached copy; per-host message.
- Curated GitHub list gains Open Saber Plus (MIT) with icon and screenshots.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:02:38 +10:00
saphidandClaude Opus 5.5 1b90c64b73 Docs: benchmark with the headset worn (Wi-Fi much rougher; controller bounded latency)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:02:05 +10:00
saphidandClaude Opus 5.5 9dd57cde4e Devices: connector tests follow ssh to the IPv4 address that answered
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:01:49 +10:00
saphidandClaude Opus 5.5 41f08ac9a2 Bundled KDE Connect: one safety net for every launch failure
Any unexpected error while launching clears the launch and reports it, so
the pad can always be started again; the temporary stderr file is made
inside the handled path and a failure reading it is tolerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:01:09 +10:00
saphidandClaude Opus 5.5 d383a26746 Devices: fixes from review round 4
- A switch clears every headset-specific list and its buttons at once.
- SSH goes to the IPv4 address that answered the probe, not the name again.
- A rejected headset edit changes nothing.
- The SteamOS/Lepton builds recorded in reports are read again per headset.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:00:09 +10:00
saphidandClaude Opus 5.5 98a5ec45bb Bundled KDE Connect: tidying up can't leave the pad stuck starting
discard() swallows OSError as well as Failure, so a launch that fails and
can't remove its copy still reports the error and can be started again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:56:00 +10:00
saphidandClaude Opus 5.5 f10b5fe159 Package ui/apk_sources in the desktop app
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:55:04 +10:00
saphid 7e2228b15e Merge branch 'apk-search' into apk-store 2026-09-28 21:54:36 +10:00
saphid a24d27013c Merge branch 'sidequest' into apk-store 2026-09-28 21:54:36 +10:00
saphid c5b5930582 Merge branch 'more-sources' into apk-store 2026-09-28 21:54:36 +10:00
saphid 70a0bd0d38 Merge branch 'user-repos' into apk-store 2026-09-28 21:54:35 +10:00
saphidandClaude Opus 5.5 e4421d966a Bundled KDE Connect: the server owns a copy until its agent speaks
The agent holds its copy from its first status line on and tidies it up
however it ends. Before that (a launch error, or stopped before the agent
ran) the server removes the copy itself. discard() only ever removes
incoming copies, never the iPhone bundle's own. The retry race test waits
for both contenders' decisions instead of sleeping.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:51:02 +10:00
saphidandClaude Opus 5.5 318bc3b84f Devices: make the pin folder before ssh saves a first-seen key into it
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:49:43 +10:00
saphidandClaude Opus 5.5 cb182dbce5 Devices: fixes from review round 3
- Attempts carry a generation: one overtaken by a switch, a removal or a login
  change routes nothing back to the old headset and can't report connected.
- Removing the active headset or changing its user/port reroutes at once,
  before anything that can fail.
- One known_hosts file per headset (~/.ssh/frame-control-hosts/<id>):
  forgetting one headset's key can't drop another's, whoever else writes.
- learn() checks, under the config lock, that the block is still what the
  attempt started from before writing to it.
- A switch stops live video and drops captures from the previous headset.
- A probe shares its time between the addresses a name resolves to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:48:39 +10:00
saphidandGPT-6 Astra 7844577be8 Redesign APK discovery as an artwork-led app store
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:47:38 +10:00
saphidandClaude Opus 5.5 39afb23d97 Docs: Steam's StartDesktopStream pairs the Frame with the Mac (verified); stream test next
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:45:52 +10:00
saphidandClaude Opus 5.5 efffd72c52 Bundled KDE Connect: no copy left behind by a stop or a local read error
- A start turned off while copying removes the copy instead of starting an
  agent that would be killed before it could tidy up.
- A local read error while copying removes the partial copy too.
- The retry race test holds the new start until the retry has decided, so
  it fails every time without the fix (checked 3/3), not by luck.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:44:02 +10:00
saphidandClaude Opus 5.5 bda9d77d2d Mac in the headset: USB route keeps a configured host-key alias and falls back to the network
Review round 12: a failed USB-C tunnel now retries the normal path; an
existing HostKeyAlias wins; --host with --usb is rejected; the Steam
desktop-streaming claim is now 'untested' (Valve documents the desktop
showing when a game loses focus); the Show/cleanup overlap test blocks
for real (it fails without the lock). Verified live: with the cable out,
the route is the normal path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:39:40 +10:00
saphidandClaude Opus 5.5 18334b5989 Devices: fixes from review round 2
- Switching headsets is serialized with the start of any install; background
  work counts as running from before its thread starts. use() reroutes every
  command at once and makes ensure() wait for the new headset.
- A new user or port reroutes commands even if the attempt then fails.
- ~/.ssh/config edits take a lock file shared with Set Up Connection
  (frame_connect.py and connect.sh, which now also writes atomically).
- A finished attempt no longer writes its older settings over a change Set
  Up Connection made meanwhile.
- Pin edits are locked and swapped atomically.
- A bare alias behind ProxyJump/ProxyCommand is left to ssh to reach.
- The page drops answers about the previous headset after a switch; the
  header switcher takes clicks in the macOS title bar.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:38:41 +10:00
saphidandClaude Opus 5.5 d5e8193ce5 Bundled KDE Connect: tidy the copied packages however a start ends
- Each start keeps its copy (holding a lock on it) until it ends, however
  it ends, then removes it; a restart can still unpack it.
- The server removes a partial copy when copying fails.
- Other copies are removed only when unlocked and over an hour old.
- Tests: a start racing the need-packages retry (one agent, not two), a
  restart that must unpack its copy, a held copy surviving the sweep.
  Both regression tests fail without their fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:36:22 +10:00
saphidandClaude Opus 5.5 4fae62ba14 Mac in the headset: use the Frame's USB-C network when plugged in; Steam streaming findings
- Plugged into the Mac, the Frame is a USB network device ("Steam Frame",
  usb0 at ~0.9 ms). The tunnel uses it when the Frame's usb0 answers,
  with the usual host key; otherwise the normal path. Interleaved runs:
  content p50 7 vs 10 ms, click to drawn 17 vs 27 ms, scroll p95 23 vs
  31-37 ms. FRAME_MACVIEW_USB=0 turns it off; the card says "over USB-C".
- Bench: --usb, and the route is recorded per run.
- Docs: Steam's own streaming (no SteamVR host on macOS; Remote Play pairs
  but streams games, not windows; test blocked); the Frame's USB network;
  the 2026-09-28 health-check boot-loop recurrence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:33:31 +10:00
saphidandClaude Opus 5.5 6765fbcb20 Bundled KDE Connect: close the races the second review found
- The need-packages retry only runs if no start() has taken over, so two
  agents can't end up running for one session.
- Each copy goes to its own incoming folder; the agent removes its own once
  connected (and any a cancelled start left over an hour ago), so a stopped
  start can't delete files another is copying or still needs.
- A missing package folder means need-packages, not an error.
- Tests keep fake agents running, so they check ready and which agent owns
  the session, plus a bounded retry and the tidy rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:28:10 +10:00
saphidandClaude Opus 5.5 13eb65603b Devices: fixes from review round 1
- No switching headsets (or changing the active one's user/port, or removing
  it) while installs run: they read the ssh settings step by step.
- Switching reroutes every command to the new headset at once, even if it
  never answers.
- ~/.ssh/config edits are serialized, use unique temp files, and back off if
  another program wrote the file meanwhile.
- stop() ends a handshake in progress and joins the connector.
- Pinned keys are written unhashed (HashKnownHosts=no); hashed ones are still
  found and forgotten via ssh-keygen.
- Set Up Connection changing a headset's user or port updates the registry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:25:41 +10:00
saphidandGPT-6 Astra 08037ab3f5 Expose F-Droid artwork and developer metadata for store entries
Resolve localized v1/v2 artwork, retain six ordered screenshots, clean summaries and refresh older source caches. Add offline metadata and cache regression coverage.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:25:24 +10:00
saphidandGPT-6 Astra 41684e2d90 Add publisher artwork to GitHub APK source entries
Include curated app images and summaries, owner avatars and social banners for topic discovery, and fixture coverage for artwork preservation.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:24:55 +10:00
saphidandClaude Opus 5.5 75b2478a55 Bundled KDE Connect: fixes from review
- iPhone build fails if the bundle can't be made, instead of shipping a
  stale archive with an empty version.
- A different build that another device is using right now is left running
  and replaced once nobody is, rather than stopped under them.
- If what's installed changed after the server looked, the agent asks for
  the packages (need-packages) and the server copies them and starts again.
- The installed-build check sends bytes, so Windows' CRLF can't break it.
- Starting again while a stopped start is still copying launches anew;
  concurrent copies use their own temporary names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:18:56 +10:00
saphidandGPT-6 Astra c06b3285f2 Add Android Steam library artwork and supervised Lepton sessions
Generate five artwork slots, refresh VR shortcuts and managed collections, and retain a signal-aware launcher around setsid so stopping the wrapper cleans its container. Cover installation, artwork and launch cleanup offline; record the Steam client startup blocker for device verification.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:15:43 +10:00
saphidandClaude Opus 5.5 1d05f57fbf Mac in the headset: a Show waits for a viewer cleanup already in progress
Review round 11: the cleanup's check and pkill now hold a lock that Show
takes to count itself in, so a new viewer can't start between them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:14:27 +10:00
saphidandClaude Opus 5.5 a954fc83c9 Devices: several headsets, several addresses each, and live connection status
Frame Control can now manage more than one Steam Frame, and reach each at any
of several addresses (LAN IPs per network, its .local name, Tailscale). A
connector in the server tries them all at once, picks the best one that
answers, follows ssh -v through each stage (network, finding, SSH, identity,
login) and streams that to the page. The header shows it live; a new Devices
tab (key 5) manages headsets, addresses and network names.

- ui/frame_devices.py: registry in devices.json, imported from the managed
  ~/.ssh/config blocks; per-headset host key pinning; config block updates.
- ui/frame_network.py: gateway IP+MAC fingerprint, Wi-Fi name, Tailscale.
- ui/frame_link.py: the connector, Test now, Tailscale/mDNS discovery, API.
- server.py: ensure_master delegates to the connector; /api/connection,
  /api/connection/events (SSE), /api/devices.
- Electron: headset switcher and Devices item in the Frame menu.
- frame_connect.py --alias; FRAME_CONTROL_DATA_DIR / FRAME_CONTROL_SSH_DIR
  keep tests off real data.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:13:58 +10:00
saphidandClaude Opus 5.5 484a50a189 Mac in the headset: no viewer cleanup while a Show is launching; relay pump always ends
Review round 10: a Show replacing its own stream could have its new viewer
ended by the cleanup; a viewer reset left the delayed relay pending.
Verified: the relay delivers what's queued, then closes the agent side.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:11:26 +10:00
saphidandClaude Opus 5.5 7f769ca2f0 Mac in the headset: end the Frame's viewer browser after Stop; relay fixes
- Chromium on the Frame outlived its last viewer window (verified: 11
  processes left after a run). Once nothing is shown, Stop ends it, unless
  Show was pressed again meanwhile; its profile is Frame Control's own.
- Relay --delay: if the agent side fails, close the viewer side too
  (review round 9).
- Docs: the final scroll run captured 57 fps; don't blame ScreenCaptureKit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:07:59 +10:00
saphidandClaude Opus 5.5 5bf1268c82 Ship KDE Connect inside Frame Control instead of downloading it on the Frame
The keyboard and trackpad no longer fetch KDE Connect from Valve's package
repository on the Frame. The desktop apps and the iPhone app's Frame bundle
carry Valve's arm64 build of kdeconnect 24.02.2-1 and the five libraries it
links (kcontacts, kpeople, modemmanager-qt, pulseaudio-qt, libfakekey),
pinned by SHA-256 in frame/kdeconnect/packages.json and downloaded at build
time from the kdeconnect-frame-24.02.2-1 release, which also holds Valve's
complete source package for each.

On first use the computer copies them over its SSH connection (the iPhone
bundle already has them on the Frame); the agent checks each SHA-256,
unpacks them and stamps which build it is, so later starts copy nothing.
No internet on the Frame, 3.6 MB instead of 8 MB, 18 MB unpacked instead of
82 MB (ModemManager and friends were packaging-only dependencies).

GPL/LGPL compliance: frame/kdeconnect/NOTICE.md names each exact version,
licence and source; per-project licence texts in frame/kdeconnect/LICENSES;
THIRD_PARTY_NOTICES.md; an About and licences dialog in the app.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:07:20 +10:00
saphidandGPT-6 Astra 784a48f218 Add authenticated F-Droid user repositories and management CLI
Verify pinned JAR/CMS signatures, v2 index hashes and APK downloads; support signed v1 fallback and persist TOFU identities. Reuse the catalogue reducer and document repository publishing with offline and live verification evidence.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:06:06 +10:00
saphidandGPT-6 Astra f4dbb1180c Add OBB transfers, private app-data backups and SideQuest source policy
Borrow expansion-file and save-management features with offline verification. Keep SideQuest page-only under its current access terms; document research, integration limits and device acceptance gaps.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:05:59 +10:00
saphidandClaude Opus 5.5 038dcd48cd Treat bare activity names as package-relative when matching the alias target
Third review follow-up (PackageParser.buildClassName). Confirmed the
targetActivity resource id 0x01010202 in Open Saber Plus's manifest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:04:55 +10:00
saphidandGPT-6 Astra f1b12a6eb6 Add unified APK source search and source management
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:04:48 +10:00
saphidandClaude Opus 5.5 71200c5179 Merge origin/main into mac-in-headset
Brings in #4, #5, #8 (fbl100's verified Remmina/VNC mirror), APK
alternatives and the website. docs/streaming.md: Mac in the headset stays
the recommendation (now verified on the Frame); Remmina keeps #8's verified
evidence as the whole-screen fallback. docs/mac-in-headset.md cites #8's
lag finding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:03:34 +10:00
saphidandGPT-6 Astra 113216cc0e Add curated GitHub APK releases and itch.io VR feed listings
Survey publisher consent and access limits; add cached sources, recorded fixtures and local APK proof.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:03:27 +10:00
saphidandClaude Opus 5.5 00b45bafc5 Mac in the headset: final benchmark numbers; relay delay is a delay line
Review round 8 (GPT-6 Astra xhigh): --delay paused upstream reads, so busy
streams saw 30-60 ms instead of 30. Verified locally: 60-62 ms round trip
with 30 ms each way under load. No saved result used --delay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 21:02:26 +10:00
saphidandClaude Opus 5.5 9e4dcdd147 Mac in the headset: measure every frame, adapt to the network, separate windows
- Per-frame timing on the Mac's clock (capture, encode, network, decode,
  draw), viewer clock sync and reports, input echo, /stats and a HUD.
- scripts/macview-bench.py: repeatable runs on the real Frame, a shaping
  relay (no sudo), interleaved A/B between agent settings; results in
  bench/results/.
- Adaptive controller: ack-based send gate with jitter-aware slack, AIMD
  bitrate that knows when a stream is app-limited, fps then size tiers.
  On a 50->3->50 Mbit/s step, scroll p95 went from 4.7 s to 72 ms; no cost
  on a clean link.
- Separate mode: real AppKit event loop (HiDPI and NSScreen now work),
  cropped capture for fixed-size windows, windows kept on their display,
  graceful quit restores windows; stop/start races fixed.
- Encoder timeline clamp (no oversized frame after a pause).
- Frame Control shows each live stream's fps, delay, bitrate and tier.

Reviewed by GPT-6 Astra xhigh (read-only), 7 rounds; findings fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:56:14 +10:00
saphidandClaude Opus 5.5 9e9e5950d0 Patch the activity the launcher alias targets, not the first MAIN filter
Second review follow-up: with several activities (e.g. a splash activity ahead
of the game), the alias fallback now prefers the real activity named by the
alias's android:targetActivity. Reads targetActivity by resource id, updates
the error text and docs/vr-apks.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:55:56 +10:00
saphidandClaude Opus 5.5 268afccf05 APK sources: shared interface for source modules
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:52:30 +10:00
saphidandClaude Opus 5.5 e1c0ac983c Repair alias-only launchers for flat apps too; never fail on a VR alias
Review follow-up. inspect() now returns 'repairable' and the filters it can
patch: the real activity's VR MAIN filter, or, when LAUNCHER/VR sits only on an
<activity-alias>, any real MAIN filter with a category to copy. install() and
patch() repair on 'repairable' instead of 'vr_activity', so a flat Godot 4
export is fixed and a VR category only on the alias no longer aborts install.
Tests cover both shapes, an already-launchable activity with an alias, and an
end-to-end patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:40:16 +10:00
saphidandClaude Opus 5.5 33a92a2e1d Tests: run in a sandbox that can't touch real app data, telemetry or the shared database
On a maintainer's Mac the compatibility-database key is in the Keychain, so a
test that reached install reporting published fake reports. Every test module
now imports tests/sandbox.py first, which points app data at a throwaway
directory (new FRAME_CONTROL_DATA_DIR), turns telemetry off and sends the
database nowhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:20:27 +10:00
saphidandClaude Opus 5.5 077eab2b79 Add LAUNCHER to the real VR activity when only an activity-alias has it
Lepton's apk-info-extractor ignores <activity-alias>, and Godot 4 exports put
LAUNCHER only on an alias (com.godot.game.GodotAppLauncher). Open Saber Plus
0.7.67 installed but Lepton exited with 'APP_ACTIVITY is empty'. Count only
real activities as launchable and patch the activity's VR intent filter.
Verified on the Frame: Open Saber Plus launches and renders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:19:38 +10:00
saphidandClaude Opus 5.5 f076527722 Send analytics to the existing PostHog project; make bug reports private
- Analytics go to the maintainer's PostHog US project 343535, tagged
  $lib = frame-control. Every event carries $ip 0.0.0.0, since PostHog
  stores the sender's address otherwise (checked live), including events
  queued by earlier versions.
- Report a problem sends a private problem_report event to PostHog instead
  of a public GitHub issue, with its own random id so a contact address
  can't be linked to analytics. The dialog asks how to reach the person and
  shows a reference. Maintainers read reports on the PostHog dashboard or
  with `python3 ui/frame_report.py inbox`.
- Community sync pages by timestamp in UTC: PostHog refuses OFFSET for
  personal API keys.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:14:36 +10:00
saphidandClaude Opus 5.5 e67802f15d Tests: keep the blocked-upload test from reaching real install reporting
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:11:48 +10:00
saphidandClaude Opus 5.5 73eef14ecd Report APKs refused before install; offer a compatibility test after installing an alternative
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 19:44:44 +10:00
saphidandClaude Opus 5.5 c3ceea9bcd Merge main into analytics-and-updates: keep APK alternatives alongside Report a problem
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 19:39:38 +10:00
saphid 35ef3af54b Merge remote-tracking branch 'origin/main' into vr-apks
# Conflicts:
#	ui/frame_android.py
#	ui/index.html
2026-09-28 17:49:47 +10:00
saphidandClaude Opus 5.5 f10282294d Fix the cross-provider review's findings on VR APK support
patch() turns corrupt-manifest struct/index errors into FrameError; a missing
layer build says so; the signing key falls back to a rename where hard links
aren't supported and explains how to recover from a bad cached key; the layer
nulls an instance it can't destroy and logs xrLocateSpaces once.

Not changed: the 1.1 Meta profile names match xr.xml's promoted names
(meta/touch_pro_controller, meta/touch_plus_controller), and grip_surface is
palm_ext renamed, so the rewrite stays (now commented).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:47:35 +10:00
saphid 6217b5f2fa Merge remote-tracking branch 'origin/main' into frame-input 2026-09-28 17:43:27 +10:00
saphidandClaude Opus 5.5 b9e31178f3 Keyboard and trackpad for the Frame, through its own KDE Connect
Home → Keyboard and trackpad, in every version of Frame Control (Mac,
Windows, Linux, iPhone, iPad) with nothing to install on the device in
your hand. On a phone: a trackpad (drag, tap, two-finger scroll and
right-click) and a field that types on the Frame. On a computer: click
the pad to pass the mouse and keyboard through; Esc to stop.

First-party route: ui/frame_input_agent.py runs on the Frame and talks
KDE Connect's LAN protocol (v7) to kdeconnectd as if it were a phone.
KDE Connect isn't on the image, but Valve's package repository has it;
the agent fetches it and four libraries into ~ (no root, survives
updates), starts it, pairs by itself (accepting over D-Bus), and stops
it again when the last device disconnects. Each device has its own
identity; a stuck KDE Connect is restarted once.

Verified against the real Frame (SteamOS 0.4.1): first-time install,
pairing, pointer moves from the Mac's server and the iPhone app
(Simulator), Mac and iPhone at once, two installs at once, a frozen
daemon replaced, and the daemon stopping when the app quits.

Reviewed by GPT-6 Astra (xhigh) over seven rounds; all findings fixed
except per-event delivery acknowledgement (documented known limit).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:42:19 +10:00
saphidandClaude Opus 5.5 97d70d0c80 Merge origin/main: background install jobs and tabbed pages
Flatpak installs record their outcome inside main's background job; failed
jobs are diagnostics too. The Privacy panel lives on the Tools page (#privacy
opens it), tab analytics use the four page names, and "Test it now?" reads the
install job's result.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:37:28 +10:00
saphidandClaude Opus 5.5 9eeca79b5d Analytics, self-update and Report a problem
- Anonymous PostHog analytics (ui/frame_telemetry.py): usage on by default
  after a first-run notice; compatibility results and error details opt-in,
  offered together by the notice's "Share more to help fix problems" button.
  Random id, no person profiles or GeoIP, scrubbed text, an offline outbox,
  and "Show what's been sent" in the new Privacy panel. Inert without a
  project key, from a source checkout, or with DO_NOT_TRACK=1.
- APK installs now record install_failed when the APK itself won't install,
  and offer a 20-second test after installing. Opted-in reports reach the
  shared database through PostHog and `frame_compat_db.py sync`.
- The desktop app updates itself from published releases (app/updater.js):
  update.json from releases/latest/download, SHA-256 checked, no downgrades;
  macOS bundle swap, Windows NSIS, Linux AppImage, otherwise the release page.
  scripts/publish-release.sh publishes a tested draft with its manifest.
- Report a problem (header button, Privacy panel, Help menu) files a GitHub
  issue through the website's feedback API, with a previewed, scrubbed
  diagnostics snapshot; activity and logs only when asked for.

Reviewed by GPT-6 Astra (xhigh, read-only) three times; all findings fixed.
Docs: docs/privacy.md, docs/releasing.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:34:21 +10:00
saphidandClaude Opus 5.5 b393e90854 Keep the Frame awake during agent work
Steam's own idle timer (60 min on AC, 15 on battery) suspends the Frame,
and SSH work doesn't count as activity. scripts/keep-awake.sh on sets both
timers to Never through Steam's DevTools (reusing ui/frame_steam.py) and
holds a logind sleep inhibitor as a user unit; off releases the inhibitor
and restores the saved timers. Findings recorded in how-the-frame-works.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:23:06 +10:00
saphidandClaude Opus 5.5 45f720883a Docs: house style for announcing features and fixes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:22:55 +10:00
saphidandClaude Opus 5.5 50d5e14539 Inject the OpenXR compatibility layer into VR APKs on install
VR apps with an arm64 OpenXR loader get frame/openxr-compat's layer unless
--no-xr-compat; the app bundle ships the layer. Verified on the Frame: Wolvic's
Quest build gets through OpenXR start-up, Open Brush still reaches FOCUSED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:33:14 +10:00
saphid fd15f1de4c Merge branch 'openxr-compat' into vr-apks 2026-09-28 16:29:11 +10:00
saphidandClaude Opus 5.5 064cf95f6d VR APK notes: Lepton's missing clipboard also stops the Godot XR Tools demo
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:29:11 +10:00
saphidandClaude Opus 5.5 8d75ede0ab OpenXR compat layer: keep the current refresh rate when SteamVR refuses a request
Quest apps ask for 72/90/120/144 Hz; SteamVR offers only the current rate and
Wolvic aborted on XR_ERROR_DISPLAY_REFRESH_RATE_UNSUPPORTED_FB. Verified on
the Frame: Wolvic's Quest build now reaches XR_SESSION_STATE_SYNCHRONIZED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:29:06 +10:00
saphid 0dbe18d824 Add APK-local OpenXR 1.1 compatibility layer for Frame 2026-09-28 16:24:27 +10:00
saphid a1bc6522c3 Detect VR APKs and repair launchers with pure Python v2 signing 2026-09-28 16:16:56 +10:00
saphidandClaude Opus 5.5 724b020e93 Document what it takes to run VR and Quest APKs in Lepton
Verified on the headset: hello_xr and Open Brush's Quest build run
immersively unmodified; Wolvic's Quest build needs a LAUNCHER category
and fails on SteamVR's Android runtime being OpenXR 1.0 only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:56:53 +10:00
saphidandClaude Opus 5.5 a3c6e5c984 Mac in the headset: stream Mac windows and screens into the Frame as panels
Frame Control can now show any Mac window, or a whole screen, as its own
SteamVR panel on the Steam Frame (Tools -> Mac in the headset, macOS only).
Place it with the SteamVR dashboard; the laser clicks and scrolls, and the
Mac's own keyboard types.

- mac/frame-mac-view (Swift, no dependencies): ScreenCaptureKit capture per
  window or display, VideoToolbox H.264 with low-latency rate control (JPEG
  fallback), a loopback HTTP/WebSocket server, CGEvent/AX input playback,
  and a display-awake assertion while anyone watches.
- ui/frame_macview.py: starts the agent, runs an ssh -R tunnel with a
  supervisor that reopens it on the same port, and launches a Chromium app
  window per stream on gamescope's :0, tagged with STEAM_GAME for its own panel.
- Frame Control's key never leaves the Mac: viewers get single-use,
  per-source tickets and reconnect keys that Stop revokes.
- ui/mac-view.html: WebCodecs decode, keyframe recovery, pointer/wheel/keys back.
- Bundled in the Mac app build; tests/test_macview.py builds and drives the
  agent on macOS.

Verified on the Frame (build 20260925.6191901) with the test pattern: panel
in about 1.5 s, about 60 fps, Mac-to-window about 11-17 ms, tunnel recovery
in 4 s. Laser input and real window capture still need a person in the headset.

Also commits the other thread's first-party rule (steam-frame skill) and
the first-party options table in docs/streaming.md.

Reviewed by GPT-6 Astra (xhigh, read-only) over six rounds; all findings fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 13:33:46 +10:00
294 changed files with 79737 additions and 454 deletions

No files matched your search

+6
View File
@@ -48,3 +48,9 @@ running `--help`: `paste-to-frame.sh`, `serve-bootstrap.sh` and
- `sudo` on the Frame asks for the user's Developer Mode password. Hand those
steps to the user (open Terminal) and keep automation to non-sudo commands.
- The Mac uses BSD userland and zsh (no `timeout`, use `head -n`).
- **First-party first.** For any new capability, investigate the first-party
way before anything else: Valve (SteamOS, Steam, Steam Link), Apple (the Mac
and iPhone), and KDE (the Frame's desktop is Plasma). It's usually the best
answer. If it isn't, write down why not. If it is, find what Frame Control
can do to make it easier to set up (install over SSH, pre-seed settings,
pair automatically, tell the user the one setting to turn on).
+5
View File
@@ -6,3 +6,8 @@
*.json text eol=lf
*.md text eol=lf
*.bat text eol=crlf
# Test fixtures are byte-exact (hashes, signatures): never convert line endings.
tests/fixtures/** -text
*.apk binary
*.jar binary
*.obb binary
+3 -1
View File
@@ -35,7 +35,9 @@ jobs:
- name: Server tests
run: python -m unittest discover -s tests -v
- name: App syntax
run: node --check app/main.js && node --check app/build/make-icon.js && node --check app/build/fetch-deps.js && node --check app/preload.js && node --check app/install-link.js
run: node --check app/main.js && node --check app/build/make-icon.js && node --check app/build/fetch-deps.js && node --check app/preload.js && node --check app/install-link.js && node --check app/updater.js
- name: Updater tests
run: node --test app/test/updater.test.js
- name: Website
run: node --test site/test/*.test.mjs && node --check site/public/js/site.js && node --check site/public/js/feedback.js
+4
View File
@@ -5,3 +5,7 @@ apk-catalog/data/index-v2*.json*
compat-db/.env.lakebed.server
compat-db/.lakebed/
tests/smoke/results/
mac/bin/
# Downloaded at build time (frame/kdeconnect/fetch.py, app/build/fetch-deps.js)
frame/kdeconnect/packages/
+31 -10
View File
@@ -35,8 +35,8 @@ See what the headset sees, install games and Android apps, move files and text a
<tr>
<td width="50%" valign="top">
**👓 Headset view**<br>
Live video of what the lenses show (about 30 fps), or a still of both eyes. Zoom, pan, full screen, save as PNG.
**👓 Headset view and Desktop**<br>
Live video of what the lenses show, or of the app panel in use, flat and still however the wearer looks around. Turn on Control and tap or click right on it to use the Frame from your phone or computer.
</td>
<td width="50%" valign="top">
@@ -72,6 +72,9 @@ Drag files onto the window to send them. Drop a game's .zip, folder or .exe to a
**📸 Screenshots**<br>
Browse the shots you take in the headset and save them to your Pictures folder.
**⌨️ Keyboard and trackpad**<br>
Type and point in the Frame's apps from your computer or phone, through KDE Connect, which Frame Control brings along and sets up on the Frame. Nothing else to install, anywhere.
</td>
</tr>
<tr>
@@ -90,9 +93,15 @@ SSH, SFTP, Steam Link, remote desktop, volume, sleep, restart and shut down.
</tr>
</table>
Nothing is installed on the Frame for any of this: the app uses what SteamOS
The optional [Family and comfort](docs/family-comfort.md) card adds session
limits, breaks, local alerts and one-click casting. A session copies a small
Frame Control worker into your headset user account.
For the other features, nothing is installed on the Frame: the app uses what SteamOS
already ships (sideloading a game copies Valve's own devkit scripts to
`~/devkit-utils`, as Valve's Devkit Client does). [How each feature works](docs/frame-control.md).
`~/devkit-utils`, as Valve's Devkit Client does). The optional
[performance HUD](docs/vr-utilities.md) copies our own Python helpers into
`~/.local/share/frame-control/vr/`. [How each feature works](docs/frame-control.md).
## Install
@@ -107,6 +116,10 @@ already ships (sideloading a game copies Valve's own devkit scripts to
a computer. Build it from [`ios/`](ios) in Xcode; see [docs/iphone.md](docs/iphone.md).
The app brings its own Python and `adb`; SSH is built into macOS and Windows.
From 0.4 it updates itself: when a new version is published, a banner offers
**Update and restart**. It sends anonymous usage statistics, which you can turn
off. Sharing compatibility results and error details is opt-in. See
[docs/privacy.md](docs/privacy.md).
Google doesn't publish `adb` for arm64 Linux, so that build uses your
distribution's. If you already have `adb`, the app uses yours.
@@ -175,9 +188,11 @@ entry to `~/.ssh/config` and keys at `~/.ssh/id_ed25519_frame` and
## Feedback
This is a first public test, so reports are really useful, especially from
Windows and Linux. The quickest way is the
[feedback form](https://frame-control.pages.dev/feedback/): no GitHub account
needed, and it opens an issue here. Please include:
Windows and Linux. The quickest way is **Report a problem** in the app (the
warning-sign button at the top, or **Help → Report a Problem…**). It adds
diagnostics with personal details removed, shows you exactly what's included,
and sends it privately to the maintainer; nothing is published. Without the app,
use the [feedback form](https://frame-control.pages.dev/feedback/). Please include:
- what you tried and what happened
- your computer's OS and your SteamOS build (Steam Settings → System)
@@ -200,10 +215,13 @@ Frame's software fits together, all checked against a real headset and labelled
| [Android apps (Lepton)](docs/apks.md) | Sideloading, the rated F-Droid catalogue, per-app instances |
| [Sideloading Linux and Windows games](docs/sideloading.md) | A .zip, folder or .exe as a Steam Devkit Game, runtime detection |
| [Install links for websites](docs/web-install.md) | `frame-control://install` links and manifests, the rules, a button to paste |
| [Steam games](docs/steam-games.md) · [VR video](docs/vr-video.md) · [WebXR in Chromium](docs/webxr-chromium.md) | Installing and buying, watching VR180/360, the Chromium build |
| [VR comfort and HUD](docs/vr-utilities.md) · [Steam games](docs/steam-games.md) · [VR video](docs/vr-video.md) · [WebXR in Chromium](docs/webxr-chromium.md) | Installing and buying, watching VR180/360, the Chromium build |
| [Mac in the headset](docs/mac-in-headset.md) | Mac windows and screens as panels in the Frame, with laser and keyboard input |
| [VR mods and custom songs](docs/mods.md) | Per-game feasibility, real-Frame results and blockers; no installer yet |
| [SSH](docs/ssh.md) · [Streaming](docs/streaming.md) · [Files](docs/file-transfer.md) · [Panels](docs/panels.md) · [Tailscale](docs/tailscale.md) | Topic notes |
| [Frame Control for iPhone](docs/iphone.md) | The iPhone and iPad app, how it runs the server on the Frame, pairing |
| [Recovery and OS images](docs/recovery-and-images.md) | Where to download the Frame's OS, what's inside, testing without the headset |
| [AI agents and assistant](docs/agents.md) | Key-free MCP tools, human approvals, and an opt-in assistant panel |
| [Testing](docs/testing.md) | Unit tests, end-to-end tests against a fake Frame in Docker, and the headset smoke test |
| [Open questions](docs/open-questions.md) | What's still unchecked |
@@ -237,9 +255,12 @@ cd app && npm install && npm start # run the app from the checkout
The server is Python stdlib only; the app is Electron. GitHub Actions runs the
tests on macOS, Windows and Linux, and a `v*` tag builds all three installers
into the release. See [building](docs/frame-control.md#building).
into a draft release, which reaches users once published. See
[building](docs/frame-control.md#building) and [releasing](docs/releasing.md).
## License
[MIT](LICENSE). Steam, Steam Frame and SteamVR are trademarks of Valve
[MIT](LICENSE). The apps also ship other people's software under its own
licence, notably KDE Connect (GPL) for the keyboard and trackpad; see
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Steam, Steam Frame and SteamVR are trademarks of Valve
Corporation. This project isn't affiliated with or endorsed by Valve.
+15
View File
@@ -0,0 +1,15 @@
# Third-party software in Frame Control
Frame Control's own code is under the [MIT licence](LICENSE). The apps also
ship other people's software, unchanged, each under its own licence:
| What | Where | Licence | Details |
|---|---|---|---|
| KDE Connect 24.02.2 and five libraries, as Valve builds them for the Frame | Desktop apps and the iPhone app; copied to the Frame for the keyboard and trackpad | GPL and LGPL (per package) | [frame/kdeconnect/NOTICE.md](frame/kdeconnect/NOTICE.md), licence texts in [frame/kdeconnect/LICENSES](frame/kdeconnect/LICENSES), complete source in the [kdeconnect-frame-24.02.2-1 release](https://github.com/saphid/frame-control/releases/tag/kdeconnect-frame-24.02.2-1) |
| Python 3.12 ([python-build-standalone](https://github.com/astral-sh/python-build-standalone)) | Desktop apps | PSF License and others | Included with it, in the app's `python` folder |
| adb (Android SDK Platform-Tools) | Desktop apps (not Linux arm64) | Apache-2.0 and others | `NOTICE.txt` in the app's `tools` folder |
| Mozilla's CA certificate list, as published by curl | Desktop apps | MPL-2.0 | https://curl.se/docs/caextract.html |
| Electron | Desktop apps | MIT (Chromium: BSD-3-Clause and others) | `LICENSE` and `LICENSES.chromium.html` in the app |
Frame Control starts KDE Connect and talks to it over its network protocol;
it doesn't link to it or include its code.
+21 -1
View File
@@ -1,7 +1,9 @@
// Downloads what the app bundles so users install nothing else: a standalone
// Python (python-build-standalone), adb (Android platform-tools) and a CA
// bundle. Each goes in build/deps/<os>-<arch>/{python,tools}, which package.json
// copies into the app's resources. Everything is pinned by version and SHA-256.
// copies into the app's resources. Also KDE Connect for the Frame (the same arm64
// packages for every build), into ../frame/kdeconnect/packages as listed in
// ../frame/kdeconnect/packages.json. Everything is pinned by version and SHA-256.
// node build/fetch-deps.js mac arm64 | win x64 | linux x64 arm64
const crypto = require("crypto");
const fs = require("fs");
@@ -127,8 +129,26 @@ async function fetch(os, arch) {
console.log(`${key}: ${version} -> ${out}`);
}
// As frame/kdeconnect/fetch.py does for the iPhone app's bundle.
async function fetchKdeConnect() {
const dir = path.join(__dirname, "..", "..", "frame", "kdeconnect");
const manifest = JSON.parse(fs.readFileSync(path.join(dir, "packages.json"), "utf8"));
const out = path.join(dir, "packages");
fs.mkdirSync(out, { recursive: true });
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
for (const p of manifest.packages) {
const file = path.join(out, p.file);
if (fs.existsSync(file) && sha(file) === p.sha256) continue;
await download(manifest.release + p.file, p.sha256, file);
}
const wanted = new Set(manifest.packages.map((p) => p.file));
for (const f of fs.readdirSync(out)) if (!wanted.has(f)) fs.rmSync(path.join(out, f), { recursive: true, force: true });
console.log(`KDE Connect for the Frame: ${manifest.packages.length} packages -> ${out}`);
}
(async () => {
const [os, ...archs] = process.argv.slice(2);
if (!os || !archs.length) throw new Error("usage: node build/fetch-deps.js <mac|win|linux> <arch>...");
for (const arch of archs) await fetch(os, arch);
await fetchKdeConnect();
})().catch((e) => { console.error(e.message); process.exit(1); });
+196 -20
View File
@@ -1,7 +1,7 @@
// Frame Control as a desktop app (macOS, Windows, Linux): starts ui/server.py on
// a free loopback port and shows it in a native window. The server does all the
// work over the `frame` SSH alias; this file only hosts it.
const { app, BrowserWindow, Menu, clipboard, dialog, ipcMain, shell } = require("electron");
const { app, BrowserWindow, Menu, Notification, clipboard, dialog, ipcMain, shell } = require("electron");
const { execFile, spawn } = require("child_process");
const { promisify } = require("util");
const fs = require("fs");
@@ -10,6 +10,7 @@ const net = require("net");
const os = require("os");
const path = require("path");
const { SCHEME, parseInstallLink, linkFromArgv } = require("./install-link");
const updater = require("./updater");
const run = promisify(execFile);
@@ -114,7 +115,11 @@ function ping(target) {
}
async function startServer() {
// The version and whether this is a built app go to ui/frame_telemetry.py, which
// sends nothing from a source checkout.
const env = { ...process.env, PATH: await loginPath(), FRAME_CONTROL_APP: "1",
FRAME_CONTROL_VERSION: app.getVersion(), FRAME_CONTROL_LOG: LOG,
...(app.isPackaged ? { FRAME_CONTROL_PACKAGED: "1" } : {}),
...(fs.existsSync(TOOLS) ? { FRAME_CONTROL_TOOLS: TOOLS } : {}) };
python = await findPython(env);
if (!python) throw new Error(`Frame Control needs Python 3.8 or later. ${PYTHON_HELP}`);
@@ -154,10 +159,16 @@ async function startServer() {
// Closing stdin lets server.py close its SSH connections and exit (the only clean
// way on Windows); SIGTERM does the same elsewhere.
// Resolves once it has exited (or after 20 s), so a replacement can take the server
// lock: server.py allows one per user.
function endServer(child) {
const gone = child.exitCode !== null || child.signalCode !== null ? Promise.resolve()
: new Promise((resolve) => child.once("exit", resolve));
try { child.stdin.end(); } catch {}
if (!IS_WIN) child.kill("SIGTERM");
setTimeout(() => { if (child.exitCode === null && child.signalCode === null) child.kill(); }, 5000).unref();
// server.py ignores a second SIGTERM while it shuts down, so the fallback is a hard kill.
setTimeout(() => { if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL"); }, 12000).unref();
return Promise.race([gone, new Promise((resolve) => setTimeout(resolve, 20000).unref())]);
}
function stopServer() {
@@ -178,27 +189,37 @@ function serverDied(why) {
if (win) win.loadURL(errorPage(`The server stopped unexpectedly (${why}). See ${LOG}.`));
}
async function restartServer() {
const old = server;
server = null;
url = null;
if (old) endServer(old);
await load();
// Restarts that overlap share one: two could each start a server, and the one
// that lost the lock would leave the app pointing at nothing.
let restarting = null;
function restartServer() {
if (!restarting) {
restarting = (async () => {
const old = server;
server = null;
url = null;
if (old) await endServer(old);
if (starting) await starting.catch(() => {}); // a start it cut short: then start afresh
await load();
})().finally(() => { restarting = null; });
}
return restarting;
}
// On macOS the page's sticky header becomes the title bar, clear of the traffic lights.
const CHROME_CSS = IS_MAC && `
header { padding-left: 92px !important; -webkit-app-region: drag; user-select: none; }
header a, header button, header input, header .chip { -webkit-app-region: no-drag; }
header a, header button, header input, header select, header .chip { -webkit-app-region: no-drag; }
`;
// Restart Server can start a new load while an older one is still waiting for
// its server; only the newest load may touch the window.
let loadGen = 0;
let starting = null; // loads that overlap share one server start
async function load() {
const gen = ++loadGen;
try {
if (!url) await startServer();
if (!url) await (starting ||= startServer().finally(() => { starting = null; }));
if (gen === loadGen && win) { await win.loadURL(url); firstRunCheck(); }
} catch (e) {
if (gen === loadGen && win) await win.loadURL(errorPage(e.message));
@@ -244,6 +265,65 @@ function fromUi(e) {
ipcMain.handle("clipboard:read", (e) => fromUi(e) ? clipboard.readText() : "");
ipcMain.handle("connection:setup", (e) => { if (fromUi(e)) setUpConnection(); });
ipcMain.on("keys:capture", (e, on) => { if (fromUi(e)) win.webContents.setIgnoreMenuShortcuts(on === true); });
ipcMain.handle("update:get", (e) => fromUi(e) ? publicUpdate() : null);
ipcMain.handle("update:check", (e) => fromUi(e) ? checkForUpdate({ manual: true }).then(publicUpdate) : null);
ipcMain.handle("update:install", (e) => { if (fromUi(e)) installUpdate(); });
// The page reports the headsets it knows (the server's Devices tab), so the Frame
// menu can switch between them. Only plain names and ids go into the menu.
const ALIAS_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
let devices = [];
ipcMain.on("devices:changed", (e, list) => {
if (!fromUi(e) || !Array.isArray(list)) return;
const next = list.slice(0, 20).filter(d => d && typeof d.id === "string" && ALIAS_RE.test(d.alias || ""))
.map(d => ({ id: d.id.slice(0, 80), name: String(d.name || d.alias).slice(0, 60), alias: d.alias, active: !!d.active }));
if (JSON.stringify(next) === JSON.stringify(devices)) return;
devices = next;
buildMenu();
});
const activeAlias = () => (devices.find(d => d.active) || {}).alias || FRAME;
// SSH through the server, so it goes to the headset and address the app is using
// (with its own pinned identity), and refuses when there's none.
function openSsh() {
if (!url) return dialog.showErrorBox("Couldn't open SSH", "Frame Control's server isn't running.");
const body = JSON.stringify({ what: "terminal" });
const req = http.request(new URL("/api/open", url), {
method: "POST", timeout: 15000,
headers: { "Content-Type": "application/json", "X-Frame-UI": "1", "Content-Length": Buffer.byteLength(body) },
}, (res) => {
let data = "";
res.on("data", (c) => { data += c; });
res.on("end", () => {
if (res.statusCode === 200) return;
let why = `HTTP ${res.statusCode}`;
try { why = JSON.parse(data).error || why; } catch {}
dialog.showErrorBox("Couldn't open SSH", why);
});
});
req.on("error", (e) => dialog.showErrorBox("Couldn't open SSH", e.message));
req.on("timeout", () => req.destroy(new Error("the server didn't answer")));
req.end(body);
}
function showDevices() {
if (win && url) win.webContents.executeJavaScript('location.hash = "devices"').catch(() => {});
}
ipcMain.handle("comfort:notify", (e, message) => {
if (!fromUi(e) || typeof message !== "string" || message.length > 500) throw new Error("Invalid notification");
if (!Notification.isSupported()) throw new Error("System notifications are unavailable");
return new Promise((resolve, reject) => {
const notification = new Notification({title: "Frame Control", body: message});
const timer = setTimeout(() => reject(new Error("Notification delivery was not confirmed. Check system notification settings.")), 5000);
notification.once("show", () => { clearTimeout(timer); resolve(true); });
notification.once("failed", (_event, error) => {
clearTimeout(timer);
reject(new Error("Notification delivery failed. Check system notification settings: " + error));
});
notification.show();
});
});
// frame-control://install links from websites (docs/web-install.md). They can
// arrive before the window or server exists (macOS open-url on a cold launch),
@@ -276,6 +356,81 @@ ipcMain.on("install-link:ready", (e) => {
deliverLinks();
});
// ---- updates (app/updater.js, docs/releasing.md) ----
// Checked shortly after launch and every few hours; the page shows a banner and
// the Update button calls installUpdate.
const UPDATE_EVERY = 6 * 3600 * 1000;
const update = { status: "idle", current: app.getVersion(), latest: null, error: null, progress: 0, how: null };
function publicUpdate() {
const r = update.latest;
return { status: update.status, current: update.current, error: update.error, progress: update.progress,
latest: r && { version: r.version, notes: r.notes, page: r.page },
canInstall: !!update.how && update.how.method !== "manual", why: update.how && update.how.why };
}
function setUpdate(fields) {
Object.assign(update, fields);
if (win && linkPage === win.webContents) win.webContents.send("update:state", publicUpdate());
}
async function checkForUpdate({ manual = false } = {}) {
if (["checking", "downloading", "ready"].includes(update.status)) return;
setUpdate({ status: "checking", error: null });
try {
const latest = await updater.latestRelease();
const how = updater.updateMethod({ platform: process.platform, isPackaged: app.isPackaged,
execPath: process.execPath, env: process.env,
exists: fs.existsSync, writable: updater.writable });
if (updater.isNewer(latest.version, update.current)) {
setUpdate({ status: "available", latest, how });
if (manual) offerUpdateDialog();
} else {
setUpdate({ status: "none", latest, how });
if (manual) dialog.showMessageBox(win, { type: "info", message: "Frame Control is up to date",
detail: `You have ${update.current}, the newest version.` });
}
} catch (e) {
// A failed check: nothing to install, and never an older release kept from before.
setUpdate({ status: "check-failed", error: e.message, latest: null });
if (manual) dialog.showMessageBox(win, { type: "warning", message: "Couldn't check for updates", detail: e.message });
}
}
async function offerUpdateDialog() {
const r = update.latest;
const { response } = await dialog.showMessageBox(win, {
type: "info", message: `Frame Control ${r.version} is available`,
detail: `You have ${update.current}.` + (update.how.method === "manual" ? ` Download it from the release page (${update.how.why}).` : ""),
buttons: [update.how.method === "manual" ? "Open Release Page" : "Update and Restart", "Later"], defaultId: 0, cancelId: 1,
});
if (response === 0) installUpdate();
}
async function installUpdate() {
// "error" here only ever means an install failed, so trying again is safe.
if (update.status !== "available" && update.status !== "error") return;
if (!update.latest || !updater.isNewer(update.latest.version, update.current)) return;
if (!update.how || update.how.method === "manual") { shell.openExternal(update.latest.page); return; }
setUpdate({ status: "downloading", progress: 0, error: null });
try {
const start = await updater.prepare(update.latest, update.how,
(done, total) => { if (total) setUpdate({ progress: done / total }); }, update.current);
setUpdate({ status: "ready", progress: 1 });
start();
quitting = true;
app.quit();
} catch (e) {
setUpdate({ status: "error", error: e.message });
}
}
function scheduleUpdateChecks() {
if (process.env.FRAME_CONTROL_NO_UPDATE_CHECK === "1") return;
setTimeout(checkForUpdate, 8000);
setInterval(checkForUpdate, UPDATE_EVERY).unref();
}
function registerScheme() {
// A checkout runs as `electron .`, so the OS must be told the script too.
// (macOS takes the scheme from Info.plist, which only the built app has.)
@@ -292,7 +447,7 @@ function createWindow() {
title: "Frame Control", backgroundColor: BG, show: false,
...(IS_MAC ? { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 18, y: 26 } }
: { icon: path.join(__dirname, "build", "icon.png") }),
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true,
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, backgroundThrottling: false,
preload: path.join(__dirname, "preload.js") },
});
win.once("ready-to-show", () => win.show());
@@ -326,25 +481,37 @@ async function runInTerminal(argv) {
}
}
async function setUpConnection() {
const alias = `FRAME_ALIAS=${FRAME}`;
// Set Up Connection for the headset in use (or another alias, from the Devices tab).
async function setUpConnection(name = activeAlias()) {
if (!ALIAS_RE.test(name)) return;
const alias = `FRAME_ALIAS=${name}`;
if (IS_MAC) return runInTerminal(["env", alias, "zsh", path.join(SCRIPTS, "connect.sh")]);
const py = python || await findPython({ ...process.env, PATH: await loginPath() });
const setup = [py || "python3", ...PY_FLAGS, path.join(ROOT, "ui", "frame_connect.py")];
// A new console inherits our environment on Windows; Linux terminals may not.
runInTerminal(IS_WIN ? setup : ["env", alias, ...setup]);
// --alias, since a new console on Windows (and some Linux terminals) doesn't get our environment.
runInTerminal([py || "python3", ...PY_FLAGS, path.join(ROOT, "ui", "frame_connect.py"), "--alias", name]);
}
function buildMenu() {
const template = [
...(IS_MAC ? [{ role: "appMenu" }] : []),
...(IS_MAC ? [{ label: app.name, submenu: [
{ role: "about" }, { label: "Check for Updates…", click: () => checkForUpdate({ manual: true }) },
{ type: "separator" }, { role: "services" }, { type: "separator" },
{ role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { role: "quit" },
] }] : []),
{ role: "fileMenu" },
{ role: "editMenu" },
{
label: "Frame",
submenu: [
{ label: "Set Up Connection…", click: setUpConnection },
{ label: IS_MAC ? "Open SSH in Terminal" : "Open SSH in a Terminal", click: () => runInTerminal(["ssh", FRAME]) },
{ label: "Set Up Connection…", click: () => setUpConnection() },
{ label: IS_MAC ? "Open SSH in Terminal" : "Open SSH in a Terminal", click: openSsh },
{ type: "separator" },
...(devices.length > 1 ? [{
label: "Headset",
submenu: devices.map(d => ({ label: d.name, type: "radio", checked: d.active,
click: () => { if (win) win.webContents.send("use-device", d.id); } })),
}] : []),
{ label: "Devices…", accelerator: "CmdOrCtrl+5", click: showDevices },
{ type: "separator" },
{ label: "Open in Browser", click: () => url && shell.openExternal(url) },
{ label: "Restart Server", click: () => win ? restartServer() : createWindow() },
@@ -364,7 +531,15 @@ function buildMenu() {
...(IS_MAC ? [{ role: "windowMenu" }] : []),
{
role: "help",
submenu: [{ label: "Project on GitHub", click: () => shell.openExternal("https://github.com/saphid/steam-frame") }],
submenu: [
...(IS_MAC ? [] : [{ label: "Check for Updates…", click: () => checkForUpdate({ manual: true }) }]),
{ label: "Report a Problem…", click: () => {
if (win && url && linkPage === win.webContents) win.webContents.send("report:open");
else shell.openExternal("https://frame-control.pages.dev/feedback/"); // the page isn't up
} },
{ label: "Release Notes", click: () => shell.openExternal(updater.RELEASES) },
{ label: "Project on GitHub", click: () => shell.openExternal("https://github.com/saphid/steam-frame") },
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
@@ -387,6 +562,7 @@ if (!app.requestSingleInstanceLock()) {
registerScheme();
buildMenu();
createWindow();
scheduleUpdateChecks();
});
app.on("activate", () => { if (!win) createWindow(); });
app.on("window-all-closed", () => app.quit());
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frame-control",
"version": "0.3.1",
"version": "0.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frame-control",
"version": "0.3.1",
"version": "0.4.0",
"license": "MIT",
"devDependencies": {
"electron": "^44.4.5",
+54 -6
View File
@@ -1,7 +1,7 @@
{
"name": "frame-control",
"productName": "Frame Control",
"version": "0.3.1",
"version": "0.4.0",
"description": "Desktop app for managing a Valve Steam Frame over SSH",
"private": true,
"main": "main.js",
@@ -9,8 +9,8 @@
"scripts": {
"start": "env -u ELECTRON_RUN_AS_NODE electron .",
"icon": "env -u ELECTRON_RUN_AS_NODE electron build/make-icon.js",
"dist": "node build/fetch-deps.js mac arm64 && electron-builder --mac --arm64 --publish never",
"dist:dir": "node build/fetch-deps.js mac arm64 && electron-builder --mac --arm64 --dir",
"dist": "sh ../mac/frame-mac-view/build.sh && node build/fetch-deps.js mac arm64 && electron-builder --mac --arm64 --publish never",
"dist:dir": "sh ../mac/frame-mac-view/build.sh && node build/fetch-deps.js mac arm64 && electron-builder --mac --arm64 --dir",
"dist:linux": "node build/fetch-deps.js linux x64 arm64 && electron-builder --linux --x64 --arm64 --publish never",
"dist:win": "node build/fetch-deps.js win x64 && electron-builder --win --x64 --publish never"
},
@@ -37,6 +37,7 @@
"main.js",
"preload.js",
"install-link.js",
"updater.js",
"package.json",
"build/icon.png"
],
@@ -46,7 +47,17 @@
"to": "ui",
"filter": [
"*.py",
"*.html"
"*.html",
"*.js",
"telemetry.json"
]
},
{
"from": "../ui/apk_sources",
"to": "ui/apk_sources",
"filter": [
"*.py",
"*.json"
]
},
{
@@ -61,7 +72,16 @@
"to": "frame/android",
"filter": [
"*.sh",
"*.py"
"*.py",
"*.js"
]
},
{
"from": "../frame/openxr-compat",
"to": "frame/openxr-compat",
"filter": [
"XrApiLayer_FRAME_compat.json",
"prebuilt/**/*.so"
]
},
{
@@ -72,6 +92,16 @@
"!**/__pycache__/**"
]
},
{
"from": "../frame/kdeconnect",
"to": "frame/kdeconnect",
"filter": [
"packages.json",
"NOTICE.md",
"LICENSES/**/*",
"packages/*.pkg.tar.zst"
]
},
{
"from": "../apk-catalog",
"to": "apk-catalog",
@@ -94,6 +124,14 @@
"filter": [
"**/*"
]
},
{
"from": "../THIRD_PARTY_NOTICES.md",
"to": "THIRD_PARTY_NOTICES.md"
},
{
"from": "../LICENSE",
"to": "LICENSE"
}
],
"mac": {
@@ -105,9 +143,19 @@
"dmg",
"zip"
],
"extraResources": [
{
"from": "../mac/bin",
"to": "mac/bin",
"filter": [
"frame-mac-view"
]
}
],
"extendInfo": {
"NSAppleEventsUsageDescription": "Frame Control opens Terminal for SSH sessions and for power actions that need the Developer Mode password.",
"NSLocalNetworkUsageDescription": "Frame Control connects to your Steam Frame over SSH on the local network."
"NSLocalNetworkUsageDescription": "Frame Control connects to your Steam Frame over SSH on the local network.",
"NSScreenCaptureUsageDescription": "Frame Control shows your Mac's windows and screens inside the Steam Frame when you ask it to."
},
"artifactName": "Frame-Control-mac-${arch}.${ext}"
},
+27 -1
View File
@@ -2,15 +2,41 @@
// to the Frame needs no pbpaste, PowerShell, xclip or wl-clipboard. Also tells
// the page where a dropped file or folder lives, so a folder can be sideloaded
// as a title without zipping it (the local server reads it from there).
// It can open Set Up Connection when the headset can't be reached.
// It can open Set Up Connection when the headset can't be reached, and keeps the
// Frame menu's list of headsets up to date.
// It also receives frame-control://install links (docs/web-install.md): only
// what the link asked for, never an install; the page asks the user first.
// And it passes update state both ways: see app/updater.js.
const { contextBridge, ipcRenderer, webUtils } = require("electron");
contextBridge.exposeInMainWorld("frameApp", {
notify: (message, request) => ipcRenderer.invoke("comfort:notify", message, request),
readClipboard: () => ipcRenderer.invoke("clipboard:read"),
setUpConnection: () => ipcRenderer.invoke("connection:setup"),
// The Frame menu's headset switcher: the page tells it the headsets, and hears picks.
devicesChanged: (list) => ipcRenderer.send("devices:changed", list),
onUseDevice: (cb) => {
ipcRenderer.removeAllListeners("use-device");
ipcRenderer.on("use-device", (_e, id) => cb(String(id)));
},
// While the keyboard-and-trackpad panel holds the keyboard, ⌘W, ⌘R and the rest go to the Frame.
captureKeys: (on) => ipcRenderer.send("keys:capture", !!on),
pathForFile: (file) => { try { return webUtils.getPathForFile(file) || ""; } catch { return ""; } },
// Updates (app/updater.js): the page shows a banner and an Update button.
update: {
get: () => ipcRenderer.invoke("update:get"),
check: () => ipcRenderer.invoke("update:check"),
install: () => ipcRenderer.invoke("update:install"),
onState: (cb) => {
ipcRenderer.removeAllListeners("update:state");
ipcRenderer.on("update:state", (_e, s) => cb(s));
},
},
// Help → Report a Problem… opens the page's report dialog (ui/frame_report.py).
onReportProblem: (cb) => {
ipcRenderer.removeAllListeners("report:open");
ipcRenderer.on("report:open", () => cb());
},
onInstallLink: (cb) => {
ipcRenderer.removeAllListeners("install-link");
ipcRenderer.on("install-link", (_e, req) => cb({ kind: req.kind, target: req.target }));
+59
View File
@@ -0,0 +1,59 @@
// Run: node --test app/test/
const test = require("node:test");
const assert = require("node:assert");
const { isNewer, assetName, updateMethod, macBundle } = require("../updater");
test("versions compare numerically, and a release beats its pre-releases", () => {
assert.ok(isNewer("0.3.10", "0.3.9"));
assert.ok(isNewer("v1.0.0", "0.9.9"));
assert.ok(!isNewer("0.3.1", "0.3.1"));
assert.ok(!isNewer("0.3.0", "0.3.1"));
assert.ok(isNewer("1.0.0", "1.0.0-beta.1"));
assert.ok(!isNewer("1.0.0-beta.1", "1.0.0"));
assert.ok(!isNewer("garbage", "0.1.0"));
});
test("asset names match what electron-builder publishes", () => {
assert.strictEqual(assetName("darwin", "arm64", "mac-zip"), "Frame-Control-mac-arm64.zip");
assert.strictEqual(assetName("win32", "x64", "nsis"), "Frame-Control-Setup-x64.exe");
assert.strictEqual(assetName("linux", "x64", "appimage"), "Frame-Control-linux-x86_64.AppImage");
assert.strictEqual(assetName("linux", "arm64", "appimage"), "Frame-Control-linux-arm64.AppImage");
});
const base = { isPackaged: true, env: {}, exists: () => false, writable: () => true };
test("macOS updates in place only from a writable, non-translocated location", () => {
const exe = "/Applications/Frame Control.app/Contents/MacOS/Frame Control";
assert.strictEqual(macBundle(exe), "/Applications/Frame Control.app");
assert.deepStrictEqual(updateMethod({ ...base, platform: "darwin", execPath: exe }),
{ method: "mac-zip", bundle: "/Applications/Frame Control.app" });
const dmg = "/Volumes/Frame Control 0.3.1/Frame Control.app/Contents/MacOS/Frame Control";
assert.strictEqual(updateMethod({ ...base, platform: "darwin", execPath: dmg }).method, "manual");
const trans = "/private/var/folders/x/AppTranslocation/ABC/d/Frame Control.app/Contents/MacOS/Frame Control";
assert.strictEqual(updateMethod({ ...base, platform: "darwin", execPath: trans }).method, "manual");
assert.strictEqual(updateMethod({ ...base, platform: "darwin", execPath: exe, writable: () => false }).method, "manual");
});
test("Windows needs the installer's copy; Linux needs an AppImage", () => {
const exe = "C:\\Users\\a\\AppData\\Local\\Programs\\Frame Control\\Frame Control.exe";
assert.strictEqual(updateMethod({ ...base, platform: "win32", execPath: exe, exists: () => true }).method, "nsis");
assert.strictEqual(updateMethod({ ...base, platform: "win32", execPath: exe }).method, "manual");
assert.strictEqual(updateMethod({ ...base, platform: "linux", execPath: "/opt/x", env: { APPIMAGE: "/home/a/F.AppImage" } }).method,
"appimage");
assert.strictEqual(updateMethod({ ...base, platform: "linux", execPath: "/opt/Frame Control/frame-control" }).method, "manual");
assert.strictEqual(updateMethod({ ...base, isPackaged: false, platform: "darwin", execPath: "x" }).method, "manual");
});
test("update.json assets always download from this repository's release", () => {
const r = require("../updater").fromManifest({ version: "0.4.0", notes: "n", assets: [
{ name: "Frame-Control-mac-arm64.zip", url: "https://evil.example/x.zip", digest: "sha256:" + "a".repeat(64) }] });
assert.strictEqual(r.assets[0].url, "https://github.com/saphid/frame-control/releases/download/v0.4.0/Frame-Control-mac-arm64.zip");
assert.throws(() => require("../updater").fromManifest({ version: "nope", assets: [] }));
});
test("prepare refuses a release that isn't newer (no downgrades)", async () => {
const { prepare } = require("../updater");
const release = { version: "0.3.1", assets: [] };
await assert.rejects(prepare(release, { method: "appimage", appImage: "/nonexistent/x" }, null, "0.4.0"), /isn't newer/);
await assert.rejects(prepare(release, { method: "appimage", appImage: "/nonexistent/x" }, null, "0.3.1"), /isn't newer/);
});
+250
View File
@@ -0,0 +1,250 @@
// Update checks and self-update for the desktop app (docs/releasing.md).
//
// The newest version is GitHub's "latest" release of saphid/frame-control. Drafts
// and pre-releases never count, so a build reaches people only when the
// maintainer publishes it after testing (scripts/publish-release.sh). That
// script attaches update.json (version, notes, each asset's SHA-256), read
// through github.com's latest/download link: the REST API allows only 60
// unauthenticated requests an hour per IP address, shared by everyone behind
// the same router, so it's only the fallback.
//
// Every download is checked against the SHA-256 digest GitHub records for the
// asset before anything is replaced. How the update is applied:
// macOS the .zip: unpacked next to the running app, swapped in by a small
// script once the app has quit, then reopened.
// Windows the NSIS installer, run silently over the current install; it
// reopens the app. A copy unpacked from the .zip is updated by hand.
// Linux the AppImage replaces itself; .deb installs are updated by hand.
// When the app can't update itself it opens the release page instead.
const { execFile, spawn } = require("child_process");
const crypto = require("crypto");
const fs = require("fs");
const https = require("https");
const os = require("os");
const path = require("path");
const REPO = "saphid/frame-control"; // renamed from saphid/steam-frame; GitHub redirects the old name
const LATEST = `https://api.github.com/repos/${REPO}/releases/latest`;
const MANIFEST = `https://github.com/${REPO}/releases/latest/download/update.json`;
const RELEASES = `https://github.com/${REPO}/releases`;
// "0.3.1" or "v0.3.1" -> [0, 3, 1]; pre-release suffixes sort before the release.
function parseVersion(v) {
const m = String(v || "").trim().replace(/^v/i, "").match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
return m ? { nums: [+m[1], +m[2], +m[3]], pre: m[4] || null } : null;
}
function isNewer(candidate, current) {
const a = parseVersion(candidate), b = parseVersion(current);
if (!a || !b) return false;
for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] > b.nums[i];
if (a.pre === b.pre) return false;
if (!a.pre) return true; // 1.0.0 is newer than 1.0.0-beta
if (!b.pre) return false;
return a.pre > b.pre;
}
// The asset this copy of the app updates from, by the names electron-builder gives them.
function assetName(platform, arch, method) {
if (method === "mac-zip") return `Frame-Control-mac-${arch}.zip`;
if (method === "nsis") return `Frame-Control-Setup-${arch}.exe`;
if (method === "appimage") return `Frame-Control-linux-${arch === "x64" ? "x86_64" : arch}.AppImage`;
return null;
}
// How this copy can update itself: mac-zip, nsis, appimage, or manual (with why).
function updateMethod({ platform, isPackaged, execPath, env, exists, writable }) {
if (!isPackaged) return { method: "manual", why: "running from a source checkout" };
if (platform === "darwin") {
const bundle = macBundle(execPath);
if (!bundle) return { method: "manual", why: "can't find the app bundle" };
if (bundle.includes("/AppTranslocation/") || bundle.startsWith("/Volumes/")) {
return { method: "manual", why: "move Frame Control to Applications first" };
}
if (!writable(path.dirname(bundle))) return { method: "manual", why: `${path.dirname(bundle)} isn't writable` };
return { method: "mac-zip", bundle };
}
if (platform === "win32") {
// electron-builder's NSIS install puts its uninstaller next to the app.
const dir = path.dirname(execPath);
if (exists(path.join(dir, "Uninstall Frame Control.exe"))) return { method: "nsis" };
return { method: "manual", why: "not installed with the installer" };
}
if (platform === "linux" && env.APPIMAGE) {
if (!writable(path.dirname(env.APPIMAGE))) return { method: "manual", why: "the AppImage's folder isn't writable" };
return { method: "appimage", appImage: env.APPIMAGE };
}
return { method: "manual", why: "installed from a package" };
}
function macBundle(execPath) {
const i = execPath.indexOf(".app/Contents/MacOS/");
return i < 0 ? null : execPath.slice(0, i + 4);
}
function get(url, { headers = {}, timeout = 20000, redirects = 5 } = {}) {
return new Promise((resolve, reject) => {
const req = https.get(url, { headers: { "user-agent": "FrameControl-updater", ...headers }, timeout }, (res) => {
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirects > 0) {
res.resume();
const next = new URL(res.headers.location, url);
if (next.protocol !== "https:") return reject(new Error("refusing a non-HTTPS redirect"));
return resolve(get(next.href, { headers, timeout, redirects: redirects - 1 }));
}
if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode} from ${new URL(url).host}`)); }
resolve(res);
});
req.on("timeout", () => req.destroy(new Error("timed out")));
req.on("error", reject);
});
}
async function getJson(url, headers) {
const res = await get(url, { headers });
let body = "";
for await (const chunk of res) body += chunk;
return JSON.parse(body);
}
// update.json and the API's release both become { version, notes, page, assets }.
function fromManifest(m) {
if (!parseVersion(m.version) || !Array.isArray(m.assets)) throw new Error("update.json is malformed");
const base = `https://github.com/${REPO}/releases/download/v${String(m.version).replace(/^v/i, "")}/`;
return { version: String(m.version).replace(/^v/i, ""), notes: String(m.notes || "").slice(0, 4000),
page: m.page || RELEASES,
// Assets always come from this repository's release, whatever the manifest says.
assets: m.assets.map((a) => ({ name: String(a.name), url: base + encodeURIComponent(String(a.name)),
size: a.size, digest: a.digest || null })) };
}
function fromApi(r) {
if (r.draft || r.prerelease) throw new Error("GitHub returned an unpublished release");
return { version: String(r.tag_name || "").replace(/^v/i, ""), notes: String(r.body || "").slice(0, 4000),
page: r.html_url || RELEASES,
assets: (r.assets || []).map((a) => ({ name: a.name, url: a.browser_download_url, size: a.size,
digest: a.digest || null })) };
}
async function latestRelease() {
try {
return fromManifest(await getJson(MANIFEST));
} catch (e) {
if (!/HTTP 404/.test(e.message)) throw e; // releases before update.json existed
}
return fromApi(await getJson(LATEST, { accept: "application/vnd.github+json" }));
}
async function download(asset, dest, onProgress) {
const m = /^sha256:([0-9a-f]{64})$/.exec(asset.digest || "");
if (!m) throw new Error(`GitHub has no SHA-256 for ${asset.name}, so it can't be checked`);
const res = await get(asset.url, { timeout: 60000 });
const total = +res.headers["content-length"] || asset.size || 0;
const hash = crypto.createHash("sha256");
// "wx": a new file only, never through an existing file or symlink at that path.
const out = fs.createWriteStream(dest, { mode: 0o755, flags: "wx" });
let done = 0;
await new Promise((resolve, reject) => {
res.on("data", (chunk) => { hash.update(chunk); done += chunk.length; onProgress && onProgress(done, total); });
res.on("error", reject);
out.on("error", reject);
out.on("finish", resolve);
res.pipe(out);
});
if (hash.digest("hex") !== m[1]) {
fs.rmSync(dest, { force: true });
throw new Error(`${asset.name} didn't match its SHA-256; nothing was changed`);
}
}
const run = (cmd, args) => new Promise((resolve, reject) =>
execFile(cmd, args, { timeout: 120000 }, (err, stdout, stderr) => err ? reject(new Error((stderr || err.message).trim())) : resolve(stdout)));
// Waits for this process to exit, swaps the new bundle in (putting the old one
// back if that fails), and reopens the app.
const MAC_SWAP = `set -u
pid="$1"; app="$2"; new="$3"; stage="$4"
while kill -0 "$pid" 2>/dev/null; do sleep 0.2; done
old="$stage/old.app"
if mv "$app" "$old"; then
if mv "$new" "$app"; then rm -rf "$old"; else mv "$old" "$app"; fi
fi
xattr -dr com.apple.quarantine "$app" 2>/dev/null
rm -rf "$stage"
open "$app"
`;
async function applyMac(release, bundle, onProgress) {
const asset = release.assets.find((a) => a.name === assetName("darwin", process.arch, "mac-zip"));
if (!asset) throw new Error(`the release has no ${assetName("darwin", process.arch, "mac-zip")}`);
// Staged beside the app, so the final move stays on one volume.
const stage = fs.mkdtempSync(path.join(path.dirname(bundle), ".frame-control-update-"));
try {
const zip = path.join(stage, asset.name);
await download(asset, zip, onProgress);
await run("/usr/bin/ditto", ["-x", "-k", zip, stage]);
fs.rmSync(zip, { force: true });
const name = fs.readdirSync(stage).find((n) => n.endsWith(".app"));
if (!name) throw new Error("the download has no app in it");
const fresh = path.join(stage, name);
const version = (await run("/usr/bin/plutil", ["-extract", "CFBundleShortVersionString", "raw",
path.join(fresh, "Contents", "Info.plist")])).trim();
if (version !== release.version) throw new Error(`the download is version ${version}, not ${release.version}`);
const script = path.join(stage, "swap.sh");
fs.writeFileSync(script, MAC_SWAP);
return () => spawn("/bin/sh", [script, String(process.pid), bundle, fresh, stage],
{ detached: true, stdio: "ignore" }).unref();
} catch (e) {
fs.rmSync(stage, { recursive: true, force: true });
throw e;
}
}
async function applyNsis(release, onProgress) {
const asset = release.assets.find((a) => a.name === assetName("win32", process.arch, "nsis"));
if (!asset) throw new Error(`the release has no ${assetName("win32", process.arch, "nsis")}`);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "frame-control-update-"));
const exe = path.join(dir, asset.name);
await download(asset, exe, onProgress);
// /S: silent, into the existing install. --force-run: open the app afterwards.
return () => spawn(exe, ["--updated", "/S", "--force-run"], { detached: true, stdio: "ignore" }).unref();
}
async function applyAppImage(release, appImage, onProgress) {
const asset = release.assets.find((a) => a.name === assetName("linux", process.arch, "appimage"));
if (!asset) throw new Error(`the release has no ${assetName("linux", process.arch, "appimage")}`);
// A private folder beside the AppImage, so the final rename stays on one filesystem.
const stage = fs.mkdtempSync(path.join(path.dirname(appImage), ".frame-control-update-"));
try {
const next = path.join(stage, asset.name);
await download(asset, next, onProgress);
fs.chmodSync(next, 0o755);
fs.renameSync(next, appImage); // the running copy keeps its open file
} finally {
fs.rmSync(stage, { recursive: true, force: true });
}
// Without FUSE the AppImage runs extracted (--appimage-extract-and-run, which isn't passed
// on to the app); a FUSE mount lives under /tmp/.mount_*. Keep the same mode on restart.
const extracted = process.env.APPIMAGE_EXTRACT_AND_RUN === "1" || !process.execPath.includes("/.mount_");
const env = { ...process.env, APPIMAGE: appImage, ...(extracted ? { APPIMAGE_EXTRACT_AND_RUN: "1" } : {}) };
// Started only once this process has exited, or the new copy would lose the single-instance lock.
return () => spawn("/bin/sh", ["-c", 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done; exec "$2"',
"sh", String(process.pid), appImage], { detached: true, stdio: "ignore", env }).unref();
}
// Downloads and prepares the update; returns a function that starts the swap,
// to be called just before the app quits.
async function prepare(release, how, onProgress, current) {
if (!isNewer(release.version, current)) throw new Error(`${release.version} isn't newer than ${current}`);
if (how.method === "mac-zip") return applyMac(release, how.bundle, onProgress);
if (how.method === "nsis") return applyNsis(release, onProgress);
if (how.method === "appimage") return applyAppImage(release, how.appImage, onProgress);
throw new Error(how.why || "this copy can't update itself");
}
function writable(dir) {
try { fs.accessSync(dir, fs.constants.W_OK); return true; } catch { return false; }
}
module.exports = { REPO, RELEASES, parseVersion, isNewer, assetName, updateMethod, macBundle, latestRelease,
fromManifest, fromApi,
download, prepare, writable };
+24
View File
@@ -0,0 +1,24 @@
<!doctype html>
<!-- Control experiment, run on the Frame itself: how often does Chromium on
gamescope put a canvas animation on screen, with no network or decoding
involved? Draws every animation frame for ?s= seconds and writes the
frame-gap histogram per second into the title, where xprop can read it. -->
<html><head><meta charset="utf-8"><title>fmv-present</title></head>
<body style="margin:0;background:#000"><canvas id="c" width="1280" height="720" style="width:100%;height:100%"></canvas>
<script>
const q = new URLSearchParams(location.search), secs = +q.get("s") || 15, tag = q.get("tag") || "";
const ctx = document.getElementById("c").getContext("2d", { alpha: false, desynchronized: true });
const perSec = []; let t0 = 0, last = 0, n = 0, gaps = [];
function frame(t) {
if (!t0) t0 = last = t;
ctx.fillStyle = "#123"; ctx.fillRect(0, 0, 1280, 720);
ctx.fillStyle = "#fff"; ctx.fillRect((n * 21) % 1280, 0, 20, 720); n++;
const s = Math.floor((t - t0) / 1000);
(perSec[s] = perSec[s] || []).push(t - last); last = t;
if (t - t0 < secs * 1000) return requestAnimationFrame(frame);
const out = perSec.map(g => g.length).join(",");
document.title = `[${tag}] done fps/s=${out}`;
}
document.title = `[${tag}] running`;
requestAnimationFrame(frame);
</script></body></html>
+32
View File
@@ -0,0 +1,32 @@
<!doctype html>
<!-- Benchmark content: a long page of text that scrolls itself at a steady
speed, so every frame changes the way reading a real page does. -->
<html lang="en"><head><meta charset="utf-8"><title>fmv-bench scroll</title>
<style>
body { font: 17px/1.55 -apple-system, "Helvetica Neue", sans-serif; margin: 0 auto; max-width: 900px; padding: 24px; color: #1d1d1f; background: #fff; }
h2 { font-size: 22px; margin: 28px 0 8px; } code { background: #f2f2f5; padding: 1px 4px; border-radius: 3px; }
</style></head><body><div id="doc"></div>
<script>
const words = "latency frame panel stream encoder decoder network display pixel window capture budget quality motion text sharp adaptive controller queue socket clock".split(" ");
let seed = 7; const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
let html = "";
for (let s = 0; s < 120; s++) {
html += `<h2>Section ${s + 1}: ${words[s % words.length]} and ${words[(s * 7) % words.length]}</h2>`;
for (let p = 0; p < 4; p++) {
let para = [];
for (let w = 0; w < 70; w++) para.push(rnd() < 0.05 ? `<code>${words[Math.floor(rnd() * words.length)]}()</code>` : words[Math.floor(rnd() * words.length)]);
html += `<p>${para.join(" ")}.</p>`;
}
}
document.getElementById("doc").innerHTML = html;
// 240 points a second, whatever the display's refresh rate.
// The title carries the page's own frame rate, so the source's rate is known.
let last = performance.now(), frames = 0, since = last;
function step(now) {
const dy = (now - last) * 0.24; last = now;
if (++frames && now - since >= 1000) { document.title = `fmv-bench scroll ${frames} fps`; frames = 0; since = now; }
if (window.scrollY + innerHeight >= document.body.scrollHeight - 2) window.scrollTo(0, 0); else window.scrollBy(0, dy);
requestAnimationFrame(step);
}
requestAnimationFrame(step);
</script></body></html>
+9
View File
@@ -0,0 +1,9 @@
<!doctype html>
<!-- Benchmark content: a plain text editor, focused, for typing tests. -->
<html lang="en"><head><meta charset="utf-8"><title>fmv-bench type</title>
<style>
html, body { margin: 0; height: 100%; background: #fff; }
textarea { box-sizing: border-box; width: 100%; height: 100%; border: 0; padding: 20px; outline: none; resize: none;
font: 20px/1.5 ui-monospace, Menlo, monospace; color: #111; caret-color: transparent; } /* a blinking caret would pass for a reply to a key */
</style></head><body><textarea id="t" autofocus spellcheck="false"></textarea>
<script>document.getElementById("t").focus();</script></body></html>
+677
View File
@@ -0,0 +1,677 @@
{
"label": "usb1",
"date": "2026-09-28T21:16:03",
"commit": "1d05f57",
"config": {
"quality": "balanced",
"mode": "separate",
"net": "none",
"delay_ms": 0,
"buffer_ms": 250,
"host": "10.86.200.233",
"ssh_opts": [],
"encoder": "",
"duration_s": 15.0,
"browser_flags": []
},
"frame_build": "20260925.6191901",
"mac": "26.5.2",
"headset": "",
"scenarios": [
{
"scenario": "test",
"frames_sent": 911,
"frames_drawn": 911,
"frames_shown": 610,
"duration_s": 15.2,
"stages_ms": {
"capture": {
"p50": 0.0,
"p95": 0.0,
"p99": 0.0,
"n": 911
},
"queue": {
"p50": 0.0,
"p95": 0.1,
"p99": 0.1,
"n": 911
},
"encode": {
"p50": 4.4,
"p95": 5.0,
"p99": 5.2,
"n": 911
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.2,
"n": 911
},
"network": {
"p50": 1.1,
"p95": 1.6,
"p99": 2.1,
"n": 911
},
"decode": {
"p50": 1.2,
"p95": 2.6,
"p99": 3.9,
"n": 911
},
"draw": {
"p50": 0.4,
"p95": 0.9,
"p99": 1.5,
"n": 911
},
"present": {
"p50": 5.8,
"p95": 13.6,
"p99": 16.7,
"n": 610
},
"content": {
"p50": 7.3,
"p95": 8.8,
"p99": 10.0,
"n": 911
},
"content_shown": {
"p50": 13.6,
"p95": 20.4,
"p99": 24.9,
"n": 610
}
},
"fps": 59.9,
"fps_shown": 40.1,
"late_pct": 0.22,
"stall_max": 32.3,
"stalls_over_100ms": 0,
"mbps": 0.48,
"keyframes": 1,
"size": "1280x720",
"captured": 912,
"viewer_never_drawn": 0,
"input_ms": {
"p50": 16.6,
"p95": 24.3,
"p99": 24.5,
"n": 30
},
"input_shown_ms": {
"p50": 20.8,
"p95": 31.8,
"p99": 33.8,
"n": 22
},
"input_parts_ms": {
"uplink": {
"p50": 0.7,
"p95": 1.0,
"p99": 1.0,
"n": 30
},
"mac": {
"p50": 9.1,
"p95": 15.0,
"p99": 15.2,
"n": 30
},
"back": {
"p50": 7.2,
"p95": 8.5,
"p99": 12.7,
"n": 30
}
},
"inputs": {
"asked": 30,
"sent": 30,
"seen": 30
},
"timeline": [
{
"t": 0,
"fps": 60,
"mbps": 0.5,
"content_p50": 7.3,
"content_p95": 8.6,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 1,
"fps": 60,
"mbps": 0.5,
"content_p50": 7.1,
"content_p95": 8.9,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 2,
"fps": 59,
"mbps": 0.48,
"content_p50": 7.2,
"content_p95": 8.6,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 3,
"fps": 60,
"mbps": 0.49,
"content_p50": 7.1,
"content_p95": 9.0,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 4,
"fps": 60,
"mbps": 0.49,
"content_p50": 6.9,
"content_p95": 8.2,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 5,
"fps": 60,
"mbps": 0.58,
"content_p50": 7.3,
"content_p95": 8.1,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 6,
"fps": 60,
"mbps": 0.47,
"content_p50": 7.4,
"content_p95": 8.6,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 7,
"fps": 60,
"mbps": 0.46,
"content_p50": 7.4,
"content_p95": 8.8,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 8,
"fps": 60,
"mbps": 0.47,
"content_p50": 7.3,
"content_p95": 8.9,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 9,
"fps": 60,
"mbps": 0.46,
"content_p50": 7.3,
"content_p95": 8.8,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 10,
"fps": 60,
"mbps": 0.45,
"content_p50": 7.3,
"content_p95": 8.7,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 11,
"fps": 60,
"mbps": 0.45,
"content_p50": 7.4,
"content_p95": 9.2,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 12,
"fps": 60,
"mbps": 0.46,
"content_p50": 7.4,
"content_p95": 8.4,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 13,
"fps": 60,
"mbps": 0.46,
"content_p50": 7.6,
"content_p95": 9.7,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 14,
"fps": 60,
"mbps": 0.46,
"content_p50": 7.6,
"content_p95": 8.5,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 15,
"fps": 12,
"mbps": 0.09,
"content_p50": 7.5,
"content_p95": 9.6,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
}
],
"adapt": [],
"content_p50": 7.3,
"content_p95": 8.8,
"input_p50": 16.6,
"input_p95": 24.3,
"grades": {
"input_replies": "local",
"content_p50": "local",
"content_p95": "local",
"input_p50": "local",
"input_p95": "local",
"fps": "local",
"late_pct": "local",
"stall_max": "local"
},
"controller": {
"adapt": true,
"baseRtt": 0.917,
"ceiling": 5529600,
"fps": 60,
"inFlight": 1,
"scale": 1,
"slack": 41.666,
"target": 5529600,
"tier": 0
},
"events": [],
"source_fps": 60.0,
"cpu": {
"mac_agent_pct": 16.7,
"frame_viewer_pct": 48.5,
"frame_tailscaled_pct": 0.5,
"frame_sshd_pct": 1.1,
"frame_gamescope_pct": 12.0,
"frame_total_pct": 39.3
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 137 Hz",
"show_s": 1.15,
"panel": "valve.steam.desktopgame.2001639889",
"src": "test"
},
{
"scenario": "scroll",
"frames_sent": 848,
"frames_drawn": 848,
"frames_shown": 617,
"duration_s": 15.1,
"stages_ms": {
"capture": {
"p50": -3.9,
"p95": 4.2,
"p99": 7.3,
"n": 848
},
"queue": {
"p50": 0.0,
"p95": 0.1,
"p99": 0.1,
"n": 848
},
"encode": {
"p50": 6.7,
"p95": 9.7,
"p99": 17.7,
"n": 848
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.4,
"n": 848
},
"network": {
"p50": 1.7,
"p95": 2.7,
"p99": 5.7,
"n": 848
},
"decode": {
"p50": 5.9,
"p95": 10.3,
"p99": 13.1,
"n": 848
},
"draw": {
"p50": 0.7,
"p95": 1.6,
"p99": 2.2,
"n": 848
},
"present": {
"p50": 4.4,
"p95": 15.8,
"p99": 20.6,
"n": 617
},
"content": {
"p50": 15.7,
"p95": 23.0,
"p99": 39.3,
"n": 848
},
"content_shown": {
"p50": 21.3,
"p95": 34.1,
"p99": 45.6,
"n": 617
}
},
"fps": 56.1,
"fps_shown": 40.7,
"late_pct": 3.3,
"stall_max": 238.5,
"stalls_over_100ms": 1,
"mbps": 9.01,
"keyframes": 1,
"size": "1920x1290",
"captured": 857,
"viewer_never_drawn": 0,
"input_ms": {
"n": 0
},
"input_shown_ms": {
"n": 0
},
"input_parts_ms": {
"uplink": {
"n": 0
},
"mac": {
"n": 0
},
"back": {
"n": 0
}
},
"inputs": {
"asked": 0,
"sent": 0,
"seen": 0
},
"timeline": [
{
"t": 0,
"fps": 44,
"mbps": 8.13,
"content_p50": 15.7,
"content_p95": 35.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 1,
"fps": 56,
"mbps": 7.04,
"content_p50": 16.3,
"content_p95": 47.3,
"bitrate": 7430400,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 2,
"fps": 57,
"mbps": 8.58,
"content_p50": 16.0,
"content_p95": 22.6,
"bitrate": 10055362,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 3,
"fps": 56,
"mbps": 9.29,
"content_p50": 16.4,
"content_p95": 21.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 4,
"fps": 58,
"mbps": 9.01,
"content_p50": 15.6,
"content_p95": 21.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 5,
"fps": 57,
"mbps": 9.13,
"content_p50": 15.9,
"content_p95": 21.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 6,
"fps": 56,
"mbps": 11.63,
"content_p50": 16.1,
"content_p95": 22.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 7,
"fps": 57,
"mbps": 8.7,
"content_p50": 16.3,
"content_p95": 22.2,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 8,
"fps": 57,
"mbps": 9.24,
"content_p50": 15.7,
"content_p95": 21.2,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 9,
"fps": 57,
"mbps": 9.21,
"content_p50": 15.0,
"content_p95": 20.5,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 10,
"fps": 56,
"mbps": 8.55,
"content_p50": 15.7,
"content_p95": 20.6,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 11,
"fps": 58,
"mbps": 9.51,
"content_p50": 14.7,
"content_p95": 19.9,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 12,
"fps": 57,
"mbps": 8.75,
"content_p50": 16.0,
"content_p95": 20.4,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 13,
"fps": 58,
"mbps": 9.25,
"content_p50": 15.0,
"content_p95": 20.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 14,
"fps": 57,
"mbps": 9.13,
"content_p50": 15.3,
"content_p95": 21.0,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 15,
"fps": 7,
"mbps": 1.17,
"content_p50": 14.2,
"content_p95": 15.3,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
}
],
"adapt": [],
"content_p50": 15.7,
"content_p95": 23.0,
"input_p50": null,
"input_p95": null,
"grades": {
"content_p50": "local",
"content_p95": "local",
"fps": "acceptable",
"late_pct": "acceptable",
"stall_max": "acceptable"
},
"controller": {
"adapt": true,
"baseRtt": 1.25,
"ceiling": 14860800,
"fps": 60,
"inFlight": 0,
"scale": 1,
"slack": 41.666,
"target": 14860800,
"tier": 0
},
"events": [
{
"t": 1.06,
"e": "down to 7430 kbit/s: queue 35 ms, held 4, oldest 32 ms, base 1 ms, sent 6282 got 4914 wants 11892"
}
],
"source_fps": 56.8,
"cpu": {
"mac_agent_pct": 8.8,
"frame_viewer_pct": 74.8,
"frame_tailscaled_pct": 0.3,
"frame_sshd_pct": 1.2,
"frame_gamescope_pct": 10.5,
"frame_total_pct": 39.0
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 137 Hz",
"show_s": 1.16,
"panel": "valve.steam.desktopgame.2001968301",
"src": "separate:7914"
}
]
}
+690
View File
@@ -0,0 +1,690 @@
{
"label": "usb2",
"date": "2026-09-28T21:17:48",
"commit": "1d05f57",
"config": {
"quality": "balanced",
"mode": "separate",
"net": "none",
"delay_ms": 0,
"buffer_ms": 250,
"host": "10.86.200.233",
"ssh_opts": [],
"encoder": "",
"duration_s": 15.0,
"browser_flags": []
},
"frame_build": "20260925.6191901",
"mac": "26.5.2",
"headset": "",
"scenarios": [
{
"scenario": "test",
"frames_sent": 913,
"frames_drawn": 913,
"frames_shown": 856,
"duration_s": 15.2,
"stages_ms": {
"capture": {
"p50": 0.0,
"p95": 0.0,
"p99": 0.0,
"n": 913
},
"queue": {
"p50": 0.0,
"p95": 0.1,
"p99": 0.1,
"n": 913
},
"encode": {
"p50": 4.3,
"p95": 4.9,
"p99": 5.2,
"n": 913
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.2,
"n": 913
},
"network": {
"p50": 0.9,
"p95": 1.2,
"p99": 1.7,
"n": 913
},
"decode": {
"p50": 1.1,
"p95": 2.5,
"p99": 3.8,
"n": 913
},
"draw": {
"p50": 0.3,
"p95": 0.8,
"p99": 1.2,
"n": 913
},
"present": {
"p50": 2.6,
"p95": 8.6,
"p99": 15.9,
"n": 856
},
"content": {
"p50": 6.9,
"p95": 8.4,
"p99": 9.6,
"n": 913
},
"content_shown": {
"p50": 9.7,
"p95": 15.8,
"p99": 23.1,
"n": 856
}
},
"fps": 60.0,
"fps_shown": 56.3,
"late_pct": 0.0,
"stall_max": 22.0,
"stalls_over_100ms": 0,
"mbps": 0.57,
"keyframes": 1,
"size": "1280x720",
"captured": 913,
"viewer_never_drawn": 0,
"input_ms": {
"p50": 16.9,
"p95": 24.1,
"p99": 26.1,
"n": 33
},
"input_shown_ms": {
"p50": 19.7,
"p95": 26.6,
"p99": 29.2,
"n": 31
},
"input_parts_ms": {
"uplink": {
"p50": 0.7,
"p95": 2.5,
"p99": 5.7,
"n": 33
},
"mac": {
"p50": 8.1,
"p95": 15.4,
"p99": 17.2,
"n": 33
},
"back": {
"p50": 7.3,
"p95": 8.3,
"p99": 8.9,
"n": 33
}
},
"inputs": {
"asked": 30,
"sent": 33,
"seen": 33
},
"timeline": [
{
"t": 0,
"fps": 60,
"mbps": 0.49,
"content_p50": 7.1,
"content_p95": 8.7,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 1,
"fps": 60,
"mbps": 0.47,
"content_p50": 7.0,
"content_p95": 8.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 2,
"fps": 60,
"mbps": 0.48,
"content_p50": 6.9,
"content_p95": 8.4,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 3,
"fps": 60,
"mbps": 0.48,
"content_p50": 7.0,
"content_p95": 9.1,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 4,
"fps": 60,
"mbps": 0.5,
"content_p50": 6.8,
"content_p95": 8.5,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 5,
"fps": 60,
"mbps": 0.67,
"content_p50": 7.0,
"content_p95": 8.2,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 6,
"fps": 60,
"mbps": 0.59,
"content_p50": 7.0,
"content_p95": 8.2,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 7,
"fps": 60,
"mbps": 0.63,
"content_p50": 6.7,
"content_p95": 7.5,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 8,
"fps": 60,
"mbps": 0.62,
"content_p50": 6.9,
"content_p95": 8.2,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 9,
"fps": 60,
"mbps": 0.64,
"content_p50": 6.8,
"content_p95": 8.0,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 10,
"fps": 60,
"mbps": 0.67,
"content_p50": 6.8,
"content_p95": 8.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 11,
"fps": 60,
"mbps": 0.62,
"content_p50": 6.7,
"content_p95": 7.9,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 12,
"fps": 60,
"mbps": 0.59,
"content_p50": 6.8,
"content_p95": 7.9,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 13,
"fps": 60,
"mbps": 0.57,
"content_p50": 6.7,
"content_p95": 8.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 14,
"fps": 60,
"mbps": 0.58,
"content_p50": 7.0,
"content_p95": 8.7,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 15,
"fps": 13,
"mbps": 0.13,
"content_p50": 6.4,
"content_p95": 7.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
}
],
"adapt": [],
"content_p50": 6.9,
"content_p95": 8.4,
"input_p50": 16.9,
"input_p95": 24.1,
"grades": {
"input_replies": "local",
"content_p50": "local",
"content_p95": "local",
"input_p50": "local",
"input_p95": "local",
"fps": "local",
"late_pct": "local",
"stall_max": "local"
},
"controller": {
"adapt": true,
"baseRtt": 0.709,
"ceiling": 5529600,
"fps": 60,
"inFlight": 0,
"scale": 1,
"slack": 41.666,
"target": 5529600,
"tier": 0
},
"events": [],
"source_fps": 60.1,
"cpu": {
"mac_agent_pct": 17.9,
"frame_viewer_pct": 51.9,
"frame_tailscaled_pct": 0.2,
"frame_sshd_pct": 1.1,
"frame_gamescope_pct": 9.6,
"frame_total_pct": 27.9
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 134 Hz",
"show_s": 1.15,
"panel": "valve.steam.desktopgame.2001639889",
"src": "test"
},
{
"scenario": "scroll",
"frames_sent": 868,
"frames_drawn": 868,
"frames_shown": 868,
"duration_s": 15.2,
"stages_ms": {
"capture": {
"p50": -4.2,
"p95": 0.5,
"p99": 6.7,
"n": 868
},
"queue": {
"p50": 0.0,
"p95": 0.1,
"p99": 0.2,
"n": 868
},
"encode": {
"p50": 6.8,
"p95": 10.2,
"p99": 17.9,
"n": 868
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.4,
"n": 868
},
"network": {
"p50": 1.5,
"p95": 2.3,
"p99": 7.6,
"n": 868
},
"decode": {
"p50": 5.9,
"p95": 10.2,
"p99": 20.3,
"n": 868
},
"draw": {
"p50": 0.7,
"p95": 1.4,
"p99": 2.1,
"n": 868
},
"present": {
"p50": 0.9,
"p95": 6.1,
"p99": 6.8,
"n": 868
},
"content": {
"p50": 15.3,
"p95": 23.5,
"p99": 53.1,
"n": 868
},
"content_shown": {
"p50": 17.7,
"p95": 25.6,
"p99": 57.5,
"n": 868
}
},
"fps": 57.2,
"fps_shown": 57.2,
"late_pct": 3.23,
"stall_max": 108.0,
"stalls_over_100ms": 1,
"mbps": 9.84,
"keyframes": 1,
"size": "1920x1290",
"captured": 870,
"viewer_never_drawn": 0,
"input_ms": {
"p50": 24.2,
"p95": 36.3,
"p99": 36.3,
"n": 6
},
"input_shown_ms": {
"p50": 24.8,
"p95": 38.5,
"p99": 38.5,
"n": 6
},
"input_parts_ms": {
"uplink": {
"p50": 2.0,
"p95": 5.4,
"p99": 5.4,
"n": 6
},
"mac": {
"p50": 7.5,
"p95": 15.8,
"p99": 15.8,
"n": 6
},
"back": {
"p50": 15.3,
"p95": 16.1,
"p99": 16.1,
"n": 6
}
},
"inputs": {
"asked": 0,
"sent": 6,
"seen": 6
},
"timeline": [
{
"t": 0,
"fps": 53,
"mbps": 9.21,
"content_p50": 16.1,
"content_p95": 71.2,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 1,
"fps": 57,
"mbps": 9.64,
"content_p50": 15.9,
"content_p95": 46.8,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 2,
"fps": 58,
"mbps": 8.99,
"content_p50": 14.8,
"content_p95": 19.2,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 3,
"fps": 57,
"mbps": 9.72,
"content_p50": 15.2,
"content_p95": 22.3,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 4,
"fps": 58,
"mbps": 9.64,
"content_p50": 16.1,
"content_p95": 23.5,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 5,
"fps": 57,
"mbps": 11.27,
"content_p50": 15.6,
"content_p95": 20.9,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 6,
"fps": 58,
"mbps": 10.44,
"content_p50": 15.1,
"content_p95": 19.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 7,
"fps": 57,
"mbps": 9.31,
"content_p50": 15.0,
"content_p95": 19.9,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 8,
"fps": 58,
"mbps": 11.69,
"content_p50": 15.9,
"content_p95": 22.0,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 9,
"fps": 58,
"mbps": 10.31,
"content_p50": 14.5,
"content_p95": 20.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 10,
"fps": 57,
"mbps": 10.47,
"content_p50": 15.2,
"content_p95": 20.6,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 11,
"fps": 58,
"mbps": 9.44,
"content_p50": 15.3,
"content_p95": 20.5,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 12,
"fps": 57,
"mbps": 9.17,
"content_p50": 15.9,
"content_p95": 18.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 13,
"fps": 58,
"mbps": 8.77,
"content_p50": 15.1,
"content_p95": 18.6,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 14,
"fps": 58,
"mbps": 9.72,
"content_p50": 15.2,
"content_p95": 19.8,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 15,
"fps": 9,
"mbps": 1.33,
"content_p50": 14.8,
"content_p95": 17.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
}
],
"adapt": [],
"content_p50": 15.3,
"content_p95": 23.5,
"input_p50": 24.2,
"input_p95": 36.3,
"grades": {
"input_replies": "local",
"content_p50": "local",
"content_p95": "local",
"input_p50": "local",
"input_p95": "local",
"fps": "acceptable",
"late_pct": "acceptable",
"stall_max": "acceptable"
},
"controller": {
"adapt": true,
"baseRtt": 1.375,
"ceiling": 14860800,
"fps": 60,
"inFlight": 1,
"scale": 1,
"slack": 41.666,
"target": 14860800,
"tier": 0
},
"events": [],
"source_fps": 57.2,
"cpu": {
"mac_agent_pct": 11.4,
"frame_viewer_pct": 83.5,
"frame_tailscaled_pct": 0.3,
"frame_sshd_pct": 1.5,
"frame_gamescope_pct": 9.7,
"frame_total_pct": 32.5
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 139 Hz",
"show_s": 1.15,
"panel": "valve.steam.desktopgame.2001786096",
"src": "separate:8187"
}
]
}
+682
View File
@@ -0,0 +1,682 @@
{
"label": "wifi1",
"date": "2026-09-28T21:16:56",
"commit": "1d05f57",
"config": {
"quality": "balanced",
"mode": "separate",
"net": "none",
"delay_ms": 0,
"buffer_ms": 250,
"host": "frame",
"ssh_opts": [],
"encoder": "",
"duration_s": 15.0,
"browser_flags": []
},
"frame_build": "20260925.6191901",
"mac": "26.5.2",
"headset": "",
"scenarios": [
{
"scenario": "test",
"frames_sent": 912,
"frames_drawn": 912,
"frames_shown": 611,
"duration_s": 15.2,
"stages_ms": {
"capture": {
"p50": 0.0,
"p95": 0.0,
"p99": 0.0,
"n": 912
},
"queue": {
"p50": 0.0,
"p95": 0.1,
"p99": 0.1,
"n": 912
},
"encode": {
"p50": 4.3,
"p95": 4.9,
"p99": 5.1,
"n": 912
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.2,
"n": 912
},
"network": {
"p50": 4.0,
"p95": 5.3,
"p99": 7.8,
"n": 912
},
"decode": {
"p50": 1.1,
"p95": 2.8,
"p99": 4.0,
"n": 912
},
"draw": {
"p50": 0.4,
"p95": 0.8,
"p99": 1.2,
"n": 912
},
"present": {
"p50": 5.9,
"p95": 14.0,
"p99": 18.5,
"n": 611
},
"content": {
"p50": 10.1,
"p95": 12.3,
"p99": 13.9,
"n": 912
},
"content_shown": {
"p50": 16.7,
"p95": 23.6,
"p99": 28.3,
"n": 611
}
},
"fps": 60.0,
"fps_shown": 40.1,
"late_pct": 0.11,
"stall_max": 25.8,
"stalls_over_100ms": 0,
"mbps": 0.48,
"keyframes": 1,
"size": "1280x720",
"captured": 912,
"viewer_never_drawn": 0,
"input_ms": {
"p50": 26.4,
"p95": 34.2,
"p99": 53.5,
"n": 30
},
"input_shown_ms": {
"p50": 28.6,
"p95": 41.5,
"p99": 55.9,
"n": 18
},
"input_parts_ms": {
"uplink": {
"p50": 8.9,
"p95": 20.4,
"p99": 28.0,
"n": 30
},
"mac": {
"p50": 5.1,
"p95": 14.5,
"p99": 16.2,
"n": 30
},
"back": {
"p50": 10.1,
"p95": 13.7,
"p99": 13.8,
"n": 30
}
},
"inputs": {
"asked": 30,
"sent": 30,
"seen": 30
},
"timeline": [
{
"t": 0,
"fps": 60,
"mbps": 0.51,
"content_p50": 10.3,
"content_p95": 11.9,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 1,
"fps": 60,
"mbps": 0.47,
"content_p50": 10.5,
"content_p95": 12.1,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 2,
"fps": 60,
"mbps": 0.49,
"content_p50": 10.1,
"content_p95": 12.8,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 3,
"fps": 60,
"mbps": 0.48,
"content_p50": 9.9,
"content_p95": 11.6,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 4,
"fps": 60,
"mbps": 0.47,
"content_p50": 9.7,
"content_p95": 11.8,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 5,
"fps": 60,
"mbps": 0.59,
"content_p50": 10.2,
"content_p95": 11.7,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 6,
"fps": 60,
"mbps": 0.47,
"content_p50": 10.2,
"content_p95": 12.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 7,
"fps": 60,
"mbps": 0.47,
"content_p50": 9.7,
"content_p95": 12.1,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 8,
"fps": 60,
"mbps": 0.47,
"content_p50": 10.4,
"content_p95": 13.1,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 9,
"fps": 60,
"mbps": 0.47,
"content_p50": 9.9,
"content_p95": 12.7,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 10,
"fps": 60,
"mbps": 0.47,
"content_p50": 9.8,
"content_p95": 12.4,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 11,
"fps": 60,
"mbps": 0.46,
"content_p50": 10.3,
"content_p95": 12.7,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 12,
"fps": 60,
"mbps": 0.49,
"content_p50": 10.2,
"content_p95": 12.0,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 13,
"fps": 60,
"mbps": 0.48,
"content_p50": 10.0,
"content_p95": 11.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 14,
"fps": 60,
"mbps": 0.46,
"content_p50": 10.2,
"content_p95": 12.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 15,
"fps": 12,
"mbps": 0.1,
"content_p50": 10.7,
"content_p95": 11.4,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
}
],
"adapt": [],
"content_p50": 10.1,
"content_p95": 12.3,
"input_p50": 26.4,
"input_p95": 34.2,
"grades": {
"input_replies": "local",
"content_p50": "local",
"content_p95": "local",
"input_p50": "local",
"input_p95": "local",
"fps": "local",
"late_pct": "local",
"stall_max": "local"
},
"controller": {
"adapt": true,
"baseRtt": 4.958,
"ceiling": 5529600,
"fps": 60,
"inFlight": 1,
"scale": 1,
"slack": 47.417,
"target": 2764800,
"tier": 0
},
"events": [
{
"t": 16.58,
"e": "down to 2764 kbit/s: queue 19 ms, held 6, oldest 210 ms, base 4 ms, sent 322 got 265 wants 459"
}
],
"source_fps": 60.0,
"cpu": {
"mac_agent_pct": 15.7,
"frame_viewer_pct": 42.5,
"frame_tailscaled_pct": 5.5,
"frame_sshd_pct": 0.7,
"frame_gamescope_pct": 8.7,
"frame_total_pct": 26.1
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 136 Hz",
"show_s": 1.26,
"panel": "valve.steam.desktopgame.2001639889",
"src": "test"
},
{
"scenario": "scroll",
"frames_sent": 852,
"frames_drawn": 852,
"frames_shown": 614,
"duration_s": 15.2,
"stages_ms": {
"capture": {
"p50": -4.4,
"p95": 2.6,
"p99": 4.2,
"n": 852
},
"queue": {
"p50": 0.0,
"p95": 0.1,
"p99": 0.1,
"n": 852
},
"encode": {
"p50": 6.7,
"p95": 9.8,
"p99": 16.7,
"n": 852
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.2,
"n": 852
},
"network": {
"p50": 5.3,
"p95": 13.1,
"p99": 28.2,
"n": 852
},
"decode": {
"p50": 6.0,
"p95": 10.9,
"p99": 19.0,
"n": 852
},
"draw": {
"p50": 0.7,
"p95": 1.4,
"p99": 1.8,
"n": 852
},
"present": {
"p50": 4.1,
"p95": 16.2,
"p99": 20.6,
"n": 614
},
"content": {
"p50": 19.5,
"p95": 31.4,
"p99": 64.5,
"n": 852
},
"content_shown": {
"p50": 25.4,
"p95": 38.8,
"p99": 76.7,
"n": 614
}
},
"fps": 56.2,
"fps_shown": 40.5,
"late_pct": 4.69,
"stall_max": 253.7,
"stalls_over_100ms": 2,
"mbps": 9.02,
"keyframes": 1,
"size": "1920x1290",
"captured": 866,
"viewer_never_drawn": 0,
"input_ms": {
"n": 0
},
"input_shown_ms": {
"n": 0
},
"input_parts_ms": {
"uplink": {
"n": 0
},
"mac": {
"n": 0
},
"back": {
"n": 0
}
},
"inputs": {
"asked": 0,
"sent": 0,
"seen": 0
},
"timeline": [
{
"t": 0,
"fps": 45,
"mbps": 7.77,
"content_p50": 19.0,
"content_p95": 163.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 1,
"fps": 51,
"mbps": 6.54,
"content_p50": 19.4,
"content_p95": 107.0,
"bitrate": 7430400,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 2,
"fps": 57,
"mbps": 8.64,
"content_p50": 19.9,
"content_p95": 31.8,
"bitrate": 10055362,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 3,
"fps": 58,
"mbps": 9.46,
"content_p50": 19.1,
"content_p95": 24.9,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 4,
"fps": 58,
"mbps": 9.11,
"content_p50": 19.4,
"content_p95": 22.9,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 5,
"fps": 57,
"mbps": 9.18,
"content_p50": 19.9,
"content_p95": 25.3,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 6,
"fps": 58,
"mbps": 11.64,
"content_p50": 19.8,
"content_p95": 49.6,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 7,
"fps": 57,
"mbps": 9.12,
"content_p50": 20.5,
"content_p95": 31.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 8,
"fps": 58,
"mbps": 9.29,
"content_p50": 19.5,
"content_p95": 25.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 9,
"fps": 57,
"mbps": 9.43,
"content_p50": 19.8,
"content_p95": 49.6,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 10,
"fps": 58,
"mbps": 8.59,
"content_p50": 19.2,
"content_p95": 24.8,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 11,
"fps": 57,
"mbps": 9.42,
"content_p50": 19.9,
"content_p95": 27.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 12,
"fps": 58,
"mbps": 9.0,
"content_p50": 20.6,
"content_p95": 31.2,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 13,
"fps": 57,
"mbps": 8.84,
"content_p50": 19.6,
"content_p95": 25.3,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 14,
"fps": 57,
"mbps": 9.17,
"content_p50": 19.3,
"content_p95": 27.8,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 15,
"fps": 9,
"mbps": 1.52,
"content_p50": 19.3,
"content_p95": 30.0,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
}
],
"adapt": [],
"content_p50": 19.5,
"content_p95": 31.4,
"input_p50": null,
"input_p95": null,
"grades": {
"content_p50": "local",
"content_p95": "local",
"fps": "acceptable",
"late_pct": "acceptable",
"stall_max": "bad"
},
"controller": {
"adapt": true,
"baseRtt": 6.959,
"ceiling": 14860800,
"fps": 60,
"inFlight": 2,
"scale": 1,
"slack": 43.54,
"target": 14860800,
"tier": 0
},
"events": [
{
"t": 1.09,
"e": "down to 7430 kbit/s: queue 131 ms, held 3, oldest 231 ms, base 6 ms, sent 4272 got 3724 wants 9321"
}
],
"source_fps": 57.0,
"cpu": {
"mac_agent_pct": 8.5,
"frame_viewer_pct": 71.7,
"frame_tailscaled_pct": 8.4,
"frame_sshd_pct": 0.9,
"frame_gamescope_pct": 8.4,
"frame_total_pct": 30.1
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 139 Hz",
"show_s": 1.25,
"panel": "valve.steam.desktopgame.2001195625",
"src": "separate:8050"
}
]
}
+682
View File
@@ -0,0 +1,682 @@
{
"label": "wifi2",
"date": "2026-09-28T21:18:40",
"commit": "1d05f57",
"config": {
"quality": "balanced",
"mode": "separate",
"net": "none",
"delay_ms": 0,
"buffer_ms": 250,
"host": "frame",
"ssh_opts": [],
"encoder": "",
"duration_s": 15.0,
"browser_flags": []
},
"frame_build": "20260925.6191901",
"mac": "26.5.2",
"headset": "",
"scenarios": [
{
"scenario": "test",
"frames_sent": 902,
"frames_drawn": 902,
"frames_shown": 604,
"duration_s": 15.2,
"stages_ms": {
"capture": {
"p50": 0.0,
"p95": 0.0,
"p99": 0.0,
"n": 902
},
"queue": {
"p50": 0.0,
"p95": 0.1,
"p99": 0.1,
"n": 902
},
"encode": {
"p50": 4.3,
"p95": 5.0,
"p99": 5.3,
"n": 902
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.3,
"n": 902
},
"network": {
"p50": 3.8,
"p95": 5.3,
"p99": 8.0,
"n": 902
},
"decode": {
"p50": 1.1,
"p95": 2.8,
"p99": 3.8,
"n": 902
},
"draw": {
"p50": 0.3,
"p95": 0.8,
"p99": 1.2,
"n": 902
},
"present": {
"p50": 5.9,
"p95": 17.0,
"p99": 18.6,
"n": 604
},
"content": {
"p50": 9.8,
"p95": 12.1,
"p99": 14.4,
"n": 902
},
"content_shown": {
"p50": 15.2,
"p95": 27.0,
"p99": 28.4,
"n": 604
}
},
"fps": 59.3,
"fps_shown": 39.7,
"late_pct": 0.55,
"stall_max": 187.5,
"stalls_over_100ms": 1,
"mbps": 0.47,
"keyframes": 1,
"size": "1280x720",
"captured": 913,
"viewer_never_drawn": 0,
"input_ms": {
"p50": 27.8,
"p95": 34.9,
"p99": 206.9,
"n": 30
},
"input_shown_ms": {
"p50": 35.1,
"p95": 52.6,
"p99": 207.2,
"n": 24
},
"input_parts_ms": {
"uplink": {
"p50": 9.8,
"p95": 22.5,
"p99": 184.8,
"n": 30
},
"mac": {
"p50": 5.3,
"p95": 14.4,
"p99": 19.6,
"n": 30
},
"back": {
"p50": 9.8,
"p95": 13.4,
"p99": 13.9,
"n": 30
}
},
"inputs": {
"asked": 30,
"sent": 30,
"seen": 30
},
"timeline": [
{
"t": 0,
"fps": 60,
"mbps": 0.48,
"content_p50": 9.3,
"content_p95": 11.2,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 1,
"fps": 50,
"mbps": 0.39,
"content_p50": 9.1,
"content_p95": 14.4,
"bitrate": 2764800,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 2,
"fps": 60,
"mbps": 0.48,
"content_p50": 9.2,
"content_p95": 11.8,
"bitrate": 2764800,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 3,
"fps": 60,
"mbps": 0.48,
"content_p50": 9.4,
"content_p95": 10.5,
"bitrate": 2764800,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 4,
"fps": 60,
"mbps": 0.48,
"content_p50": 9.5,
"content_p95": 12.5,
"bitrate": 3091280,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 5,
"fps": 60,
"mbps": 0.47,
"content_p50": 10.0,
"content_p95": 12.8,
"bitrate": 4757991,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 6,
"fps": 60,
"mbps": 0.57,
"content_p50": 9.5,
"content_p95": 11.7,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 7,
"fps": 60,
"mbps": 0.47,
"content_p50": 9.8,
"content_p95": 11.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 8,
"fps": 60,
"mbps": 0.47,
"content_p50": 10.2,
"content_p95": 12.2,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 9,
"fps": 60,
"mbps": 0.47,
"content_p50": 10.0,
"content_p95": 13.2,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 10,
"fps": 60,
"mbps": 0.47,
"content_p50": 10.2,
"content_p95": 12.5,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 11,
"fps": 60,
"mbps": 0.45,
"content_p50": 9.9,
"content_p95": 11.6,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 12,
"fps": 60,
"mbps": 0.46,
"content_p50": 10.0,
"content_p95": 11.6,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 13,
"fps": 60,
"mbps": 0.47,
"content_p50": 10.3,
"content_p95": 12.3,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 14,
"fps": 60,
"mbps": 0.48,
"content_p50": 9.8,
"content_p95": 11.6,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
},
{
"t": 15,
"fps": 12,
"mbps": 0.1,
"content_p50": 9.3,
"content_p95": 11.5,
"bitrate": 5529600,
"tier": 0,
"w": 1280,
"net": null
}
],
"adapt": [],
"content_p50": 9.8,
"content_p95": 12.1,
"input_p50": 27.8,
"input_p95": 34.9,
"grades": {
"input_replies": "local",
"content_p50": "local",
"content_p95": "local",
"input_p50": "local",
"input_p95": "local",
"fps": "local",
"late_pct": "local",
"stall_max": "acceptable"
},
"controller": {
"adapt": true,
"baseRtt": 5.084,
"ceiling": 5529600,
"fps": 60,
"inFlight": 1,
"scale": 1,
"slack": 49.603,
"target": 5529600,
"tier": 0
},
"events": [
{
"t": 1.77,
"e": "down to 2764 kbit/s: queue 23 ms, held 5, oldest 0 ms, base 5 ms, sent 315 got 315 wants 472"
}
],
"source_fps": 60.1,
"cpu": {
"mac_agent_pct": 16.1,
"frame_viewer_pct": 40.9,
"frame_tailscaled_pct": 5.2,
"frame_sshd_pct": 0.7,
"frame_gamescope_pct": 8.4,
"frame_total_pct": 25.2
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 136 Hz",
"show_s": 1.27,
"panel": "valve.steam.desktopgame.2001639889",
"src": "test"
},
{
"scenario": "scroll",
"frames_sent": 832,
"frames_drawn": 832,
"frames_shown": 602,
"duration_s": 15.1,
"stages_ms": {
"capture": {
"p50": -3.9,
"p95": 0.8,
"p99": 6.8,
"n": 832
},
"queue": {
"p50": 0.0,
"p95": 0.1,
"p99": 1.9,
"n": 832
},
"encode": {
"p50": 6.8,
"p95": 10.5,
"p99": 18.8,
"n": 832
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.5,
"n": 832
},
"network": {
"p50": 5.8,
"p95": 18.7,
"p99": 56.7,
"n": 832
},
"decode": {
"p50": 5.9,
"p95": 11.7,
"p99": 26.0,
"n": 832
},
"draw": {
"p50": 0.7,
"p95": 1.4,
"p99": 1.7,
"n": 832
},
"present": {
"p50": 4.4,
"p95": 18.5,
"p99": 25.0,
"n": 602
},
"content": {
"p50": 20.2,
"p95": 36.9,
"p99": 90.7,
"n": 832
},
"content_shown": {
"p50": 26.4,
"p95": 45.4,
"p99": 101.0,
"n": 602
}
},
"fps": 54.9,
"fps_shown": 39.7,
"late_pct": 6.96,
"stall_max": 204.9,
"stalls_over_100ms": 2,
"mbps": 9.04,
"keyframes": 1,
"size": "1920x1290",
"captured": 852,
"viewer_never_drawn": 0,
"input_ms": {
"n": 0
},
"input_shown_ms": {
"n": 0
},
"input_parts_ms": {
"uplink": {
"n": 0
},
"mac": {
"n": 0
},
"back": {
"n": 0
}
},
"inputs": {
"asked": 0,
"sent": 0,
"seen": 0
},
"timeline": [
{
"t": 0,
"fps": 49,
"mbps": 8.68,
"content_p50": 19.8,
"content_p95": 119.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 1,
"fps": 47,
"mbps": 6.06,
"content_p50": 19.8,
"content_p95": 88.6,
"bitrate": 7430400,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 2,
"fps": 52,
"mbps": 8.07,
"content_p50": 19.4,
"content_p95": 25.5,
"bitrate": 10055362,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 3,
"fps": 56,
"mbps": 9.79,
"content_p50": 21.4,
"content_p95": 29.1,
"bitrate": 13549185,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 4,
"fps": 57,
"mbps": 8.93,
"content_p50": 21.5,
"content_p95": 35.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 5,
"fps": 57,
"mbps": 10.59,
"content_p50": 21.1,
"content_p95": 35.4,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 6,
"fps": 56,
"mbps": 11.55,
"content_p50": 19.7,
"content_p95": 66.9,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 7,
"fps": 57,
"mbps": 8.8,
"content_p50": 18.8,
"content_p95": 24.8,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 8,
"fps": 57,
"mbps": 9.17,
"content_p50": 19.5,
"content_p95": 24.4,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 9,
"fps": 57,
"mbps": 9.39,
"content_p50": 19.4,
"content_p95": 23.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 10,
"fps": 57,
"mbps": 8.53,
"content_p50": 19.5,
"content_p95": 38.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 11,
"fps": 56,
"mbps": 9.23,
"content_p50": 20.7,
"content_p95": 30.9,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 12,
"fps": 53,
"mbps": 8.87,
"content_p50": 20.6,
"content_p95": 35.4,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 13,
"fps": 57,
"mbps": 8.94,
"content_p50": 20.4,
"content_p95": 28.2,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 14,
"fps": 57,
"mbps": 9.14,
"content_p50": 21.0,
"content_p95": 35.5,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 15,
"fps": 7,
"mbps": 1.2,
"content_p50": 21.8,
"content_p95": 23.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
}
],
"adapt": [],
"content_p50": 20.2,
"content_p95": 36.9,
"input_p50": null,
"input_p95": null,
"grades": {
"content_p50": "local",
"content_p95": "local",
"fps": "acceptable",
"late_pct": "bad",
"stall_max": "acceptable"
},
"controller": {
"adapt": true,
"baseRtt": 6.833,
"ceiling": 14860800,
"fps": 60,
"inFlight": 2,
"scale": 1,
"slack": 44.791,
"target": 14860800,
"tier": 0
},
"events": [
{
"t": 1.22,
"e": "down to 7430 kbit/s: queue 74 ms, held 7, oldest 235 ms, base 6 ms, sent 5436 got 4487 wants 11082"
}
],
"source_fps": 56.4,
"cpu": {
"mac_agent_pct": 8.8,
"frame_viewer_pct": 69.5,
"frame_tailscaled_pct": 9.5,
"frame_sshd_pct": 0.8,
"frame_gamescope_pct": 8.4,
"frame_total_pct": 31.8
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 139 Hz",
"show_s": 1.25,
"panel": "valve.steam.desktopgame.2001116820",
"src": "separate:8327"
}
]
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,984 @@
{
"label": "adapt-clean",
"date": "2026-09-28T17:13:59",
"commit": "a3c6e5c+dirty",
"config": {
"quality": "balanced",
"mode": "separate",
"net": "none",
"delay_ms": 0,
"buffer_ms": 250,
"host": "frame",
"ssh_opts": [],
"encoder": "",
"duration_s": 20.0,
"browser_flags": []
},
"frame_build": "20260925.6191901",
"mac": "26.5.2",
"headset": ":39.639829 [Info] - 0 - entering standby",
"scenarios": [
{
"scenario": "test",
"frames_sent": 653,
"frames_drawn": 653,
"frames_shown": 649,
"duration_s": 20.7,
"stages_ms": {
"capture": {
"p50": 0.0,
"p95": 0.0,
"p99": 0.0,
"n": 653
},
"queue": {
"p50": 11.2,
"p95": 16.1,
"p99": 17.1,
"n": 653
},
"encode": {
"p50": 3.5,
"p95": 4.2,
"p99": 4.4,
"n": 653
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.2,
"n": 653
},
"network": {
"p50": 2.9,
"p95": 7.0,
"p99": 12.0,
"n": 653
},
"decode": {
"p50": 1.0,
"p95": 2.4,
"p99": 2.8,
"n": 653
},
"draw": {
"p50": 0.3,
"p95": 0.6,
"p99": 0.9,
"n": 653
},
"present": {
"p50": 0.5,
"p95": 0.9,
"p99": 5.9,
"n": 649
},
"content": {
"p50": 19.8,
"p95": 26.2,
"p99": 30.1,
"n": 653
},
"content_shown": {
"p50": 20.3,
"p95": 26.9,
"p99": 31.1,
"n": 649
}
},
"fps": 31.5,
"fps_shown": 31.3,
"late_pct": 98.01,
"stall_max": 144.0,
"stalls_over_100ms": 2,
"mbps": 0.13,
"keyframes": 2,
"size": "960x540",
"captured": 1336,
"viewer_never_drawn": 0,
"input_ms": {
"p50": 43.5,
"p95": 61.8,
"p99": 63.4,
"n": 40
},
"input_shown_ms": {
"p50": 44.3,
"p95": 62.3,
"p99": 63.9,
"n": 40
},
"input_parts_ms": {
"uplink": {
"p50": 11.1,
"p95": 29.2,
"p99": 36.1,
"n": 40
},
"mac": {
"p50": 10.4,
"p95": 28.2,
"p99": 32.4,
"n": 40
},
"back": {
"p50": 17.5,
"p95": 27.9,
"p99": 28.8,
"n": 40
}
},
"inputs": {
"sent": 40,
"seen": 40
},
"timeline": [
{
"t": 0,
"fps": 31,
"mbps": 0.13,
"content_p50": 21.5,
"content_p95": 27.1,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 1,
"fps": 31,
"mbps": 0.12,
"content_p50": 22.4,
"content_p95": 26.3,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 2,
"fps": 32,
"mbps": 0.13,
"content_p50": 18.9,
"content_p95": 24.8,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 3,
"fps": 31,
"mbps": 0.12,
"content_p50": 18.2,
"content_p95": 24.6,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 4,
"fps": 32,
"mbps": 0.13,
"content_p50": 21.0,
"content_p95": 25.6,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 5,
"fps": 32,
"mbps": 0.12,
"content_p50": 19.5,
"content_p95": 24.3,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 6,
"fps": 31,
"mbps": 0.12,
"content_p50": 22.2,
"content_p95": 25.6,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 7,
"fps": 32,
"mbps": 0.13,
"content_p50": 19.9,
"content_p95": 24.0,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 8,
"fps": 31,
"mbps": 0.21,
"content_p50": 22.7,
"content_p95": 26.9,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 9,
"fps": 31,
"mbps": 0.12,
"content_p50": 20.2,
"content_p95": 26.1,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 10,
"fps": 32,
"mbps": 0.12,
"content_p50": 18.2,
"content_p95": 23.2,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 11,
"fps": 31,
"mbps": 0.12,
"content_p50": 19.8,
"content_p95": 24.2,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 12,
"fps": 32,
"mbps": 0.13,
"content_p50": 19.2,
"content_p95": 24.8,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 13,
"fps": 32,
"mbps": 0.12,
"content_p50": 21.0,
"content_p95": 24.3,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 14,
"fps": 30,
"mbps": 0.13,
"content_p50": 20.4,
"content_p95": 96.7,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 15,
"fps": 32,
"mbps": 0.13,
"content_p50": 18.2,
"content_p95": 24.4,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 16,
"fps": 31,
"mbps": 0.12,
"content_p50": 18.4,
"content_p95": 25.1,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 17,
"fps": 32,
"mbps": 0.13,
"content_p50": 17.2,
"content_p95": 23.5,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 18,
"fps": 32,
"mbps": 0.19,
"content_p50": 19.4,
"content_p95": 25.2,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 19,
"fps": 31,
"mbps": 0.14,
"content_p50": 21.1,
"content_p95": 84.3,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 20,
"fps": 24,
"mbps": 0.08,
"content_p50": 20.8,
"content_p95": 25.1,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
}
],
"adapt": [],
"content_p50": 19.8,
"content_p95": 26.2,
"input_p50": 43.5,
"input_p95": 61.8,
"grades": {
"content_p50": "local",
"content_p95": "local",
"input_p50": "local",
"input_p95": "local",
"fps": "bad",
"late_pct": "bad",
"stall_max": "acceptable"
},
"source_fps": 64.5,
"cpu": {
"mac_agent_pct": 11.3,
"frame_viewer_pct": 35.9,
"frame_tailscaled_pct": 3.5,
"frame_sshd_pct": 0.6,
"frame_gamescope_pct": 7.5,
"frame_total_pct": 16.1
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 141 Hz",
"show_s": 4.68,
"panel": "valve.steam.desktopgame.2001639889",
"src": "test"
},
{
"scenario": "scroll",
"frames_sent": 937,
"frames_drawn": 937,
"frames_shown": 720,
"duration_s": 20.7,
"stages_ms": {
"capture": {
"p50": -3.9,
"p95": 0.9,
"p99": 8.2,
"n": 937
},
"queue": {
"p50": 0.0,
"p95": 16.3,
"p99": 20.1,
"n": 937
},
"encode": {
"p50": 6.4,
"p95": 7.2,
"p99": 9.3,
"n": 937
},
"socket": {
"p50": 0.1,
"p95": 0.2,
"p99": 0.2,
"n": 937
},
"network": {
"p50": 7.3,
"p95": 13.0,
"p99": 22.8,
"n": 937
},
"decode": {
"p50": 6.2,
"p95": 12.7,
"p99": 18.6,
"n": 937
},
"draw": {
"p50": 0.6,
"p95": 1.1,
"p99": 1.9,
"n": 937
},
"present": {
"p50": 2.9,
"p95": 18.5,
"p99": 24.5,
"n": 720
},
"content": {
"p50": 19.9,
"p95": 37.2,
"p99": 50.2,
"n": 937
},
"content_shown": {
"p50": 27.0,
"p95": 45.7,
"p99": 55.0,
"n": 720
}
},
"fps": 45.3,
"fps_shown": 34.8,
"late_pct": 28.63,
"stall_max": 99.3,
"stalls_over_100ms": 0,
"mbps": 7.59,
"keyframes": 2,
"size": "1920x1290",
"captured": 1215,
"viewer_never_drawn": 0,
"input_ms": {
"n": 0
},
"input_shown_ms": {
"n": 0
},
"input_parts_ms": {
"uplink": {
"n": 0
},
"mac": {
"n": 0
},
"back": {
"n": 0
}
},
"inputs": {
"sent": 0,
"seen": 0
},
"timeline": [
{
"t": 0,
"fps": 30,
"mbps": 4.01,
"content_p50": 31.9,
"content_p95": 52.3,
"bitrate": 4867140,
"tier": 2,
"w": 1920,
"net": null
},
{
"t": 1,
"fps": 30,
"mbps": 4.12,
"content_p50": 29.9,
"content_p95": 45.3,
"bitrate": 3807054,
"tier": 2,
"w": 1920,
"net": null
},
{
"t": 2,
"fps": 30,
"mbps": 3.26,
"content_p50": 31.8,
"content_p95": 52.5,
"bitrate": 3235995,
"tier": 2,
"w": 1920,
"net": null
},
{
"t": 3,
"fps": 30,
"mbps": 3.72,
"content_p50": 24.5,
"content_p95": 37.7,
"bitrate": 4238740,
"tier": 2,
"w": 1920,
"net": null
},
{
"t": 4,
"fps": 30,
"mbps": 5.09,
"content_p50": 25.2,
"content_p95": 39.6,
"bitrate": 5992063,
"tier": 2,
"w": 1920,
"net": null
},
{
"t": 5,
"fps": 31,
"mbps": 7.29,
"content_p50": 29.1,
"content_p95": 53.5,
"bitrate": 5526067,
"tier": 2,
"w": 1920,
"net": null
},
{
"t": 6,
"fps": 35,
"mbps": 4.58,
"content_p50": 25.3,
"content_p95": 32.4,
"bitrate": 6018152,
"tier": 1,
"w": 1920,
"net": null
},
{
"t": 7,
"fps": 46,
"mbps": 7.12,
"content_p50": 25.1,
"content_p95": 34.3,
"bitrate": 8412933,
"tier": 1,
"w": 1920,
"net": null
},
{
"t": 8,
"fps": 46,
"mbps": 8.25,
"content_p50": 27.1,
"content_p95": 40.3,
"bitrate": 10760191,
"tier": 1,
"w": 1920,
"net": null
},
{
"t": 9,
"fps": 47,
"mbps": 9.3,
"content_p50": 27.8,
"content_p95": 41.2,
"bitrate": 13717060,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 10,
"fps": 55,
"mbps": 8.98,
"content_p50": 16.5,
"content_p95": 21.7,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 11,
"fps": 51,
"mbps": 9.32,
"content_p50": 18.6,
"content_p95": 35.4,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 12,
"fps": 56,
"mbps": 9.53,
"content_p50": 15.4,
"content_p95": 26.2,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 13,
"fps": 55,
"mbps": 8.46,
"content_p50": 18.6,
"content_p95": 23.2,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 14,
"fps": 54,
"mbps": 9.82,
"content_p50": 15.9,
"content_p95": 28.4,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 15,
"fps": 54,
"mbps": 11.11,
"content_p50": 15.5,
"content_p95": 26.1,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 16,
"fps": 54,
"mbps": 9.79,
"content_p50": 17.4,
"content_p95": 25.3,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 17,
"fps": 57,
"mbps": 9.3,
"content_p50": 16.3,
"content_p95": 23.5,
"bitrate": 14860800,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 18,
"fps": 55,
"mbps": 8.3,
"content_p50": 19.5,
"content_p95": 31.0,
"bitrate": 7984791,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 19,
"fps": 51,
"mbps": 8.56,
"content_p50": 16.4,
"content_p95": 29.1,
"bitrate": 10220855,
"tier": 0,
"w": 1920,
"net": null
},
{
"t": 20,
"fps": 40,
"mbps": 7.13,
"content_p50": 18.3,
"content_p95": 23.9,
"bitrate": 12025604,
"tier": 0,
"w": 1920,
"net": null
}
],
"adapt": [],
"content_p50": 19.9,
"content_p95": 37.2,
"input_p50": null,
"input_p95": null,
"grades": {
"content_p50": "local",
"content_p95": "local",
"fps": "acceptable",
"late_pct": "bad",
"stall_max": "local"
},
"source_fps": 58.7,
"cpu": {
"mac_agent_pct": 6.6,
"frame_viewer_pct": 72.4,
"frame_tailscaled_pct": 8.1,
"frame_sshd_pct": 0.9,
"frame_gamescope_pct": 10.0,
"frame_total_pct": 24.2
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 141 Hz",
"show_s": 6.43,
"panel": "valve.steam.desktopgame.2001414593",
"src": "separate:9384"
},
{
"scenario": "type",
"frames_sent": 25,
"frames_drawn": 25,
"frames_shown": 25,
"duration_s": 14.7,
"stages_ms": {
"capture": {
"p50": -0.3,
"p95": 0.9,
"p99": 1.1,
"n": 25
},
"queue": {
"p50": 0.0,
"p95": 0.0,
"p99": 0.0,
"n": 25
},
"encode": {
"p50": 5.0,
"p95": 33.4,
"p99": 33.6,
"n": 25
},
"socket": {
"p50": 0.1,
"p95": 0.1,
"p99": 0.2,
"n": 25
},
"network": {
"p50": 6.1,
"p95": 14.9,
"p99": 24.4,
"n": 25
},
"decode": {
"p50": 3.0,
"p95": 10.5,
"p99": 11.4,
"n": 25
},
"draw": {
"p50": 0.6,
"p95": 1.0,
"p99": 1.1,
"n": 25
},
"present": {
"p50": 0.8,
"p95": 1.4,
"p99": 1.7,
"n": 25
},
"content": {
"p50": 12.6,
"p95": 41.0,
"p99": 46.3,
"n": 25
},
"content_shown": {
"p50": 13.9,
"p95": 41.8,
"p99": 46.6,
"n": 25
}
},
"fps": 1.7,
"fps_shown": 1.7,
"late_pct": 100.0,
"stall_max": 3099.2,
"stalls_over_100ms": 21,
"mbps": 0.04,
"keyframes": 4,
"size": "960x646",
"captured": 79,
"viewer_never_drawn": 0,
"input_ms": {
"p50": 41.3,
"p95": 60.3,
"p99": 68.6,
"n": 18
},
"input_shown_ms": {
"p50": 43.0,
"p95": 61.1,
"p99": 69.6,
"n": 18
},
"input_parts_ms": {
"uplink": {
"p50": 5.3,
"p95": 11.6,
"p99": 14.2,
"n": 18
},
"mac": {
"p50": 23.1,
"p95": 35.0,
"p99": 42.6,
"n": 18
},
"back": {
"p50": 11.8,
"p95": 19.2,
"p99": 22.7,
"n": 18
}
},
"inputs": {
"sent": 66,
"seen": 18
},
"timeline": [
{
"t": 3,
"fps": 1,
"mbps": 0.01,
"content_p50": 11.8,
"content_p95": 11.8,
"bitrate": 1224075,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 4,
"fps": 1,
"mbps": 0.02,
"content_p50": 14.7,
"content_p95": 14.7,
"bitrate": 2091896,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 5,
"fps": 4,
"mbps": 0.1,
"content_p50": 18.6,
"content_p95": 37.4,
"bitrate": 3071304,
"tier": 3,
"w": 1440,
"net": null
},
{
"t": 6,
"fps": 5,
"mbps": 0.13,
"content_p50": 4.2,
"content_p95": 19.2,
"bitrate": 4031277,
"tier": 3,
"w": 1440,
"net": null
},
{
"t": 7,
"fps": 6,
"mbps": 0.04,
"content_p50": 12.0,
"content_p95": 46.3,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 10,
"fps": 1,
"mbps": 0.01,
"content_p50": 12.2,
"content_p95": 12.2,
"bitrate": 300000,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 12,
"fps": 1,
"mbps": 0.02,
"content_p50": -1.2,
"content_p95": -1.2,
"bitrate": 842857,
"tier": 4,
"w": 960,
"net": null
},
{
"t": 14,
"fps": 1,
"mbps": 0.02,
"content_p50": 14.4,
"content_p95": 14.4,
"bitrate": 2091896,
"tier": 4,
"w": 960,
"net": null
}
],
"adapt": [],
"content_p50": 12.6,
"content_p95": 41.0,
"input_p50": 41.3,
"input_p95": 60.3,
"grades": {
"content_p50": "local",
"content_p95": "acceptable",
"input_p50": "local",
"input_p95": "local"
},
"source_fps": 5.4,
"cpu": {
"mac_agent_pct": 1.2,
"frame_viewer_pct": 5.0,
"frame_tailscaled_pct": 1.5,
"frame_sshd_pct": 0.2,
"frame_gamescope_pct": 4.7,
"frame_total_pct": 8.5
},
"viewer": "h264 software; ANGLE (Mesa, zink Vulkan 1.4(Turnip Adreno (TM) 750 (MESA_TURNIP)), OpenGL 4.6); raf 140 Hz",
"show_s": 5.12,
"panel": "valve.steam.desktopgame.2001910179",
"src": "separate:9510"
}
]
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+26 -3
View File
@@ -1,9 +1,32 @@
# compat-db: Frame Control's compatibility database
A private [Lakebed](https://docs.lakebed.dev/) capsule holding compatibility
reports for Android apps on the Steam Frame. For now only the maintainer's
copy of Frame Control has the key to read or write it. Everyone else's reports
stay on their own Mac (see `shared()` in `ui/frame_compat_db.py`).
reports for Android apps on the Steam Frame. Only the maintainer's copy of
Frame Control has the key to read or write it (see `shared()` in
`ui/frame_compat_db.py`). Everyone else's reports stay on their computer
unless they turn on **Share compatibility results**. Then the reports also go
to PostHog as `compat_report` events, and the maintainer syncs them in (below).
## Community reports
```sh
python3 ui/frame_compat_db.py sync --dry-run # what would be added
python3 ui/frame_compat_db.py sync # add them
```
`sync` reads `compat_report` events through PostHog's query API and adds
them with `via` set to `community`, `community-probe` or `community-install`.
It skips invalid reports and anything over 30 per reporter per day. Each run
re-reads the last 30 days, because an offline copy sends its reports late,
with the time they were made. `posthog-sync.json`, next to the outbox,
remembers which reports it has handled and each reporter's daily count, so
nothing is added twice and the cap holds across runs.
It needs:
- the PostHog project id: `"project"` in `ui/telemetry.json`
- a personal API key with `query:read`: `POSTHOG_PERSONAL_API_KEY`, or in the
Keychain (service `frame-control-posthog`, account `personal-api-key`)
- Live: `https://frame-compat.lakebed.app` (deploy `dep_dDmcsosVSiFirpW6`,
claimed, so it doesn't expire). The browser page only says it's private.
+222
View File
@@ -0,0 +1,222 @@
# Frame Control for AI agents
**Documented interface:** Frame Control's own stdlib Python MCP adapter wraps
its loopback HTTP API. No API key, hosted service, model SDK or third-party
helper app is needed. The assistant is our HTML/Python implementation hosted
in the platform Chromium browser. Its optional LLM endpoint is user configuration.
Installing other apps is an optional management action, never a prerequisite.
## Connect an MCP client
The default MCP command starts a private HTTP backend on a free loopback port,
with a fresh local access key. It stops that backend when the MCP client closes
stdin or sends SIGTERM. It uses its own SSH control socket, so closing it does
not close the desktop app's connection. No manually started server is needed.
Add this stdio server to your MCP client (use absolute paths):
```json
{
"mcpServers": {
"frame-control": {
"command": "python3",
"args": ["/absolute/path/frame-control/ui/frame_mcp.py"]
}
}
}
```
For Codex, the equivalent registration is:
```sh
codex mcp add frame-control -- python3 /absolute/path/frame-control/ui/frame_mcp.py
```
New agent sessions load the entry. An already running session may need its MCP
connections reloaded; registration does not retroactively add tools to its
initial tool inventory. Keep the checkout at that path while it is registered.
Use `codex mcp remove frame-control` to remove only this registration.
To reuse a running server instead, pass `--url http://127.0.0.1:47810`.
The desktop app uses a random port; use that port with `--url`, or run the
checkout server above. If the HTTP server uses `FRAME_UI_KEY`, pass the same
value in the MCP process environment. This is local access control, not an LLM
API key. The adapter only accepts loopback HTTP servers, refuses redirects and
ignores environment proxies. Stdout contains newline-delimited JSON-RPC only.
It supports MCP initialization, ping, tool listing and tool calls; no sampling,
resources, prompts or streaming transport.
| Tool | Arguments | Effect |
|---|---|---|
| `computer_state` | none | Read-only gamescope window IDs/focus and bounded AT-SPI tree; reports incomplete observations |
| `status` | none | Battery, services, installed games and Flatpaks |
| `screenshot` | `view`: `headset` (default) or `desktop` | Returns PNG image content to the MCP client |
| `job` | `id` | Background install status; poll until `done`, inspect `error` |
| `launch` | `appid` | Launch an installed Steam app |
| `install` / `uninstall` | `id` | Install from Flathub / remove a user Flatpak |
| `send_text` | `text` | Frame desktop clipboard; desktop must be open |
| `send_file` | `path` | File on the HTTP server computer, up to 16 MiB, copied to Frame `~/Downloads` |
| `panel` | `id` | Launch an installed Flatpak as a panel using the existing launcher |
| `power` | `action`: `suspend`, `reboot`, `poweroff` | Open a terminal for the user to enter the sudo password |
| `keep_awake` | `action`: `on`, `off`, `status` | Optional keep-awake script interface |
Only install free software with its developer's consent. There is no purchase,
entitlement bypass or arbitrary shell tool. `install` returns a background job
ID; it does not claim the installation has finished. APK and sideloaded title
installs remain in the main UI for now.
### Approval is a separate human action
Every mutation first returns an `approvalUrl`, exact action and `confirmation`
token. Ask the user to open that URL and choose **Approve this action** or
**Reject**. Then repeat the same tool and arguments with the token in
`confirmation`. The server refuses execution before approval, changed arguments,
expired tokens and reuse. A file approval binds the content hash as well as the
path. Approvals last five minutes and disappear when the HTTP server restarts.
A failed execution also consumes the approval; review a fresh request to retry.
The panel does not execute an action merely because it was approved.
MCP has no approval tool. This is protection against accidental model tool
calls, not a sandbox against a client with independent shell/HTTP access to your
computer. Grant the MCP client only the access you intend. Status, captures and computer-state observations
are returned directly to that client, which may forward them to its configured
model. The assistant's separate opt-in does not govern an external MCP client.
Power still requires the existing password prompt in a local terminal. MCP
never receives passwords. Power via `FRAME_LOCAL=1` is unsupported: use the main
UI. The panel launcher and keep-awake adapter require zsh on the computer.
[PR #16](https://github.com/saphid/frame-control/pull/16) owns
`scripts/keep-awake.sh on|off|status`. This branch does not copy or change it.
Until that script is present, the tool reports it unavailable. Keep-awake is
never automatic: `on` changes the shared idle timers; explicitly approve `off`
to restore them after work. It is not a per-agent lease; coordinate with other
users. No changes are made to the analytics/update interfaces in
[PR #17](https://github.com/saphid/frame-control/pull/17). Prompts, keys, model
replies, screenshots and approval payloads are not sent to analytics.
## Assistant panel
Open **Tools → Open assistant**, or `http://127.0.0.1:47810/assistant`.
To put the same page in the headset, with the HTTP server still running:
```sh
python3 scripts/assistant-on-frame.py --port 47810
```
This starts an SSH reverse forward bound to Frame loopback (port 47812 by
default), then a dedicated Chromium profile tagged as a SteamVR panel. Keep the
command running. Ctrl-C closes this browser profile and the tunnel; it leaves
other Chromium windows and the existing HTTP server alone. A failed cleanup
prints the temporary profile path so it can be removed when the Frame returns.
Use `--frame-port` if the default is busy. Chromium must already be available as
`org.chromium.Chromium`; the launcher never installs anything automatically.
Place the panel with SteamVR's normal docking controls.
Enter your full **chat-completions endpoint**, model name and optional key.
An OpenAI-compatible local server works without a key; no OpenAI account is
required. HTTP is allowed only on loopback; other endpoints require HTTPS.
Loopback refers to the computer running the HTTP server, even in the headset.
Endpoints with embedded credentials, query strings or redirects are refused.
Check the message consent box and press **Send message**. Screenshot context is
a separate unchecked box and sends one fresh capture with that request. Both
boxes reset after sending, and changing endpoint/model revokes consent. Nothing
is sent when opening the page or entering configuration. There is no model
list fetch, saved history, automatic screenshot capture or assistant telemetry.
Each send is independent: previous messages and replies are not included.
Configuration, credentials and chat remain in page memory; close/reload the page
or choose **Clear everything** to clear them. A request already sent cannot be
recalled. Only the chosen endpoint gets the request; proxy environment variables
and redirects are disabled. Its privacy and retention policy still applies.
Replies are plain text and cannot call tools or operate the Frame. A model must
support image inputs to accept screenshot context.
## Evidence and limits
**Verified 2026-09-28, SteamOS 0.4.1, BUILD_ID 20260925.6191901:** loopback HTTP
status through an SSH reverse tunnel; platform Chromium created a separate
SteamVR panel (confirmed in `GAMESCOPE_FOCUSABLE_APPS`); headset capture returned
a PNG. These checks preceded the UI implementation. No power or global settings
were changed.
**Inferred:** visual comfort and controller keyboard usability while wearing
the headset; panel creation in gamescope alone does not establish these.
Windows/Linux launcher support, live third-party model endpoints, installs,
uninstalls, power and keep-awake changes are not covered by that feasibility
check. See the PR for the final unit and end-to-end results.
**Verified end to end on the same Frame/build (2026-09-28):** a stdio MCP client
initialized, read status, retrieved a headset PNG, and transferred a test file
only after approval through the Chromium page. Remote file bytes matched;
reusing the confirmation was rejected. The actual headset Chromium page sent
text and then separately opted-in image context to a local test endpoint and
displayed its replies. Without consent there were zero endpoint requests.
The test endpoint returned canned replies: model inference and a live external
provider remain **unverified**. The launcher’s Ctrl-C cleanup was checked;
profiles, SSH tunnels and the test file were removed. No installs, removals,
launches of user games, power operations or keep-awake changes were performed.
**Verified locally:** unit coverage includes the stdio subprocess, approval
binding/expiry/replay/concurrency, file-change rejection, and a real local HTTP
endpoint for opt-in, text/image payloads and redirect refusal. Fake-Frame
regressions are in `tests/e2e/test_agents.py`; local Docker execution was blocked
because the Docker daemon was unavailable. The ARM64 fake-Frame CI job passed
on this branch (run 36421345682).
**Verified on the same Frame/build:** both Ctrl-C and SIGTERM close the dedicated
browser profile and SSH tunnel and remove the profile and panel log.
**Verified end to end on the same Frame/build (2026-09-29), with mutations:**
a stdio MCP client started `ui/frame_mcp.py` in its default mode (private
backend, no API key, no prestarted server) and a human approved or rejected
each change in the approval page in a real Chrome window:
| Tool | Result on the Frame |
|---|---|
| `send_file` | Approved; the file arrived in `~/Downloads` with identical contents |
| `install` | Approved; `io.github.fizzyizzy05.binary` job finished in about 35 s |
| `panel` | Approved; gamescope listed a new panel window, and `computer_state` reported the same window ID and PID. The app rendered in that window (below) |
| `launch` | Approved; Keep Talking and Nobody Explodes (341800) started under Proton and `computer_state` reported it as the focused app |
| `uninstall` | Approved; app and locale removed |
| `power` | Rejected in the page. Unapproved retries, the same token used for `uninstall`, and a retry after rejection were all refused. Nothing was powered off |
| `send_text` | Approved, then refused because the Plasma desktop was not open (documented requirement) |
| `keep_awake` | `status` reports the script unavailable until PR #16 lands |
A separate Claude Code CLI session, with only this server configured, read
status, `computer_state` and a headset capture, and requested an install. It
received an approval URL and did not execute anything.
The assistant opened as a Frame panel through `scripts/assistant-on-frame.py`.
Against a loopback stub model, a send without consent made zero requests. With
consent it made exactly one, carrying the text and a fresh Frame screenshot.
Consent unticked itself after sending. SIGTERM removed the panel, profile, log
and tunnel.
**Not verified while unworn:** every headset capture was a uniform dark frame,
so SteamVR's rendered view of panels and the game could not be checked; window
captures (`xwd`) were used instead. MCP can launch a game or panel but has no
tool to stop one: the tester stopped them over SSH. Removing an app leaves any
runtime it pulled in; Flatpak may also remove related extensions when that
runtime is removed by hand.
![Approval page showing the exact install action](img/mcp-approval-install.png)
![The installed Flatpak rendering in its own gamescope panel window](img/mcp-panel-binary.png)
![Assistant in Frame Chromium, after an opted-in request to the local test endpoint](img/assistant-panel.png)
## Computer-use coverage
MCP is the tool transport, not a limit on what an agent can do. A screenshot,
accessibility snapshot, click or keystroke can all be MCP tools when we have a
reliable underlying implementation. See [the investigation](computer-use.md)
for the verified boundaries. `computer_state` adds observation, not an input
channel: it cannot click an approval button or send keyboard/mouse events.
**Verified 2026-09-29, SteamOS 0.4.1, BUILD_ID 20260925.6191901:** the command saved
by `codex mcp add` launched without a prestarted server, negotiated MCP, listed
12 tools, read live Frame status and returned X11 window state plus AT-SPI
observations. It exited 0 at EOF. Steam's accessibility tree had inaccessible
children, reported as `incomplete: true`; this is not a complete actionable UI.
+144
View File
@@ -0,0 +1,144 @@
# Announcing changes
How we tell people about Frame Control features and fixes as they merge. The
same few sentences feed the X post, the release notes and the website, so they
are written once, in the pull request, while the change is fresh.
## What gets announced
| Kind | Announce? | Example |
|---|---|---|
| **New** — something you can now do | Yes, its own post | Stream Mac windows into the Frame as panels |
| **Better** — something existing got noticeably easier, faster or wider | Yes, its own post or a roundup | APKs install without the Android SDK |
| **Fixed** — something broken that users hit | Yes if people reported it or it blocked a flow; otherwise the next roundup | Mac mirror showed a zoomed-in corner |
| **Release** — a tagged build | Always, one post linking the release | Frame Control 0.3.1 |
| Tests, refactors, CI, docs-only, website polish | No | Fake Frame tests, screenshot crop |
If a change isn't worth a sentence to someone who owns a Frame, it isn't
announced.
## The voice
Write it the way the README and release notes already read.
- **Lead with what the person can now do**, in their words: "Install older
versions of an app when the newest won't run on the Frame", not "Add APK
version fallback resolver".
- **Plain and specific.** Name the thing, give the number: "about 30 fps",
"4,500 apps", "up to 8 older versions". No "blazing", "game-changing",
"excited to announce", "huge", or exclamation marks.
- **Say where it works.** Platforms and what it was tested on, briefly:
"Tested on a real Frame from macOS 27." Don't claim what wasn't tested.
- **Say the catch.** If it needs a setup step, an unsigned build, or only works
on one OS, say so in the same post.
- **Sentence case**, full sentences, British spelling to match the docs.
Contractions are fine.
- **No emoji in the text.** One image, GIF or short clip carries the tone
instead. The only symbol is the kind label below.
- **Unofficial, always.** Never imply Valve made or endorses it. Say "Steam
Frame" for the headset and "Frame Control" for the app.
- **Credit people.** If a user reported the bug or suggested the feature and is
happy to be named, thank them by handle.
## The formats
Every announceable PR ends with an `## Announcement` section holding these.
The reviewer checks it like code.
### 1. The post (X, and any other social account)
```
<Kind>: <what you can do now, one sentence>
<one or two sentences: how it works, the catch, or what it was tested on>
<link>
```
- `<Kind>` is `New`, `Better` or `Fixed`.
- 280 characters maximum including the link (X counts any link as 23).
- One link: the release if it has shipped, otherwise the PR.
- One visual when the change is visible: a screenshot from the app, a GIF, or a
short clip from the headset. Alt text describes what it shows.
- No hashtags, except `#SteamFrame` on releases and on posts about something
new, because people search for it.
### 2. The release-note line
One bullet under **New in x.y.z**, same as the current release notes: the
first half of the post's first sentence, no kind label, no link.
### 3. Release post
```
Frame Control <version>: <the headline change>
<one sentence on the headline change>. Also: <two or three short items>.
Windows, macOS and Linux: <release link>
#SteamFrame
```
The release title on GitHub uses the same `Frame Control <version>: <headline>`
line, as 0.3.0 and 0.3.1 already do.
### Roundups
Small fixes that don't earn their own post wait for a roundup, posted with the
next release or when three or more have piled up:
```
Fixed in Frame Control this week:
- <fix>
- <fix>
- <fix>
<link>
```
## Examples from what has already merged
**#11, older APK versions**
```
New: when an Android app is too new for the Frame, Frame Control now offers
older versions that will install.
It checks F-Droid, its archive and IzzyOnDroid, and verifies each download
before it goes on the headset.
https://github.com/saphid/steam-frame/pull/11
```
**#8, Mac mirror fixes**
```
Fixed: mirroring your Mac into the Steam Frame now fits the whole desktop in
the panel, asks for the right password, and shows the cursor.
Tested end to end on a real Frame from macOS 27.
https://github.com/saphid/steam-frame/pull/8
```
**v0.3.1**
```
Frame Control 0.3.1: install APKs without the Android SDK
Frame Control now reads APK files itself, so there's nothing extra to install.
Also: Linux and Windows game sideloading, and one-click install links.
Windows, macOS and Linux: https://github.com/saphid/steam-frame/releases/tag/v0.3.1
#SteamFrame
```
## Posting
Nothing is posted without a person approving it. The flow is:
1. The PR carries its `## Announcement` section.
2. On merge, the post is drafted from that section (manually for now).
3. Alex approves or edits it, then it's posted from the project account.
4. Replies and questions that turn out to be bugs become GitHub issues labelled
`feedback`, same as the website form.
+144
View File
@@ -0,0 +1,144 @@
# APK repositories
Frame Control supports **F-Droid-format repositories**, including F-Droid,
F-Droid archive, IzzyOnDroid and user-provided HTTPS repositories. Repository
indexes are authenticated before their apps appear. Search lists builds with
Android API ≤30 and arm64-v8a or no native libraries, using the same streaming
reducer as the existing catalogue. This does not guarantee an app works in Lepton.
## Formats considered
| Format | Users and purpose | Support in this source |
|---|---|---|
| F-Droid v2 | F-Droid, IzzyOnDroid, self-hosted fdroidserver repositories; consumed by F-Droid clients including Droid-ify and Neo Store | Preferred: signed `entry.jar` authenticates `entry.json`; its SHA-256 authenticates `index-v2.json`, which supplies APK SHA-256 hashes |
| F-Droid v1 | Older F-Droid servers and clients | Fallback: verify `index-v1.jar`, then read its signed `index-v1.json` |
| Obtainium configurations / exports | Obtainium users share app URLs plus source-specific filters and update settings; exports can contain a list of app configuration objects | Not imported here: configurations describe how to find releases, not one signed repository index |
| SideQuest listings / custom feeds | SideQuest's own app discovery and installation service | No interoperable signed custom-repository specification was established from the public project documentation examined; SideQuest needs its own adapter |
| GitHub release lists | Developers publish APK assets on release pages; community lists link to projects | Not a repository standard: asset naming, build selection and publisher verification vary; handled separately from this F-Droid source |
| Minimal JSON list | A private list could contain package, title, APK URL and SHA-256 | Deliberately not introduced: unsigned hashes downloaded alongside files do not authenticate their publisher; another bespoke signing/update protocol would duplicate F-Droid |
Research references (checked 2026-09-28):
- [F-Droid APIs](https://f-droid.org/docs/All_our_APIs/) and
[repository setup](https://f-droid.org/docs/Setup_an_F-Droid_App_Repo/).
- [F-Droid signing keys](https://f-droid.org/docs/Release_Channels_and_Signing_Keys/)
and [IzzyOnDroid's repository page and fingerprint](https://apt.izzysoft.de/fdroid/).
- [Droid-ify](https://github.com/Droid-ify/client) and
[Neo Store](https://github.com/NeoApplications/Neo-Store).
- [Obtainium](https://github.com/ImranR98/Obtainium), its
[configuration/deep-link format](https://wiki.obtainium.imranr.dev/deep_links/),
and [community app configurations](https://apps.obtainium.imranr.dev/).
- [SideQuest's public client](https://github.com/SideQuestVR/SideQuest).
The absence of a specification in these materials is not proof that no
historical or private custom-feed format exists.
## Add a repository in Frame Control
From the Frame Control checkout, use its source-management CLI:
```sh
python3 ui/apk_sources/fdroid.py add 'https://example.org/fdroid/repo?fingerprint=YOUR_64_HEX_CERTIFICATE_FINGERPRINT' --name 'My apps'
python3 ui/apk_sources/fdroid.py list
python3 ui/apk_sources/fdroid.py search SOURCE_ID 'music'
python3 ui/apk_sources/fdroid.py download SOURCE_ID org.example.app
python3 ui/apk_sources/fdroid.py remove SOURCE_ID
```
Replace `SOURCE_ID` with the `id` printed by `add` or `list`. `--fingerprint`
can also supply the pin. `fdroidrepos://example.org/fdroid/repo?fingerprint=…`
links are accepted and converted to HTTPS. Conflicting fingerprints are refused.
A URL must identify the repository directory, not its website or an index file.
Adding fetches and validates the complete index **before saving** the source.
Without a fingerprint, Frame Control verifies the JAR signature and remembers
its signer: trust on first use (TOFU). This establishes continuity with the
first server response, not independent publisher identity. Obtain the published
fingerprint through a trusted channel when possible; the store's Add a source
form shows the pinned one ("Trusted on first use: …") so you can compare it.
Re-adding an existing URL preserves its pin; changing it requires deliberately
removing and re-adding it.
The API for the search/server integration is in `ui/apk_sources/fdroid.py`:
`add_repo(url, fingerprint=None, name=None)`, `remove_repo(source_id)`,
`set_enabled(source_id, enabled)`, and `user_repos()`. The module also exposes
`sources`, `search`, `details`, and `download` from the shared source contract.
This change supplies the CLI and API; the integrated source-management UI is
separate work. Built-in sources can be disabled but cannot be removed.
Settings and pins live in `frame_host.data_dir('apk-repos.json')`
(`~/Library/Application Support/Frame Control/apk-repos.json` on macOS).
Authenticated reduced indexes and APKs live under
`frame_host.cache_dir('apk-sources')`; indexes refresh after 24 hours. An
expired index is still served (marked stale in the store) while it refreshes in
the background; a failed refresh is retried after 10 minutes.
Only the running Frame Control app prunes cached APKs (at start and after store
downloads); the command-line tools never do.
The existing catalogue's unverified index cache is never treated as authenticated.
Rollback protection: each repository's newest accepted signed index timestamp
is kept in `apk-repo-state.json` next to the settings, and an older index is
refused. Once a repository has served a v2 `entry.jar`, a missing `entry.jar`
is an error rather than a reason to fall back to `index-v1.jar`. `entry.jar`
must be signed with SHA-2 (SHA-1 is still accepted for legacy `index-v1.jar`).
Removing a repository clears its state.
## Publish your own repository
Only publish free APKs you own or have the developer's permission to distribute.
Do not publish paid app mirrors or bypass store licences. Check distribution
terms before adding someone else's repository; this module does not infer legal
permission from a signature or automatically audit a repository's terms.
Install a current [fdroidserver](https://f-droid.org/docs/Installing_the_Server_and_Repo_Tools/)
and its documented Android/Java dependencies on the publishing machine, then:
```sh
mkdir my-fdroid
cd my-fdroid
fdroid init
# Set repo_url in config.yml to https://example.org/fdroid/repo
# Also set repo_name and repo_description; keep the generated signing key safe.
cp /path/to/your-free-app.apk repo/
fdroid update --create-metadata
# Review the generated metadata (name, summary, licence, source and website).
fdroid update
```
Serve the generated **repo directory** at that HTTPS URL, including APKs,
icons, `entry.jar`, `index-v2.json` and `index-v1.jar`. Do not publish the
private signing keystore or configuration passwords. Configure fdroidserver's
`serverwebroot` and run `fdroid deploy` for managed publication, or copy the
public directory with your existing deployment tool. Publish the SHA-256
repository certificate fingerprint displayed by fdroidserver in a link such as
`https://example.org/fdroid/repo?fingerprint=…`.
Keep the repository signing key backed up: changing it breaks existing pins.
For updates, add the new APK, edit metadata as needed, run `fdroid update` and
publish again. Test the published URL with Frame Control's `add`, `search` and
`download` commands. The above publisher setup is documented from fdroidserver;
it was not executed as part of this implementation.
## Verification and limits
The stdlib verifier supports one RSA PKCS#1 v1.5 JAR/CMS signer with a key of
2048–8192 bits; SHA-256/384/512 and legacy SHA-1 digest encodings are
recognized. It checks the signer certificate pin, the signature over `.SF`,
the whole-manifest digest, and the manifest's digest of the JSON member.
ECDSA, DSA, RSA-PSS, multiple signers and section-only `.SF` manifests are
rejected. Certificates are pinned identities, not validated as Web PKI chains.
HTTPS certificates are separately checked by Python's normal TLS validation.
v1 fallback occurs only when `entry.jar` returns HTTP 404 or 410. Signature,
fingerprint, index hash, TLS and server errors never trigger an unsigned
fallback. APKs are cached by SHA-256 and checked again before reuse. Here,
`verified: true` means the bytes match the signed repository's APK hash; it
is not an independent APK publisher-signature or runtime compatibility verdict.
There is no repository timestamp rollback/expiry policy or automated signing-key
rotation yet. An old correctly signed index can still validate.
Offline fixtures exercise v2, v1, TOFU, pin changes, disabled sources, cache
reuse, URL rejection and corruption of every signature/hash layer. On the Mac,
the real IzzyOnDroid repository was added with its published pin, searched for
Tiny Music Player, and its 16,520-byte APK downloaded with SHA-256
`d7bcb24d101b04beb3394b695b24be4e2c3d6ed702f1d0e06bc4dd707f64d86a`.
No headset connection or installation was performed.
+103
View File
@@ -0,0 +1,103 @@
# Developer-consented APK sources
Surveyed 2026-09-28. Free access is not proof of redistribution permission or
Frame compatibility. These adapters fetch only public publisher releases or
link to publisher pages. They do not acquire store entitlements, defeat access
checks, install anything, or rehost APKs. See [VR compatibility](vr-apks.md).
| Source | Developer consent and automated-access position | API/feed; VR coverage | Decision |
|---|---|---|---|
| [itch.io](https://itch.io/docs/legal/terms) | Publishers warrant distribution rights (§4). Users may access content through the service; this is not blanket scraping permission. Main robots excludes `/game/download/`; author subdomains exclude `/*/download/`. No challenge bypass. | Public free Android RSS for `openxr` and `oculus-quest`; substantial indie VR. Server API is mostly authenticated publisher/account functionality, not a general anonymous store-download API. | Implement RSS search, artwork and page links; `downloadable: False`. The supplied free-download script follows keyed download pages excluded by robots, so it is not shipped. |
| [GitHub releases](https://docs.github.com/en/rest/releases/releases) | Maintainers publish assets; curated repositories below establish provenance. Public hosting or an open-source topic alone does not establish rights to every uploaded binary. Use supported REST API under [API terms](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service#h-api-terms), not HTML crawling. | Releases API includes APK assets and sometimes SHA-256. Topic search finds OpenXR/Quest projects. 60 unauthenticated requests/hour; authenticated user limits are generally 5,000/hour, with separate search/secondary limits. | Implement curated downloads and explicit topic discovery. Unreviewed topic results are page-only. |
| [Uptodown](https://www.uptodown.com/aboutus) | Developer distribution program exists, but that does not prove publisher authorization for every catalog item. [Privacy policy](https://www.uptodown.com/aboutus/privacy) explicitly describes protection against automated access. General automation permission was not established. | Broad Android catalog, limited VR focus; no supported public consumer-download API established in this survey. | Page links only; no downloader. Do not infer consent from an unchanged APK signature. |
| [APKPure](https://apkpure.com/terms) | Third-party APK catalog; individual publisher consent and automation rights were not established. Terms request returned HTTP 403; no bypass attempted. | Broad Android coverage, incidental VR; internal endpoints are not permission to automate. | Exclude automatic indexing/downloading; user may open site. |
| [APKMirror](https://www.apkmirror.com/faq/) | Publisher-signed files and a free-app policy are not a blanket developer-consent or automation grant. FAQ request returned HTTP 403, so current terms could not be confirmed. | General Android/version archive; APK bundles often need another installer; little VR focus. No supported consumer-download API established. | Page links only, no scraping or bundle conversion. |
| [Aptoide](https://en.aptoide.com/company/legal) | Terms define an app supplier as developer, owner or authorized distributor; user stores still require per-item provenance. API availability alone does not settle third-party access rights. | API ecosystem and general Android catalog; weak VR focus. | Defer until a publisher-owned store and its API terms can be approved. No blanket community-store downloader. |
| [Amazon Appstore](https://developer.amazon.com/docs/app-submission/understanding-submission.html) | Official developer submissions; store account, device and license rules apply. Publisher submission APIs do not authorize public binary extraction. | Fire-device distribution; Android-device Appstore support ended in 2025; little Quest relevance. | Official product links only; no account or entitlement extraction. |
| [PICO / ByteDance store](https://developer.picoxr.com/document/distribute) | Official publisher channel with store/device entitlements. No public unauthenticated binary-download grant established; documentation request encountered a redirect error. | Strong standalone VR; PICO builds may depend on PICO services/extensions. | Store links only. A developer's independently published GitHub/itch build can qualify separately. |
| [Meta Horizon Store / former App Lab](https://www.meta.com/experiences/) | Official developer submissions. A free store entitlement is still an entitlement; no license bypass or authenticated store extraction. App Lab was folded into the main store in 2024. | Strongest Quest coverage; no supported anonymous APK-download API established. | Store links only; independently distributed free builds use their publisher source. |
| [Khronos samples](https://github.com/KhronosGroup/OpenXR-SDK-Source) | Official upstream, Apache-2.0 sample; developer-published release APKs. GitHub API terms apply. | `hello_xr` Vulkan/OpenGL ES APKs; excellent OpenXR diagnostics. | Included in GitHub curated list, Vulkan variant selected. |
| [Meta OpenXR samples](https://github.com/meta-quest/Meta-OpenXR-SDK) | Official upstream; check each sample's license. Source availability does not imply a published APK, and some samples require Meta extensions/services. | Source/build examples, inconsistent ready-made APK releases. | Link to upstream; add specific free APKs only after release/provenance review. |
| [Godot XR demos](https://github.com/GodotVR/godot-xr-tools) | Official project source and publisher demo pages; licenses and dependencies vary by demo. | OpenXR examples on GitHub/itch. Older Godot builds can fail on Lepton's missing clipboard service. | Covered by source discovery; no compatibility promise from an OpenXR tag. |
The table distinguishes observed restrictions from unknown permission. An
unverified policy is a reason to defer automation, not a claim that a site is
unlawful. Only the two implemented source kinds are registered by their own
`sources()` functions; the other rows are recommendations, not new UI entries.
## Adapters
`ui/apk_sources/github.py` uses `github_curated.json`: Khronos `hello_xr`,
[Open Brush](https://github.com/icosa-foundation/open-brush), and
[SuperTux 3D](https://github.com/SgtBilko76/SuperTux-3D). These have official
OpenXR project/release evidence, not a blanket claim of headset compatibility.
Open Brush's compatibility evidence is recorded in [vr-apks.md](vr-apks.md).
Open Brush and SuperTux publish the selected builds as prereleases; curated
opt-ins preserve that label in version records. Exact APK filename patterns
avoid downloading desktop archives or alternate non-Quest builds.
[OpenSaberPlus](https://github.com/arpruss/OpenSaberPlus) was examined but not
curated: GitHub reports its license as `NOASSERTION`, and current OpenXR APK
provenance was not established in this pass.
Default GitHub search is offline against this small list. Queries
`topic:openxr`, `topic:oculus-quest`, and `topic:quest` explicitly call repository
search. Results outside the curated list stay page-only, even if a repository
claims an open-source license. This prevents an arbitrary tagged mirror from
becoming a trusted downloader. Extend the curated JSON after provenance review.
Set optional `FRAME_GITHUB_TOKEN` in the process environment for a higher API
quota. Tokens are sent only to `api.github.com`, never written to the cache,
never sent to asset hosts, and removed on redirects. The adapter does not
read `gh` credentials automatically. Metadata is cached for one hour under
`frame_host.cache_dir('apk-sources', 'publisher')`. A cold details request
fetches at most ten releases. Rate-limit errors are surfaced without retry
loops. Asset IDs and release tags are not Android version codes: metadata
leaves the latter unknown and rejects a requested `version_code` rather than
silently fetching a different build.
`itch.py` exposes separate OpenXR and Quest feed sources, so one feed's failure
does not suppress the other at the aggregator level. Queries filter the current
feed window locally: this is not an exhaustive historical itch search. Only
explicit zero-price Android entries are returned. Covers are exposed in
`images`; absent screenshots, APK version, ABI and minimum SDK stay unknown.
Curated GitHub entries include publisher artwork and plain-language summaries.
Repository image URLs are pinned to inspected commits. Open Brush screenshots
come from its README-linked Steam listing; SuperTux uses the upstream gameplay
preview embedded in the port's README (not a headset capture). The hello_xr
sample has a launcher icon and GitHub social banner; no published screenshot
was found in the inspected repository/README, so its screenshot list is empty.
Uncurated topic results use the owner's avatar and GitHub's repository social
preview. These are repository placeholders, not app screenshots. Itch's recorded
RSS includes only covers, so screenshot lists remain empty without page scraping. VR is
based on curated evidence or a VR-specific feed/topic, not a compatibility claim.
Downloads stream to unique temporary files, require an APK manifest entry,
restrict HTTPS origins and redirects, and enforce a 2 GiB ceiling. `verified`
means the downloaded SHA-256 matches GitHub's published digest. Without such a
digest, the computed SHA-256 is returned with `verified: False`; neither value
claims publisher-signature validation. Installation must inspect the APK as
usual. OBBs, split APKs, paid assets and external release-body download links
are unsupported.
## Evidence and limits
On this Mac, Python 3.9 downloaded the real Khronos Vulkan 1.1.63 APK through
the GitHub adapter, matched its published SHA-256
`f24bbe8ba6f6339fca658628868ba8189cbc33390d6ac508f69d76fb67b5fa34`, and
`python3 ui/frame_android.py info <apk>` exited 0: package
`org.khronos.openxr.hello_xr.vulkan`, version code 1063, minimum API 24,
arm64-v8a present, OpenXR detected. No Frame connection or installation occurred.
The itch OpenXR RSS was fetched successfully and recorded as a fixture.
Subsequent live adapter search encountered HTTP 429; it is not claimed as a
successful live end-to-end search. Fixture search finds Off Nominal and parses
nine Android entries from the ten-item feed (one has only an HTML platform).
A real itch download and APK inspection were deliberately not performed:
robots restrictions take precedence over that requested proof. No current
policy text is claimed verified where the table records failed access.
Tests use recorded, reduced API/RSS fixtures with network access blocked in
the new test class. They cover selection, prereleases, unknown topic results,
paid/non-Android exclusion, URL restrictions, redirect credential removal,
caching, rate limits, checksum mismatch, non-APK rejection and partial-file
cleanup. See `.claude/NOTES-more-sources.md` for commands and local evidence.
+14
View File
@@ -5,6 +5,12 @@ in **Lepton**, Valve's Waydroid-based container. Lepton is built for games,
not general Android use
([GamingOnLinux](https://www.gamingonlinux.com/2026/09/lepton-from-valve-to-run-android-games-on-linux-is-now-open-source/)).
**VR streaming clients:** WiVRn 26.9 and ALVR 20.14.1 install, but both fail
OpenXR instance creation on the checked Frame because its Android runtime lacks
`XR_KHR_convert_timespec_time` (**verified** 2026-09-28, SteamOS 0.4.1,
BUILD_ID 20260925.6191901). They are not Frame Control dependencies. See
[the feasibility results and options](linux-vr-streaming.md).
## Install from the Mac: one app, one Lepton instance (verified 2026-09-25)
Use Frame Control's **Android apps** section (search, Install, Test, Report), drop
@@ -325,3 +331,11 @@ because gamescope scales Lepton's surface to fit the same panel. Also unverified
whether the settings survive the app or its Lepton instance relaunching.
Lepton Development rebuilds its Android data on exit, so there they probably
don't.
## Expansion files and save backups
SideQuest-inspired CLI helpers install local OBB files into an already-running
app instance and back up/restore a stopped instance's private app data. See
[SideQuest features and limits](sidequest.md) for commands, archive scope and
verification status. These paths have offline coverage; real Frame storage and
permissions remain unverified. They do not change APK install or launch behavior.
+67
View File
@@ -0,0 +1,67 @@
# Computer use through Frame Control MCP
The MCP transport can carry semantic actions or visual computer-use actions.
The limits are the Frame's underlying interfaces, permissions and whether an
action can be targeted and verified. A stereoscopic headset screenshot alone
is not a reliable coordinate system for clicking a particular app window.
## What exists, and the right route
| Surface | Evidence and route | Remaining work or boundary |
|---|---|---|
| Frame management | **Verified:** existing SSH/HTTP operations for status, capture and file transfer work through MCP. Typed install/launch/power tools wrap the existing API. | Extend typed operations before adding generic mouse automation. Preserve explicit approval for consequential changes. |
| App/window observation | **Verified 2026-09-29:** `computer_state` reads gamescope X11 window/app/process triples, focused app and the installed AT-SPI library. | Bounded to 96 accessible nodes and six levels. Trees may be truncated, stale, hidden or incomplete. Snapshot paths and XIDs are observations, never durable action permissions. |
| Chromium page content | **Verified previously:** the assistant rendered and could be exercised through CDP in an isolated Frame Chromium profile. | A shipped click/type surface needs exact owned browser/target binding, fresh element references, lifecycle cleanup, consent and post-action readback. Do not expose unrestricted JavaScript or attach to arbitrary existing profiles automatically. |
| Steam UI | **Verified 2026-09-29:** the AT-SPI service listed the Steam client's Chromium process and frame nodes, but child traversal was incomplete. Existing `frame_steam.py` uses Steam's loopback CDP endpoint for specific operations. | Prefer those narrow Steam interfaces. Presence of AT-SPI does not prove controls are actionable, and generic pointer injection is not proved for VR menus. |
| Other Linux apps | **Verified 2026-09-29:** Frame ships libX11, libXtst and libatspi; `/dev/uinput` is writable by the current user. | Library presence and access permissions do not prove that a game accepts input. Global virtual input can affect whichever app has focus. Do not ship a blind keyboard/mouse tool on this evidence alone. |
| Panel focus and layouts | **Documented in [#41](https://github.com/saphid/frame-control/pull/41):** `POST /api/panels` accepts `list`, `focus` and `open`. Focus was verified there. | Reuse that owned interface after integration. Its tested gamescope-owned overlay transform setters return `PermissionDenied`; no reliable saved spatial-layout interface was established. Do not duplicate its implementation here. |
| Shared keyboard/trackpad | **Documented in [#19](https://github.com/saphid/frame-control/pull/19):** `/api/input` supplies state/start and event submission, implemented with a bundled KDE Connect daemon. | This branch does not import, launch or depend on that daemon. The user's own-implementation rule remains authoritative. A first-party input implementation or permitted bundled-library route needs its own delivery evidence before MCP integration. |
| Physical/device boundaries | **Documented:** an asleep Frame may be off the network; power authorization can require the user's password; physical pairing and headset fit/comfort require the user. | MCP cannot bypass offline hardware, consent, compositor permissions or physical verification. Keep explicit human handoffs. |
## Reusing the existing computer-use work
**Documented:** the installed `cua-driver` skill has the right control pattern:
observe an exact window, use a semantic target if available, fall back to pixels
from that same snapshot, then read back the result. Its browser route requires
an exact process/window/target binding and session-scoped element references.
Those are useful design rules for Frame tools.
**Verified locally 2026-09-29:** `cua-driver describe get_window_state` describes
host-local process/window IDs and macOS AX inspection. It does not establish an
SSH Frame target. The installed skill's advertised Linux companion file is
missing. A native ARM64 Frame backend, its dependencies and remote transport
have not been verified. We therefore do not claim that the existing Mac driver
can control the Frame by passing it a Frame PID or screenshot, and we do not
make the feature depend on installing that application.
Frame Control's `computer_state` is our own Python implementation over installed
platform libraries. It sends the probe over SSH stdin, writes no helper to disk,
and exits after one observation. Missing displays/libraries return explicit
errors; a 15-second process deadline prevents a stalled accessibility call from
leaving a probe behind. Window names and accessibility text are untrusted app
content, never instructions to an agent.
**Recommended next implementation:** an isolated Chromium session with typed
snapshot/click/type/scroll tools and exact fresh target binding, then individually
verified native app actions. Use the headset capture to judge appearance, not to
invent a screen-to-window coordinate transform. Direct tool calls must retain
approval rules; a generic computer-use tool must not become a route around the
MCP approval panel, install confirmation or power confirmation.
## Isolated browser input proof
**Verified 2026-09-29, SteamOS 0.4.1, BUILD_ID 20260925.6191901:** a temporary
Frame Chromium profile loaded a local test page through an SSH reverse tunnel.
CDP `Input.insertText` entered the test string in its own input. A CDP
`Input.dispatchMouseEvent` press/release on its own button copied that string
to the page's result; DOM readback matched exactly. The browser profile,
loopback forwards and panel log were removed afterward. No user app was typed
into, no global settings were changed and no third-party helper app was used.
AT-SPI did **not** expose the test page's controls in that same probe, even with
Chromium's renderer-accessibility flag. It returned the partial Steam-client
tree instead. The reason remains **unverified**; this is an evidence gap, not
proof that Frame accessibility cannot work. For a first implementation,
Chromium's proven page-specific CDP route is stronger than assuming complete
AT-SPI coverage. This proof does not ship unrestricted click/type tools or
establish input delivery to SteamVR's menus.
+180
View File
@@ -0,0 +1,180 @@
# Headsets, addresses and the connection
Frame Control can manage more than one Steam Frame, and each headset can be
reached at more than one address: a LAN IP at home, another at the office, its
mDNS name (`frame.local`), its Tailscale IP or MagicDNS name. The **Devices**
tab (key 5) lists them, and the connection pill in the header shows what the
app is doing to reach the one in use, step by step, as it happens.
The code is in three modules, all stdlib-only Python on your computer:
| Module | What it does |
|---|---|
| `ui/frame_devices.py` | The registry: headsets, their addresses, networks; importing and updating `~/.ssh/config`; pinned host keys |
| `ui/frame_network.py` | Which network this computer is on, and Tailscale's state |
| `ui/frame_link.py` | The connector: finds the headset, keeps the SSH connection, publishes each stage; the Devices API |
## Headsets
Each headset keeps its own SSH alias, as Set Up Connection has always written
it: the first is `frame`, the next `frame-2`, and so on. Terminal's
`ssh frame-2` and the helper scripts (`FRAME_ALIAS=frame-2 scripts/push.sh …`)
work for each one.
- **Nothing to migrate by hand.** On first start, the app imports every
`# >>> steam-frame (ALIAS) >>>` block in `~/.ssh/config` as a headset, with
the block's HostName as its first address. It also copies the host key your
`known_hosts` already trusts for that address into the headset's own
known_hosts file, `~/.ssh/frame-control-hosts/<id>`, so nobody is asked to trust it again.
- **Add a headset** runs Set Up Connection (`scripts/connect.sh` on macOS,
`ui/frame_connect.py --alias NAME` elsewhere) in a terminal with a new alias.
When it writes its block, the app picks the headset up by itself. If Set Up
Connection runs again and finds a headset somewhere new, that address is added
at the top of its list.
- **Use this headset** (or the switcher in the header, or the app's
**Frame → Headset** menu) moves the whole app to another headset; every panel
reloads from it. From that moment no command goes to the previous headset, even
if the new one never answers. It waits while an install is running, since an
install reads the SSH settings step by step.
- **Remove** forgets a headset. Its `~/.ssh/config` block stays unless you tick
the box; either way it isn't imported again unless Set Up Connection changes it.
- A plain `FRAME_ALIAS` that Set Up Connection never configured still works: the
app shows it as not set up and lets ssh's own config decide where it goes.
## Addresses
Each address has a kind (LAN, mDNS, Tailscale or Other, guessed from the address
and changeable), an optional label, the networks it has worked on, and when it
last worked with its round-trip time.
When connecting, the app **tries all addresses at once** (TCP to the SSH port)
and ranks them:
1. addresses that worked on the network this computer is on now;
2. mDNS names;
3. Tailscale addresses, if Tailscale is running here;
4. addresses not tried on this network yet;
5. addresses that only ever worked on other networks;
6. Tailscale addresses while Tailscale is off.
Your order on the Devices tab breaks ties. The best-ranked address that answers
wins; one that answers first waits up to 0.35 s for a better-ranked one that is
still trying. If SSH to the winner fails in a way another address could fix
(a different device answered there, or the link dropped), the next one that
answered is tried. Every success records the network on that address, so next
time on that network it's tried first.
**Test now** probes every address and tries SSH on each one that answers, without
disturbing the connection in use: "SSH works", "answered as a different
headset", "refused this computer's key", or why it didn't answer. **Find on
Tailscale** lists your tailnet's devices (likely headsets first, from `tailscale
status --json`, including the Mac app's own CLI) with buttons to add their
MagicDNS name or IP. **Find on this network** asks mDNS for SteamOS devkit
services and checks `ALIAS.local` and `frame.local`.
## Networks
A network is told apart by its default gateway: the router's IP address plus its
hardware (MAC) address, read with `route`/`arp` (macOS), `ip route`/`ip neigh`
(Linux) or `route print`/`arp -a` (Windows). That works on wired networks, and
on macOS 14 and later, which hides the Wi-Fi name from apps without Location
permission. Where the system does share the Wi-Fi name, it's shown, and you can
name any network yourself ("Home Wi-Fi") on the Devices tab.
The app rereads the gateway every 5 seconds and Tailscale's state every
30 seconds. Changing networks reconnects.
## The connection, stage by stage
The connector runs in the server (`frame_link.Link`) and moves through:
1. **Checking this computer's network**: gateway, Wi-Fi, this computer's IP, Tailscale.
2. **Finding the headset**: each address resolving, trying, answered in N ms,
no answer, refused, or can't be found.
3. **Opening SSH** to the address that answered.
4. **Checking the headset's identity**: the host key must match the one pinned
for this headset.
5. **Logging in** as the headset's user.
6. **Connected** via network N, address A, round trip T; or **failed** at a stage
with the reason in plain words and a countdown to the next try (5, 10, 20,
then every 30 seconds). Retry now skips the wait.
Stages 3 to 5 come from following `ssh -v` as it runs. On macOS and Linux the
connection is an SSH ControlMaster that every command shares; when it dies (the
headset slept or left the network) the connector notices and starts again. On
Windows, where OpenSSH can't share a connection, the same handshake runs once
and each command then connects on its own; a command that can't reach the
headset makes the connector start again.
Once connected, every `ssh`, `scp` and `rsync` the app runs gets
`-o HostName=<address> -o HostKeyAlias=frame-control-<id>
-o UserKnownHostsFile=~/.ssh/frame-control-hosts/<id> -o HashKnownHosts=no -o User=… -o Port=…`. The
alias's block in `~/.ssh/config` is also updated to the last address that
worked (and to the user and port you set), so Terminal's `ssh frame` and the
scripts follow. Edits to `~/.ssh/config` take a lock file
(`~/.ssh/config.frame-control.lock`) that Set Up Connection takes too, and never
write over a change someone else made since the app last read the file.
**Host keys are pinned per headset, not per address.** Your own `known_hosts`
is keyed by address, so a different device answering at a remembered IP (a DHCP
lease that moved) would look like a new host there. The app keeps one known_hosts
file per headset instead, so saving or forgetting one headset's key never touches
another's: a different device answering at one of its
addresses is refused, and the pill says so. A headset's first connection trusts
the key it shows, as Set Up Connection does. After reinstalling SteamOS the
headset has a new key; **Forget identity** on the Devices tab lets the next
connection save the new one.
## One server at a time
Only one Frame Control server runs per user (a lock file, `server.lock`, in the app's
data folder). Two would each connect, reconnect and edit the headsets on their own, and
one could move the other's install to a different headset. A second one, say
`scripts/frame-ui.sh` while the app is open, exits with "Frame Control is already
running". `FRAME_CONTROL_DATA_DIR` gives a separate one, with its own headsets.
## API
All under the usual `/api/` guards (loopback `Host`, `X-Frame-UI` header).
| Request | Returns |
|---|---|
| `GET /api/connection` | The connection state: `phase` (connecting, connected, failed), `device`, `network`, `stages`, `probes`, `via`, `error`, `retry_at`, `tests`, `version` |
| `GET /api/connection/events` | The same as server-sent events, one each time it changes (the page reads it with `fetch`, since `EventSource` can't send the header) |
| `GET /api/devices` | Headsets, the current network, known networks, the next free alias |
| `GET /api/devices/tailscale?id=` | Tailscale peers, likely headsets first |
| `GET /api/devices/mdns?id=` | Headsets found on this network |
| `POST /api/devices` | `{"action": ...}`: `use`, `update` (name, user, port), `remove`, `address-add`, `address-update`, `address-remove`, `address-move`, `test`, `forget-identity`, `name-network`, `setup` (alias, optional host), `retry` |
Every host, alias and user is checked against strict patterns before it's
stored, because they end up in ssh arguments and `~/.ssh/config`; nothing goes
through a shell.
## The registry file
`devices.json` in the app's data folder (`~/Library/Application Support/Frame
Control` on macOS, `%APPDATA%\Frame Control` on Windows,
`~/.local/share/frame-control` on Linux). It's plain JSON so the iPhone app can
share the format later (it still connects to one host; see
[iphone.md](iphone.md)):
```json
{"version": 1, "active": "f67f8b7e",
"devices": [{"id": "f67f8b7e", "name": "Steam Frame", "alias": "frame", "user": "steamos", "port": 22,
"identity_files": ["~/.ssh/id_ed25519_frame"],
"addresses": [{"host": "frame.local", "kind": "mdns", "label": "",
"networks": ["n-e0998baa61"], "last_ok": 1790593550.4, "last_rtt_ms": 0.9}]}],
"networks": {"n-e0998baa61": {"name": "Home Wi-Fi", "ssid": null, "gateway": "192.168.1.1",
"gateway_mac": "b4:fb:e4:b5:67:55", "wifi": true, "last_seen": 1790593550.0}}}
```
A network id is `n-` and the first 10 hex digits of SHA-1 of `gateway|mac`.
## Tests
`tests/test_devices.py`, `tests/test_network.py` and `tests/test_link.py` run
with the other unit tests. They use a stand-in `ssh` (`tests/fakessh/ssh`) that
prints what `ssh -v` prints and plays a ControlMaster, real sockets on this
computer for the addresses, and temporary folders for `~/.ssh`
(`FRAME_CONTROL_SSH_DIR`) and the app data (`FRAME_CONTROL_DATA_DIR`), so they
never touch yours.
+47
View File
@@ -0,0 +1,47 @@
Frame client feasibility, 2026-09-28
SteamOS VERSION_ID=0.4.1 BUILD_ID=20260925.6191901; uname -m=aarch64
SteamVR: vrserver log reports 2.18.1; process 2256 remained alive across checks.
Base: dcf9689f6459e576d35fc507eb702ca6b2bf4dad (main).
Unmodified upstream release APKs; installed with ui/frame_android.py install APK --vr.
No Linux host attached. No pairing, streamed video, input, audio or worn-headset checks.
Selected logcat lines only; timestamps in Android logs are UTC.
WiVRn-release.apk
https://github.com/WiVRn/WiVRn/releases/tag/v26.9
sha256=1df6649ec77224fcc821af0ab4897222bdf3d7eb6ce6ad636461336724111331
E/OpenXR-Loader( 1140): Error [GENERAL | xrCreateInstance | OpenXR-Loader] : LoaderInstance::CreateInstance, no support found for requested extension: XR_KHR_convert_timespec_time
I/WiVRn ( 1140): [2026-09-28 12:07:58.987] [WiVRn] [info] Failed to create OpenXR instance version 1.1.58: XR_ERROR_EXTENSION_NOT_PRESENT
E/OpenXR-Loader( 1140): Error [GENERAL | xrCreateInstance | OpenXR-Loader] : LoaderInstance::CreateInstance, no support found for requested extension: XR_KHR_convert_timespec_time
I/WiVRn ( 1140): [2026-09-28 12:07:59.034] [WiVRn] [info] Failed to create OpenXR instance version 1.0.58: XR_ERROR_EXTENSION_NOT_PRESENT
E/WiVRn ( 1140): [2026-09-28 12:07:59.035] [WiVRn] [error] Error during initialization: Failed to create OpenXR instance: XR_ERROR_EXTENSION_NOT_PRESENT
alvr_client_android.apk
https://github.com/alvr-org/ALVR/releases/tag/v20.14.1
sha256=be68feeb02665e3d69f1cdbcabf38ea4d15c42868a7ec6b5e698dbefee4e4e36
E/OpenXR-Loader( 1139): Error [GENERAL | xrCreateInstance | OpenXR-Loader] : LoaderInstance::CreateInstance, no support found for requested extension: XR_KHR_convert_timespec_time
I/RustStdoutStderr( 1139): Error [GENERAL | xrCreateInstance | OpenXR-Loader] : LoaderInstance::CreateInstance, no support found for requested extension: XR_KHR_convert_timespec_time
E/[ALVR NATIVE-RUST]( 1139): ALVR panicked: What happened:
E/[ALVR NATIVE-RUST]( 1139): panicked at alvr/client_openxr/src/lib.rs:220:10:
E/[ALVR NATIVE-RUST]( 1139): called `Result::unwrap()` on an `Err` value: ERROR_EXTENSION_NOT_PRESENT
Cleanup verified: both app directories, compatdata directories and Steam shortcuts absent.
Both test containers stopped and removed. No Steam/SteamVR restart or global setting changes.
Valve release notes fetched from ISteamNews/GetNewsForApp/v2 (appid=250820).
SteamVR Beta Updated - 2.18.1
https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1844751498219787
Added tethered Quest support over USB (must be used with Steam Link Beta)
Introducing SteamVR 2.17
https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1843481262693486
Adds initial support for USB streaming. Note: Requires new Steam Client Beta.
Fix crash using Steam Link on Linux when games submit invalid textures.
Improve streaming recovery when using Steam Link on Linux.
SteamVR Beta Updated - 2.17.8
https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1842212951314598
Fix crash using Steam Link on Linux when games submit invalid textures.
Improve streaming recovery when using Steam Link on Linux.
Adds initial support for USB streaming.
USB streaming can be used without WiFi by opting into the Steam Client Beta.
+128
View File
@@ -0,0 +1,128 @@
# Mod feasibility test, 2026-09-28
**Verified observations**, with limits below. Device: `aarch64`, SteamOS
`VERSION_ID=0.4.1`, `BUILD_ID=20260925.6191901`; Proton version file:
`1788505046 proton-11.0-2c-arm64`; `vrcmd --stats`: SteamVR `2.18.1`.
Times below are the Frame's AEST clock.
## Inventory and allowed content
`ssh frame 'python3 - owned' < ui/frame_steam.py` returned 868 games before
the test. Beat Saber (620980) and Skyrim VR (611670) were absent. Half-Life 2:
VR Mod – Episode One (2177750) was installed. Hogwarts Legacy (990080),
Horizon Zero Dawn (1151640) and Horizon Forbidden West (2420110) were listed
but not installed. The full personal library is not committed.
**Documented, owner report after these tests:** Alex has Beat Saber on a
Quest 2, which is charging. That copy was not accessed or inspected during
this test. Its version, transfer path and Frame compatibility remain unknown;
Steam ownership is still not established.
Gravitas's public Steam metadata reported `is_free: true`, developer Galaxy
Shark Studios, Windows only. Frame Control's existing Steam helper requested
its install. Steam first returned free-license state 3, then config state 7,
then queued the download. The helper did not accept state 3; a later read
found state 7. The source of that transition was not observed. A later
`install 1067310` returned `state: installed`. No purchase occurred.
UEVR was downloaded from the author's [1.05 release](https://github.com/praydog/UEVR/releases/tag/1.05),
not a mirror. `UEVR.zip` was 7,399,455 bytes. Its SHA-256 matched the author's
`UEVR.zip.sha256` (a UTF-16 text file):
```text
af4f2f91306802d7ee4e8497d483a547ac8e9a3067dbafb81324100524215d3c
```
Microsoft's Windows x64 .NET runtime and Windows Desktop runtime 6.0.36 ZIPs
were downloaded using URLs in the official release metadata. Both SHA-512
hashes matched that metadata. They were extracted into a test-only user
directory, not installed globally. No other mod manager was used.
## Half-Life 2 VR: visible setup, not gameplay
Launched the already-installed Episode One using
`steam steam://rungameid/2177750`. The existing manifest recorded build
25413453 and a shared base depot from app 658920, build 25413418.
Selected fresh Steam / SteamVR log lines:
```text
22:07:50 proton waitforexitandrun .../Half-Life 2 VR/ep1vr.exe
22:08:03 SetApplicationPid: Setting app steam.app.2177750 PID to 27990
22:08:03 Successfully loaded binding file '.../hlvr/cfg/steamvr/bindings_frame.json' for app 'steam.app.2177750'.
```
The game's `episodicvr/console.log` reached `Creating VR hand HUD...`,
`Creating VR weapon HUD...` and `Calibrating VR base position`. It also
contained missing material/weapon warnings. SteamVR logged a missing
`frame_hmd` binding as well as the successful controller binding load;
controller input was not exercised.
`ui/frame_vrshot.py` produced a stereo capture showing the mod's
“First time setup” and “Dominant hand” dialog. This establishes visible
startup, not a played level or comfortable performance. The capture includes
room passthrough and is deliberately not published. The test's `hl2.exe`
process was terminated; the pre-existing game installation was preserved.
## Gravitas and UEVR: injection unverified
The game was launched through Steam. The injector was then started in the
same `steamapps/compatdata/1067310` prefix using the shipped
`SteamLinuxRuntime_4-arm64/_v2-entry-point` and Proton 11 ARM64. The first
attempt reported:
```text
Application: UEVRInjector.exe
Message: You must install .NET to run this application.
Architecture: x64
App host version: 6.0.35
.NET location: Not found
```
With `DOTNET_ROOT` and `DOTNET_ROOT_X64` pointing at the test-only Windows
runtime directory, Proton loaded `Microsoft.NETCore.App/6.0.36` and
`Microsoft.WindowsDesktop.App/6.0.36`. A 25-second launcher timeout was too
short to establish whether the UI worked. A longer attempt produced a
625×372 `UEVR` X11 window on display `:1`. This is window creation evidence,
not a successful injection. That launcher returned exit 0; this was not
treated as proof that a mod worked.
A combined launch set `PROTON_REMOTE_DEBUG_CMD` to `UEVRInjector.exe` and
ran Gravitas's `Drop.exe` in the same runtime/prefix. The process
`SkyArk/Binaries/Win64/Drop-Win64-Shipping.exe` and a 1600×900 window titled
`SkyArk (64-bit, PCD3D_SM5)` appeared. The launcher exited **1** before an
injection or stereo game scene could be verified. Output included:
```text
Proton: Error while copying to ".../windows/system32/amdxcffx64.dll": No such file or directory
Error [GENERAL | xrCreateInstance | OpenXR-Loader] : xrCreateInstance failed
X Error of failed request: BadWindow (invalid Window parameter)
Major opcode of failed request: 10 (X_UnmapWindow)
X Error of failed request: XI_BadDevice (invalid Device parameter)
Minor opcode of failed request: 28 (X_GetDeviceButtonMapping)
```
These errors do **not** establish an ARM64/FEX incompatibility. Other work
was launching apps on the shared Frame. SteamVR's PIDs changed during the
experiment; `ps` recorded the replacement `vrserver` and `vrcompositor`
starting at **22:13:03**, corroborated by `steamvr.service` journal startup
lines. This test did not request a Steam/SteamVR restart. The combined
attempt ran at 22:14:17–22:14:27, after that restart. A stable, coordinated
session is needed to distinguish launcher/environment problems from game or
mod incompatibility.
## Cleanup and remaining checks
- No game executable or OpenVR DLL was replaced. No global runtime or power
setting was changed by this test. No R.E.A.L., OpenComposite or Beat Saber
payload was installed.
- Removed the test-only UEVR/.NET directory and downloaded ZIPs from the
Frame. Final process checks found no `UEVRInjector.exe`,
`Drop-Win64-Shipping.exe`, `ep1vr.exe` or `hl2.exe`. SteamVR was running.
- Gravitas and its Steam-created prefix remain installed for a repeat test;
the pre-existing HL2 VR install remains. Steam may retain normal shader
caches, logs and prefix temporary files.
- Still unverified: UEVR injection and removal, R.E.A.L. releases and
permissions, OpenComposite, Beat Saber playback on either build, and our
own manager's end-to-end install/uninstall. No installer UI is justified
by these results. See the [support table and next checks](../mods.md).
+53
View File
@@ -0,0 +1,53 @@
{
"date": "2026-09-28",
"os": {
"version": "0.4.1",
"build": "20260925.6191901",
"variant": "vr"
},
"performance": {
"compositorFps": 72.0,
"frameMs": 13.89,
"appFps": null,
"gpuMs": 3.07,
"compositorCpuMs": 0.61,
"cpuPercent": 40.6,
"gpuMHz": 903.0
},
"batteryPercent": 16,
"maxTempC": 73.5,
"ownership": [
{
"id": 1009850,
"owned": false,
"installed": false,
"frame": 0
},
{
"id": 1173510,
"owned": false,
"installed": false,
"frame": 0
},
{
"id": 1068820,
"owned": false,
"installed": false,
"frame": 0
},
{
"id": 908520,
"owned": false,
"installed": false,
"frame": 0
},
{
"id": 1494460,
"owned": false,
"installed": false,
"frame": 0
}
],
"hudLifecycle": "open, duplicate-open, close passed before control-test pause; overlay probe removal verified",
"comfort": "Initial 1cm seated probe restored exactly. Later commits/readback disagreed while Frame in use; Alex paused control tests. No controls shipped."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

+58
View File
@@ -0,0 +1,58 @@
# Independent review attempts — 2026-09-28
Target: the implementation and evidence in
[3fb541d](https://github.com/saphid/frame-control/commit/3fb541d) and
[6a63a5a](https://github.com/saphid/frame-control/commit/6a63a5a), supplied as a
frozen diff before those commits were made. No executable code changed after
the final review snapshot.
Requested model: **SWE-2 Max**, explicitly selected with `--model swe-2-max`.
No completed verdict or model self-identification was returned. This is not a
passed review, and there is no "no actionable findings" claim.
## First attempt
Launcher:
```sh
devin -p --model swe-2-max --permission-mode auto --respect-workspace-trust false --prompt-file /tmp/frame-vr-review-prompt.txt
```
The prompt required read-only review, no delegation, no edits and no Frame
access. The reviewer inspected surrounding code, then stopped while checking
the public OpenVR header. The tool runner reported:
> warning: rejected a tool call that requires confirmation. Running in non-interactive mode.
The real launcher exit status was **0**, but no verdict was returned. A zero
process status here is not evidence that the review completed.
## Tool-free retry
Launcher:
```sh
devin -p --model swe-2-max --permission-mode auto --respect-workspace-trust false --prompt-file /tmp/frame-vr-review-final-prompt.txt
```
The self-contained prompt supplied the complete frozen changes, surrounding
code, standards and locally fetched authoritative OpenVR header excerpts. It
explicitly prohibited tools, edits, delegation and device access. This avoided
the first attempt's permission boundary without escalating permissions.
No output or verdict arrived within the fifteen-minute review window. The
process was sent SIGTERM at 918 seconds; the shell recorded real exit status
**143**. Findings are unavailable. Review must be completed before considering
this partial draft ready; playspace feasibility is also still paused.
## Other validation
- 166 Python unit tests passed locally.
- 8 website tests and UI JavaScript syntax passed locally.
- Desktop and phone-width attached-preview checks passed using live telemetry;
paid optional install buttons were disabled, and unavailable metrics cleared.
- [Fake-Frame CI](https://github.com/saphid/frame-control/actions/runs/36423548487/job/108931878908)
passed, as did Windows/Linux server tests and the main checks job. Docker was
unavailable locally. macOS/iOS jobs were still queued at this handoff.
- Real-device evidence and the paused-control limitation are in
[VR utilities](../../vr-utilities.md).
+122
View File
@@ -0,0 +1,122 @@
# Family and comfort
Frame Control's Home tab has a **Family and comfort** card, on desktop and
on iPhone. No third-party notification or parental-control app is needed.
This is Frame Control code using Python, Steam and SteamVR already on the Frame.
![Family and comfort controls in the desktop app](img/comfort-desktop.png)
## Sessions
Set a limit of 1–240 minutes, optional break and check-in intervals, then
**Start session**. Break and check-in intervals of 0 turn those reminders off.
**Cancel session** cancels the timer and monitoring without changing the game.
Cancel before starting a session with different settings.
The Frame shows a one-minute warning, then opens Steam Home in its dashboard.
**Games stay running**: save and pause before the limit. Some games pause when
the dashboard opens; others do not. There is no kill, power-off, Steam restart,
account restriction or parental lock. The wearer can return to the game.
**Documented implementation:** the timer is a single, opt-in Python worker in
the Frame user's account. Desktop and iPhone share its state. It keeps going
when the companion disconnects, closes or is suspended. It exits after
completion or cancellation (normally within five seconds). Cancellation waits
for any in-flight SteamVR action to finish within its timeout; it is not a boot
service. A Frame reboot invalidates the session. Suspend counts toward the
limit, using Linux's boot-time clock. If a warning was delayed by suspend or a
SteamVR failure, Home waits until at least a full minute after a successful
warning. A failed Home transition remains active and retries, with an error
shown in the companion. A stale worker is reported as unverified enforcement.
## Alerts and breaks
During a session, battery, overheating and check-in alerts go to connected
companions. Break reminders and session warnings also appear on the headset.
- **Low battery:** 15% or below while discharging. One alert until charging or
recovery to 20%, so values around 15% do not produce repeated notifications.
- **Overheating:** a thermal zone reaches its own kernel-reported hot/critical
trip, or the battery reports `Overheat`. Missing sensors mean unknown, not
safe. These are status alerts, not medical advice or an extra thermal governor.
- **Check in:** an alert after the chosen number of active minutes.
- **Breaks:** a SteamVR reminder and companion notification at the chosen interval.
**Inferred:** SteamVR activity levels 1 and 2 are a useful proxy for use, not
proof someone is wearing the headset. Inactive readings reset continuous use;
missing readings add no time. Long gaps count at most 30 seconds. Breaks and
check-ins are distinct from the elapsed-time session limit.
Click **Enable / test notifications** on each companion. iOS asks for permission;
macOS, Windows and Linux follow their notification settings. The page also shows
recent events and errors. Keep Frame Control open and connected for companion
alerts. **Phone alerts are local, not push notifications:** iOS suspension,
force-quit or a lost SSH connection prevents live delivery. Old alerts are not
replayed as a notification burst on reconnect. Headset warnings and the session
limit continue without the phone. A physical iPhone's background delivery has
not been verified and is not guaranteed.
## Casting
**Cast headset view** starts the existing headset Live view and requests full
screen where supported. Show that screen to people in the room, or use the
computer/phone's own screen mirroring. It creates no new stream transport,
public URL or LAN server. iPhone uses the inline viewer if full screen is not
available. The image includes private content visible to the wearer.
## What is installed
The shared authenticated `/api/comfort` endpoint copies three bundled Python
files to `~/.cache/frame-control/comfort/<content-hash>/`. Session state and
locks live in `~/.local/state/frame-control/comfort/`, with a private directory
and 0600 state file. There is no network listener or system service. Cancel a
session before removing these directories. The iPhone's normal server still
exits on disconnect; the explicitly started comfort worker is the exception.
## Verification
**Verified 2026-09-28**, SteamOS 0.4.1, build `20260925.6191901`: shipped
`/opt/steamvr/bin/linuxarm64/vrcmd --notify TEXT` reported success for a custom
reminder. Steam's CDP `SteamUIStore.Navigate('/library/home')` and
`SteamClient.OpenVR.VROverlay.ShowDashboard('valve.steam.gamepadui.main')`
opened Home while the running app ID stayed unchanged. Prior page and dashboard
visibility were restored. Kernel hot/critical trips and SteamVR activity were
read from the real device. No temperature or battery fault was induced.
**Verified locally:** deterministic fake-Frame tests cover late warnings,
failed warnings/Home actions, cancellation, activity gaps, thresholds, duplicate
suppression, reboot invalidation, shared session state and the exact Home
JavaScript. `python3 -m unittest discover -s tests` runs them. The iOS Simulator
build tests notification content and bounds. Physical iPhone delivery and
wearer-perceived headset notification visibility remain unverified.
**Verified end to end on the same Frame:** a two-minute session with no companion
connection for 135 seconds emitted its warning, break and check-in, then opened
Home. The running app ID was unchanged; the test restored the previous page and
dashboard visibility and confirmed the worker exited. Casting through the Home
shortcut decoded the existing headset stream at 30 fps.
**Verified on the iOS 26.5 Simulator:** connected to the real Frame, approved the
notification prompt, and saw the native Frame Control test banner. Seven iOS
tests passed.
![Native test notification in the iOS Simulator](img/comfort-notification-ios.png)
Desktop and 390-pixel phone layouts had no horizontal overflow.
On macOS the development Electron app's real notification attempt was denied
(`UNErrorDomain` 1); the bridge now returns that failure instead of reporting
success. Successful macOS/Windows/Linux notification display remains unverified.
**Verified on the real Frame:** its naturally discharging 15% battery produced
one low-battery event during a short session; the test then cancelled the
session. Overheating alerts use fake sensor samples in tests: the shared
headset was not deliberately overheated.
**Verified 2026-09-29 on the same Frame:** a fresh one-minute session opened
Home more than 60 seconds after the successful warning. The test restored the
previous page and dashboard visibility. Local regression coverage now includes
slow notification delivery, a total Home-action timeout, failed worker startup,
unreadable saved state, malformed activity samples and notification UX: 173
Python tests passed. Desktop and 390-pixel layouts were checked again; system
notification-denial guidance stayed visible across polls. Initial event history
did not replay notifications, and only the latest new event was announced.
+44 -14
View File
@@ -17,13 +17,16 @@ python3 ui/server.py # anywhere: then open http://127.0.0.1:47810
## Features
The window has four tabs: **Home** (headset view, status, screenshots),
The window has five tabs: **Home** (headset view, status, screenshots),
**Games** (installed games, sideloaded titles, getting games), **Android** (apps,
the catalogue, display settings, reports) and **Tools** (sending files and text,
Flatpaks, remote and power). Keys 1–4 switch between them. Files can be dropped
anywhere in the window. When the Frame can't be reached, one banner says why in
plain words and the app retries every few seconds, filling everything in once it
answers. Flatpak and Android installs run in the background; the bottom bar
the catalogue, display settings, reports), **Tools** (sending files and text,
Flatpaks, remote and power) and **Devices** (your headsets and their addresses).
Keys 1–5 switch between them. Files can be dropped anywhere in the window. A
connection pill in the header always shows which headset, which network this
computer is on, the address in use or being tried, and each step of connecting
as it happens; click it for the whole timeline. When the Frame can't be
reached, a banner says why in plain words, what was tried, and counts down to
the next try, filling everything in once it answers. Flatpak and Android installs run in the background; the bottom bar
counts them while they run.
- **Headset view**: what the lenses show, as SteamVR composites it (the room,
@@ -56,9 +59,11 @@ counts them while they run.
Steam library), then launch, stop, test or remove it. **Report an APK** records
whether any APK worked (F-Droid or not: pick a file, type a package, or use an
installed app). Your reports are saved on your computer and change the verdicts
you see. They aren't uploaded anywhere: the shared database is maintainer-only
for now (see [compat-db/README.md](../compat-db/README.md)). Uses the app's bundled
`adb`, or yours if you have one.
you see. With **Share compatibility results** on (Privacy & updates), they also
go to the shared database ([privacy.md](privacy.md),
[compat-db/README.md](../compat-db/README.md)). A failed install records
itself when the APK was the problem, and after an install the app offers a
20-second test. Uses the app's bundled `adb`, or yours if you have one.
- **Android display**: pick a running Lepton instance (by the app in it) and set
its resolution (Native 1920×1080, or Sharp 2560×1440 with density scaled to
match), UI scale (Smaller / Default / Larger, or an exact dpi) and text size
@@ -70,8 +75,17 @@ counts them while they run.
Runtime picked from the program's header), listed under **Sideloaded titles**
with Launch and Remove; see [sideloading.md](sideloading.md). Send typed text, or your computer's clipboard, to the
Frame clipboard.
- **Mac in the headset** (macOS): show any Mac window, or a whole screen, as
its own panel in the headset. Place it with the SteamVR dashboard, click and
scroll with the laser, and type on the Mac. Streams hardware H.264 through
an SSH tunnel; see [mac-in-headset.md](mac-in-headset.md).
- **Flatpaks**: install and remove them (quick picks: Moonlight, Firefox, VLC,
Remmina).
- **Devices**: several headsets, each with several addresses (LAN IPs per
network, its `.local` mDNS name, its Tailscale IP or MagicDNS name). The app
tries them all at once and learns which worked on which network. Add, edit,
reorder and test addresses, find a headset on Tailscale or on this network,
name your networks, and switch headsets. See [devices.md](devices.md).
- **One-click tools**: SSH or SFTP in a terminal window, Steam Link, and remote
desktop (Windows App on macOS, Remote Desktop on Windows, Remmina or FreeRDP on
Linux). Sleep, restart and shut down open a terminal window because SteamOS
@@ -86,13 +100,18 @@ app bundles `ui/`, `scripts/`, `frame/android/`, Valve's `frame/devkit-utils/` a
([python-build-standalone](https://github.com/astral-sh/python-build-standalone))
and `adb` from Google's platform-tools, so there's nothing else to install. It
also bundles curl's copy of Mozilla's CA list, because Python on Windows only
trusts root certificates already in the Windows store.
`app/build/fetch-deps.js` downloads both, pinned by SHA-256.
trusts root certificates already in the Windows store. And it bundles KDE
Connect for the Frame (Valve's arm64 build and five libraries, 3.6 MB,
[`frame/kdeconnect`](../frame/kdeconnect/NOTICE.md)), which it copies to the
Frame for the keyboard and trackpad.
`app/build/fetch-deps.js` downloads all of it, pinned by SHA-256.
The server is Python stdlib only and listens on 127.0.0.1. It rejects requests
with a non-local `Host` header, and any `/api/` request without a custom
header, so other websites can't drive it or read captures. Everything reaches
the headset through the `frame` SSH alias. On macOS and Linux it keeps one
the headset through its SSH alias (`frame` for the first one), pointed at the
address that answered with `-o HostName=` (`ui/frame_link.py`, described in
[devices.md](devices.md)). On macOS and Linux it keeps one
multiplexed SSH connection open, so status and each capture take about 0.3 s.
Windows' OpenSSH can't share a connection, so there each request connects on
its own and the app is a little slower. What differs between the three
@@ -148,5 +167,16 @@ npm run dist:win # Windows: installer and .zip
npm run dist:linux # Linux: AppImage and .deb, x64 and arm64
```
Pushing a `v*` tag builds all three in GitHub Actions and attaches them to the
release (`.github/workflows/release.yml`).
Pushing a `v*` tag builds all three in GitHub Actions and attaches them to a
draft release (`.github/workflows/release.yml`). Running copies are offered it
once you publish it: see [releasing.md](releasing.md).
## AI agents and assistant
**Documented:** [the MCP adapter and assistant panel](agents.md) are Frame
Control implementations. MCP wraps this HTTP API without API keys. Changes
require a separate user approval; power also retains its password prompt. The
assistant uses a user-chosen endpoint and sends nothing until the user opts in
for a message. Screenshot context is separately opt-in. Model replies cannot
operate the headset. Tools → Open assistant opens the page; the linked guide
covers putting it in a Chromium panel on the Frame.
+10 -1
View File
@@ -38,6 +38,11 @@ Lepton (Android 11, podman container "lepton-dev") ← its own panel, app 305600
| SteamVR settings live in `~/.config/openvr/config/steamvr.vrsettings`, not under `~/.local/share/Steam/config/`. `dashboard.lastAccessedExternalOverlayKey` names the last panel you used. | Settings tweaks |
| The Steam client's journal (`journalctl --user`) carries SteamVR system UI lines such as `[Overlays] Created: …` and `vroverlay_uid<appid>`. It's the quickest way to see panels come and go. | Debugging |
| Present: `rsync`, `flatpak`, `python3`, `git`, `qdbus6`, `xrdp`, `xprop`, `xwininfo`, `xterm`, `konsole`, `dolphin`, `gamescopectl`. Missing: `wl-copy`, `xclip`, `xsel`, `kdeconnect-cli`, `tailscale` (installable in `~`, see below), `krfb`, `wayvnc`. | Script design |
| **SteamOS updates arrive on their own.** The Frame went from 0.3.0 (build 20260922.6101926) to **0.4.1, build 20260925.6191901**, between 2026-09-27 and 2026-09-28 with no action from us; `~` (keys, user Flatpaks, `~/.local/share`) survived. **Verified 2026-09-28.** | Keep changes in `~` |
| **Valve's package repository has more than the image.** `pacman -Si` / `pacman -Sp` work as `steamos` without root and list Valve's own builds, such as `kdeconnect` 24.02.2 and `python-evdev` 1.7.0 in `extra`. Unpacking those packages into `~` runs them without touching the read-only root. The repository URLs say not to share them, so never write them down; Valve also publishes each build's source package there (`sources/packages/`), which is how Frame Control got the complete source for the KDE Connect it ships. **Verified 2026-09-28**, SteamOS 0.4.1. | [streaming.md](streaming.md#input-type-and-point-in-the-frame-from-the-mac-or-iphone) |
| **gamescope has its own input injection.** An EIS socket at `/run/user/1000/gamescope-0-ei` (libei 1.4.1 is on the image) offers "Gamescope Virtual Input": relative and absolute pointer, buttons, scroll, keyboard. It drives the panel that has focus in the headset, on either X display. Focus moves only with the controller's laser (or to a new panel when none has it); `gamescopectl focus_info` prints the focus state to the journal. **Verified 2026-09-29.** | [streaming.md](streaming.md#live-view-and-control-watch-a-panel-and-tap-on-it) |
| **A panel's own pixels:** `ffmpeg -f x11grab -window_id <window> -i :<display>` captures one window (x11grab of the root is black under gamescope). Panels live on `:0` (Steam's UI, windows tagged by `panel-on-frame.sh`) or `:1` (apps Steam starts). **Verified 2026-09-29.** | Frame Control's Desktop view |
| **gamescope runs two Xwayland displays.** `:0` holds Steam's VR bar and menus (`valve.steam.gamepadui.*`) and ignores XTest pointer motion; `:1` holds apps such as Chromium and takes it. There's also a libei socket, `/run/user/1000/gamescope-0-ei`. **Verified 2026-09-28**, SteamOS 0.4.1. | Keyboard and trackpad |
| Flathub is a **system** remote. `--user` installs over SSH work and show up in the desktop menu. | `install-apps.sh` |
| `/` is 10 GB and read-only. `/home` is 929 GB. | Where to put things |
| Clipboard: Klipper over the nested D-Bus bus (`qdbus6 org.kde.klipper …`). | `paste-to-frame.sh` |
@@ -52,13 +57,17 @@ Lepton (Android 11, podman container "lepton-dev") ← its own panel, app 305600
| **T3 Code desktop runs natively.** The stock release `T3-Code-0.0.42-arm64.AppImage` in `~/Applications/T3CodeDesktop/` starts with no extra setup: glibc 2.39, `libfuse.so.2`, GTK 3, NSS and libsecret are on the image. `panel-on-frame.sh --name t3code-desktop -- '~/Applications/T3CodeDesktop/T3-Code.AppImage'` gives it its own panel (`valve.steam.desktopgame.2000281357`, `--ozone-platform=x11`). Its bundled server listens on `127.0.0.1:3773` and shows up in onboarding as the `frame` computer, with `passwordStore: gnome-libsecret`. The image has no agent CLI and no `node`. Agents run through the LAN CLIProxyAPI (`llm-proxy.lan:8317`, which resolves on the Frame). Claude Code 2.1.283 comes from `claude.ai/install.sh`, and Codex 0.157.1 from the `codex-aarch64-unknown-linux-musl` release tarball, both into `~/.local/bin`. `with-cliproxy` and a mode-600 `~/.config/cliproxyapi/secrets.env` are copied from the Mac. The wrappers `claude-cliproxy` and `codex-cliproxy` (a `-c model_provider=cliproxy`, `wire_api="responses"`, `env_key="CLIPROXY_API_KEY"`) are set as `providers.claudeAgent.binaryPath` and `providers.codex.binaryPath` in `~/.t3/userdata/settings.json`, and T3 picked that up without a restart. Through the wrappers, `claude auth status` reports `loggedIn: true` (`oauth_token`), and both CLIs answered a prompt with `kimi-k3`. `gamescopectl screenshot` captured another layer (the Lepton T3 app) rather than this panel. `DISPLAY=:0 xwd -id <win>` piped to `ffmpeg` captures the window itself (1920×1080). **Verified 2026-09-26**, BUILD_ID 20260922.6101926. | Running T3 Code as a host on the Frame |
| Power actions need `sudo`, which asks for the Developer Mode password over SSH. | Frame Control's power buttons |
| **SSH server:** OpenSSH 9.7p1. It offers `publickey,password` (keyboard-interactive is off, PAM on) and also asks `userdbctl ssh-authorized-keys` for keys. OpenSSH ≥ 8.8 rejects SHA-1 `ssh-rsa` signatures by default, so a client whose RSA support is SHA-1 only (the Swift library Citadel, for one) can't log in with the RSA key that devkit pairing installs; use ed25519 (**inferred** from OpenSSH defaults). **Verified 2026-09-27**, BUILD_ID 20260922.6101926. | [iphone.md](iphone.md), `ui/frame_connect.py` |
| **A Chromium app window on gamescope's `:0` with its own `STEAM_GAME` becomes a panel, even when started over SSH.** `chromium-xr/chrome --ozone-platform=x11 --app=URL --window-size=1280,720` got a 1920×1080 window, and tagging it produced `[Overlays] Created: valve.steam.desktopgame.<id>` in `~/.local/share/Steam/logs/vrwebhelper_systemui.txt`. Chromium XR decoded a 1080p H.264 WebCodecs stream from the Mac at about 60 fps. XTest events sent to `:0` (libXtst through Python ctypes) did not reach the page. The Frame has `libXtst`, `xprop`, `xwininfo`, `curl` and the `C.utf8` locale. **Verified 2026-09-28**, BUILD_ID 20260925.6191901. | [mac-in-headset.md](mac-in-headset.md) |
| **Streaming video into a panel: what the Frame adds.** Chromium XR decodes H.264 in **software** (1920×1290 at about 9 Mbit/s took 4–6 ms per frame); its GL is ANGLE → zink → Turnip on the Adreno 750, and it logs `GetVSyncParametersIfAvailable() failed`. An idle page's `requestAnimationFrame` runs at about 140 Hz; when the headset is worn or woken, SteamVR picks the 4320×2160 @ 144 Hz mode. **An unworn headset throttles panel apps** whatever they draw: a local canvas page ran at 58 fps for about 6 s, then 28, then about 15 once in standby (vrserver logs `entering standby`, with `power.pauseCompositorOnStandby 1`). `vrcmd --handlewakeup` gave 137–144 fps for about 2 s, then 36. So a panel's frame rate and compositor delay can only be measured while it's worn. `tailscaled` runs with `--tun=userspace-networking` and cost about 8% of a core at 9 Mbit/s (0.5% over the LAN address). Wi-Fi power saving is on (`iw … get power_save`). **Verified 2026-09-28**, BUILD_ID 20260925.6191901. | [mac-in-headset.md](mac-in-headset.md#measuring) |
| **USB-C networking.** Plugged into a Mac, the Frame is a USB network device: macOS names the port "Steam Frame" (here `en9`, 10.86.200.234/29), and the Frame's `usb0` is 10.86.200.233. Ping is about 0.9 ms, and SSH works with the usual host key (`-o HostName=10.86.200.233 -o HostKeyAlias=<tailscale name>`). Steam's Remote Play discovery also broadcasts over it. **Verified 2026-09-28**, BUILD_ID 20260925.6191901. | Frame Control's Mac stream uses it when present ([mac-in-headset.md](mac-in-headset.md)) |
| **Tools on the image:** Python 3.12.3, `ffmpeg`, `openssl`, `curl`, `rsync`, `zip`/`unzip`, `flatpak`, `wpctl`, `podman`. **No `adb`.** `steamos` is uid 1000, in `wheel`, and sudoers has `%wheel ALL=(ALL) ALL`, so `sudo -S` takes the Developer Mode password on stdin. **Verified 2026-09-27.** | Running Frame Control's server on the Frame (`FRAME_LOCAL=1`, [iphone.md](iphone.md)) |
| **Each Lepton instance is a podman container** named `lepton-steamlaunch-<instance id>`, labelled with its ADB port (`podman ps --format '{{.Names}} {{.Labels.adb_port}}'`). `podman exec <container> /system/bin/sh -c '…'` runs Android's shell inside it with no adb at all (used for `pidof` and `logcat` by the app tester). Running `wm size`/`wm density` that way is untested. **Verified 2026-09-27.** | `ui/frame_android.py`, the iPhone app's display settings |
| **Asleep means off the network.** In standby the Frame stops answering on its LAN address, `frame.local` and Tailscale alike (`Host is down`, `No route to host`, timeouts), and ping fails. It was unreachable for about 2.5 hours until woken. Nothing over SSH can wake it. **Verified 2026-09-27.** | Frame Control's offline banner and retries |
| **What puts it to sleep is Steam's idle timer**, not logind. The journal shows `steamui_system: Switching to power state: [ k_ESystemPowerState_Sleep ] reason: 'ComputeNextPowerState: active: 3600 < 3600 (k_EACState_Connected)'`, then Steam suspends. SSH work doesn't count as activity. The timers are the client settings `system_idle_suspend_ac_sec` (3600) and `system_idle_suspend_battery_sec` (900); 0 means Never (Settings → Power → Sleep after inactivity). They can be written over DevTools the way the settings page does. logind refuses a `systemd-inhibit --mode=block` sleep lock from an SSH session (`Interactive authentication required`) but accepts one started with `systemd-run --user`. `scripts/keep-awake.sh on|off|status` does both and restores the old timers on `off`. **Verified 2026-09-28**, BUILD_ID 20260925.6191901. Whether Steam's suspend honours the inhibitor on its own is **inferred** (polkit gives `steamos` no `suspend-ignore-inhibit`), not tested. | Keeping the Frame awake for agent work |
| **Battery at full on a charger** can read `Discharging` at about 0 W (for example 99 %, 0.0 W, USB-C PD 18 W). Treat under 0.5 W on a charger as "not charging", not "draining". **Verified 2026-09-27.** | Frame Control's battery card |
| **The OS image is downloadable.** Valve's recovery images for the Frame are at `https://steamdeck-images.steamos.cloud/recovery/`. The root filesystem inside is btrfs, and it runs as an SSH test target on ARM64 Linux without the headset (`tests/frame-container/frame-image.sh`). **Verified 2026-09-27.** | [recovery-and-images.md](recovery-and-images.md) |
| **Boot / recovery menu.** Hold Power ~10 s until the LED goes off, then power on while holding the **AUX button on top of the Power button** (not the volume keys) until a text menu appears. Entries: `Current` (SteamOS-A/B + build), `Previous` (the other A/B slot), `Boot from USB`, `Repair Steam Installation`, `Erase User Data` (factory reset), `ADB mode`, `Battery Ship Mode`. It auto-boots `Current` after a ~15 s countdown. **Volume Up/Down (left side) move, AUX (right side) selects.** For a boot loop, Valve says pick `Previous` (keeps user data); then `Repair Steam Installation`; `Erase User Data` wipes `~` (SSH keys, Tailscale, Flatpaks, T3 setup). Last resort is a full re-image, two ways: (1) USB: write `steamframe-oobe-repair-<build>.img.bz2` to an 8 GB+ USB-C stick (Balena Etcher on the Mac), pick `Boot from USB`, then use "Wipe Device & Install SteamOS" / "Repair SteamOS" (keeps games and personal content) from the recovery desktop; (2) cable/EDL: `steamframe-oobe-repair-qdl-<build>.tar.gz`, run `flash.sh` (Linux) or `flash.cmd` (Windows), then with the Frame off for 10 s hold Power + Vol Up + Vol Down for 10 s and plug it in; it reflashes and reboots. Both images: `https://steamdeck-images.steamos.cloud/recovery/` (build 20260922.5153644, 0.3.0, 3.8 GiB each, no published checksums); local copies in `~/Downloads/steam-frame-recovery/`. File names, checksums and what's inside: [recovery-and-images.md](recovery-and-images.md). Source: Valve's [SteamOS Recovery FAQ](https://help.steampowered.com/en/faqs/view/1B71-EDF2-EB6D-2BB3) and [Installation and Repair FAQ](https://help.steampowered.com/en/faqs/view/65B4-2AA3-5F37-4227), plus a menu photo in [EloiStree/HelloSteamFrame#9](https://github.com/EloiStree/HelloSteamFrame/issues/9). **Inferred** (Valve docs, 2026-09-26); not yet tried on our Frame. | Recovering from a boot loop |
| **Boot loop cause: the SteamVR health check.** `steamvr.service` runs `/usr/share/deckard/steamvr-health-check`, which appends `frog:glasses:` to `$XDG_RUNTIME_DIR/steamvr-short-session-tracker` on every failed or <10 s SteamVR run. At 3 it runs `steam-health-check --repair-now`, which **deletes all of `~/.local/share/Steam` (games, login, Developer Mode) and `~/.steam`**, keeping only `registry.vdf`. At 4 it also tries `steamos-bootconf set-mode reboot-other` (fails as the user: `bootenv: Permission denied`). SteamVR normally fails 1–2 times per boot while it waits for the Steam client (`SteamAPI_InitEx failed … Steam is probably not running`, then `fatal stalled cross-thread pipe`). Once Steam has been wiped, it has to re-download a ~210 MB client on every boot, so SteamVR keeps failing, Steam keeps getting wiped and the Frame reboots, in a loop. Also, the Steam updater can deadlock at `Installing update...` (main process blocked writing to the `-child-update-ui` process, which is stuck in `drm_syncobj_array_wait_timeout`). Killing only the `-child-update-ui` process lets the install finish (`package/*.installed` appears). **Fix without sudo:** over USB-C ADB (`adb -s frame shell` works as `steamos` while the Frame is looping; SSH is refused once Developer Mode is lost), truncate both `/run/user/1000/steam{,vr}-short-session-tracker` files and `chmod 444` them (the health check then logs `Permission denied` and does nothing; this is tmpfs, so it resets on reboot). Unstick the updater if needed, let Steam finish installing, then hold Power 10 s and start the Frame normally. `systemctl reboot` over ADB needs interactive auth. After the fix, sign in to Steam and turn Developer Mode back on. **Verified 2026-09-26**, BUILD_ID 20260922.6101926, slot B (clean boot: 0 SteamVR failures, SSH and Tailscale back). | Diagnosing a boot loop |
| **Boot loop cause: the SteamVR health check.** `steamvr.service` runs `/usr/share/deckard/steamvr-health-check`, which appends `frog:glasses:` to `$XDG_RUNTIME_DIR/steamvr-short-session-tracker` on every failed or <10 s SteamVR run. At 3 it runs `steam-health-check --repair-now`, which **deletes all of `~/.local/share/Steam` (games, login, Developer Mode) and `~/.steam`**, keeping only `registry.vdf`. At 4 it also tries `steamos-bootconf set-mode reboot-other` (fails as the user: `bootenv: Permission denied`). SteamVR normally fails 1–2 times per boot while it waits for the Steam client (`SteamAPI_InitEx failed … Steam is probably not running`, then `fatal stalled cross-thread pipe`). Once Steam has been wiped, it has to re-download a ~210 MB client on every boot, so SteamVR keeps failing, Steam keeps getting wiped and the Frame reboots, in a loop. Also, the Steam updater can deadlock at `Installing update...` (main process blocked writing to the `-child-update-ui` process, which is stuck in `drm_syncobj_array_wait_timeout`). Killing only the `-child-update-ui` process lets the install finish (`package/*.installed` appears). **Fix without sudo:** over USB-C ADB (`adb -s frame shell` works as `steamos` while the Frame is looping; SSH is refused once Developer Mode is lost), truncate both `/run/user/1000/steam{,vr}-short-session-tracker` files and `chmod 444` them (the health check then logs `Permission denied` and does nothing; this is tmpfs, so it resets on reboot). Unstick the updater if needed, let Steam finish installing, then hold Power 10 s and start the Frame normally. `systemctl reboot` over ADB needs interactive auth. After the fix, sign in to Steam and turn Developer Mode back on. **Verified 2026-09-26**, BUILD_ID 20260922.6101926, slot B (clean boot: 0 SteamVR failures, SSH and Tailscale back). **Seen again 2026-09-28** on BUILD_ID 20260925.6191901, beta client 1790377368. The Frame rebooted by itself while off the network. At 21:13 the check tried `reboot-other` (`bootenv: Permission denied`) and reset the unpacked Steam install, and the tracker had 15 entries. The tracker fix above, applied over SSH, stopped the resets (the checks then log `Permission denied`), but Steam itself kept exiting about 17 s after each start (34 restarts). Cause not found yet. | Diagnosing a boot loop |
## Debug recipes
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+14 -2
View File
@@ -12,7 +12,8 @@ An iPhone can't run Python or `ssh`, but the Frame can. So the app:
1. connects to the Frame over SSH itself (the [Citadel](https://github.com/orlandos-nl/Citadel)
Swift SSH library), with its own ed25519 key from the Keychain;
2. copies Frame Control's server and helpers (`ios/scripts/make_frame_bundle.py`,
under 1 MB) to `~/.cache/frame-control/<version>` on the Frame, once per version;
4.6 MB, 3.6 MB of it the KDE Connect the keyboard and trackpad use) to
`~/.cache/frame-control/<version>` on the Frame, once per version;
3. starts `ui/server.py` there with `FRAME_LOCAL=1`. It listens only on the
Frame's own 127.0.0.1, and it stops when the phone disconnects (`--exit-on-eof`);
4. tunnels to it through the SSH session and shows the same page as the desktop
@@ -25,7 +26,10 @@ as its transport too), so the desktop and phone share one code path. Android
display settings use `podman exec` into each Lepton container instead of adb,
which the Frame doesn't have.
Nothing is left running on the Frame after the phone disconnects; the copied
The app server stops after the phone disconnects. An explicitly started
[comfort session](family-comfort.md) keeps its timer and headset reminders running
until the session ends or is cancelled; phone notifications require the app to
remain connected and running. The copied
files stay in `~/.cache/frame-control` (delete it any time).
## Pairing
@@ -107,3 +111,11 @@ running), a real sleep/restart/shut down on the Frame, and a physical iPhone.
Debug builds have Simulator test hooks (`FRAME_TEST_HOST`, `FRAME_TEST_PAGE`,
`FRAME_TEST_JS`, and the tunnel URL in the app's Caches folder); release builds
don't.
## Family and comfort
The shared Home card sets session limits, breaks and check-ins, and offers
**Cast headset view**. **Enable / test notifications** requests iOS notification
permission and sends a local test. These are local notifications, not APNs push;
iOS background suspension can interrupt phone alerts. The headset timer still
runs. See [the behavior and verification limits](family-comfort.md).
+195
View File
@@ -0,0 +1,195 @@
# PC VR streaming from Linux
**Recommendation, 2026-09-28:** test Valve's current SteamVR/Steam Link path
on a Linux gaming PC before building another streamer. Valve now documents
Linux streaming fixes and USB support. We have no Linux host attached, so
Linux-to-Frame VR streaming remains **unverified here**.
This is the feasibility and options report for
[#24](https://github.com/saphid/frame-control/issues/24), not a shipped streaming
feature. Frame Control's features must use our own implementation or standard
platform components. WiVRn and ALVR are research comparisons, not dependencies.
An optional install shortcut is the most we would offer for a third-party app.
Our own streamer requires Alex's choice before implementation.
## What was checked on the Frame
**Verified** on 2026-09-28: aarch64, SteamOS **0.4.1**, BUILD_ID
`20260925.6191901`, SteamVR **2.18.1**. Version and build are recorded separately;
earlier docs associate this build with other SteamOS version labels.
| Client | Installation | Runtime result |
|---|---|---|
| WiVRn **26.9**, upstream `WiVRn-release.apk` | API 29, arm64-v8a; installed in its own immersive Lepton instance | OpenXR instance creation fails: missing `XR_KHR_convert_timespec_time`. Both 1.1.58 and 1.0.58 attempts return `XR_ERROR_EXTENSION_NOT_PRESENT` |
| ALVR **20.14.1**, upstream `alvr_client_android.apk` | API 26, arm64-v8a; installed in its own immersive Lepton instance | Same missing extension. Client panics at `client_openxr/src/lib.rs:220` with `ERROR_EXTENSION_NOT_PRESENT` |
The [evidence excerpt](evidence/linux-vr/2026-09-28.txt) includes APK SHA-256s,
upstream release links, loader errors and cleanup results. These are failures
before an OpenXR session, not successful VR clients. WiVRn was launched twice;
ALVR's container remained up despite its client panic. Container liveness alone
does not establish VR compatibility.
Both APKs already declare `MAIN` and `LAUNCHER`. They were installed unmodified
using this branch's existing `python3 ui/frame_android.py install APK --vr`,
then launched through their Steam shortcuts. Logs came from the instance's
`podman exec … /system/bin/logcat`; the user journal also retained WiVRn's errors
after its container exited. No headset was worn and no host was connected.
All test app files, compatdata, shortcuts and containers were removed afterwards.
SteamVR's original process remained running. No global settings changed.
### Relation to the VR APK branch
**Documented from source:** [PR #20](https://github.com/saphid/frame-control/pull/20)
was read, not edited (branch inspected at
[`038dcd4`](https://github.com/saphid/frame-control/commit/038dcd48cd75336f6a86c63c7878bfc9c52deec9)).
Its compatibility layer handles OpenXR version negotiation, some controller
profiles and refresh-rate requests. It does **not** implement
`XR_KHR_convert_timespec_time`. Its launcher fix is unnecessary for these APKs.
This report has **no unmerged code dependency** on that PR, and neither APK was
tested with its layer injected.
**Documented from upstream source:** WiVRn requests the extension in
[`application.cpp`](https://github.com/WiVRn/WiVRn/blob/bbc6e4cc36c355fa6180980abd231673dc15115d/client/application.cpp#L1286)
and uses it to convert `CLOCK_MONOTONIC` into `XrTime` in
[`instance::now()`](https://github.com/WiVRn/WiVRn/blob/bbc6e4cc36c355fa6180980abd231673dc15115d/client/xr/instance.cpp#L335).
ALVR also [requests it unconditionally](https://github.com/alvr-org/ALVR/blob/a9f6542fa507a841f40ab4f3fcb531427cd02550/alvr/client_openxr/src/lib.rs#L188).
Simply deleting the extension request or returning made-up timestamps would
not prove correct tracking or timing. A real fix needs a valid clock mapping
and further runtime tests. No such patch was made.
### Native SteamOS aarch64 clients
**Verified:** the Frame has a native OpenXR runtime manifest at
`~/.config/openxr/1/active_runtime.json`, pointing to SteamVR's
`bin/linuxarm64/vrclient.so`.
**Documented:** WiVRn's [26.9 README](https://github.com/WiVRn/WiVRn/blob/bbc6e4cc36c355fa6180980abd231673dc15115d/README.md)
describes its Linux client as debugging-only, without audio or hardware decode.
ALVR 20.14.1's [non-Android decoder](https://github.com/alvr-org/ALVR/blob/a9f6542fa507a841f40ab4f3fcb531427cd02550/alvr/client_core/src/video_decoder/mod.rs)
returns no decoded frames. The inspected releases ship Android clients, not a
ready-to-run native Frame client.
**Inferred:** a native port is possible research, but neither release offers a
demonstrated native alternative to the blocked APKs. Native builds, native
extension enumeration, hardware decoding and audio were **not tested**. The
Android extension failure does not establish that the native runtime lacks it.
## (a) Valve's own path — recommended first
**Documented**, from Valve's release notes rather than launch-window reports:
- [SteamVR 2.17.8 beta](https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1842212951314598)
says “Fix crash using Steam Link on Linux when games submit invalid textures”
and “Improve streaming recovery when using Steam Link on Linux.” It also
adds initial USB streaming, with Steam Client Beta required to use USB
without Wi-Fi.
- [SteamVR 2.17 release](https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1843481262693486)
repeats Linux streaming fixes and initial USB support. USB is no longer
solely a claim about an old beta, but version/channel requirements still
need checking on the actual host.
- [SteamVR 2.18.1 beta](https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1844751498219787)
adds USB-tethered **Quest** support with Steam Link Beta. That entry is not
proof of a Frame/Linux combination.
- The [Steam Link page](https://store.steampowered.com/app/353380/Steam_Link/)
lists Linux desktop clients, while its Quest VR requirements still say
Windows 10 or newer. Desktop Steam Link support is not equivalent to VR host
support, and the Quest requirements are not a Frame support matrix.
**Inferred:** Valve has a Linux VR streaming path worth testing. The old blanket
claim “Linux cannot stream VR” is no longer justified by the evidence. These
release notes do not establish which Linux GPU/driver/Frame combinations work.
USB changes the transport; it does not by itself prove host encoder support.
**Not verified:** Linux host discovery, pairing, wireless or USB streaming,
stereo rendering, controllers, haptics, audio, latency, or a game. Frame-only
inspection cannot establish any of these. Flat Remote Play and a desktop shown
on a panel are not substitutes for this test.
Next test, once a Linux gaming PC is available: record distro, GPU/driver,
Steam client channel/version and SteamVR version; use the Frame's built-in
Steam connection flow, first wirelessly and then over a data-capable USB cable.
Launch a free OpenXR sample or developer-consented VR game. Verify stereo,
head/controller tracking, haptics and audio while worn; retain both ends' logs
and measure latency and recovery after a link interruption. Restore any test
channel changes. Do not change the shared headset's channel just for this report.
If that works, Frame Control can provide our own host checks, setup guidance
and session controls around Valve's existing platform. First establish which
controls have a usable interface; no stable automated pairing API has been
verified. **Estimate (inferred):** 2–5 engineer-days for the hardware feasibility
pass; another 1–2 weeks for a small integration if those interfaces exist.
## (b) Our own streaming — proposal only
This is a new VR transport and device integration, not a desktop capture feature.
A plausible first target is **one Linux GPU family, one host, one Frame**, using
SteamVR on both ends. Our host driver would expose a remote HMD/controllers,
receive poses and inputs, and obtain stereo textures for hardware encoding.
Our Frame OpenXR app would decode, submit the correct eye views and render poses
at predicted display times, and return tracking/input. SteamVR/OpenXR, bundled
codec/transport libraries and platform GPU APIs fit the ownership rule; a
WiVRn/ALVR/Monado server dependency would not.
**Inferred design risks:** Linux SteamVR texture-sharing/driver interfaces and
Frame decode-to-GPU interoperability need a spike before committing to this
architecture. Sending an already-composited desktop mirror loses the stereo,
pose and timing information we need. Late reprojection, clock conversion,
backpressure, controller bindings, audio sync and reconnects are substantial
work. A runtime shim must not assume `XrTime` equals monotonic nanoseconds.
### Reuse from `mac-in-headset`
**Documented from our code**, read-only at
[`1b90c64`](https://github.com/saphid/frame-control/commit/1b90c64b73bace54c63a3aae154c5d29a9448d72):
- Reuse the ideas for low-latency encoding without B-frames, dropping work
before encoding, bounded queues, keyframe recovery, adaptive bitrate,
per-frame timing and authenticated session setup.
- Its VideoToolbox encoder and ScreenCaptureKit capture are macOS-specific.
Linux needs a new GPU encoder path (for example VA-API or NVENC via bundled
libraries) and VR texture capture, not a port of window capture.
- Its WebSocket over SSH is useful for a first controlled transport experiment
and control messages. Reliable TCP can stall behind lost packets; a VR media
path needs measured deadline behaviour, likely datagrams with loss recovery
using an ordinary bundled transport library. Do not invent cryptography.
- Its Chromium/WebCodecs panel viewer is not a VR client. That branch reports
software H.264 decoding and occasional long Wi-Fi stalls on the Frame.
Its desktop latency measurements are not motion-to-photon measurements or
evidence that a 90/120 Hz stereo stream will work.
**Size/effort estimate (inferred, one experienced full-time engineer, hardware
available):**
| Phase | Deliverable / stop condition | Effort |
|---|---|---|
| Feasibility | Linux driver texture access, Frame hardware decode into OpenXR, pose/clock loop; stop if any cannot meet frame deadlines | 2–4 weeks |
| First end-to-end prototype | One GPU/codec, stereo sample over a controlled LAN, head/controllers, logs and teardown | 4–8 additional weeks |
| Usable limited beta | Audio/haptics, pairing, recovery, bitrate/loss handling, installer, worn testing and latency work | 6–12 additional weeks |
| Wider support | Multiple GPU vendors/distros, USB and Wi-Fi variation, long-session stability | 2–4 additional months |
Planning range: **12–24 engineer-weeks for a limited beta**, roughly
**10–25k lines of our code plus tests/tooling**, excluding bundled libraries.
This is a low-confidence scope estimate, not a delivery promise; an unsupported
driver or decode interface could block it entirely. Foveated streaming,
eye tracking and parity with Valve are excluded. A Linux gaming PC and repeatable
worn-headset testing are prerequisites. **Do not build this until Alex chooses.**
## (c) Optional “install WiVRn” shortcut only
Allowed as a clearly optional convenience, never a prerequisite for a Frame
Control feature. **Documented:** WiVRn's server Flatpak ID is
`io.github.wivrn.wivrn`; its client/server versions must match, and its Flatpak
includes xrizer/OpenComposite. Those are properties of an independently
installed third-party stack, not components of our implementation.
**Recommendation:** defer the shortcut while the current client fails before
session creation. If offered later, label that compatibility result and let
the user choose the install; do not present “install” as “streaming works.”
**Estimate (inferred):** 1–2 engineer-days for an optional host-side shortcut
with package/version detection and honest status, excluding third-party fixes.
No shortcut, host install, pairing automation or streaming UI was built here.
Choose **(a)** for the next hardware test. Keep **(b)** as a separately approved
project if Valve's path fails or lacks a required capability. **(c)** does not
solve the verified client blocker and should not be the product's foundation.
+485
View File
@@ -0,0 +1,485 @@
# Mac in the headset
Frame Control can show any Mac window, or a whole Mac screen, as its own panel
in the Steam Frame. You place each panel anywhere in the room with the SteamVR
dashboard. The laser clicks and drags, the thumbstick scrolls, and you type on
the Mac's own keyboard. Find it under **Tools → Mac in the headset** (macOS
only).
The confidence labels are the same as in [ssh.md](ssh.md).
## Why this design
First-party options come first, as the repo's rule asks, with the reason
each one was or wasn't chosen. The full list for every device is in
[streaming.md](streaming.md#first-party-options-and-why-they-do-or-dont-fit).
Checked 2026-09-28.
| Goal | First-party option | Chosen? | Why |
|---|---|---|---|
| One Mac screen in the headset | **Apple Screen Sharing** (VNC) → Remmina (Remmina 1.4.43 is already installed on this Frame) | Kept as the fallback (`panel-on-frame.sh mac-screen`) | It's the closest to first-party and needs nothing new. But VNC sends compressed tiles rather than video, so moving content is slow: noticeable lag even on a good 5 GHz link (**verified** 2026-09-27, see [streaming.md](streaming.md)), and the Mac's pointer isn't in the picture without a helper. It shows only whole screens |
| One Mac screen | **Steam Remote Play**, Mac as host (Valve) | For Mac games only; see [Steam's own streaming](#steams-own-streaming) | The Mac's and the Frame's Steam clients already find each other (**verified**). But Remote Play streams a game (the whole desktop only while the game is out of focus, untested from a Mac), never single windows, and a Mac can't host the Frame's VR streaming |
| One Mac screen | **AirPlay** (Apple) | No | Apple licenses AirPlay receivers only to TV and speaker makers, and nothing official runs on Linux. UxPlay is an unofficial receiver, and it mirrors a whole screen, not single windows |
| One Mac screen | **Sidecar / Mac Virtual Display** (Apple) | No | These work only with an iPad or Apple Vision Pro |
| **Each Mac window as its own panel** | None | – | No first-party way does this: Apple's per-app streaming is only for Vision Pro, and Valve's desktop streaming needs a Windows SteamVR host. So Frame Control does it itself |
| Mac keyboard and trackpad driving the headset | **Bluetooth HID** | No | macOS can't act as a Bluetooth keyboard or mouse. A real Bluetooth keyboard paired with the Frame still works |
| Mac keyboard and trackpad | **KDE Connect** (KDE) | No | The Frame has no `kdeconnectd` and it isn't on Flathub. Its Mac app has no keyboard or mouse sharing (**inferred**), and on Wayland it can only reach the desktop panel |
| Mac keyboard and trackpad | **xrdp** (Valve, Developer Mode) | No | It runs a separate Linux session that you view on the Mac. It isn't the headset's view, and it doesn't carry input the other way |
What that leaves is our own stream: nothing to install on the Mac or the
Frame, and whole screens or single windows. Here the Mac's own keyboard and
trackpad need no forwarding, because the windows are still on the Mac. The
laser is the only input that has to be sent back.
Other routes that were compared:
| Option | One screen | Each window | Speed | Verdict |
|---|---|---|---|---|
| Sunshine → Moonlight | ✓ | – | Good | Sunshine's macOS support is still experimental ([discussion #777](https://github.com/orgs/LizardByte/discussions/777)), and it captures whole screens only |
| Virtual Desktop, Immersed | – | – | – | No Frame client as of September 2026 |
| **Frame Control's own stream** | ✓ | ✓ | Hardware H.264, sending only changed frames | **Built** |
To type into VR surfaces other than these panels (SteamVR's dashboard,
games), the Frame supports a uinput keyboard and mouse without sudo
(verified 2026-09-27: `steamos` is in `input`, and `/dev/uinput` is
`root:input 660`). That's a separate feature, not part of this one.
## Steam's own streaming
The Frame is built around Steam streaming, so this was checked first
(2026-09-28). It fits Mac games, not Mac windows.
- **VR streaming from the Mac: no.** The Frame streams VR from a PC running
SteamVR ("Steam Link" with foveated streaming). SteamVR dropped macOS in
2020, and Valve lists PCs, laptops, Steam Deck and Steam Machine as
hosts, never a Mac (**documented**:
[UploadVR](https://www.uploadvr.com/steamvr-drops-mac-support/),
[Road to VR](https://roadtovr.com/steam-frame-game-certification-specs/)).
- **Flat Remote Play from the Mac: probably, for Steam games.**
- Steam on this Mac has streaming on, and the two Steam clients already
see each other. The Frame's `remote_connections.txt` shows it
connecting directly to "Alexs-MacBook-Pro-7" at 192.168.1.211:27036,
and the Mac's shows the Frame connecting over Wi-Fi and over the USB-C
link (**verified** in both clients' logs).
- Whether a stream then starts, and how a flat game looks in the headset
(reviews describe a theater screen), is **not tested yet**. The Frame's
Steam was crash-looping during this session (below).
- Mac-hosted Remote Play has a long-standing report of the stream
closing as the game loads
([Steam forum](https://steamcommunity.com/groups/homestream/discussions/1/574921459914429988/),
**reported**).
- **The Mac desktop through Steam: untested; single windows: no.** Valve
says Remote Play shows the host's desktop when the game loses focus
([Steam Remote Play FAQ](https://help.steampowered.com/en/faqs/view/0689-74B8-92AC-10F2),
**documented**), so a whole Mac screen may be reachable by starting a
game, then switching away from it. Nobody has tried that from a Mac
host. It would still be one screen in one panel: Remote Play has
nothing like one panel per Mac window, so Frame Control's own stream
stays the way to see separate windows.
- **Steam has a "stream desktop" call, and it pairs with a Mac
(verified 2026-09-28).** In the Remote Play device list, the Frame's
Steam UI calls `SteamClient.RemotePlay.StartDesktopStream(<client id>)`
for a connected device. Called over CDP with the Mac's client ID, it made
the Mac's Steam show "Authorize Device" and ask for a 4-digit code shown
on the Frame. Once the code was entered, the Mac logged
`k_ERemoteDeviceAuthorizationSuccess`. No stream started in that attempt,
and a second attempt, now that the device is authorized, is the next
test. If it streams the Mac's desktop, that's a whole-screen option built
into Steam: one panel, Valve's encoder and transport. It still wouldn't
give each window its own panel.
- **What Steam's work did give us: the USB-C link.** Plugged into the Mac,
the Frame appears as a network port called "Steam Frame". Steam's Remote
Play discovery uses it, and so does Frame Control's stream now (see
"USB-C, when it's plugged in" below).
Next steps, once Steam on the Frame is healthy:
1. Call `StartDesktopStream` again now that the Frame is authorized, and
compare its latency and sharpness with Frame Control's stream of the same
screen.
2. Stream the one Mac game installed here (Fortune Mill) from the Frame's
library, over Wi-Fi and over USB-C.
3. Record whether it starts, how it's shown, and its latency. Steam's
streaming overlay shows this; our benchmark can't measure it.
4. While streaming, switch away from the game on the Mac, and see whether
the Mac's desktop appears in the headset, and whether its keyboard and
pointer work.
5. If it works, Frame Control's Games page could offer "Stream from the
Mac" for Mac-installed games.
## How it works
```
Mac Frame
ScreenCaptureKit (one window or display)
→ VideoToolbox H.264 (hardware, low-latency,
no B-frames)
→ frame-mac-view, 127.0.0.1 ──ssh -R──→ 127.0.0.1:479xx
→ Chromium app window per stream
(WebCodecs decode), on gamescope's
X display, tagged STEAM_GAME
→ its own SteamVR panel
← CGEvent (clicks, drags, wheel, keys) ←──── pointer, wheel and key events
```
- **The agent** is `mac/bin/frame-mac-view`, built from `mac/frame-mac-view`
(Swift, no dependencies; `build.sh`). Frame Control's server starts it on
first use and stops it on quit.
- It captures with ScreenCaptureKit, which sends frames only when something
changes, so idle windows cost nothing.
- It encodes in hardware with VideoToolbox's low-latency rate control (plain
real-time mode where that's unavailable).
- While anyone is watching, it keeps the Mac's display awake. A sleeping
display isn't drawn, so there would be nothing to capture.
- **The link** is an `ssh -R` tunnel on its own connection. It's encrypted and
works anywhere `ssh frame` works, Tailscale included, with no firewall
changes on the Mac. If the headset sleeps or the network drops, Frame
Control reopens the tunnel on the same port, and open viewers reconnect by
themselves.
- **USB-C, when it's plugged in.** Connected to the Mac by cable, the Frame
is also a USB network device: macOS lists a network port called "Steam
Frame", and the Frame's `usb0` answers in under 1 ms. Frame Control
checks for it each time it opens the tunnel and uses it when it's there,
with the Frame's usual SSH host key. Otherwise it uses the normal path.
`FRAME_MACVIEW_USB=0` turns this off. **Verified** 2026-09-28, in two
interleaved pairs of runs:
| | USB-C | Wi-Fi (Tailscale) |
|---|---|---|
| test: content p50 / p95 | 6.9–7.3 / 8.4–8.8 ms | 9.8–10.1 / 12.1–12.3 ms |
| test: click to drawn p50 | 16.6–16.9 ms | 26.4–27.8 ms |
| scroll: content p95 | 23.0–23.5 ms | 31.4–36.9 ms |
| scroll: late frames | 3.2–3.3% | 4.7–7.0% |
(`bench/results/2026-09-28-*-usb1.json`, `-usb2`, `-wifi1`, `-wifi2`.)
- **Access.**
- Frame Control's own key never leaves the Mac.
- Each viewer is opened with a **single-use ticket**. It's tied to one
window or display and expires after a minute. It's spent as soon as the
viewer confirms it has received its reconnect key. Until then, a retry
gets the same key, so a connection lost at that moment doesn't strand
the viewer. Stop revokes tickets that haven't been used yet.
- After that, the viewer holds a reconnect key for that one source, in
memory only. **Stop** revokes it.
- Remaining risk: a program running as `steamos` on the Frame could read a
ticket from Chromium's command line in the first second or so and use it
first. That gets it the one source being opened, not the Mac, and the
real viewer would then fail to connect. Android apps in Lepton run in
their own podman container, so they shouldn't see the Frame's process
list (inferred, not checked).
- **The viewer** is `ui/mac-view.html`, served by the agent. It opens on the
Frame as a Chromium app window, preferring Chromium XR (`~/chromium-xr`,
built with H.264) over Flathub Chromium.
- The page puts `[fcNNNNN]` in its title. The launcher finds the window by
that tag and sets `STEAM_GAME` to a stable id per source, which gives it
its own panel (see [panels.md](panels.md)). The same Mac window gets the
same panel id each time.
- It decodes with WebCodecs. If it falls behind, it skips to the next
keyframe instead of showing old frames late.
- It falls back to JPEG stills (**Compatible** quality) where H.264 isn't
available.
- **Flow control.** The agent never lets frames queue up anywhere on the
way. It skips capture frames *before* encoding, so no reference frame goes
missing, and it lowers the bitrate, then the frame rate, then the size, to
fit the link (see [Adapting to the network](#adapting-to-the-network)).
- **Input.**
- A click on a window's panel brings that Mac window to the front
(Accessibility API), then clicks at the same point. Double clicks, right
clicks, drags and the wheel work too.
- Keys typed into the panel are sent as Mac key codes. Any keys or buttons
still held down are released if the viewer loses focus or disconnects, or
when the stream stops. Characters the key
table doesn't know, such as those from other keyboard layouts, are typed
as text.
- The Mac's own keyboard and trackpad keep working as normal. Click a panel
with the laser, then type on the Mac.
## Permissions (Mac)
- **Screen Recording**, to see windows. Without it, the card asks for it.
- **Accessibility**, so input from the headset reaches the Mac. Without it the
stream still works, and the viewer says clicks won't go through.
Both are granted to Frame Control. After granting, press **Refresh**, which
restarts the helper so it picks them up. The app is ad-hoc signed, so macOS
may ask again after an update.
## Quality settings
| Setting | Long side | fps | Codec | Use |
|---|---|---|---|---|
| Sharp | 2560 | 60 | H.264, ~0.14 bits/pixel | Text-heavy windows on a strong link |
| Balanced (default) | 1920 | 60 | H.264, ~0.1 bits/pixel | Most things |
| Light | 1280 | 30 | H.264 | Weak Wi-Fi or Tailscale off the LAN |
| Compatible | 1280 | 20 | JPEG | A Frame browser without H.264 |
## Measuring
Every frame carries a sequence number, and the agent records its journey on
the Mac's clock (`Sources/Stats.swift`):
| Stage | From → to |
|---|---|
| capture | the Mac composited it (ScreenCaptureKit's display time) → the agent got it |
| queue, encode | → encoding started → the encoder finished |
| network | → the viewer received it |
| decode, draw | → WebCodecs decoded it → it was drawn on the page's canvas |
| present | → the page's next animation frame |
- **Clock sync.** The viewer syncs its clock to the Mac's the way NTP does:
it pings over the stream's own WebSocket and keeps the sample with the
shortest round trip. It then reports, in Mac time, when each frame arrived
(right away, so the agent can pace itself) and when it was decoded and
drawn (in batches every 250 ms).
- **Input.** The first frame captured after a click or key carries that
event's id. So input latency is the viewer's event → injected on the Mac →
the first frame after it → drawn in the headset.
- **Where to see it.**
- `GET /stats` (key required) returns every frame and input record.
- `/status` includes a two-second summary, which Frame Control's card
shows next to each live stream.
- In the headset, add `?stats=1` to the viewer or press
Ctrl+Alt+Shift+S for an overlay.
- **The benchmark.** `scripts/macview-bench.py` runs fixed scenarios on the
real Frame, from the Mac, with nobody wearing the headset:
- **test** is the moving test pattern.
- **scroll** is a Chrome page on its own display scrolling at 240 pt/s,
which gives about 9 Mbit/s of real 1920×1290 video.
- **type** types into a Chrome text box, first fast and then with pauses.
It writes `bench/results/<date>-<commit>-<label>.json`, compares two
results, and runs interleaved A/B tests between agent settings (`ab`).
Wi-Fi changes from minute to minute, so single runs at different times
aren't comparable. Throttled links come from a shaping relay on the Mac,
which needs no sudo (`--net 50@0,3@8,50@16` means 50 Mbit/s, then 3 from
8 s, then 50 from 16 s).
- **What's graded.** "Content" runs from when the Mac composited a frame
(or when ScreenCaptureKit delivered it, if that was earlier) to when it was
drawn in the viewer. The Frame's compositor adds its own delay after that.
That part is reported, but not graded: an unworn Frame throttles panels to
about 36 fps after a few seconds, and to 15 fps in standby, whatever they
draw. This was **verified** with a local canvas page that ran on the Frame
with no network involved (`bench/pages/present.html`). The compositor's
share needs a run with the headset worn.
Targets: content p50 ≤ 25 ms (p95 ≤ 40), click to photon p50 ≤ 50 ms
(p95 ≤ 70), 60 fps with ≤ 1% late frames, no stall over 100 ms, and adapting
to a new link rate within 1 s.
Baseline on 2026-09-28 (**verified**, home Wi-Fi, Tailscale, Balanced,
`bench/results/2026-09-28-a3c6e5c-dirty-baseline-fixed.json`; ms p50/p95):
| Scenario | Content | Input to drawn | fps drawn | Notes |
|---|---|---|---|---|
| test (1280×720) | 9.9 | 28.8 / 39.0 | 59 | encode 4.0, network 4.0, decode 1.3 |
| scroll (1920×1290) | 14.7 | – | 50.4 | encode 6.7, decode 6.4 (software), 9.4 Mbit/s, 16% late |
| type (1920×1290) | 16.7 | 51.2 / 73.2 | – | most of the input time is the Mac app reacting |
After this work, with the controller on (**verified**, same setup,
`bench/results/2026-09-28-9e4dcdd-final.json`; ms p50/p95). Content is
now measured from the earlier of display time and delivery, which adds
about 5 ms to scroll compared with the baseline's way of measuring:
| Scenario | Content | Input to drawn | fps drawn | Grades |
|---|---|---|---|---|
| test | 10.5 / 16.7 | 29.6 / 36.3 | 60 | all within target |
| scroll | 19.8 / 27.4 | – | 55.9 | fps, late frames (4.8%) and worst gap (222 ms) only "acceptable": Wi-Fi stalls (the Mac captured 57 fps in this run; earlier runs got 46–53 from virtual displays) |
| type | 15.0 / 21.4 | 43.0 / 60.5 | – | all within target |
Of the targets, click to photon is met without the Frame's compositor (the
headset has to be worn to measure its share), and so is content latency.
The frame-rate and no-stall targets aren't yet met while scrolling.
**Worn** (**verified** 2026-09-28 22:00, `vrcmd --stats` activity level 1,
home Wi-Fi over Tailscale, `bench/results/2026-09-28-39afb23-worn.json`;
ms p50/p95):
| Scenario | Content | Input to drawn | fps drawn | What happened |
|---|---|---|---|---|
| test | 10.8 / 15.3 | 25.7 / 34.4 | 58 | as unworn |
| scroll | 22.2 / 141 | – | 37.7 | Wi-Fi queued 56–364 ms and held frames for 220–270 ms; the controller went to 2.6 Mbit/s and 45 fps |
| type | 13.5 / 24.3 | 56.8 / 106 | – | slower replies than unworn (43 / 60) |
The Frame's CPU wasn't the limit: about 27% in total, and the viewer took
45% of one core. The link to a headset on someone's head is much rougher
than to one lying still. The adaptation keeps latency bounded there, but
the frame rate drops. The USB-C cable avoids Wi-Fi entirely. Frames
actually shown were still about 39 fps for the test pattern while worn, so
the unworn throttling isn't the whole story. The cause is **unknown**.
What was learned (all **verified**, unless marked):
- The biggest costs are encoding (4–7 ms), network (4–6 ms), and decoding
on the Frame. Chromium XR on the Frame decodes H.264 in **software**.
- Wi-Fi alone stalls for 240–580 ms now and then, over Tailscale and over
the LAN alike. Over the LAN (`--host 192.168.1.237`) latency was no
better, but the Frame used about 8% less CPU, because tailscaled runs in
userspace there.
- With no controller, a link that slows down queues without limit. In one
run, frames arrived 1.9 s late, and at worst 9.4 s late.
- Tried, and no help, so not kept: VideoToolbox options (require hardware,
no frame delay, prioritise speed, a hard data-rate cap), Chromium flags
(`--disable-gpu-vsync`, `--disable-frame-rate-limit`,
`--use-angle=vulkan`), and a 120 Hz virtual display.
- Inconclusive, so off by default: keeping the Frame's Wi-Fi awake during
typing (`FRAME_MAC_VIEW_WARM=40`, a tiny message every 40 ms for 5 s
after input). Over three interleaved runs each, input p50 went 44 → 47 ms
and p95 92 → 71 ms, and the ranges overlapped widely
(`…-ab-keepwarm.json`). In that run, and in the baseline's typing, the
harness typed spaces as "+" (a URL-encoding bug, since fixed), so they
went through the text path rather than as space keys.
- Pointer moves now go out on an 8 ms timer, not on the page's next
animation frame, which an unworn Frame slows to 15–36 Hz. This is
**inferred** to help dragging; the benchmark has no drag scenario yet.
- Kept: the encoder's timestamps never jump more than two frame intervals.
Before this, the first frame after a pause got a quarter of a second's
bit budget, and one P-frame reached 204 KB.
## Adapting to the network
`Sources/Controller.swift`, per stream, latency first:
- **The gate.** The viewer acknowledges every frame as it arrives. A new
frame is sent only while the oldest unacknowledged one is younger than
the path's usual round trip, plus one frame interval, plus room for this
link's normal jitter (1.5 times its recent spread, 25–80 ms). So frames
never queue in SSH, TCP or the Wi-Fi driver. While the link is stuck, the
newest picture waits and goes out as soon as it moves.
- **The bitrate.** The link counts as congested when, for two checks in a
row (100 ms apart), round trips grow by more than 40 ms while the stream
uses much of its budget, or the gate holds frames back, or a frame is
stuck for 100 ms. Then the bitrate drops to a bit under what actually got
through: at least a fifth off, and at most half. Once the link has been
clear for a second, it rises by 10% steps, never above the quality
setting's bitrate.
- **The tier.** When the bitrate stays low, and the stream is really
limited by the link rather than having little to send, it steps down:
60 → 45 → 30 fps, then 75%, then 50% of the pixels. It goes straight to
the tier the bitrate supports after half a second, and steps back up one
tier at a time after two seconds with room to spare.
- `FRAME_MAC_VIEW_ADAPT=0` turns it off, for comparison.
Measured on the real Frame, 2026-09-28 (**verified**; interleaved A/B, off
versus on, medians of the runs, ms):
| Link | Scenario | Content p95, off → on | fps drawn, off → on | Result file |
|---|---|---|---|---|
| Clean Wi-Fi (3 runs each) | scroll | 32.3 → 33.6 | 57 → 56.2 | `…-ab-adapt-clean2.json` |
| Clean Wi-Fi | test | 15 → 14.2 | 60 → 59.7 | same |
| 50 → 3 → 50 Mbit/s at 8 s and 16 s (2 runs each) | scroll | **4670 → 72** | 45 → 42 | `…-ab-adapt-step3.json` |
| 50 → 3 → 50 Mbit/s | test | 66 → 16 | 60 → 60 | same |
- **Clean link.** On a clean link it costs nothing measurable. An earlier
version with a fixed gate slack lost 9 fps to Wi-Fi jitter while
scrolling (47 → 38 fps), and that's why the slack now follows the link's
jitter.
- **Throttled link.** Without the controller, frames queued for up to 5.6 s
and never caught up while the link was slow (p95 1.8–5.6 s, second by
second). With it, in the two runs:
| | Run 1 | Run 2 |
|---|---|---|
| Worst second's p95 just after the drop | 219 ms | 428 ms |
| p95 back under 100 ms for 3 s in a row | after 1 s | after 4 s |
| Stepped down to 1440 px at 30 fps | 1.8 s after the drop | 3.5 s after |
| p95 per second after that, on the 3 Mbit/s link | 55–94 ms | 60–148 ms |
Sending one frame takes about 27 ms on that link by itself. After the
link recovered, the stream was back at full size and 60 fps in about
7.5 s. It steps up one tier every two seconds, on purpose, so it doesn't
bounce. The 1 s adaptation target was met in one run of two.
- **A hiccup on a small stream.** In one clean run, a Wi-Fi hiccup made an
earlier version halve the test pattern's bitrate five times and drop it
to half size. That cut couldn't help: the stream only sends
0.47 Mbit/s. Now the controller estimates what a stream wants (captures
per second × average frame size). While a stream wants about half its
budget or less, no cut takes it below twice what it wants, so it doesn't change
tier.
## Checked so far (2026-09-28)
- **Mac (checked by hand, macOS 26.5.2).**
- The agent builds, and lists windows and displays.
- In a browser, the test pattern decoded at about 60 fps (H.264) and 30 fps
(JPEG, about 22 Mbps).
- A click in the viewer arrived at the same point in the source.
- `pmset -g assertions` showed the display-awake assertion only while a
stream was being watched.
- **Automated** (`tests/test_macview.py`, on CI's macOS runner): the agent
builds (a build failure fails the job).
- `/ping` and the page are open; everything else needs the key.
- Tickets work once and only for their own source, and Stop revokes
reconnect keys.
- A WebSocket frame claiming 2^63 bytes closes that socket, and the agent
keeps running.
- A stream sends an SPS-led H.264 keyframe, and sends another when asked.
- Stop ends the stream on the Mac even if the viewer ignores it.
- The test doesn't decode video, time it, or check where clicks land.
- **Linux aarch64 (verified in a stand-in, not on the Frame).** In an Arch
Linux ARM container with sshd, Xvfb as `:0` and Chromium 153:
- The real `show` path worked in 1–2.3 s: tunnel, ticket, launcher,
window found and tagged `STEAM_GAME`.
- Frame Control's key didn't appear anywhere in the stand-in's process
list.
- After the tunnel was killed, it came back on the same port within 4 s,
and the viewer reconnected by itself.
- Chromium decoded the stream in software with the GPU off.
- Stop closed the window, and Chromium exited.
- This test found and fixed a bug: in a C locale, `xwininfo` can't print a
title with non-ASCII characters, so the launcher reads `_NET_WM_NAME`
with `xprop`.
- **On the Frame (verified 2026-09-28, SteamOS build 20260925.6191901,
from the Mac, nobody wearing the headset).** Frame Control's **Show** with
the test pattern:
- The panel was ready in 1.45 s. SteamVR logged `[Overlays] Created:
valve.steam.desktopgame.2001639889` (in `vrwebhelper_systemui.txt`).
- The window was tagged `STEAM_GAME`, and gamescope sized it to 1920×1080
although 1280×720 was asked for.
- Chromium XR decoded it live at about 60 fps: the frame counter advanced
62 in 1.04 s.
- Over home Wi-Fi, the frames in the viewer window had been drawn on the
Mac about 11–17 ms earlier, plus `xwd`'s own time. That's measured
against the two clocks, which were 37–39 ms apart (±4 ms, measured over
one SSH session). SteamVR's compositor and the display come on top.
- Clicks and keys injected with XTest into gamescope's Xwayland didn't
reach the page. That's inconclusive, not a failure: XTest on gamescope
isn't how real input arrives. The laser should arrive as a left mouse
button and the thumbstick as a wheel, because the window has an app id
(from gamescope's source; see [panels.md](panels.md)).
- **Not yet checked:**
- Clicking, dragging and scrolling with the laser while wearing the
headset.
- Real window capture and input on a Mac with both permissions granted.
- Keys from SteamVR's on-screen keyboard.
- Whether Flathub Chromium has H.264. Chromium XR is used when it's
installed, as it is on this Frame.
- Latency with real, busy windows at Sharp.
- A benchmark run while wearing the headset. Only then does the Frame show
panels at full rate, so only then can the compositor's share of the
latency, and the frame rate you actually see, be measured.
## Limits
- Only windows on the Mac's current desktop (Space) are listed, and
minimised windows can't be captured.
- A window's panel shows only that window. Its menus and sheets are separate
windows on the Mac, so open them from the Mac or use **Whole screen**.
- Keys go to whichever Mac window is in front. Clicking a panel brings its
window to the front first.
- Ctrl stays Ctrl. On the Mac, copy is ⌘C, so use Meta+C on a keyboard paired
with the Frame.
## Switching panels and workspace limits
**Tools → Panel switcher** lists open SteamVR panels, including Mac viewers.
Use **Show** to request focus or **Open in headset** for Frame Control's own
switcher panel. It uses SteamVR/gamescope and Chromium, with no third-party
overlay app. [Device checks and limits](panels.md#frame-controls-panel-switcher)
include the difference between a panel surviving a scene launch and staying
visible over it.
Saved spatial layouts are blocked on this build: the public OpenVR transform
setter denies access to gamescope-owned panels. Reconnecting an existing viewer
is supported; restoring its room position after a reboot is not. We do not
save short-lived Mac window IDs or viewer access keys as if they were a durable
workspace. See [the feasibility evidence](panels.md#saved-spatial-layouts-blocked-on-the-current-panel-route).
+200
View File
@@ -0,0 +1,200 @@
# Real separation of Mac windows in the headset
Research and experiments on 2026-09-28 (macOS 26.5.2, Apple M5 Pro), for
making each streamed Mac window truly independent. Builds on
[mac-in-headset.md](mac-in-headset.md). Labels: **verified** (tried here),
**documented**, **source** (read in someone's code) or **reported**.
## What "separate" lacks today
Today each window is captured on its own
(`SCContentFilter(desktopIndependentWindow:)`), but it still lives on the
Mac's one desktop:
- **Clicks.** A click has to raise the window first, so it reorders the
Mac's windows, steals focus and moves the real cursor.
- **Child windows.** A window's menus, popovers, sheets and tooltips are
separate windows, so they aren't in its panel. Apple documents that the
single-window filter leaves them out.
- **Size.** A panel's size is tied to the window's size on the Mac screen.
- **Hidden windows.** Minimised windows, and windows on other Spaces, can't
be shown live.
## How the Mac draws, and where pixels can be read
Apps draw with Core Animation into IOSurfaces. WindowServer's compositor
stacks every window onto each display, including virtual ones, and sends
the result to the screen. Pixels can be read at three points:
| Where | How | Gets | Doesn't get |
|---|---|---|---|
| One window's content | ScreenCaptureKit `desktopIndependentWindow` (public, what we use) | Live, zero-copy, even when covered by other windows (**documented**) | Menus, popovers, sheets. It pauses while the window is minimised (**reported**) |
| One window plus its children | `SCStreamConfiguration.includeChildWindows` (macOS 14.2+, **reported**) | Menus, popovers and sheets attached to the window | Anything the app puts outside the window's bounds |
| A whole display | ScreenCaptureKit display filter, with apps or windows included or excluded | Everything on that display, cursor included | Only what's on that display |
| A snapshot of any window | Private `CGSHWCaptureWindowList`, which AltTab uses for minimised windows and other Spaces (**source**: `alt-tab-macos` `PrivateApis.swift`) | Minimised windows and other Spaces | It's one still image, not a live stream |
| Another app's layer tree | Private `CALayerHost` with a context id | A live, zero-copy picture | It only works when the other app cooperates, so it's no good for arbitrary windows (**reported**) |
`CGWindowListCreateImage` and `CGDisplayStream` are obsolete in the macOS 15
SDK (**reported** by MacPorts and JUCE). Nothing reads another app's pixels
without the Screen Recording permission.
## The idea: each window gets its own virtual display
A virtual display (the private `CGVirtualDisplay`, as used by BetterDisplay,
DeskPad and quest-display) is a compositor target with no physical screen.
Put one streamed window alone on its own virtual display, sized to its
panel, and capture the whole display:
- **Nothing can overlap it.** A plain click at that point always lands on
that window, so input doesn't need raising or background-event tricks.
- **Menus, sheets, popovers, tooltips and context menus appear on the same
display, so they're in the panel.** With "Displays have separate Spaces"
on (it is on this Mac), each display also has its own menu bar, so the
app's menu bar can be part of the panel.
- **The panel's size is the display's size,** in HiDPI. Resizing the panel
means changing the display mode and resizing the window to fill it
(Accessibility API).
- **Minimised windows and other Spaces stop being a problem,** because
streamed windows live on their own displays.
- **The cursor goes where the laser points.** That display's panel is the
one being used, much as Vision Pro's Mac Virtual Display works. Keys from
the Mac's keyboard go to the window last clicked.
### Verified here (no permission needed)
- **Creating one.** It needs no permission or entitlement. 1920×1080 HiDPI
gives a 3840×2160-pixel, 60 Hz display, placed next to the built-in
screen, and `NSScreen.screensHaveSeparateSpaces` was true.
- **Many at once.** 16 were created at once with no error.
- **Placement.** `CGConfigureDisplayOrigin` far away failed with error
1014: macOS keeps displays edge to edge. Disabling one with the private
`CGSConfigureDisplayEnabled` from a command-line tool also failed with 1014.
- **Removal is deferred while the physical screen sleeps.** With the
built-in display asleep, virtual displays were not removed when released,
or when their process exited, even from their own `.app`. A second display
with the same vendor, product and serial couldn't be created. All of them
disappeared as soon as the screen woke (`caffeinate -u`). The helper must
therefore:
- keep a fixed pool of displays and reuse them rather than create new ones;
- keep the screen awake while they exist, which it already does while
streaming.
### Built: "Give each window its own display"
Frame Control's helper now does this (`mac/frame-mac-view/Sources/Separate.swift`),
and it's the default in the Tools card. Streams of this kind are named
`separate:<window id>`.
- For each window shown, it creates a HiDPI virtual display. The display is
sized to the window plus a menu bar, with a fresh serial each time, so a
display left over while the screen slept can't block a new one.
- The window is moved onto the display and resized to fill it (Accessibility
API), and the whole display is captured.
- On **Stop** the window goes back where it was, and the display is released.
- Plain per-window capture is still there: untick "Give each window its own
display". Separate mode needs Accessibility (to move the window), and
without it, Show says so rather than quietly falling back.
Verified 2026-09-28 (macOS 26.5.2, M5 Pro), using a TextEdit test document
and the benchmark's Chrome windows:
- **Separation works.** The window moved onto its own display and streamed,
with its first frames in 3.8 s. The display showed TextEdit's own menu bar.
- **Child windows come along.** A context menu and the Page Setup sheet
opened on the same display and were in the capture.
- **Input.** Typing through the stream reached the window. The benchmark's
typing scenario does this on every run.
- **Stop.** Stop put the window back at exactly its old size (656×422), and
the display was removed.
- **The helper must run a real Cocoa event loop.** Earlier, it ran only a
`RunLoop`. With that, AppKit never learned about new displays:
- `NSScreen.screens` never listed them, so placing the window always waited
3 s and then gave up;
- the display's HiDPI mode never applied, so windows were captured at 1×
(1280×860 instead of 1920×1290 pixels) and text was soft.
Running `NSApplication` (with no Dock icon) fixed both. The screen now
appears within 100 ms, and captures are at 2× (**verified** in
`bench/results/*-baseline.json` against `*-baseline-fixed.json`).
- **Frame rate.** Chrome drew at 60–61 fps on the virtual display, but
ScreenCaptureKit delivered only about 46–53 fps from it, whereas the
built-in ProMotion screen gave 121 fps (**verified**). Neither a 120 Hz
virtual display (`FRAME_MAC_VIEW_VD_HZ=120`) nor a looser
`minimumFrameInterval` changed that. Cause unknown.
- **Windows that keep their own size** (Calculator, 230×408). The window
moved and streamed, but stayed small in the display's corner, so the panel
was mostly wallpaper. Now only that corner is captured
(`SCStreamConfiguration.sourceRect`): the menu bar and the window, at least
480×360 points so menus fit. Clicks map to the cropped area. The first
frame arrived in 0.6–1.1 s, and on Stop the window went back and the
display was removed. A display's menu bar names the active app, so it
shows the window's own menus only once the panel has been clicked.
- **Several windows at once** (on the Mac, with a local stand-in viewer that
acknowledges frames). Three and four Chrome windows, each scrolling on its
own display at 1920×1290 pixels, streamed at 53–56 fps and about
9 Mbit/s each. The helper used 20% (three) or 24% (four) of one CPU core.
The hardware encoder is shared: its time per frame went from 6.7 ms for
one stream to 7–15 ms for three and 11–25 ms for four, so each extra
moving window adds latency to the others. Once in four runs, before a fix,
one window left its display when several displays appeared at the same
moment, and its stream stopped: nothing on that display was changing any
more. The helper now checks every second and puts the window back. Three
further runs with four windows were clean, and every display was gone
afterwards (checked with `CGGetOnlineDisplayList`).
- **Quitting.** On SIGTERM, or when Frame Control goes away, the helper ends
every stream first, so windows go back before their displays disappear.
Verified: a 900×600 window was back at 900×600 after SIGTERM. Closing a
viewer 0.05–1.5 s into startup left the window at its old size and no
extra display.
- **A consent prompt.** macOS 26 asks whether to let ScreenCaptureKit apps
"bypass the system private window picker". The prompt appeared *on the
virtual display*, so it would show up inside the headset panel. Allow it
once on the Mac.
### Still to check
1. That a context menu opens over the text being clicked, not just somewhere
on the display. This needs a retest after the consent prompt above is
allowed.
2. Stage Manager, which is reported to undo Accessibility resizes.
3. Where the Dock and the cursor go on a virtual display.
4. Closing the lid with virtual displays attached (clamshell).
5. Many moving windows at once in the headset: whether to lower the bitrate
or frame rate of panels you aren't using, so the one you are using keeps
the encoder to itself.
6. All of it in the headset, with the laser.
## Input without disturbing the Mac (a finer option)
For windows that stay on the Mac's own screen, input can go to a window
behind others without raising it:
- **Background click.** `CGEventPostToPid` with the CGEvent fields 91 and
92, which say which window a click is for (`kCGMouseEventWindowUnderMousePointer`
and `...ThatCanHandleThisEvent`), plus a window-relative location
(**reported**, reverse-engineered, needs testing).
- **Focus without raise.** yabai makes a window key without raising it by
posting event records with the private `SLPSPostEventRecordTo` (**source**:
`yabai/src/window_manager.c`).
- **Known failures.**
- Chromium and Electron ignore background clicks unless they're built
with the 2026 `acceptsFirstMouse` fix (electron/electron#54493).
- Games and canvas apps often need real activation.
- Password fields (Secure Event Input) drop synthetic keys.
- IME composition, as for Chinese or Japanese input, isn't reliable.
With a virtual display per window, most of this isn't needed. It's worth
having for hovering over one panel while typing in another.
## Encoding and transport
- **Codec.** Keep H.264 4:2:0. Apple's High Performance Screen Sharing uses
4:4:4 over two virtual displays (**documented**), but the Frame's Chromium
decodes H.264 in software, and no 4:4:4 decode path is confirmed there.
- **Sharper static text.** When a panel has been still for a moment, send a
high-quality refresh, either a keyframe at low quantisation or a lossless
WebP overlay, so static text is sharp. Moving content stays as video.
- **Transport.** Keep WebSocket over SSH for now, as it works everywhere.
WebRTC (UDP, congestion control) is the proven step up for Wi-Fi.
WebTransport over QUIC is promising, but its server side on macOS is
unverified.
+101
View File
@@ -0,0 +1,101 @@
# Flat-to-VR mods and Beat Saber songs
**Status: feasibility work, not an installer.** Frame Control does not yet
manage mods or Beat Saber songs. [Issue #26](https://github.com/saphid/frame-control/issues/26)
stays open: neither UEVR injection nor Beat Saber custom-song playback has
been verified on this Frame. Alex has an owned copy on a Quest 2; that copy
has not been inspected. There is no Mods button until the underlying
install, playback and removal have been checked.
The mod manager must be Frame Control's own implementation. Mods and songs
are permitted third-party content; BSManager, ModsBeforeFriday, MO2 and other
managers must not be dependencies. Steam, Proton and SteamVR remain platform
dependencies. No game purchases, entitlement bypasses, withdrawn builds or
unofficial mod mirrors are part of this work.
## Per-game support
Checked 2026-09-28 on SteamOS **0.4.1**, BUILD_ID **20260925.6191901**, aarch64,
with **Proton 11.0-2c ARM64** and SteamVR **2.18.1**. “Verified” describes only
the observation stated, not a promise that the game is playable. “Documented”
means an upstream source describes it; “inferred” means it still needs a test.
| Game / build | Mod or content | Evidence and support status | Next check |
|---|---|---|---|
| Half-Life 2: VR Mod – Episode One, Steam 2177750, build 25413453 | Official Steam community mod, shared base depot 658920 build 25413418 | **Verified: startup only.** Already installed; launched through Proton ARM64. The stereo headset capture showed its first-time setup, and SteamVR loaded `bindings_frame.json`. Gameplay, controller interaction, fresh installation and removal are unverified. | Complete first-time setup and play a level before offering a tested install shortcut. |
| Gravitas, Steam 1067310, Windows | UEVR 1.05 | **Verified: prerequisites and windows only.** Free Steam install completed. The game produced a `SkyArk (64-bit, PCD3D_SM5)` window. UEVR needed .NET; with official .NET 6.0.36 libraries it produced a `UEVR` window. A combined run exited 1 with X11 errors before injection was verified. **Inferred: compatibility remains unknown**, not proven broken. | Retry during a stable headset session; verify injection, stereo scene output, controls and removal. |
| Beat Saber, Steam 620980, Windows / Proton | Basic custom songs; later SongCore and version-matched mods | **Verified: absent from the 868-game library returned by this Frame.** Store metadata lists Windows, not Linux. **Documented:** the PC game reads basic maps from `Beat Saber_Data/CustomLevels` without a mod manager. Playback on Frame is unverified. | An already-owned, legitimately installed copy is required. Do not buy it as part of this task. |
| Beat Saber, claimed native ARM64 build | Custom songs / native mods | **Inferred: unverified.** The research mentions this build but supplies no verified official distributable or tested layout. CPU architecture alone does not identify Android versus Linux, the game version or the mod ABI. | Establish official provenance, ownership, binary type and version before touching files. Do not apply Quest patches to an unidentified build. |
| Beat Saber, Alex's Quest 2 copy | Custom songs / Android mods | **Documented: owner-reported copy on Quest 2**, currently charging. No APK, version or installed mods inspected; no Frame playback verified. This does not establish ownership of the Steam build. | When the Quest is available, inspect the owned copy's version and supported transfer path, then test Lepton/OpenXR compatibility without bypassing entitlement checks. |
| Hogwarts Legacy, Steam 990080 | R.E.A.L. | **Verified: listed in this Frame's owned library, not installed.** Official release access and redistribution permission were not established; the referenced author Patreon page returned HTTP 403. No archive downloaded or game tested. | Obtain a current free release from the author and confirm its terms before any test. A news report saying “free” is not a redistribution grant. |
| Horizon Zero Dawn, Steam 1151640; Horizon Forbidden West, Steam 2420110 | R.E.A.L. | **Verified: both listed as owned, neither installed.** Same source/permission blocker as above; runtime support is unverified. | Check each game's supported version against an accessible official release. |
| Half-Life 2 VR / other OpenVR games | OpenComposite, per-game replacement | **Documented:** forwards OpenVR calls to OpenXR. **Inferred: Frame compatibility unknown.** Not installed or tested. HL2 VR reached setup with the shipped OpenVR path already. | Test a specific game and replacement DLL only if needed; preserve its original DLL. Never switch the shared headset's runtime globally. |
| Doom / Quake / Half-Life Team Beef ports | Author's VR ports plus separately owned or free game data | **Inferred: untested.** Android ARM64 support does not establish OpenXR extension or controller compatibility on Lepton. | Choose an official release and legally usable data set, then test that exact port. |
| Skyrim VR, Steam 611670 | SKSEVR / HIGGS / PLANCK stack | **Verified: Skyrim VR is absent from this library.** Owning flat Skyrim or Special Edition is not the VR game's entitlement. Runtime and mod support are unverified. | An already-owned VR copy and version-matched official mod releases are required. |
The [test record](evidence/mods-2026-09-28.md) distinguishes process startup,
visible output and failures. It also records a SteamVR restart during the
shared session, which prevents attributing the failed UEVR attempt to FEX.
## Beat Saber: songs first
**Documented:** the [BSMG PC guide](https://bsmg.wiki/pc-modding.html)
describes extracting each map into its own directory below
`Beat Saber/Beat Saber_Data/CustomLevels`. Basic custom songs do not require
SongCore; maps that require mod features need their matching dependencies.
This is a candidate for our own file manager, not a verified Frame feature.
Alex's Quest 2 copy is a separate Android candidate. Its ownership does not
make the PC `CustomLevels` layout applicable. Until the actual build is
inspected, neither a direct song-copy recipe nor APK patching is justified.
Only maps whose music and chart are permitted for distribution may be used
as test fixtures or bundled content. A public download alone does not establish
those rights. Start with an original or explicitly licensed basic map.
**Documented:** [ModsBeforeFriday](https://github.com/Lauriethefish/ModsBeforeFriday)
targets Quest Beat Saber over WebUSB/ADB. It is not a generic native ARM64
modding protocol. [BSManager](https://github.com/Zagrios/bs-manager/releases/tag/v1.6.0)
publishes an aarch64 Flatpak, but its architecture says nothing about Beat
Saber or its plugins running on Frame. Neither app is an installation step
or dependency for Frame Control.
## Requirements for our manager
These are **planned**, not implemented or verified:
1. Resolve the selected Steam game's real library, installed build, executable
architecture and Proton prefix. Confirm ownership through Steam; a directory
or app manifest alone is not proof. Keep downloading, installed and playable
as separate states.
2. Download a pinned mod version from the author's official release. Record
the URL, version, license and digest. Verify the published digest when
available; an upstream SHA-256 detects corruption but is not a signature.
Do not treat “free to download” as permission to redistribute.
3. Stage and validate archives before writing into the game or prefix. Reject
path traversal, links escaping the destination, archive bombs and unexpected
executable content in song packs. Check song metadata and its referenced
files, not just the `.zip` suffix.
4. Refuse changes while the game is running. Back up originals and journal
every managed file and digest. Apply changes atomically where possible and
roll back partial failures. Keep runtime prerequisites scoped to this game.
5. Uninstall only files still matching our receipt; restore originals without
overwriting later user edits. Preserve saves, unrelated mods and songs.
Song removal must target one managed map, never the whole CustomLevels tree.
6. Expose one-click actions beside the game only after real-Frame install,
playback and uninstall pass. Test filesystem and download failure handling
with fake-Frame fixtures; those cannot prove FEX injection or VR rendering.
## Sources
- [UEVR 1.05 official release](https://github.com/praydog/UEVR/releases/tag/1.05)
and [author's usage instructions](https://github.com/praydog/UEVR#getting-started).
- [Microsoft .NET 6 release metadata](https://builds.dotnet.microsoft.com/dotnet/release-metadata/6.0/releases.json),
including the SHA-512 hashes used for the test runtimes.
- [OpenComposite's OpenXR branch](https://gitlab.com/znixian/OpenOVR/-/tree/openxr),
including per-game installation and the need to preserve original DLLs.
- [R.E.A.L. author post referenced by the research](https://www.patreon.com/realvr/posts/but-wheres-link-165840151)
(HTTP 403 from this environment; contents not verified).
- [Half-Life 2 VR official site](https://halflife2vr.com/) and
[Episode One on Steam](https://store.steampowered.com/app/2177750/).
- [Beat Saber store metadata](https://store.steampowered.com/api/appdetails?appids=620980).
+18 -4
View File
@@ -102,10 +102,11 @@ Still open: 4, 6, 7, 12–15, 16 (off-LAN and after a reboot), 17–21.
20. **F-Droid 2.0** (Compose 1.12): does it run? If so, the catalogue can
install it instead of 1.17.2.
21. **DeoVR local files:** does DeoVR's file browser show `Videos → VR`
(the symlink from `push-vr-video.sh`) or `Z:\home\steamos\Videos\VR`, and do
the colour-coded test clips play in 3D (red left eye, cyan right) for both
H.264 and H.265? Does the DLNA browser find a server on the Mac?
21. **Owned media player:** worn-headset comfort, audio quality/lip sync, long
movies and 4K/8K decoding remain to check. Native spatial-photo container
extraction, large/immersive splats and existing-panel theatre docking need
further implementation. Remote eye isolation, short hardware decode and
owned-overlay cleanup are verified; see [vr-video.md](vr-video.md).
## Verified 2026-09-27
@@ -129,6 +130,19 @@ Still open: 4, 6, 7, 12–15, 16 (off-LAN and after a reboot), 17–21.
- The Mac EDL flashing script in `~/Downloads/steam-frame-recovery/` hasn't
been run against a Frame.
## Mac in the headset (2026-09-28)
The test pattern streams to the Frame as its own panel at about 60 fps
(verified, build 20260925.6191901; see
[mac-in-headset.md](mac-in-headset.md#checked-so-far-2026-09-28)). Still to
check in the headset:
- Laser clicks, drags and thumbstick scrolling in a viewer panel.
- Real window capture and input once Screen Recording and Accessibility are
granted to Frame Control.
- Keys from SteamVR's on-screen keyboard.
- Whether Flathub Chromium decodes H.264 (otherwise use Compatible).
## Unconfirmed claims made in these docs
- `/home` and `/etc` persist across Frame OS updates. This is inferred from
+168 -2
View File
@@ -121,5 +121,171 @@ a limit on the number of floating panels.
script needed.
- **Inside the desktop panel**: KWin tiling (Meta+arrow keys with a Bluetooth
keyboard) or virtual desktops arrange windows within the 1280×800 rectangle.
- **Windows-only overlay tools** (Desktop+, OVR Toolkit, OVRdrop) do this for a
PC's desktop in SteamVR. They don't run on the Frame's standalone Linux.
- **Optional overlay tools:** Desktop+, OVR Toolkit and similar software are
separate from Frame Control. Public reports describe some Proton support;
Windows-only does not by itself prove a Frame app cannot run. Local status
and sources are in [VR utilities](vr-utilities.md).
- **Our performance HUD:** Home → VR comfort and performance → Open HUD in
headset creates its own gamescope panel using built-in tools. It needs no
third-party overlay app. [Metrics and verification](vr-utilities.md).
## Frame Control's panel switcher
**Verified 2026-09-28**, SteamOS 0.4.1, BUILD_ID `20260925.6191901`,
SteamVR 2.18.1: **Tools → Panel switcher** lists SteamVR's open main panels,
including panels that are currently hidden. **Show** asks SteamVR to bring one
forward. **Open in headset** opens the same switcher as its own panel; choose
it again from Steam's dashboard after switching away. Refresh updates the list.
This is a list, not thumbnail Exposé.
![Frame Control's switcher rendered on the Frame](img/panel-switcher.png)
This is our own Python/HTML implementation (`ui/frame_panels.py`), using the
Frame's shipped `vrcmd` OpenVR client and gamescope. The headset page uses
Chromium (Chromium XR when present, then system Chromium, then the existing
Chromium Flatpak). No XSOverlay, OVR Toolkit, WayVR or other overlay application
is needed. This dependency boundary also applies to future layout and panel
persistence work: platform APIs and bundled libraries are fine; another app
must not implement the feature for us.
The companion runs the helper over SSH. Opening it in the headset installs a
copy under `~/.local/share/frame-control/panels/` and starts a loopback HTTP
server and an isolated Chromium profile. There is no startup service or global
setting change. Close the switcher to stop its server and browser. Other
Chromium profiles, Steam and SteamVR are left alone. If the window or runtime
closes, use **Open in headset** again.
The page carries a random, per-process access key in its URL fragment, removes
it from the address bar, keeps it in tab session storage for page reloads, and
sends it in a header. Panel lists and actions need
that key; Host and Origin checks reject other sites. The key permits only
listing panels, requesting focus and closing this switcher. Like Mac viewer
launch tickets, it is initially readable by another process running as the
same Frame user. Panel titles are rendered as text, never HTML. The companion
retains its existing request guards. No Mac capture credentials cross this API.
**Verified:** the real headset page rendered its panel list (image above), its
HTTP focus request changed `GAMESCOPE_FOCUSED_APP` to `2000999030`, a request
without the key returned HTTP 403, and Close stopped the helper and its browser.
Opening an already running switcher requests its focus rather than creating a
second one. The companion uses the same list/focus helper. **Unverified:** laser
selection while wearing the headset, physical placement, and non-XR Chromium.
The API reports that focus was *requested*: another action can take focus before
we observe the result. Closed panels are rejected after re-enumeration.
### Shared-device recheck, 2026-09-29
**Verified:** the follow-up's atomic `mkdir /tmp/frame-test.lock` attempts
failed because another thread held the lock. The existing lock was left alone;
no applications were installed, launched or stopped in this follow-up. The last
read-only battery check showed 62%, charging. The 180 Python and 8 website tests
passed again locally.
**Unverified in this follow-up:** the prepared browser-button test (Refresh,
selection, reload and Close) and repeated OpenXR transition could not run under
the shared lock. The device results elsewhere in this page are the earlier
2026-09-28 observations, not results from this blocked recheck. In particular,
HTTP focus is not evidence of worn-headset laser input. Follow the
[shared-device test procedure](testing.md#headset-smoke-test) for the next run.
## Saved spatial layouts: blocked on the current panel route
**Verified 2026-09-28**, same build, using a temporary xterm panel with
`STEAM_GAME=2000999031` and `FnTable:IVROverlay_028` from
`/opt/steamvr/bin/linuxarm64/libopenvr_api.so`:
| OpenVR call | Result |
|---|---|
| `FindOverlay("valve.steam.desktopgame.2000999031")` | Success |
| `GetOverlayWidthInMeters` | Success, 2.67 m |
| `SetOverlayWidthInMeters` (same width) | Success |
| `GetOverlayTransformType` | Success, type 5 (`VROverlayTransform_DashboardTab`) |
| `GetOverlayTransformAbsolute` | 18 (`WrongTransformType`) |
| `SetOverlayTransformAbsolute` (identity rotation, 1.2 m up, 1.5 m forward) | 12 (`PermissionDenied`); type remained 5 |
The public interface names type 5 **DashboardTab**; SteamVR's dashboard code
places these panels through its scene graph. It owns the frame/docking
transforms. A successful width setter does not grant permission to restore the
position. `vrcmd --dock-overlay world <key>` dispatched a docking request but
the dashboard logged `Failed to get SGTransform in setInitialTransformForLocation.
Invalid transform ID`. This does not establish working world placement.
**Inferred:** saving X11 pixel rectangles or Mac window IDs would not restore
this spatial arrangement. Mac window IDs also change when an application
reopens; viewer tickets and reconnect keys must not go into a layout file.
The base Mac stream reconnects after a network break, but that is different
from recreating windows and their room positions after a reboot.
There is consequently no Save/Restore control yet. A durable layout needs a
working transform restore path, stable source identity, and a fresh capture
permission/ticket flow. The tested gamescope-owned overlay route denies that
transform operation. A future Frame Control-owned overlay renderer, or a
supported platform API for dashboard frame transforms, needs its own device
proof before building layout UI. This is a blocker for the current approach,
not a claim that all possible implementations are impossible. Reboot recovery
was not tested: the shared headset was not rebooted.
## Panels during an immersive session
**Verified 2026-09-28**, same build: our Chromium switcher panel remained in
OpenVR's overlay list before, during and after the Frame's shipped `helloxr -g
Vulkan` sample. During the test `vrcmd --stats` identified
`system.generated.openxr.helloxr.helloxr`, with 242 frame submissions. The test
ended only its own sample process; no SteamVR, Steam, power or global settings
were changed. The switcher was still selectable afterwards.
This proves survival of that panel across an OpenXR scene session, **not** that
it stayed visibly composited over the scene: OpenVR reported it `not_visible`
before, during and after. **Verified:** calling `ShowOverlay` on our
*gamescope-owned* switcher overlay returns 12 (`PermissionDenied`). A helper
cannot force that panel visible using the public overlay call. Use the
switcher/dashboard to request access to it; we do not fight the runtime with a
repeated force-focus loop.
**Verified in a second controlled run:** a live H.264 test-pattern stream from
this checkout's Mac helper, through its own SSH tunnel and a temporary Chromium
profile, survived the same OpenXR sample (257 scene-frame submissions). Its
panel `2000999032` changed from `visible` before launch to `not_visible` during
and after the scene. The Mac helper still reported the same `test` stream;
captured frames increased from 35 to 232, with 29.5 decoded/drawn fps afterwards.
The test did not capture personal Mac windows or inject Mac input. The sample,
viewer, temporary profile, tunnel and Mac helper were cleaned up. This proves
stream survival, and also shows why it must not be advertised as always visible.
**Unverified:** persistent visible placement while playing a Steam-launched VR
game, Plasma desktop and real Mac-window behavior during that launch, and worn
headset input. Other threads were launching games and changing the runtime on
the shared device, so those transitions were not treated as controlled evidence.
A runtime/X-server restart can destroy the viewer windows; a network reconnect
cannot recreate them. No “always visible during games” guarantee is shipped.
## Keyboard passthrough feasibility
**Verified 2026-09-28**, same build, using `FnTable:IVRTrackedCamera_006`:
`HasCamera(0)` returned success and true. `GetCameraFrameSize` returned 100
(`OperationFailed`), with zero dimensions, for all three public frame types
(distorted, undistorted and maximum-undistorted), including after acquiring the
video service. Acquisition returned success and a handle; release returned 101
(`InvalidHandle`). The probe shut down its OpenVR client afterwards. No camera
frames were captured and no camera settings were changed.
**Documented:** the public OpenVR camera interface provides camera frame sizes,
intrinsics, projections and streaming handles; these are prerequisites for a
spatially aligned camera cutout. See Valve's
[OpenVR C API](https://github.com/ValveSoftware/openvr/blob/master/headers/openvr_capi.h).
**Inferred:** camera presence alone does not establish access to camera pixels.
The failed frame-size path blocks a keyboard cutout in our current panel
implementation. We have not established a keyboard detector or a calibrated
camera-to-panel mapping. Built-in full-room passthrough is not proof of a
public, selectively masked camera stream. No keyboard cutout is offered, and
no third-party camera/overlay app is substituted for it.
## Frame Control's media theatre
[The owned media player](vr-video.md) can show its video or stereo image on a
larger, head-relative screen with its own dark surround. **Verified remotely
2026-09-28**, SteamOS 0.4.1 / BUILD_ID 20260925.6191901: screen, eye isolation,
surround and cleanup. It does not alter panel docking or global settings.
Its `Overlay` RGBA rendering hook is available to stream producers; applying
SteamVR theatre docking to existing Mac/PC panels is still unverified here.
+143
View File
@@ -0,0 +1,143 @@
# Privacy and analytics
Frame Control sends anonymous analytics to [PostHog](https://posthog.com)
(US cloud) so the maintainer can see how many people use it, which features
matter and where installs fail. You choose how much in **Privacy & updates**,
the last panel on the page. `ui/frame_telemetry.py` is the whole
implementation.
## The three levels
| Level | Default | What it sends |
|---|---|---|
| Anonymous usage statistics | On, after a notice on first run | The events in the table below |
| Share compatibility results | Off | Your Android compatibility reports and tests |
| Send error details | Off | Scrubbed error messages and tracebacks |
Nothing is sent until the first-run notice has been shown. The notice's
**Share more to help fix problems** button turns on the second and third
levels together. Either can be turned off later. Turning a level off
deletes that level's events that haven't been sent yet.
**Show what's been sent** in the panel lists the last 50 events that left your
computer, exactly as they were sent.
## Anonymous
- Events carry a random id, made when Frame Control first runs and kept in
its data folder (`telemetry/settings.json`). It isn't derived from your
computer, account or network. To get a new one, delete that file.
- Events are sent without person profiles (`$process_person_profile: false`)
and without location lookup (`$geoip_disable: true`). Each carries a
placeholder address (`$ip: 0.0.0.0`), so PostHog stores that instead of
yours.
- Every event includes the app version, OS name (macOS, Windows or Linux),
CPU architecture and Python version.
## Usage events
| Event | When | Properties besides the common ones |
|---|---|---|
| `app_installed` | First run | |
| `app_updated` | First run of a new version | `from_version` |
| `app_opened` | At most once a day | |
| `frame_connected` | The first time a SteamOS build is seen | `steamos_build`, `steamos_version` |
| `tab_viewed` | The first click on each tab in a session | `tab` |
| `install_finished` | Any install finishes, working or not | `kind` (apk, flatpak, steam, title, web), `ok`, `seconds`, `error_category`, `installer_code`, and see below |
| `update_offered`, `update_started`, `update_failed` | The update banner | `to_version`, `error_category` |
`install_finished` never includes a file name, path or error message. An
error becomes one category from a fixed list (for example `apk_wrong_abi` or
`frame_unreachable`), plus Android's own `INSTALL_FAILED_…` code when there
is one. It names what was installed only when that's already public:
- F-Droid catalogue apps: `package`. Never the version, since a local build can reuse a
catalogue app's package name
- Flathub apps: `flatpak_id`
- Steam games: `steam_appid`
- A sideloaded title: only its runtime (Proton or Linux)
Any other APK is sent as `catalog: false`, with no name.
## Compatibility results (opt-in)
Each report becomes a `compat_report` event with the fields the Report dialog
shows: package, version, result or rating, your notes, how it was run, and the
SteamOS and Lepton builds. Before sending:
- the notes, app name and version are scrubbed like error messages (see
below)
- the APK's source is kept only if it's `F-Droid` or the public host name of
a download link (`https://example.com/…`). File names, user names,
passwords, ports, paths, IP addresses and local host names are dropped
When you turn this on, reports you made earlier on this computer are shared
too.
The maintainer's `python3 ui/frame_compat_db.py sync` copies these events
into the compatibility database, marked `via=community…`. It takes at most
30 per reporter per day.
## Error details (opt-in)
`$exception` events carry an error message, the Frame Control file, line and
function it came from, and the request that failed (for example
`POST /api/android install`). Before anything is sent, the message is
scrubbed:
- your home folder becomes `~`, and any user name becomes `<user>`
- IP and MAC addresses, email addresses, `.local`, `.lan` and Tailscale host
names, Steam ids, SSH and PEM keys, API tokens and long hex strings are
replaced
- URLs are cut down to their scheme and a public host name, or `<url>`. User
names, passwords, ports, paths and queries are dropped
- `token=`, `key=`, `password=` and similar values are replaced
The same error is sent at most once every 10 minutes.
## Report a problem
**Report a problem** is the warning-sign button in the header, also in the
Privacy panel and under **Help → Report a Problem…**. It sends the report
privately to Frame Control's PostHog project as a `problem_report` event, the
same way as the analytics above, so only the maintainer can read it and
nothing is published. It works whatever the analytics settings are, because
the person sends it deliberately. The report has the kind, title and text you
wrote, how to reach you if you gave it, a short reference shown after sending,
and the diagnostics below. It has its own random id, so it isn't linked to
your analytics events.
With **Include diagnostics** ticked (the default), the report adds:
- the app version and whether it's a built app
- the OS, its release and CPU, and the Python version
- the Frame's SteamOS build, if it has connected since the app started
- which analytics levels are on
**Also include recent activity and the server log** is off by default,
because those lines can name files and apps. When ticked, it adds the newest
Activity lines and server log lines, without the request lines.
Everything is scrubbed like error details and limited to what fits in the
report. Environment details are kept first, then the newest lines. **Show
exactly what's included** shows the snapshot that will be sent, and later
activity isn't added to it. If PostHog can't be reached, **Copy report** puts
the whole report on the clipboard.
The maintainer reads reports on the Frame Control dashboard in PostHog, or
with `python3 ui/frame_report.py inbox [days]`, which uses the same personal
API key as `frame_compat_db.py sync`.
## Turning it all off
Untick the boxes, or set `DO_NOT_TRACK=1` or `FRAME_CONTROL_TELEMETRY=0` in
the environment that starts Frame Control. A copy run from a source checkout
never sends anything unless `FRAME_CONTROL_TELEMETRY=1` is set.
## Update checks
The desktop app asks GitHub for the latest release shortly after starting,
then every 6 hours: the latest release's `update.json` on GitHub, or
`api.github.com/repos/saphid/frame-control/releases/latest` if that fails.
Those requests carry no id. To stop it, set
`FRAME_CONTROL_NO_UPDATE_CHECK=1`. See [releasing.md](releasing.md).
+59
View File
@@ -0,0 +1,59 @@
# Releasing and updates
Frame Control checks for updates itself. The desktop app offers a new version
only once it's GitHub's **latest release**, and drafts and pre-releases never
count. So a build reaches people only when you publish it, after testing it.
## Steps
1. Bump `version` in `app/package.json`, commit, and push a tag:
```sh
git tag v0.4.0 && git push origin v0.4.0
```
`.github/workflows/release.yml` builds macOS, Windows and Linux, and
attaches everything to a **draft** release for that tag. Nobody is
offered a draft.
2. Download the draft's installers and test them. An installed copy of the
previous version won't offer the draft, so install it directly.
3. Write the release notes on the draft. The update banner links to them.
4. Publish:
```sh
scripts/publish-release.sh v0.4.0
```
The script checks that all eight installers are attached, each with the
SHA-256 digest GitHub records. It attaches `update.json` (the version, the
notes and each installer's digest), then publishes the release and marks it
latest. From then on, running copies see the update. They check about 8
seconds after starting, then every 6 hours, and anyone can use **Check for
Updates…** (the app menu on macOS, the Help menu elsewhere).
To pull a bad release, mark the previous one as latest
(`gh release edit v0.3.9 --latest`) or turn the bad one back into a draft.
Copies that already updated stay on it. Nothing downgrades them.
## How a copy updates itself
`app/updater.js` reads `update.json` from
`github.com/saphid/frame-control/releases/latest/download/`. It falls back to the
REST API only when a release has no manifest, because the API allows just 60
unauthenticated requests an hour per IP address, shared by a whole household.
Then it downloads the installer for its platform and checks it
against the SHA-256 digest GitHub publishes for the asset. It refuses if the
digest is missing or doesn't match. Then:
| Installed from | Update |
|---|---|
| macOS `.dmg`, app in a writable folder such as Applications | The `.zip` is unpacked next to the app and its version checked. After the app quits, a small script swaps the new app in, putting the old one back if that fails, and reopens it. Updates don't get the download quarantine, so there's no `xattr` step. |
| Windows installer | The new `Setup` runs silently over the install (`/S --force-run`) and reopens the app. |
| Linux AppImage | The new AppImage replaces the old file and is started. |
| macOS app still on the disk image or translocated, Windows `.zip`, Linux `.deb` | The banner opens the release page instead. |
Version 0.3.1 and earlier have no updater, so people on them have to download
the new version once by hand.
+1 -1
View File
@@ -99,7 +99,7 @@ controls to place each panel. See [docs/panels.md](panels.md).
| `scripts/frame-ui.sh` | Mac | Start the Frame Control web UI (`ui/server.py`) and open it (**verified**) |
| `scripts/apk-catalog.sh` | Mac | Refresh the rated F-Droid catalogue that Frame Control's Android section shows (**verified**) |
| `scripts/compat-db-backup.sh` | Mac | Maintainer-only: back up the shared compatibility database locally and to Google Drive (**verified**) |
| `scripts/push-vr-video.sh` | Mac → Frame | Upload VR180/360 videos to `~/Videos/VR`, linked into DeoVR's Proton prefix; `--launch` starts DeoVR (**verified**: upload and link; in-headset playback of local files not yet checked). See [docs/vr-video.md](vr-video.md) |
| `scripts/push-vr-video.sh` | Computer → Frame | Upload movies, stereo PNG/JPEG or small `.splat` files to `~/Videos/FrameControl`; `--launch` starts our OpenVR player, `--theatre` adds a larger screen and dark surround. No separate viewer. **Verified remotely**: decode, stereo output and cleanup; worn-headset checks remain. See [docs/vr-video.md](vr-video.md) |
| `scripts/push.sh` | Mac → Frame | `rsync` files to `~/Downloads` (or a given path) on the Frame (**verified**) |
| `scripts/serve-bootstrap.sh` | Mac | Fallback: serve `bootstrap-on-frame.sh` with your public key embedded |
| `scripts/bootstrap-on-frame.sh` | Frame | Fallback: install the key and enable `sshd` |
+142
View File
@@ -0,0 +1,142 @@
# SideQuest and Frame Control
Researched 2026-09-28. SideQuest is both a Quest discovery website and a desktop
sideloading/device-management app. Its Quest labels are **not** evidence that a
game works on Lepton: inspect the APK for arm64/OpenXR, Android API requirements,
VrApi and Meta services (see [VR APKs](vr-apks.md)).
## Features worth borrowing
Desktop evidence is the public [SideQuest source at af2ac70](https://github.com/SideQuestVR/SideQuest/tree/af2ac7043db122bca3c8db18f2b58f1660e9befb),
especially [ADB operations](https://github.com/SideQuestVR/SideQuest/blob/af2ac7043db122bca3c8db18f2b58f1660e9befb/desktop-app/src/app/adb-client.service.ts),
[drag and drop](https://github.com/SideQuestVR/SideQuest/blob/af2ac7043db122bca3c8db18f2b58f1660e9befb/desktop-app/src/app/drag-and-drop.service.ts),
and the [legacy repository index](https://github.com/SideQuestVR/SideQuest/blob/af2ac7043db122bca3c8db18f2b58f1660e9befb/desktop-app/src/app/packages/package.service.ts).
Website evidence: [SideQuest](https://sidequestvr.com/) and its public Angular
bundle `main-4MMXZRXL.js`, inspected locally without browser automation.
No SideQuest implementation code was copied.
| SideQuest feature | Frame Control before this change | Borrow? / effort |
|---|---|---|
| Store descriptions, screenshots, banners, trailers, ratings | F-Droid names, icons, compatibility verdicts and reports; no equivalent rich VR store | Yes, from authorised sources; medium. Search and library workers own presentation/artwork. |
| OBB expansion-file install | APK-only install | **Implemented helper and CLI**, medium. Essential for games whose assets are separate from the APK. |
| App-data backup/restore | Persistent instances and optional keep-data uninstall, no portable save archive | **Implemented private-data helper and CLI**, medium. Back up before updates or experiments. |
| File manager (list, upload, download, remove) | General Send to Frame, no Android file browser | Useful later, medium; requires clear instance selection and scoped paths. |
| Installed-app management (launch, uninstall, backup) | List, launch, stop, remove, probe | Already mostly covered. Backup added here. |
| Update notices / account library | Compatible-version lookup; no source-aware installed update notices | Useful later, medium; needs original version code and source identity recorded on install. |
| Custom repositories | Built-in F-Droid catalogue and compatible-version indexes | Separate user-repos worker. Legacy SideQuest source has a fixed SideQuestRepos index; arbitrary current custom-repo support was not verified. |
| Drag-and-drop APK/OBB install | APK drag-and-drop already works | OBB backend added here; future UI can call it. UI drop wiring is not included. |
| Tags, price, headset filters, reviews | Text search and Lepton verdicts, not Quest headset metadata | Useful, medium; search worker owns filters. Keep source headset claims distinct from tested Frame compatibility. |
| Screenshot/video capture and streaming | Frame screenshots/VR capture already present | Reuse existing tools; do not port Quest capture commands. |
| Device settings and ADB utilities | Frame/Android display settings, SSH and own-instance tools | Borrow selectively; Quest CPU/GPU presets and wireless-ADB setup do not map directly to Lepton. |
Priority: expansion files, then save backup/restore. Rich discovery and update
notices follow once a permitted metadata source and source/version persistence
are available. This patch deliberately exposes CLI/backend operations, leaving
shared UI, install(), Steam artwork and launch behavior to sibling work.
## SideQuest as a source: page-only
[Terms](https://sidequestvr.com/terms), “Prohibited Activities”, (i) prohibits
copying/distributing/disclosing the Service including automated or non-automated
“scraping”; (xi) prohibits content access through means other than those provided
or authorised by the Service; (xii) prohibits bypassing access restrictions.
The terms describe downloading developer-posted games through the Service, but
do not establish permission for this third-party API integration.
[robots.txt](https://sidequestvr.com/robots.txt) requests a three-second crawl
delay and disallows `/search/`, `/user/*` and `/sideload/*`. Robots permission
would not override the terms. The API host's robots request returned HTTP 403;
a request for the first shared website JS chunk also returned 403. No bypass,
account token, cookies, browser session or private endpoint was used.
The homepage publishes `https://api.sidequestvr.com` and
`https://cdn.sidequestvr.com`. The website bundle calls `searchApps(...)` and
`getApp(id, null)`; their actual HTTP search/detail routes could not be established
from the retrieved bundle. Do not invent endpoints. The open-source desktop
[install flow](https://github.com/SideQuestVR/SideQuest/blob/af2ac7043db122bca3c8db18f2b58f1660e9befb/electron/app.ts)
POSTs `{token: ...}` to `/install-from-key`. It consumes
`data.apps[].urls[]`, with `provider` values including `APK`, `OBB`,
`Github Release` and `Mod`, and `link_url`. This is a website-issued install-key
flow, not evidence of an anonymous download API. It is not implemented here.
`ui/apk_sources/sidequest.py` implements the shared interface conservatively:
- `sources()` marks SideQuest `page_only` and explains why.
- `search()` raises a user-readable `SourceError` with the browse URL (zero
limit returns no rows). It does not invent app results or report a false
“no matching games”. The aggregate search UI should surface this source error.
- `details()` accepts a numeric listing id and returns its canonical page link,
`downloadable: False`, empty versions/tags/headsets and the `images` shape
`{icon: None, banner: None, screenshots: []}`. Name is explicitly a listing id;
unknown facts, including free/VR status, stay `None`.
- `download()` refuses with that page link. Paid/external listings cannot be
downloaded by this adapter either. No downloads means no verification claim.
The JSON fixture records policy evidence, **not a purported live app response**.
No listing metadata, artwork URLs, or OBB download URLs were scraped.
The requested real SideQuest → OpenXR APK → `frame_android.py info` test is
**blocked by the terms**, and was not performed. No alternate source is silently
substituted. A future integration needs SideQuest's permission or an expressly
supported third-party API, plus recorded search/detail/download fixtures,
free/direct-download classification, and size/hash verification. A calculated
local SHA-256 alone must not be called publisher verification.
## OBB files
```sh
python3 ui/frame_android.py install-obb org.example.game main.42.org.example.game.obb
python3 ui/frame_android.py install-obb org.example.game main.42.org.example.game.obb patch.42.org.example.game.obb
```
Install the APK first. The named instance must already be running; the helper
never launches an app or uses Lepton Development. It requires standard
`main|patch.<versionCode>.<package>.obb` filenames and nonempty files, validates
the entire batch before transfer, streams each file through SSH into that
instance, checks its SHA-256 **inside Android**, then renames it into
`/sdcard/Android/obb/<package>/`. `verified: True` here means transfer integrity
against the local input, not publisher authentication. Publication is atomic per
file, not for the whole batch; retry after a partial batch failure. Existing OBBs
with different version codes remain. The filename version must match the game;
the current install metadata does not expose its version code for comparison.
Restart the game yourself after the transfer if it cached missing expansion data.
Both read-only SSH attempts to the Frame timed out. Therefore the exact
host-side `/sdcard` mapping and persistence of expansion data were **not verified**.
`compatdata/<instance>/internal/<package>` is documented as `/data/data/<package>`;
it must not be mistaken for `/sdcard`. Using Android's path avoids guessing a
host layout, but device verification across restart/update is still required.
No OBB file was installed on the Frame during this work.
## Private app-data backups
```sh
python3 ui/frame_android.py stop org.example.game
python3 ui/frame_android.py backup-data org.example.game ./game-save.tar.gz
python3 ui/frame_android.py restore-data org.example.game ./game-save.tar.gz
```
Keep the instance stopped throughout either operation; do not launch it from
Steam concurrently. The remote guard fails if Podman cannot enumerate containers
or reports that instance running. The helpers use `podman unshare` to read/write
Android's mapped ownership without changing the live data's permissions.
The archive covers **only** `compatdata/<instance>/internal/<package>`, not the
APK, external `/sdcard/Android/data`, OBBs, keystore, or the full Android snapshot.
It contains a package/instance manifest and regular files/directories. Backups
are private (0600), validated before publication, and never overwrite an existing
backup. Keep them safe: app data can contain credentials and is not encrypted.
Restore checks the package and instance, rejects absolute/traversing/duplicate
paths, links and devices, caps files at 100,000 and content at 20 GiB, and validates
again on the Frame. It extracts into a separate directory, preserves numeric
ownership, ordinary modes and timestamps, then swaps the private-data directory.
Setuid/setgid bits are not restored. The previous directory remains beside it as
`.<package>.before-restore-<timestamp>`; the returned `previous` path identifies
it. This is an additional recovery copy, not an automatic deletion policy.
Locally verified: archive round trip including recovery copy, malformed archive
rejection, transfer command construction and failure handling. Not verified:
real Frame UID mappings/permissions, Android app-level recovery, live FUSE OBB
writes or persistence. Backups reject symlinks/special files; an app requiring
those needs a separately designed backup format. These CLI features still need
a real-device acceptance pass before being exposed as a polished UI workflow.
+12
View File
@@ -4,6 +4,10 @@ Frame Control's **Get games** section lists the games you own with each one's
Steam Frame rating, installs them on the Frame, and searches the Steam store.
This page covers how it works underneath, so you can do the same from a shell.
For flat-to-VR mods and Beat Saber custom songs, see the
[per-game feasibility table](mods.md). Mod support is separate from Steam's
Frame rating; there is no mod installer yet.
## How it works
The Frame's Steam client runs with `-cef-enable-debugging`. So its UI, a
@@ -106,3 +110,11 @@ returns nothing without `cc`, so Frame Control takes the country from
- Installing when there's more than one library folder, such as a microSD card.
- Uninstalling. `steam://uninstall/<appid>` should open a confirmation in the
headset.
## Optional VR software
The [VR utilities list](vr-utilities.md#optional-software) is separate from our
controls and HUD. It checks software ownership as well as games; paid utilities
are installable only when already in the loaded Frame account library. No
purchase flow is added. Public reports, local results and ownership are shown
separately, and untested tools remain untested.
+169 -15
View File
@@ -5,7 +5,10 @@ This covers three directions, plus input:
- **A. Frame → Mac**: see and control the headset from the Mac.
- **B. Mac → Frame**: use the Mac's desktop inside the headset.
- **C. iPhone → Frame**: mirror the phone inside the headset.
- **PC VR from Linux**: [feasibility and options](linux-vr-streaming.md),
including Valve's streaming and USB support. No Linux host tested yet.
- **Input**: type and point in the Frame from the Mac or iPhone.
- **Live view and Control**: watch a panel flat and tap on it to use it.
The confidence labels are the same as in [ssh.md](ssh.md).
@@ -24,24 +27,29 @@ Mac with keyboard, mouse, and clipboard.
## B. Show the Mac's desktop inside the Frame
The Frame's streaming features are built around a **Windows PC running
SteamVR** plus the USB Wi-Fi 6E dongle. Even Linux hosts had VR-streaming
problems at launch
The Frame's VR streaming uses **SteamVR** on the host. Linux hosts had
VR-streaming problems at launch
([Steam discussion](https://steamcommunity.com/app/4165890/discussions/0/528765047224280796/),
[gbl08ma](https://gbl08ma.com/posts/steam-frame-a-linux-machine-doesnt-support-linux/)).
Valve's later 2.17.8 notes explicitly describe Steam Link fixes on Linux and
initial USB streaming support (**documented**, not tested from a Linux host
here). See [the current comparison](linux-vr-streaming.md#a-valves-own-path--recommended-first).
**macOS isn't a supported SteamVR host**, so for the Mac we're only looking at
flat 2D desktop streaming into a window on the Frame's Linux desktop.
| Option | Setup | Confidence | Verdict |
|---|---|---|---|
| **macOS Screen Sharing (VNC) → Remmina on the Frame** | **Mac:** System Settings → General → Sharing → Screen Sharing on → (i) → enable "VNC viewers may control screen with password". **Frame:** `./scripts/install-apps.sh remmina` from the Mac, then open Remmina in the headset and connect to `vnc://<mac>.local` | **Verified 2026-09-27** (Frame BUILD_ID 20260925.6191901, macOS 27.0), in its own panel via `panel-on-frame.sh mac-screen`. Remmina is on Flathub for **aarch64** with VNC and RDP ([Flathub](https://flathub.org/apps/org.remmina.Remmina)). The Frame desktop runs Flatpaks ([UploadVR](https://www.uploadvr.com/flatpaks-open-source-steam-frame/)). macOS VNC is built in. | **Recommended.** Nothing to install on the Mac, and it's easy to set up. Noticeable lag, even at lower Remmina quality settings on a good 5 GHz link, where neither Wi-Fi nor the Frame's CPU was the bottleneck. Usable for reading and coding, but not for games. You'll type the Mac's hostname once in Remmina on the headset, then save the profile. To avoid even that, the script can pre-seed a Remmina profile over SSH (see below). |
| **Frame Control → Tools → Mac in the headset** | Nothing to install. Allow Screen Recording and Accessibility for Frame Control, then press **Show** next to any window or screen | **Verified 2026-09-28** on the Frame (panel in 1.5 s, measured with `scripts/macview-bench.py`: about 10–20 ms from the Mac drawing a frame to the viewer drawing it); laser input not yet tried while wearing it | **Recommended.** Hardware H.264 over an SSH tunnel, adapting to the link. Each window becomes its own panel you can place anywhere. Laser clicks and scrolls, and the Mac's keyboard types. See [mac-in-headset.md](mac-in-headset.md) |
| **macOS Screen Sharing (VNC) → Remmina on the Frame** | **Mac:** System Settings → General → Sharing → Screen Sharing on → (i) → enable "VNC viewers may control screen with password". **Frame:** `./scripts/install-apps.sh remmina` from the Mac, then open Remmina in the headset and connect to `vnc://<mac>.local` | **Verified 2026-09-27** (Frame BUILD_ID 20260925.6191901, macOS 27.0), in its own panel via `panel-on-frame.sh mac-screen`. Remmina is on Flathub for **aarch64** with VNC and RDP ([Flathub](https://flathub.org/apps/org.remmina.Remmina)). The Frame desktop runs Flatpaks ([UploadVR](https://www.uploadvr.com/flatpaks-open-source-steam-frame/)). macOS VNC is built in. | **Fallback** (whole screens only). Nothing to install on the Mac, and it's easy to set up. Noticeable lag, even at lower Remmina quality settings on a good 5 GHz link, where neither Wi-Fi nor the Frame's CPU was the bottleneck. Usable for reading and coding, but not for games. You'll type the Mac's hostname once in Remmina on the headset, then save the profile. To avoid even that, the script can pre-seed a Remmina profile over SSH (see below). |
| Sunshine (Mac) → Moonlight (Frame Flatpak) | `brew install` Sunshine on the Mac, then `./scripts/install-apps.sh moonlight` | Moonlight Flatpak supports **aarch64** ([Flathub](https://flathub.org/apps/com.moonlight_stream.Moonlight)). **Sunshine on macOS is poorly supported**: install problems on Apple Silicon/Sequoia, and no virtual gamepads ([LizardByte discussion #777](https://github.com/orgs/LizardByte/discussions/777)). | Try it if VNC is too laggy. Expect some friction. |
| Steam Remote Play with the Mac as host | Steam on the Mac, Steam Link/Remote Play on the Frame | macOS-hosted Remote Play is reported broken or flaky in 2024–2026 ([Steam discussion](https://steamcommunity.com/groups/homestream/discussions/1/574921459914429988/)) | Not recommended. It's only for games, if it works at all. |
| Immersed / Virtual Desktop | Vendor apps | Immersed has a Mac agent but no known Frame client. Virtual Desktop's developer said he'd "try" to port it ([NewsBreak](https://www.newsbreak.com/news/4892834783961-virtual-desktop-dev-says-he-ll-try-to-bring-the-app-to-steam-frame)). | Not available as of 2026-09-25. Check again later. |
| WiVRn / ALVR | VR streaming from a Linux or Windows PC | Irrelevant for a Mac host (no SteamVR/OpenXR runtime on macOS) | N/A |
For **VR video files** (180°/360° stereo), don't stream the Mac's screen. Play
them on the Frame in DeoVR instead: see [vr-video.md](vr-video.md).
For **local movies and stereo photos**, Frame Control's own OpenVR player
runs on the Frame; see [vr-video.md](vr-video.md). It currently renders a flat
stereo screen. VR180/360 projection is not implemented; the same page records
DeoVR only as an optional, independently installed alternative.
### Pre-seeding the Remmina profile (no typing in the headset)
@@ -80,6 +88,21 @@ its header. (Verified 2026-09-27.)
Going the other way, pointing a controller at the panel moves the Mac's mouse,
because Remmina forwards input (`viewonly=0`).
## First-party options, and why they do or don't fit
Checked 2026-09-28. The first-party way is usually the best one, so these are
listed first; the sections above and below explain the alternatives.
| Goal | First-party option | Fits? | Why, and what would make it easier |
|---|---|---|---|
| Type and point from the **iPhone** | **KDE Connect** (KDE; official [iOS app](https://apps.apple.com/app/kde-connect/id1580245991)) remote touchpad and keyboard, plus clipboard and files | **Best candidate, untested on the Frame** | The Frame doesn't have it (verified: no `kdeconnectd`), it isn't on Flathub, and the root is read-only, so it would have to run from `~` or a container. On Wayland it types through KWin, so it can only reach the desktop panel, not SteamVR or games (**inferred**). Steam Deck users report its remote input breaking after SteamOS updates ([SteamOS #1939](https://github.com/ValveSoftware/SteamOS/issues/1939)). If it works, Frame Control could install it and pair it for you. |
| Type and point from the **Mac** | KDE Connect for macOS (KDE builds) | Same as above | Its Mac app sends clipboard and files but has no keyboard/mouse sharing (**inferred**). |
| Either | **Bluetooth keyboard and mouse** paired in SteamOS (Valve) | Yes, with real hardware | Neither device can pretend to be one: iOS refuses the HID service ([Apple forums](https://developer.apple.com/forums/thread/733916)), and macOS has no built-in way. |
| Either | **xrdp** in Developer Mode (Valve) | No | Input goes into a *separate* desktop shown on the Mac, not into what you see in the headset. |
| **Mac screen** in the Frame | **Screen Sharing** (Apple's VNC server) + Remmina (already installed on this Frame, profile pre-seeded by `install-apps.sh`) | **Yes, closest to first-party** | Only the Mac side is first-party; Remmina is the client. Turn on System Settings → General → Sharing → Screen Sharing → (i) → "VNC viewers may control screen with password". Still to test in the headset (open question 11). |
| Mac screen | **Steam Remote Play** with the Mac as host (Valve) | Probably not | macOS isn't a SteamVR host, and Mac-hosted Remote Play is reported broken ([Steam forum](https://steamcommunity.com/groups/homestream/discussions/1/574921459914429988/)). One quick test is worth doing: Steam on the Mac, then Remote Play from the Frame's Steam. |
| Mac or **iPhone screen** | **AirPlay** (Apple) | Not officially | It's Apple's own mirroring for both, but Apple only licenses receivers to TV and speaker makers; nothing official runs on Linux. UxPlay (below) is the unofficial receiver. |
## C. Show the iPhone's screen inside the Frame
iOS only shares its screen two ways: **AirPlay** (Screen Mirroring in Control
@@ -93,18 +116,149 @@ capture it.
## Input: type and point in the Frame from the Mac or iPhone
**Verified 2026-09-27** on the headset: `steamos` is in the `input` group and
`/dev/uinput` is `crw-rw-r-- root input`, so **our own code can create a
virtual keyboard and mouse without sudo**. The Frame has no `python-evdev`,
`ydotool`, `wtype` or KDE Connect; `kwin_wayland` and `plasmashell` run only
while the desktop panel is open in the headset.
**Built: Home → Keyboard and trackpad**, in every version of Frame Control
(Mac, Windows, Linux, iPhone and iPad), with nothing to install on the device
you're holding. On a phone the panel is a trackpad (drag to move, tap to click,
two fingers to scroll, two-finger tap to right-click) plus a text field that
types on the Frame. On a computer, clicking the pad passes your mouse and
keyboard through to the Frame until you press Esc (⌘ is sent as Ctrl on a Mac).
| Option | Mac | iPhone | Notes |
It goes through **KDE Connect**, the first-party route (KDE makes the Frame's
desktop): Frame Control's server runs [`ui/frame_input_agent.py`](../ui/frame_input_agent.py)
on the Frame, which talks KDE Connect's own LAN protocol to the Frame's
`kdeconnectd` as if it were a phone. KDE Connect does the typing and clicking.
**Verified 2026-09-28** (SteamOS 0.4.1, build 20260925.6191901):
- KDE Connect isn't installed on the Frame, so **Frame Control ships it**:
Valve's own build for the Frame (`kdeconnect` 24.02.2-1 from its `extra`
repository) plus the five libraries it links that the Frame lacks
(`kcontacts`, `kpeople`, `modemmanager-qt`, `pulseaudio-qt`, `libfakekey`),
pinned by SHA-256 in [`frame/kdeconnect/packages.json`](../frame/kdeconnect/packages.json).
The builds download them from the
[kdeconnect-frame-24.02.2-1 release](https://github.com/saphid/frame-control/releases/tag/kdeconnect-frame-24.02.2-1)
(`app/build/fetch-deps.js`, `frame/kdeconnect/fetch.py`). The address of
Valve's repository for the Frame isn't to be shared, and Valve's public
aarch64 preview repository has KDE Connect 25.08, built against newer KDE
libraries than the Frame has.
- On first use, the computer copies them to the Frame over the SSH connection
it already has. The iPhone app's bundle, already copied to the Frame, has
them too. The agent checks each SHA-256 and unpacks them into
`~/.local/share/frame-control/kdeconnect` (3.6 MB copied, 18 MB unpacked,
about 2 s). There's no internet download on the Frame, no root, and nothing
on the read-only system, so SteamOS updates leave it alone. A stamp there
(`root/.frame-control-packages`) records which build it is; a newer Frame
Control replaces it.
- `pacman -Sp kdeconnect …` also pulls in ModemManager, libqmi, libmbim,
libqrtr-glib and ppp (packaging dependencies). `kdeconnectd` and its plugins
don't link any of them (checked with `ldd` against the six packages alone),
so Frame Control leaves them out.
- Licences: the packages are GPL and LGPL; Frame Control stays MIT because it
only starts `kdeconnectd` and speaks its protocol. The notice, licence texts
and complete source are in [`frame/kdeconnect`](../frame/kdeconnect/NOTICE.md),
[`THIRD_PARTY_NOTICES.md`](../THIRD_PARTY_NOTICES.md) and the app's
**About and licences** (Tools).
- It pairs by itself: the agent asks to pair and accepts on KDE Connect's side
over D-Bus (`qdbus6 … acceptPairing`). It keeps its identity in
`…/kdeconnect/bridge`, so later connections are already paired. (A pair
request to a device that's already paired makes KDE Connect unpair it, so
the agent only asks when it isn't paired.)
- Protocol version 7: whoever opens the TCP connection sends its identity line
in plain text, then acts as the **TLS server** (KDE Connect's
`lanlinkprovider.cpp`). Remote input is `kdeconnect.mousepad.request` with
`dx`/`dy`, `singleclick`, `rightclick`, `singlehold`/`singlerelease`,
`scroll`, `key` (any text) or `specialKey` (1 Backspace … 14 Escape,
21–32 F1–F12) and modifier flags.
- **KDE Connect runs only while something uses the keyboard and trackpad.**
Each device gets its own KDE Connect identity (KDE Connect keeps one
connection per device, so a shared one would make a phone and a computer
knock each other off). The last one to disconnect stops KDE Connect, so it
isn't left running, or discoverable on your network, afterwards.
- KDE Connect 24.02 **hangs or crashes when asked to unpair a device that's
offline** (seen twice: once spinning at 100% CPU with D-Bus unresponsive,
once exiting). Frame Control never unpairs. If its copy stops answering, the
agent restarts it once (tested by freezing it with `kill -STOP`).
- Moves from the iPhone app (Simulator) and the Mac's server moved the Frame's
X pointer by exactly the amount sent, including with the bundled packages
copied over SSH (2026-09-28).
- gamescope runs **two Xwayland displays**. `:0` holds Steam's VR bar and menus
and ignores injected pointer motion; `:1` holds apps such as Chromium and
takes it. KDE Connect runs on `:1`, so it reaches apps, not Steam's own menus.
There's also a `gamescope-0-ei` (libei) socket.
- Typing through KDE Connect lands in a Chromium panel on `:1` (seen in the
panel's own capture, 2026-09-29). It **can't reach panels on `:0`** (Frame
Control's own panels, Steam's UI) and, since XTest positions are clamped to
`:1`'s 1280×720 root, can't reach beyond that in a bigger window. Control on
the live view (below) has neither limit.
- **Not yet tested:** whether it reaches the KDE desktop panel (Plasma is its
own session).
- **Known limit:** keys and clicks typed while the link is reconnecting wait
and are sent once it's back, but anything sent in the moment the Wi-Fi
drops, before SSH notices, can be lost. Confirming every event would add a
round trip to each pointer move.
## Live view and Control: watch a panel and tap on it
**Built: Home → Desktop / Headset view → Control.** The live view has two
sources:
- **Headset view**: what the lenses show (SteamVR's mirror, `/dev/video99`). It
moves with the wearer's head, so Control makes the view a trackpad: drag to
move the pointer, tap to click, press and hold to right-click, two fingers to
scroll. With a mouse, moving over the view moves the pointer.
- **Desktop**: the app panel in use in the headset, from its own window, so it
stays still however the wearer looks around. Control makes taps and clicks
land exactly where you put them. Dragging is a mouse drag, press and hold is a
right-click, two fingers scroll, and on a computer the mouse, wheel and
keyboard work directly on it (⌘ is sent as Ctrl on a Mac). A picker shows any
other panel, view only.
Below the view, a text field and key buttons type on the Frame from a phone.
How (**verified 2026-09-29**, SteamOS 0.4.1, build 20260925.6191901):
- **Input goes through gamescope's own injection.** gamescope serves an EIS
socket (`/run/user/1000/gamescope-0-ei`; Steam feeds Remote Play input through
it), and `libei` 1.4.1 is on the image. [`ui/frame_touch.py`](../ui/frame_touch.py)
talks to it with `ctypes`: nothing to install. gamescope offers one device,
"Gamescope Virtual Input", with relative and absolute pointer, buttons,
scroll and keyboard (Linux key codes; no text capability, so the text field
types printable ASCII on a US layout). Its absolute region is unbounded; the
pointer uses the focused panel's display coordinates, and gamescope fits each
window to its display, so a 1920×1080 window on the 1280×720 `:1` takes
positions at two thirds scale. Taps on a 1280×720 page landed on the exact
pixel.
- **It reaches the panel that has focus** (`GAMESCOPE_FOCUSED_WINDOW` on `:0`'s
root), on either X display. In the OpenVR backend focus moves only on SteamVR
overlay events (the controller's laser entering or clicking a panel), or to a
new panel when none holds it (read from gamescope's `OpenVRBackend.cpp`, seen
with `gamescopectl focus_info`, which writes to the journal). Neither
`GAMESCOPECTRL_BASELAYER_WINDOW`/`_APPID` nor X focus moves it, and no
gamescope command does. So Control follows the wearer: whatever they last
used is what your taps reach. A window without a Steam app id (`STEAM_GAME`)
gets a connector of its own and doesn't hold focus.
- **Keys in a burst can arrive out of order**, so the helper paces them (8 ms
apart).
- **Known limit:** if focus moves to another panel in the middle of a drag, the
release goes to the panel that has focus then. Whether gamescope hands it to
the window that got the press isn't known yet. When the session ends, the
helper lets go of every button and key it still holds.
- **The Desktop picture is the window's own pixels**: `ffmpeg -f x11grab
-window_id <window> -i :<display>` works on gamescope's redirected windows,
while grabbing the root gives black. It streams as H.264 like the headset view
(about 30 fps at 720p).
- Tested from the iPhone app (Simulator): a tap on the Desktop view focused a
text box in the panel and the text field typed into it; a trackpad move went
exactly (+40, +25).
Our own `uinput` keyboard and mouse would also work (`steamos` is in the
`input` group and `/dev/uinput` is group-writable, verified 2026-09-27), and
remains the fallback if the bundled KDE Connect ever stops working on a new SteamOS.
| Other option | Mac | iPhone | Why not |
|---|---|---|---|
| **A uinput keyboard and mouse in Frame Control's server** | ✓ | ✓ | **Recommended.** The server opens `/dev/uinput` with `ctypes` (standard library only) and the page sends key and pointer events through the tunnel it already has. On the phone: a trackpad area (drag to move, tap to click, two fingers to scroll) and the iOS keyboard for typing. On the Mac: a "control the Frame" mode that captures the keyboard and pointer (Esc to release). Uinput devices look like real hardware to the kernel, so libinput, KWin and gamescope should take them; [frame-voice](https://github.com/DeeJanuz/frame-voice) already types into a Frame through a uinput keyboard. **Untested**: which surfaces in VR (desktop panel, SteamVR dashboard, games, Android apps in Lepton) accept the pointer. About a day or two of work |
| **Bluetooth keyboard and mouse** | – | – | Real hardware paired in SteamOS settings. The iPhone can't pretend to be a Bluetooth keyboard: iOS won't advertise the HID service ([Apple forums](https://developer.apple.com/forums/thread/733916)) |
| **Bluetooth keyboard and mouse** | – | – | Needs real hardware, paired in SteamOS settings. The iPhone can't pretend to be a Bluetooth keyboard: iOS won't advertise the HID service ([Apple forums](https://developer.apple.com/forums/thread/733916)) |
| **Deskflow** (formerly Input Leap / Barrier) | ✓ | – | Moves the Mac's own mouse and keyboard onto the Frame's screen edge. Flathub has an aarch64 build ([Flathub](https://flathub.org/apps/org.deskflow.deskflow)); on Wayland it needs the InputCapture/libei portal, and only works while Plasma is running. No iPhone client |
| **KDE Connect** | ~ | ✓ | Its iOS app has a remote touchpad and keyboard, but the Frame would need KDE Connect installed (not on Flathub; `pacman` on a read-only root). More moving parts than the uinput route |
| **Remmina / Steam Link / RDP** | ✓ | – | Input only reaches the streamed session, not the headset's own apps |
Other ways to get text in:
+62
View File
@@ -104,6 +104,20 @@ switch while SSH is down:
## Headset smoke test
**Documented shared-device procedure:** before a test installs, launches or
stops an application, acquire `ssh frame 'mkdir /tmp/frame-test.lock'`. If it
fails, leave that lock alone and continue offline work. Only the thread that
acquired it releases it with `ssh frame 'rmdir /tmp/frame-test.lock'`, after
cleanup. Keep each device session to a few minutes.
Check battery capacity and charging state under `/sys/class/power_supply`
before and after; keep capacity above 20%. Stop only processes started by the
test, remove temporary installs and profiles, and restore the prior dashboard
state. Leave Steam and SteamVR running. Do not reboot or change global settings.
Record the build, actual interaction results, cleanup and any unworn-headset
limits alongside screenshots or logs. These are caller responsibilities; the
smoke script below does not acquire this shared lock itself.
```sh
scripts/frame-smoke.sh # needs `ssh frame` to work without a password
scripts/frame-smoke.sh --pair # also pairs a throwaway key: approve it in the headset
@@ -148,3 +162,51 @@ For example, on 2026-09-27 the smoke test found that Steam's `create-shortcut`
refuses ids with a hyphen (`missing/invalid arguments`), which the fake had
accepted. The fake now refuses them the same way, and Frame Control makes ids
Steam accepts.
## Owned media player
`tests/test_media.py` covers layout evidence and overrides, OU eye ordering,
hardware-decoder command construction, malformed splats, stereo parallax and
fake-Frame library/process ownership. `tests/e2e/test_media_transfer.py` checks the real
HTTP/SSH upload and library listing without pretending the fake renders VR.
Real decode timings and captured stereo output are recorded in
[vr-video.md](vr-video.md). Generated media only; no external player required.
**Verified 2026-09-28**, real Frame BUILD_ID 20260925.6191901: `~/.local`
and `~/.local/share` are `steamos:steamos`, mode 0755. The fake supervisor
sets those parent owners too; previously its root-created Steam manifests
left the parents root-owned and incorrectly prevented user runtime installs.
## Agent interfaces
`tests/test_agent.py` exercises MCP stdio, exact-action human approvals and the
assistant against an in-process HTTP endpoint with canned responses (no keys or
external calls). `tests/e2e/test_agents.py` runs the MCP/HTTP/SSH path against the
fake Frame for approved installs, clipboard and file transfer. Headset Chromium
rendering and real screenshots still need a device; see [agent evidence](agents.md#evidence-and-limits).
## Family and comfort
`tests/test_comfort.py` uses an injected clock, fake headset sensor readings and
actions, plus a Node fake of Steam's Home API. It covers warnings before Home,
late/suspended sessions, cancellation, failed actions, duplicate alerts, reboot
invalidation, per-zone thermal trips and shared on-headset state. The server
guards reject invalid session settings before SSH. See
[real-device evidence and limits](family-comfort.md#verification).
## Panel switcher
`tests/test_panels.py` supplies fake-Frame `vrcmd --overlays` output, checks
main-panel filtering (including hidden panels), revalidates closed panels before
focus, and drives the headset helper's real loopback HTTP server to test access
keys, Host/Origin guards, malformed requests, offline errors and Close. It runs
in the normal unit suite without OpenVR or a headset. The fixture format comes
from SteamVR 2.18.1, BUILD_ID `20260925.6191901`; it does not simulate rendering.
On the Frame, run `python3 -` over SSH with `ui/frame_panels.py` on stdin to
list panels. `--focus <key>` rechecks the list and requests focus. In Frame
Control, **Tools → Panel switcher → Open in headset** exercises installation,
Chromium rendering and the same helper through HTTP. Close the switcher after
testing. [The recorded device checks](panels.md#frame-controls-panel-switcher)
cover actual focus, HTTP guards and an OpenXR sample transition, and separately
identify the unverified Steam-game, spatial layout, reboot and laser behaviors.
+274
View File
@@ -0,0 +1,274 @@
# VR APKs and Quest games in Lepton
What it takes to run an immersive (OpenXR) Android app, including Meta Quest
builds, on the Frame. Checked on SteamOS BUILD_ID 20260925.6191901, Lepton
v2.8.14 (rootfs v2.8.11), SteamVR 2.18.1, on 2026-09-28, unless marked
**inferred**.
## How a VR APK reaches SteamVR (verified)
- Lepton ships a standard Khronos system runtime manifest,
`/vendor/etc/openxr/1/active_runtime.json`, pointing at SteamVR's Android
client, `/data/steamvr/runtime/bin/androidarm64/vrclient.so`. That is the
host's `/opt/steamvr/bin/androidarm64/`, bind-mounted in.
- An APK's own Khronos-style `libopenxr_loader.so` tries the runtime brokers
(`org.khronos.openxr.runtime_broker`, `…system_runtime_broker`), finds
neither, then falls back to that manifest. Nothing in the APK has to change
for discovery.
- Install the APK without the flatscreen marker
(`python3 ui/frame_android.py install app.apk --vr`). The marker only
controls Lepton's 2D Android surface; the app itself has to start an
OpenXR session.
- Lepton also loads Valve's `XR_APILAYER_VALVE_fdm_injection` layer from
`/vendor/etc/openxr/1/api_layers/implicit.d/`. Only layers in Valve's own
directories are picked up (`liblepton/vulkan_layers.sh`), so a third-party
layer has to ship inside the APK.
**Open Brush 2.32.29, the Quest APK from its GitHub release (Unity OpenXR,
Vulkan), works unmodified.** Its manifest already has `LAUNCHER` next to
`com.oculus.intent.category.VR`. Unity asked for OpenXR 1.1, got
`XR_ERROR_API_VERSION_UNSUPPORTED`, retried with 1.0 and succeeded. SteamVR
took it as the scene app, created Touch, simple-controller and Frame-controller
bindings, and the session reached `XR_SESSION_STATE_FOCUSED`. A headset capture
(`ui/frame_vrshot.py`, after waking the compositor and closing the dashboard)
showed a dark sky over a mountain horizon; nobody wore the headset to confirm
it was Open Brush's scene or to try drawing.
**Khronos `hello_xr` (Vulkan, 1.1.63 release APK) works unmodified:**
`Instance RuntimeName=SteamVR/OpenXR RuntimeVersion=2.18.1`, 1728×1728
swapchains per eye, session `IDLE → READY → SYNCHRONIZED` (the headset was
not being worn, so it did not reach `FOCUSED`).
## What SteamVR's Android runtime supports (verified, from `vrclient.so`)
- **OpenXR 1.0 only.** An app requesting `XR_API_VERSION_1_0` works; one
requesting 1.1 (`XR_CURRENT_API_VERSION` in a 1.1 SDK) gets
`XR_ERROR_API_VERSION_UNSUPPORTED` from the runtime.
- Extensions include `XR_KHR_opengl_es_enable`, `XR_KHR_vulkan_enable{,2}`,
`XR_KHR_composition_layer_depth`, `XR_KHR_locate_spaces`,
`XR_EXT_local_floor`, `XR_EXT_uuid`, `XR_EXT_palm_pose`,
`XR_EXT_hand_tracking`, `XR_EXT_eye_gaze_interaction`, and these Meta ones:
`XR_FB_display_refresh_rate`, `XR_FB_foveation{,_configuration,_vulkan}`,
`XR_FB_space_warp`, `XR_FB_swapchain_update_state`,
`XR_META_foveation_eye_tracked`, `XR_META_recommended_layer_resolution`,
`XR_META_vulkan_swapchain_create_info`, `XR_META_performance_metrics`.
- Not present: `XR_FB_passthrough`, `XR_FB_hand_tracking_*`,
`XR_FB_spatial_entity*`, `XR_FB_color_space`,
`XR_KHR_android_thread_settings`, `XR_OCULUS_*`.
- Interaction profiles include `oculus/touch_controller`, `khr/simple_controller`,
`valve/frame_controller` and the usual PC controllers. Valve documents Touch
bindings as a working fallback on the Frame controllers.
## What stops a Quest APK (verified with Wolvic 1.9, `oculusvr` build)
1. **Lepton won't start it.** Lepton's `apk-info-extractor` only accepts an
activity whose intent filter has `android.intent.action.MAIN` and
`android.intent.category.LAUNCHER`. Quest apps use
`com.oculus.intent.category.VR` instead, so Lepton logs `APP_ACTIVITY is
empty` and exits. There is no override. **Fix:** add the `LAUNCHER`
category to that intent filter and re-sign. After that, Wolvic started.
2. **OpenXR 1.1.** Wolvic's Quest build then requested OpenXR 1.1 and aborted
on `XR_ERROR_API_VERSION_UNSUPPORTED`. Unity's OpenXR plugin retries with
1.0 (Open Brush, above), so this mostly bites native and non-Unity apps. **Fix (inferred):** an API layer
inside the APK that asks the runtime for 1.0 and maps the 1.1 core
functions to the extensions the runtime does have (`XR_KHR_locate_spaces`,
`XR_EXT_local_floor`, `XR_EXT_uuid`, `XR_EXT_palm_pose`).
3. **Lepton's missing clipboard service** still applies to VR apps. The Godot
XR Tools demo's Quest build (itch.io) dies in `Godot.<init>` casting the
null clipboard service to `ClipboardManager`, before any OpenXR call. See
the clipboard table in [apks.md](apks.md).
4. **Not yet reached:** required Meta-only extensions (each app differs),
swapchain formats (the Lynx Wolvic build needed `GL_SRGB8_ALPHA8`), and
Meta platform services.
The loader was never the problem: Wolvic's Quest `libopenxr_loader.so` is a
Khronos-style loader and found SteamVR through `/vendor`.
## In the Steam library
Every successful APK install goes through the same mandatory artwork writer:
CLI (including `scripts/install-apk.sh`), upload, catalogue, version finder,
web download and source modules calling `frame_android.install`. Native
Linux/Windows sideloads also use it, preserving their devkit runtime wiring.
A new shortcut is rolled back if artwork fails; failure is never reported as
an installed app with a blank tile.
Artwork preference is **SteamGridDB → source images → generated fallback**.
Set the optional free key in Frame Control's **Library artwork settings**, or
`STEAMGRIDDB_API_KEY` (`FRAME_STEAMGRIDDB_API_KEY` also works). Environment
settings override the saved key. Without a key there are no provider calls or
warnings. Saved keys stay in host app data, mode 0600 on POSIX, and are never
returned by the settings API or copied to the headset. Exact title matches
(including a trailing “VR” variant) use the highest-scored returned static,
non-NSFW image in each slot. Provider failures use the next source.
Sources pass `install(apk_path, artwork={...})`: keys are `grid`, `wide`,
`hero`, `logo`, `icon`, `banner`, `feature_graphic`, `screenshot`, or a list
`screenshots`. Values are PNG/JPEG bytes or HTTP(S) URLs (12 MiB and
4096×4096 pixels maximum; any PNG depth or interlace, since the Frame's
Chromium decodes them). URLs must resolve to public addresses, follow at most
three redirects and share one deadline per install. Any source that fails,
for any reason, becomes a warning and generated art. Banners and feature graphics supply hero/wide art;
screenshots are the next fallback. Source images are cached for refresh.
All images are fitted to 600×900 portrait, 920×430 wide, 3840×1240 hero,
1280×480 logo and 256×256 icon. Explicit logos retain transparency.
Photo-based portrait, wide and hero slots are JPEG: Steam takes at most
12 MiB per slot, and on the Frame (2026-09-28) a noise-heavy 3840×1240 hero
came to more than 12 MiB as PNG, 3.7 MB as JPEG (2.7 s to render); a
landscape photo hero 5.6 MB as PNG, 0.76 MB as JPEG (0.75 s). A render that
still fails is retried once with generated art. Steam keeps a slot's `.png`
and `.jpg` side by side, so each slot is cleared before it is set.
Generated art uses the APK icon, a dominant-colour gradient, a blurred
backdrop and large foreground icon with shadow. Steam's Chromium canvas and
Motiva Sans render real text consistently regardless of the host OS; no
Pillow, host font installation or bitmap font is needed. The hero has no
title; the generated logo is a transparent title. APKs with no usable icon
get a typographic monogram. The desktop package includes the renderer.
Backfill installed Android apps without reinstalling or stopping them:
```sh
python3 ui/frame_android.py refresh-art org.godotengine.open_saber_plus
python3 ui/frame_android.py refresh-art --all
```
Devkit titles installed by Frame Control have the same command,
`python3 ui/frame_titles.py refresh-art ID|--all`. The settings panel's
refresh covers both. The API is `POST /api/android` with
`{"action":"refresh-art","all":true}` (apps and titles) or a `package`, and
`POST /api/titles` with `{"action":"refresh-art","id":…}`; each returns a
background job. Batch results retain per-item errors, and the CLIs exit
nonzero if any failed. Apps and titles without complete artwork show **Add
artwork** and `list` prints the command. Only entries marked `art_pending` at
install (a title Steam registered after an install made while it wasn't
running) are backfilled automatically, when Frame Control lists them with
Steam running (at most every five minutes), and that backfill only fills
slots Steam has no art for: names, icons, flags and any art the user set are
kept. Older installs without the flag are refreshed only on request.
Steam's app overviews carry no `devkit_gameid` (checked 2026-09-28, build
20260925.6191901, on every non-Steam shortcut). A title's shortcut is found by
its saved id, or by an executable or start folder inside
`~/devkit-game/<id>/`, read from `appDetailsStore`; never by display name.
That the devkit shortcut's exe/start folder sit inside the title folder is
inferred from `docs/sideloading.md` (`proton waitforexitandrun
"/home/steamos/devkit-game/<id>/<exe>"`), not yet seen in app details.
Devkit titles keep the VR flag Steam gave them.
**Verified on build 20260925.6191901, SteamVR 2.18.1 (2026-09-28):** both
Open Saber Plus and SuperTux were backfilled. Steam's cached portrait, wide,
hero and logo PNGs have the dimensions above; each shortcut points at its
256×256 icon. This Frame client mishandles custom-art type 4 (documented as
Icon), overwriting the wide capsule; the implementation uses custom types
0–3 and **SetShortcutIcon** separately.
Steam accepts display name, executable/start directory, icon, VR flag and
sort-as name. Android apps join **Android**, immersive apps also **Android
VR**; native sideloads join **Sideloaded**. Existing collection members and
unrelated collections are preserved (both games retained **Played**).
Dynamic/read-only collection conflicts produce warnings. The native notes
API supports a managed **Installation details** note (package, version and
source) while preserving other notes. Notes are keyed by sanitized shortcut
name, so Steam itself cannot distinguish equal-name shortcut notes. No
supported shortcut description/store-page, developer/publisher, release
metadata or custom achievement API was found; these are not fabricated.
The launcher supervises Lepton and handles TERM/INT/HUP and normal exit by
stopping its own container and child process group. A lock refuses duplicate launches;
a container still running while the lock is free was orphaned by a killed
launcher and is stopped before the new launch. Lepton doesn't inherit the
lock. Orphan recovery only stops the app's own, deterministically named
container; a Lepton host process whose launcher was killed before it created
the container may linger briefly. Removing an app or title still deletes its files when Steam isn't
running; tidying Steam's collections and artwork is best effort. Steam Stop uses `TerminateApp` with the exact
64-bit game ID string. Frame Control's Stop additionally has a direct-container
fallback. The stable instance ID and compatdata paths remain unchanged.
Lepton normally forwards the instance `SteamAppId` to Android, causing
SteamVR to associate the scene with a different, artwork-less app. The
launcher uses Lepton's supported `LEPTON_ENV_SteamAppId` passthrough to send
the actual shortcut ID to Android while retaining the stable container ID.
**Verified:** Open Saber was alive 22 seconds after Steam Play, SteamVR
identified `steam.app.3346865537`, and its scene appeared in the headset
capture without the previous blank Resume tile. Steam Stop then removed its
tracked process and stopped the container. An earlier 32-second session was
also tracked until Steam Stop. No global standby or dashboard overrides were
installed; wear detection and other user-opened overlays still apply.
**SuperTux limitation:** Steam launched and tracked it, but SDL crashed during
activity creation because Lepton lacks `ClipboardManager`. Its container
cleaned up on exit after about 17 seconds. Consequently sustained SuperTux
Play/Stop and its VR scene could not be verified. This is an APK/runtime
compatibility failure, separate from library presentation.
Evidence is under `/tmp/vrlib-evidence/` on the development Mac: final artwork
preview and three design passes, `steam-cache-final.log`,
`steam-details-targets.json`, `opensaber-identity-session.log`,
`opensaber-identity-headset.png`, and `supertux-lepton.log`. The preview is
rendered artwork, not a Steam UI screenshot; CDP screenshot capture timed
out. Authenticated SteamGridDB, Windows/Linux packaged builds and the sibling
source-search endpoint remain unverified (the public install seam is tested).
## Out of scope
- **Meta entitlement.** Apps that call the Oculus Platform SDK
(`libovrplatformloader.so`) to check the Quest store licence need Meta's
services. Frame Control won't work around that.
- **VrApi-era apps** (`libvrapi.so`, before OpenXR) need an API translator,
not a patch.
## Frame Control does this for you
APK uploads and `python3 ui/frame_android.py install app.apk` detect VR
manifest categories, Samsung's `vr_only` flag and the arm64 OpenXR loader.
VR apps default to immersive mode without the flatscreen marker. The upload
selector or CLI `--flat` / `--vr` overrides that choice. Compatibility notes
identify legacy VrApi, Meta platform SDK and OpenXR libraries.
Lepton only starts an `<activity>` whose MAIN intent filter has LAUNCHER; it
ignores `<activity-alias>`, which is where Godot 4 exports put LAUNCHER. When no
real activity qualifies, Frame Control adds LAUNCHER to the VR activity's MAIN
filter, or to the activity the launcher alias targets, then repacks and v2-signs
the APK locally before copying it; `meta.json` records
`"patched": ["launcher"]`. Unchanged ZIP members retain their compressed
bytes; stored libraries are aligned to 16 KiB. The RSA signing identity lives
in Frame Control's per-user app-data directory as `apk-signing-key.json`
(mode 0600). Keep this key to preserve the signer on subsequent patched
updates. A re-signed APK cannot update an installation signed by its original
publisher; Android also treats it as a different signer for signature checks.
VR apps with an arm64 OpenXR loader also get the OpenXR compatibility layer
([frame/openxr-compat](../frame/openxr-compat/README.md)): an implicit API
layer in the APK's `assets/openxr/1/api_layers/implicit.d/`, which the app's
own loader picks up next to Valve's layer. It asks SteamVR for OpenXR 1.0 when
the app wants 1.1 and enables the extensions that became 1.1 core; maps
`xrLocateSpaces` to `xrLocateSpacesKHR` and `grip_surface` to `palm_ext`; drops
1.1 controller profiles SteamVR doesn't know; stubs
`XR_KHR_android_thread_settings` and `XR_OCULUS_android_session_state_enable`;
and keeps the current refresh rate when SteamVR refuses a requested one.
`meta.json` records `"patched": ["openxr-compat"]`. Skip it with
`install … --no-xr-compat`. Its decisions go to logcat under `FrameXrCompat`.
Verified on the headset (2026-09-28):
- **Wolvic 1.9, Quest build**, installed as downloaded: Frame Control added
`LAUNCHER` and the layer. The layer turned OpenXR 1.1.48 into 1.0.63, the
instance and session were created, and a 144 Hz refresh request that SteamVR
refused was kept at the current rate. The session reached `SYNCHRONIZED`;
then Wolvic's Gecko engine crashed (null SIGSEGV on its Gecko thread, the
same crash its Lynx build has), which is Wolvic's, not OpenXR's.
- **Open Brush, Quest build**, with the layer: 1.1.54 → 1.0.63, the thread
settings stub in use, `bytedance/pico4_controller` bindings dropped, and the
session reached `FOCUSED`, the same as without the layer.
Inspect or prepare an APK without contacting the headset:
```sh
python3 ui/frame_android.py info app.apk
python3 ui/frame_android.py patch app.apk patched.apk
python3 ui/frame_android.py patch app.apk patched.apk --add assets/openxr/1/api_layers/implicit.d/X.json=X.json --add lib/arm64-v8a/libX.so=libX.so
```
The patch fixes Lepton's launch-category requirement. It does not supply an
OpenXR 1.1 translation layer, Meta services or a VrApi implementation.
+139
View File
@@ -0,0 +1,139 @@
# VR comfort and performance
Frame Control owns its HUD and telemetry. They use SteamVR/OpenVR, gamescope,
Python and xterm already on the Frame, plus the Frame's sensors. No feature
requires fpsVR, OVR Advanced Settings, XSOverlay or another third-party app.
The software list is a separate, optional convenience.
This is **part of [#25](https://github.com/saphid/frame-control/issues/25)**,
not completion of the issue. Playspace controls remain blocked on the device
checks below. The PR stays draft.
## Our performance HUD
On **Home → VR comfort and performance**, the app shows a timestamped sample
with each status refresh (30 seconds, or Refresh). **Open HUD in headset**
starts our text HUD as a gamescope panel, refreshed every two seconds. In the
SteamVR dashboard, select **Frame Control HUD**, then Float in World or dock
it to a controller. **Close HUD**, or closing its terminal, ends it. Opening
it twice reuses the existing process.
| Value | Meaning and source |
|---|---|
| Compositor FPS / period | Differences between two `IVRCompositor_029::GetFrameTiming` frame indices and monotonic compositor timestamps, sampled 200 ms apart. Output cadence, not game FPS or a long-term average. |
| Application FPS | Reciprocal of OpenVR's client frame interval. Unavailable if there is no positive interval; not inferred from refresh rate. |
| Render GPU time | OpenVR total render GPU milliseconds, not GPU utilisation. |
| Compositor CPU | OpenVR compositor render CPU milliseconds, not game CPU time. |
| System CPU | `/proc/stat` busy-time delta across the sample, with guest time counted once and iowait treated as idle. |
| GPU clock | `3d00000.gpu/cur_freq`, converted from Hz to MHz; frequency is not load. |
| Hottest sensor / battery | Existing thermal-zone and battery sysfs reads from `frame_status.py`. |
OpenVR uses background application mode, which does not start SteamVR or keep
it running. This mode also returned live timing in a read-only device probe.
Missing sensors, a stopped or incompatible SteamVR runtime, and non-advancing
frame indices display **Unavailable**, never invented zero FPS. Failed status
refreshes clear the HUD card rather than keeping a stale live-looking sample.
The HUD itself adds CPU/GPU work; it is a diagnostic, not a zero-overhead benchmark.
**Verified 2026-09-28**, SteamOS 0.4.1, build `20260925.6191901`, SteamVR
2.18.1: the exact OpenVR interface and 192-byte timing layout returned advancing
frame indices and live GPU/CPU timing. Sensor reads, creation of overlay
`valve.steam.desktopgame.2000250025`, duplicate-open handling, and closing the
HUD passed. The temporary probe overlay disappeared after its process closed.
[Sanitized device sample](evidence/vr-utilities/device.json).
**Unverified:** visual placement while wearing the headset, controller docking,
and overhead during gameplay. The companion card was checked in the attached
preview using live Frame data. Creating an overlay does not establish that it
was visible to the wearer.
The optional HUD copies only `frame_status.py` and `frame_vr.py` into
`~/.local/share/frame-control/vr/`. It tags only the window whose X11 PID matches
its own xterm, avoiding other threads' windows. Stop checks both PID and Linux
process start time before sending SIGTERM. It does not stop Steam or SteamVR,
edit their settings, install a service, or need sudo.
## Playspace, seated height and recenter: paused
**Verified 2026-09-28:** SteamVR exposes `IVRChaperoneSetup_006` and
`IVRChaperone_004`. An initial 1 cm seated zero-pose translation committed,
read back and restored numerically. A later trial, while the shared Frame was
in use, changed universe IDs after commits and returned transforms that did
not match the requested write or restore. Journal entries reported
`CommitWorkingCopy`, `VREvent_ChaperoneUniverseHasChanged` and
`VREvent_ChaperoneRoomSetupCommitted`. The collision-bound arrays and play-area
size matched in the saved before/after records, but the origin matrices did not.
Alex confirmed the headset was in use and asked to pause control tests. No
further playspace writes were made. The exploratory control implementation was
removed from the shipping API; `recenter`, `adjust` and `restore` are rejected.
This is an **unresolved feasibility check**, not evidence that OpenVR controls
cannot work. Concurrent use and the Frame driver's coordinate-system handling
still need to be separated.
The probe's original and last-read poses remain in the Frame's
`~/.local/share/frame-control/vr/comfort.json` for investigation. This draft
neither reads nor applies that baseline. Do not blindly replay it into a room
that may have changed. No recenter test was reached in the later trial.
Before adding controls, on an idle Frame:
1. Establish current room and tracking state, and inspect the retained probe
evidence before considering any restoration.
2. Prove seated and standing height/move operations in the Frame driver's
current coordinates, including delayed readback, coordinate rebasing and
recovery. Show that the physical safety boundary stays correct.
3. Verify recenter independently, and test a full apply/restore cycle plus a
concurrent room-change refusal. Add a fake OpenVR test for those contracts.
4. Check the apparent result in a seated and a standing app before exposing UI.
**Documented:** seated and standing are tracking origins selected by an app;
changing a seated origin cannot force every game to support seated play.
**Inferred from the installed SteamVR defaults:** there is no generic snap-turn
or locomotion-vignette setting. `dashboard.verticalOffsetCm_2` and
`steamvr.panelMaskVignette` affect panels, not the player's height or game
locomotion. The app gives game-setting hints for snap-turn, teleport movement
and movement vignette instead of writing these unrelated settings.
## Optional software
**Verified 2026-09-28 (same build):** a read-only query of Steam's loaded
`appStore.allApps` found none of these five apps on the Frame account. The query
includes software, which the existing games-only library filter excludes.
Steam's public app-details API listed only Desktop+ as free. No software was
purchased, installed or launched during these ownership checks.
| Utility | Local Frame status | Public evidence / optional source |
|---|---|---|
| OVR Advanced Settings (1009850) | **Untested**, not owned | Steam edition is paid. Developer's [free source and releases](https://github.com/OpenVR-Advanced-Settings/OpenVR-AdvancedSettings). No verified Frame result in this work. |
| XSOverlay (1173510) | **Untested**, not owned | Supplied research attributes Proton support with tweaks to [Road to VR](https://www.roadtovr.com/valve-steam-frame-review/). This is a public report, not our verification. |
| OVR Toolkit (1068820) | **Untested**, not owned | Same [public Frame report](https://www.roadtovr.com/valve-steam-frame-review/); no local verification. |
| fpsVR (908520) | **Untested**, not owned | [Steam listing](https://store.steampowered.com/app/908520/). No Frame-specific result established in the supplied research. General PC VR reviews do not verify Frame support. |
| Desktop+ (1494460) | **Untested**, free | [Developer source](https://github.com/elvissteinjr/DesktopPlus). No verified Frame result in this work. |
**Documented (supplied research):** the XSOverlay/OVR Toolkit claims are kept as
attributed leads. **Verified source check 2026-09-28:** fetching the linked
review returned HTTP 200 and the expected review title, but neither utility name
appeared in the fetched HTML or extracted text. The claims could not be
corroborated from that page; this is not proof of incompatibility. They do not make those apps dependencies or mark them
locally verified. No paid utility is auto-acquired. A server-side check blocks
installation through the Steam endpoint if the paid utility is absent from the
loaded Frame library; an empty/unavailable library fails closed. Desktop+ uses
the existing free Steam install flow, which may need a license confirmation in
the headset. None is advertised as known-good without local evidence.
Compatibility reports reuse the existing `compat-db` storage and validation
with `package: "steam:<appid>"`, rather than colliding with Android package IDs.
The optional list shows the latest `works`, `issues` or `broken` report with its
date, build and notes, separately from public sources and ownership. With no
report the status stays **untested**. No shared database schema change or
production deployment is needed; no reports were published by this work.
## Validation and review
166 unit tests and 8 website tests passed locally. The new fake-Frame cases
passed in GitHub CI (Docker was unavailable locally). Desktop and phone-width
preview checks passed. The two independent review attempts did not produce a
verdict; [commands, real exit statuses and limitations](evidence/vr-utilities/review.md).
+167 -72
View File
@@ -1,81 +1,176 @@
# Watching VR video (180°/360°) on the Frame
# Movies, stereo photos and splats
The confidence labels are the same as in [ssh.md](ssh.md). Everything here
was checked on SteamOS 0.3.0, build 20260922.6101926.
Frame Control has its own Frame-side OpenVR player. It uses SteamOS's ffmpeg
and V4L2 hardware decoder, Python and SteamVR. **No separate player or viewer
is required.** Chromium and immersive WebXR are not in this playback path.
## The short version
## Use it
1. Install **DeoVR Video Player** from Steam (free, app 837380) and start it once
in the headset. That creates its Proton prefix.
2. On the Mac: `scripts/push-vr-video.sh --launch ~/Movies/beach_180_LR.mp4`
3. In the headset, open DeoVR's local file browser → **Videos → VR** and
pick the file.
In **Tools → Media in the headset**, send a file, choose its layout and press
**Play**. **Theatre** gives it a larger screen and an 85% black surround.
**Stop** removes both. Refresh reads the library and the player's state.
The screen follows your head; it isn't a saved world-space panel.
## Why DeoVR
The Frame's Chromium can't play VR video in 3D. It has no immersive WebXR
([how-the-frame-works.md](how-the-frame-works.md)). DeoVR's Windows build
runs under Proton ARM64 + FEX as a real SteamVR app. **Verified 2026-09-25:**
it found the Steam Frame headset and controller over OpenVR, and decoded
7680×3840 and 8192×4096 H.265 VR180 side-by-side streams through AVPro's
hardware Media Foundation path, mapped onto a 180° dome or fisheye mesh.
Known quirks (verified):
- The first launch takes about 45 s while it compiles shaders.
- Grid thumbnails stay blank. Unity's own video player, which DeoVR uses for
previews, fails with `0xc00d36bb` under Proton. Full playback uses AVPro
and isn't affected.
- The in-app store and web content are separate from local files. You don't
need an account to play your own files.
## Getting files onto the headset
`scripts/push-vr-video.sh` copies files with `rsync --partial`, so an
interrupted upload resumes. They go to `~/Videos/VR` on the Frame (`/home`
has about 860 GB free). The script also links that folder into DeoVR's prefix
as `C:\users\steamuser\Videos\VR`. It's reachable at
`Z:\home\steamos\Videos\VR` as well. **Verified** that the upload and link
work. **Not yet checked** whether DeoVR's file browser lands there
(open question 21).
Speed: a test upload over Wi-Fi ran at about 3–5 MB/s (verified 2026-09-25,
one sample). At that rate an 8K file of several GB takes tens of minutes, so
start big uploads before you put the headset on.
## Naming files so they play correctly
DeoVR guesses the projection from the file name. Its binary contains the tags
`_180`, `_360`, `_fisheye`, `_fisheye190`, `_mkx200`, `_vrca220` and `_rf52`
(verified). For stereo layout, the common DeoVR convention is `_LR`/`_SBS`
(side by side) and `_TB` (top/bottom) (inferred). If a video looks wrong
(doubled, warped, or flat), change the projection and stereo mode in DeoVR's
player menu.
Examples: `trip_180_LR.mp4`, `concert_360_TB.mp4`, `hike_fisheye190_LR.mp4`.
Codecs: H.265 at 8K played (verified, streamed). Local H.264 and H.265
files haven't been played yet. The test clips below cover that.
## Test clips
The script doesn't include these. To check a setup, make two 20 s clips:
3840×1920, 180° side by side, with the left eye tinted red and the right eye
cyan. In the headset each eye should see only its own colour. A single mixed
colour means the stereo split is wrong.
The command-line route uses the same player:
```sh
ffmpeg -f lavfi -i testsrc2=size=1920x1920:rate=30:duration=20 \
-filter_complex "[0:v]split[a][b];[a]colorchannelmixer=rr=1:gg=0.3:bb=0.3[l];[b]colorchannelmixer=rr=0.3:gg=1:bb=1[r];[l][r]hstack" \
-c:v libx264 -pix_fmt yuv420p -b:v 20M frame-test_180_LR_h264.mp4
scripts/push-vr-video.sh frame-test_180_LR_h264.mp4
scripts/push-vr-video.sh --launch --theatre ~/Movies/film_SBS.mp4
scripts/push-vr-video.sh --launch --layout ou ~/Pictures/stereo.png
scripts/push-vr-video.sh --launch capture.splat
scripts/push-vr-video.sh --list
scripts/push-vr-video.sh --stop
```
## Streaming from the Mac instead of copying (untested)
Uploads live in `~/Videos/FrameControl/<id>/` on the Frame. Each upload gets
its own directory, so sending another file with the same name doesn't replace
it. The UI's existing upload limit is 8 GiB. Transfers use the app's rsync/scp
path; resumable large uploads are not implemented here yet. Existing
`~/Videos/VR` files and Proton prefixes are left alone. The script no longer
launches DeoVR, links a Proton prefix, or accepts directories.
DeoVR has a DLNA browser (the binary contains `Searching for DLNA
devices...` and a UPnP ContentDirectory client). A DLNA server on the Mac
should therefore appear in DeoVR without copying anything, for example
`brew install rclone` then `rclone serve dlna ~/Movies/VR`. This is **inferred**, not
tried. 8K VR video needs roughly 50–100 Mbit/s sustained, and the Wi-Fi
sample above (about 30–40 Mbit/s) suggests copying first is the safer default.
| Media | Supported preview |
|---|---|
| Movies | H.264 / H.265, mono or left-first SBS / top-first OU; audio through the Frame's PulseAudio-compatible server |
| Stereo photos | PNG / JPEG containing both eyes, SBS or OU |
| Gaussian splats | Common 32-byte `.splat` records, 1–20,000 Gaussians; a stationary stereo preview |
Auto layout reads delimited filename tags: `_SBS`, `_HSBS`, `_LR`, `_OU`,
`_TB`, `_HOU`, `_HTB`, `_FSBS`, `_FOU`, `_FTB`. SBS/OU without `F` means
half-resolution packing. Full packing preserves each eye's original aspect.
It also accepts FFmpeg's `stereo_mode` metadata (`left_right`, `top_bottom`,
`mono`); left/right and top/bottom metadata are treated as full packing.
Choose an explicit layout if that assumption doesn't match the file.
Unknown or conflicting tags ask for a choice, rather than silently flattening
stereo. The explicit selector always wins. Layout is not guessed from resolution.
## What was verified
**Verified remotely, 2026-09-28:** SteamOS 0.4.1, BUILD_ID
`20260925.6191901`, SteamVR 2.18.1. Nobody wore the headset for these checks.
All test media were generated by us. No paid content or DRM was involved.
- Stock `ffmpeg` `h264_v4l2m2m` decoded 150 frames of our 1920×1080 H.264
test in 0.128 s (decode only); converting all frames to RGBA took 0.242 s.
- Our ffmpeg → Python → OpenVR path submitted all 150 frames and exited 0.
The 1280×720 prototype took 4.775 s; the full 1920×1080 player took
4.618 s. These are wall times, not in-headset frame-rate measurements.
Finishing a 5 s clip early showed those probes weren't paced, so the final
player paces output at 30 fps: all 150 frames then completed in **5.025 s**.
- The H.265 hardware decoder also completed the generated 1080p clip
(60 frames, exit 0); this is a short compatibility check, not a 4K/8K benchmark.
- Our actual player, launched through Frame Control's media API, displayed
SBS and OU photos with red only in the left-eye capture and cyan only in
the right. OpenVR's `SideBySide_Parallel` flag separates the eyes; we
rearrange OU rows ourselves.
- A generated 48-Gaussian `.splat` rendered in our own CPU renderer and appeared
in both eyes with separate perspective projections.
- Theatre's owned dark surround and screen appeared in captures. Steam's
dashboard remained available over them. Stop removed the owned overlays
and ended the dedicated user service. No global SteamVR setting was changed.
- A generated H.264/AAC clip reached the Pulse audio output and completed
all 100 video frames with exit 0. This proves the output path, not audible
quality or lip sync.
![Frame Control SBS eye-isolation proof: red left, cyan right](img/media-sbs-proof.png)
![Our Gaussian-splat stereo preview on the Frame](img/media-splat-proof.png)
**Verified end to end, 2026-09-29** (same build; headset unworn): uploads
through the HTTP API, the web UI and `scripts/push-vr-video.sh`, all played by
the owned player. Our generated test files:
| Case | Result |
|---|---|
| H.264 half-SBS 1920×1080 with AAC, theatre | 240/240 frames in 8.09 s; audio stream "Frame Control Media" in PulseAudio |
| H.265 half-OU 1920×1080 | 180/180 frames in 6.03 s; red left eye, cyan right |
| H.264 full-SBS 3840×1080, `stereo_mode=left_right` only | Detected from metadata; 150/150 frames in 5.03 s |
| H.264 1280×720, explicit 2D | 150/150 frames in 5.02 s |
| SBS PNG, OU JPEG (theatre) | Correct eye in each capture |
| 3,000-Gaussian `.splat` | Rendered in about 5 s, then held until Stop |
| 2D file on Auto, `_SBS_OU` file, HEIC, VP9 | Refused with the documented message |
| Second Play while one runs | Refused: "Stop the current media…" |
Stop always left the unit inactive, and no player process remained.
**Standby (verified):** an unworn Frame turns its displays off a few seconds
after it wakes. `SetOverlayRaw` then returns `RequestFailed` (23). The
first run's movie died there. The player now drops frames while the headset
is in standby, keeps the audio and its clock going, and resumes the picture
when the headset wakes. The 8 s movie above dropped 40 frames and finished.
Stills and the theatre surround are re-sent after waking. Five minutes
without an accepted frame is reported as an error. Headset-view captures
taken during standby show a flat dark frame, not our screen.
![Media panel in Frame Control while a photo plays on the Frame](img/media-ui-panel.png)
**Verified failed route:** GStreamer 1.24.2's `playbin` selected
`v4l2h264dec`, delivered the first RGBA sample and then segfaulted (exit 139)
in the basic appsink probe and the OpenVR probe. We do not ship that route.
The ffmpeg path above completed instead.
## Limits and blockers
- **Not verified:** worn-headset comfort, sound quality/lip sync, long movies,
4K/8K decode, HDR, controller interaction or behaviour on other OS builds.
Output is bounded to 1920×1080 packed pixels before OU rearrangement.
- **Not implemented:** pause, seeking, subtitles, playlists, right-eye-first
layouts, non-square pixel correction, VR180/360 projection and fisheye.
This preview is a flat stereo screen, not a dome player.
- **Native spatial-photo blocker:** HEIC/HEIF/AVIF/MPO stereo-container
extraction isn't implemented in our viewer. We reject these rather than
displaying one image and calling it spatial. Export both eyes to PNG/JPEG
first. This is a current implementation gap, not a claim that the Frame
cannot support these containers.
- **Large/immersive splat blocker:** the owned CPU rasterizer projects 3D
covariance into each eye and alpha-composites Gaussians, but renders a
fixed view at 320×240 per eye. It caps each Gaussian footprint at 32 pixels
and normalizes the scene to a two-metre box. Large scenes, PLY/SPZ, live
head-position parallax and navigation need a GPU scene renderer; this
version rejects files above 20,000 records. It is a stereo preview, not
an immersive walk-through.
- A single player can run at a time. It stops at EOF, on **Stop**, or after
four hours in any case, so longer movies are cut off. Still images remain
until Stop or that timeout. There is no delete action yet: remove old
uploads from `~/Videos/FrameControl/` over SSH. The log is
`~/.local/share/frame-control/media/player.log` on the Frame.
- **Panel/stream theatre remains outside this media slice:** SteamVR's
`vrcmd --dock-overlay` accepts `theater`, but docking an existing panel
and its dimming behaviour were not verified here. The shared headset had
workspace and stream tests active. We did not reposition their panels.
Integration with [#22](https://github.com/saphid/frame-control/issues/22)
and [#31](https://github.com/saphid/frame-control/issues/31) can use the
owned rendering hook below; no second stream/window manager is introduced.
## Integration hook
`POST /api/upload` with `X-Mode: media` and `X-Filename` sends a file and
returns its `id`. `POST /api/media` accepts:
```json
{"action":"play","id":"<32 hex characters>/film_SBS.mp4","layout":"auto","theatre":true}
```
Other actions are `list`, `status`, `stop`. These use the normal `X-Frame-UI`
guard. Play reports **starting**, not a claim that frames reached the headset;
read status for `playing`, `ended`, `stopped` or `error`.
The own-code files are copied to `~/.local/share/frame-control/media/` and
run in the `frame-control-media.service` systemd user unit. Stop affects only
that unit, including its decoder child.
For a Frame-side stream producer, `frame_media_player.Overlay` exposes
`create(key, width, distance, stereo=False, aspect=1, order=1)`,
`pixels(handle, rgba, width, height)` and `close()`. Use an owned unique key,
pass interleaved RGBA bytes (left/right halves for stereo) and always close in
`finally`. `create` uses a head-relative transform; it does not move another
app's panel. The player demonstrates a separate black surround overlay.
`frame_media.stereo_pixels` converts OU to SBS. This is the narrow rendering
hook for stream/workspace work; it doesn't capture or manage a Mac/PC stream.
## Optional separate app
You can independently install **DeoVR Video Player** (free Steam app 837380)
if you prefer its VR180/360 features. Earlier tests on SteamOS 0.3.0,
build `20260922.6101926` (2026-09-25), verified its OpenVR initialization and
8K H.265 streamed VR180 decoding under Proton. Frame Control's media features
neither install nor launch it, and do not depend on it. Its behaviour and
file-naming conventions are not evidence about our player.
+168
View File
@@ -0,0 +1,168 @@
"""Private-data archives, run under podman unshare on the Frame. Stdlib only."""
import contextlib
import json
import os
from pathlib import Path, PurePosixPath
import shutil
import sys
import tarfile
import tempfile
import time
MAX_BYTES = 20 * 1024 ** 3
MAX_FILES = 100000
MAX_MANIFEST = 1024 * 1024
def inspect_archive(path, package, instance):
names, total, manifest = set(), 0, None
with tarfile.open(path, 'r:gz') as archive:
for member in archive:
name = member.name
parts = PurePosixPath(name).parts
if (not parts or name.startswith('/') or '..' in parts or
name != '/'.join(parts) or name in names or '\\' in name):
raise ValueError('unsafe or duplicate archive path')
if name == 'data' and not member.isdir():
raise ValueError('data root must be a directory')
names.add(name)
if len(names) > MAX_FILES or not (member.isdir() or member.isfile()):
raise ValueError('archive has too many files, links or special files')
if member.uid < 0 or member.gid < 0 or member.uid > 65535 or member.gid > 65535:
raise ValueError('archive owner outside Android user namespace')
total += member.size
if total > MAX_BYTES:
raise ValueError('archive exceeds 20 GiB')
if name == 'manifest.json' and member.isfile() and member.size <= MAX_MANIFEST:
manifest = json.load(archive.extractfile(member))
elif parts[0] != 'data':
raise ValueError('unexpected archive member')
if (not isinstance(manifest, dict) or manifest.get('format') != 1 or
manifest.get('package') != package or manifest.get('instance') != instance or
'data' not in names):
raise ValueError('backup does not match this package and instance')
return {'files': len(names) - 1, 'bytes': total, 'package': package, 'instance': instance,
'skipped_links': manifest.get('skipped_link_count', 0)}
def backup(root, package, instance, output):
import io
source = root / package
if source.is_symlink() or not source.is_dir():
raise ValueError('private app data does not exist or is a symlink')
count, total, links, skipped = 0, 0, [], 0
def checked(member):
nonlocal count, total, skipped
if member.issym(): # never followed or restored; listed in the manifest instead
skipped += 1
if len(links) < 1000:
links.append({'path': member.name[:512], 'target': member.linkname[:256]})
return None
if member.islnk(): # a second name for a file already archived: store its content again
member.type, member.linkname = tarfile.REGTYPE, ''
member.size = os.lstat(str(source / member.name[len('data/'):])).st_size
count += 1
total += member.size
if not (member.isdir() or member.isfile()) or count > MAX_FILES or total > MAX_BYTES:
raise ValueError('private data contains special files or exceeds backup limits')
return member
with tarfile.open(fileobj=output, mode='w|gz', dereference=False) as archive:
archive.add(str(source), arcname='data', filter=checked)
# Written last so that it can list what was skipped.
manifest = json.dumps({'format': 1, 'package': package, 'instance': instance,
'skipped_links': links, 'skipped_link_count': skipped}).encode()
member = tarfile.TarInfo('manifest.json')
member.size, member.mode = len(manifest), 0o600
archive.addfile(member, io.BytesIO(manifest))
def restore(root, package, instance, input_stream):
source = root / package
if source.is_symlink() or not source.is_dir():
raise ValueError('private app data does not exist or is a symlink')
with tempfile.TemporaryDirectory(prefix='.frame-restore-', dir=str(root)) as work:
work = Path(work)
archive_path = work / 'backup.tar.gz'
with archive_path.open('wb') as output:
size = 0
while True:
chunk = input_stream.read(1024 * 1024)
if not chunk:
break
size += len(chunk)
if size > MAX_BYTES:
raise ValueError('compressed backup exceeds 20 GiB')
output.write(chunk)
result = inspect_archive(archive_path, package, instance)
stage = work / 'stage'
stage.mkdir(mode=0o700)
with tarfile.open(archive_path, 'r:gz') as archive:
directories = []
for member in archive:
if member.name == 'manifest.json':
continue
target = stage / member.name
if member.isdir():
target.mkdir(parents=True, exist_ok=True)
directories.append((target, member))
else:
target.parent.mkdir(parents=True, exist_ok=True)
with archive.extractfile(member) as src, target.open('xb') as dst:
shutil.copyfileobj(src, dst, 1024 * 1024)
apply_metadata(target, member)
for target, member in reversed(directories):
apply_metadata(target, member)
with package_lock(root, package): # another restore of this package must not delete our copy
previous = root / ('.' + package + '.before-restore-' + str(time.time_ns()))
source.rename(previous)
try:
(stage / 'data').rename(source)
except BaseException:
previous.rename(source)
raise
# Keep only the newest pre-restore copy of this package's data.
for old in root.glob('.' + package + '.before-restore-*'):
if old != previous and not old.is_symlink():
shutil.rmtree(str(old), ignore_errors=True)
result['previous'] = str(previous)
return result
@contextlib.contextmanager
def package_lock(root, package):
import fcntl
fd = os.open(str(root / ('.' + package + '.restore.lock')), os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
yield
finally:
os.close(fd) # releases the lock
def apply_metadata(path, member):
os.chown(str(path), member.uid, member.gid)
os.chmod(str(path), member.mode & 0o777)
os.utime(str(path), (member.mtime, member.mtime))
def main():
action, package, instance = sys.argv[1:]
instance = int(instance)
root = Path.home() / '.local/share/Steam/steamapps/compatdata' / str(instance) / 'internal'
if root.is_symlink() or root.resolve() != root.absolute():
raise ValueError('private-data directory traverses a symlink')
if action == 'backup':
backup(root, package, instance, sys.stdout.buffer)
elif action == 'restore':
print(json.dumps(restore(root, package, instance, sys.stdin.buffer)))
else:
raise ValueError('unknown app-data action')
if __name__ == '__main__':
try:
main()
except (OSError, ValueError, tarfile.TarError) as error:
sys.exit(str(error))
+45 -1
View File
@@ -19,6 +19,25 @@ done
# A number that isn't a real Steam app; it names this app's Lepton context.
export SteamAppId="$(cat "$DIR/instance.id")"
[[ "$SteamAppId" =~ ^[0-9]+$ ]] || { echo "invalid instance.id" >&2; exit 1; }
# Keep the stable Lepton context, but identify the Android VR client as its
# actual Steam shortcut. Lepton applies LEPTON_ENV_* after its own passthrough.
if [[ -f "$DIR/shortcut.id" ]]; then
shortcut="$(cat "$DIR/shortcut.id")"
[[ "$shortcut" =~ ^[0-9]+$ ]] || { echo "invalid shortcut.id" >&2; exit 1; }
export LEPTON_ENV_SteamAppId="$shortcut"
fi
exec 9>"$DIR/launch.lock"
flock -n 9 || { echo "Android app is already running" >&2; exit 1; }
CONTAINER="lepton-steamlaunch-$SteamAppId"
# Holding the lock means no launcher owns a running container: it was orphaned
# (this script SIGKILLed), so stop it rather than refuse every later Play. The
# name is this app's alone. A Lepton host process whose launcher was killed
# before it made the container may linger briefly; nothing else is killed.
if [[ "$(podman inspect --format '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)" == true ]]; then
echo "Stopping orphaned $CONTAINER" >&2
podman stop -t 5 "$CONTAINER" >/dev/null 2>&1 || true
fi
export STEAM_COMPAT_INSTALL_PATH="$DIR"
# Must be under ~/.local/share/Steam: only that tree is mounted in the container.
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
@@ -29,4 +48,29 @@ mkdir -p "$STEAM_COMPAT_DATA_PATH" "$STEAM_FOSSILIZE_DUMP_PATH"
# Lepton's setpgid --foreground re-exec needs a terminal that Steam shortcuts
# and SSH don't have; give it its own session instead.
export IS_PARENT=true
exec setsid --wait "$LEPTON" waitforexitandrun -- "$DIR/app.apk"
# Keep this shell in Steam's process tree; setsid alone has no container cleanup.
child=""
cleanup() {
trap '' TERM INT HUP
if [[ -n "$child" ]]; then
kill -TERM -- "-$child" 2>/dev/null || true
kill -TERM "$child" 2>/dev/null || true
fi
podman stop -t 5 "$CONTAINER" >/dev/null 2>&1 || true
if [[ -n "$child" ]]; then
kill -KILL -- "-$child" 2>/dev/null || true
kill -KILL "$child" 2>/dev/null || true
wait "$child" 2>/dev/null || true
fi
}
trap cleanup EXIT
trap 'exit 143' TERM
trap 'exit 130' INT
trap 'exit 129' HUP
# 9>&-: the lock is this launcher's alone; Lepton's tree mustn't keep it held.
setsid --wait "$LEPTON" waitforexitandrun -- "$DIR/app.apk" 9>&- &
child=$!
rc=0
wait "$child" || rc=$?
child=""
exit "$rc"
+146
View File
@@ -0,0 +1,146 @@
// Runs in Steam's Chromium context: identical fonts/rendering from every host OS.
async function renderLibraryArtwork(input) {
const sizes = {grid:[600,900], wide:[920,430], hero:[3840,1240], logo:[1280,480], icon:[256,256]};
const label = String(input.label || 'Untitled').trim().slice(0,180);
const font = '"Motiva Sans", "Noto Sans", Arial, sans-serif';
await document.fonts.load(`800 120px ${font}`, label);
const images = {}, warnings = [];
for (const [slot, item] of Object.entries(input.images || {})) {
try {
const img = new Image();
img.src = `data:image/${item[0]};base64,${item[1]}`;
await img.decode();
if (!img.width || !img.height || img.width*img.height > 16777216) throw Error('dimensions');
images[slot] = img;
} catch (_) { warnings.push(`${slot} could not be decoded; generated art used`); }
}
let icon = images.icon;
if (icon) {
// Remove only a near-black matte connected to the outside of an opaque icon.
const cut=document.createElement('canvas');cut.width=icon.width;cut.height=icon.height;
const c=cut.getContext('2d');c.drawImage(icon,0,0);
const pixels=c.getImageData(0,0,cut.width,cut.height), d=pixels.data, w=cut.width,h=cut.height;
const corners=[0,w-1,(h-1)*w,h*w-1];
if(corners.every(i=>d[i*4+3]>250 && Math.max(d[i*4],d[i*4+1],d[i*4+2])<24)) {
const seen=new Uint8Array(w*h), queue=corners.slice();
for(let q=0;q<queue.length;q++) {
const i=queue[q];if(seen[i])continue;seen[i]=1;
if(Math.max(d[i*4],d[i*4+1],d[i*4+2])>24)continue;
d[i*4+3]=0;
if(i%w)queue.push(i-1);if(i%w<w-1)queue.push(i+1);
if(i>=w)queue.push(i-w);if(i<w*(h-1))queue.push(i+w);
}
c.putImageData(pixels,0,0);icon=cut;
}
}
// Quantized, saturated dominant colors avoid white/black icon margins.
let colors = [[48,91,137], [24,36,63]];
if (icon) {
const sample = document.createElement('canvas'); sample.width=48; sample.height=48;
const s=sample.getContext('2d'); s.drawImage(icon,0,0,48,48);
const data=s.getImageData(0,0,48,48).data, bins=new Map();
for (let i=0;i<data.length;i+=4) {
const rgb=[data[i],data[i+1],data[i+2]], hi=Math.max(...rgb), lo=Math.min(...rgb);
if (data[i+3]<150 || hi<45 || lo>220 || hi-lo<25) continue;
const key=rgb.map(v=>Math.round(v/32)*32).join(',');
bins.set(key,(bins.get(key)||0)+1);
}
const ranked=[...bins].sort((a,b)=>b[1]-a[1]);
if (ranked.length) {
colors[0]=ranked[0][0].split(',').map(Number);
colors[1]=(ranked.find(([key])=>key.split(',').reduce((n,v,i)=>n+Math.abs(Number(v)-colors[0][i]),0)>170)||ranked[0])[0].split(',').map(Number);
}
}
// Preserve hue while lifting muted icon colors into a richer background palette.
colors=colors.map(c=>{const low=Math.min(...c),range=Math.max(...c)-low||1;
return c.map(v=>45+(v-low)/range*165);});
const rgb=(c,a=1)=>`rgba(${c.map(v=>Math.min(255,Math.round(v))).join(',')},${a})`;
function image(ctx,img,x,y,w,h,cover=false) {
const scale=cover?Math.max(w/img.width,h/img.height):Math.min(w/img.width,h/img.height);
const dw=img.width*scale,dh=img.height*scale;
ctx.save(); ctx.beginPath(); ctx.rect(x,y,w,h); ctx.clip();
ctx.drawImage(img,x+(w-dw)/2,y+(h-dh)/2,dw,dh); ctx.restore();
}
function title(ctx,w,h,top,bottom,maxSize) {
let lines=[],size=maxSize;
const maxWidth=w*.84;
for (;size>=18;size-=2) {
ctx.font=`800 ${size}px ${font}`;
lines=[]; let line='';
for (const word of label.split(/\s+/)) {
const next=line?line+' '+word:word;
if (line && ctx.measureText(next).width>maxWidth) {lines.push(line);line=word;} else line=next;
}
lines.push(line);
if (lines.length*size*1.08<=bottom-top && lines.every(l=>ctx.measureText(l).width<=maxWidth)) break;
}
if(lines.length===2) {
const words=lines[0].split(' ');
if(words.length>1) {
const first=words.slice(0,-1).join(' '), second=words.slice(-1)[0]+' '+lines[1];
if(ctx.measureText(second).width<=maxWidth &&
Math.abs(ctx.measureText(first).width-ctx.measureText(second).width)<
Math.abs(ctx.measureText(lines[0]).width-ctx.measureText(lines[1]).width)) lines=[first,second];
}
}
// A long unbroken label is still fitted, including scripts without spaces.
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillStyle='#fff';
ctx.shadowColor='rgba(0,0,0,.45)'; ctx.shadowBlur=size*.28; ctx.shadowOffsetY=size*.06;
let y=top+(bottom-top-lines.length*size*1.08)/2+size*.54;
for (const line of lines) {ctx.fillText(line,w/2,y,maxWidth); y+=size*1.08;}
ctx.shadowBlur=0; ctx.shadowOffsetY=0;
}
const result={};
for (const [slot,[w,h]] of Object.entries(sizes)) {
const canvas=document.createElement('canvas'); canvas.width=w; canvas.height=h;
const ctx=canvas.getContext('2d'); ctx.imageSmoothingQuality='high';
const direct=images[slot];
const feature=images.feature_graphic||images.banner;
const scene=direct || ((slot==='hero'||slot==='wide') && (feature||images.screenshot));
if (scene) {
if (slot==='logo'||slot==='icon') image(ctx,scene,0,0,w,h);
else image(ctx,scene,0,0,w,h,true);
} else if (slot==='logo') {
title(ctx,w,h,h*.08,h*.92,150);
} else {
const gradient=ctx.createLinearGradient(0,0,w,h);
gradient.addColorStop(0,rgb(colors[0].map(v=>v*.68)));
gradient.addColorStop(.6,rgb(colors[1].map(v=>v*.32)));
gradient.addColorStop(1,'#080c16'); ctx.fillStyle=gradient;ctx.fillRect(0,0,w,h);
if (icon) {
ctx.save();ctx.globalAlpha=.16;ctx.filter=`blur(${Math.round(w*.055)}px) saturate(1.4)`;
image(ctx,icon,-w*.15,-h*.15,w*1.3,h*1.3,true);ctx.restore();
}
const glow=ctx.createRadialGradient(w*.5,h*.32,0,w*.5,h*.32,w*.8);
glow.addColorStop(0,rgb(colors[0],.27));glow.addColorStop(1,rgb(colors[1],0));
ctx.fillStyle=glow;ctx.fillRect(0,0,w,h);
const vignette=ctx.createLinearGradient(0,h*.25,0,h);
vignette.addColorStop(0,'rgba(0,0,0,0)');vignette.addColorStop(1,'rgba(0,0,0,.56)');
ctx.fillStyle=vignette;ctx.fillRect(0,0,w,h);
const box=slot==='grid'?[w*.12,h*.14,w*.76,w*.76]:
slot==='wide'?[w*.36,h*.06,w*.28,h*.59]:
slot==='hero'?[w*.365,h*.12,w*.27,h*.78]:[w*.08,h*.08,w*.84,h*.84];
if (icon) {
ctx.save();ctx.shadowColor='rgba(0,0,0,.65)';ctx.shadowBlur=Math.min(w,h)*.055;
ctx.shadowOffsetY=Math.min(w,h)*.022;
// Opaque square icons read as deliberate app tiles, not pasted rectangles.
const [x,y,bw,bh]=box, side=Math.min(bw,bh);
if(slot!=='hero' && icon===images.icon) {
ctx.beginPath();ctx.roundRect(x+(bw-side)/2,y+(bh-side)/2,side,side,side*.14);ctx.clip();
}
image(ctx,icon,...box);ctx.restore();
} else if (slot !== 'hero') {
// A typographic monogram when the APK contains no usable image.
ctx.font=`800 ${Math.min(w,h)*.48}px ${font}`;ctx.fillStyle='rgba(255,255,255,.94)';
ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText([...label][0]||'A',w/2,h*.38);
}
if (slot==='grid') title(ctx,w,h,h*.7,h*.93,66);
if (slot==='wide') title(ctx,w,h,h*.69,h*.92,52);
// Hero intentionally has no title: Steam overlays the transparent logo.
}
// Photos as PNG can pass Steam's 12 MiB limit at hero size; the logo keeps its transparency.
const jpeg=scene && slot!=='logo' && slot!=='icon';
result[slot]=[jpeg?'jpg':'png', canvas.toDataURL(jpeg?'image/jpeg':'image/png',.9).split(',')[1]];
}
return {images:result,warnings,font};
}
+229 -9
View File
@@ -5,9 +5,11 @@ Python stdlib only; the Mac runs it with `ssh frame python3 - <args> < this`.
steam_shortcuts.py add NAME EXE START_DIR [ICON] -> prints the shortcut app id
steam_shortcuts.py list -> JSON [{appid, name, exe}]
steam_shortcuts.py configure APPID NAME EXE START_DIR ICON VR ARTWORK_JSON
steam_shortcuts.py stop APPID
steam_shortcuts.py remove APPID
"""
import base64, json, os, socket, struct, sys, urllib.request
import base64, glob, json, os, re, socket, struct, sys, urllib.request
DEVTOOLS = 'http://127.0.0.1:8080/json'
@@ -22,10 +24,10 @@ def target_ws():
class WS:
"""Just enough RFC 6455 for one CDP request/response on loopback."""
def __init__(self, url):
def __init__(self, url, timeout=20):
host_port, path = url[len('ws://'):].split('/', 1)
host, port = host_port.split(':')
self.s = socket.create_connection((host, int(port)), timeout=20)
self.s = socket.create_connection((host, int(port)), timeout=timeout)
key = base64.b64encode(os.urandom(16)).decode()
self.s.sendall((f'GET /{path} HTTP/1.1\r\nHost: {host_port}\r\nUpgrade: websocket\r\n'
f'Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n'
@@ -69,8 +71,8 @@ class WS:
return msg.decode()
def evaluate(js):
ws = WS(target_ws())
def evaluate(js, timeout=20):
ws = WS(target_ws(), timeout)
ws.send(json.dumps({'id': 1, 'method': 'Runtime.evaluate', 'params': {
'expression': js, 'awaitPromise': True, 'returnByValue': True}}))
while True:
@@ -83,6 +85,206 @@ def evaluate(js):
return res.get('result', {}).get('value')
# Steam's ELibraryAssetType (Capsule, Hero, Logo, Header, Icon).
ASSETS = {'grid': 0, 'hero': 1, 'logo': 2, 'wide': 3, 'icon': 4}
def collections_js(appid, wanted=()):
wanted = list(wanted)
return f'''async function syncCollections() {{
const wanted = {json.dumps(wanted)};
if (typeof collectionStore === "undefined" ||
typeof collectionStore.GetUserCollectionsByName !== "function" ||
typeof collectionStore.NewUnsavedCollection !== "function" ||
typeof collectionStore.SaveCollection !== "function")
return ["Steam collections API unavailable"];
const app = {{appid: {appid}}};
const warnings = [];
for (const name of ["Android", "Android VR", "Sideloaded"]) {{
const matches = collectionStore.GetUserCollectionsByName(name);
let collection = matches.find(c => !c.bIsDynamic && c.bAllowsDragAndDrop);
if (wanted.includes(name)) {{
if (!collection && matches.length) {{
warnings.push(name + " is an existing dynamic or read-only collection");
continue;
}}
if (!collection) {{
collection = collectionStore.NewUnsavedCollection(name, undefined, [app]);
}} else {{
collection.AsDragDropCollection().AddApps([app]);
}}
await collectionStore.SaveCollection(collection);
}} else if (collection) {{
collection.AsDragDropCollection().RemoveApps([app]);
await collectionStore.SaveCollection(collection);
}}
}}
return warnings;
}}'''
def notes_js(name, details):
filename = 'notes_shortcut_' + re.sub(r'[!-/:-@ \[\\\]\^`]', '_', name.strip())
content = '\n'.join(str(details[k]) for k in ('package', 'version', 'source') if details.get(k))
return f'''if (SteamClient.GameNotes && typeof SteamClient.GameNotes.GetNotes === "function" &&
typeof SteamClient.GameNotes.SaveNotes === "function") {{
try {{
const file = {json.dumps(filename)};
const previous = await SteamClient.GameNotes.GetNotes(file, file + "_images/");
if (previous.result !== 1 && previous.result !== 9) throw Error("read " + previous.result);
const data = previous.result === 1 ? JSON.parse(previous.notes) : {{notes: [], shortcut_name: {json.dumps(name)}}};
if (!Array.isArray(data.notes)) throw Error("unexpected notes format");
const id = "frame-control-library", now = Math.floor(Date.now()/1000);
const old = data.notes.find(n => n.id === id);
const note = {{id, shortcut_name: {json.dumps(name)}, title: "Installation details",
content: {json.dumps(content)}, ordinal: old ? old.ordinal : data.notes.length,
time_created: old ? old.time_created : now, time_modified: now}};
data.notes = data.notes.filter(n => n.id !== id).concat([note]);
const result = await SteamClient.GameNotes.SaveNotes(file, JSON.stringify(data));
if (result !== 1) throw Error("save " + result);
}} catch (e) {{ warnings.push("Steam notes: " + String(e)); }}
}}'''
MAX_ART = 12 * 1024 * 1024 # Steam's custom artwork limit per slot
def render(plan):
with open(plan) as f:
source = json.load(f)
images = {}
for slot, path in source['images'].items():
ext = os.path.splitext(path)[1][1:]
with open(path, 'rb') as f:
data = f.read(MAX_ART + 1)
if len(data) > MAX_ART:
raise ValueError('source artwork too large')
images[slot] = [ext, base64.b64encode(data).decode()]
renderer = globals().get('ART_RENDERER')
if renderer is None:
with open(os.path.join(os.path.dirname(__file__), 'library_artwork.js')) as f:
renderer = f.read()
try:
return _render(plan, renderer, source['label'], images)
except (ValueError, OSError, EOFError, SystemExit) as e:
# Generated art from the icon alone always fits; a photo that didn't must not fail the install.
result = _render(plan, renderer, source['label'], {k: v for k, v in images.items() if k == 'icon'})
result['warnings'].insert(0, 'Source artwork could not be rendered (' + str(e)[:120] + '); generated art used')
return result
def _render(plan, renderer, label, images):
# A 4K photo takes seconds to decode and encode on the Frame; allow well beyond that.
result = evaluate(renderer + '\nrenderLibraryArtwork(' + json.dumps({'label': label, 'images': images}) + ')',
timeout=75)
if not isinstance(result, dict) or set(result.get('images', {})) != set(ASSETS):
raise ValueError('incomplete artwork render')
paths = {}
for slot, (ext, encoded) in result['images'].items():
data = base64.b64decode(encoded, validate=True)
signature = {'png': b'\x89PNG\r\n\x1a\n', 'jpg': b'\xff\xd8\xff'}.get(ext)
if not signature or not data.startswith(signature) or len(data) > MAX_ART:
raise ValueError(slot + ' render is ' + str(len(data)) + ' bytes of ' + str(ext))
paths[slot] = os.path.join(os.path.dirname(plan), slot + '.' + ext)
with open(paths[slot] + '.tmp', 'wb') as f:
f.write(data)
for slot, path in paths.items():
os.replace(path + '.tmp', path)
for stale in ('png', 'jpg'):
other = os.path.join(os.path.dirname(plan), slot + '.' + stale)
if other != path and os.path.exists(other):
os.remove(other)
return {'paths': paths, 'warnings': list(result.get('warnings', []))}
# Steam's own file for each custom-art type in userdata/*/config/grid/.
GRID_FILES = {0: 'p', 1: '_hero', 2: '_logo', 3: ''}
def custom_art(appid):
"""The custom-art types this shortcut already has in Steam, for any local user."""
found = set()
for kind, suffix in GRID_FILES.items():
pattern = os.path.expanduser(f'~/.local/share/Steam/userdata/*/config/grid/{int(appid)}{suffix}.*')
if any(os.path.splitext(p)[1].lower() in ('.png', '.jpg', '.jpeg') for p in glob.glob(pattern)):
found.add(kind)
return found
def configure(appid, name, exe, start_dir, icon, vr, artwork, options=None):
"""options: category, details, fill_only (only empty slots and a missing icon; name and flags untouched)."""
options = options or {}
category = options.get('category', 'Android')
fill = bool(options.get('fill_only'))
existing = custom_art(appid) if fill else set()
if set(artwork) != set(ASSETS):
raise ValueError('all five Steam artwork slots are required')
images = []
for slot, path in artwork.items():
if slot not in ASSETS:
raise ValueError('unknown artwork slot')
ext = os.path.splitext(path)[1][1:]
if ext not in ('png', 'jpg'):
raise ValueError('artwork must be PNG or JPEG')
with open(path, 'rb') as f:
data = f.read(MAX_ART + 1)
if len(data) > MAX_ART:
raise ValueError('artwork is too large')
# Frame's custom-art API maps type 4 to Header; use SetShortcutIcon.
if slot != 'icon' and ASSETS[slot] not in existing:
images.append([ASSETS[slot], ext, base64.b64encode(data).decode()])
return evaluate(f'''(async () => {{
const id = {int(appid)}, warnings = [], fill = {json.dumps(fill)};
const overview = appStore.GetAppOverviewByAppID(id);
if (!fill) {{
SteamClient.Apps.SetShortcutName(id, {json.dumps(name)});
if ({json.dumps(exe)}) SteamClient.Apps.SetShortcutExe(id, {json.dumps(exe)});
if ({json.dumps(start_dir)}) SteamClient.Apps.SetShortcutStartDir(id, {json.dumps(start_dir)});
if (typeof SteamClient.Apps.SetShortcutSortAs === "function")
SteamClient.Apps.SetShortcutSortAs(id, {json.dumps(name)});
}}
if (!fill || !(overview && overview.icon_data)) SteamClient.Apps.SetShortcutIcon(id, {json.dumps(icon)});
// null (devkit titles) or filling gaps: leave the VR flag as Steam has it.
if ({json.dumps(vr)} !== null && !fill) {{
if (typeof SteamClient.Apps.SetShortcutIsVR === "function")
SteamClient.Apps.SetShortcutIsVR(id, {json.dumps(vr)});
else warnings.push("Steam VR shortcut flag API unavailable");
}}
if (typeof SteamClient.Apps.SetCustomArtworkForApp === "function") {{
for (const [type, ext, data] of {json.dumps(images)}) {{
// Steam keeps a slot's PNG and JPEG side by side; clear it so a stale one can't win.
if (!fill && typeof SteamClient.Apps.ClearCustomArtworkForApp === "function")
try {{ await SteamClient.Apps.ClearCustomArtworkForApp(id, type); }} catch (e) {{}}
await SteamClient.Apps.SetCustomArtworkForApp(id, data, ext, type);
}}
}} else throw new Error("Steam artwork API unavailable; installation is incomplete");
{collections_js(int(appid), [category] + (['Android VR'] if vr and category == 'Android' else []))}
try {{ warnings.push(...await syncCollections()); }}
catch (e) {{ warnings.push("Steam collections: " + String(e)); }}
{notes_js(name, options.get('details', {}))}
return {{warnings}};
}})()''', timeout=60)
def remove(appid):
# Collections and artwork are tidy-up: only a missing RemoveShortcut may fail the removal.
return evaluate(f'''(async () => {{
const id = {int(appid)}, warnings = [];
{collections_js(int(appid))}
try {{ warnings.push(...await syncCollections()); }}
catch (e) {{ warnings.push("Steam collections: " + String(e)); }}
if (typeof SteamClient.Apps.ClearCustomArtworkForApp === "function") {{
for (const type of [0, 1, 2, 3]) {{
try {{ await SteamClient.Apps.ClearCustomArtworkForApp(id, type); }}
catch (e) {{ warnings.push("Steam artwork " + type + ": " + String(e)); }}
}}
}} else warnings.push("Steam artwork removal API unavailable");
SteamClient.Apps.RemoveShortcut(id);
return {{warnings}};
}})()''')
def main():
cmd, args = sys.argv[1], sys.argv[2:]
if cmd == 'add':
@@ -97,12 +299,30 @@ def main():
}})()'''
print(evaluate(js))
elif cmd == 'list':
js = '''(() => appStore.allApps.filter(a => a.app_type === 1073741824)
.map(a => ({appid: a.appid, name: a.display_name})))()'''
# Overviews carry no exe or devkit id (checked 2026-09-28); app details do, once registered.
js = '''(async () => Promise.all(appStore.allApps.filter(a => a.app_type === 1073741824).map(async a => {
let d = typeof appDetailsStore !== "undefined" && appDetailsStore.GetAppDetails(a.appid);
if (!d && typeof SteamClient.Apps.RegisterForAppDetails === "function") d = await new Promise(ok => {
let reg;
const timer = setTimeout(() => { if (reg) reg.unregister(); ok(null); }, 3000);
reg = SteamClient.Apps.RegisterForAppDetails(a.appid, x => {
clearTimeout(timer); setTimeout(() => reg && reg.unregister()); ok(x); });
});
return {appid: a.appid, name: a.display_name, devkit_gameid: a.devkit_gameid,
exe: d ? d.strShortcutExe || "" : "", start_dir: d ? d.strShortcutStartDir || "" : ""};
})))()'''
print(json.dumps(evaluate(js)))
elif cmd == 'render':
print(json.dumps(render(args[0])))
elif cmd == 'configure':
vr = {'1': True, '0': False}.get(args[5]) # '' leaves Steam's VR flag alone
print(json.dumps(configure(int(args[0]), *args[1:5], vr, json.loads(args[6]),
json.loads(args[7]) if len(args) > 7 else None)))
elif cmd == 'stop':
evaluate(f'SteamClient.Apps.TerminateApp({json.dumps(str((int(args[0]) << 32) | 0x02000000))}, false)')
print('stopping')
elif cmd == 'remove':
evaluate(f'SteamClient.Apps.RemoveShortcut({int(args[0])})')
print('removed')
print(json.dumps(remove(int(args[0]))))
else:
sys.exit(__doc__)
@@ -0,0 +1,26 @@
Copyright (c) <year> <owner>. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
@@ -0,0 +1,446 @@
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
[This is the first released version of the library GPL. It is numbered 2 because
it goes with version 2 of the ordinary GPL.]
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public Licenses are intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users.
This license, the Library General Public License, applies to some specially
designated Free Software Foundation software, and to any other libraries whose
authors decide to use it. You can use it for your libraries, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for this service if you
wish), that you receive source code or can get it if you want it, that you
can change the software or use pieces of it in new free programs; and that
you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to
deny you these rights or to ask you to surrender the rights. These restrictions
translate to certain responsibilities for you if you distribute copies of
the library, or if you modify it.
For example, if you distribute copies of the library, whether gratis or for
a fee, you must give the recipients all the rights that we gave you. You must
make sure that they, too, receive or can get the source code. If you link
a program with the library, you must provide complete object files to the
recipients so that they can relink them with the library, after making changes
to the library and recompiling it. And you must show them these terms so they
know their rights.
Our method of protecting your rights has two steps: (1) copyright the library,
and (2) offer you this license which gives you legal permission to copy, distribute
and/or modify the library.
Also, for each distributor's protection, we want to make certain that everyone
understands that there is no warranty for this free library. If the library
is modified by someone else and passed on, we want its recipients to know
that what they have is not the original version, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We
wish to avoid the danger that companies distributing free software will individually
obtain patent licenses, thus in effect transforming the program into proprietary
software. To prevent this, we have made it clear that any patent must be licensed
for everyone's free use or not licensed at all.
Most GNU software, including some libraries, is covered by the ordinary GNU
General Public License, which was designed for utility programs. This license,
the GNU Library General Public License, applies to certain designated libraries.
This license is quite different from the ordinary one; be sure to read it
in full, and don't assume that anything in it is the same as in the ordinary
license.
The reason we have a separate public license for some libraries is that they
blur the distinction we usually make between modifying or adding to a program
and simply using it. Linking a program with a library, without changing the
library, is in some sense simply using the library, and is analogous to running
a utility program or application program. However, in a textual and legal
sense, the linked executable is a combined work, a derivative of the original
library, and the ordinary General Public License treats it as such.
Because of this blurred distinction, using the ordinary General Public License
for libraries did not effectively promote software sharing, because most developers
did not use the libraries. We concluded that weaker conditions might promote
sharing better.
However, unrestricted linking of non-free programs would deprive the users
of those programs of all benefit from the free status of the libraries themselves.
This Library General Public License is intended to permit developers of non-free
programs to use free libraries, while preserving your freedom as a user of
such programs to change the free libraries that are incorporated in them.
(We have not seen how to achieve this as regards changes in header files,
but we have achieved it as regards changes in the actual functions of the
Library.) The hope is that this will lead to faster development of free libraries.
The precise terms and conditions for copying, distribution and modification
follow. Pay close attention to the difference between a "work based on the
library" and a "work that uses the library". The former contains code derived
from the library, while the latter only works together with the library.
Note that it is possible for a library to be covered by the ordinary General
Public License rather than by this special one.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which contains a
notice placed by the copyright holder or other authorized party saying it
may be distributed under the terms of this Library General Public License
(also called "this License"). Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data prepared
so as to be conveniently linked with application programs (which use some
of those functions and data) to form executables.
The "Library", below, refers to any such software library or work which has
been distributed under these terms. A "work based on the Library" means either
the Library or any derivative work under copyright law: that is to say, a
work containing the Library or a portion of it, either verbatim or with modifications
and/or translated straightforwardly into another language. (Hereinafter, translation
is included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for making modifications
to it. For a library, complete source code means all the source code for all
modules it contains, plus any associated interface definition files, plus
the scripts used to control compilation and installation of the library.
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running a program
using the Library is not restricted, and output from such a program is covered
only if its contents constitute a work based on the Library (independent of
the use of the Library in a tool for writing it). Whether that is true depends
on what the Library does and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's complete source
code as you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and disclaimer
of warranty; keep intact all the notices that refer to this License and to
the absence of any warranty; and distribute a copy of this License along with
the Library.
You may charge a fee for the physical act of transferring a copy, and you
may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Library or any portion of it,
thus forming a work based on the Library, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all
of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices stating that
you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no charge to all
third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a table of
data to be supplied by an application program that uses the facility, other
than as an argument passed when the facility is invoked, then you must make
a good faith effort to ensure that, in the event an application does not supply
such function or table, the facility still operates, and performs whatever
part of its purpose remains meaningful.
(For example, a function in a library to compute square roots has a purpose
that is entirely well-defined independent of the application. Therefore, Subsection
2d requires that any application-supplied function or table used by this function
must be optional: if the application does not supply it, the square root function
must still compute square roots.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Library, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Library, the distribution of the whole must be
on the terms of this License, whose permissions for other licensees extend
to the entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works based
on the Library.
In addition, mere aggregation of another work not based on the Library with
the Library (or with a work based on the Library) on a volume of a storage
or distribution medium does not bring the other work under the scope of this
License.
3. You may opt to apply the terms of the ordinary GNU General Public License
instead of this License to a given copy of the Library. To do this, you must
alter all the notices that refer to this License, so that they refer to the
ordinary GNU General Public License, version 2, instead of to this License.
(If a newer version than version 2 of the ordinary GNU General Public License
has appeared, then you can specify that version instead if you wish.) Do not
make any other change in these notices.
Once this change is made in a given copy, it is irreversible for that copy,
so the ordinary GNU General Public License applies to all subsequent copies
and derivative works made from that copy.
This option is useful when you wish to copy part of the code of the Library
into a program that is not a library.
4. You may copy and distribute the Library (or a portion or derivative of
it, under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you accompany it with the complete corresponding
machine-readable source code, which must be distributed under the terms of
Sections 1 and 2 above on a medium customarily used for software interchange.
If distribution of object code is made by offering access to copy from a designated
place, then offering equivalent access to copy the source code from the same
place satisfies the requirement to distribute the source code, even though
third parties are not compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the Library, but
is designed to work with the Library by being compiled or linked with it,
is called a "work that uses the Library". Such a work, in isolation, is not
a derivative work of the Library, and therefore falls outside the scope of
this License.
However, linking a "work that uses the Library" with the Library creates an
executable that is a derivative of the Library (because it contains portions
of the Library), rather than a "work that uses the library". The executable
is therefore covered by this License. Section 6 states terms for distribution
of such executables.
When a "work that uses the Library" uses material from a header file that
is part of the Library, the object code for the work may be a derivative work
of the Library even though the source code is not. Whether this is true is
especially significant if the work can be linked without the Library, or if
the work is itself a library. The threshold for this to be true is not precisely
defined by law.
If such an object file uses only numerical parameters, data structure layouts
and accessors, and small macros and small inline functions (ten lines or less
in length), then the use of the object file is unrestricted, regardless of
whether it is legally a derivative work. (Executables containing this object
code plus portions of the Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may distribute
the object code for the work under the terms of Section 6. Any executables
containing that work also fall under Section 6, whether or not they are linked
directly with the Library itself.
6. As an exception to the Sections above, you may also compile or link a "work
that uses the Library" with the Library to produce a work containing portions
of the Library, and distribute that work under terms of your choice, provided
that the terms permit modification of the work for the customer's own use
and reverse engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the Library
is used in it and that the Library and its use are covered by this License.
You must supply a copy of this License. If the work during execution displays
copyright notices, you must include the copyright notice for the Library among
them, as well as a reference directing the user to the copy of this License.
Also, you must do one of these things:
a) Accompany the work with the complete corresponding machine-readable source
code for the Library including whatever changes were used in the work (which
must be distributed under Sections 1 and 2 above); and, if the work is an
executable linked with the Library, with the complete machine-readable "work
that uses the Library", as object code and/or source code, so that the user
can modify the Library and then relink to produce a modified executable containing
the modified Library. (It is understood that the user who changes the contents
of definitions files in the Library will not necessarily be able to recompile
the application to use the modified definitions.)
b) Accompany the work with a written offer, valid for at least three years,
to give the same user the materials specified in Subsection 6a, above, for
a charge no more than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy from a designated
place, offer equivalent access to copy the above specified materials from
the same place.
d) Verify that the user has already received a copy of these materials or
that you have already sent this user a copy.
For an executable, the required form of the "work that uses the Library" must
include any data and utility programs needed for reproducing the executable
from it. However, as a special exception, the source code distributed need
not include anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the operating
system on which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license restrictions of
other proprietary libraries that do not normally accompany the operating system.
Such a contradiction means you cannot use both them and the Library together
in an executable that you distribute.
7. You may place library facilities that are a work based on the Library side-by-side
in a single library together with other library facilities not covered by
this License, and distribute such a combined library, provided that the separate
distribution of the work based on the Library and of the other library facilities
is otherwise permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work based on the
Library, uncombined with any other library facilities. This must be distributed
under the terms of the Sections above.
b) Give prominent notice with the combined library of the fact that part of
it is a work based on the Library, and explaining where to find the accompanying
uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute the Library
except as expressly provided under this License. Any attempt otherwise to
copy, modify, sublicense, link with, or distribute the Library is void, and
will automatically terminate your rights under this License. However, parties
who have received copies, or rights, from you under this License will not
have their licenses terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not signed
it. However, nothing else grants you permission to modify or distribute the
Library or its derivative works. These actions are prohibited by law if you
do not accept this License. Therefore, by modifying or distributing the Library
(or any work based on the Library), you indicate your acceptance of this License
to do so, and all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the Library),
the recipient automatically receives a license from the original licensor
to copy, distribute, link with or modify the Library subject to these terms
and conditions. You may not impose any further restrictions on the recipients'
exercise of the rights granted herein. You are not responsible for enforcing
compliance by third parties to this License.
11. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are imposed
on you (whether by court order, agreement or otherwise) that contradict the
conditions of this License, they do not excuse you from the conditions of
this License. If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations, then as
a consequence you may not distribute the Library at all. For example, if a
patent license would not permit royalty-free redistribution of the Library
by all those who receive copies directly or indirectly through you, then the
only way you could satisfy both it and this License would be to refrain entirely
from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system which is implemented by public license practices.
Many people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose
that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in certain
countries either by patents or by copyrighted interfaces, the original copyright
holder who places the Library under this License may add an explicit geographical
distribution limitation excluding those countries, so that distribution is
permitted only in or among countries not thus excluded. In such case, this
License incorporates the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new versions of
the Library General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to address
new problems or concerns.
Each version is given a distinguishing version number. If the Library specifies
a version number of this License which applies to it and "any later version",
you have the option of following the terms and conditions either of that version
or of any later version published by the Free Software Foundation. If the
Library does not specify a license version number, you may choose any version
ever published by the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free programs
whose distribution conditions are incompatible with these, write to the author
to ask for permission. For software which is copyrighted by the Free Software
Foundation, write to the Free Software Foundation; we sometimes make exceptions
for this. Our decision will be guided by the two goals of preserving the free
status of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest possible
use to the public, we recommend making it free software that everyone can
redistribute and change. You can do so by permitting redistribution under
these terms (or, alternatively, under the terms of the ordinary General Public
License).
To apply these terms, attach the following notices to the library. It is safest
to attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the "copyright"
line and a pointer to where the full notice is found.
one line to give the library's name and an idea of what it does.
Copyright (C) year name of author
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Library General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more
details.
You should have received a copy of the GNU Library General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the library, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in
the library `Frob' (a library for tweaking knobs) written
by James Random Hacker.
signature of Ty Coon, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
@@ -0,0 +1,19 @@
MIT License Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,49 @@
UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
Unicode Data Files include all data files under the directories http://www.unicode.org/Public/,
http://www.unicode.org/reports/, http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/,
http://www.unicode.org/ivd/data/, and http://www.unicode.org/utility/trac/browser/.
Unicode Data Files do not include PDF online code charts under the directory
http://www.unicode.org/Public/.
Software includes any source code published in the Unicode Standard or under
the directories http://www.unicode.org/Public/, http://www.unicode.org/reports/,
http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/,
and http://www.unicode.org/utility/trac/browser/.
NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING,
INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA FILES ("DATA FILES"),
AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND
BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE,
DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
COPYRIGHT AND PERMISSION NOTICE Copyright © 1991-2016 Unicode, Inc. All rights
reserved. Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the Unicode data files and any associated documentation (the "Data Files")
or Unicode software and any associated documentation (the "Software") to deal
in the Data Files or Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, and/or sell copies
of the Data Files or Software, and to permit persons to whom the Data Files
or Software are furnished to do so, provided that either
(a) this copyright and permission notice appear with all copies of the Data
Files or Software, or
(b) this copyright and permission notice appear in associated Documentation.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall not
be used in advertising or otherwise to promote the sale, use or other dealings
in these Data Files or Software without prior written authorization of the
copyright holder.
@@ -0,0 +1,208 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution
as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct
or indirect, to cause the direction or management of such entity, whether
by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled object
code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work (an example is provided in the Appendix
below).
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative
Works shall not include works that remain separable from, or merely link (or
bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative
Works thereof, that is intentionally submitted to Licensor for inclusion in
the Work by the copyright owner or by an individual or Legal Entity authorized
to submit on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication
sent to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor
for the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently incorporated
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
Derivative Works of, publicly display, publicly perform, sublicense, and distribute
the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License,
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section) patent
license to make, have made, use, offer to sell, sell, import, and otherwise
transfer the Work, where such license applies only to those patent claims
licensable by such Contributor that are necessarily infringed by their Contribution(s)
alone or by combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
that the Work or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses granted to You
under this License for that Work shall terminate as of the date such litigation
is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or
Derivative Works thereof in any medium, with or without modifications, and
in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy
of this License; and
(b) You must cause any modified files to carry prominent notices stating that
You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source
form of the Work, excluding those notices that do not pertain to any part
of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy
of the attribution notices contained within such NOTICE file, excluding those
notices that do not pertain to any part of the Derivative Works, in at least
one of the following places: within a NOTICE text file distributed as part
of the Derivative Works; within the Source form or documentation, if provided
along with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works
that You distribute, alongside or as an addendum to the NOTICE text from the
Work, provided that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction,
or distribution of Your modifications, or for any such Derivative Works as
a whole, provided Your use, reproduction, and distribution of the Work otherwise
complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any
Contribution intentionally submitted for inclusion in the Work by You to the
Licensor shall be under the terms and conditions of this License, without
any additional terms or conditions. Notwithstanding the above, nothing herein
shall supersede or modify the terms of any separate license agreement you
may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names,
trademarks, service marks, or product names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
in writing, Licensor provides the Work (and each Contributor provides its
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied, including, without limitation, any warranties
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
of using or redistributing the Work and assume any risks associated with Your
exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether
in tort (including negligence), contract, or otherwise, unless required by
applicable law (such as deliberate and grossly negligent acts) or agreed to
in writing, shall any Contributor be liable to You for damages, including
any direct, indirect, special, incidental, or consequential damages of any
character arising as a result of this License or out of the use or inability
to use the Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all other commercial
damages or losses), even if such Contributor has been advised of the possibility
of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work
or Derivative Works thereof, You may choose to offer, and charge a fee for,
acceptance of support, warranty, indemnity, or other liability obligations
and/or rights consistent with this License. However, in accepting such obligations,
You may act only on Your own behalf and on Your sole responsibility, not on
behalf of any other Contributor, and only if You agree to indemnify, defend,
and hold each Contributor harmless for any liability incurred by, or claims
asserted against, such Contributor by reason of your accepting any such warranty
or additional liability. END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own identifying
information. (Don't include the brackets!) The text should be enclosed in
the appropriate comment syntax for the file format. We also recommend that
a file or class name and description of purpose be included on the same "printed
page" as the copyright notice for easier identification within third-party
archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,26 @@
Copyright (c) <year> <owner>. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
@@ -0,0 +1,319 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public License is intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users. This General Public License applies to
most of the Free Software Foundation's software and to any other program whose
authors commit to using it. (Some other Free Software Foundation software
is covered by the GNU Lesser General Public License instead.) You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for this service if you
wish), that you receive source code or can get it if you want it, that you
can change the software or use pieces of it in new free programs; and that
you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to
deny you these rights or to ask you to surrender the rights. These restrictions
translate to certain responsibilities for you if you distribute copies of
the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or
for a fee, you must give the recipients all the rights that you have. You
must make sure that they, too, receive or can get the source code. And you
must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2)
offer you this license which gives you legal permission to copy, distribute
and/or modify the software.
Also, for each author's protection and ours, we want to make certain that
everyone understands that there is no warranty for this free software. If
the software is modified by someone else and passed on, we want its recipients
to know that what they have is not the original, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We
wish to avoid the danger that redistributors of a free program will individually
obtain patent licenses, in effect making the program proprietary. To prevent
this, we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification
follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice
placed by the copyright holder saying it may be distributed under the terms
of this General Public License. The "Program", below, refers to any such program
or work, and a "work based on the Program" means either the Program or any
derivative work under copyright law: that is to say, a work containing the
Program or a portion of it, either verbatim or with modifications and/or translated
into another language. (Hereinafter, translation is included without limitation
in the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running the Program
is not restricted, and the output from the Program is covered only if its
contents constitute a work based on the Program (independent of having been
made by running the Program). Whether that is true depends on what the Program
does.
1. You may copy and distribute verbatim copies of the Program's source code
as you receive it, in any medium, provided that you conspicuously and appropriately
publish on each copy an appropriate copyright notice and disclaimer of warranty;
keep intact all the notices that refer to this License and to the absence
of any warranty; and give any other recipients of the Program a copy of this
License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you
may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it,
thus forming a work based on the Program, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all
of these conditions:
a) You must cause the modified files to carry prominent notices stating that
you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or
in part contains or is derived from the Program or any part thereof, to be
licensed as a whole at no charge to all third parties under the terms of this
License.
c) If the modified program normally reads commands interactively when run,
you must cause it, when started running for such interactive use in the most
ordinary way, to print or display an announcement including an appropriate
copyright notice and a notice that there is no warranty (or else, saying that
you provide a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this License.
(Exception: if the Program itself is interactive but does not normally print
such an announcement, your work based on the Program is not required to print
an announcement.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Program, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Program, the distribution of the whole must be
on the terms of this License, whose permissions for other licensees extend
to the entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works based
on the Program.
In addition, mere aggregation of another work not based on the Program with
the Program (or with a work based on the Program) on a volume of a storage
or distribution medium does not bring the other work under the scope of this
License.
3. You may copy and distribute the Program (or a work based on it, under Section
2) in object code or executable form under the terms of Sections 1 and 2 above
provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source code,
which must be distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to give
any third party, for a charge no more than your cost of physically performing
source distribution, a complete machine-readable copy of the corresponding
source code, to be distributed under the terms of Sections 1 and 2 above on
a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to distribute
corresponding source code. (This alternative is allowed only for noncommercial
distribution and only if you received the program in object code or executable
form with such an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for making
modifications to it. For an executable work, complete source code means all
the source code for all modules it contains, plus any associated interface
definition files, plus the scripts used to control compilation and installation
of the executable. However, as a special exception, the source code distributed
need not include anything that is normally distributed (in either source or
binary form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component itself
accompanies the executable.
If distribution of executable or object code is made by offering access to
copy from a designated place, then offering equivalent access to copy the
source code from the same place counts as distribution of the source code,
even though third parties are not compelled to copy the source along with
the object code.
4. You may not copy, modify, sublicense, or distribute the Program except
as expressly provided under this License. Any attempt otherwise to copy, modify,
sublicense or distribute the Program is void, and will automatically terminate
your rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses terminated
so long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed
it. However, nothing else grants you permission to modify or distribute the
Program or its derivative works. These actions are prohibited by law if you
do not accept this License. Therefore, by modifying or distributing the Program
(or any work based on the Program), you indicate your acceptance of this License
to do so, and all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program),
the recipient automatically receives a license from the original licensor
to copy, distribute or modify the Program subject to these terms and conditions.
You may not impose any further restrictions on the recipients' exercise of
the rights granted herein. You are not responsible for enforcing compliance
by third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are imposed
on you (whether by court order, agreement or otherwise) that contradict the
conditions of this License, they do not excuse you from the conditions of
this License. If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations, then as
a consequence you may not distribute the Program at all. For example, if a
patent license would not permit royalty-free redistribution of the Program
by all those who receive copies directly or indirectly through you, then the
only way you could satisfy both it and this License would be to refrain entirely
from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply and
the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system, which is implemented by public license practices.
Many people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose
that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain
countries either by patents or by copyrighted interfaces, the original copyright
holder who places the Program under this License may add an explicit geographical
distribution limitation excluding those countries, so that distribution is
permitted only in or among countries not thus excluded. In such case, this
License incorporates the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions of
the General Public License from time to time. Such new versions will be similar
in spirit to the present version, but may differ in detail to address new
problems or concerns.
Each version is given a distinguishing version number. If the Program specifies
a version number of this License which applies to it and "any later version",
you have the option of following the terms and conditions either of that version
or of any later version published by the Free Software Foundation. If the
Program does not specify a version number of this License, you may choose
any version ever published by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs
whose distribution conditions are different, write to the author to ask for
permission. For software which is copyrighted by the Free Software Foundation,
write to the Free Software Foundation; we sometimes make exceptions for this.
Our decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing and reuse
of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively convey the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.
<one line to give the program's name and an idea of what it does.>
Copyright (C)< yyyy> <name of author>
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when
it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software,
and you are welcome to redistribute it under certain conditions; type `show
c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may be
called something other than `show w' and `show c'; they could even be mouse-clicks
or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the program, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision'
(which makes passes at compilers) written by James Hacker.
<signature of Ty Coon >, 1 April 1989 Ty Coon, President of Vice This General
Public License does not permit incorporating your program into proprietary
programs. If your program is a subroutine library, you may consider it more
useful to permit linking proprietary applications with the library. If this
is what you want to do, use the GNU Lesser General Public License instead
of this License.
@@ -0,0 +1,319 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public License is intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users. This General Public License applies to
most of the Free Software Foundation's software and to any other program whose
authors commit to using it. (Some other Free Software Foundation software
is covered by the GNU Lesser General Public License instead.) You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for this service if you
wish), that you receive source code or can get it if you want it, that you
can change the software or use pieces of it in new free programs; and that
you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to
deny you these rights or to ask you to surrender the rights. These restrictions
translate to certain responsibilities for you if you distribute copies of
the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or
for a fee, you must give the recipients all the rights that you have. You
must make sure that they, too, receive or can get the source code. And you
must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2)
offer you this license which gives you legal permission to copy, distribute
and/or modify the software.
Also, for each author's protection and ours, we want to make certain that
everyone understands that there is no warranty for this free software. If
the software is modified by someone else and passed on, we want its recipients
to know that what they have is not the original, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We
wish to avoid the danger that redistributors of a free program will individually
obtain patent licenses, in effect making the program proprietary. To prevent
this, we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification
follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice
placed by the copyright holder saying it may be distributed under the terms
of this General Public License. The "Program", below, refers to any such program
or work, and a "work based on the Program" means either the Program or any
derivative work under copyright law: that is to say, a work containing the
Program or a portion of it, either verbatim or with modifications and/or translated
into another language. (Hereinafter, translation is included without limitation
in the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running the Program
is not restricted, and the output from the Program is covered only if its
contents constitute a work based on the Program (independent of having been
made by running the Program). Whether that is true depends on what the Program
does.
1. You may copy and distribute verbatim copies of the Program's source code
as you receive it, in any medium, provided that you conspicuously and appropriately
publish on each copy an appropriate copyright notice and disclaimer of warranty;
keep intact all the notices that refer to this License and to the absence
of any warranty; and give any other recipients of the Program a copy of this
License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you
may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it,
thus forming a work based on the Program, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all
of these conditions:
a) You must cause the modified files to carry prominent notices stating that
you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or
in part contains or is derived from the Program or any part thereof, to be
licensed as a whole at no charge to all third parties under the terms of this
License.
c) If the modified program normally reads commands interactively when run,
you must cause it, when started running for such interactive use in the most
ordinary way, to print or display an announcement including an appropriate
copyright notice and a notice that there is no warranty (or else, saying that
you provide a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this License.
(Exception: if the Program itself is interactive but does not normally print
such an announcement, your work based on the Program is not required to print
an announcement.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Program, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Program, the distribution of the whole must be
on the terms of this License, whose permissions for other licensees extend
to the entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works based
on the Program.
In addition, mere aggregation of another work not based on the Program with
the Program (or with a work based on the Program) on a volume of a storage
or distribution medium does not bring the other work under the scope of this
License.
3. You may copy and distribute the Program (or a work based on it, under Section
2) in object code or executable form under the terms of Sections 1 and 2 above
provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source code,
which must be distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to give
any third party, for a charge no more than your cost of physically performing
source distribution, a complete machine-readable copy of the corresponding
source code, to be distributed under the terms of Sections 1 and 2 above on
a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to distribute
corresponding source code. (This alternative is allowed only for noncommercial
distribution and only if you received the program in object code or executable
form with such an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for making
modifications to it. For an executable work, complete source code means all
the source code for all modules it contains, plus any associated interface
definition files, plus the scripts used to control compilation and installation
of the executable. However, as a special exception, the source code distributed
need not include anything that is normally distributed (in either source or
binary form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component itself
accompanies the executable.
If distribution of executable or object code is made by offering access to
copy from a designated place, then offering equivalent access to copy the
source code from the same place counts as distribution of the source code,
even though third parties are not compelled to copy the source along with
the object code.
4. You may not copy, modify, sublicense, or distribute the Program except
as expressly provided under this License. Any attempt otherwise to copy, modify,
sublicense or distribute the Program is void, and will automatically terminate
your rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses terminated
so long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed
it. However, nothing else grants you permission to modify or distribute the
Program or its derivative works. These actions are prohibited by law if you
do not accept this License. Therefore, by modifying or distributing the Program
(or any work based on the Program), you indicate your acceptance of this License
to do so, and all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program),
the recipient automatically receives a license from the original licensor
to copy, distribute or modify the Program subject to these terms and conditions.
You may not impose any further restrictions on the recipients' exercise of
the rights granted herein. You are not responsible for enforcing compliance
by third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are imposed
on you (whether by court order, agreement or otherwise) that contradict the
conditions of this License, they do not excuse you from the conditions of
this License. If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations, then as
a consequence you may not distribute the Program at all. For example, if a
patent license would not permit royalty-free redistribution of the Program
by all those who receive copies directly or indirectly through you, then the
only way you could satisfy both it and this License would be to refrain entirely
from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply and
the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system, which is implemented by public license practices.
Many people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose
that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain
countries either by patents or by copyrighted interfaces, the original copyright
holder who places the Program under this License may add an explicit geographical
distribution limitation excluding those countries, so that distribution is
permitted only in or among countries not thus excluded. In such case, this
License incorporates the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions of
the General Public License from time to time. Such new versions will be similar
in spirit to the present version, but may differ in detail to address new
problems or concerns.
Each version is given a distinguishing version number. If the Program specifies
a version number of this License which applies to it and "any later version",
you have the option of following the terms and conditions either of that version
or of any later version published by the Free Software Foundation. If the
Program does not specify a version number of this License, you may choose
any version ever published by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs
whose distribution conditions are different, write to the author to ask for
permission. For software which is copyrighted by the Free Software Foundation,
write to the Free Software Foundation; we sometimes make exceptions for this.
Our decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing and reuse
of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively convey the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.
<one line to give the program's name and an idea of what it does.>
Copyright (C) <yyyy> <name of author>
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when
it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software,
and you are welcome to redistribute it under certain conditions; type `show
c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may be
called something other than `show w' and `show c'; they could even be mouse-clicks
or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the program, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision'
(which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989 Ty Coon, President of Vice This General
Public License does not permit incorporating your program into proprietary
programs. If your program is a subroutine library, you may consider it more
useful to permit linking proprietary applications with the library. If this
is what you want to do, use the GNU Lesser General Public License instead
of this License.
@@ -0,0 +1,625 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and
other kinds of works.
The licenses for most software and other practical works are designed to take
away your freedom to share and change the works. By contrast, the GNU General
Public License is intended to guarantee your freedom to share and change all
versions of a program--to make sure it remains free software for all its users.
We, the Free Software Foundation, use the GNU General Public License for most
of our software; it applies also to any other work released this way by its
authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for them if you wish), that
you receive source code or can get it if you want it, that you can change
the software or use pieces of it in new free programs, and that you know you
can do these things.
To protect your rights, we need to prevent others from denying you these rights
or asking you to surrender the rights. Therefore, you have certain responsibilities
if you distribute copies of the software, or if you modify it: responsibilities
to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or
for a fee, you must pass on to the recipients the same freedoms that you received.
You must make sure that they, too, receive or can get the source code. And
you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert
copyright on the software, and (2) offer you this License giving you legal
permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that
there is no warranty for this free software. For both users' and authors'
sake, the GPL requires that modified versions be marked as changed, so that
their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified
versions of the software inside them, although the manufacturer can do so.
This is fundamentally incompatible with the aim of protecting users' freedom
to change the software. The systematic pattern of such abuse occurs in the
area of products for individuals to use, which is precisely where it is most
unacceptable. Therefore, we have designed this version of the GPL to prohibit
the practice for those products. If such problems arise substantially in other
domains, we stand ready to extend this provision to those domains in future
versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States
should not allow patents to restrict development and use of software on general-purpose
computers, but in those that do, we wish to avoid the special danger that
patents applied to a free program could make it effectively proprietary. To
prevent this, the GPL assures that patents cannot be used to render the program
non-free.
The precise terms and conditions for copying, distribution and modification
follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of works,
such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this License.
Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals
or organizations.
To "modify" a work means to copy from or adapt all or part of the work in
a fashion requiring copyright permission, other than the making of an exact
copy. The resulting work is called a "modified version" of the earlier work
or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based on the
Program.
To "propagate" a work means to do anything with it that, without permission,
would make you directly or secondarily liable for infringement under applicable
copyright law, except executing it on a computer or modifying a private copy.
Propagation includes copying, distribution (with or without modification),
making available to the public, and in some countries other activities as
well.
To "convey" a work means any kind of propagation that enables other parties
to make or receive copies. Mere interaction with a user through a computer
network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" to the
extent that it includes a convenient and prominently visible feature that
(1) displays an appropriate copyright notice, and (2) tells the user that
there is no warranty for the work (except to the extent that warranties are
provided), that licensees may convey the work under this License, and how
to view a copy of this License. If the interface presents a list of user commands
or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work for making
modifications to it. "Object code" means any non-source form of a work.
A "Standard Interface" means an interface that either is an official standard
defined by a recognized standards body, or, in the case of interfaces specified
for a particular programming language, one that is widely used among developers
working in that language.
The "System Libraries" of an executable work include anything, other than
the work as a whole, that (a) is included in the normal form of packaging
a Major Component, but which is not part of that Major Component, and (b)
serves only to enable use of the work with that Major Component, or to implement
a Standard Interface for which an implementation is available to the public
in source code form. A "Major Component", in this context, means a major essential
component (kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to produce
the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all the source
code needed to generate, install, and (for an executable work) run the object
code and to modify the work, including scripts to control those activities.
However, it does not include the work's System Libraries, or general-purpose
tools or generally available free programs which are used unmodified in performing
those activities but which are not part of the work. For example, Corresponding
Source includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically linked
subprograms that the work is specifically designed to require, such as by
intimate data communication or control flow between those subprograms and
other parts of the work.
The Corresponding Source need not include anything that users can regenerate
automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright
on the Program, and are irrevocable provided the stated conditions are met.
This License explicitly affirms your unlimited permission to run the unmodified
Program. The output from running a covered work is covered by this License
only if the output, given its content, constitutes a covered work. This License
acknowledges your rights of fair use or other equivalent, as provided by copyright
law.
You may make, run and propagate covered works that you do not convey, without
conditions so long as your license otherwise remains in force. You may convey
covered works to others for the sole purpose of having them make modifications
exclusively for you, or provide you with facilities for running those works,
provided that you comply with the terms of this License in conveying all material
for which you do not control copyright. Those thus making or running the covered
works for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of your copyrighted
material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions
stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure
under any applicable law fulfilling obligations under article 11 of the WIPO
copyright treaty adopted on 20 December 1996, or similar laws prohibiting
or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention
of technological measures to the extent such circumvention is effected by
exercising rights under this License with respect to the covered work, and
you disclaim any intention to limit operation or modification of the work
as a means of enforcing, against the work's users, your or third parties'
legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive
it, in any medium, provided that you conspicuously and appropriately publish
on each copy an appropriate copyright notice; keep intact all notices stating
that this License and any non-permissive terms added in accord with section
7 apply to the code; keep intact all notices of the absence of any warranty;
and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you
may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce
it from the Program, in the form of source code under the terms of section
4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and
giving a relevant date.
b) The work must carry prominent notices stating that it is released under
this License and any conditions added under section 7. This requirement modifies
the requirement in section 4 to "keep intact all notices".
c) You must license the entire work, as a whole, under this License to anyone
who comes into possession of a copy. This License will therefore apply, along
with any applicable section 7 additional terms, to the whole of the work,
and all its parts, regardless of how they are packaged. This License gives
no permission to license the work in any other way, but it does not invalidate
such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate
Legal Notices; however, if the Program has interactive interfaces that do
not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works,
which are not by their nature extensions of the covered work, and which are
not combined with it such as to form a larger program, in or on a volume of
a storage or distribution medium, is called an "aggregate" if the compilation
and its resulting copyright are not used to limit the access or legal rights
of the compilation's users beyond what the individual works permit. Inclusion
of a covered work in an aggregate does not cause this License to apply to
the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections
4 and 5, provided that you also convey the machine-readable Corresponding
Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including
a physical distribution medium), accompanied by the Corresponding Source fixed
on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including
a physical distribution medium), accompanied by a written offer, valid for
at least three years and valid for as long as you offer spare parts or customer
support for that product model, to give anyone who possesses the object code
either (1) a copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical medium customarily
used for software interchange, for a price no more than your reasonable cost
of physically performing this conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written
offer to provide the Corresponding Source. This alternative is allowed only
occasionally and noncommercially, and only if you received the object code
with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis
or for a charge), and offer equivalent access to the Corresponding Source
in the same way through the same place at no further charge. You need not
require recipients to copy the Corresponding Source along with the object
code. If the place to copy the object code is a network server, the Corresponding
Source may be on a different server (operated by you or a third party) that
supports equivalent copying facilities, provided you maintain clear directions
next to the object code saying where to find the Corresponding Source. Regardless
of what server hosts the Corresponding Source, you remain obligated to ensure
that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform
other peers where the object code and Corresponding Source of the work are
being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from
the Corresponding Source as a System Library, need not be included in conveying
the object code work.
A "User Product" is either (1) a "consumer product", which means any tangible
personal property which is normally used for personal, family, or household
purposes, or (2) anything designed or sold for incorporation into a dwelling.
In determining whether a product is a consumer product, doubtful cases shall
be resolved in favor of coverage. For a particular product received by a particular
user, "normally used" refers to a typical or common use of that class of product,
regardless of the status of the particular user or of the way in which the
particular user actually uses, or expects or is expected to use, the product.
A product is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent the
only significant mode of use of the product.
"Installation Information" for a User Product means any methods, procedures,
authorization keys, or other information required to install and execute modified
versions of a covered work in that User Product from a modified version of
its Corresponding Source. The information must suffice to ensure that the
continued functioning of the modified object code is in no case prevented
or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically
for use in, a User Product, and the conveying occurs as part of a transaction
in which the right of possession and use of the User Product is transferred
to the recipient in perpetuity or for a fixed term (regardless of how the
transaction is characterized), the Corresponding Source conveyed under this
section must be accompanied by the Installation Information. But this requirement
does not apply if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has been installed
in ROM).
The requirement to provide Installation Information does not include a requirement
to continue to provide support service, warranty, or updates for a work that
has been modified or installed by the recipient, or for the User Product in
which it has been modified or installed. Access to a network may be denied
when the modification itself materially and adversely affects the operation
of the network or violates the rules and protocols for communication across
the network.
Corresponding Source conveyed, and Installation Information provided, in accord
with this section must be in a format that is publicly documented (and with
an implementation available to the public in source code form), and must require
no special password or key for unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License
by making exceptions from one or more of its conditions. Additional permissions
that are applicable to the entire Program shall be treated as though they
were included in this License, to the extent that they are valid under applicable
law. If additional permissions apply only to part of the Program, that part
may be used separately under those permissions, but the entire Program remains
governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any
additional permissions from that copy, or from any part of it. (Additional
permissions may be written to require their own removal in certain cases when
you modify the work.) You may place additional permissions on material, added
by you to a covered work, for which you have or can give appropriate copyright
permission.
Notwithstanding any other provision of this License, for material you add
to a covered work, you may (if authorized by the copyright holders of that
material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of
sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author
attributions in that material or in the Appropriate Legal Notices displayed
by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring
that modified versions of such material be marked in reasonable ways as different
from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors
of the material; or
e) Declining to grant rights under trademark law for use of some trade names,
trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by
anyone who conveys the material (or modified versions of it) with contractual
assumptions of liability to the recipient, for any liability that these contractual
assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered "further restrictions"
within the meaning of section 10. If the Program as you received it, or any
part of it, contains a notice stating that it is governed by this License
along with a term that is a further restriction, you may remove that term.
If a license document contains a further restriction but permits relicensing
or conveying under this License, you may add to a covered work material governed
by the terms of that license document, provided that the further restriction
does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place,
in the relevant source files, a statement of the additional terms that apply
to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form
of a separately written license, or stated as exceptions; the above requirements
apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided
under this License. Any attempt otherwise to propagate or modify it is void,
and will automatically terminate your rights under this License (including
any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from
a particular copyright holder is reinstated (a) provisionally, unless and
until the copyright holder explicitly and finally terminates your license,
and (b) permanently, if the copyright holder fails to notify you of the violation
by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently
if the copyright holder notifies you of the violation by some reasonable means,
this is the first time you have received notice of violation of this License
(for any work) from that copyright holder, and you cure the violation prior
to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses
of parties who have received copies or rights from you under this License.
If your rights have been terminated and not permanently reinstated, you do
not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy
of the Program. Ancillary propagation of a covered work occurring solely as
a consequence of using peer-to-peer transmission to receive a copy likewise
does not require acceptance. However, nothing other than this License grants
you permission to propagate or modify any covered work. These actions infringe
copyright if you do not accept this License. Therefore, by modifying or propagating
a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives
a license from the original licensors, to run, modify and propagate that work,
subject to this License. You are not responsible for enforcing compliance
by third parties with this License.
An "entity transaction" is a transaction transferring control of an organization,
or substantially all assets of one, or subdividing an organization, or merging
organizations. If propagation of a covered work results from an entity transaction,
each party to that transaction who receives a copy of the work also receives
whatever licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the Corresponding
Source of the work from the predecessor in interest, if the predecessor has
it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights
granted or affirmed under this License. For example, you may not impose a
license fee, royalty, or other charge for exercise of rights granted under
this License, and you may not initiate litigation (including a cross-claim
or counterclaim in a lawsuit) alleging that any patent claim is infringed
by making, using, selling, offering for sale, or importing the Program or
any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License
of the Program or a work on which the Program is based. The work thus licensed
is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned or controlled
by the contributor, whether already acquired or hereafter acquired, that would
be infringed by some manner, permitted by this License, of making, using,
or selling its contributor version, but do not include claims that would be
infringed only as a consequence of further modification of the contributor
version. For purposes of this definition, "control" includes the right to
grant patent sublicenses in a manner consistent with the requirements of this
License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent
license under the contributor's essential patent claims, to make, use, sell,
offer for sale, import and otherwise run, modify and propagate the contents
of its contributor version.
In the following three paragraphs, a "patent license" is any express agreement
or commitment, however denominated, not to enforce a patent (such as an express
permission to practice a patent or covenant not to sue for patent infringement).
To "grant" such a patent license to a party means to make such an agreement
or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the
Corresponding Source of the work is not available for anyone to copy, free
of charge and under the terms of this License, through a publicly available
network server or other readily accessible means, then you must either (1)
cause the Corresponding Source to be so available, or (2) arrange to deprive
yourself of the benefit of the patent license for this particular work, or
(3) arrange, in a manner consistent with the requirements of this License,
to extend the patent license to downstream recipients. "Knowingly relying"
means you have actual knowledge that, but for the patent license, your conveying
the covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that country
that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement,
you convey, or propagate by procuring conveyance of, a covered work, and grant
a patent license to some of the parties receiving the covered work authorizing
them to use, propagate, modify or convey a specific copy of the covered work,
then the patent license you grant is automatically extended to all recipients
of the covered work and works based on it.
A patent license is "discriminatory" if it does not include within the scope
of its coverage, prohibits the exercise of, or is conditioned on the non-exercise
of one or more of the rights that are specifically granted under this License.
You may not convey a covered work if you are a party to an arrangement with
a third party that is in the business of distributing software, under which
you make payment to the third party based on the extent of your activity of
conveying the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by you
(or copies made from those copies), or (b) primarily for and in connection
with specific products or compilations that contain the covered work, unless
you entered into that arrangement, or that patent license was granted, prior
to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied
license or other defenses to infringement that may otherwise be available
to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise)
that contradict the conditions of this License, they do not excuse you from
the conditions of this License. If you cannot convey a covered work so as
to satisfy simultaneously your obligations under this License and any other
pertinent obligations, then as a consequence you may not convey it at all.
For example, if you agree to terms that obligate you to collect a royalty
for further conveying from those to whom you convey the Program, the only
way you could satisfy both those terms and this License would be to refrain
entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to
link or combine any covered work with a work licensed under version 3 of the
GNU Affero General Public License into a single combined work, and to convey
the resulting work. The terms of this License will continue to apply to the
part which is the covered work, but the special requirements of the GNU Affero
General Public License, section 13, concerning interaction through a network
will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the
GNU General Public License from time to time. Such new versions will be similar
in spirit to the present version, but may differ in detail to address new
problems or concerns.
Each version is given a distinguishing version number. If the Program specifies
that a certain numbered version of the GNU General Public License "or any
later version" applies to it, you have the option of following the terms and
conditions either of that numbered version or of any later version published
by the Free Software Foundation. If the Program does not specify a version
number of the GNU General Public License, you may choose any version ever
published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of
the GNU General Public License can be used, that proxy's public statement
of acceptance of a version permanently authorizes you to choose that version
for the Program.
Later license versions may give you additional or different permissions. However,
no additional obligations are imposed on any author or copyright holder as
a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM
PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM
AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO
USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot
be given local legal effect according to their terms, reviewing courts shall
apply local law that most closely approximates an absolute waiver of all civil
liability in connection with the Program, unless a warranty or assumption
of liability accompanies a copy of the Program in return for a fee. END OF
TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively state the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like
this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain
conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands might
be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General Public
License instead of this License. But first, please read <https://www.gnu.org/
licenses /why-not-lgpl.html>.
@@ -0,0 +1,232 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and other kinds of works.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
“This License” refers to version 3 of the GNU General Public License.
“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
A “covered work” means either the unmodified Program or a work based on the Program.
To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.
@@ -0,0 +1,467 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts as the
successor of the GNU Library Public License, version 2, hence the version
number 2.1.]
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public Licenses are intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users.
This license, the Lesser General Public License, applies to some specially
designated software packages--typically libraries--of the Free Software Foundation
and other authors who decide to use it. You can use it too, but we suggest
you first think carefully about whether this license or the ordinary General
Public License is the better strategy to use in any particular case, based
on the explanations below.
When we speak of free software, we are referring to freedom of use, not price.
Our General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for this service if you
wish); that you receive source code or can get it if you want it; that you
can change the software and use pieces of it in new free programs; and that
you are informed that you can do these things.
To protect your rights, we need to make restrictions that forbid distributors
to deny you these rights or to ask you to surrender these rights. These restrictions
translate to certain responsibilities for you if you distribute copies of
the library or if you modify it.
For example, if you distribute copies of the library, whether gratis or for
a fee, you must give the recipients all the rights that we gave you. You must
make sure that they, too, receive or can get the source code. If you link
other code with the library, you must provide complete object files to the
recipients, so that they can relink them with the library after making changes
to the library and recompiling it. And you must show them these terms so they
know their rights.
We protect your rights with a two-step method: (1) we copyright the library,
and (2) we offer you this license, which gives you legal permission to copy,
distribute and/or modify the library.
To protect each distributor, we want to make it very clear that there is no
warranty for the free library. Also, if the library is modified by someone
else and passed on, the recipients should know that what they have is not
the original version, so that the original author's reputation will not be
affected by problems that might be introduced by others.
Finally, software patents pose a constant threat to the existence of any free
program. We wish to make sure that a company cannot effectively restrict the
users of a free program by obtaining a restrictive license from a patent holder.
Therefore, we insist that any patent license obtained for a version of the
library must be consistent with the full freedom of use specified in this
license.
Most GNU software, including some libraries, is covered by the ordinary GNU
General Public License. This license, the GNU Lesser General Public License,
applies to certain designated libraries, and is quite different from the ordinary
General Public License. We use this license for certain libraries in order
to permit linking those libraries into non-free programs.
When a program is linked with a library, whether statically or using a shared
library, the combination of the two is legally speaking a combined work, a
derivative of the original library. The ordinary General Public License therefore
permits such linking only if the entire combination fits its criteria of freedom.
The Lesser General Public License permits more lax criteria for linking other
code with the library.
We call this license the "Lesser" General Public License because it does Less
to protect the user's freedom than the ordinary General Public License. It
also provides other free software developers Less of an advantage over competing
non-free programs. These disadvantages are the reason we use the ordinary
General Public License for many libraries. However, the Lesser license provides
advantages in certain special circumstances.
For example, on rare occasions, there may be a special need to encourage the
widest possible use of a certain library, so that it becomes a de-facto standard.
To achieve this, non-free programs must be allowed to use the library. A more
frequent case is that a free library does the same job as widely used non-free
libraries. In this case, there is little to gain by limiting the free library
to free software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free programs
enables a greater number of people to use a large body of free software. For
example, permission to use the GNU C Library in non-free programs enables
many more people to use the whole GNU operating system, as well as its variant,
the GNU/Linux operating system.
Although the Lesser General Public License is Less protective of the users'
freedom, it does ensure that the user of a program that is linked with the
Library has the freedom and the wherewithal to run that program using a modified
version of the Library.
The precise terms and conditions for copying, distribution and modification
follow. Pay close attention to the difference between a "work based on the
library" and a "work that uses the library". The former contains code derived
from the library, whereas the latter must be combined with the library in
order to run.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other program
which contains a notice placed by the copyright holder or other authorized
party saying it may be distributed under the terms of this Lesser General
Public License (also called "this License"). Each licensee is addressed as
"you".
A "library" means a collection of software functions and/or data prepared
so as to be conveniently linked with application programs (which use some
of those functions and data) to form executables.
The "Library", below, refers to any such software library or work which has
been distributed under these terms. A "work based on the Library" means either
the Library or any derivative work under copyright law: that is to say, a
work containing the Library or a portion of it, either verbatim or with modifications
and/or translated straightforwardly into another language. (Hereinafter, translation
is included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for making modifications
to it. For a library, complete source code means all the source code for all
modules it contains, plus any associated interface definition files, plus
the scripts used to control compilation and installation of the library.
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running a program
using the Library is not restricted, and output from such a program is covered
only if its contents constitute a work based on the Library (independent of
the use of the Library in a tool for writing it). Whether that is true depends
on what the Library does and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's complete source
code as you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and disclaimer
of warranty; keep intact all the notices that refer to this License and to
the absence of any warranty; and distribute a copy of this License along with
the Library.
You may charge a fee for the physical act of transferring a copy, and you
may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Library or any portion of it,
thus forming a work based on the Library, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all
of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices stating that
you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no charge to all
third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a table of
data to be supplied by an application program that uses the facility, other
than as an argument passed when the facility is invoked, then you must make
a good faith effort to ensure that, in the event an application does not supply
such function or table, the facility still operates, and performs whatever
part of its purpose remains meaningful.
(For example, a function in a library to compute square roots has a purpose
that is entirely well-defined independent of the application. Therefore, Subsection
2d requires that any application-supplied function or table used by this function
must be optional: if the application does not supply it, the square root function
must still compute square roots.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Library, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Library, the distribution of the whole must be
on the terms of this License, whose permissions for other licensees extend
to the entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works based
on the Library.
In addition, mere aggregation of another work not based on the Library with
the Library (or with a work based on the Library) on a volume of a storage
or distribution medium does not bring the other work under the scope of this
License.
3. You may opt to apply the terms of the ordinary GNU General Public License
instead of this License to a given copy of the Library. To do this, you must
alter all the notices that refer to this License, so that they refer to the
ordinary GNU General Public License, version 2, instead of to this License.
(If a newer version than version 2 of the ordinary GNU General Public License
has appeared, then you can specify that version instead if you wish.) Do not
make any other change in these notices.
Once this change is made in a given copy, it is irreversible for that copy,
so the ordinary GNU General Public License applies to all subsequent copies
and derivative works made from that copy.
This option is useful when you wish to copy part of the code of the Library
into a program that is not a library.
4. You may copy and distribute the Library (or a portion or derivative of
it, under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you accompany it with the complete corresponding
machine-readable source code, which must be distributed under the terms of
Sections 1 and 2 above on a medium customarily used for software interchange.
If distribution of object code is made by offering access to copy from a designated
place, then offering equivalent access to copy the source code from the same
place satisfies the requirement to distribute the source code, even though
third parties are not compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the Library, but
is designed to work with the Library by being compiled or linked with it,
is called a "work that uses the Library". Such a work, in isolation, is not
a derivative work of the Library, and therefore falls outside the scope of
this License.
However, linking a "work that uses the Library" with the Library creates an
executable that is a derivative of the Library (because it contains portions
of the Library), rather than a "work that uses the library". The executable
is therefore covered by this License. Section 6 states terms for distribution
of such executables.
When a "work that uses the Library" uses material from a header file that
is part of the Library, the object code for the work may be a derivative work
of the Library even though the source code is not. Whether this is true is
especially significant if the work can be linked without the Library, or if
the work is itself a library. The threshold for this to be true is not precisely
defined by law.
If such an object file uses only numerical parameters, data structure layouts
and accessors, and small macros and small inline functions (ten lines or less
in length), then the use of the object file is unrestricted, regardless of
whether it is legally a derivative work. (Executables containing this object
code plus portions of the Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may distribute
the object code for the work under the terms of Section 6. Any executables
containing that work also fall under Section 6, whether or not they are linked
directly with the Library itself.
6. As an exception to the Sections above, you may also combine or link a "work
that uses the Library" with the Library to produce a work containing portions
of the Library, and distribute that work under terms of your choice, provided
that the terms permit modification of the work for the customer's own use
and reverse engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the Library
is used in it and that the Library and its use are covered by this License.
You must supply a copy of this License. If the work during execution displays
copyright notices, you must include the copyright notice for the Library among
them, as well as a reference directing the user to the copy of this License.
Also, you must do one of these things:
a) Accompany the work with the complete corresponding machine-readable source
code for the Library including whatever changes were used in the work (which
must be distributed under Sections 1 and 2 above); and, if the work is an
executable linked with the Library, with the complete machine-readable "work
that uses the Library", as object code and/or source code, so that the user
can modify the Library and then relink to produce a modified executable containing
the modified Library. (It is understood that the user who changes the contents
of definitions files in the Library will not necessarily be able to recompile
the application to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the Library. A
suitable mechanism is one that (1) uses at run time a copy of the library
already present on the user's computer system, rather than copying library
functions into the executable, and (2) will operate properly with a modified
version of the library, if the user installs one, as long as the modified
version is interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at least three years,
to give the same user the materials specified in Subsection 6a, above, for
a charge no more than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy from a designated
place, offer equivalent access to copy the above specified materials from
the same place.
e) Verify that the user has already received a copy of these materials or
that you have already sent this user a copy.
For an executable, the required form of the "work that uses the Library" must
include any data and utility programs needed for reproducing the executable
from it. However, as a special exception, the materials to be distributed
need not include anything that is normally distributed (in either source or
binary form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component itself
accompanies the executable.
It may happen that this requirement contradicts the license restrictions of
other proprietary libraries that do not normally accompany the operating system.
Such a contradiction means you cannot use both them and the Library together
in an executable that you distribute.
7. You may place library facilities that are a work based on the Library side-by-side
in a single library together with other library facilities not covered by
this License, and distribute such a combined library, provided that the separate
distribution of the work based on the Library and of the other library facilities
is otherwise permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work based on the
Library, uncombined with any other library facilities. This must be distributed
under the terms of the Sections above.
b) Give prominent notice with the combined library of the fact that part of
it is a work based on the Library, and explaining where to find the accompanying
uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute the Library
except as expressly provided under this License. Any attempt otherwise to
copy, modify, sublicense, link with, or distribute the Library is void, and
will automatically terminate your rights under this License. However, parties
who have received copies, or rights, from you under this License will not
have their licenses terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not signed
it. However, nothing else grants you permission to modify or distribute the
Library or its derivative works. These actions are prohibited by law if you
do not accept this License. Therefore, by modifying or distributing the Library
(or any work based on the Library), you indicate your acceptance of this License
to do so, and all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the Library),
the recipient automatically receives a license from the original licensor
to copy, distribute, link with or modify the Library subject to these terms
and conditions. You may not impose any further restrictions on the recipients'
exercise of the rights granted herein. You are not responsible for enforcing
compliance by third parties with this License.
11. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are imposed
on you (whether by court order, agreement or otherwise) that contradict the
conditions of this License, they do not excuse you from the conditions of
this License. If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations, then as
a consequence you may not distribute the Library at all. For example, if a
patent license would not permit royalty-free redistribution of the Library
by all those who receive copies directly or indirectly through you, then the
only way you could satisfy both it and this License would be to refrain entirely
from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system which is implemented by public license practices.
Many people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose
that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in certain
countries either by patents or by copyrighted interfaces, the original copyright
holder who places the Library under this License may add an explicit geographical
distribution limitation excluding those countries, so that distribution is
permitted only in or among countries not thus excluded. In such case, this
License incorporates the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new versions of
the Lesser General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to address
new problems or concerns.
Each version is given a distinguishing version number. If the Library specifies
a version number of this License which applies to it and "any later version",
you have the option of following the terms and conditions either of that version
or of any later version published by the Free Software Foundation. If the
Library does not specify a license version number, you may choose any version
ever published by the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free programs
whose distribution conditions are incompatible with these, write to the author
to ask for permission. For software which is copyrighted by the Free Software
Foundation, write to the Free Software Foundation; we sometimes make exceptions
for this. Our decision will be guided by the two goals of preserving the free
status of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest possible
use to the public, we recommend making it free software that everyone can
redistribute and change. You can do so by permitting redistribution under
these terms (or, alternatively, under the terms of the ordinary General Public
License).
To apply these terms, attach the following notices to the library. It is safest
to attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the "copyright"
line and a pointer to where the full notice is found.
< one line to give the library's name and an idea of what it does. >
Copyright (C) < year > < name of author >
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation; either version 2.1 of the License, or (at your option)
any later version.
This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License along
with this library; if not, write to the Free Software Foundation, Inc., 51
Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information
on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the library, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in
the library `Frob' (a library for tweaking knobs) written
by James Random Hacker.
< signature of Ty Coon > , 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
@@ -0,0 +1,163 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates the terms
and conditions of version 3 of the GNU General Public License, supplemented
by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser General
Public License, and the "GNU GPL" refers to version 3 of the GNU General Public
License.
"The Library" refers to a covered work governed by this License, other than
an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided by the
Library, but which is not otherwise based on the Library. Defining a subclass
of a class defined by the Library is deemed a mode of using an interface provided
by the Library.
A "Combined Work" is a work produced by combining or linking an Application
with the Library. The particular version of the Library with which the Combined
Work was made is also called the "Linked Version".
The "Minimal Corresponding Source" for a Combined Work means the Corresponding
Source for the Combined Work, excluding any source code for portions of the
Combined Work that, considered in isolation, are based on the Application,
and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the object
code and/or source code for the Application, including any data and utility
programs needed for reproducing the Combined Work from the Application, but
excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License without
being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a facility
refers to a function or data to be supplied by an Application that uses the
facility (other than as an argument passed when the facility is invoked),
then you may convey a copy of the modified version:
a) under this License, provided that you make a good faith effort to ensure
that, in the event an Application does not supply the function or data, the
facility still operates, and performs whatever part of its purpose remains
meaningful, or
b) under the GNU GPL, with none of the additional permissions of this License
applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from a header
file that is part of the Library. You may convey such object code under terms
of your choice, provided that, if the incorporated material is not limited
to numerical parameters, data structure layouts and accessors, or small macros,
inline functions and templates (ten or fewer lines in length), you do both
of the following:
a) Give prominent notice with each copy of the object code that the Library
is used in it and that the Library and its use are covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that, taken together,
effectively do not restrict modification of the portions of the Library contained
in the Combined Work and reverse engineering for debugging such modifications,
if you also do each of the following:
a) Give prominent notice with each copy of the Combined Work that the Library
is used in it and that the Library and its use are covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during execution, include
the copyright notice for the Library among these notices, as well as a reference
directing the user to the copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this License,
and the Corresponding Application Code in a form suitable for, and under terms
that permit, the user to recombine or relink the Application with a modified
version of the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying Corresponding Source.
1) Use a suitable shared library mechanism for linking with the Library. A
suitable mechanism is one that (a) uses at run time a copy of the Library
already present on the user's computer system, and (b) will operate properly
with a modified version of the Library that is interface-compatible with the
Linked Version.
e) Provide Installation Information, but only if you would otherwise be required
to provide such information under section 6 of the GNU GPL, and only to the
extent that such information is necessary to install and execute a modified
version of the Combined Work produced by recombining or relinking the Application
with a modified version of the Linked Version. (If you use option 4d0, the
Installation Information must accompany the Minimal Corresponding Source and
Corresponding Application Code. If you use option 4d1, you must provide the
Installation Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the Library side
by side in a single library together with other library facilities that are
not Applications and are not covered by this License, and convey such a combined
library under terms of your choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based on the
Library, uncombined with any other library facilities, conveyed under the
terms of this License.
b) Give prominent notice with the combined library that part of it is a work
based on the Library, and explaining where to find the accompanying uncombined
form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions of the
GNU Lesser General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to address
new problems or concerns.
Each version is given a distinguishing version number. If the Library as you
received it specifies that a certain numbered version of the GNU Lesser General
Public License "or any later version" applies to it, you have the option of
following the terms and conditions either of that published version or of
any later version published by the Free Software Foundation. If the Library
as you received it does not specify a version number of the GNU Lesser General
Public License, you may choose any version of the GNU Lesser General Public
License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide whether
future versions of the GNU Lesser General Public License shall apply, that
proxy's public statement of acceptance of any version is permanent authorization
for you to choose that version for the Library.
@@ -0,0 +1,12 @@
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the license or (at your option) at any later version that is
accepted by the membership of KDE e.V. (or its successor
approved by the membership of KDE e.V.), which shall act as a
proxy as defined in Section 14 of version 3 of the license.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
@@ -0,0 +1,12 @@
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the license or (at your option) any later version
that is accepted by the membership of KDE e.V. (or its successor
approved by the membership of KDE e.V.), which shall act as a
proxy as defined in Section 6 of version 3 of the license.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
@@ -0,0 +1,9 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,11 @@
Copyright (c) <year> <owner>.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
@@ -0,0 +1,468 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts as the
successor of the GNU Library Public License, version 2, hence the version
number 2.1.]
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public Licenses are intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users.
This license, the Lesser General Public License, applies to some specially
designated software packages--typically libraries--of the Free Software Foundation
and other authors who decide to use it. You can use it too, but we suggest
you first think carefully about whether this license or the ordinary General
Public License is the better strategy to use in any particular case, based
on the explanations below.
When we speak of free software, we are referring to freedom of use, not price.
Our General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for this service if you
wish); that you receive source code or can get it if you want it; that you
can change the software and use pieces of it in new free programs; and that
you are informed that you can do these things.
To protect your rights, we need to make restrictions that forbid distributors
to deny you these rights or to ask you to surrender these rights. These restrictions
translate to certain responsibilities for you if you distribute copies of
the library or if you modify it.
For example, if you distribute copies of the library, whether gratis or for
a fee, you must give the recipients all the rights that we gave you. You must
make sure that they, too, receive or can get the source code. If you link
other code with the library, you must provide complete object files to the
recipients, so that they can relink them with the library after making changes
to the library and recompiling it. And you must show them these terms so they
know their rights.
We protect your rights with a two-step method: (1) we copyright the library,
and (2) we offer you this license, which gives you legal permission to copy,
distribute and/or modify the library.
To protect each distributor, we want to make it very clear that there is no
warranty for the free library. Also, if the library is modified by someone
else and passed on, the recipients should know that what they have is not
the original version, so that the original author's reputation will not be
affected by problems that might be introduced by others.
Finally, software patents pose a constant threat to the existence of any free
program. We wish to make sure that a company cannot effectively restrict the
users of a free program by obtaining a restrictive license from a patent holder.
Therefore, we insist that any patent license obtained for a version of the
library must be consistent with the full freedom of use specified in this
license.
Most GNU software, including some libraries, is covered by the ordinary GNU
General Public License. This license, the GNU Lesser General Public License,
applies to certain designated libraries, and is quite different from the ordinary
General Public License. We use this license for certain libraries in order
to permit linking those libraries into non-free programs.
When a program is linked with a library, whether statically or using a shared
library, the combination of the two is legally speaking a combined work, a
derivative of the original library. The ordinary General Public License therefore
permits such linking only if the entire combination fits its criteria of freedom.
The Lesser General Public License permits more lax criteria for linking other
code with the library.
We call this license the "Lesser" General Public License because it does Less
to protect the user's freedom than the ordinary General Public License. It
also provides other free software developers Less of an advantage over competing
non-free programs. These disadvantages are the reason we use the ordinary
General Public License for many libraries. However, the Lesser license provides
advantages in certain special circumstances.
For example, on rare occasions, there may be a special need to encourage the
widest possible use of a certain library, so that it becomes a de-facto standard.
To achieve this, non-free programs must be allowed to use the library. A more
frequent case is that a free library does the same job as widely used non-free
libraries. In this case, there is little to gain by limiting the free library
to free software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free programs
enables a greater number of people to use a large body of free software. For
example, permission to use the GNU C Library in non-free programs enables
many more people to use the whole GNU operating system, as well as its variant,
the GNU/Linux operating system.
Although the Lesser General Public License is Less protective of the users'
freedom, it does ensure that the user of a program that is linked with the
Library has the freedom and the wherewithal to run that program using a modified
version of the Library.
The precise terms and conditions for copying, distribution and modification
follow. Pay close attention to the difference between a "work based on the
library" and a "work that uses the library". The former contains code derived
from the library, whereas the latter must be combined with the library in
order to run.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other program
which contains a notice placed by the copyright holder or other authorized
party saying it may be distributed under the terms of this Lesser General
Public License (also called "this License"). Each licensee is addressed as
"you".
A "library" means a collection of software functions and/or data prepared
so as to be conveniently linked with application programs (which use some
of those functions and data) to form executables.
The "Library", below, refers to any such software library or work which has
been distributed under these terms. A "work based on the Library" means either
the Library or any derivative work under copyright law: that is to say, a
work containing the Library or a portion of it, either verbatim or with modifications
and/or translated straightforwardly into another language. (Hereinafter, translation
is included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for making modifications
to it. For a library, complete source code means all the source code for all
modules it contains, plus any associated interface definition files, plus
the scripts used to control compilation and installation of the library.
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running a program
using the Library is not restricted, and output from such a program is covered
only if its contents constitute a work based on the Library (independent of
the use of the Library in a tool for writing it). Whether that is true depends
on what the Library does and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's complete source
code as you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and disclaimer
of warranty; keep intact all the notices that refer to this License and to
the absence of any warranty; and distribute a copy of this License along with
the Library.
You may charge a fee for the physical act of transferring a copy, and you
may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Library or any portion of it,
thus forming a work based on the Library, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all
of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices stating that
you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no charge to all
third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a table of
data to be supplied by an application program that uses the facility, other
than as an argument passed when the facility is invoked, then you must make
a good faith effort to ensure that, in the event an application does not supply
such function or table, the facility still operates, and performs whatever
part of its purpose remains meaningful.
(For example, a function in a library to compute square roots has a purpose
that is entirely well-defined independent of the application. Therefore, Subsection
2d requires that any application-supplied function or table used by this function
must be optional: if the application does not supply it, the square root function
must still compute square roots.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Library, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Library, the distribution of the whole must be
on the terms of this License, whose permissions for other licensees extend
to the entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works based
on the Library.
In addition, mere aggregation of another work not based on the Library with
the Library (or with a work based on the Library) on a volume of a storage
or distribution medium does not bring the other work under the scope of this
License.
3. You may opt to apply the terms of the ordinary GNU General Public License
instead of this License to a given copy of the Library. To do this, you must
alter all the notices that refer to this License, so that they refer to the
ordinary GNU General Public License, version 2, instead of to this License.
(If a newer version than version 2 of the ordinary GNU General Public License
has appeared, then you can specify that version instead if you wish.) Do not
make any other change in these notices.
Once this change is made in a given copy, it is irreversible for that copy,
so the ordinary GNU General Public License applies to all subsequent copies
and derivative works made from that copy.
This option is useful when you wish to copy part of the code of the Library
into a program that is not a library.
4. You may copy and distribute the Library (or a portion or derivative of
it, under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you accompany it with the complete corresponding
machine-readable source code, which must be distributed under the terms of
Sections 1 and 2 above on a medium customarily used for software interchange.
If distribution of object code is made by offering access to copy from a designated
place, then offering equivalent access to copy the source code from the same
place satisfies the requirement to distribute the source code, even though
third parties are not compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the Library, but
is designed to work with the Library by being compiled or linked with it,
is called a "work that uses the Library". Such a work, in isolation, is not
a derivative work of the Library, and therefore falls outside the scope of
this License.
However, linking a "work that uses the Library" with the Library creates an
executable that is a derivative of the Library (because it contains portions
of the Library), rather than a "work that uses the library". The executable
is therefore covered by this License. Section 6 states terms for distribution
of such executables.
When a "work that uses the Library" uses material from a header file that
is part of the Library, the object code for the work may be a derivative work
of the Library even though the source code is not. Whether this is true is
especially significant if the work can be linked without the Library, or if
the work is itself a library. The threshold for this to be true is not precisely
defined by law.
If such an object file uses only numerical parameters, data structure layouts
and accessors, and small macros and small inline functions (ten lines or less
in length), then the use of the object file is unrestricted, regardless of
whether it is legally a derivative work. (Executables containing this object
code plus portions of the Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may distribute
the object code for the work under the terms of Section 6. Any executables
containing that work also fall under Section 6, whether or not they are linked
directly with the Library itself.
6. As an exception to the Sections above, you may also combine or link a "work
that uses the Library" with the Library to produce a work containing portions
of the Library, and distribute that work under terms of your choice, provided
that the terms permit modification of the work for the customer's own use
and reverse engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the Library
is used in it and that the Library and its use are covered by this License.
You must supply a copy of this License. If the work during execution displays
copyright notices, you must include the copyright notice for the Library among
them, as well as a reference directing the user to the copy of this License.
Also, you must do one of these things:
a) Accompany the work with the complete corresponding machine-readable source
code for the Library including whatever changes were used in the work (which
must be distributed under Sections 1 and 2 above); and, if the work is an
executable linked with the Library, with the complete machine-readable "work
that uses the Library", as object code and/or source code, so that the user
can modify the Library and then relink to produce a modified executable containing
the modified Library. (It is understood that the user who changes the contents
of definitions files in the Library will not necessarily be able to recompile
the application to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the Library. A
suitable mechanism is one that (1) uses at run time a copy of the library
already present on the user's computer system, rather than copying library
functions into the executable, and (2) will operate properly with a modified
version of the library, if the user installs one, as long as the modified
version is interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at least three years,
to give the same user the materials specified in Subsection 6a, above, for
a charge no more than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy from a designated
place, offer equivalent access to copy the above specified materials from
the same place.
e) Verify that the user has already received a copy of these materials or
that you have already sent this user a copy.
For an executable, the required form of the "work that uses the Library" must
include any data and utility programs needed for reproducing the executable
from it. However, as a special exception, the materials to be distributed
need not include anything that is normally distributed (in either source or
binary form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component itself
accompanies the executable.
It may happen that this requirement contradicts the license restrictions of
other proprietary libraries that do not normally accompany the operating system.
Such a contradiction means you cannot use both them and the Library together
in an executable that you distribute.
7. You may place library facilities that are a work based on the Library side-by-side
in a single library together with other library facilities not covered by
this License, and distribute such a combined library, provided that the separate
distribution of the work based on the Library and of the other library facilities
is otherwise permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work based on the
Library, uncombined with any other library facilities. This must be distributed
under the terms of the Sections above.
b) Give prominent notice with the combined library of the fact that part of
it is a work based on the Library, and explaining where to find the accompanying
uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute the Library
except as expressly provided under this License. Any attempt otherwise to
copy, modify, sublicense, link with, or distribute the Library is void, and
will automatically terminate your rights under this License. However, parties
who have received copies, or rights, from you under this License will not
have their licenses terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not signed
it. However, nothing else grants you permission to modify or distribute the
Library or its derivative works. These actions are prohibited by law if you
do not accept this License. Therefore, by modifying or distributing the Library
(or any work based on the Library), you indicate your acceptance of this License
to do so, and all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the Library),
the recipient automatically receives a license from the original licensor
to copy, distribute, link with or modify the Library subject to these terms
and conditions. You may not impose any further restrictions on the recipients'
exercise of the rights granted herein. You are not responsible for enforcing
compliance by third parties with this License.
11. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are imposed
on you (whether by court order, agreement or otherwise) that contradict the
conditions of this License, they do not excuse you from the conditions of
this License. If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations, then as
a consequence you may not distribute the Library at all. For example, if a
patent license would not permit royalty-free redistribution of the Library
by all those who receive copies directly or indirectly through you, then the
only way you could satisfy both it and this License would be to refrain entirely
from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system which is implemented by public license practices.
Many people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose
that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in certain
countries either by patents or by copyrighted interfaces, the original copyright
holder who places the Library under this License may add an explicit geographical
distribution limitation excluding those countries, so that distribution is
permitted only in or among countries not thus excluded. In such case, this
License incorporates the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new versions of
the Lesser General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to address
new problems or concerns.
Each version is given a distinguishing version number. If the Library specifies
a version number of this License which applies to it and "any later version",
you have the option of following the terms and conditions either of that version
or of any later version published by the Free Software Foundation. If the
Library does not specify a license version number, you may choose any version
ever published by the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free programs
whose distribution conditions are incompatible with these, write to the author
to ask for permission. For software which is copyrighted by the Free Software
Foundation, write to the Free Software Foundation; we sometimes make exceptions
for this. Our decision will be guided by the two goals of preserving the free
status of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest possible
use to the public, we recommend making it free software that everyone can
redistribute and change. You can do so by permitting redistribution under
these terms (or, alternatively, under the terms of the ordinary General Public
License).
To apply these terms, attach the following notices to the library. It is safest
to attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the "copyright"
line and a pointer to where the full notice is found.
<one line to give the library's name and an idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation; either version 2.1 of the License, or (at your option)
any later version.
This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License along
with this library; if not, write to the Free Software Foundation, Inc., 51
Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the library, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in
the library `Frob' (a library for tweaking knobs) written
by James Random Hacker.
< signature of Ty Coon > , 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
@@ -0,0 +1,24 @@
libFakeKey
A simple library for converting utf8 chars into 'fake' keypresses.
Uses ideas from Fontconfig, libvirtkeys.c, keysym2ucs.c and dasher.
Authored By Matthew Allum <amllum@openedhand.com>
Copyright (C) 2004 OpenedHand
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA.
@@ -0,0 +1,446 @@
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
[This is the first released version of the library GPL. It is numbered 2 because
it goes with version 2 of the ordinary GPL.]
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public Licenses are intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users.
This license, the Library General Public License, applies to some specially
designated Free Software Foundation software, and to any other libraries whose
authors decide to use it. You can use it for your libraries, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for this service if you
wish), that you receive source code or can get it if you want it, that you
can change the software or use pieces of it in new free programs; and that
you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to
deny you these rights or to ask you to surrender the rights. These restrictions
translate to certain responsibilities for you if you distribute copies of
the library, or if you modify it.
For example, if you distribute copies of the library, whether gratis or for
a fee, you must give the recipients all the rights that we gave you. You must
make sure that they, too, receive or can get the source code. If you link
a program with the library, you must provide complete object files to the
recipients so that they can relink them with the library, after making changes
to the library and recompiling it. And you must show them these terms so they
know their rights.
Our method of protecting your rights has two steps: (1) copyright the library,
and (2) offer you this license which gives you legal permission to copy, distribute
and/or modify the library.
Also, for each distributor's protection, we want to make certain that everyone
understands that there is no warranty for this free library. If the library
is modified by someone else and passed on, we want its recipients to know
that what they have is not the original version, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We
wish to avoid the danger that companies distributing free software will individually
obtain patent licenses, thus in effect transforming the program into proprietary
software. To prevent this, we have made it clear that any patent must be licensed
for everyone's free use or not licensed at all.
Most GNU software, including some libraries, is covered by the ordinary GNU
General Public License, which was designed for utility programs. This license,
the GNU Library General Public License, applies to certain designated libraries.
This license is quite different from the ordinary one; be sure to read it
in full, and don't assume that anything in it is the same as in the ordinary
license.
The reason we have a separate public license for some libraries is that they
blur the distinction we usually make between modifying or adding to a program
and simply using it. Linking a program with a library, without changing the
library, is in some sense simply using the library, and is analogous to running
a utility program or application program. However, in a textual and legal
sense, the linked executable is a combined work, a derivative of the original
library, and the ordinary General Public License treats it as such.
Because of this blurred distinction, using the ordinary General Public License
for libraries did not effectively promote software sharing, because most developers
did not use the libraries. We concluded that weaker conditions might promote
sharing better.
However, unrestricted linking of non-free programs would deprive the users
of those programs of all benefit from the free status of the libraries themselves.
This Library General Public License is intended to permit developers of non-free
programs to use free libraries, while preserving your freedom as a user of
such programs to change the free libraries that are incorporated in them.
(We have not seen how to achieve this as regards changes in header files,
but we have achieved it as regards changes in the actual functions of the
Library.) The hope is that this will lead to faster development of free libraries.
The precise terms and conditions for copying, distribution and modification
follow. Pay close attention to the difference between a "work based on the
library" and a "work that uses the library". The former contains code derived
from the library, while the latter only works together with the library.
Note that it is possible for a library to be covered by the ordinary General
Public License rather than by this special one.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which contains a
notice placed by the copyright holder or other authorized party saying it
may be distributed under the terms of this Library General Public License
(also called "this License"). Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data prepared
so as to be conveniently linked with application programs (which use some
of those functions and data) to form executables.
The "Library", below, refers to any such software library or work which has
been distributed under these terms. A "work based on the Library" means either
the Library or any derivative work under copyright law: that is to say, a
work containing the Library or a portion of it, either verbatim or with modifications
and/or translated straightforwardly into another language. (Hereinafter, translation
is included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for making modifications
to it. For a library, complete source code means all the source code for all
modules it contains, plus any associated interface definition files, plus
the scripts used to control compilation and installation of the library.
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running a program
using the Library is not restricted, and output from such a program is covered
only if its contents constitute a work based on the Library (independent of
the use of the Library in a tool for writing it). Whether that is true depends
on what the Library does and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's complete source
code as you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and disclaimer
of warranty; keep intact all the notices that refer to this License and to
the absence of any warranty; and distribute a copy of this License along with
the Library.
You may charge a fee for the physical act of transferring a copy, and you
may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Library or any portion of it,
thus forming a work based on the Library, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all
of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices stating that
you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no charge to all
third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a table of
data to be supplied by an application program that uses the facility, other
than as an argument passed when the facility is invoked, then you must make
a good faith effort to ensure that, in the event an application does not supply
such function or table, the facility still operates, and performs whatever
part of its purpose remains meaningful.
(For example, a function in a library to compute square roots has a purpose
that is entirely well-defined independent of the application. Therefore, Subsection
2d requires that any application-supplied function or table used by this function
must be optional: if the application does not supply it, the square root function
must still compute square roots.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Library, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Library, the distribution of the whole must be
on the terms of this License, whose permissions for other licensees extend
to the entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works based
on the Library.
In addition, mere aggregation of another work not based on the Library with
the Library (or with a work based on the Library) on a volume of a storage
or distribution medium does not bring the other work under the scope of this
License.
3. You may opt to apply the terms of the ordinary GNU General Public License
instead of this License to a given copy of the Library. To do this, you must
alter all the notices that refer to this License, so that they refer to the
ordinary GNU General Public License, version 2, instead of to this License.
(If a newer version than version 2 of the ordinary GNU General Public License
has appeared, then you can specify that version instead if you wish.) Do not
make any other change in these notices.
Once this change is made in a given copy, it is irreversible for that copy,
so the ordinary GNU General Public License applies to all subsequent copies
and derivative works made from that copy.
This option is useful when you wish to copy part of the code of the Library
into a program that is not a library.
4. You may copy and distribute the Library (or a portion or derivative of
it, under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you accompany it with the complete corresponding
machine-readable source code, which must be distributed under the terms of
Sections 1 and 2 above on a medium customarily used for software interchange.
If distribution of object code is made by offering access to copy from a designated
place, then offering equivalent access to copy the source code from the same
place satisfies the requirement to distribute the source code, even though
third parties are not compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the Library, but
is designed to work with the Library by being compiled or linked with it,
is called a "work that uses the Library". Such a work, in isolation, is not
a derivative work of the Library, and therefore falls outside the scope of
this License.
However, linking a "work that uses the Library" with the Library creates an
executable that is a derivative of the Library (because it contains portions
of the Library), rather than a "work that uses the library". The executable
is therefore covered by this License. Section 6 states terms for distribution
of such executables.
When a "work that uses the Library" uses material from a header file that
is part of the Library, the object code for the work may be a derivative work
of the Library even though the source code is not. Whether this is true is
especially significant if the work can be linked without the Library, or if
the work is itself a library. The threshold for this to be true is not precisely
defined by law.
If such an object file uses only numerical parameters, data structure layouts
and accessors, and small macros and small inline functions (ten lines or less
in length), then the use of the object file is unrestricted, regardless of
whether it is legally a derivative work. (Executables containing this object
code plus portions of the Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may distribute
the object code for the work under the terms of Section 6. Any executables
containing that work also fall under Section 6, whether or not they are linked
directly with the Library itself.
6. As an exception to the Sections above, you may also compile or link a "work
that uses the Library" with the Library to produce a work containing portions
of the Library, and distribute that work under terms of your choice, provided
that the terms permit modification of the work for the customer's own use
and reverse engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the Library
is used in it and that the Library and its use are covered by this License.
You must supply a copy of this License. If the work during execution displays
copyright notices, you must include the copyright notice for the Library among
them, as well as a reference directing the user to the copy of this License.
Also, you must do one of these things:
a) Accompany the work with the complete corresponding machine-readable source
code for the Library including whatever changes were used in the work (which
must be distributed under Sections 1 and 2 above); and, if the work is an
executable linked with the Library, with the complete machine-readable "work
that uses the Library", as object code and/or source code, so that the user
can modify the Library and then relink to produce a modified executable containing
the modified Library. (It is understood that the user who changes the contents
of definitions files in the Library will not necessarily be able to recompile
the application to use the modified definitions.)
b) Accompany the work with a written offer, valid for at least three years,
to give the same user the materials specified in Subsection 6a, above, for
a charge no more than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy from a designated
place, offer equivalent access to copy the above specified materials from
the same place.
d) Verify that the user has already received a copy of these materials or
that you have already sent this user a copy.
For an executable, the required form of the "work that uses the Library" must
include any data and utility programs needed for reproducing the executable
from it. However, as a special exception, the source code distributed need
not include anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the operating
system on which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license restrictions of
other proprietary libraries that do not normally accompany the operating system.
Such a contradiction means you cannot use both them and the Library together
in an executable that you distribute.
7. You may place library facilities that are a work based on the Library side-by-side
in a single library together with other library facilities not covered by
this License, and distribute such a combined library, provided that the separate
distribution of the work based on the Library and of the other library facilities
is otherwise permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work based on the
Library, uncombined with any other library facilities. This must be distributed
under the terms of the Sections above.
b) Give prominent notice with the combined library of the fact that part of
it is a work based on the Library, and explaining where to find the accompanying
uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute the Library
except as expressly provided under this License. Any attempt otherwise to
copy, modify, sublicense, link with, or distribute the Library is void, and
will automatically terminate your rights under this License. However, parties
who have received copies, or rights, from you under this License will not
have their licenses terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not signed
it. However, nothing else grants you permission to modify or distribute the
Library or its derivative works. These actions are prohibited by law if you
do not accept this License. Therefore, by modifying or distributing the Library
(or any work based on the Library), you indicate your acceptance of this License
to do so, and all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the Library),
the recipient automatically receives a license from the original licensor
to copy, distribute, link with or modify the Library subject to these terms
and conditions. You may not impose any further restrictions on the recipients'
exercise of the rights granted herein. You are not responsible for enforcing
compliance by third parties to this License.
11. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are imposed
on you (whether by court order, agreement or otherwise) that contradict the
conditions of this License, they do not excuse you from the conditions of
this License. If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations, then as
a consequence you may not distribute the Library at all. For example, if a
patent license would not permit royalty-free redistribution of the Library
by all those who receive copies directly or indirectly through you, then the
only way you could satisfy both it and this License would be to refrain entirely
from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system which is implemented by public license practices.
Many people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose
that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in certain
countries either by patents or by copyrighted interfaces, the original copyright
holder who places the Library under this License may add an explicit geographical
distribution limitation excluding those countries, so that distribution is
permitted only in or among countries not thus excluded. In such case, this
License incorporates the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new versions of
the Library General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to address
new problems or concerns.
Each version is given a distinguishing version number. If the Library specifies
a version number of this License which applies to it and "any later version",
you have the option of following the terms and conditions either of that version
or of any later version published by the Free Software Foundation. If the
Library does not specify a license version number, you may choose any version
ever published by the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free programs
whose distribution conditions are incompatible with these, write to the author
to ask for permission. For software which is copyrighted by the Free Software
Foundation, write to the Free Software Foundation; we sometimes make exceptions
for this. Our decision will be guided by the two goals of preserving the free
status of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest possible
use to the public, we recommend making it free software that everyone can
redistribute and change. You can do so by permitting redistribution under
these terms (or, alternatively, under the terms of the ordinary General Public
License).
To apply these terms, attach the following notices to the library. It is safest
to attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the "copyright"
line and a pointer to where the full notice is found.
one line to give the library's name and an idea of what it does.
Copyright (C) year name of author
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Library General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
This library is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more
details.
You should have received a copy of the GNU Library General Public License
along with this library; if not, write to the Free Software Foundation, Inc.,
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the library, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in
the library `Frob' (a library for tweaking knobs) written
by James Random Hacker.
signature of Ty Coon, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
@@ -0,0 +1,26 @@
Copyright (c) <year> <owner>. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
Loaded 100 of 294 files, more files were not shown because too many files have changed in this diff. Show more