testsAndMisc/python_pkg/app_icons/apps.py
kuhyx bd725b21b7 app_icons: add a decision-tree glyph and register untools
The new ~/untools app (guided thinking tools) needs a family launcher icon
rather than the stock Flutter beach one.

The glyph is one node branching into two -- "decompose a problem into parts",
which is the move every one of the app's 25 frameworks makes in some form.
Nodes are hollow rings rather than filled dots so the 72px stroke leaves
visible interior at icon size.

A two-level, seven-node tree was drawn first and rejected on the generator's
own evidence: it reported the glyph overflowing the 560px safe box by
~170x162px, and rendering confirmed the doubled connectors closed up into a
solid mass at launcher size. One clean split survives the mask and still reads
as a hierarchy. Branch spread was then narrowed to clear a residual 44px.

linux=False for this app: its desktop build is the web build in a Chrome
--app window, so there is no GTK app to hand a hicolor PNG to.

Also whitelists com.kuhy.untools in phone_focus_mode/config.sh. That entry is
source-of-truth only -- the current Pixel 6a is not rooted, has no
/data/local/tmp/focus_mode and no running daemon, so nothing was pushed to the
device and no app is being killed there. Kept for the rooted BL9000 phone.

Verified: 44 app_icons tests pass with the package at 100% coverage; the
generator emits 6 assets with no safe-box warning; the rendered icon was
inspected and matches the family; and the resulting APK installed and stayed
alive on 23181JEGR08034.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hVZ5HfcrcrVzinmjQuQup
2026-08-09 17:23:35 +02:00

188 lines
6.0 KiB
Python

"""Registry of the Flutter apps that use the shared icon family.
Adding a new app here (plus a glyph in :mod:`python_pkg.app_icons.glyphs`) is
all that is needed for it to get an on-style icon. The accent is the one
shared family accent (`#B8862E`, from the `unified-design-system` skill) —
apps are distinguished by glyph shape, not color; do not give a new app its
own accent.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Final
@dataclass(frozen=True)
class AppIcon:
"""One app's icon configuration.
Parameters:
key (str): Short identifier used on the command line.
repo (Path): Flutter project root, i.e. the directory holding pubspec.yaml.
accent (str): Hex accent colour. Shared across every app in the family
(the `unified-design-system` accent) — not app-specific.
glyph (str): Glyph name from :data:`~python_pkg.app_icons.glyphs.GLYPHS`.
icon_name (str): Basename for Linux hicolor/desktop installation.
linux (bool): Whether the app has a Linux desktop target.
"""
key: str
repo: Path
accent: str
glyph: str
icon_name: str
linux: bool
@property
def asset_dir(self) -> Path:
"""Return the directory holding this app's icon source assets."""
return self.repo / "assets" / "icon"
_HOME = Path.home()
APPS: Final[dict[str, AppIcon]] = {
app.key: app
for app in (
AppIcon(
key="dufs_client",
# The live app: ~/dufs_client is a stale, remote-less checkout that
# was superseded when dufs-cloud absorbed it (see testsAndMisc
# CLAUDE.md). Same package name, so it is easy to icon the wrong one.
repo=_HOME / "dufs-cloud" / "app",
accent="#B8862E",
glyph="cloud-down",
icon_name="dufs-client",
linux=False,
),
AppIcon(
key="workout_app",
repo=_HOME / "screen-locker" / "stronglift_replacement" / "workout_app",
accent="#B8862E",
glyph="barbell",
icon_name="workout-app",
linux=False,
),
AppIcon(
key="wake_alarm_sync",
repo=_HOME / "wake-alarm" / "phone_app",
accent="#B8862E",
glyph="clock",
icon_name="wake-alarm-sync",
linux=False,
),
AppIcon(
key="diet_guard_app",
repo=_HOME / "diet-guard" / "app",
accent="#B8862E",
glyph="shield-cutlery",
icon_name="diet-guard-app",
linux=True,
),
AppIcon(
key="todo",
repo=_HOME / "todo",
accent="#B8862E",
glyph="checklist",
icon_name="todo",
linux=True,
),
AppIcon(
key="untools",
repo=_HOME / "untools",
accent="#B8862E",
glyph="decision-tree",
icon_name="untools",
# Desktop is the web build in a Chrome --app window, so there is
# no GTK app to hand a hicolor PNG to.
linux=False,
),
AppIcon(
key="habit_stack",
repo=_HOME / "habit_stack",
accent="#B8862E",
glyph="chain-link",
icon_name="habit-stack",
linux=False,
),
AppIcon(
key="home_inventory",
repo=_HOME / "home_inventory",
accent="#B8862E",
glyph="storage-box",
icon_name="home-inventory",
linux=True,
),
AppIcon(
key="billsplit",
# The only app in this registry that lives inside the testsAndMisc
# monorepo rather than in its own repo, so its path carries a
# directory component the others do not.
repo=_HOME / "testsAndMisc" / "billsplit",
accent="#B8862E",
glyph="receipt-split",
icon_name="billsplit",
# `flutter create` scaffolded a linux/ target, but the app is
# phone-first and ships no desktop entry, so the hicolor icon set
# is not generated for it.
linux=False,
),
AppIcon(
key="epopeja_karta",
repo=_HOME / "epopeja_karta",
accent="#B8862E",
glyph="quill-nib",
icon_name="epopeja-karta",
# A linux/ target exists, but only so the app can be driven on the
# desktop during development. It is a character sheet you bring to
# the table, so it ships no desktop entry and gets no hicolor set.
linux=False,
),
AppIcon(
key="octoforge",
# The Flutter app is a subdirectory of the repo: the repo root
# holds the pure-Dart core and the tooling alongside it.
repo=_HOME / "octoforge" / "octoforge" / "app",
accent="#B8862E",
glyph="anvil",
icon_name="octoforge",
# Unlike the other phone-first apps, this one is genuinely used on
# the desktop too, so it gets the hicolor set and a desktop entry.
linux=True,
),
AppIcon(
key="kuhylog",
repo=_HOME / "kuhylog" / "kuhylog",
accent="#B8862E",
glyph="track-bars",
icon_name="kuhylog",
# Android is the real target (home-screen widget, quick-settings
# tile); the Linux build exists for trying the UI without a phone,
# so it still wants the hicolor set.
linux=True,
),
)
}
def get_app(key: str) -> AppIcon:
"""Look up an app by key.
Parameters:
key (str): App identifier, e.g. ``"todo"``.
Returns:
AppIcon: The matching configuration.
Raises:
KeyError: If no app with that key is registered.
"""
try:
return APPS[key]
except KeyError:
available = ", ".join(sorted(APPS))
msg = f"unknown app {key!r}; available: {available}"
raise KeyError(msg) from None