mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Merge pull request #837 from permissionlesstech/codex/readme-screenshot-skill
Add Android README screenshot studio skill
This commit is contained in:
commit
13669b0d86
249
.agents/skills/android-readme-screenshot-studio/SKILL.md
Normal file
249
.agents/skills/android-readme-screenshot-studio/SKILL.md
Normal file
@ -0,0 +1,249 @@
|
||||
---
|
||||
name: android-readme-screenshot-studio
|
||||
description: Create or refresh polished, high-resolution screenshots of the Bitchat Android app for README and repository showcase use. Use this skill whenever a user asks for README screenshots, app-store-like repository images, a populated mesh-chat showcase, voice-note or media conversation captures, a geohash globe image, higher-resolution Pixel captures, or a PR that adds or replaces documentation screenshots. It owns the complete workflow from latest-main isolation and deterministic synthetic fixtures through real app rendering, visual inspection, system-chrome cropping, README asset updates, clean builds, and an optional PR. Do not use it for before/after UI regression evidence, which belongs to android-ui-visual-review, or for physical mesh behavior, which belongs to mesh-lab.
|
||||
compatibility: Requires git, gh, the Android SDK and emulator, adb, Java/Gradle, Python 3, and image inspection support. FFmpeg is useful for capture-only media preparation.
|
||||
---
|
||||
|
||||
# Android README Screenshot Studio
|
||||
|
||||
Create repository screenshots from the real Bitchat Android UI, not from a
|
||||
drawn mockup. The result should look intentional enough for the top of the
|
||||
README while remaining reproducible, synthetic, and honest about what a static
|
||||
emulator capture proves.
|
||||
|
||||
## Resolve the brief
|
||||
|
||||
Extract as much as possible from the conversation before asking questions.
|
||||
Confirm or infer:
|
||||
|
||||
1. Which surfaces are needed, such as mesh chat and geohash globe.
|
||||
2. The exact visible state and ordering. Treat phrases like “photo, messages,
|
||||
three voices, thumbs-up” as a chronological contract rather than a loose
|
||||
suggestion.
|
||||
3. Whether existing README images should be preserved, replaced, or added.
|
||||
4. The target base. Default repository work to the latest `origin/main`.
|
||||
5. Whether GitHub publication and merge are authorized. A request to “open a
|
||||
PR and merge it” authorizes both; otherwise do not merge.
|
||||
|
||||
Do not invent the subject of a requested photo. Nicknames, channel names, or
|
||||
previous fixture copy are not sufficient justification for choosing an outdoor,
|
||||
urban, political, or personal scene. Reuse an existing rights-safe asset when
|
||||
the subject should remain stable, or ask for the intended subject. If the user
|
||||
explicitly approves synthetic imagery, disclose it and keep its source
|
||||
capture-only unless they request a committed asset.
|
||||
|
||||
Read [references/showcase-recipes.md](references/showcase-recipes.md) for every
|
||||
run. It contains the concrete mesh-chat and globe recipes, framing guidance,
|
||||
and the final acceptance checklist.
|
||||
|
||||
When a populated screen requires a debug fixture, also read
|
||||
[../android-ui-visual-review/references/fixture-recipes.md](../android-ui-visual-review/references/fixture-recipes.md).
|
||||
|
||||
## Work in a fresh tree
|
||||
|
||||
Protect the user's active checkout:
|
||||
|
||||
1. Inspect `git status` without modifying it.
|
||||
2. Fetch `origin/main`.
|
||||
3. Create a new `codex/` branch in a temporary worktree rooted at the current
|
||||
`origin/main`.
|
||||
4. Keep screenshots and raw captures in a separate temporary artifact
|
||||
directory so cleanup or checkout operations cannot remove them.
|
||||
|
||||
Do not switch the user's original checkout, reuse a dirty branch, or mix an
|
||||
unrelated PR into the screenshot change. If the request continues an existing
|
||||
screenshot PR, reuse its already-isolated worktree only after verifying its
|
||||
head and base.
|
||||
|
||||
## Establish the capture contract
|
||||
|
||||
Before building, write a compact local matrix containing:
|
||||
|
||||
- surface and navigation path;
|
||||
- chronological fixture contents;
|
||||
- expected visible top and bottom rows;
|
||||
- Pixel profile, physical resolution, logical width, theme, and locale;
|
||||
- crop policy and final asset dimensions;
|
||||
- existing asset path and README reference;
|
||||
- behaviors the static screenshot does not prove.
|
||||
|
||||
Use the current production UI and latest `main` interaction model. Trace the
|
||||
screen entry point and state source before adding a fixture. A beautiful capture
|
||||
of a stale or fake UI is not acceptable.
|
||||
|
||||
## Use a high-resolution Pixel canvas
|
||||
|
||||
Prefer the newest stable Android runtime installed locally and a large Pixel Pro
|
||||
profile. For the current README style, target at least:
|
||||
|
||||
- a 448 dp logical width;
|
||||
- 1344 px physical width;
|
||||
- a native portrait height near 2992 px;
|
||||
- 480 dpi or the profile's native density.
|
||||
|
||||
Pixel 9 Pro XL at 1344×2992 and 480 dpi is a known-good baseline, not a
|
||||
hard-coded requirement. A newer stable large Pixel profile is acceptable when
|
||||
its guest properties are verified.
|
||||
|
||||
After boot, record the guest values with an explicit emulator selector:
|
||||
|
||||
```sh
|
||||
adb -s "$ANDROID_README_SERIAL" shell getprop ro.build.version.release
|
||||
adb -s "$ANDROID_README_SERIAL" shell getprop ro.build.version.sdk
|
||||
adb -s "$ANDROID_README_SERIAL" shell getprop ro.build.version.security_patch
|
||||
adb -s "$ANDROID_README_SERIAL" shell wm size
|
||||
adb -s "$ANDROID_README_SERIAL" shell wm density
|
||||
```
|
||||
|
||||
Never publish emulator selectors, AVD names, local paths, usernames, IP
|
||||
addresses, or other machine identifiers.
|
||||
|
||||
## Build a deterministic showcase fixture
|
||||
|
||||
Launch the Activity before injecting process-local state. Prefer existing debug
|
||||
hooks. If they cannot express the composition, add the smallest temporary
|
||||
command under:
|
||||
|
||||
```text
|
||||
app/src/debug/java/com/bitchat/android/testhook/
|
||||
```
|
||||
|
||||
The fixture should:
|
||||
|
||||
- use synthetic names, peer IDs, message IDs, and copy;
|
||||
- use the real local mesh peer ID for self-authored messages;
|
||||
- use a fixed epoch so timestamps and ordering are stable;
|
||||
- insert records in the exact requested chronology;
|
||||
- report structured counts through the test-hook result file;
|
||||
- copy capture-only media into the app's cache or files directory;
|
||||
- populate only the peers and state needed for the header;
|
||||
- avoid persistence unless persistence itself is the subject.
|
||||
|
||||
For voice notes, route real audio files through the app's waveform extractor.
|
||||
Use short, distinct, locally synthesized speech clips or other rights-safe
|
||||
speech audio. Never draw a decorative waveform and call it speech. Wait for
|
||||
asynchronous decoding before capture, then inspect that pauses and syllable
|
||||
envelopes look plausibly different between notes.
|
||||
|
||||
For image attachments, use a rights-safe existing asset or an explicitly
|
||||
approved synthetic source. Keep fixture media outside production source sets
|
||||
and remove every capture-only hook before committing.
|
||||
|
||||
## Capture from the real app
|
||||
|
||||
Build and install the ABI-matching debug APK, satisfy onboarding and
|
||||
permissions, inject the fixture, and navigate to the intended surface.
|
||||
|
||||
Capture directly:
|
||||
|
||||
```sh
|
||||
adb -s "$ANDROID_README_SERIAL" exec-out screencap -p > "$ARTIFACT_PATH"
|
||||
```
|
||||
|
||||
Inspect the full screenshot immediately. Check message count and order,
|
||||
nickname ownership, peer count, waveform variety, image visibility, globe
|
||||
center, grid precision, clipping, and composer placement.
|
||||
|
||||
Crop only Android system chrome. Preserve Bitchat's app header, translucent
|
||||
overlap, content, and composer. Derive the crop from the observed status and
|
||||
navigation insets; do not blindly reuse pixel offsets from a different profile.
|
||||
Keep every final README screenshot in a matched portrait size.
|
||||
|
||||
Use image inspection after the crop. File dimensions and a successful ADB
|
||||
command do not prove that the desired composition is visible.
|
||||
|
||||
## Update repository assets
|
||||
|
||||
Discover the current README references before writing. Prefer stable paths under
|
||||
`docs/screenshots/` and replace only the assets the user requested.
|
||||
|
||||
When adding a showcase section:
|
||||
|
||||
- keep the layout readable on GitHub;
|
||||
- give every image meaningful alt text;
|
||||
- use relative repository paths;
|
||||
- avoid machine-generated cache files or capture sources;
|
||||
- keep paired screenshots at identical dimensions.
|
||||
|
||||
Run the bundled validator for every final asset:
|
||||
|
||||
```sh
|
||||
python3 \
|
||||
.agents/skills/android-readme-screenshot-studio/scripts/validate_readme_screenshots.py \
|
||||
--repo-root . \
|
||||
--readme README.md \
|
||||
--require-same-size \
|
||||
--asset docs/screenshots/readme-mesh-chat.png \
|
||||
--asset docs/screenshots/readme-geohash-globe.png
|
||||
```
|
||||
|
||||
When replacing only one image in an existing pair, pass both the changed and
|
||||
unchanged assets with `--require-same-size`, and verify the unchanged asset's
|
||||
checksum. Pass only one asset and omit `--require-same-size` only when the
|
||||
README has no paired screenshot to preserve.
|
||||
|
||||
## Remove the fixture and verify cleanly
|
||||
|
||||
Before committing:
|
||||
|
||||
1. Remove temporary imports, commands, helpers, resources, and fixture media
|
||||
with a focused patch.
|
||||
2. Verify `git diff -- app/src/debug` is empty.
|
||||
3. Verify `git status --short` lists only the intended README and screenshot
|
||||
files.
|
||||
4. Run `git diff --check`.
|
||||
5. Run `./gradlew assembleDebug` after fixture removal.
|
||||
6. Re-run the screenshot validator.
|
||||
7. Confirm the user's original checkout is still untouched.
|
||||
|
||||
The final commit must not contain synthetic peer data, generated photo sources,
|
||||
audio clips, ADB outputs, emulator configuration, or local capture reports
|
||||
unless the user separately requested those artifacts in the repository.
|
||||
|
||||
## Record honest evidence
|
||||
|
||||
Use the capture manifest and report format from
|
||||
`../android-ui-visual-review/` when before/after evidence is useful. For a
|
||||
README-only change with no production UI delta, identical before/after images
|
||||
are acceptable when explicitly labeled “no production UI delta.”
|
||||
|
||||
State limitations plainly:
|
||||
|
||||
- a populated mesh timeline proves rendering, not physical message delivery;
|
||||
- a voice row proves waveform rendering, not audio playback;
|
||||
- an attachment proves image rendering, not media transfer;
|
||||
- a globe proves picker state, not live location or relay behavior.
|
||||
|
||||
## Commit, publish, and optionally merge
|
||||
|
||||
GitHub writes require user authorization. When authorized:
|
||||
|
||||
1. Stage only intended files.
|
||||
2. Commit without overriding author or committer identity.
|
||||
3. Push the `codex/` branch.
|
||||
4. Use `gh pr create` or update the existing PR.
|
||||
5. Describe the exact capture sequence, Pixel/runtime facts, synthetic fixture
|
||||
disclosure, validation commands, and limitations.
|
||||
6. Verify the PR head and checks with `gh pr view` and `gh pr checks`.
|
||||
7. Merge only when the user explicitly requested it and required checks allow
|
||||
it. Prefer the repository's normal merge strategy and use `gh`.
|
||||
8. Verify the merged state and resulting `main` commit.
|
||||
|
||||
When GitHub publication is not authorized, leave the finished commit or local
|
||||
change in the isolated worktree and hand back its branch and artifact paths.
|
||||
Do not silently push it.
|
||||
|
||||
Do not place local paths, device selectors, generated-image paths, or personal
|
||||
machine details in commits, PR text, comments, or merge messages.
|
||||
|
||||
## Final handoff
|
||||
|
||||
Lead with the outcome and include:
|
||||
|
||||
- PR and merge URL or status;
|
||||
- final screenshot paths and dimensions;
|
||||
- one-line composition summary per surface;
|
||||
- build and validator results;
|
||||
- synthetic media disclosure;
|
||||
- current CI state or merged commit;
|
||||
- confirmation that the original checkout was not modified.
|
||||
@ -0,0 +1,43 @@
|
||||
{
|
||||
"skill_name": "android-readme-screenshot-studio",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "Refresh the Bitchat README with two polished screenshots from the latest main UI: a populated mesh chat and the geohash globe centered on the Middle East without zooming. Use a large high-resolution Pixel, open a PR, and merge it after checks pass.",
|
||||
"expected_output": "The agent works in a fresh latest-main worktree, captures the real app on a verified large Pixel profile, uses deterministic synthetic state, centers the globe without changing whole-Earth scale, removes capture hooks, validates the assets, opens a PR, waits for checks, and merges only because the prompt explicitly authorizes it.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Uses a fresh worktree rooted at current origin/main and leaves the original checkout untouched.",
|
||||
"Captures from the real Android UI on a verified high-resolution large Pixel profile.",
|
||||
"Keeps the requested Middle East focus while preserving whole-Earth zoom.",
|
||||
"Removes temporary fixtures and passes a clean debug build before committing.",
|
||||
"Uses gh for the authorized PR and merge without publishing machine identifiers."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prompt": "Replace only the README chat image. It should read top to bottom as: a photo, four short messages back and forth, three voice messages back and forth, then one thumbs-up. Keep the current globe exactly as-is and make the speech waveforms look natural.",
|
||||
"expected_output": "The agent treats the requested order and counts as exact, asks or resolves the photo subject instead of inventing it, uses the real waveform extractor on three distinct speech clips, preserves the globe asset, visually inspects the final crop, and leaves only the chat PNG changed.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Implements exactly one photo, four alternating texts, three alternating voice notes, and one final thumbs-up.",
|
||||
"Does not infer a photo subject from nicknames or unrelated fixture copy.",
|
||||
"Produces waveforms from real rights-safe speech audio through the app extractor rather than drawing bars.",
|
||||
"Preserves the globe asset and verifies that only the chat screenshot remains in the diff.",
|
||||
"Removes the debug fixture before the final clean build."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prompt": "Show me before and after screenshots for the Compose header changes in PR #412 at 320 dp and 411 dp, and post the comparison as a PR comment.",
|
||||
"expected_output": "The agent recognizes that this is visual regression evidence rather than README marketing capture and routes the task to android-ui-visual-review instead of applying the README screenshot workflow.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Routes the request to android-ui-visual-review.",
|
||||
"Does not replace README assets.",
|
||||
"Uses the PR merge-base and paired before/after capture workflow.",
|
||||
"Publishes only the requested PR comment rather than opening an unrelated screenshot PR."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,172 @@
|
||||
# README showcase recipes
|
||||
|
||||
Use these recipes as composition guidance, then adapt them to the user's exact
|
||||
request and the current UI. The requested chronology and framing always win over
|
||||
the examples.
|
||||
|
||||
## Mesh-chat showcase
|
||||
|
||||
### Visual goal
|
||||
|
||||
Make the screen read as a real conversation at a glance:
|
||||
|
||||
1. one strong media anchor near the top;
|
||||
2. a short text exchange with alternating senders;
|
||||
3. a compact voice-note exchange with visibly different speech envelopes;
|
||||
4. a small final reaction or acknowledgement;
|
||||
5. the app header and composer framing the timeline.
|
||||
|
||||
Avoid stuffing every supported feature into one frame. The screenshot should
|
||||
show capability through hierarchy, not through maximum item count.
|
||||
|
||||
### Known-good fixture shape
|
||||
|
||||
For a request like “photo, messages, three voices, thumbs-up,” use exactly:
|
||||
|
||||
| Order | Type | Sender |
|
||||
|---|---|---|
|
||||
| 1 | Image | self or remote, according to the story |
|
||||
| 2 | Short text | other sender |
|
||||
| 3 | Short text | alternating sender |
|
||||
| 4 | Short text | alternating sender |
|
||||
| 5 | Short text | alternating sender |
|
||||
| 6 | Voice note | sender A |
|
||||
| 7 | Voice note | sender B |
|
||||
| 8 | Voice note | sender A |
|
||||
| 9 | `👍` | sender B |
|
||||
|
||||
Keep copy conversational and concise. Use synthetic names and avoid real
|
||||
locations, contacts, identities, or sensitive content.
|
||||
|
||||
For public mesh rendering, pass the current mesh peer ID as `senderPeerID` on
|
||||
self-authored messages. This exercises the same ownership and color path as
|
||||
production. Alternate sender IDs so every back-and-forth row renders its sender
|
||||
header instead of being grouped away.
|
||||
|
||||
### Photo framing
|
||||
|
||||
A portrait or tall crop can let the latest rows stay visible while the older
|
||||
photo slides partially behind Bitchat's translucent header. This is visually
|
||||
useful only when the photo subject remains legible.
|
||||
|
||||
- Preserve the app's own rounded image treatment.
|
||||
- Do not bake UI chrome into the photo.
|
||||
- Avoid important content under the header overlap.
|
||||
- Do not select a photo subject from nicknames alone.
|
||||
- If generating a synthetic photo, obtain or infer subject approval first,
|
||||
disclose generation, and keep the source outside the final commit unless
|
||||
requested.
|
||||
|
||||
### Natural voice rows
|
||||
|
||||
Prepare three short speech clips with different durations, pauses, and cadence.
|
||||
Locally available offline TTS plus an audio transcoder is sufficient. Use the
|
||||
same audio format the app normally records or plays, such as M4A.
|
||||
|
||||
After copying the files into an app-readable location, create
|
||||
`BitchatMessageType.Audio` messages that point to those actual files. Let
|
||||
`AudioWaveformExtractor` and `VoiceWaveformCache` produce the bars.
|
||||
|
||||
Reject the capture when:
|
||||
|
||||
- all three envelopes look identical;
|
||||
- the bars are uniform or sinusoidal rather than speech-like;
|
||||
- duration labels are missing or implausible;
|
||||
- a waveform is clipped by the screen edge;
|
||||
- a temporary progress or cancel state is visible.
|
||||
|
||||
### Layout tuning
|
||||
|
||||
Inject the complete fixture in one operation so the list adopts it as history
|
||||
instead of animating rows during capture. Let the reverse-layout list settle at
|
||||
the newest message.
|
||||
|
||||
If the oldest photo is not partially visible, prefer changing its aspect ratio
|
||||
or the number of short text rows over manually scrolling to an unstable offset.
|
||||
If the newest reaction falls behind the composer, shorten earlier content or
|
||||
reduce media height. Do not crop app content to solve a fixture problem.
|
||||
|
||||
## Geohash globe showcase
|
||||
|
||||
### Visual goal
|
||||
|
||||
Show:
|
||||
|
||||
- the entire Earth;
|
||||
- a readable geohash grid;
|
||||
- the selected coarse cell;
|
||||
- the requested geographic focus beneath the selection crosshair;
|
||||
- the picker hint and precision controls.
|
||||
|
||||
Use the current picker Activity and renderer. Do not composite a globe or grid
|
||||
outside the app.
|
||||
|
||||
### Focus without zoom
|
||||
|
||||
When a user says “focus on the Middle East, no zoom”:
|
||||
|
||||
1. open or seed the picker at a geohash centered on the requested area;
|
||||
2. allow the globe to center on that location;
|
||||
3. reduce precision to the same coarsest whole-Earth level used for the
|
||||
showcase;
|
||||
4. preserve camera distance while confirming the center moved;
|
||||
5. capture with the whole globe still visible.
|
||||
|
||||
The exact seed may change with picker implementation. Validate against visible
|
||||
geography and the selected geohash label rather than assuming the seed worked.
|
||||
|
||||
Reject the capture when:
|
||||
|
||||
- the requested region is off-center;
|
||||
- Earth is clipped;
|
||||
- reducing precision also changed the camera distance against the brief;
|
||||
- the selected cell or crosshair is illegible;
|
||||
- controls overlap the globe;
|
||||
- stale system bars remain in the final README asset.
|
||||
|
||||
## Crop and output
|
||||
|
||||
Capture the full physical screen first. Determine the status-bar and
|
||||
navigation-bar insets from the current profile, then crop those insets only.
|
||||
|
||||
For a Pixel 9 Pro XL profile at 1344×2992, a 1344×2780 final image was a
|
||||
known-good result in one verified run. Treat those numbers as evidence, not as a
|
||||
universal crop rule.
|
||||
|
||||
Every paired README image should:
|
||||
|
||||
- be a valid PNG;
|
||||
- share width and height;
|
||||
- retain the app header and composer or controls;
|
||||
- exclude Android status and gesture/navigation chrome;
|
||||
- remain sharp at GitHub's rendered width.
|
||||
|
||||
## Acceptance checklist
|
||||
|
||||
### Chat
|
||||
|
||||
- [ ] Media subject matches the user's brief.
|
||||
- [ ] Message chronology exactly matches the requested sequence.
|
||||
- [ ] Sender ownership and alternation are correct.
|
||||
- [ ] Voice-note count is exact.
|
||||
- [ ] Waveforms were extracted from real speech audio and look distinct.
|
||||
- [ ] Final reaction is visible above the composer.
|
||||
- [ ] Header peer count and nickname are synthetic and intentional.
|
||||
|
||||
### Globe
|
||||
|
||||
- [ ] Requested region is centered.
|
||||
- [ ] Zoom level matches the brief.
|
||||
- [ ] Whole Earth and grid are visible.
|
||||
- [ ] Selected cell and crosshair are legible.
|
||||
- [ ] Hint and precision controls are unobstructed.
|
||||
|
||||
### Repository
|
||||
|
||||
- [ ] Only requested README and PNG assets remain in the diff.
|
||||
- [ ] Temporary debug fixture and media are removed.
|
||||
- [ ] Final clean debug build passes.
|
||||
- [ ] Screenshot validator passes.
|
||||
- [ ] PR text contains no machine or personal identifiers.
|
||||
- [ ] Static-capture limitations are disclosed.
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate high-resolution README screenshot assets without external packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def png_dimensions(path: Path) -> tuple[int, int]:
|
||||
with path.open("rb") as handle:
|
||||
header = handle.read(24)
|
||||
if len(header) < 24 or header[:8] != PNG_SIGNATURE or header[12:16] != b"IHDR":
|
||||
fail(f"{path.name} is not a valid PNG with an IHDR header")
|
||||
width, height = struct.unpack(">II", header[16:24])
|
||||
if width <= 0 or height <= 0:
|
||||
fail(f"{path.name} has invalid dimensions: {width}x{height}")
|
||||
return width, height
|
||||
|
||||
|
||||
def resolve_inside(root: Path, value: Path, label: str) -> tuple[Path, Path]:
|
||||
candidate = value if value.is_absolute() else root / value
|
||||
resolved = candidate.resolve()
|
||||
try:
|
||||
relative = resolved.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{label} must stay inside the repository") from exc
|
||||
if not resolved.is_file():
|
||||
fail(f"{label} does not exist: {relative.as_posix()}")
|
||||
return resolved, relative
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate README PNG references, dimensions, and repository-safe paths."
|
||||
)
|
||||
parser.add_argument("--repo-root", type=Path, default=Path("."))
|
||||
parser.add_argument("--readme", type=Path, default=Path("README.md"))
|
||||
parser.add_argument("--asset", action="append", type=Path, required=True)
|
||||
parser.add_argument("--min-width", type=int, default=1080)
|
||||
parser.add_argument("--min-height", type=int, default=1920)
|
||||
parser.add_argument("--require-same-size", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = args.repo_root.resolve()
|
||||
if not root.is_dir():
|
||||
fail("repository root does not exist")
|
||||
if args.min_width <= 0 or args.min_height <= 0:
|
||||
fail("minimum dimensions must be positive")
|
||||
|
||||
readme_path, readme_relative = resolve_inside(root, args.readme, "README")
|
||||
readme_text = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
assets: list[dict[str, object]] = []
|
||||
seen: set[Path] = set()
|
||||
dimensions: set[tuple[int, int]] = set()
|
||||
|
||||
for index, value in enumerate(args.asset):
|
||||
asset_path, relative = resolve_inside(root, value, f"asset[{index}]")
|
||||
if asset_path in seen:
|
||||
fail(f"duplicate asset: {relative.as_posix()}")
|
||||
seen.add(asset_path)
|
||||
|
||||
if asset_path.suffix.lower() != ".png":
|
||||
fail(f"README screenshot must be a PNG: {relative.as_posix()}")
|
||||
if relative.parts[:2] != ("docs", "screenshots"):
|
||||
fail(
|
||||
"README screenshots must live under docs/screenshots: "
|
||||
f"{relative.as_posix()}"
|
||||
)
|
||||
|
||||
reference = relative.as_posix()
|
||||
if reference not in readme_text and f"./{reference}" not in readme_text:
|
||||
fail(f"README does not reference asset: {reference}")
|
||||
|
||||
width, height = png_dimensions(asset_path)
|
||||
if width < args.min_width or height < args.min_height:
|
||||
fail(
|
||||
f"{reference} is below the high-resolution minimum: "
|
||||
f"{width}x{height} < {args.min_width}x{args.min_height}"
|
||||
)
|
||||
dimensions.add((width, height))
|
||||
assets.append({"path": reference, "width": width, "height": height})
|
||||
|
||||
if args.require_same_size and len(dimensions) != 1:
|
||||
rendered = ", ".join(f"{width}x{height}" for width, height in sorted(dimensions))
|
||||
fail(f"README screenshots do not share one size: {rendered}")
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ok",
|
||||
"readme": readme_relative.as_posix(),
|
||||
"assets": assets,
|
||||
"same_size": len(dimensions) == 1,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (OSError, UnicodeError, ValueError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user