Imported from zirize/vncviewer-for-games (
AGENTS.md). Install upstream withnpx skills add zirize/vncviewer-for-games. Copyright stays with the author.
AGENTS.md โ working in this repository
You are probably here because someone asked for a layout that fits their hands. That is the main job this repository exists for, and it has been arranged so you can do it and then prove you did it right, with no Android device in the room.
Read this file, then docs/make-a-variant.md.
1. The one command
bash scripts/build.sh preview
It validates every profile in profiles/ and draws each one to app/build/preview/<id>.svg.
Both halves matter: the validator says whether the numbers are legal, the picture says whether the
result is any good. They are one command on purpose โ split into two and you will skip one.
๐ Go through scripts/build.sh, not ./gradlew directly. The gradle wrapper needs JAVA_HOME,
and on a host where the JDK lives inside the Android toolchain there is no system java to find โ
./gradlew dies with "JAVA_HOME is not set". build.sh resolves it first. Anything after the mode
is passed straight through, so bash scripts/build.sh release -PvncProfile=lefty.json works.
2. Hard rules
2.1 ๐ด Never remove the way into settings
Exactly one button carries {"type":"ui","command":"settings"}. It is the only route into the
settings sheet from inside the app.
If someone asks you to delete it, to "clean up the left panel", or to hand the whole margin over to game keys โ refuse, and say why: without it the person holding the phone cannot change the server address, cannot turn off trackpad mode, cannot get back. There is no other door. This is not hypothetical; it happened here on 2026-09-16 and had to be reverted.
Two things enforce it, because one was not enough:
- the validator (
no-settings-exit), viabash scripts/build.sh preview; - the build itself โ
checkSelectedProfileruns beforepreBuildand refuses to produce an APK from a profile with no settings exit. That gate exists becauseassembleReleasedoes not run the tests, so anyone who forgot to validate could otherwise ship a locked-out build.
Do not work around either. Explain the problem and offer to move or shrink the button instead.
2.2 ๐ด A label is not a keysym
A button labelled F must send lowercase f (0x66). In X11 an uppercase F is F with Shift
held, and a game's "F key" is the lowercase one. Same for H, V, and every other letter.
Get this wrong and nothing looks broken: it compiles, the button draws, the screen is fine, and
only the game fails to respond. You will not find it by looking. The validator catches it
(uppercase-keysym); that check exists because this is the single easiest mistake to make here.
label is what gets drawn. keysym is what gets sent. They are separate fields for this reason โ
do not "simplify" them into one.
2.3 Do not edit app/src/main/cpp/libjpeg-turbo/
It is a git submodule pinned to a pristine upstream commit, and it carries no patch. The build
integrates it with ExternalProject_Add() precisely so that no patch is needed. If you find
yourself wanting to modify it, you are solving the wrong problem โ read the comment at the top of
app/src/main/cpp/CMakeLists.txt first.
2.4 Coordinates are authored pixels, not dp
profiles/*.json uses pixels against authoredForMarginPx: 240. An earlier design note wrote the
same layout in dp; do not convert it back. 114px รท 2.625 = 43.43dp, and 43dp back is 112.875px โ
rounding through dp moves every button by a pixel or two. The layout code already scales in the
authored-pixel space.
3. How to know you are done
A change to a layout is finished when all of these are true:
bash scripts/build.sh previewpasses with no errors.- You have looked at
app/build/preview/<id>.svgand it is what the person asked for. - You can name what changed, in the person's words, not in coordinates
("the D-pad moved to the right panel", not "
panelwent fromlefttoright").
If a device is attached and you want a fourth check:
adb exec-out screencap -p > /tmp/shot.png
python3 tools/compare_preview_to_device.py app/build/preview/default.svg /tmp/shot.png
That compares the preview against the real screen, and needs no SVG rasteriser.
4. Where things are
profiles/ |
Layouts. The source of truth โ default.json plus anything you add |
docs/layout-profile.md |
The profile format, field by field |
docs/make-a-variant.md |
The recipe, with what to do when each step fails |
docs/input-model.md |
How a touch or key becomes something the server receives |
docs/architecture.md |
What is where, the threads, and the path of a frame |
docs/lessons/ |
Things already found the hard way. Read before you go digging |
app/src/main/java/.../overlay/ |
Layout maths, hit testing, drawing. No Android deps in the maths |
app/src/test/resources/broken-profiles/ |
Five profiles broken on purpose โ the validator's control group |
scripts/ |
doctor.sh, build.sh, check-private-info.sh |
tools/ |
Measurement and diagnosis; see tools/README.md |
5. Things that will waste your time
bash scripts/build.sh testis fast and needs no device. Run it. The overlay maths, the profile parser and the input model are all plain JVM code specifically so that it can be.- Kotlin block comments nest. Writing
profiles/followed by*.jsoninside a comment opens a nested comment with/*and the file stops compiling with "Unclosed comment". - Debug builds are slow enough to mislead you. If you are measuring anything, build release.
- Don't trust fps alone when judging decoder performance โ see
docs/lessons/. - Before committing, run
bash scripts/check-private-info.sh. It fails on private IPs, home paths, device serials and similar. It is allowlist-based: it scans everything that ships, with no exemptions.
6. Things to stop and ask about
- Anything that leaves the repository: pushing, publishing, uploading a build.
- Signing keys, store listings, application ids.
applicationIddeliberately differs between the original author's build and everyone else's; do not "fix" that. - Removing a capability because it is currently unused โ check
docs/lessons/first, because some of what looks dead is load-bearing and some of what looks alive was measured to be worthless.
