testsAndMisc/python_pkg/app_icons/glyphs.py
Krzysztof Rudnicki 45f4311993
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
app_icons: add home_inventory storage-box glyph; whitelist it on the phone
The new ~/home_inventory Flutter app needs two things from this repo: a
family launcher icon, and a focus-mode whitelist entry so the daemon does
not pm disable-user it ~1s after launch.

The glyph is an isometric carton — a hexagon plus the three edges meeting
at the near-top corner. A front-on rectangle with a lid seam was tried
first and rejected on the contact sheet: a seam plus a divider reads as a
browser window, not a box.

Body is 488x440, not a rounder 500x440, because STROKE_WIDTH straddles the
path and adds 72px to each rendered dimension; 488 is the widest body whose
ink still fits SAFE_BOX. The generator warned about exactly this at 500.

Accent stays the one shared family accent — apps differ by glyph, not
colour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:35:55 +02:00

148 lines
5.7 KiB
Python

"""Per-app glyph artwork for the shared icon family.
A glyph is a raw SVG fragment drawn in the 1024x1024 master canvas. It does not
need to be perfectly positioned: :mod:`python_pkg.app_icons.render` measures the
rendered ink and recentres the fragment, so every icon in the family sits at the
same optical height.
Fragments inherit ``fill="none"``, ``stroke``, ``stroke-width`` and round caps
from the wrapping group. A fragment that needs a filled silhouette instead
overrides those locally and uses :data:`~python_pkg.app_icons.style.ACCENT_MARKER`
for its colour.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Final
@dataclass(frozen=True)
class Glyph:
"""One app's artwork.
Parameters:
name (str): Stable identifier, used as the ``--glyph`` CLI value.
description (str): What the glyph depicts, for ``--list`` output.
body (str): SVG fragment drawn in the 1024x1024 master canvas.
"""
name: str
description: str
body: str
# A cloud silhouette outline with a download arrow underneath: a client that
# pulls files off a remote dufs server.
_CLOUD_DOWN = """\
<g transform="translate(296,196) scale(18)" stroke-width="4">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 \
2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 \
0-2.64-2.05-4.78-4.65-4.96z"/>
</g>
<path d="M 512 628 L 512 744"/>
<path d="M 444 676 L 512 744 L 580 676"/>"""
# Bar, two plates and two collars. Plate spacing is well above the minimum
# negative space so the bar does not fill in at launcher size.
_BARBELL = """\
<path d="M 268 512 L 756 512"/>
<path d="M 364 356 L 364 668"/>
<path d="M 660 356 L 660 668"/>
<path d="M 268 432 L 268 592"/>
<path d="M 756 432 L 756 592"/>"""
# Plain clock face. Deliberately no bell "ears": at launcher size they read as
# noise rather than as an alarm clock.
_CLOCK = """\
<circle cx="512" cy="512" r="244"/>
<path d="M 512 512 L 512 350"/>
<path d="M 512 512 L 636 512"/>"""
# Shield outline guarding filled cutlery. The fork is paired with a knife on
# purpose: a lone three-tine fork reads as a trident at small sizes.
_SHIELD_CUTLERY = """\
<path d="M 512 275 L 752 366 L 752 544 C 752 655 646 723 512 753 \
C 378 723 272 655 272 544 L 272 366 Z"/>
<g transform="translate(356,362) scale(13)" fill="{{ACCENT}}" stroke="none">
<path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03\
C11.34 12.84 13 11.12 13 9V2h-2v7z"/>
<path d="M16 12h2.5v10H21V2c-2.76 0-5 2.24-5 5v5z"/>
</g>"""
# Two ticked-off list rows. More specific to a notes/todo app than a lone
# checkmark, and still legible at 48dp.
_CHECKLIST = """\
<path d="M 272 372 L 346 446 L 478 314"/>
<path d="M 594 400 L 752 400"/>
<path d="M 272 620 L 346 694 L 478 562"/>
<path d="M 594 648 L 752 648"/>"""
# Two interlocking stadium (pill-ring) outlines, both rotated -45 deg and
# offset along the anti-diagonal so their strokes overlap in the middle --
# reads as both "habit-stacking" (linked items) and "don't break the chain".
# Ring height is tall enough that the 72px stroke leaves a genuinely hollow
# interior (height - 2*stroke-width well above MIN_NEGATIVE_SPACE); a
# shorter ring's stroke fills the interior solid and reads as a blob.
_CHAIN_LINK = """\
<g transform="translate(467,557) rotate(-45) translate(-150,-110)">
<rect x="0" y="0" width="300" height="220" rx="110" ry="110"/>
</g>
<g transform="translate(557,467) rotate(-45) translate(-150,-110)">
<rect x="0" y="0" width="300" height="220" rx="110" ry="110"/>
</g>"""
# An isometric storage carton: hexagonal silhouette plus the three edges that
# meet at the near-top corner (one down the front, two up the top face).
#
# A front-on rectangle with a lid seam was tried first and rejected — a seam
# plus a divider reads as a browser window or a table header, not a box. The
# isometric form has no such collision anywhere else in the family, and it is
# the shape people already recognise as "a package of stuff".
#
# Geometry: a regular hexagon, centre (512,512), circumradius 240, pointy top
# and bottom. Opposite faces sit 208px apart, leaving 136px of hollow once the
# 72px stroke is drawn — comfortably past MIN_NEGATIVE_SPACE, so no face fills
# in at 48dp. Rendered ink is 488x552, inside SAFE_BOX on both axes.
_STORAGE_BOX = """\
<path d="M 512 272 L 720 392 L 720 632 L 512 752 L 304 632 L 304 392 Z"/>
<path d="M 512 512 L 512 752"/>
<path d="M 512 512 L 304 392"/>
<path d="M 512 512 L 720 392"/>"""
GLYPHS: Final[dict[str, Glyph]] = {
glyph.name: glyph
for glyph in (
Glyph("cloud-down", "Cloud with a download arrow", _CLOUD_DOWN),
Glyph("barbell", "Barbell with plates and collars", _BARBELL),
Glyph("clock", "Clock face with hands", _CLOCK),
Glyph("shield-cutlery", "Shield guarding a fork and knife", _SHIELD_CUTLERY),
Glyph("checklist", "Two ticked-off list rows", _CHECKLIST),
Glyph("chain-link", "Two interlocking chain links", _CHAIN_LINK),
Glyph("storage-box", "Isometric storage carton", _STORAGE_BOX),
)
}
def get_glyph(name: str) -> Glyph:
"""Look up a glyph by name.
Parameters:
name (str): Glyph identifier, e.g. ``"barbell"``.
Returns:
Glyph: The matching glyph.
Raises:
KeyError: If no glyph with that name exists. The message lists the
available names, since this is usually a CLI typo.
"""
try:
return GLYPHS[name]
except KeyError:
available = ", ".join(sorted(GLYPHS))
msg = f"unknown glyph {name!r}; available: {available}"
raise KeyError(msg) from None