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
This commit is contained in:
Krzysztof Rudnicki 2026-08-09 17:23:35 +02:00
parent 8f2c46de7c
commit bd725b21b7
4 changed files with 83 additions and 0 deletions

View File

@ -0,0 +1,44 @@
{
"intent": "Give the new ~/untools Flutter app a launcher icon from the shared family (charcoal field + accent glyph), so it does not ship the stock Flutter beach icon, and whitelist its package in phone_focus_mode's config.",
"scope": [
"python_pkg/app_icons/glyphs.py — new decision-tree glyph",
"python_pkg/app_icons/apps.py — register the untools app",
"phone_focus_mode/config.sh — add com.kuhy.untools to the productive-apps whitelist",
"Non-goal: no change to render/style/generate; the shared palette and safe-box rules are untouched"
],
"changes": [
"Added _DECISION_TREE: one node branching into two, drawn as hollow rings (r=78) so the 72px stroke leaves visible negative space at icon size. A two-level, seven-node version was tried first and rejected — the generator's safe-box check flagged it as overflowing by ~170x162px, and at 48dp the doubled connectors closed into a solid mass. Branch spread was then narrowed from 324/700 to 346/678 to clear a residual 44px horizontal overflow.",
"Registered the untools AppIcon (accent #B8862E, glyph decision-tree, linux=False because the desktop build is the web build in a Chrome --app window, so there is no GTK app to hand a hicolor PNG to).",
"Added com.kuhy.untools to config.sh's user-requested productive apps. Note this is source-of-truth only: the current Pixel 6a (23181JEGR08034) is not rooted, has no /data/local/tmp/focus_mode directory and no running daemon, so nothing was pushed to the device and no app is at risk of being killed there. Kept for the rooted BL9000 phone / any future redeploy."
],
"verification": [
{
"command": "PYTHONPATH=~/testsAndMisc python3 -m pytest python_pkg/app_icons/tests -q --cov=python_pkg.app_icons",
"result": "pass",
"evidence": "44 passed; app_icons at 100% (apps.py 24/24, glyphs.py 28/28, cli.py 60/60, generate.py 41/41). The repo-wide 5.35% total is the monorepo bar applied to a single-subpackage run, not a regression from this change."
},
{
"command": "PYTHONPATH=~/testsAndMisc python3 -m python_pkg.app_icons generate --app untools --android",
"result": "pass",
"evidence": "wrote 6 asset files to /home/kuhy/untools/assets/icon; regenerated Android mipmaps; no safe-box warning on the final geometry."
},
{
"command": "visual inspection of assets/icon/icon.png and icon_foreground.png",
"result": "pass",
"evidence": "Symmetric tree, three hollow rings holding their negative space, charcoal #211D1B field with #B8862E glyph — consistent with the rest of the family."
},
{
"command": "adb -s 23181JEGR08034 install -r app-release.apk && am start",
"result": "pass",
"evidence": "Installed and launched; app alive after 4s (pid present), confirming no focus-mode interference on this device."
}
],
"risks": [
"The glyph reads as a generic org-chart/hierarchy at very small sizes; it is distinguished from the family's other glyphs by shape, and no other app uses a node-and-edge motif.",
"config.sh now lists a package that is not enforced on the current device. Harmless, but a reader could infer the whitelist is live here when it is not — the comment in this evidence file and the commit message state otherwise."
],
"rollback": [
"Revert the three files; delete ~/untools/assets/icon and the android mipmaps it regenerated.",
"After rollback, re-run the app_icons test suite (44 tests) and confirm the registry test still passes with the untools entry removed."
]
}

View File

@ -362,6 +362,7 @@ com.kuhy.diet_guard_app
com.kuhy.habit_stack
com.kuhy.home_inventory
com.kuhy.epopeja_karta
com.kuhy.untools
com.shazam.android
com.ichi2.anki
com.metrolist.music

View File

@ -89,6 +89,16 @@ APPS: Final[dict[str, AppIcon]] = {
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",

View File

@ -78,6 +78,29 @@ _CHECKLIST = """\
<path d="M 272 620 L 346 694 L 478 562"/>
<path d="M 594 648 L 752 648"/>"""
# A branching decision tree: one node splitting into two. Reads as "decompose
# a problem into parts", the move every one of the 25 thinking tools makes in
# some form -- an issue tree most literally, but also a fishbone's ribs and a
# 2x2's quadrants.
#
# Nodes are hollow circles rather than filled dots: at 48dp a filled dot and
# the 72px stroke merge into a blob, while the ring keeps visible negative
# space (r=78 leaves 156-72=84px of interior).
#
# Two levels of branching were tried first (7 nodes) and rejected: the
# generator's safe-box check flagged it as overflowing by ~170px, and at icon
# size the doubled connectors closed up into a solid mass. One clean split
# survives the launcher mask and still reads as a tree rather than a Y-fork,
# because the square shoulders and vertical drops keep the hierarchy legible.
_DECISION_TREE = """\
<circle cx="512" cy="300" r="78"/>
<path d="M 512 378 L 512 452"/>
<path d="M 346 452 L 678 452"/>
<path d="M 346 452 L 346 528"/>
<path d="M 678 452 L 678 528"/>
<circle cx="346" cy="606" r="78"/>
<circle cx="678" cy="606" r="78"/>"""
# 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".
@ -238,6 +261,11 @@ GLYPHS: Final[dict[str, Glyph]] = {
Glyph("quill-nib", "Dip-pen nib with vent hole and slit", _QUILL_NIB),
Glyph("anvil", "Blacksmith's anvil with horn and flared foot", _ANVIL),
Glyph("track-bars", "Rising log bars with a plotted trend point", _TRACK_BARS),
Glyph(
"decision-tree",
"Node branching into two levels of child nodes",
_DECISION_TREE,
),
)
}