Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
CI & Build / Python tests (push) Successful in 23s
Android / Core and FFI clippy and tests (push) Successful in 51s
CI & Build / integration (push) Successful in 1m49s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m20s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m46s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 8m31s
Android / Build the server image (push) Successful in 1s
#6225 "Comment standard sweep: root, config, CI workflows, docs and packaging", milestone 546 "Bring the repo to the comment standard (rule 32) and compose defaults (rule 198)". - README restructured in newcomer order: what it is, quick start, configuration, setup pitfalls, updating and backups, features, troubleshooting, development, licence. Its quick-start compose carries no restart policy. - Dockerfile and .env.example trimmed to one or two lines per setting. - Workflows, packaging scripts and docs lose issue, run, note, rule and milestone references and their history narration. Comments that still described path filters and "step 6" as future work now state what the workflows do. - ci-requirements.md rewritten against the current workflows. It said the build gated on typecheck and lint only, the integration lane did not gate, a v* tag published :latest, and ci-tauri lacked bsdtar and zstd; none of those is true now (the last checked against run 9630's log). - Echo strings in guard-forward.sh and version.sh drop their note citations; every string a test asserts on is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.2 KiB
Python
129 lines
5.2 KiB
Python
"""Every icon Inkwell ships, from one drawing.
|
|
|
|
The mark is a black inkpot with a quill standing in it, on the brand yellow.
|
|
It is drawn ONCE below, in a 512 box, and each variant places it
|
|
with its own scale, because each platform crops differently:
|
|
|
|
- icon.svg the rounded tile: web favicon/PWA "any", desktop app icon
|
|
- icon-maskable.svg full bleed, mark inside the central 80% (PWA "maskable")
|
|
- Android foreground mark alone on transparency inside the 66dp safe circle of a
|
|
108dp adaptive canvas; the yellow is the background layer, and
|
|
the transparent mark is also what themed (monochrome) icons need
|
|
|
|
Run from the repo root after changing the drawing; needs rsvg-convert (librsvg).
|
|
Generated output is committed, like any other generated file.
|
|
|
|
python3 packaging/icons.py
|
|
"""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
INK = "#18181b"
|
|
GOLD = "#F5C518"
|
|
|
|
# The quill is drawn upright at the pot mouth (0,0), then rotated into place.
|
|
QUILL_AT = "translate(256 232) rotate(34)"
|
|
|
|
QUILL_INK = f'''
|
|
<g transform="{QUILL_AT}">
|
|
<path d="M0 6 L0 -46" stroke="{INK}" stroke-width="12" stroke-linecap="round"/>
|
|
<path d="M0 -34 C 46 -78 58 -176 4 -252 C -34 -196 -40 -104 0 -34 Z" fill="{INK}"/>
|
|
</g>'''
|
|
|
|
POT_INK = f'''
|
|
<path fill="{INK}" d="M222 262 C 166 268 140 300 140 338 L140 374 C140 396 156 410 178 410
|
|
L334 410 C356 410 372 396 372 374 L372 338 C372 300 346 268 290 262 Z"/>
|
|
<rect x="214" y="236" width="84" height="34" rx="9" fill="{INK}"/>
|
|
<rect x="200" y="226" width="112" height="22" rx="11" fill="{INK}"/>'''
|
|
|
|
# The detail — the quill's shaft, two splits in its vane, the glint on the pot — is
|
|
# CUT OUT of the ink rather than painted over it in yellow. On the tile the two look
|
|
# the same; on Android's transparent foreground only a cut-out is right, because a
|
|
# themed icon draws the foreground's alpha as a silhouette, and painted detail
|
|
# would come out as solid, with the vane splits sticking out past its edge.
|
|
CUTS = '''
|
|
<g transform="{at}">
|
|
<path d="M1 -52 L3 -232" stroke-width="6" stroke-linecap="round"/>
|
|
<path d="M62 -128 L30 -108 M64 -176 L32 -152" stroke-width="8"/>
|
|
</g>
|
|
<path d="M170 344 C 172 318 188 300 214 292" stroke-width="10" stroke-linecap="round"/>'''.format(at=QUILL_AT)
|
|
|
|
GLYPH = f'''
|
|
<defs>
|
|
<mask id="inkwell-cuts" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512">
|
|
<rect width="512" height="512" fill="#fff"/>
|
|
<g stroke="#000" fill="none">{CUTS}
|
|
</g>
|
|
</mask>
|
|
</defs>
|
|
<g mask="url(#inkwell-cuts)">{QUILL_INK}{POT_INK}
|
|
</g>'''
|
|
# Glyph bounds, measured from a render: centre the mark, then scale per variant.
|
|
CX, CY = 262, 226
|
|
|
|
def place(scale, cy=256):
|
|
return f'translate(256 {cy}) scale({scale}) translate({-CX} {-CY})'
|
|
|
|
def svg(body, label=True):
|
|
aria = ' role="img" aria-label="Inkwell"' if label else ''
|
|
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"{aria}>\n{body}\n</svg>\n'
|
|
|
|
def tile(scale=0.92):
|
|
return svg(f''' <!-- Rounded-tile app icon: brand yellow, and the inkwell — a black inkpot with a
|
|
quill standing in it. Source for every raster icon. -->
|
|
<rect width="512" height="512" rx="112" fill="{GOLD}"/>
|
|
<g transform="{place(scale)}">{GLYPH}
|
|
</g>''')
|
|
|
|
def maskable(scale=0.8):
|
|
return svg(f''' <!-- Maskable variant: full-bleed background so platform masks (circle, squircle,
|
|
rounded-square) never clip the mark, which sits inside the central 80% safe
|
|
zone. Same mark as icon.svg. -->
|
|
<rect width="512" height="512" fill="{GOLD}"/>
|
|
<g transform="{place(scale)}">{GLYPH}
|
|
</g>''')
|
|
|
|
def foreground(scale=0.64):
|
|
# Android adaptive foreground: the mark alone on transparency, inside the 66dp
|
|
# safe circle of a 108dp canvas. The yellow lives in the background layer.
|
|
return svg(f''' <g transform="{place(scale)}">{GLYPH}
|
|
</g>''', label=False)
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
WEB = ROOT / "frontend" / "public"
|
|
DESKTOP = ROOT / "desktop" / "src-tauri"
|
|
ANDROID = ROOT / "android" / "app" / "src" / "main" / "res" / "mipmap-xxxhdpi"
|
|
|
|
|
|
def render(svg_text: str, size: int, out: Path, background: str | None = None) -> None:
|
|
cmd = ["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(out)]
|
|
if background:
|
|
cmd += ["-b", background]
|
|
subprocess.run(cmd, input=svg_text.encode(), check=True)
|
|
|
|
|
|
def main() -> None:
|
|
icon, mask, fg = tile(), maskable(), foreground()
|
|
(WEB / "icon.svg").write_text(icon)
|
|
(WEB / "icon-maskable.svg").write_text(mask)
|
|
|
|
render(icon, 32, WEB / "favicon-32.png")
|
|
render(icon, 192, WEB / "icon-192.png")
|
|
render(icon, 512, WEB / "icon-512.png")
|
|
render(mask, 512, WEB / "icon-maskable-512.png")
|
|
# iOS rounds the corners itself and shows transparency as black.
|
|
render(mask, 180, WEB / "apple-touch-icon.png")
|
|
|
|
render(icon, 1024, DESKTOP / "app-icon.png")
|
|
render(icon, 32, DESKTOP / "icons" / "32x32.png")
|
|
render(icon, 128, DESKTOP / "icons" / "128x128.png")
|
|
render(icon, 256, DESKTOP / "icons" / "128x128@2x.png")
|
|
render(icon, 512, DESKTOP / "icons" / "icon.png")
|
|
|
|
render(fg, 512, ANDROID / "ic_launcher_foreground.png")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|