Merge branch 'main' of https://github.com/kuhyx/utils
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sickdays pre-commit / pre-commit (push) Has been cancelled
sickdays tests / test (push) Has been cancelled

This commit is contained in:
Krzysztof Rudnicki 2026-11-02 12:04:04 +01:00
commit 35569a6335
5 changed files with 70 additions and 15 deletions

View File

@ -122,7 +122,10 @@ def desktop_problems(repo: Path) -> list[str]:
if not names: if not names:
out.append(f"{rel}: no Name= in [Desktop Entry]") out.append(f"{rel}: no Name= in [Desktop Entry]")
for name in names: for name in names:
out += [f"{rel}: Name={p}" for p in launcher_label.label_problems(name)] out += [
f"{rel}: Name={p}"
for p in launcher_label.label_problems(name, words=True)
]
return out return out

View File

@ -7,7 +7,8 @@ the launcher font is capped too (`label_widths.py`): FocusOwner, 10
characters, was still cut. The same rules live characters, was still cut. The same rules live
in `~/src/testsAndMisc/python_pkg/app_icons/labels.py` (`label_problems`); in `~/src/testsAndMisc/python_pkg/app_icons/labels.py` (`label_problems`);
the messages are mirrored here, not imported, because utils must not depend the messages are mirrored here, not imported, because utils must not depend
on another repo. on another repo. A desktop entry's `Name=` (rule 2) may hold single spaces
between words (`words=True`); the phone launcher may not.
The name is `<application android:label>` of the main manifest, plus the The name is `<application android:label>` of the main manifest, plus the
`android:label` of any activity (or alias) whose intent-filter holds MAIN + `android:label` of any activity (or alias) whose intent-filter holds MAIN +
@ -38,27 +39,39 @@ from repo_contract.label_widths import MAX_WIDTH, label_width
MAX_LENGTH = 10 MAX_LENGTH = 10
STRINGS = Path("android/app/src/main/res/values/strings.xml") STRINGS = Path("android/app/src/main/res/values/strings.xml")
_ASCII_LETTERS = re.compile(r"[A-Za-z]+") _ASCII_LETTERS = re.compile(r"[A-Za-z]+")
_ASCII_WORDS = re.compile(r"[A-Za-z]+( [A-Za-z]+)*")
_STRING_REF = re.compile(r"@string/(\w+)") _STRING_REF = re.compile(r"@string/(\w+)")
_PLACEHOLDER = re.compile(r"\$\{(\w+)\}") _PLACEHOLDER = re.compile(r"\$\{(\w+)\}")
_UNRESOLVED = "cannot resolve the launcher name" _UNRESOLVED = "cannot resolve the launcher name"
def label_problems(name: str) -> list[str]: def label_problems(name: str, *, words: bool = False) -> list[str]:
"""Every launcher-name rule `name` breaks (app_icons labels.py wording).""" """Every launcher-name rule `name` breaks (app_icons labels.py wording).
`words=True` is for a desktop entry's `Name=` (rule 2), shown in a menu or
dmenu where "Sick days" reads better than "Sickdays": one space between
words is fine. The phone launcher (rule 1) never passes it.
"""
if not name: if not name:
return ["label is empty"] return ["label is empty"]
problems: list[str] = [] problems: list[str] = []
letters_only = _ASCII_LETTERS.fullmatch(name) is not None if words:
if not letters_only: valid = _ASCII_WORDS.fullmatch(name) is not None
problems.append( message = (
f"{name!r} must contain only ASCII letters (no spaces, _ or symbols)" f"{name!r} must be ASCII letters, one space between words (no _ or symbols)"
) )
else:
valid = _ASCII_LETTERS.fullmatch(name) is not None
message = f"{name!r} must contain only ASCII letters (no spaces, _ or symbols)"
if not valid:
problems.append(message)
if not name[0].isupper(): if not name[0].isupper():
problems.append(f"{name!r} must start with a capital letter") problems.append(f"{name!r} must start with a capital letter")
# The width table only knows ASCII letters; other names already failed. # The width table only knows ASCII letters; other names already failed.
if letters_only and label_width(name) > MAX_WIDTH: letters = name.replace(" ", "")
if valid and label_width(letters) > MAX_WIDTH:
problems.append( problems.append(
f"{name!r} is {label_width(name)} wide in the launcher font; " f"{name!r} is {label_width(letters)} wide in the launcher font; "
f"at most {MAX_WIDTH} fit" f"at most {MAX_WIDTH} fit"
) )
if len(name) > MAX_LENGTH: if len(name) > MAX_LENGTH:

View File

@ -98,6 +98,7 @@ def test_no_flutter_app_no_rules(repo: Path) -> None:
def test_desktop_names_checked(repo: Path) -> None: def test_desktop_names_checked(repo: Path) -> None:
flutter_app(repo) flutter_app(repo)
write(repo, "linux/good.desktop", DESKTOP.format(name="Todo")) write(repo, "linux/good.desktop", DESKTOP.format(name="Todo"))
write(repo, "linux/spaced.desktop", DESKTOP.format(name="Sick days"))
write(repo, "linux/bad.desktop", DESKTOP.format(name="todo app")) write(repo, "linux/bad.desktop", DESKTOP.format(name="todo app"))
write( write(
repo, repo,
@ -108,10 +109,6 @@ def test_desktop_names_checked(repo: Path) -> None:
track(repo) track(repo)
write(repo, "linux/untracked.desktop", DESKTOP.format(name="bad name")) write(repo, "linux/untracked.desktop", DESKTOP.format(name="bad name"))
assert launcher.problems(repo) == [ assert launcher.problems(repo) == [
(
"launcher: linux/bad.desktop: Name='todo app' must contain only ASCII"
" letters (no spaces, _ or symbols)"
),
"launcher: linux/bad.desktop: Name='todo app' must start with a capital letter", "launcher: linux/bad.desktop: Name='todo app' must start with a capital letter",
"launcher: linux/none.desktop: no Name= in [Desktop Entry]", "launcher: linux/none.desktop: no Name= in [Desktop Entry]",
] ]

View File

@ -177,3 +177,45 @@ def test_manifest_without_application(tmp_path: Path) -> None:
assert ll.problems(app) == [ assert ll.problems(app) == [
f"{MAIN}: <application> has no android:label; cannot resolve the launcher name" f"{MAIN}: <application> has no android:label; cannot resolve the launcher name"
] ]
WORDS_RULE = "must be ASCII letters, one space between words (no _ or symbols)"
@pytest.mark.parametrize(
("name", "expected"),
[
("Sick days", []),
("Todo", []),
("sick days", ["'sick days' must start with a capital letter"]),
(
"Sick days",
[f"'Sick days' {WORDS_RULE}"],
),
(
"Sick days ",
[f"'Sick days ' {WORDS_RULE}"],
),
(
"Sick_days",
[f"'Sick_days' {WORDS_RULE}"],
),
(
"Focus owner x",
[
"'Focus owner x' is 607 wide in the launcher font; at most 522 fit",
"'Focus owner x' has 13 characters; at most 10 fit",
],
),
],
)
def test_desktop_names_may_have_spaces_between_words(
name: str, expected: list[str]
) -> None:
assert ll.label_problems(name, words=True) == expected
def test_the_phone_label_still_refuses_a_space() -> None:
assert ll.label_problems("Sick days") == [
"'Sick days' must contain only ASCII letters (no spaces, _ or symbols)"
]

View File

@ -1,6 +1,6 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Sickdays Name=Sick days
Comment=Mark sick days and choose which lockers still apply Comment=Mark sick days and choose which lockers still apply
Exec=sickdays gui Exec=sickdays gui
Icon=face-sick Icon=face-sick